feat(memory): P1 长期记忆升级 —— 异步攒批 Consolidate + 软删 + importance/last_seen #1
+2
-1
@@ -188,7 +188,8 @@ RBAC 未做,暂以单管理员账号代理;概览口径必须是**系统级*
|
|||||||
- [ ] 熔断器接回 failover(harness/circuitbreaker.go 与 llm/failover.go 未接合,可能长卡备用)| M
|
- [ ] 熔断器接回 failover(harness/circuitbreaker.go 与 llm/failover.go 未接合,可能长卡备用)| M
|
||||||
- [x] coordinator 专家超时 ✅ —— specialistTool 加 timeout(默认 specialistTimeout=3min),InvokableRun 包 WithTimeout;超时作为"观察"跳过该专家(err=nil,lead 据其余综合,不中断协调)。2 单测(超时~50ms 跳过 / 正常不受影响)。
|
- [x] coordinator 专家超时 ✅ —— specialistTool 加 timeout(默认 specialistTimeout=3min),InvokableRun 包 WithTimeout;超时作为"观察"跳过该专家(err=nil,lead 据其余综合,不中断协调)。2 单测(超时~50ms 跳过 / 正常不受影响)。
|
||||||
- [x] Branch else 兜底 ✅ —— branchNode 识别 default/else 边,主选择(true/false)未命中时走它;trace 明确区分「走 default / 未匹配收口结束(END) / 正常选路」,杜绝静默落 END 被误当 bug。3 断言单测(default 命中/条件真走 true/无 default 仍空)。compose 层原有 len==0→END 收口保留。
|
- [x] Branch else 兜底 ✅ —— branchNode 识别 default/else 边,主选择(true/false)未命中时走它;trace 明确区分「走 default / 未匹配收口结束(END) / 正常选路」,杜绝静默落 END 被误当 bug。3 断言单测(default 命中/条件真走 true/无 default 仍空)。compose 层原有 len==0→END 收口保留。
|
||||||
- [ ] DSL 拓扑/节点-工具映射校验(gateway dsl/parser.go:23 TODO,现仅 JSON 格式校验)| M
|
- [x] DSL 拓扑校验 ✅ —— gateway ParseAndAssemble 加 validateTopology:拦重复/空 id + 悬挂边(source/target 指向不存在节点);对空图/报告任务{topic}/非标准载荷宽松放过不误伤。单测(合法/重复id/空id/悬挂边×2/空端点/空图放过) + live(悬挂边→422,合法→202)。节点-工具映射校验需工具注册表,留后续。
|
||||||
|
- [ ] 熔断器接回 failover(circuitbreaker.go 与 failover.go 未接合,可能长卡备用)| M ← T4.E 唯一剩项(最微妙/blast radius 最大)
|
||||||
|
|
||||||
### [ ] T4.F 健壮性 / 安全 / 性能收口(多为 S,可穿插着做)
|
### [ ] T4.F 健壮性 / 安全 / 性能收口(多为 S,可穿插着做)
|
||||||
- [ ] 关键 DB 写失败上浮 5xx(现 best-effort 返 200,前端无感,task_handler.go:69 等)| M
|
- [ ] 关键 DB 写失败上浮 5xx(现 best-effort 返 200,前端无感,task_handler.go:69 等)| M
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/sundynix/sundynix-shared/contract"
|
"github.com/sundynix/sundynix-shared/contract"
|
||||||
)
|
)
|
||||||
@@ -20,7 +21,11 @@ func ParseAndAssemble(raw json.RawMessage) (*contract.Task, error) {
|
|||||||
if err := json.Unmarshal(raw, &probe); err != nil {
|
if err := json.Unmarshal(raw, &probe); err != nil {
|
||||||
return nil, errors.New("invalid dsl json: " + err.Error())
|
return nil, errors.New("invalid dsl json: " + err.Error())
|
||||||
}
|
}
|
||||||
// TODO: 节点拓扑校验 / 节点-工具映射
|
// 拓扑校验:前置拦截明确的结构错误(重复/空 id、悬挂边),
|
||||||
|
// 避免坏图进编排后被 compose 静默跳过(dangling edge 会被 nodeByID 检查悄悄丢弃)。
|
||||||
|
if err := validateTopology(raw); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
return &contract.Task{
|
return &contract.Task{
|
||||||
ID: newID(),
|
ID: newID(),
|
||||||
Graph: raw,
|
Graph: raw,
|
||||||
@@ -28,6 +33,47 @@ func ParseAndAssemble(raw json.RawMessage) (*contract.Task, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// validateTopology 做保守的拓扑校验:只拦不含糊的结构错误,对空图/非标准图(如报告任务的
|
||||||
|
// {topic:...})与无法按图结构解析的载荷一律宽松放过,杜绝误伤合法提交。
|
||||||
|
// - 节点 id 不得为空、不得重复
|
||||||
|
// - 每条边的 source/target 不得为空、且必须指向存在的节点(悬挂边直接拒)
|
||||||
|
func validateTopology(raw json.RawMessage) error {
|
||||||
|
var g struct {
|
||||||
|
Nodes []struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
} `json:"nodes"`
|
||||||
|
Edges []struct {
|
||||||
|
Source string `json:"source"`
|
||||||
|
Target string `json:"target"`
|
||||||
|
} `json:"edges"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw, &g); err != nil {
|
||||||
|
return nil // 非标准图结构(字段类型不匹配)→ 跳过拓扑校验,不误伤
|
||||||
|
}
|
||||||
|
ids := make(map[string]bool, len(g.Nodes))
|
||||||
|
for i, n := range g.Nodes {
|
||||||
|
if n.ID == "" {
|
||||||
|
return fmt.Errorf("dsl 校验失败:节点[%d] 缺少 id", i)
|
||||||
|
}
|
||||||
|
if ids[n.ID] {
|
||||||
|
return fmt.Errorf("dsl 校验失败:节点 id 重复:%s", n.ID)
|
||||||
|
}
|
||||||
|
ids[n.ID] = true
|
||||||
|
}
|
||||||
|
for i, e := range g.Edges {
|
||||||
|
if e.Source == "" || e.Target == "" {
|
||||||
|
return fmt.Errorf("dsl 校验失败:边[%d] 的 source/target 不能为空", i)
|
||||||
|
}
|
||||||
|
if !ids[e.Source] {
|
||||||
|
return fmt.Errorf("dsl 校验失败:边[%d] 引用不存在的源节点:%s", i, e.Source)
|
||||||
|
}
|
||||||
|
if !ids[e.Target] {
|
||||||
|
return fmt.Errorf("dsl 校验失败:边[%d] 引用不存在的目标节点:%s", i, e.Target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func newID() string {
|
func newID() string {
|
||||||
var b [8]byte
|
var b [8]byte
|
||||||
_, _ = rand.Read(b[:])
|
_, _ = rand.Read(b[:])
|
||||||
|
|||||||
@@ -31,3 +31,41 @@ func TestParseAndAssemble_Errors(t *testing.T) {
|
|||||||
t.Error("非法 JSON 应报错")
|
t.Error("非法 JSON 应报错")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseAndAssemble_Topology(t *testing.T) {
|
||||||
|
ok := func(raw string) {
|
||||||
|
t.Helper()
|
||||||
|
if _, err := ParseAndAssemble(json.RawMessage(raw)); err != nil {
|
||||||
|
t.Errorf("应通过: %s\n err=%v", raw, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bad := func(raw, wantSub string) {
|
||||||
|
t.Helper()
|
||||||
|
_, err := ParseAndAssemble(json.RawMessage(raw))
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("应被拒: %s", raw)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), wantSub) {
|
||||||
|
t.Errorf("错误应含 %q, got %q", wantSub, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 合法:多节点 + 边引用齐全。
|
||||||
|
ok(`{"nodes":[{"id":"a","kind":"input"},{"id":"b","kind":"agent"}],"edges":[{"source":"a","target":"b"}]}`)
|
||||||
|
// 宽松放过:空图 / 报告任务的 {topic} / 无 nodes 字段。
|
||||||
|
ok(`{"nodes":[],"edges":[]}`)
|
||||||
|
ok(`{"topic":"季度报告"}`)
|
||||||
|
ok(`{"version":"1"}`)
|
||||||
|
|
||||||
|
// 拒绝:重复 id。
|
||||||
|
bad(`{"nodes":[{"id":"a"},{"id":"a"}],"edges":[]}`, "重复")
|
||||||
|
// 拒绝:空 id。
|
||||||
|
bad(`{"nodes":[{"id":""}],"edges":[]}`, "缺少 id")
|
||||||
|
// 拒绝:悬挂边(目标节点不存在)。
|
||||||
|
bad(`{"nodes":[{"id":"a"}],"edges":[{"source":"a","target":"ghost"}]}`, "不存在的目标节点")
|
||||||
|
// 拒绝:悬挂边(源节点不存在,即便无节点)。
|
||||||
|
bad(`{"nodes":[],"edges":[{"source":"x","target":"y"}]}`, "不存在的源节点")
|
||||||
|
// 拒绝:边端点为空。
|
||||||
|
bad(`{"nodes":[{"id":"a"}],"edges":[{"source":"a","target":""}]}`, "不能为空")
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user