feat(agent): Agent 工作台 - 计划插话记忆、统一模型选择器并修复摘要与视频入库

将计划清单、运行中插话和个人记忆接入云端 Agent;全站模型选择器统一为左右二级菜单。画布摘要按预算截断不再整轮拒绝,首页视频未知宽高可入库。README 补充贡献者 morrow。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
ddCat
2026-09-17 20:21:25 +08:00
parent b6bae4f957
commit 6892732b06
80 changed files with 7364 additions and 182 deletions

View File

@@ -0,0 +1,57 @@
package app
import (
"testing"
"infinite-canvas/backend/internal/model"
)
func TestCloudAgentEmptyModelOutputDetection(t *testing.T) {
cases := []struct {
name string
task *model.Task
want bool
}{
{"空回合 → 可恢复", &model.Task{Status: model.TaskStatusFailed, Error: "画布 Agent 接口没有返回内容"}, true},
{"成功的任务不算空回合", &model.Task{Status: model.TaskStatusSucceeded, Error: ""}, false},
{"别的失败原因不该被吞", &model.Task{Status: model.TaskStatusFailed, Error: "上游 429 限流"}, false},
{"nil 安全", nil, false},
}
for _, c := range cases {
if got := cloudAgentEmptyModelOutput(c.task); got != c.want {
t.Fatalf("%s:cloudAgentEmptyModelOutput = %v, want %v", c.name, got, c.want)
}
}
if cloudAgentMaxEmptyOutputNudges <= 0 || cloudAgentMaxEmptyOutputNudges > 3 {
t.Fatalf("空回合催促上限应在 1..3 之间,got %d", cloudAgentMaxEmptyOutputNudges)
}
}
func TestCloudAgentTruncatedToolArguments(t *testing.T) {
if !cloudAgentTruncatedToolArguments(&model.Task{Error: "工具参数不是完整 JSON"}) {
t.Fatal("截断的工具参数应判为可恢复")
}
if cloudAgentTruncatedToolArguments(&model.Task{Error: "上游 429 限流"}) {
t.Fatal("其它失败原因不该被当成截断")
}
}
func TestCloudAgentOutputViolation(t *testing.T) {
if got := cloudAgentOutputViolation(stringsRepeat("x", 32001), 1); got == "" {
t.Fatal("超长正文应判定违规")
}
if got := cloudAgentOutputViolation("ok", 9); got == "" {
t.Fatal("超过 8 个工具调用应判定违规")
}
if got := cloudAgentOutputViolation("ok", 2); got != "" {
t.Fatalf("合规输出不应违规:%s", got)
}
}
func stringsRepeat(value string, count int) string {
out := make([]byte, 0, len(value)*count)
for i := 0; i < count; i++ {
out = append(out, value...)
}
return string(out)
}