@dream-craft/canvas-agent 0.6.6 → 0.6.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,10 +13,13 @@
13
13
  - 只有用户明确说要在生图/视频工作台生成时,才使用 `workbench_image_*`、`workbench_video_*`。生成任务提交后应说明已经在画布或工作台开始生成,不要在实际没有结果时声称“已生成”。
14
14
  - 需要生成内容时直接调用对应生成工具,不要绑定特定业务场景,不要模拟鼠标点击,不要要求用户手动复制 JSON。
15
15
  - 需要获取、保存或核对生成结果图片时,使用画布节点元数据里的资源地址(`canvas_get_state` 读取)或工具返回的文件路径;不要用浏览器自动化(js/browser 工具)从页面 DOM 抓取或下载图片,也不要在网页 DOM 里查找画布内容。
16
- - 画布工具返回 `unsupported call: <工具名>` 时,含义是**该工具名未在本会话被 Codex 动态工具发现激活**,不是画布未连接、也不是工具不存在——同会话其他画布工具能调用成功即是佐证。对策:先用工具搜索(tool_search)搜该工具名或其功能描述(如 `canvas_get_state`、"read canvas nodes state")把工具激活,再调用;换一次搜索词仍激活失败才向用户报告缺口。禁止把 `unsupported call` 当成「画布未连接」向用户陈述或写进问卷 contextText。开始操作画布前,若 `canvas_get_state` 尚未验证可用(首次调用即报 unsupported call),先激活再继续,不要跳过画布状态读取直接凭记忆操作。
16
+ - `canvas_get_state` 不可用(激活失败)时,**禁止对任何自己没有 firsthand 信息的节点做删除、覆盖内容等不可逆操作**——不知道节点里是什么就删,可能毁掉优化结果、生成产物或用户手动创建的内容(实测:仅凭"应该是残留节点"的猜测删掉了用户报告异常的节点)。此时只允许:如实向用户报告状态读取缺口、请用户贴出节点内容或确认节点用途、或做已完全知情的操作。要动一个节点,先核实它是什么;核实不了就问。
17
+ - 画布工具返回 `unsupported call: <工具名>` 时,含义是**该工具名未出现在本会话 tool_search 的输出工具子集里**,不是画布未连接、也不是工具不存在——同会话其他画布工具能调用成功即是佐证。tool_search 的机制:返回按语义匹配的 namespace(如 mcp__infinite_canvas)+ **附带本次激活的工具子集**(受 limit 限制,默认很小),只有出现在输出里的工具才能调用;namespace 描述里提到某工具名不代表它已被激活。对策:tool_search 时**显式传大 limit(如 20)**、query 用工具名字面或独特功能词;拿到输出后**核对目标工具名是否真的列在其中**,没列出就换 query 再搜(如 `canvas_get_state`、"read canvas state nodes metadata");换两次查询仍失败才向用户报告缺口。禁止把 `unsupported call` 当成「画布未连接」向用户陈述或写进问卷 contextText。开始操作画布前,若 `canvas_get_state` 尚未验证可用(首次调用即报 unsupported call),先激活再继续,不要跳过画布状态读取直接凭记忆操作。
17
18
 
18
19
  ## Windows shell 与工具缺失行为
19
20
 
20
- - 沙箱环境是 Windows PowerShell 5.1。shell 只用于三类用途:读取文件(`Get-Content`)、等待(`Start-Sleep`)、文件搜索(`Get-ChildItem` / `Select-String`)。不要使用 bash/Unix 语法(`ls -la`、`head`、`grep`、`/dev/null` 等);`python3`、`pwsh` 在本机不存在,python 在沙箱 shell 的 PATH 里调不到——不要用脚本语言生成 JSON 参数,直接把 JSON 写进工具参数。
21
+ - 沙箱环境是 Windows PowerShell 5.1。shell 只用于三类用途:读取文件(`Get-Content`)、等待(`Start-Sleep`)、文件搜索(`Get-ChildItem` / `Select-String`)。不要使用 bash/Unix 语法(`ls -la`、`head`、`grep`、`/dev/null` 等);`python3`、`pwsh` 在本机不存在,python 在沙箱 shell 的 PATH 里调不到——不要用脚本语言生成 JSON 参数,直接把 JSON 写进工具参数。
22
+ - 用 `Get-Content` 读任何 .md/.json/.toml 等文本文件时**必须带 `-Encoding UTF8`**(如 `Get-Content <path> -Encoding UTF8`):不带该参数中文会读成 GBK 乱码,乱码会吞掉技能/配置文件里的关键指令(实测:SKILL.md 乱码后丢失了工具优先级条款,导致整个交互形态降级)。读到乱码输出时不要基于它继续推理,换 `-Encoding UTF8` 重读一次。
23
+ - 读技能全文**不要走 `read_mcp_resource`(skills server)**——实测该服务持续报 "not ready"(两轮会话均失败),不要在它上面重试或等待;技能清单里每个技能都带磁盘 `path`,直接 `Get-Content <path> -Encoding UTF8` 读 SKILL.md 即可。
21
24
  - 当工具清单中没有 `canvas_*` 前缀工具(infinite-canvas MCP 未注册或未连接)时,立即向用户报告「画布工具不可用,请检查 Canvas Agent 连接」并停止执行任务;不要把工具名当命令执行,不要在文件系统里搜索画布状态或 MCP 配置。
