Files
Aska 6dbb4a2e18
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat: 唤醒式架构批次——capacity/L4/会话编排/管理面板写CRUD/前端集成(77-81)
【capacity 唤醒式核心(77)】internal/runtime/capacity:(Decider L1启发式+L2上下文+人格覆盖→三态静息/唤醒/全能→允许工具集+capacity_awaken事件)。loop 外围允许集过滤(不硬阻断=能力≠边界红线)。config 加 Capacity(Enabled/MinConfidence/ToolSets)。wire buildCapacity 接入 loop。

【L4 记忆驱动意图(80)】recall.go:从记忆召回用户习惯(搞一下xx=办事)→唤醒L1/L2漏掉/升置信;群聊陌生人不读owner习惯(隔离红线);RecordIntent闭环(判定写回记忆越用越准);默认保守静息。

【会话消息编排(78)】gateway/session_orchestrator.go:每会话(模式/pending有界5/tail上下文尾巴20);Submit非阻塞(idle立即/busy入队ack/满拒绝);完成回调按序续接(tail+新消息合并增量注入非全量重放);Interrupt中断;REST 202/409+WS session_busy/pending_ack事件;不重造任务引擎复用loop。

【前端集成(79)】ws.ts 加 capacity_awaken/session_busy/pending_ack 解析;store 加 awakenState/sessionBusy;TopBar/ChatShell 加唤醒态徽标+忙态提示(不阻塞输入框);状态组件 status.tsx;Miuix风格。

