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,50 @@
import { expect, test } from "bun:test";
// 自研(2026-09-17):Agent 运行中插话。
//
// 这条功能的核心是**取消一个此前刻意加上的限制**:运行时输入框本来是禁用的
// (旧代码 `disabled={busy || running || …}`),策略提示词也据此写着「运行中用户无法打字」。
// 所以这里的断言要盯住两件事:入口真的打开了,以及旧前提没有残留在代码里。
test("运行中不再锁死输入框,发送走插话而不是新建轮次", async () => {
const panel = await Bun.file(new URL("../src/components/canvas/canvas-cloud-agent-panel.tsx", import.meta.url)).text();
// 运行中走插话分支(不建轮次、不走那套扣费幂等记账)
expect(panel).toContain("await interject(value)");
expect(panel).toContain("sendAgentInterjection(activeRun.id, { text: value, messageId })");
// 乐观落进时间线时带插话标记,与「开新一轮」的消息在界面上区分开
expect(panel).toContain('interjection: "sent"');
// 输入框的禁用条件里不能再有 running —— 只保留「连不上事件流」「历史/待确认未水合」这类真不能发的状态
expect(panel).toContain('disabled={Boolean(run && connectionStatus !== "connected")');
expect(panel).not.toContain("disabled={busy || running ||");
// 停止按钮由 running 驱动,发送按钮由我们自己的请求驱动,两者不再互斥
expect(panel).toContain("sending={busy}");
expect(panel).toContain("running={running}");
// 两个 SSE 事件都要处理:回显(多标签页)与未送达退回
expect(panel).toContain('event.type === "user_interjection"');
expect(panel).toContain('event.type === "user_interjection_dropped"');
// 退回时要让文案能被再次发送
expect(panel).toContain("setPrompt?.((current) => (current.trim() ? current : text))");
});
test("输入框运行中同时给出「发送(插话)」与「停止」", async () => {
const composer = await Bun.file(new URL("../src/components/canvas/canvas-cloud-agent-chat-ui.tsx", import.meta.url)).text();
// 关键改动:canStop 不再等于 sending —— 否则运行中两个动作只能二选一
expect(composer).toContain("const canStop = Boolean(running && onStop);");
expect(composer).toContain("const canSubmit = !disabled && !sending && Boolean(prompt.trim() || attachments.length);");
expect(composer).not.toContain("const canStop = Boolean(sending && onStop);");
expect(composer).toContain('aria-label="停止本轮"');
// 插话/未送达要有可见标记
expect(composer).toContain('interjection?: "sent" | "undelivered";');
expect(composer).toContain('item.interjection === "undelivered" ? "插话未送达" : "插话"');
});
test("插话走独立接口,不碰轮次接口", async () => {
const api = await Bun.file(new URL("../src/services/api/agent.ts", import.meta.url)).text();
expect(api).toContain("export function sendAgentInterjection(");
expect(api).toContain("/interjections");
expect(api).toContain("messageId: string");
});