From e422285c597f79f45c96f8d67ae551be0ce2a587 Mon Sep 17 00:00:00 2001 From: HouYunFei <1844025705@qq.com> Date: Sun, 14 Jun 2026 18:27:44 +0800 Subject: [PATCH] feat(agent): implement local Canvas Agent with MCP integration and HTTP server --- CHANGELOG.md | 2 + README.md | 6 +- canvas-agent/.gitignore | 2 + canvas-agent/README.md | 107 +++ canvas-agent/bun.lock | 304 +++++++ canvas-agent/package.json | 35 + canvas-agent/src/agents.ts | 300 +++++++ canvas-agent/src/canvas-session.ts | 94 ++ canvas-agent/src/config.ts | 27 + canvas-agent/src/http-server.ts | 88 ++ canvas-agent/src/index.ts | 6 + canvas-agent/src/mcp-server.ts | 29 + canvas-agent/src/schemas.ts | 35 + canvas-agent/src/tools.ts | 26 + canvas-agent/src/types.ts | 8 + canvas-agent/tsconfig.json | 14 + docs/content/docs/overview/meta.json | 2 +- .../progress/local-agent-integration-plan.mdx | 248 ++++++ docs/content/docs/progress/meta.json | 1 + docs/content/docs/progress/pending-test.mdx | 1 + docs/content/docs/progress/todo.mdx | 2 + docs/src/app/(home)/page.tsx | 2 +- docs/src/lib/layout.shared.tsx | 2 +- .../(user)/canvas/[id]/canvas-client-page.tsx | 81 +- .../components/canvas-local-agent-panel.tsx | 836 ++++++++++++++++++ .../canvas/stores/use-canvas-agent-store.ts | 53 ++ .../(user)/canvas/utils/canvas-agent-ops.ts | 84 ++ 27 files changed, 2388 insertions(+), 7 deletions(-) create mode 100644 canvas-agent/.gitignore create mode 100644 canvas-agent/README.md create mode 100644 canvas-agent/bun.lock create mode 100644 canvas-agent/package.json create mode 100644 canvas-agent/src/agents.ts create mode 100644 canvas-agent/src/canvas-session.ts create mode 100644 canvas-agent/src/config.ts create mode 100644 canvas-agent/src/http-server.ts create mode 100644 canvas-agent/src/index.ts create mode 100644 canvas-agent/src/mcp-server.ts create mode 100644 canvas-agent/src/schemas.ts create mode 100644 canvas-agent/src/tools.ts create mode 100644 canvas-agent/src/types.ts create mode 100644 canvas-agent/tsconfig.json create mode 100644 docs/content/docs/progress/local-agent-integration-plan.mdx create mode 100644 web/src/app/(user)/canvas/components/canvas-local-agent-panel.tsx create mode 100644 web/src/app/(user)/canvas/stores/use-canvas-agent-store.ts create mode 100644 web/src/app/(user)/canvas/utils/canvas-agent-ops.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 41451b0..ee4682a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased ++ [新增] 新增canvas-agent通过codex/claude code操作画布。 + ## v0.2.5 - 2026-06-08 + [新增] 新增图片切图功能。 diff --git a/README.md b/README.md index 1a57fa8..a27868b 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ - 无限画布:多画布项目、节点拖拽缩放、连线、小地图、撤销重做、导入导出。 - AI 创作:支持 OpenAI 兼容接口的文生图、图生图、参考图编辑、文本问答和视频生成;Seedance 2.0 可通过火山方舟 Agent Plan 接入。 - 画布助手:围绕选中节点和上游节点对话、生图,并把结果插回画布。 +- 本地 Agent:通过本机 Canvas Agent 连接 Codex / Claude Code,让 Agent 通过 MCP 操作当前画布。 - 提示词库:抓取多个 GitHub 开源项目,按案例整理数百个图片提示词。 完整功能说明见 [docs/features.md](docs2/features.md)。 @@ -67,11 +68,11 @@ docker compose -f docker-compose.local.yml up -d --build 如果使用 New API,可在 `系统设置 -> 聊天方式 -> 添加聊天设置` 中填入: ```text -https://infinite-canvas-cpco.onrender.com?apiKey={key}&baseUrl={address} +https://canvas.best?apiKey={key}&baseUrl={address} ``` 跳转后会自动打开配置弹窗并填入 API Key 和 Base URL。 -如果自己部署了,可以把 `https://infinite-canvas-cpco.onrender.com` 替换成你部署的地址。 +如果自己部署了,可以把 `https://canvas.best` 替换成你部署的地址。 ## 效果展示 @@ -100,6 +101,7 @@ https://infinite-canvas-cpco.onrender.com?apiKey={key}&baseUrl={address} - [后端数据库说明](docs2/backend-database.md) - [系统配置数据结构](docs2/system-settings.md) - [接口响应约定](docs2/api-response.md) +- [本地 Canvas Agent](canvas-agent/README.md) ## 赞助支持 diff --git a/canvas-agent/.gitignore b/canvas-agent/.gitignore new file mode 100644 index 0000000..de4d1f0 --- /dev/null +++ b/canvas-agent/.gitignore @@ -0,0 +1,2 @@ +dist +node_modules diff --git a/canvas-agent/README.md b/canvas-agent/README.md new file mode 100644 index 0000000..1f815e2 --- /dev/null +++ b/canvas-agent/README.md @@ -0,0 +1,107 @@ +# Infinite Canvas Agent + +本地 Canvas Agent 用来连接线上画布网页和用户电脑上的 Codex / Claude Code。 + +## 启动 + +```bash +npx -y canvas-agent +``` + +本仓库开发时也可以直接运行: + +```bash +cd canvas-agent +npm install +npm run build +node dist/index.js +``` + +启动后会输出本机地址和 token: + +```txt +Local URL: http://127.0.0.1:17371 +Connect token: xxxxxx +``` + +在画布右上角点击 `Agent`,填入地址和 token 后连接。 + +Canvas Agent 默认只监听 `127.0.0.1`。网页第一次带正确 token 连接后,Canvas Agent 会记录该网页 Origin;之后其他 Origin 不能复用这个本地 Agent,除非用户清理 `~/.infinite-canvas/canvas-agent.json` 里的 `origins`。 + +## Codex MCP + +如果希望 Codex 终端能直接操作画布,需要先把 Canvas Agent 注册成 Codex MCP。 + +Canvas Agent 启动后,给 Codex 添加 MCP: + +```bash +codex mcp add infinite-canvas -- npx -y canvas-agent mcp +``` + +本仓库开发时可以改成,实际使用建议替换为本机绝对路径: + +```bash +codex mcp add infinite-canvas -- node /path/to/infinite-canvas/canvas-agent/dist/index.js mcp +``` + +Canvas Agent 源码使用 TypeScript 编写,MCP 协议层使用官方 `@modelcontextprotocol/sdk`,工具入参使用 `zod` 描述。 + +如果希望终端里的 Codex 不被 MCP 审批卡住,可以在 `~/.codex/config.toml` 里给这个 MCP 设置自动放行: + +```toml +[mcp_servers.infinite-canvas] +command = "npx" +args = ["-y", "canvas-agent", "mcp"] +default_tools_approval_mode = "approve" +``` + +可用工具: + +- `canvas_get_state` +- `canvas_get_selection` +- `canvas_export_snapshot` +- `canvas_apply_ops` +- `canvas_create_text_node` +- `canvas_create_image_prompt_flow` + +`canvas_apply_ops` 示例: + +```json +{ + "ops": [ + { + "type": "add_node", + "nodeType": "text", + "title": "标题", + "position": { "x": 0, "y": 0 }, + "metadata": { "content": "文本内容" } + } + ] +} +``` + +## 侧边栏 Codex + +本地面板会把提示词发送给 Canvas Agent。Canvas Agent 使用官方 `@openai/codex` CLI 的 `codex app-server --stdio` 启动并复用同一个 Codex thread,启动时会注入 `infinite-canvas` MCP 配置并自动放行 MCP 审批,真正执行画布修改前仍由网页侧边栏二次确认。 + +侧边栏会展示 Codex 返回的 `thread.started`、`turn.started`、`item.*`、`turn.completed` 等结构化事件;收到 app-server 的 `item/agentMessage/delta` 时,Canvas Agent 会转成 `item.updated`,网页会用同一条消息做真实流式更新,并把工具细节收进运行日志。 + +侧边栏上传或粘贴的图片会先发到本机 Canvas Agent,再由 Canvas Agent 临时写入本机文件并作为 app-server `localImage` 输入传给 Codex;前端会提示附件体积,单次请求体限制为 30MB。 + +## Claude Code + +Claude Code Adapter 代码暂时保留,但当前网页侧边栏只开放 Codex。后续开放 Claude 入口时,Canvas Agent 会调用本机 `claude -p --output-format stream-json` 并把流式 JSON 事件转发到侧边栏。 + +如果希望 Claude Code 也能操作画布,需要给 Claude Code 添加同一个 MCP。建议用 user scope,避免 Canvas Agent 从不同目录启动时找不到配置: + +```bash +claude mcp add --scope user --transport stdio infinite-canvas -- npx -y canvas-agent mcp +``` + +本仓库开发时可以改成: + +```bash +claude mcp add --scope user --transport stdio infinite-canvas -- node /path/to/infinite-canvas/canvas-agent/dist/index.js mcp +``` + +Canvas Agent 调用 Claude Code 时会默认带上 `--allowedTools mcp__infinite-canvas__*`,画布写操作仍由网页侧边栏确认。 diff --git a/canvas-agent/bun.lock b/canvas-agent/bun.lock new file mode 100644 index 0000000..a6049c7 --- /dev/null +++ b/canvas-agent/bun.lock @@ -0,0 +1,304 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "canvas-agent", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.12.1", + "@openai/codex": "^0.139.0", + "express": "^5.1.0", + "zod": "^3.25.0", + }, + "devDependencies": { + "@types/express": "^5.0.0", + "@types/node": "^22.0.0", + "tsx": "^4.19.0", + "typescript": "^5.8.0", + }, + }, + }, + "packages": { + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.28.1.tgz", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.28.1.tgz", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="], + + "@hono/node-server": ["@hono/node-server@1.19.14", "https://registry.npmmirror.com/@hono/node-server/-/node-server-1.19.14.tgz", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="], + + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "https://registry.npmmirror.com/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="], + + "@openai/codex": ["@openai/codex@0.139.0", "https://registry.npmmirror.com/@openai/codex/-/codex-0.139.0.tgz", { "optionalDependencies": { "@openai/codex-darwin-arm64": "npm:@openai/codex@0.139.0-darwin-arm64", "@openai/codex-darwin-x64": "npm:@openai/codex@0.139.0-darwin-x64", "@openai/codex-linux-arm64": "npm:@openai/codex@0.139.0-linux-arm64", "@openai/codex-linux-x64": "npm:@openai/codex@0.139.0-linux-x64", "@openai/codex-win32-arm64": "npm:@openai/codex@0.139.0-win32-arm64", "@openai/codex-win32-x64": "npm:@openai/codex@0.139.0-win32-x64" }, "bin": { "codex": "bin/codex.js" } }, "sha512-wr2fRE+fzW0CjEbfFsLh1ftarVEcw0CMLWS7QyA0nyOz5qacQPVq3cq2+/U7oEbwm1TOqoi0Fm1nxniB5FkpmA=="], + + "@openai/codex-darwin-arm64": ["@openai/codex@0.139.0-darwin-arm64", "https://registry.npmmirror.com/@openai/codex/-/codex-0.139.0-darwin-arm64.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-o+0ZKWwgDFMMLO7rwinzO0PQsgK+Vme1pMN2GeAxsX29ZgGZcyPICfpJbeGSUO1mb2a36Skjx6nfdRnxMY0r7w=="], + + "@openai/codex-darwin-x64": ["@openai/codex@0.139.0-darwin-x64", "https://registry.npmmirror.com/@openai/codex/-/codex-0.139.0-darwin-x64.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-9gkBWzu6DB2rqU4DbpxD3DE5bofGpsK46Lp0h0I+bKWc2IIcxvSi8K2utKmBLoJCbKrn4JQu7dFNGRqEfENung=="], + + "@openai/codex-linux-arm64": ["@openai/codex@0.139.0-linux-arm64", "https://registry.npmmirror.com/@openai/codex/-/codex-0.139.0-linux-arm64.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-tBQE5lZciRHeWZGuURgjP9S717MvTIpQMc593+DNxY2LQxozkngOkzFSQd1+/UmQKGrCqdFLu5irIwPXpSZyEw=="], + + "@openai/codex-linux-x64": ["@openai/codex@0.139.0-linux-x64", "https://registry.npmmirror.com/@openai/codex/-/codex-0.139.0-linux-x64.tgz", { "os": "linux", "cpu": "x64" }, "sha512-14UgzDS+X4crkvdt6S02A/ZZOrS8ZyWiuTRpguCtnhNamb7unSuDxy86BWgpAl3sqiTaN2CP8VLyp2ohQ8Nbzw=="], + + "@openai/codex-sdk": ["@openai/codex-sdk@0.139.0", "https://registry.npmmirror.com/@openai/codex-sdk/-/codex-sdk-0.139.0.tgz", { "dependencies": { "@openai/codex": "0.139.0" } }, "sha512-r4lDckaVx4mVZy1v/7ykEhkeWjVfM/4oGJfhG0AP4+zN3Sa+jcf5hdY4EHfJasofBcp0tIF/7JCKHpv534R+tw=="], + + "@openai/codex-win32-arm64": ["@openai/codex@0.139.0-win32-arm64", "https://registry.npmmirror.com/@openai/codex/-/codex-0.139.0-win32-arm64.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-nlwRjsYotH1Rtqu/Q0VwQbIeO2UX1mkHK84Ov9qn/hl29QqqoBtno0tRyqIPbkXFIVQuWiAYXlV3ugLwH5fTrQ=="], + + "@openai/codex-win32-x64": ["@openai/codex@0.139.0-win32-x64", "https://registry.npmmirror.com/@openai/codex/-/codex-0.139.0-win32-x64.tgz", { "os": "win32", "cpu": "x64" }, "sha512-lQrVLNz+90wdvWVNFDvCkHQRiAK9ZllmkTka3c8eqSDqdJk35Gpgppfv9Xtw5M2ZBtTq0sBdWBiCMyzGDBSpmQ=="], + + "@types/body-parser": ["@types/body-parser@1.19.6", "https://registry.npmmirror.com/@types/body-parser/-/body-parser-1.19.6.tgz", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="], + + "@types/connect": ["@types/connect@3.4.38", "https://registry.npmmirror.com/@types/connect/-/connect-3.4.38.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], + + "@types/express": ["@types/express@5.0.6", "https://registry.npmmirror.com/@types/express/-/express-5.0.6.tgz", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "^2" } }, "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA=="], + + "@types/express-serve-static-core": ["@types/express-serve-static-core@5.1.1", "https://registry.npmmirror.com/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A=="], + + "@types/http-errors": ["@types/http-errors@2.0.5", "https://registry.npmmirror.com/@types/http-errors/-/http-errors-2.0.5.tgz", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="], + + "@types/node": ["@types/node@22.19.21", "https://registry.npmmirror.com/@types/node/-/node-22.19.21.tgz", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-VMeFBSCKQKmm2swI2kW51SFusDqekC6q9trBCvJ/JliDchFSuoYYKN7yVNjPthP1HKZcx3U1gI/wTcEBjEFKTA=="], + + "@types/qs": ["@types/qs@6.15.1", "https://registry.npmmirror.com/@types/qs/-/qs-6.15.1.tgz", {}, "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw=="], + + "@types/range-parser": ["@types/range-parser@1.2.7", "https://registry.npmmirror.com/@types/range-parser/-/range-parser-1.2.7.tgz", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], + + "@types/send": ["@types/send@1.2.1", "https://registry.npmmirror.com/@types/send/-/send-1.2.1.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ=="], + + "@types/serve-static": ["@types/serve-static@2.2.0", "https://registry.npmmirror.com/@types/serve-static/-/serve-static-2.2.0.tgz", { "dependencies": { "@types/http-errors": "*", "@types/node": "*" } }, "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ=="], + + "accepts": ["accepts@2.0.0", "https://registry.npmmirror.com/accepts/-/accepts-2.0.0.tgz", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + + "ajv": ["ajv@8.20.0", "https://registry.npmmirror.com/ajv/-/ajv-8.20.0.tgz", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], + + "ajv-formats": ["ajv-formats@3.0.1", "https://registry.npmmirror.com/ajv-formats/-/ajv-formats-3.0.1.tgz", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], + + "body-parser": ["body-parser@2.2.2", "https://registry.npmmirror.com/body-parser/-/body-parser-2.2.2.tgz", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], + + "bytes": ["bytes@3.1.2", "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + + "content-disposition": ["content-disposition@1.1.0", "https://registry.npmmirror.com/content-disposition/-/content-disposition-1.1.0.tgz", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="], + + "content-type": ["content-type@1.0.5", "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + + "cookie": ["cookie@0.7.2", "https://registry.npmmirror.com/cookie/-/cookie-0.7.2.tgz", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + + "cookie-signature": ["cookie-signature@1.2.2", "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.2.2.tgz", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], + + "cors": ["cors@2.8.6", "https://registry.npmmirror.com/cors/-/cors-2.8.6.tgz", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], + + "cross-spawn": ["cross-spawn@7.0.6", "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "debug": ["debug@4.4.3", "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "depd": ["depd@2.0.0", "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + + "dunder-proto": ["dunder-proto@1.0.1", "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "ee-first": ["ee-first@1.1.1", "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + + "encodeurl": ["encodeurl@2.0.0", "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + + "es-define-property": ["es-define-property@1.0.1", "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.2", "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="], + + "esbuild": ["esbuild@0.28.1", "https://registry.npmmirror.com/esbuild/-/esbuild-0.28.1.tgz", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="], + + "escape-html": ["escape-html@1.0.3", "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + + "etag": ["etag@1.8.1", "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + + "eventsource": ["eventsource@3.0.7", "https://registry.npmmirror.com/eventsource/-/eventsource-3.0.7.tgz", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], + + "eventsource-parser": ["eventsource-parser@3.1.0", "https://registry.npmmirror.com/eventsource-parser/-/eventsource-parser-3.1.0.tgz", {}, "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg=="], + + "express": ["express@5.2.1", "https://registry.npmmirror.com/express/-/express-5.2.1.tgz", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], + + "express-rate-limit": ["express-rate-limit@8.5.2", "https://registry.npmmirror.com/express-rate-limit/-/express-rate-limit-8.5.2.tgz", { "dependencies": { "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-uri": ["fast-uri@3.1.2", "https://registry.npmmirror.com/fast-uri/-/fast-uri-3.1.2.tgz", {}, "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ=="], + + "finalhandler": ["finalhandler@2.1.1", "https://registry.npmmirror.com/finalhandler/-/finalhandler-2.1.1.tgz", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], + + "forwarded": ["forwarded@0.2.0", "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + + "fresh": ["fresh@2.0.0", "https://registry.npmmirror.com/fresh/-/fresh-2.0.0.tgz", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], + + "fsevents": ["fsevents@2.3.3", "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "function-bind": ["function-bind@1.1.2", "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "gopd": ["gopd@1.2.0", "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "has-symbols": ["has-symbols@1.1.0", "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "hasown": ["hasown@2.0.4", "https://registry.npmmirror.com/hasown/-/hasown-2.0.4.tgz", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], + + "hono": ["hono@4.12.25", "https://registry.npmmirror.com/hono/-/hono-4.12.25.tgz", {}, "sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ=="], + + "http-errors": ["http-errors@2.0.1", "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.1.tgz", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], + + "iconv-lite": ["iconv-lite@0.7.2", "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.7.2.tgz", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], + + "inherits": ["inherits@2.0.4", "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "ip-address": ["ip-address@10.2.0", "https://registry.npmmirror.com/ip-address/-/ip-address-10.2.0.tgz", {}, "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA=="], + + "ipaddr.js": ["ipaddr.js@1.9.1", "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + + "is-promise": ["is-promise@4.0.0", "https://registry.npmmirror.com/is-promise/-/is-promise-4.0.0.tgz", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], + + "isexe": ["isexe@2.0.0", "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "jose": ["jose@6.2.3", "https://registry.npmmirror.com/jose/-/jose-6.2.3.tgz", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="], + + "json-schema-traverse": ["json-schema-traverse@1.0.0", "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "json-schema-typed": ["json-schema-typed@8.0.2", "https://registry.npmmirror.com/json-schema-typed/-/json-schema-typed-8.0.2.tgz", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "media-typer": ["media-typer@1.1.0", "https://registry.npmmirror.com/media-typer/-/media-typer-1.1.0.tgz", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], + + "merge-descriptors": ["merge-descriptors@2.0.0", "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-2.0.0.tgz", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], + + "mime-db": ["mime-db@1.54.0", "https://registry.npmmirror.com/mime-db/-/mime-db-1.54.0.tgz", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + + "mime-types": ["mime-types@3.0.2", "https://registry.npmmirror.com/mime-types/-/mime-types-3.0.2.tgz", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + + "ms": ["ms@2.1.3", "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "negotiator": ["negotiator@1.0.0", "https://registry.npmmirror.com/negotiator/-/negotiator-1.0.0.tgz", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + + "object-assign": ["object-assign@4.1.1", "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "object-inspect": ["object-inspect@1.13.4", "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + + "on-finished": ["on-finished@2.4.1", "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], + + "once": ["once@1.4.0", "https://registry.npmmirror.com/once/-/once-1.4.0.tgz", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "parseurl": ["parseurl@1.3.3", "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + + "path-key": ["path-key@3.1.1", "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-to-regexp": ["path-to-regexp@8.4.2", "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-8.4.2.tgz", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="], + + "pkce-challenge": ["pkce-challenge@5.0.1", "https://registry.npmmirror.com/pkce-challenge/-/pkce-challenge-5.0.1.tgz", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="], + + "proxy-addr": ["proxy-addr@2.0.7", "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + + "qs": ["qs@6.15.2", "https://registry.npmmirror.com/qs/-/qs-6.15.2.tgz", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw=="], + + "range-parser": ["range-parser@1.2.1", "https://registry.npmmirror.com/range-parser/-/range-parser-1.2.1.tgz", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], + + "raw-body": ["raw-body@3.0.2", "https://registry.npmmirror.com/raw-body/-/raw-body-3.0.2.tgz", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], + + "require-from-string": ["require-from-string@2.0.2", "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + + "router": ["router@2.2.0", "https://registry.npmmirror.com/router/-/router-2.2.0.tgz", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], + + "safer-buffer": ["safer-buffer@2.1.2", "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "send": ["send@1.2.1", "https://registry.npmmirror.com/send/-/send-1.2.1.tgz", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], + + "serve-static": ["serve-static@2.2.1", "https://registry.npmmirror.com/serve-static/-/serve-static-2.2.1.tgz", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], + + "setprototypeof": ["setprototypeof@1.2.0", "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + + "shebang-command": ["shebang-command@2.0.0", "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "side-channel": ["side-channel@1.1.1", "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.1.tgz", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4", "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ=="], + + "side-channel-list": ["side-channel-list@1.0.1", "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.1.tgz", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="], + + "side-channel-map": ["side-channel-map@1.0.1", "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + + "statuses": ["statuses@2.0.2", "https://registry.npmmirror.com/statuses/-/statuses-2.0.2.tgz", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + + "toidentifier": ["toidentifier@1.0.1", "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + + "tsx": ["tsx@4.22.4", "https://registry.npmmirror.com/tsx/-/tsx-4.22.4.tgz", { "dependencies": { "esbuild": "~0.28.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg=="], + + "type-is": ["type-is@2.1.0", "https://registry.npmmirror.com/type-is/-/type-is-2.1.0.tgz", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="], + + "typescript": ["typescript@5.9.3", "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@6.21.0", "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + + "unpipe": ["unpipe@1.0.0", "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + + "vary": ["vary@1.1.2", "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + + "which": ["which@2.0.2", "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "wrappy": ["wrappy@1.0.2", "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "zod": ["zod@3.25.76", "https://registry.npmmirror.com/zod/-/zod-3.25.76.tgz", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "https://registry.npmmirror.com/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], + + "type-is/content-type": ["content-type@2.0.0", "https://registry.npmmirror.com/content-type/-/content-type-2.0.0.tgz", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], + } +} diff --git a/canvas-agent/package.json b/canvas-agent/package.json new file mode 100644 index 0000000..8dc9952 --- /dev/null +++ b/canvas-agent/package.json @@ -0,0 +1,35 @@ +{ + "name": "canvas-agent", + "version": "0.1.0", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "bin": { + "canvas-agent": "./dist/index.js" + }, + "files": [ + "dist", + "README.md" + ], + "scripts": { + "dev": "tsx src/index.ts", + "build": "tsc -p tsconfig.json", + "start": "node dist/index.js", + "prepack": "npm run build" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.12.1", + "@openai/codex": "^0.139.0", + "express": "^5.1.0", + "zod": "^3.25.0" + }, + "devDependencies": { + "@types/express": "^5.0.0", + "@types/node": "^22.0.0", + "tsx": "^4.19.0", + "typescript": "^5.8.0" + }, + "engines": { + "node": ">=18" + } +} diff --git a/canvas-agent/src/agents.ts b/canvas-agent/src/agents.ts new file mode 100644 index 0000000..6854138 --- /dev/null +++ b/canvas-agent/src/agents.ts @@ -0,0 +1,300 @@ +import { spawn, type ChildProcess, type StdioOptions } from "node:child_process"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { createRequire } from "node:module"; +import { fileURLToPath } from "node:url"; + +import { AGENT_PROMPT, VERSION } from "./config.js"; +import type { AgentAttachment, AgentEmit } from "./types.js"; + +type Json = Record; +type AgentEvent = Json & { type: string; usage?: unknown }; +type PendingRequest = { resolve: (value: unknown) => void; reject: (error: Error) => void }; + +let codexQueue: Promise = Promise.resolve(); +let codexApp: CodexAppClient | null = null; +let codexThreadId = ""; +const canvasAgentMcp = canvasAgentMcpCommand(); +const require = createRequire(import.meta.url); + +export function withAgentPrompt(prompt: string) { + return prompt.trim() ? `${AGENT_PROMPT}\n\n用户请求:${prompt}` : ""; +} + +export async function runCodexTurn(prompt: string, emit: AgentEmit, attachments: AgentAttachment[] = []) { + if (!prompt.trim()) return; + codexQueue = codexQueue.catch(() => undefined).then(() => runCodexTurnNow(prompt, emit, attachments)); + await codexQueue; +} + +async function runCodexTurnNow(prompt: string, emit: AgentEmit, attachments: AgentAttachment[]) { + let files: string[] = []; + try { + files = await writeAttachmentFiles(attachments); + codexApp ||= await CodexAppClient.start(emit); + codexThreadId ||= await codexApp.startThread(); + await codexApp.startTurn(codexThreadId, prompt, files); + } catch (error) { + emit("agent_error", { message: errorMessage(error) }); + } finally { + await Promise.all(files.map((file) => fs.unlink(file).catch(() => undefined))); + } +} + +export function runClaudeTurn(prompt: string, emit: AgentEmit) { + if (!prompt.trim()) return; + const child = spawnAgent("claude", ["-p", "--output-format", "stream-json", "--verbose", "--include-partial-messages", "--allowedTools", "mcp__infinite-canvas__*", prompt], ["ignore", "pipe", "pipe"], emit); + if (!child) return; + pipeJsonLines(child, emit, "claude"); +} + +class CodexAppClient { + private nextId = 1; + private buffer = ""; + private textByItem = new Map(); + private deltaCount = 0; + private lastUsage: unknown = null; + private pending = new Map(); + private activeTurns = new Map(); + private completedTurns = new Map(); + + private constructor(private child: ChildProcess, private emit: AgentEmit) {} + + static async start(emit: AgentEmit) { + const child = spawn(process.execPath, [codexBin(), "app-server", "--stdio"], { stdio: ["pipe", "pipe", "pipe"], windowsHide: true }); + const client = new CodexAppClient(child, emit); + child.stdout?.on("data", (chunk) => client.read(chunk.toString())); + child.stderr?.on("data", (chunk) => emit("agent_log", { text: chunk.toString() })); + child.on("error", (error) => emit("agent_error", { message: error.message })); + child.on("exit", (code) => { + client.failAll(`Codex app-server exited: ${code ?? 0}`); + codexApp = null; + codexThreadId = ""; + emit("agent_log", { text: `Codex app-server exited: ${code ?? 0}` }); + }); + await client.request("initialize", { clientInfo: { name: "canvas-agent", title: "Infinite Canvas Agent", version: VERSION }, capabilities: { experimentalApi: true, requestAttestation: false } }); + client.notify("initialized"); + return client; + } + + async startThread() { + const result = await this.request("thread/start", { approvalPolicy: "never", sandbox: "workspace-write", config: codexConfig(), threadSource: "user" }); + const id = String(field(field(result, "thread"), "id") || ""); + if (!id) throw new Error("Codex app-server 没有返回 thread id"); + return id; + } + + async startTurn(threadId: string, prompt: string, images: string[]) { + const result = await this.request("turn/start", { threadId, input: codexInput(prompt, images), approvalPolicy: "never" }); + const turnId = String(field(field(result, "turn"), "id") || ""); + if (!turnId) throw new Error("Codex app-server 没有返回 turn id"); + const completed = this.completedTurns.get(turnId); + if (this.completedTurns.has(turnId)) { + this.completedTurns.delete(turnId); + if (completed) throw completed; + return; + } + await new Promise((resolve, reject) => this.activeTurns.set(turnId, { resolve, reject })); + } + + private request(method: string, params: unknown) { + const id = this.nextId++; + this.write({ id, method, params }); + return new Promise((resolve, reject) => this.pending.set(id, { resolve, reject })); + } + + private notify(method: string, params?: unknown) { + this.write(params === undefined ? { method } : { method, params }); + } + + private write(value: unknown) { + this.child.stdin?.write(`${JSON.stringify(value)}\n`); + } + + private read(chunk: string) { + this.buffer += chunk; + const lines = this.buffer.split(/\r?\n/); + this.buffer = lines.pop() || ""; + lines.filter(Boolean).forEach((line) => { + try { + this.handle(JSON.parse(line) as Json); + } catch { + this.emit("agent_log", { text: line }); + } + }); + } + + private handle(message: Json) { + const id = Number(message.id); + if (message.error && this.pending.has(id)) return this.reject(id, String(field(message.error, "message") || "Codex request failed")); + if (this.pending.has(id)) return this.resolve(id, message.result); + if (typeof message.method === "string" && "id" in message) return this.answerServerRequest(message); + if (typeof message.method === "string") this.handleNotification(message.method, (message.params || {}) as Json); + } + + private handleNotification(method: string, params: Json) { + if (method === "item/agentMessage/delta") return this.emitDelta(params); + if (method === "thread/tokenUsage/updated") this.lastUsage = normalizeUsage(params); + const event = normalizeCodexNotification(method, params); + if (!event) return; + if (event.type === "turn.completed") event.usage = this.lastUsage; + this.emit("agent_event", { agent: "codex", ...event }); + if (event.type === "turn.completed") { + const turnId = String(field(params, "turnId") || field(field(params, "turn"), "id") || ""); + const pending = this.activeTurns.get(turnId); + const error = field(field(params, "turn"), "error"); + if (pending) { + this.activeTurns.delete(turnId); + error ? pending.reject(new Error(String(field(error, "message") || "Codex turn failed"))) : pending.resolve(event); + } else if (turnId) { + this.completedTurns.set(turnId, error ? new Error(String(field(error, "message") || "Codex turn failed")) : null); + } + this.emit("agent_event", { agent: "codex", type: "stream.summary", delta_count: this.deltaCount }); + this.deltaCount = 0; + this.emit("agent_done", { agent: "codex", usage: event.usage }); + } + } + + private emitDelta(params: Json) { + const id = String(field(params, "itemId") || ""); + const text = `${this.textByItem.get(id) || ""}${String(field(params, "delta") || "")}`; + this.deltaCount += 1; + this.textByItem.set(id, text); + this.emit("agent_event", { agent: "codex", type: "item.updated", item: { id, type: "agent_message", text } }); + } + + private answerServerRequest(message: Json) { + const method = String(message.method); + const result = method === "mcpServer/elicitation/request" ? { action: "accept", content: {}, _meta: null } : { decision: "decline" }; + this.write({ id: message.id, result }); + this.emit("agent_event", { agent: "codex", type: "server.request", method, params: message.params, result }); + } + + private resolve(id: number, result: unknown) { + const pending = this.pending.get(id); + if (pending) (this.pending.delete(id), pending.resolve(result)); + } + + private reject(id: number, message: string) { + const pending = this.pending.get(id); + if (pending) (this.pending.delete(id), pending.reject(new Error(message))); + } + + failAll(message: string) { + [...this.pending.values(), ...this.activeTurns.values()].forEach((item) => item.reject(new Error(message))); + this.pending.clear(); + this.activeTurns.clear(); + } +} + +function canvasAgentMcpCommand() { + const current = process.argv.find((arg) => /index\.(t|j)s$/.test(arg)) || ""; + const entry = path.resolve(current || fileURLToPath(new URL("./index.js", import.meta.url))); + const tsx = path.join(path.dirname(entry), "..", "node_modules", "tsx", "dist", "cli.mjs"); + return entry.endsWith(".ts") ? { command: process.execPath, args: [tsx, entry, "mcp"] } : { command: process.execPath, args: [entry, "mcp"] }; +} + +function codexConfig() { + return { mcp_servers: { "infinite-canvas": { command: canvasAgentMcp.command, args: canvasAgentMcp.args, default_tools_approval_mode: "approve", startup_timeout_sec: 20, tool_timeout_sec: 90 } } }; +} + +function codexInput(prompt: string, images: string[]) { + return [{ type: "text", text: prompt, text_elements: [] }, ...images.map((file) => ({ type: "localImage", path: file }))]; +} + +function normalizeCodexNotification(method: string, params: Json): AgentEvent | null { + if (method === "thread/started") return { type: "thread.started", thread_id: field(field(params, "thread"), "id") }; + if (method === "turn/started") return { type: "turn.started" }; + if (method === "turn/completed") return { type: "turn.completed", usage: null }; + if (method === "item/started") return { type: "item.started", item: normalizeItem(field(params, "item")) }; + if (method === "item/completed") return { type: "item.completed", item: normalizeItem(field(params, "item")) }; + if (method === "error") return { type: "error", message: field(params, "message") }; + return null; +} + +function normalizeItem(item: unknown) { + const value = item && typeof item === "object" ? { ...(item as Json) } : {}; + if (value.type === "agentMessage") value.type = "agent_message"; + if (value.type === "mcpToolCall") value.type = "mcp_tool_call"; + if (value.type === "agent_message" && typeof value.id === "string") value.text = String(value.text || ""); + if ("arguments" in value) value.arguments = parseMaybeJson(value.arguments); + return value; +} + +function normalizeUsage(params: Json) { + const total = field(field(params, "tokenUsage"), "total") as Json | undefined; + return { + input_tokens: field(total, "inputTokens"), + cached_input_tokens: field(total, "cachedInputTokens"), + output_tokens: field(total, "outputTokens"), + reasoning_output_tokens: field(total, "reasoningOutputTokens"), + }; +} + +function parseMaybeJson(value: unknown) { + if (typeof value !== "string") return value; + try { + return JSON.parse(value); + } catch { + return value; + } +} + +function field(value: unknown, key: string) { + return value && typeof value === "object" ? (value as Json)[key] : undefined; +} + +async function writeAttachmentFiles(attachments: AgentAttachment[]) { + return await Promise.all(attachments.filter((item) => item.dataUrl?.startsWith("data:image/")).map(writeAttachmentFile)); +} + +async function writeAttachmentFile(item: AgentAttachment) { + const [, meta = "", data = ""] = item.dataUrl?.match(/^data:([^;]+);base64,(.+)$/) || []; + if (!data) throw new Error(`图片附件无效:${item.name || "未命名图片"}`); + const file = path.join(os.tmpdir(), `infinite-canvas-${Date.now()}-${Math.random().toString(16).slice(2)}.${imageExt(meta || item.type)}`); + await fs.writeFile(file, Buffer.from(data, "base64")); + return file; +} + +function imageExt(type = "") { + if (type.includes("png")) return "png"; + if (type.includes("webp")) return "webp"; + return "jpg"; +} + +function codexBin() { + return path.join(path.dirname(require.resolve("@openai/codex/package.json")), "bin", "codex.js"); +} + +function pipeJsonLines(child: ReturnType, emit: AgentEmit, agent: string) { + let out = ""; + child.stdout?.on("data", (chunk) => { + out += chunk.toString(); + const lines = out.split(/\r?\n/); + out = lines.pop() || ""; + lines.filter(Boolean).forEach((line) => { + try { + emit("agent_event", { agent, ...JSON.parse(line) }); + } catch { + emit("agent_event", { agent, type: "raw", text: line }); + } + }); + }); + child.stderr?.on("data", (chunk) => emit("agent_log", { text: chunk.toString() })); + child.on("error", (error) => emit("agent_error", { message: error.message })); + child.on("close", (code) => emit("agent_done", { agent, code })); +} + +function spawnAgent(name: string, args: string[], stdio: StdioOptions, emit: AgentEmit) { + try { + return spawn(name, args, { stdio, shell: process.platform === "win32", windowsHide: true }); + } catch (error) { + emit("agent_error", { message: errorMessage(error) }); + return null; + } +} + +function errorMessage(error: unknown) { + return error instanceof Error ? error.message : String(error); +} diff --git a/canvas-agent/src/canvas-session.ts b/canvas-agent/src/canvas-session.ts new file mode 100644 index 0000000..256c1d2 --- /dev/null +++ b/canvas-agent/src/canvas-session.ts @@ -0,0 +1,94 @@ +import crypto from "node:crypto"; +import type { ServerResponse } from "node:http"; + +import { type ToolName } from "./schemas.js"; +import { compactCanvasState, compactNode, isToolName, nextCanvasX, parseToolInput } from "./tools.js"; +import type { CanvasSnapshot } from "./types.js"; + +type PendingRequest = { resolve: (value: unknown) => void; reject: (error: Error) => void }; + +export class CanvasSession { + private clients = new Map(); + private pending = new Map(); + private canvasState: CanvasSnapshot | null = null; + + health() { + return { ok: true, hasCanvas: Boolean(this.canvasState), clients: this.clients.size }; + } + + openEvents(url: URL, res: ServerResponse) { + const clientId = url.searchParams.get("clientId") || crypto.randomUUID(); + res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" }); + this.clients.set(clientId, res); + sendEvent(res, "hello", { ok: true, clientId }); + const timer = setInterval(() => sendEvent(res, "ping", { time: Date.now() }), 15000); + res.on("close", () => { + clearInterval(timer); + this.clients.delete(clientId); + if (this.canvasState?.clientId === clientId) this.canvasState = null; + }); + } + + updateState(body: unknown, clientId?: string) { + this.canvasState = { ...((body && typeof body === "object" && !Array.isArray(body) ? body : {}) as Record), clientId } as CanvasSnapshot; + } + + resolveResult(body: { requestId?: string; error?: string; result?: unknown }) { + const item = body.requestId ? this.pending.get(body.requestId) : null; + if (!item || !body.requestId) return; + this.pending.delete(body.requestId); + body.error ? item.reject(new Error(body.error)) : item.resolve(body.result); + } + + emitAll(type: string, payload: unknown) { + this.clients.forEach((client) => sendEvent(client, type, payload)); + } + + async callTool(name: unknown, rawInput: unknown) { + if (!isToolName(name)) throw new Error(`未知工具:${String(name)}`); + let tool: ToolName = name; + let input = parseToolInput(tool, rawInput) as Record; + const readTool = ["canvas_get_state", "canvas_get_selection", "canvas_export_snapshot"].includes(tool); + if (readTool && (!this.clients.size || !this.canvasState)) throw new Error("当前没有已连接画布"); + if (tool === "canvas_get_state" || tool === "canvas_export_snapshot") return compactCanvasState(this.canvasState); + if (tool === "canvas_get_selection") { + const ids = new Set(this.canvasState?.selectedNodeIds || []); + return { nodes: (this.canvasState?.nodes || []).filter((node) => ids.has(node.id)).map(compactNode) }; + } + if (tool === "canvas_create_text_node") { + const text = input as { text?: string; x?: number; y?: number; title?: string }; + input = { ops: [{ type: "add_node", nodeType: "text", title: text.title, position: { x: text.x ?? 0, y: text.y ?? 0 }, metadata: { content: text.text || "" } }] }; + tool = "canvas_apply_ops"; + } + if (tool === "canvas_create_image_prompt_flow") { + const flow = input as { prompt: string; x?: number; y?: number }; + const x = Number(flow.x ?? nextCanvasX(this.canvasState)); + const y = Number(flow.y ?? 0); + const textId = `text-${crypto.randomUUID()}`; + const configId = `config-${crypto.randomUUID()}`; + input = { ops: [{ type: "add_node", id: textId, nodeType: "text", title: "提示词", position: { x, y }, metadata: { content: flow.prompt } }, { type: "add_node", id: configId, nodeType: "config", title: "图片生成", position: { x: x + 420, y }, metadata: { generationMode: "image", composerContent: flow.prompt } }, { type: "connect_nodes", fromNodeId: textId, toNodeId: configId }, { type: "select_nodes", ids: [configId] }] }; + tool = "canvas_apply_ops"; + } + if (tool !== "canvas_apply_ops") throw new Error(`未知工具:${tool}`); + if (!this.clients.size) throw new Error("当前没有已连接画布"); + return await this.requestCanvasTool(tool, input); + } + + private async requestCanvasTool(name: ToolName, input: Record) { + const requestId = crypto.randomUUID(); + const client = this.clients.get(this.canvasState?.clientId || "") || this.clients.values().next().value; + if (!client) throw new Error("当前没有已连接画布"); + sendEvent(client, "tool_call", { requestId, name, input }); + return await new Promise((resolve, reject) => { + const timer = setTimeout(() => { + this.pending.delete(requestId); + reject(new Error("画布操作超时")); + }, 30000); + this.pending.set(requestId, { resolve: (value) => (clearTimeout(timer), resolve(value)), reject: (error) => (clearTimeout(timer), reject(error)) }); + }); + } +} + +function sendEvent(res: ServerResponse, type: string, payload: unknown) { + res.write(`event: ${type}\ndata: ${JSON.stringify(payload)}\n\n`); +} diff --git a/canvas-agent/src/config.ts b/canvas-agent/src/config.ts new file mode 100644 index 0000000..256ec1d --- /dev/null +++ b/canvas-agent/src/config.ts @@ -0,0 +1,27 @@ +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +export const DEFAULT_PORT = 17371; +export const CONFIG_DIR = path.join(os.homedir(), ".infinite-canvas"); +export const CONFIG_FILE = path.join(CONFIG_DIR, "canvas-agent.json"); +export const VERSION = "0.1.0"; +export const AGENT_PROMPT = "你正在帮助用户操作 Infinite Canvas 网页画布。需要改动画布时优先使用已配置的 infinite-canvas MCP 工具:先 canvas_get_state 读取当前画布,再用 canvas_apply_ops 提交 JSON 操作。不要模拟鼠标点击,不要要求用户手动复制 JSON。"; + +export type CanvasAgentConfig = { url: string; token: string; origins?: string[] }; + +export function loadConfig(create = false): CanvasAgentConfig { + try { + return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8")) as CanvasAgentConfig; + } catch { + const config = { url: `http://127.0.0.1:${Number(process.env.PORT) || DEFAULT_PORT}`, token: crypto.randomBytes(18).toString("hex") }; + if (create) saveConfig(config); + return config; + } +} + +export function saveConfig(config: CanvasAgentConfig) { + fs.mkdirSync(CONFIG_DIR, { recursive: true }); + fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2)); +} diff --git a/canvas-agent/src/http-server.ts b/canvas-agent/src/http-server.ts new file mode 100644 index 0000000..3b82fa4 --- /dev/null +++ b/canvas-agent/src/http-server.ts @@ -0,0 +1,88 @@ +import express, { type NextFunction, type Request, type Response } from "express"; + +import { DEFAULT_PORT, loadConfig, saveConfig, type CanvasAgentConfig } from "./config.js"; +import { CanvasSession } from "./canvas-session.js"; +import { runClaudeTurn, runCodexTurn, withAgentPrompt } from "./agents.js"; +import type { AgentAttachment } from "./types.js"; + +export function startHttpServer() { + const config = loadConfig(true); + const port = Number(process.env.PORT) || Number(new URL(config.url).port) || DEFAULT_PORT; + config.url = `http://127.0.0.1:${port}`; + saveConfig(config); + + const session = new CanvasSession(); + const emit = (type: string, payload: unknown) => session.emitAll(type, payload); + const app = express(); + app.disable("x-powered-by"); + app.use(express.json({ limit: "30mb" })); + app.use((req, res, next) => { + const url = requestUrl(req, config); + if (!setCors(req, res, url, config)) return void res.status(403).json({ ok: false, error: "origin not allowed" }); + if (req.method === "OPTIONS") return void res.json({}); + next(); + }); + app.get("/health", (_req, res) => res.json(session.health())); + app.get("/config", (_req, res) => res.json({ ok: true, url: config.url, hasToken: true })); + app.use((req, res, next) => { + if (validToken(req, requestUrl(req, config), config.token)) return next(); + res.status(401).json({ ok: false, error: "invalid token" }); + }); + app.get("/events", (req, res) => session.openEvents(requestUrl(req, config), res)); + app.post("/canvas/state", (req, res) => { + session.updateState(req.body, String(req.query.clientId || "") || undefined); + res.json({ ok: true }); + }); + app.post("/canvas/result", (req, res) => { + session.resolveResult(req.body); + res.json({ ok: true }); + }); + app.post("/api/tools", route(async (req, res) => res.json({ ok: true, result: await session.callTool(req.body?.name, req.body?.input || {}) }))); + app.post("/agent/codex/turn", route(async (req, res) => { + const attachments = Array.isArray(req.body?.attachments) ? (req.body.attachments as AgentAttachment[]) : []; + void runCodexTurn(withAgentPrompt(String(req.body?.prompt || "")), emit, attachments); + res.json({ ok: true }); + })); + app.post("/agent/claude/turn", (req, res) => { + runClaudeTurn(withAgentPrompt(String(req.body?.prompt || "")), emit); + res.json({ ok: true }); + }); + app.use((_req, res) => res.status(404).json({ ok: false, error: "not found" })); + app.use((error: Error, _req: Request, res: Response, _next: NextFunction) => res.status(500).json({ ok: false, error: error.message })); + + app.listen(port, "127.0.0.1", () => { + console.log("Infinite Canvas Agent"); + console.log(`Local URL: ${config.url}`); + console.log(`Connect token: ${config.token}`); + console.log("Codex MCP: codex mcp add infinite-canvas -- npx -y canvas-agent mcp"); + }); +} + +function route(handler: (req: Request, res: Response) => Promise) { + return (req: Request, res: Response, next: NextFunction) => void handler(req, res).catch(next); +} + +function requestUrl(req: Request, config: CanvasAgentConfig) { + return new URL(req.originalUrl || req.url || "/", config.url); +} + +function setCors(req: Request, res: Response, url: URL, config: CanvasAgentConfig) { + const origin = req.headers.origin; + res.setHeader("Access-Control-Allow-Origin", origin || "*"); + res.setHeader("Access-Control-Allow-Headers", "content-type,x-canvas-agent-token"); + res.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS"); + res.setHeader("Access-Control-Allow-Private-Network", "true"); + if (!origin || req.method === "OPTIONS" || url.pathname === "/health" || url.pathname === "/config") return true; + config.origins ||= []; + if (validToken(req, url, config.token) && !config.origins.includes(origin)) { + config.origins.push(origin); + saveConfig(config); + } + res.setHeader("Vary", "Origin"); + return config.origins.includes(origin); +} + +function validToken(req: Request, url: URL, token: string) { + const header = req.headers["x-canvas-agent-token"]; + return url.searchParams.get("token") === token || header === token || (Array.isArray(header) && header.includes(token)); +} diff --git a/canvas-agent/src/index.ts b/canvas-agent/src/index.ts new file mode 100644 index 0000000..826f7cc --- /dev/null +++ b/canvas-agent/src/index.ts @@ -0,0 +1,6 @@ +#!/usr/bin/env node +import { startHttpServer } from "./http-server.js"; +import { startMcpServer } from "./mcp-server.js"; + +if (process.argv[2] === "mcp") await startMcpServer(); +else startHttpServer(); diff --git a/canvas-agent/src/mcp-server.ts b/canvas-agent/src/mcp-server.ts new file mode 100644 index 0000000..3e7aaf7 --- /dev/null +++ b/canvas-agent/src/mcp-server.ts @@ -0,0 +1,29 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; + +import { AGENT_PROMPT, loadConfig, type CanvasAgentConfig, VERSION } from "./config.js"; +import { toolDescriptions, toolInputSchemas, toolNames, type ToolName } from "./schemas.js"; + +type CanvasAgentToolResponse = { ok?: boolean; result?: unknown; error?: string }; + +export async function startMcpServer() { + const config = loadConfig(true); + const server = new McpServer({ name: "canvas-agent", version: VERSION }, { instructions: AGENT_PROMPT }); + toolNames.forEach((name) => registerCanvasTool(server, config, name)); + await server.connect(new StdioServerTransport()); +} + +function registerCanvasTool(server: McpServer, config: CanvasAgentConfig, name: ToolName) { + const schema = toolInputSchemas[name]; + server.registerTool(name, { description: toolDescriptions[name], inputSchema: schema.shape }, async (input: unknown) => { + const result = await postCanvasAgentTool(config, name, schema.parse(input)); + return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] }; + }); +} + +async function postCanvasAgentTool(config: CanvasAgentConfig, name: ToolName, input: unknown) { + const res = await fetch(`${config.url}/api/tools`, { method: "POST", headers: { "content-type": "application/json", "x-canvas-agent-token": config.token }, body: JSON.stringify({ name, input }) }); + const body = (await res.json()) as CanvasAgentToolResponse; + if (!body.ok) throw new Error(body.error || "tool call failed"); + return body.result; +} diff --git a/canvas-agent/src/schemas.ts b/canvas-agent/src/schemas.ts new file mode 100644 index 0000000..471f625 --- /dev/null +++ b/canvas-agent/src/schemas.ts @@ -0,0 +1,35 @@ +import { z } from "zod"; + +const recordSchema = z.record(z.unknown()); +const positionSchema = z.object({ x: z.number(), y: z.number() }); +const viewportSchema = z.object({ x: z.number(), y: z.number(), k: z.number() }); + +export const toolNames = ["canvas_get_state", "canvas_get_selection", "canvas_export_snapshot", "canvas_apply_ops", "canvas_create_text_node", "canvas_create_image_prompt_flow"] as const; +export type ToolName = (typeof toolNames)[number]; + +export const canvasOpSchema = z.discriminatedUnion("type", [ + z.object({ type: z.literal("add_node"), nodeType: z.enum(["image", "text", "config", "video", "audio"]).optional(), id: z.string().optional(), title: z.string().optional(), x: z.number().optional(), y: z.number().optional(), width: z.number().optional(), height: z.number().optional(), position: positionSchema.optional(), metadata: recordSchema.optional() }).passthrough(), + z.object({ type: z.literal("update_node"), id: z.string(), patch: recordSchema.optional(), metadata: recordSchema.optional() }).passthrough(), + z.object({ type: z.literal("delete_node"), id: z.string().optional(), ids: z.array(z.string()).optional() }).passthrough(), + z.object({ type: z.literal("connect_nodes"), id: z.string().optional(), fromNodeId: z.string(), toNodeId: z.string() }).passthrough(), + z.object({ type: z.literal("set_viewport"), viewport: viewportSchema }).passthrough(), + z.object({ type: z.literal("select_nodes"), ids: z.array(z.string()) }).passthrough(), +]); + +export const toolInputSchemas = { + canvas_get_state: z.object({}).passthrough(), + canvas_get_selection: z.object({}).passthrough(), + canvas_export_snapshot: z.object({}).passthrough(), + canvas_apply_ops: z.object({ ops: z.array(canvasOpSchema) }), + canvas_create_text_node: z.object({ text: z.string().optional(), x: z.number().optional(), y: z.number().optional(), title: z.string().optional() }), + canvas_create_image_prompt_flow: z.object({ prompt: z.string(), x: z.number().optional(), y: z.number().optional() }), +} satisfies Record; + +export const toolDescriptions: Record = { + canvas_get_state: "读取当前网页画布的节点、连线、选区和视口。", + canvas_get_selection: "读取当前网页画布选中的节点。", + canvas_export_snapshot: "导出当前画布快照,用于理解布局。", + canvas_apply_ops: "批量操作当前网页画布。ops 支持 add_node、update_node、delete_node、connect_nodes、set_viewport、select_nodes。", + canvas_create_text_node: "在当前画布创建文本节点。", + canvas_create_image_prompt_flow: "创建提示词文本节点和图片生成配置节点,并自动连线。", +}; diff --git a/canvas-agent/src/tools.ts b/canvas-agent/src/tools.ts new file mode 100644 index 0000000..1f5539d --- /dev/null +++ b/canvas-agent/src/tools.ts @@ -0,0 +1,26 @@ +import { toolInputSchemas, toolNames, type ToolName } from "./schemas.js"; +import type { CanvasNode, CanvasSnapshot } from "./types.js"; + +export function isToolName(name: unknown): name is ToolName { + return typeof name === "string" && toolNames.includes(name as ToolName); +} + +export function parseToolInput(name: ToolName, input: unknown) { + return toolInputSchemas[name].parse(input ?? {}); +} + +export function compactCanvasState(state: CanvasSnapshot | null) { + if (!state) throw new Error("当前没有已连接画布"); + return { ...state, nodes: (state.nodes || []).map(compactNode) }; +} + +export function compactNode(node: CanvasNode) { + const metadata = { ...(node.metadata || {}) }; + if (typeof metadata.content === "string" && metadata.content.length > 240) metadata.content = `${metadata.content.slice(0, 120)}...`; + return { id: node.id, type: node.type, title: node.title, position: node.position, width: node.width, height: node.height, metadata }; +} + +export function nextCanvasX(state: CanvasSnapshot | null) { + const nodes = state?.nodes || []; + return nodes.length ? Math.max(...nodes.map((node) => node.position.x + node.width)) + 80 : 0; +} diff --git a/canvas-agent/src/types.ts b/canvas-agent/src/types.ts new file mode 100644 index 0000000..d513918 --- /dev/null +++ b/canvas-agent/src/types.ts @@ -0,0 +1,8 @@ +export type Position = { x: number; y: number }; +export type Viewport = { x: number; y: number; k: number }; +export type CanvasNodeType = "image" | "text" | "config" | "video" | "audio"; +export type CanvasNode = { id: string; type: CanvasNodeType; title?: string; position: Position; width: number; height: number; metadata?: Record }; +export type CanvasConnection = { id: string; fromNodeId: string; toNodeId: string }; +export type CanvasSnapshot = { projectId?: string; title?: string; nodes?: CanvasNode[]; connections?: CanvasConnection[]; selectedNodeIds?: string[]; viewport?: Viewport; clientId?: string }; +export type AgentEmit = (type: string, payload: unknown) => void; +export type AgentAttachment = { name?: string; type?: string; dataUrl?: string }; diff --git a/canvas-agent/tsconfig.json b/canvas-agent/tsconfig.json new file mode 100644 index 0000000..543a8e2 --- /dev/null +++ b/canvas-agent/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": true, + "declaration": true, + "outDir": "dist", + "rootDir": "src", + "skipLibCheck": true + }, + "include": ["src/**/*.ts"] +} diff --git a/docs/content/docs/overview/meta.json b/docs/content/docs/overview/meta.json index df2a1fe..b5e61c5 100644 --- a/docs/content/docs/overview/meta.json +++ b/docs/content/docs/overview/meta.json @@ -8,6 +8,6 @@ "render", "docker", "third-party-prompt-repositories", - "[在线体验](https://infinite-canvas-cpco.onrender.com/)" + "[在线体验](https://canvas.best/)" ] } diff --git a/docs/content/docs/progress/local-agent-integration-plan.mdx b/docs/content/docs/progress/local-agent-integration-plan.mdx new file mode 100644 index 0000000..864c872 --- /dev/null +++ b/docs/content/docs/progress/local-agent-integration-plan.mdx @@ -0,0 +1,248 @@ +--- +title: 本地 Agent 接入规划 +description: 规划通过本地 Canvas Agent、MCP 和侧边栏助手连接 Codex / Claude Code 操作画布 +--- + +# 本地 Agent 接入规划 + +本文档规划个人用户在访问线上画布网页时,如何连接自己电脑上的 Codex / Claude Code,并让 Agent 通过对话和工具调用操作当前画布。 + +## 目标 + +- 用户打开线上画布后,可以启动本机服务连接当前画布。 +- 用户可以在 Codex 终端里通过 MCP 操作画布。 +- 用户也可以在网页侧边栏里和本地 Codex / Claude Code 对话。 +- 侧边栏需要展示普通消息、流式输出、工具调用、工具结果和错误提示。 +- 线上服务不保存用户本地 Codex / Claude Code 登录态、API Key 或本地文件权限。 + +## 核心结论 + +浏览器页面不能直接启动本地进程,也不应该直接控制本机 Codex / Claude Code。推荐增加一个用户本机运行的 `canvas-agent` 服务: + +```txt +线上画布网页 <-> 本机 canvas-agent <-> Codex / Claude Code + | + +-> MCP Server + +-> 画布连接 +``` + +`canvas-agent` 是本地可信边界,负责启动或连接 Codex / Claude Code、暴露 MCP 工具、维护画布连接、转发侧边栏对话事件。 + +## 推荐目录 + +后续实现时建议在项目根目录新增独立 npm 包: + +```txt +canvas-agent/ + package.json + tsconfig.json + src/ + index.ts + config.ts + http-server.ts + canvas-session.ts + mcp-server.ts + agents.ts + schemas.ts + tools.ts + types.ts +``` + +原因: + +- 这是用户本机运行的 Node 服务,不属于线上 Go 后端,也不属于纯前端页面。 +- 后续可以发布成 npm 包,用户通过 `npx` 或全局安装启动。 +- Codex SDK、Codex MCP、Claude SDK 都更适合在本地 Node 进程中接入。 +- HTTP 路由使用 Express,MCP 协议层使用官方 `@modelcontextprotocol/sdk`,工具入参使用 `zod`,避免手写协议和松散 JSON。 + +## 本机启动方式 + +MVP 阶段优先提供 `npx`: + +```bash +npx -y canvas-agent +``` + +在本仓库内开发调试时可直接运行: + +```bash +cd canvas-agent +npm install +npm run build +node dist/index.js +``` + +启动后输出: + +```txt +Infinite Canvas Agent +Local URL: http://127.0.0.1:17371 +Connect token: xxxxxx +``` + +网页侧边栏填写或自动发现 `http://127.0.0.1:17371` 和 token 后连接本机服务。 + +## 前端需要增加的能力 + +前端不直接暴露 HTTP 接口给本机服务,优先由网页主动连接本机 Canvas Agent。当前 MVP 使用 SSE 接收本机事件,HTTP POST 上报画布状态和工具结果: + +```txt +网页 -> http://127.0.0.1:17371/events?token=xxx +网页 -> http://127.0.0.1:17371/canvas/state?token=xxx +网页 -> http://127.0.0.1:17371/canvas/result?token=xxx +``` + +前端需要提供一个画布 Agent 控制层,内部调用现有 store / hook,不直接让外部操作 React 组件: + +```ts +canvasAgent.getState() +canvasAgent.getSelection() +canvasAgent.applyOps(ops) +canvasAgent.focusNodes(ids) +canvasAgent.exportSnapshot() +``` + +建议先支持最小操作集: + +- `add_node`:新增图片、文本、音频、视频、生成配置节点。 +- `update_node`:更新节点位置、尺寸、内容和配置。 +- `delete_node`:删除节点。 +- `connect_nodes`:连接两个节点。 +- `set_viewport`:移动或缩放当前视口。 +- `select_nodes`:选中节点。 + +工具入参应使用画布业务 JSON,不使用模拟鼠标点击或屏幕坐标自动化。 + +MCP 读取画布状态时默认返回摘要,不直接把完整图片、视频、音频或超长 base64 内容塞给 Agent。 + +## MCP 工具设计 + +`canvas-agent` 内置 MCP Server,让 Codex CLI 可以连接: + +```txt +Codex CLI <-> canvas-agent MCP <-> 当前网页画布 +``` + +建议首批 MCP 工具: + +- `canvas_get_state`:读取当前画布节点、连线、选区和视口摘要。 +- `canvas_apply_ops`:批量执行画布操作。 +- `canvas_get_selection`:读取当前选中的节点。 +- `canvas_export_snapshot`:导出当前画布快照,用于让 Agent 理解布局。 +- `canvas_create_text_node`:快捷创建文本节点。 +- `canvas_create_image_prompt_flow`:快捷创建提示词文本节点和图片生成配置节点。 + +用户本机 Codex 配置示例: + +```bash +codex mcp add infinite-canvas -- npx -y canvas-agent mcp +``` + +本仓库调试时可使用,实际配置建议替换为本机绝对路径: + +```bash +codex mcp add infinite-canvas -- node /path/to/infinite-canvas/canvas-agent/dist/index.js mcp +``` + +网页侧边栏助手另走 Canvas Agent 的对话通道。Canvas Agent 使用官方 `@openai/codex` CLI 的 `codex app-server --stdio` 启动并续用同一个 thread,启动时会注入 `infinite-canvas` MCP 配置并把 `default_tools_approval_mode` 设为 `approve`,避免 Codex 自己的 MCP 审批卡住侧边栏;真正修改画布前由网页侧边栏做二次确认。 + +侧边栏图片附件通过 HTTP 发送到本机 Canvas Agent,Canvas Agent 临时写入本机文件后作为 app-server `localImage` 输入传给 Codex;MVP 会在输入区提示附件体积,单次请求体限制为 30MB。 + +## 侧边栏助手设计 + +侧边栏助手连接 `canvas-agent` 后,由 Canvas Agent 选择 Agent Adapter: + +```txt +Sidebar -> canvas-agent -> Codex app-server stdio +Sidebar -> canvas-agent -> Claude Code CLI / Claude Agent SDK +``` + +侧边栏优先展示 Codex app-server 原生结构化事件。前端只消费必要事件: + +```ts +type AgentEvent = + | { type: "thread.started"; thread_id: string } + | { type: "turn.started" } + | { type: "item.started" | "item.updated" | "item.completed"; item: ThreadItem } + | { type: "turn.completed"; usage: Usage } + | { type: "turn.failed"; error: { message: string } }; +``` + +Claude 后续接入时再单独做 Claude Adapter;当前前端默认只展示 Codex。 + +## Codex 接入优先级 + +优先使用官方 `@openai/codex` CLI 的 `codex app-server --stdio`。 + +- Codex app-server 会输出 `item/agentMessage/delta`;Canvas Agent 转成 `item.updated` 后,前端用同一条消息做真实流式渲染。 +- 图片附件使用 app-server `localImage` 输入,不手写多模态协议。 + +参考: + +- https://github.com/openai/codex/tree/main/codex-rs/app-server +- https://developers.openai.com/codex/codex-manual.md + +## Claude Code 接入优先级 + +Claude Code 侧当前 MVP 先调用本机 Claude Code CLI 的流式 JSON 输出,由 `canvas-agent` 统一转换事件和工具调用;后续可升级为 Claude Agent SDK。 + +如果希望 Claude Code 也能操作当前画布,需要给 Claude Code 配置同一个 MCP: + +```bash +claude mcp add --scope user --transport stdio infinite-canvas -- npx -y canvas-agent mcp +``` + +Canvas Agent 调用 Claude Code 时默认允许 `mcp__infinite-canvas__*`,避免 print 模式被工具审批卡住;真正修改画布仍由网页侧边栏确认。 + +参考: + +- https://docs.anthropic.com/en/docs/claude-code/sdk +- https://code.claude.com/docs/en/agent-sdk/typescript + +## 安全边界 + +- Canvas Agent 默认只监听 `127.0.0.1`。 +- Canvas Agent 启动时生成 token,网页连接必须携带 token。 +- Canvas Agent 只接受允许的 Origin,默认允许用户当前打开的画布域名。 +- 画布操作默认先在侧边栏展示工具调用,`canvas_apply_ops` 默认需要用户确认后执行,并保留最近一次工具操作撤销入口。 +- 不把 Codex / Claude 登录态、API Key、用户本地文件路径上传到线上服务。 +- 线上网页只保存 Canvas Agent 地址和必要偏好,不保存本地密钥。 + +## 实现阶段 + +### 第一阶段:Codex 终端操作画布 + +1. 新增 `canvas-agent/` npm 包。 +2. Canvas Agent 提供 SSE/HTTP 连接,网页连接并注册当前画布。 +3. 前端新增 `canvasAgent` 控制层。 +4. Canvas Agent 内置 MCP Server。 +5. Codex 通过 MCP 调用 `canvas_get_state` 和 `canvas_apply_ops`。 + +### 第二阶段:网页侧边栏连接 Codex + +1. 前端新增本地 Agent 侧边栏。 +2. Canvas Agent 通过官方 `@openai/codex` CLI 的 `codex app-server --stdio` 接入 Codex,复用同一个 thread,注入 `infinite-canvas` MCP,并展示结构化事件流。 +3. 运行日志只保留关键 Codex 事件,避免把流式中间更新刷满日志。 +4. 侧边栏展示消息流、工具调用、工具结果、错误和图片附件大小提示。 + +### 第三阶段:接入 Claude Code + +1. Canvas Agent 新增 Claude Code CLI Adapter。 +2. 统一 Claude Code 的消息和工具调用事件。 +3. 前端侧边栏增加 Agent 类型选择。 + +### 第四阶段:体验完善 + +1. 增加本机连接状态、重连和 token 更新。 +2. 优化工具调用确认、撤销和批量工具队列体验。 +3. 增加常用画布动作模板。 +4. 增加 npm 发布和用户安装文档。 + +## MVP 验收标准 + +- 用户运行 `npx -y canvas-agent` 后,线上画布能显示已连接。 +- 用户在 Codex CLI 中可以创建文本节点、移动节点、连接节点。 +- 网页侧边栏能发送一条消息给本地 Codex,并展示流式回复。 +- 网页侧边栏默认只展示本地 Codex,并展示结构化回复。 +- 侧边栏能展示一次 `canvas_apply_ops` 工具调用和结果。 +- 断开 Canvas Agent 后,网页能显示清晰的本机连接失败提示。 diff --git a/docs/content/docs/progress/meta.json b/docs/content/docs/progress/meta.json index f1254f7..84b1325 100644 --- a/docs/content/docs/progress/meta.json +++ b/docs/content/docs/progress/meta.json @@ -4,6 +4,7 @@ "defaultOpen": true, "pages": [ "[更新日志](/docs/progress/changelog)", + "local-agent-integration-plan", "pending-test", "todo" ] diff --git a/docs/content/docs/progress/pending-test.mdx b/docs/content/docs/progress/pending-test.mdx index 7151092..dd5c39a 100644 --- a/docs/content/docs/progress/pending-test.mdx +++ b/docs/content/docs/progress/pending-test.mdx @@ -5,6 +5,7 @@ description: 当前版本已实现但仍需人工验证的变更项 # 待测试 +- 本地 Agent 接入 MVP:新增 `canvas-agent/` 本机 Node 服务,源码使用 TypeScript,HTTP 路由使用 Express,MCP 协议层使用官方 `@modelcontextprotocol/sdk`,工具入参使用 `zod`;支持仓库内 `cd canvas-agent && npm install && npm run build && node dist/index.js` 或发布后 `npx -y canvas-agent` 启动并输出本机地址和 token;根 README 已增加本地 Canvas Agent 入口;Canvas Agent 默认监听 `127.0.0.1`,使用 token 连接并记录允许的网页 Origin;画布右上角新增 `Agent` 面板,默认只展示 Codex,可拖拽调整宽度,并与右侧画布助手一致支持打开/收起宽度动画;Agent 面板首次打开后收起不卸载,网页侧对话、日志、连接配置、等待状态和待确认工具调用会放在 Zustand 内存 store 中保留,暂不持久化且暂不读取 Codex 本地历史;以对话气泡展示用户消息、Codex 文本、工具调用和错误,前端按 Codex `type/item/usage` 事件格式读取 `agent_message`、工具结果和 `turn.completed` 状态,收到真实 `item.updated` 时用同一个消息 ID 流式更新 assistant 文本,未收到 `item.updated` 时不做伪流式,原始事件 JSON 收进独立“运行日志”弹窗,可切换排查日志和原始 JSON,排查日志会合并本地 Agent 地址、连接状态、等待状态、pending tool、发送/接收关键节点,支持复制排查日志和最近错误,`working...` 旁可直接打开日志;连接成功和本轮结束不再写入对话区,状态留在面板头部,运行日志不再记录 item.updated 和 reasoning 噪声事件;对话区去掉单独背景色并融入面板背景,消息内容裸文本展示,不再显示 `Codex`/`You` 名称,Codex 消息保留透明背景 OpenAI 图标,用户消息保留透明背景用户头像或默认用户图标,回复前显示 `working...` 等待状态;工具调用和授权状态改为对话流里的轻量小卡片,`canvas_apply_ops` 在执行前通过内联确认卡片让用户取消或执行,工具卡片会合并执行结果并可展开查看输入、输出和授权详情;底部输入区改为一体式对话框,图片上传入口和发送按钮都在框内,支持选择或粘贴图片、显示附件体积、发送前限制约 30MB,并在用户消息中显示缩略图,图片会通过本机 Canvas Agent 临时写入文件后作为 app-server `localImage` 输入传给 Codex,Canvas Agent 单次请求体限制为 30MB;Canvas Agent 使用官方 `@openai/codex` CLI 的 `codex app-server --stdio` 事件流,复用同一个 Codex thread,并在 app-server 配置中注入 `infinite-canvas` MCP 和 `default_tools_approval_mode = "approve"`,侧边栏无需用户预先全局 `codex mcp add` 也可调用画布工具;Claude Code Adapter 代码暂保留但前端入口先隐藏;画布可响应 `canvas_apply_ops` 工具调用来新增/更新/删除/连接节点、选中节点和调整视口,执行后可撤销最近一次工具操作;Canvas Agent 内置 MCP stdio 模式并向 Codex 返回画布工具使用说明,Codex 终端仍可通过 `codex mcp add infinite-canvas -- node /path/to/infinite-canvas/canvas-agent/dist/index.js mcp` 或发布后的 `codex mcp add infinite-canvas -- npx -y canvas-agent mcp` 调用 `canvas_get_state`、`canvas_get_selection`、`canvas_export_snapshot`、`canvas_apply_ops`、`canvas_create_text_node` 和 `canvas_create_image_prompt_flow`,Claude Code 可通过 `claude mcp add --scope user --transport stdio infinite-canvas -- npx -y canvas-agent mcp` 接入同一组工具;需要验证浏览器连接、侧边栏自动注入 MCP 后的 Codex 工具调用、终端 Codex MCP 工具调用、工具确认/取消/撤销、Codex thread 复用、运行日志弹窗复制、图片附件预览和断开重连提示。 - 视频创作台创建视频任务成功后会立即写入左侧生成记录,状态显示为“生成中”;刷新页面后会读取本地任务 ID 继续轮询,任务成功或失败后更新同一条记录,需要验证 OpenAI 视频接口和 Seedance 任务接口的刷新恢复。 - 修复画布文字节点编辑时双击进入编辑、全选文本出现重影和错位的问题;文字节点内容编辑框改为使用原生 textarea 文本渲染,仍保留 `@` 资源候选插入,需要验证编辑态选区、展示态文字换行和右上角“生图”按钮避让都正常。 - 配置弹窗新增 WebDAV 同步配置,可填写 WebDAV 地址、远程目录、用户名和密码/应用密码,并选择“前端直连”或“Next.js 转发”;远端会按 `canvas/`、`assets/`、`image-workbench/`、`video-workbench/` 四个业务目录分别写入 `manifest.json` 清单和 `files/` 媒体目录,会合并画布项目、我的素材、生图/视频生成记录和引用到的本地媒体文件,四个业务分区会并发同步,并在同步中显示读取远端、检查媒体、上传新增媒体、上传清单等阶段和文件计数进度条;WebDAV 请求会做超时提示,目录已存在但 `MKCOL` 返回 `423 Locked` 时会复查目录存在后继续同步,需要在支持 CORS 的 NAS/WebDAV 服务和不支持 CORS 的 Koofr 转发模式下验证首次上传、第二台设备拉取合并、再次同步和认证失败提示。 diff --git a/docs/content/docs/progress/todo.mdx b/docs/content/docs/progress/todo.mdx index 9327723..658fcb6 100644 --- a/docs/content/docs/progress/todo.mdx +++ b/docs/content/docs/progress/todo.mdx @@ -6,3 +6,5 @@ description: 当前项目后续值得处理的事项 # TODO 本文档用来记录当前项目后续比较值得处理的事项。 + +- 本地 Agent 接入后续:按[本地 Agent 接入规划](/docs/progress/local-agent-integration-plan)把 Claude Code CLI Adapter 升级为 Claude Agent SDK Adapter,完善 Codex SDK thread 恢复入口、工具队列体验和正式 npm 发布流程。 diff --git a/docs/src/app/(home)/page.tsx b/docs/src/app/(home)/page.tsx index 211e2f0..1dfd11b 100644 --- a/docs/src/app/(home)/page.tsx +++ b/docs/src/app/(home)/page.tsx @@ -3,7 +3,7 @@ import { ArrowUpRight, BookOpen, Rocket } from 'lucide-react'; import { appName, gitConfig } from '@/lib/shared'; const githubUrl = `https://github.com/${gitConfig.user}/${gitConfig.repo}`; -const demoUrl = 'https://infinite-canvas-cpco.onrender.com/'; +const demoUrl = 'https://canvas.best/'; const starHistoryUrl = `https://www.star-history.com/?repos=${gitConfig.user}%2F${gitConfig.repo}&type=date`; const starHistoryChart = `https://api.star-history.com/chart?repos=${gitConfig.user}/${gitConfig.repo}&type=date&transparent=true`; const darkStarHistoryChart = `${starHistoryChart}&theme=dark`; diff --git a/docs/src/lib/layout.shared.tsx b/docs/src/lib/layout.shared.tsx index dcb46c3..cb32025 100644 --- a/docs/src/lib/layout.shared.tsx +++ b/docs/src/lib/layout.shared.tsx @@ -28,7 +28,7 @@ export function baseOptions(): BaseLayoutProps { ), - url: 'https://infinite-canvas-cpco.onrender.com/', + url: 'https://canvas.best/', external: true, on: 'nav', }, diff --git a/web/src/app/(user)/canvas/[id]/canvas-client-page.tsx b/web/src/app/(user)/canvas/[id]/canvas-client-page.tsx index ca3be53..a1d6374 100644 --- a/web/src/app/(user)/canvas/[id]/canvas-client-page.tsx +++ b/web/src/app/(user)/canvas/[id]/canvas-client-page.tsx @@ -3,7 +3,7 @@ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import type { ChangeEvent as ReactChangeEvent, DragEvent as ReactDragEvent, MouseEvent as ReactMouseEvent, PointerEvent as ReactPointerEvent } from "react"; import { useParams, useRouter } from "next/navigation"; -import { BookOpen, Home, ImageIcon, Images, List, Menu, MessageSquare, Music2, Plus, Redo2, Settings2, Trash2, Undo2, Upload, Video } from "lucide-react"; +import { BookOpen, Bot, Home, ImageIcon, Images, List, Menu, MessageSquare, Music2, Plus, Redo2, Settings2, Trash2, Undo2, Upload, Video } from "lucide-react"; import { saveAs } from "file-saver"; import { requestEdit, requestGeneration, requestImageQuestion } from "@/services/api/image"; @@ -27,6 +27,7 @@ import { ActiveConnectionPath, ConnectionPath } from "../components/canvas-conne import { CanvasConfigComposer } from "../components/canvas-config-composer"; import { CanvasConfigNodePanel } from "../components/canvas-config-node-panel"; import { CanvasAssistantPanel } from "../components/canvas-assistant-panel"; +import { CanvasLocalAgentPanel } from "../components/canvas-local-agent-panel"; import { CanvasNodeContextMenu } from "../components/canvas-context-menu"; import { CanvasNodeAngleDialog, type CanvasImageAngleParams } from "../components/canvas-node-angle-dialog"; import { CanvasNodeCropDialog, type CanvasImageCropRect } from "../components/canvas-node-crop-dialog"; @@ -43,6 +44,7 @@ import { CanvasToolbar } from "../components/canvas-toolbar"; import { AssetPickerModal, type AssetPickerTab, type InsertAssetPayload } from "../components/asset-picker-modal"; import { CanvasZoomControls } from "../components/canvas-zoom-controls"; import { useCanvasStore } from "../stores/use-canvas-store"; +import { applyCanvasAgentOps, type CanvasAgentOp, type CanvasAgentSnapshot } from "../utils/canvas-agent-ops"; import { buildCanvasResourceReferences, buildNodeMentionReferences } from "../utils/canvas-resource-references"; import { CanvasNodeType, @@ -290,6 +292,9 @@ function InfiniteCanvasPage() { const [previewNodeId, setPreviewNodeId] = useState(null); const [assistantCollapsed, setAssistantCollapsed] = useState(true); const [assistantMounted, setAssistantMounted] = useState(false); + const [localAgentCollapsed, setLocalAgentCollapsed] = useState(true); + const [localAgentMounted, setLocalAgentMounted] = useState(false); + const [agentUndoSnapshot, setAgentUndoSnapshot] = useState(null); const [titleEditing, setTitleEditing] = useState(false); const [titleDraft, setTitleDraft] = useState(""); const [historyState, setHistoryState] = useState({ canUndo: false, canRedo: false }); @@ -648,6 +653,44 @@ function InfiniteCanvasPage() { nodes.forEach((node) => map.set(node.id, buildNodeMentionReferences(node, nodes, connections))); return map; }, [connections, nodes]); + const agentSnapshot = useMemo( + () => ({ projectId, title: currentProject?.title || "未命名画布", nodes, connections, selectedNodeIds: Array.from(selectedNodeIds), viewport }), + [connections, currentProject?.title, nodes, projectId, selectedNodeIds, viewport], + ); + const applyAgentOps = useCallback( + (ops: CanvasAgentOp[]) => { + const before = { projectId, title: currentProject?.title || "未命名画布", nodes: nodesRef.current, connections: connectionsRef.current, selectedNodeIds: Array.from(selectedNodeIdsRef.current), viewport: viewportRef.current }; + const next = applyCanvasAgentOps(before, ops); + nodesRef.current = next.nodes; + connectionsRef.current = next.connections; + selectedNodeIdsRef.current = new Set(next.selectedNodeIds); + viewportRef.current = next.viewport; + setAgentUndoSnapshot(before); + setNodes(next.nodes); + setConnections(next.connections); + setSelectedNodeIds(new Set(next.selectedNodeIds)); + setSelectedConnectionId(null); + setViewport(next.viewport); + setContextMenu(null); + return { ...next, projectId, title: currentProject?.title || "未命名画布" }; + }, + [currentProject?.title, projectId], + ); + const undoAgentOps = useCallback(() => { + if (!agentUndoSnapshot) return null; + nodesRef.current = agentUndoSnapshot.nodes; + connectionsRef.current = agentUndoSnapshot.connections; + selectedNodeIdsRef.current = new Set(agentUndoSnapshot.selectedNodeIds); + viewportRef.current = agentUndoSnapshot.viewport; + setNodes(agentUndoSnapshot.nodes); + setConnections(agentUndoSnapshot.connections); + setSelectedNodeIds(new Set(agentUndoSnapshot.selectedNodeIds)); + setSelectedConnectionId(null); + setViewport(agentUndoSnapshot.viewport); + setContextMenu(null); + setAgentUndoSnapshot(null); + return { ...agentUndoSnapshot, projectId, title: currentProject?.title || "未命名画布" }; + }, [agentUndoSnapshot, currentProject?.title, projectId]); const createNode = useCallback( (type: CanvasNodeType, position?: Position) => { const targetPosition = position || getCanvasCenter(); @@ -2286,6 +2329,15 @@ function InfiniteCanvasPage() { [insertAssistantImage, insertAssistantText, screenToCanvas, size.height, size.width], ); + const localAgentOpen = localAgentMounted && !localAgentCollapsed; + const openLocalAgent = () => { + setLocalAgentMounted(true); + setLocalAgentCollapsed(false); + }; + const closeLocalAgent = () => { + setLocalAgentCollapsed(true); + }; + if (!projectLoaded) return ; return ( @@ -2309,10 +2361,12 @@ function InfiniteCanvasPage() { onUndo={undoCanvas} onRedo={redoCanvas} assistantCollapsed={assistantCollapsed} + localAgentOpen={localAgentOpen} onExpandAssistant={() => { setAssistantMounted(true); setAssistantCollapsed(false); }} + onToggleLocalAgent={() => (localAgentOpen ? closeLocalAgent() : openLocalAgent())} /> setAssistantMounted(false)} /> ) : null} + {localAgentMounted ? ( + + ) : null} ); } @@ -2634,7 +2698,9 @@ function CanvasTopBar({ onUndo, onRedo, assistantCollapsed, + localAgentOpen, onExpandAssistant, + onToggleLocalAgent, }: { title: string; titleDraft: string; @@ -2653,7 +2719,9 @@ function CanvasTopBar({ onUndo: () => void; onRedo: () => void; assistantCollapsed: boolean; + localAgentOpen: boolean; onExpandAssistant: () => void; + onToggleLocalAgent: () => void; }) { const colorTheme = useThemeStore((state) => state.theme); const theme = canvasThemes[colorTheme]; @@ -2746,9 +2814,18 @@ function CanvasTopBar({ setAccountOpen(false); }} /> + + {assistantCollapsed ? ( <> - + + setAgentState({ token: event.target.value })} placeholder="Agent token" /> +
+ +
+ + +
+
+ + + setAgentState({ logOpen: false })} + onClear={clearEventLogs} + onCopied={(text) => message.success(text)} + onCopyBlocked={(text) => message.warning(text)} + /> + +
+ {messages.map((item) => ( + + ))} + {pendingTool ? : null} + {waiting ? setAgentState({ logOpen: true })} /> : null} +
+ + setAgentState({ prompt })} onSubmit={sendPrompt} onAddFiles={addAttachments} onRemoveAttachment={removeAttachment} /> + + + ); +} + +function AgentComposer({ prompt, attachments, connected, sending, theme, onPromptChange, onSubmit, onAddFiles, onRemoveAttachment }: { prompt: string; attachments: AgentAttachment[]; connected: boolean; sending: boolean; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; onPromptChange: (value: string) => void; onSubmit: () => void; onAddFiles: (files: FileList | File[] | null) => Promise; onRemoveAttachment: (id: string) => void }) { + const fileInputRef = useRef(null); + const canSubmit = connected && !sending && Boolean(prompt.trim() || attachments.length); + const sizeText = attachments.length ? `${formatBytes(attachmentPayloadBytes(attachments))} / 30MB` : ""; + return ( +
event.stopPropagation()}> +
+ {attachments.length ? ( +
+ {attachments.map((item) => ( +
+ {item.name} + +
+ ))} +
+ ) : null} +