【管理面板写CRUD(81)】20个写端点(POST/PUT/DELETE /admin/*,owner+审计落库):personas(真持久化YAML)/users apikey(签发掩码+撤销)/iot控制(真下发)/automations(建/触发/删)/llm(test真调用+临时)/channels(connect/close/身份测试actor白名单)/plugins(卸载)/memories(search真检索)/sessions(export)。无存储如实501不伪造。拆4文件满足≤500红线。前端9模块接真写。

【wire收口】buildCapacity接入loop+OnCapacity事件;server.go newOrchestrator接hub。

实机验证:iot ac.living control 200 真实改变 on:true;observability/metrics 200、health ok;全仓 build/vet/test 47包绿+前端tsc/vite。
2026-09-06 21:54:40 +08:00

178 lines
6.7 KiB
Go

package capacity
import (
"context"
"testing"
)
// uRecall builds an Input over messages with the given recall (L4 feed), mirroring
// the in() helper but adding the recall hook.
func inRecall(messages []string, recall []RecallItem) Input {
in := in(messages...)
in.Recall = recall
return in
}
// taskRecord is a canonical intent record written back by RecordIntent, carrying a
// learned task expression for the closed loop.
func taskRecord(expr string, conf float64) string {
return formatIntentRecord(Judgment{IsTask: true, TaskKind: taskKindOps, Confidence: conf, Text: expr})
}
func TestDecide_L4_WakesMissedTask_CanonicalRecord(t *testing.T) {
d := New(activeConfig())
// "部署" is a strong verb but "搞一下那个部署" carries no imperative marker or
// object noun, so L1 is undecided and L2 (single turn) is undecided too. A learned
// habit (canonical record) that "搞一下这个部署" is a task must wake it (L4 fills
// the gap 设计 §2.2).
recall := []RecallItem{{
Content: taskRecord("搞一下这个部署", 0.85),
Scope: "user",
Score: 0.8,
}}
dec := d.Decide(context.Background(), inRecall([]string{"搞一下那个部署"}, recall))
if dec.State != StateAwake {
t.Fatalf("state = %s, want awake (L4 habit learned 搞一下=办事)", dec.State)
}
if !contains(dec.AllowedTools, "terminal") {
t.Fatalf("awake allowed should include terminal (ops habit), got %v", dec.AllowedTools)
}
if dec.Confidence < 0.6 {
t.Fatalf("confidence = %v, want >= 0.6", dec.Confidence)
}
if !dec.Judgment.IsTask {
t.Fatalf("judgment.IsTask = false, want true")
}
}
func TestDecide_L4_WakesMissedTask_HabitMemory(t *testing.T) {
d := New(activeConfig())
// A habit-style memory (not written by RecordIntent) that leans 办事 and shares the
// phrase must also wake, without L1/L2 support.
recall := []RecallItem{{
Content: "用户习惯:他习惯说搞一下就是要我办事",
Scope: "user",
Score: 0.5,
}}
dec := d.Decide(context.Background(), inRecall([]string{"搞一下那个脚本"}, recall))
if dec.State != StateAwake {
t.Fatalf("state = %s, want awake (habit memory leans 办事)", dec.State)
}
}
func TestDecide_L4_BoostsConfidenceOnAwake(t *testing.T) {
d := New(activeConfig())
// L2 already wakes this follow-up at 0.66; a strong learned habit corroborating
// the same task phrasing must raise the confidence (设计: 越用越准).
without := d.Decide(context.Background(), in("帮我查一下资料", "还有吗"))
if without.State != StateAwake || without.Confidence < 0.6 {
t.Fatalf("baseline = state %s conf %v, want awake >= 0.6", without.State, without.Confidence)
}
recall := []RecallItem{{
Content: taskRecord("还有吗", 0.9),
Scope: "user",
}}
with := d.Decide(context.Background(), inRecall([]string{"帮我查一下资料", "还有吗"}, recall))
if with.State != StateAwake {
t.Fatalf("with-recall state = %s, want awake", with.State)
}
if with.Confidence <= without.Confidence {
t.Fatalf("with-recall confidence = %v, want > baseline %v", with.Confidence, without.Confidence)
}
}
func TestDecide_L4_MissingRecall_FallsBackToL1L2(t *testing.T) {
d := New(activeConfig())
// No recall (nil or empty) keeps L1/L2 behavior identical.
base := d.Decide(context.Background(), in("帮我查一下资料"))
nilRecall := d.Decide(context.Background(), inRecall([]string{"帮我查一下资料"}, nil))
emptyRecall := d.Decide(context.Background(), inRecall([]string{"帮我查一下资料"}, []RecallItem{}))
for _, got := range []Decision{nilRecall, emptyRecall} {
if got.State != base.State || got.Confidence != base.Confidence {
t.Fatalf("no-recall = state %s conf %v, want baseline %s %v",
got.State, got.Confidence, base.State, base.Confidence)
}
}
}
func TestDecide_L4_IgnoresNonAllowedScope(t *testing.T) {
d := New(activeConfig())
// A recalled item outside the allowed scopes (stranger / secret) must not feed L4
// even when it leans task and shares the phrase — 只读用户授权范围记忆.
for _, scope := range []string{"stranger", "secret", "group"} {
recall := []RecallItem{{
Content: "用户习惯:他说搞一下就是要我办事",
Scope: scope,
Score: 0.9,
}}
dec := d.Decide(context.Background(), inRecall([]string{"搞一下那个部署"}, recall))
if dec.State != StateQuiet {
t.Fatalf("scope %q leaked into L4: state = %s, want quiet", scope, dec.State)
}
}
}
func TestDecide_L4_NonTaskHabit_StaysConservative(t *testing.T) {
d := New(activeConfig())
// A recalled preference that is not a task habit (coffee) must not wake a neutral
// turn — 默认保守 still holds.
recall := []RecallItem{{
Content: "用户习惯:他喜欢喝冰美式",
Scope: "user",
Score: 0.9,
}}
dec := d.Decide(context.Background(), inRecall([]string{"今天喝冰美式了吗"}, recall))
if dec.State != StateQuiet {
t.Fatalf("non-task habit woke to %s, want quiet", dec.State)
}
}
func TestDecide_L4_UndecidedNoHabit_ConservativeQuiet(t *testing.T) {
d := New(activeConfig())
dec := d.Decide(context.Background(), inRecall([]string{"嗯", "这个", "然后"}, nil))
if dec.State != StateQuiet {
t.Fatalf("undecided state = %s, want quiet (conservative)", dec.State)
}
}
func TestRecordIntent_TaskFormatsParses(t *testing.T) {
d := New(activeConfig())
j := Judgment{IsTask: true, TaskKind: taskKindQuery, Confidence: 0.9, Text: "帮我查一下今天的天气;再跑个测试"}
rec := d.RecordIntent(j)
if rec == "" {
t.Fatal("RecordIntent of a task judgment returned empty")
}
got, ok := parseIntentRecord(rec)
if !ok {
t.Fatalf("parseIntentRecord failed on %q", rec)
}
if !got.isTask || got.taskKind != taskKindQuery || got.text != j.Text {
t.Fatalf("round-trip mismatch: %+v, want isTask=true kind=query text=%q", got, j.Text)
}
if got.confidence != j.Confidence {
t.Fatalf("confidence = %v, want %v", got.confidence, j.Confidence)
}
}
func TestRecordIntent_NonTaskReturnsEmpty(t *testing.T) {
d := New(activeConfig())
if rec := d.RecordIntent(Judgment{IsTask: false, Text: "你好"}); rec != "" {
t.Fatalf("non-task judgment persisted %q, want empty (nothing worth learning)", rec)
}
}
func TestDecide_JudgmentPopulated(t *testing.T) {
d := New(activeConfig())
task := d.Decide(context.Background(), in("帮我查一下资料"))
if !task.Judgment.IsTask || task.Judgment.TaskKind != taskKindQuery {
t.Fatalf("task judgment = %+v, want IsTask=true kind=query", task.Judgment)
}
if task.Judgment.Text != "帮我查一下资料" {
t.Fatalf("judgment text = %q, want the current turn", task.Judgment.Text)
}
quiet := d.Decide(context.Background(), in("你好"))
if quiet.Judgment.IsTask || quiet.Judgment.TaskKind != "" {
t.Fatalf("quiet judgment = %+v, want IsTask=false kind empty", quiet.Judgment)
}
}