22
25
  - 同一 shell 命令因环境类错误(命令不存在、语法错误、权限拒绝)连续失败 2 次后,停止更换写法,向用户说明具体缺口;不要继续用其他脚本语言或管道方式重试。画布工具返回的业务错误(如「当前没有已连接画布」)按前文切换画布的规则处理,不属于此类。
@@ -166,8 +166,8 @@ export const toolInputSchemas = {
166
166
  export const toolDescriptions = {
167
167
  site_navigate: "跳转网站页面。path 可为 / (首页)、/canvas (我的画布)、/canvas/:id (指定画布)、/image (生图工作台)、/video (视频创作台)、/prompts (提示词库)、/assets (我的素材)、/config (配置)。操作画布前若不在画布页,先用本工具打开画布。",
168
168
  canvas_list_projects: "列出用户全部画布(仅标题、创建/更新时间、节点数、连线数,不含完整数据),支持 keyword 搜索和 page/pageSize 分页。返回的 id 可配合 site_navigate 跳转到 /canvas/:id 打开对应画布。",
169
- canvas_get_state: "读取当前网页画布的节点、连线、选区和视口。默认节点 metadata.content 超过 240 字符会被截断;full: true 返回未截断内容,做提示词/种子与生成参数(如画幅比例)的一致性分析时必须带 full: true,避免漏看长文本里的画幅假设。",
170
- canvas_get_selection: "读取当前网页画布选中的节点。",
169
+ canvas_get_state: "canvas_get_state(读取画布状态快照,get canvas state snapshot):返回当前画布的全部节点 nodes、连线 connections、选区 selection 与视口,含每个节点的 metadata。默认节点 metadata.content 超过 240 字符会被截断;full: true 返回未截断内容,做提示词/种子与生成参数(如画幅比例)的一致性分析时必须带 full: true,避免漏看长文本里的画幅假设。",
170
+ canvas_get_selection: "canvas_get_selection(读取画布选区,get canvas selection):返回当前网页画布选中的节点列表。",
171
171
  canvas_export_snapshot: "导出当前画布快照,用于理解布局。",
172
172
  canvas_apply_ops: "批量操作当前网页画布。ops 支持 add_node(nodeType 可为内置类型或已启用插件的 \"<pluginId>:<name>\" 类型;省略 nodeType 会静默建成 text 节点且不报错,创建任何非 text 节点时必须显式传 nodeType)、update_node、delete_node、delete_connections、connect_nodes、set_viewport、select_nodes、arrange_nodes、run_generation。ops 最小示例:[{\"type\":\"add_node\",\"id\":\"main\",\"nodeType\":\"text\",\"title\":\"主提示词\",\"metadata\":{\"content\":\"提示词内容\"}}],连接节点用 {\"type\":\"connect_nodes\",\"fromNodeId\":\"main\",\"toNodeId\":\"target\"},connect_nodes 必须同时提供 fromNodeId 与 toNodeId。arrange_nodes 只带意图(ids 或 all 二选一 + mode/axis/side),坐标由网页端排版算法计算并在整理后聚焦结果,不要自己算坐标或预判结果。mode=auto/flow/by_media/row/column/grid/spread 为防重叠整理,结果不重叠;mode=align/distribute 是对齐/等距工具,按定义会让节点在一条线上重叠或贴合,这是预期行为,不要据此判定整理失败。",
173
173
  canvas_create_node: "创建任意类型节点:text、image、config、video、audio,或已启用画布插件的节点类型(如 \"prompt-enhancer:optimize\",需先在画布「节点插件」管理器启用对应插件)。适合创建占位图、媒体占位、配置节点或自定义 metadata 节点。",
@@ -10,6 +10,28 @@ import { DEFAULT_PORT, ensureSiteWorkspace, loadConfig, saveConfig, updateSiteWo
10
10
  import { logger } from "../utils/logger.js";
11
11
  import { checkVersions } from "../version-check.js";
12
12
  import { SkillStore, SkillStoreError } from "../skills/store.js";
13
+ /** 远程图片转存的体积与耗时上限(防止本地服务被大文件拖垮)。 */
14
+ const REMOTE_IMAGE_MAX_BYTES = 20 * 1024 * 1024;
15
+ const REMOTE_IMAGE_TIMEOUT_MS = 30_000;
16
+ /** 拒绝把转存端点当作访问内网/回环地址的跳板(SSRF 防护)。 */
17
+ function isPrivateHost(hostname) {
18
+ const host = hostname.toLowerCase();
19
+ if (host === "localhost" || host === "::1" || host.endsWith(".localhost") || host.endsWith(".local"))
20
+ return true;
21
+ if (/^127\./.test(host) || /^10\./.test(host) || /^192\.168\./.test(host) || /^169\.254\./.test(host))
22
+ return true;
23
+ const matched = /^172\.(\d{1,3})\./.exec(host);
24
+ if (matched) {
25
+ const second = Number(matched[1]);
26
+ if (second >= 16 && second <= 31)
27
+ return true;
28
+ }
29
+ if (/^0\./.test(host))
30
+ return true;
31
+ if (host.includes(":"))
32
+ return true; // IPv6 字面量(含回环与链路本地)一律拒绝
33
+ return false;
34
+ }
13
35
  /** 启动仅监听本机的 Canvas Agent HTTP 服务。 */
14
36
  export function startHttpServer() {
15
37
  const config = loadConfig(true);
@@ -176,13 +198,45 @@ export function startHttpServer() {
176
198
  app.post("/agent/local-image", route(async (req, res) => {
177
199
  const filePath = String(req.body?.path || "");
178
200
  if (!path.isAbsolute(filePath) || !/\.(?:avif|gif|jpe?g|png|webp)$/i.test(filePath))
179
- return res.status(400).json({ ok: false, error: "图片路径无效" });
201
+ return void res.status(400).json({ ok: false, error: "图片路径无效" });
180
202
  const file = await stat(filePath);
181
203
  if (!file.isFile())
182
- return res.status(400).json({ ok: false, error: "图片文件无效" });
204
+ return void res.status(400).json({ ok: false, error: "图片文件无效" });
183
205
  res.setHeader("Cache-Control", "no-store");
184
206
  res.type(path.extname(filePath)).send(await readFile(filePath));
185
207
  }));
208
+ // 远程图片转存:浏览器直连上游图片常被 CORS 拦下,<img> 能显示但取不到字节,产物无法持久化。
209
+ // 这里是本地服务端,取字节无跨域限制——不参与任何模型请求转发,模型请求仍直连上游。
210
+ app.post("/agent/fetch-image", route(async (req, res) => {
211
+ const raw = String(req.body?.url || "").trim();
212
+ if (!/^https?:\/\//i.test(raw))
213
+ return void res.status(400).json({ ok: false, error: "仅支持 http/https 图片地址" });
214
+ const target = new URL(raw);
215
+ if (isPrivateHost(target.hostname))
216
+ return void res.status(400).json({ ok: false, error: "拒绝访问内网地址" });
217
+ const controller = new AbortController();
218
+ const timer = setTimeout(() => controller.abort(), REMOTE_IMAGE_TIMEOUT_MS);
219
+ try {
220
+ const response = await fetch(target, { signal: controller.signal, redirect: "follow" });
221
+ if (!response.ok)
222
+ return void res.status(502).json({ ok: false, error: `上游返回 ${response.status}` });
223
+ const contentType = response.headers.get("content-type") || "";
224
+ if (!contentType.startsWith("image/"))
225
+ return void res.status(415).json({ ok: false, error: "目标不是图片" });
226
+ const buffer = Buffer.from(await response.arrayBuffer());
227
+ if (buffer.byteLength > REMOTE_IMAGE_MAX_BYTES)
228
+ return void res.status(413).json({ ok: false, error: "图片超过大小上限" });
229
+ res.setHeader("Cache-Control", "no-store");
230
+ res.type(contentType).send(buffer);
231
+ }
232
+ catch (error) {
233
+ const message = error instanceof Error ? error.message : String(error);
234
+ res.status(502).json({ ok: false, error: `图片下载失败:${message}` });
235
+ }
236
+ finally {
237
+ clearTimeout(timer);
238
+ }
239
+ }));
186
240
  app.post("/api/tools", route(async (req, res) => res.json({ ok: true, result: await session.callTool(req.body?.name, req.body?.input || {}) })));
187
241
  app.get("/agent/codex/workspace", (_req, res) => {
188
242
  const workspace = ensureSiteWorkspace(config);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dream-craft/canvas-agent",
3
- "version": "0.6.6",
3
+ "version": "0.6.8",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",