feat(canvas): 画布工作区 - 整合侧栏与风格预设并修复提示词回填

选择性整合 #547,保留主分支节点与工具条设计。新增默认折叠的节点、资产、任务和历史面板,风格与预设保留在节点内,移除重复入口。

补齐工具权限、提示词解析、资源引用保护及数据库迁移,修复提示词清空和连续编辑后的生成配置同步。

验证:前端全量 1925 项测试、构建、类型检查及 lint 通过;后端 go test ./... 通过。真实模型、PostgreSQL 实库与登录态页面验收仍待进行。

Refs #547

Co-authored-by: bensharp <bensharp@qq.com>
This commit is contained in:
ddCatandbensharp committed 2026-09-20 16:40:08 +08:00
1 parent 0ea9f7d94b
commit 9148ceab66
66 files changed
+6127 -97

No files matched your search

@@ -4,6 +4,7 @@ import test from "node:test";
import "./node-registry/definitions/index.ts";
// @ts-expect-error -- Node 原生 TypeScript 测试运行器需要保留扩展名。
import { canvasNodeHasCommittedContent, writeCanvasNodePrompt } from "./canvas-node-prompt.ts";
import { canonicalGenerationMetadata } from "./generation-contract";
// @ts-expect-error -- Node 原生 TypeScript 测试运行器需要保留扩展名。
import { CanvasNodeType, type CanvasNodeData } from "../../types/canvas.ts";
@@ -49,6 +50,28 @@ test("空媒体草稿同时初始化提交提示词和编辑提示词", () => {
assert.equal(metadata?.generationSpec?.prompt, "第一版提示词");
});
test("模板提示词清空后重新读取和保存重载均保持为空", () => {
for (const type of [CanvasNodeType.Image, CanvasNodeType.Video, CanvasNodeType.Audio]) {
for (const content of [undefined, `https://example.com/${type}`]) {
const current = node(type, { content, prompt: "已提交提示词" });
const applied = writeCanvasNodePrompt(current, "模板预设文字");
const cleared = writeCanvasNodePrompt(applied, "");
assert.equal(cleared.metadata?.composerContent, "");
assert.equal(cleared.metadata?.generationSpec?.prompt, "");
assert.equal(cleared.metadata?.prompt, content ? "已提交提示词" : "");
const reloaded = JSON.parse(JSON.stringify(cleared)) as CanvasNodeData;
assert.equal(canonicalGenerationMetadata(reloaded, type).composerContent, "");
}
}
});
test("已有生成合同的提示词支持连续编辑而不回填旧模板", () => {
const applied = writeCanvasNodePrompt(node(CanvasNodeType.Image), "模板预设文字");
const edited = writeCanvasNodePrompt(applied, "修改后的文字");
assert.equal(edited.metadata?.generationSpec?.prompt, "修改后的文字");
assert.equal(canonicalGenerationMetadata(edited, "image").composerContent, "修改后的文字");
});
test("图片仅保留 storageKey 时仍视为已有媒体结果", () => {
const current = node(CanvasNodeType.Image, {
storageKey: "canvas/image/result.png",