Compare commits

..

1 Commits

Author SHA1 Message Date
HouYunFei b3b02f75a9 feat(logging): implement detailed logging for Codex operations and HTTP requests 2026-07-28 17:42:11 +08:00
13 changed files with 334 additions and 41 deletions
+6
View File
@@ -1,6 +1,12 @@
# CHANGELOG # CHANGELOG
## Unreleased ## Unreleased
+ [新增] Canvas Agent 支持通过 `--debug` 输出彩色详细日志并按日期保存到本地日志文件。
+ [修复] 修复 Agent 发送后的运行提示闪退及回复需要切换标签页才显示的问题。
+ [优化] Agent 历史记录支持多选批量删除,点击记录可直接进入对话。
+ [优化] Canvas Agent 改为从独立指令文件初始化当前工作目录的 AGENTS.md,不再为每轮消息重复拼接前置提示词。
## v0.11.0 - 2026-07-28 ## v0.11.0 - 2026-07-28
+ [新增] 画布文本生成新增推理强度设置,支持默认调用与自定义调用脚本读取所选档位。 + [新增] 画布文本生成新增推理强度设置,支持默认调用与自定义调用脚本读取所选档位。
+8
View File
@@ -8,6 +8,14 @@
npx -y @basketikun/canvas-agent npx -y @basketikun/canvas-agent
``` ```
需要排查连接、线程、Codex app-server 或工具调用问题时,可开启 Debug 模式:
```bash
npx -y @basketikun/canvas-agent --debug
```
Debug 日志会以 `[DEBUG][HH:mm:ss]` 等传统格式输出到终端,并按启动日期保存到 `~/.infinite-canvas/logs/canvas-agent-YYYY-MM-DD.log`。终端日志带级别颜色,文件日志为纯文本;日志包含 HTTP、SSE、线程、turn、Codex app-server 和工具调用事件,token 与图片 Data URL 会自动隐藏。
本仓库开发时也可以直接运行: 本仓库开发时也可以直接运行:
```bash ```bash
+10
View File
@@ -0,0 +1,10 @@
# Infinite Canvas Agent
你正在帮助用户操作 Infinite Canvas 网站。
- 切换网站页面使用 `site_navigate`,可跳转 `/``/canvas``/canvas/:id``/image``/video``/prompts``/assets``/config`
- 修改画布前优先使用 `canvas_get_state`,再根据任务使用已配置的 infinite-canvas MCP 工具;复杂批量改动使用 `canvas_apply_ops`
- 用户要求把上传附件放入画布或作为生成参考图时,必须先用 `canvas_create_attachment_nodes` 创建真实图片节点,再把节点 ID 传给生成流程,不要创建空图片占位节点。
- 当前不在画布页时,先用 `site_navigate` 打开画布。查看已有画布时,先用 `canvas_list_projects` 获取画布清单和 ID。
- 生图与视频工作台分别使用 `workbench_image_*``workbench_video_*` 工具;提示词和素材分别使用 `prompts_search``assets_*` 工具。
- 需要生成内容时直接调用对应生成工具,不要绑定特定业务场景,不要模拟鼠标点击,不要要求用户手动复制 JSON。
+53
View File
@@ -8,6 +8,7 @@
"@modelcontextprotocol/sdk": "^1.12.1", "@modelcontextprotocol/sdk": "^1.12.1",
"@openai/codex": "^0.139.0", "@openai/codex": "^0.139.0",
"express": "^5.1.0", "express": "^5.1.0",
"winston": "^3.19.0",
"zod": "^3.25.0", "zod": "^3.25.0",
}, },
"devDependencies": { "devDependencies": {
@@ -19,6 +20,10 @@
}, },
}, },
"packages": { "packages": {
"@colors/colors": ["@colors/colors@1.6.0", "", {}, "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA=="],
"@dabh/diagnostics": ["@dabh/diagnostics@2.0.8", "", { "dependencies": { "@so-ric/colorspace": "^1.1.6", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q=="],
"@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/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-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=="],
@@ -89,6 +94,8 @@
"@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=="], "@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=="],
"@so-ric/colorspace": ["@so-ric/colorspace@1.1.6", "", { "dependencies": { "color": "^5.0.2", "text-hex": "1.0.x" } }, "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw=="],
"@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/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/connect": ["@types/connect@3.4.38", "https://registry.npmmirror.com/@types/connect/-/connect-3.4.38.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
@@ -109,12 +116,16 @@
"@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=="], "@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=="],
"@types/triple-beam": ["@types/triple-beam@1.3.5", "", {}, "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="],
"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=="], "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": ["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=="], "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=="],
"async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
"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=="], "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=="], "bytes": ["bytes@3.1.2", "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
@@ -123,6 +134,14 @@
"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=="], "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=="],
"color": ["color@5.0.3", "", { "dependencies": { "color-convert": "^3.1.3", "color-string": "^2.1.3" } }, "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA=="],
"color-convert": ["color-convert@3.1.3", "", { "dependencies": { "color-name": "^2.0.0" } }, "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg=="],
"color-name": ["color-name@2.1.1", "", {}, "sha512-p2FdgwVx1a9yWBHP2wI0VgShkDpgN4kZISkxdNipGBJWpa5G6b04OINlVWCyJj0JmfvcPrgqt95E9k8yvaOJFg=="],
"color-string": ["color-string@2.1.4", "", { "dependencies": { "color-name": "^2.0.0" } }, "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg=="],
"content-disposition": ["content-disposition@1.1.0", "https://registry.npmmirror.com/content-disposition/-/content-disposition-1.1.0.tgz", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="], "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=="], "content-type": ["content-type@1.0.5", "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
@@ -143,6 +162,8 @@
"ee-first": ["ee-first@1.1.1", "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], "ee-first": ["ee-first@1.1.1", "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
"enabled": ["enabled@2.0.0", "", {}, "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="],
"encodeurl": ["encodeurl@2.0.0", "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], "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-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=="],
@@ -169,8 +190,12 @@
"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=="], "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=="],
"fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="],
"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=="], "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=="],
"fn.name": ["fn.name@1.1.0", "", {}, "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="],
"forwarded": ["forwarded@0.2.0", "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], "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=="], "fresh": ["fresh@2.0.0", "https://registry.npmmirror.com/fresh/-/fresh-2.0.0.tgz", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
@@ -203,6 +228,8 @@
"is-promise": ["is-promise@4.0.0", "https://registry.npmmirror.com/is-promise/-/is-promise-4.0.0.tgz", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], "is-promise": ["is-promise@4.0.0", "https://registry.npmmirror.com/is-promise/-/is-promise-4.0.0.tgz", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
"is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
"isexe": ["isexe@2.0.0", "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "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=="], "jose": ["jose@6.2.3", "https://registry.npmmirror.com/jose/-/jose-6.2.3.tgz", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="],
@@ -211,6 +238,10 @@
"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=="], "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=="],
"kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="],
"logform": ["logform@2.7.0", "", { "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", "fecha": "^4.2.0", "ms": "^2.1.1", "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" } }, "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], "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=="], "media-typer": ["media-typer@1.1.0", "https://registry.npmmirror.com/media-typer/-/media-typer-1.1.0.tgz", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
@@ -233,6 +264,8 @@
"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=="], "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=="],
"one-time": ["one-time@1.0.0", "", { "dependencies": { "fn.name": "1.x.x" } }, "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g=="],
"parseurl": ["parseurl@1.3.3", "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], "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-key": ["path-key@3.1.1", "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
@@ -249,10 +282,16 @@
"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=="], "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=="],
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
"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=="], "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=="], "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=="],
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="],
"safer-buffer": ["safer-buffer@2.1.2", "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], "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=="], "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=="],
@@ -273,10 +312,18 @@
"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=="], "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=="],
"stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="],
"statuses": ["statuses@2.0.2", "https://registry.npmmirror.com/statuses/-/statuses-2.0.2.tgz", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], "statuses": ["statuses@2.0.2", "https://registry.npmmirror.com/statuses/-/statuses-2.0.2.tgz", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
"text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="],
"toidentifier": ["toidentifier@1.0.1", "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], "toidentifier": ["toidentifier@1.0.1", "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
"triple-beam": ["triple-beam@1.4.1", "", {}, "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="],
"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=="], "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=="], "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=="],
@@ -287,10 +334,16 @@
"unpipe": ["unpipe@1.0.0", "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], "unpipe": ["unpipe@1.0.0", "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"vary": ["vary@1.1.2", "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], "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=="], "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=="],
"winston": ["winston@3.19.0", "", { "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.8", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.9.0" } }, "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA=="],
"winston-transport": ["winston-transport@4.9.0", "", { "dependencies": { "logform": "^2.7.0", "readable-stream": "^3.6.2", "triple-beam": "^1.3.0" } }, "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A=="],
"wrappy": ["wrappy@1.0.2", "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], "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": ["zod@3.25.76", "https://registry.npmmirror.com/zod/-/zod-3.25.76.tgz", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
+3
View File
@@ -9,10 +9,12 @@
}, },
"files": [ "files": [
"dist", "dist",
"agent-instructions.md",
"README.md" "README.md"
], ],
"scripts": { "scripts": {
"dev": "tsx src/index.ts", "dev": "tsx src/index.ts",
"debug": "tsx src/index.ts --debug",
"test": "tsx --test src/canvas-session.test.ts", "test": "tsx --test src/canvas-session.test.ts",
"build": "tsc -p tsconfig.json", "build": "tsc -p tsconfig.json",
"start": "node dist/index.js", "start": "node dist/index.js",
@@ -22,6 +24,7 @@
"@modelcontextprotocol/sdk": "^1.12.1", "@modelcontextprotocol/sdk": "^1.12.1",
"@openai/codex": "^0.139.0", "@openai/codex": "^0.139.0",
"express": "^5.1.0", "express": "^5.1.0",
"winston": "^3.19.0",
"zod": "^3.25.0" "zod": "^3.25.0"
}, },
"devDependencies": { "devDependencies": {
+27 -4
View File
@@ -6,6 +6,7 @@ import { createRequire } from "node:module";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { AGENT_PROMPT, VERSION } from "./config.js"; import { AGENT_PROMPT, VERSION } from "./config.js";
import { logger } from "./utils/logger.js";
import type { AgentAttachment, AgentEmit } from "./types.js"; import type { AgentAttachment, AgentEmit } from "./types.js";
type Json = Record<string, unknown>; type Json = Record<string, unknown>;
@@ -39,6 +40,7 @@ export function interruptCodexTurn(threadId?: string) {
async function runCodexTurnNow(prompt: string, emit: AgentEmit, attachments: AgentAttachment[], options: CodexRunOptions) { async function runCodexTurnNow(prompt: string, emit: AgentEmit, attachments: AgentAttachment[], options: CodexRunOptions) {
let files: string[] = []; let files: string[] = [];
try { try {
logger.info("Preparing Codex turn", { threadId: options.threadId, cwd: options.cwd, prompt, attachmentCount: attachments.length });
options.onStart?.(); options.onStart?.();
files = await writeAttachmentFiles(attachments); files = await writeAttachmentFiles(attachments);
const app = await getCodexApp(options.appEmit || emit); const app = await getCodexApp(options.appEmit || emit);
@@ -55,6 +57,7 @@ async function runCodexTurnNow(prompt: string, emit: AgentEmit, attachments: Age
await app.startTurn(threadId, prompt, files, options.onTurn); await app.startTurn(threadId, prompt, files, options.onTurn);
} }
} catch (error) { } catch (error) {
logger.error("Codex turn failed", error);
emit("agent_error", { message: errorMessage(error) }); emit("agent_error", { message: errorMessage(error) });
} finally { } finally {
options.onFinish?.(); options.onFinish?.();
@@ -153,12 +156,21 @@ class CodexAppClient {
private constructor(private child: ChildProcess, private emit: AgentEmit) {} private constructor(private child: ChildProcess, private emit: AgentEmit) {}
static async start(emit: AgentEmit) { static async start(emit: AgentEmit) {
logger.info("Starting Codex app-server", { executable: process.execPath, codex: codexBin() });
const child = spawn(process.execPath, [codexBin(), "app-server", "--stdio"], { stdio: ["pipe", "pipe", "pipe"], windowsHide: true }); const child = spawn(process.execPath, [codexBin(), "app-server", "--stdio"], { stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
const client = new CodexAppClient(child, emit); const client = new CodexAppClient(child, emit);
child.stdout?.on("data", (chunk) => client.read(chunk.toString())); child.stdout?.on("data", (chunk) => client.read(chunk.toString()));
child.stderr?.on("data", (chunk) => emit("agent_log", { text: chunk.toString() })); child.stderr?.on("data", (chunk) => {
child.on("error", (error) => emit("agent_error", { message: error.message })); const text = chunk.toString();
logger.warn("Codex app-server stderr", { text });
emit("agent_log", { text });
});
child.on("error", (error) => {
logger.error("Codex app-server process error", error);
emit("agent_error", { message: error.message });
});
child.on("exit", (code) => { child.on("exit", (code) => {
logger.warn("Codex app-server exited", { code });
client.failAll(`Codex app-server exited: ${code ?? 0}`); client.failAll(`Codex app-server exited: ${code ?? 0}`);
codexApp = null; codexApp = null;
codexThreadId = ""; codexThreadId = "";
@@ -214,6 +226,7 @@ class CodexAppClient {
interruptCurrentTurn() { interruptCurrentTurn() {
if (this.activeTurns.size === 0) return false; if (this.activeTurns.size === 0) return false;
try { try {
logger.warn("Interrupting active Codex turn", { threadId: codexThreadId, activeTurns: this.activeTurns.size });
this.child.kill("SIGINT"); this.child.kill("SIGINT");
return true; return true;
} catch { } catch {
@@ -232,6 +245,7 @@ class CodexAppClient {
} }
private write(value: unknown) { private write(value: unknown) {
logger.debug("Codex app-server request", value);
this.child.stdin?.write(`${JSON.stringify(value)}\n`); this.child.stdin?.write(`${JSON.stringify(value)}\n`);
} }
@@ -242,13 +256,15 @@ class CodexAppClient {
lines.filter(Boolean).forEach((line) => { lines.filter(Boolean).forEach((line) => {
try { try {
this.handle(JSON.parse(line) as Json); this.handle(JSON.parse(line) as Json);
} catch { } catch (error) {
logger.warn("Invalid Codex app-server output", { error, line });
this.emit("agent_log", { text: line }); this.emit("agent_log", { text: line });
} }
}); });
} }
private handle(message: Json) { private handle(message: Json) {
logger.debug("Codex app-server response", message);
const id = Number(message.id); 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 (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 (this.pending.has(id)) return this.resolve(id, message.result);
@@ -261,6 +277,13 @@ class CodexAppClient {
if (method === "thread/tokenUsage/updated") this.lastUsage = normalizeUsage(params); if (method === "thread/tokenUsage/updated") this.lastUsage = normalizeUsage(params);
const event = normalizeCodexNotification(method, params); const event = normalizeCodexNotification(method, params);
if (!event) return; if (!event) return;
if (event.type === "item.completed") {
const item = field(event, "item") as Json | undefined;
const id = String(field(item, "id") || "");
const streamedText = this.textByItem.get(id);
if (item?.type === "agent_message" && streamedText && !item.text) item.text = streamedText;
if (id) this.textByItem.delete(id);
}
if (event.type === "turn.completed") event.usage = this.lastUsage; if (event.type === "turn.completed") event.usage = this.lastUsage;
this.emit("agent_event", { agent: "codex", ...event }); this.emit("agent_event", { agent: "codex", ...event });
if (event.type === "turn.completed") { if (event.type === "turn.completed") {
@@ -432,7 +455,7 @@ function threadMessages(thread: unknown): AgentHistoryMessage[] {
} }
if (type === "agentMessage") { if (type === "agentMessage") {
const text = String(field(item, "text") || "").trim(); const text = String(field(item, "text") || "").trim();
if (text) messages.push({ id, role: "assistant", title: "Codex", text, streamId: id }); if (text) messages.push({ id, role: "assistant", title: "Codex", text });
} }
if (type === "mcpToolCall") { if (type === "mcpToolCall") {
const tool = String(field(item, "tool") || "工具调用"); const tool = String(field(item, "tool") || "工具调用");
+12
View File
@@ -1,6 +1,7 @@
import crypto from "node:crypto"; import crypto from "node:crypto";
import type { ServerResponse } from "node:http"; import type { ServerResponse } from "node:http";
import { logger } from "./utils/logger.js";
import { type ToolName } from "./schemas.js"; import { type ToolName } from "./schemas.js";
import { compactCanvasState, compactNode, isToolName, nextCanvasX, parseToolInput } from "./tools.js"; import { compactCanvasState, compactNode, isToolName, nextCanvasX, parseToolInput } from "./tools.js";
import type { AgentAttachment, CanvasNode, CanvasNodeType, CanvasSnapshot } from "./types.js"; import type { AgentAttachment, CanvasNode, CanvasNodeType, CanvasSnapshot } from "./types.js";
@@ -51,12 +52,14 @@ export class CanvasSession {
setCodexState(patch: Partial<CodexState>) { setCodexState(patch: Partial<CodexState>) {
this.codexState = { ...this.codexState, ...patch }; this.codexState = { ...this.codexState, ...patch };
logger.debug("Codex state changed", this.codexState);
this.emitAll("codex_state", this.codexState); this.emitAll("codex_state", this.codexState);
} }
openEvents(url: URL, res: ServerResponse) { openEvents(url: URL, res: ServerResponse) {
const clientId = url.searchParams.get("clientId") || crypto.randomUUID(); const clientId = url.searchParams.get("clientId") || crypto.randomUUID();
const statusOnly = url.searchParams.get("role") === "status"; const statusOnly = url.searchParams.get("role") === "status";
logger.info("SSE client connected", { clientId, statusOnly });
res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" }); res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" });
if (!statusOnly) { if (!statusOnly) {
this.clients.set(clientId, res); this.clients.set(clientId, res);
@@ -70,6 +73,7 @@ export class CanvasSession {
const timer = setInterval(() => sendEvent(res, "ping", { time: Date.now() }), 15000); const timer = setInterval(() => sendEvent(res, "ping", { time: Date.now() }), 15000);
res.on("close", () => { res.on("close", () => {
clearInterval(timer); clearInterval(timer);
logger.info("SSE client disconnected", { clientId, statusOnly });
if (statusOnly || this.clients.get(clientId) !== res) return; if (statusOnly || this.clients.get(clientId) !== res) return;
this.clients.delete(clientId); this.clients.delete(clientId);
this.clientFocusOrder.delete(clientId); this.clientFocusOrder.delete(clientId);
@@ -88,21 +92,25 @@ export class CanvasSession {
const targetClientId = clientId || this.activeClientId; const targetClientId = clientId || this.activeClientId;
if (!targetClientId) return; if (!targetClientId) return;
this.canvasStates.set(targetClientId, { ...((body && typeof body === "object" && !Array.isArray(body) ? body : {}) as Record<string, unknown>), clientId: targetClientId } as CanvasSnapshot); this.canvasStates.set(targetClientId, { ...((body && typeof body === "object" && !Array.isArray(body) ? body : {}) as Record<string, unknown>), clientId: targetClientId } as CanvasSnapshot);
logger.debug("Canvas state updated", { clientId: targetClientId, nodes: Array.isArray((body as CanvasSnapshot | null)?.nodes) ? (body as CanvasSnapshot).nodes.length : 0, connections: Array.isArray((body as CanvasSnapshot | null)?.connections) ? (body as CanvasSnapshot).connections.length : 0 });
} }
activateClient(clientId: string) { activateClient(clientId: string) {
if (!this.clients.has(clientId)) throw new Error("当前网页未连接"); if (!this.clients.has(clientId)) throw new Error("当前网页未连接");
this.activeClientId = clientId; this.activeClientId = clientId;
this.clientFocusOrder.set(clientId, ++this.focusSequence); this.clientFocusOrder.set(clientId, ++this.focusSequence);
logger.debug("Canvas client activated", { clientId });
} }
bindClient(clientId: string) { bindClient(clientId: string) {
if (!this.clients.has(clientId)) throw new Error("当前网页未连接"); if (!this.clients.has(clientId)) throw new Error("当前网页未连接");
this.boundClientId = clientId; this.boundClientId = clientId;
logger.debug("Canvas client bound to turn", { clientId });
} }
releaseClient(clientId: string) { releaseClient(clientId: string) {
if (this.boundClientId === clientId) this.boundClientId = ""; if (this.boundClientId === clientId) this.boundClientId = "";
logger.debug("Canvas client released from turn", { clientId });
} }
setTurnAttachments(clientId: string, attachments: AgentAttachment[]) { setTurnAttachments(clientId: string, attachments: AgentAttachment[]) {
@@ -142,6 +150,7 @@ export class CanvasSession {
const item = body.requestId ? this.pending.get(body.requestId) : null; const item = body.requestId ? this.pending.get(body.requestId) : null;
if (!item || !body.requestId || item.clientId !== clientId) return false; if (!item || !body.requestId || item.clientId !== clientId) return false;
this.pending.delete(body.requestId); this.pending.delete(body.requestId);
logger.debug("Canvas tool result received", { clientId, requestId: body.requestId, error: body.error, result: body.result });
body.error ? item.reject(new Error(body.error)) : item.resolve(body.result); body.error ? item.reject(new Error(body.error)) : item.resolve(body.result);
return true; return true;
} }
@@ -156,6 +165,7 @@ export class CanvasSession {
async callTool(name: unknown, rawInput: unknown) { async callTool(name: unknown, rawInput: unknown) {
if (!isToolName(name)) throw new Error(`未知工具:${String(name)}`); if (!isToolName(name)) throw new Error(`未知工具:${String(name)}`);
logger.info("MCP tool called", { name, input: rawInput, targetClientId: this.targetClientId });
let tool: ToolName = name; let tool: ToolName = name;
let input = parseToolInput(tool, rawInput) as Record<string, unknown>; let input = parseToolInput(tool, rawInput) as Record<string, unknown>;
if (SITE_TOOLS.has(tool)) { if (SITE_TOOLS.has(tool)) {
@@ -296,9 +306,11 @@ export class CanvasSession {
const client = this.clients.get(clientId); const client = this.clients.get(clientId);
if (!client) throw new Error("当前没有已连接画布"); if (!client) throw new Error("当前没有已连接画布");
sendEvent(client, "tool_call", { requestId, name, input }); sendEvent(client, "tool_call", { requestId, name, input });
logger.debug("Canvas tool request sent", { requestId, name, input, clientId });
return await new Promise((resolve, reject) => { return await new Promise((resolve, reject) => {
const timer = setTimeout(() => { const timer = setTimeout(() => {
this.pending.delete(requestId); this.pending.delete(requestId);
logger.warn("Canvas tool request timed out", { requestId, name, clientId });
reject(new Error("画布操作超时")); reject(new Error("画布操作超时"));
}, 30000); }, 30000);
this.pending.set(requestId, { clientId, resolve: (value) => (clearTimeout(timer), resolve(value)), reject: (error) => (clearTimeout(timer), reject(error)) }); this.pending.set(requestId, { clientId, resolve: (value) => (clearTimeout(timer), resolve(value)), reject: (error) => (clearTimeout(timer), reject(error)) });
+13 -4
View File
@@ -7,7 +7,8 @@ export const DEFAULT_PORT = 17371;
export const CONFIG_DIR = path.join(os.homedir(), ".infinite-canvas"); export const CONFIG_DIR = path.join(os.homedir(), ".infinite-canvas");
export const CONFIG_FILE = path.join(CONFIG_DIR, "canvas-agent.json"); export const CONFIG_FILE = path.join(CONFIG_DIR, "canvas-agent.json");
export const VERSION = readPackageVersion(); export const VERSION = readPackageVersion();
export const AGENT_PROMPT = "你正在帮助用户操作 Infinite Canvas 网站。切换网站页面用 site_navigate,可跳 / (首页)、/canvas (我的画布)、/canvas/:id (指定画布)、/image、/video、/prompts、/assets、/config。需要改动画布时优先使用已配置的 infinite-canvas MCP 工具:先 canvas_get_state 读取当前画布,再根据任务使用 canvas_create_text_node、canvas_generate_text、canvas_generate_image、canvas_generate_video、canvas_generate_audio、canvas_create_generation_flow、canvas_create_config_node、canvas_run_generation、canvas_update_node、canvas_connect_nodes 等通用工具;复杂批量改动再用 canvas_apply_ops,删除连线可用 delete_connections。本轮若有用户上传的图片附件,会同时给出 attachmentId;用户要求把附件放入画布或作为生成参考图时,必须先用 canvas_create_attachment_nodes 创建真实图片节点,再把返回的节点 ID 传给 canvas_create_generation_flow.referenceNodeIds,不要创建空图片占位节点。若当前不在画布页,画布工具会报错,需先用 site_navigate 打开画布。想了解或打开用户已有画布,用 canvas_list_projects 获取画布清单和 id,再用 site_navigate 跳 /canvas/:id 打开。生图工作台可用 workbench_image_get_config 看可选项、workbench_image_generate 填提示词并生成;视频创作台对应 workbench_video_get_config 与 workbench_video_generate;用 prompts_search 分页搜索提示词库;用 assets_list 查看「我的素材」、assets_add 新增文本或图片素材。需要生成内容时直接调用对应生成工具,不要绑定特定业务场景。不要模拟鼠标点击,不要要求用户手动复制 JSON。"; export const AGENT_PROMPT = fs.readFileSync(new URL("../agent-instructions.md", import.meta.url), "utf8");
const initializedWorkspaces = new Set<string>();
export type SiteWorkspaceConfig = { workspacePath: string; activeThreadId?: string; pinnedThreadIds?: string[] }; export type SiteWorkspaceConfig = { workspacePath: string; activeThreadId?: string; pinnedThreadIds?: string[] };
export type CanvasAgentConfig = { url: string; token: string; origins?: string[]; workspace?: SiteWorkspaceConfig }; export type CanvasAgentConfig = { url: string; token: string; origins?: string[]; workspace?: SiteWorkspaceConfig };
@@ -31,12 +32,12 @@ export function ensureSiteWorkspace(config: CanvasAgentConfig) {
const current = config.workspace; const current = config.workspace;
if (current?.workspacePath) { if (current?.workspacePath) {
const workspacePath = resolveWorkspacePath(current.workspacePath); const workspacePath = resolveWorkspacePath(current.workspacePath);
fs.mkdirSync(workspacePath, { recursive: true }); initializeWorkspace(workspacePath);
return { ...current, workspacePath }; return { ...current, workspacePath };
} }
const workspacePath = path.join(CONFIG_DIR, "codex-workspaces", "site"); const workspacePath = path.join(CONFIG_DIR, "codex-workspaces", "site");
config.workspace = { workspacePath }; config.workspace = { workspacePath };
fs.mkdirSync(workspacePath, { recursive: true }); initializeWorkspace(workspacePath);
saveConfig(config); saveConfig(config);
return { workspacePath }; return { workspacePath };
} }
@@ -46,11 +47,19 @@ export function updateSiteWorkspace(config: CanvasAgentConfig, patch: Partial<Si
const workspacePath = patch.workspacePath ? resolveWorkspacePath(patch.workspacePath) : current.workspacePath; const workspacePath = patch.workspacePath ? resolveWorkspacePath(patch.workspacePath) : current.workspacePath;
const next = { ...current, ...patch, workspacePath }; const next = { ...current, ...patch, workspacePath };
config.workspace = { workspacePath: next.workspacePath, activeThreadId: next.activeThreadId, pinnedThreadIds: next.pinnedThreadIds }; config.workspace = { workspacePath: next.workspacePath, activeThreadId: next.activeThreadId, pinnedThreadIds: next.pinnedThreadIds };
fs.mkdirSync(workspacePath, { recursive: true }); initializeWorkspace(workspacePath);
saveConfig(config); saveConfig(config);
return config.workspace; return config.workspace;
} }
function initializeWorkspace(workspacePath: string) {
if (initializedWorkspaces.has(workspacePath)) return;
fs.mkdirSync(workspacePath, { recursive: true });
const instructionsFile = path.join(workspacePath, "AGENTS.md");
if (!fs.existsSync(instructionsFile)) fs.writeFileSync(instructionsFile, AGENT_PROMPT);
initializedWorkspaces.add(workspacePath);
}
function resolveWorkspacePath(value: string) { function resolveWorkspacePath(value: string) {
if (value === "~") return os.homedir(); if (value === "~") return os.homedir();
if (value.startsWith("~/")) return path.join(os.homedir(), value.slice(2)); if (value.startsWith("~/")) return path.join(os.homedir(), value.slice(2));
+19 -2
View File
@@ -3,6 +3,7 @@ import express, { type NextFunction, type Request, type Response } from "express
import { DEFAULT_PORT, ensureSiteWorkspace, loadConfig, saveConfig, updateSiteWorkspace, type CanvasAgentConfig } from "./config.js"; import { DEFAULT_PORT, ensureSiteWorkspace, loadConfig, saveConfig, updateSiteWorkspace, type CanvasAgentConfig } from "./config.js";
import { CanvasSession } from "./canvas-session.js"; import { CanvasSession } from "./canvas-session.js";
import { archiveCodexThread, interruptCodexTurn, isRecoverableThreadError, listCodexThreads, readCodexThread, resumeCodexThread, runClaudeTurn, runCodexTurn, startCodexThread, summarizeCodexThread, verifyCodexThreadWorkspace, withAgentPrompt } from "./agents.js"; import { archiveCodexThread, interruptCodexTurn, isRecoverableThreadError, listCodexThreads, readCodexThread, resumeCodexThread, runClaudeTurn, runCodexTurn, startCodexThread, summarizeCodexThread, verifyCodexThreadWorkspace, withAgentPrompt } from "./agents.js";
import { logger } from "./utils/logger.js";
import type { AgentAttachment } from "./types.js"; import type { AgentAttachment } from "./types.js";
export function startHttpServer() { export function startHttpServer() {
@@ -15,6 +16,7 @@ export function startHttpServer() {
const emit = (type: string, payload: unknown) => { const emit = (type: string, payload: unknown) => {
const data = payload && typeof payload === "object" && !Array.isArray(payload) ? payload as Record<string, unknown> : { value: payload }; const data = payload && typeof payload === "object" && !Array.isArray(payload) ? payload as Record<string, unknown> : { value: payload };
const threadId = String(data.threadId || data.thread_id || ensureSiteWorkspace(config).activeThreadId || ""); const threadId = String(data.threadId || data.thread_id || ensureSiteWorkspace(config).activeThreadId || "");
logger.debug("Agent event", { type, threadId, payload: data });
threadId ? session.emitThread(type, threadId, data) : session.emitAll(type, data); threadId ? session.emitThread(type, threadId, data) : session.emitAll(type, data);
}; };
const setActiveThread = (activeThreadId: string, payload: Record<string, unknown> = {}) => { const setActiveThread = (activeThreadId: string, payload: Record<string, unknown> = {}) => {
@@ -25,6 +27,13 @@ export function startHttpServer() {
const app = express(); const app = express();
app.disable("x-powered-by"); app.disable("x-powered-by");
app.use(express.json({ limit: "30mb" })); app.use(express.json({ limit: "30mb" }));
app.use((req, res, next) => {
if (!logger.enabled) return next();
const startedAt = Date.now();
const url = requestUrl(req, config);
res.on("finish", () => logger.debug("HTTP request", { method: req.method, path: url.pathname, status: res.statusCode, durationMs: Date.now() - startedAt, origin: req.headers.origin, clientId: url.searchParams.get("clientId") }));
next();
});
app.use((req, res, next) => { app.use((req, res, next) => {
const url = requestUrl(req, config); const url = requestUrl(req, config);
if (!setCors(req, res, url, config)) return void res.status(403).json({ ok: false, error: "origin not allowed" }); if (!setCors(req, res, url, config)) return void res.status(403).json({ ok: false, error: "origin not allowed" });
@@ -108,6 +117,7 @@ export function startHttpServer() {
const prompt = String(req.body?.prompt || ""); const prompt = String(req.body?.prompt || "");
if (!prompt.trim()) return res.status(400).json({ ok: false, error: "请输入任务内容" }); if (!prompt.trim()) return res.status(400).json({ ok: false, error: "请输入任务内容" });
const clientId = String(req.body?.clientId || ""); const clientId = String(req.body?.clientId || "");
logger.info("Codex turn accepted", { clientId, threadId: req.body?.threadId, prompt, attachments: attachments.map(({ id, name, type, size, width, height }) => ({ id, name, type, size, width, height })) });
session.setCodexState({ busy: true, threadId: String(req.body?.threadId || workspace.activeThreadId || ""), turnId: "" }); session.setCodexState({ busy: true, threadId: String(req.body?.threadId || workspace.activeThreadId || ""), turnId: "" });
try { try {
let threadId = String(req.body?.threadId || workspace.activeThreadId || ""); let threadId = String(req.body?.threadId || workspace.activeThreadId || "");
@@ -130,7 +140,7 @@ export function startHttpServer() {
const data = payload && typeof payload === "object" && !Array.isArray(payload) ? payload as Record<string, unknown> : { value: payload }; const data = payload && typeof payload === "object" && !Array.isArray(payload) ? payload as Record<string, unknown> : { value: payload };
session.emitThread(type, threadId, data); session.emitThread(type, threadId, data);
}; };
void runCodexTurn(withAgentPrompt(withAttachmentContext(prompt, attachmentRefs)), turnEmit, attachments, { void runCodexTurn(withAttachmentContext(prompt, attachmentRefs), turnEmit, attachments, {
threadId, threadId,
cwd: workspace.workspacePath, cwd: workspace.workspacePath,
appEmit: emit, appEmit: emit,
@@ -148,9 +158,11 @@ export function startHttpServer() {
}, },
onTurn: (actualTurnId) => { onTurn: (actualTurnId) => {
turnId = actualTurnId; turnId = actualTurnId;
logger.info("Codex turn started", { threadId, turnId });
session.setCodexState({ busy: true, threadId, turnId }); session.setCodexState({ busy: true, threadId, turnId });
}, },
onFinish: () => { onFinish: () => {
logger.info("Codex turn finished", { threadId, turnId });
session.clearTurnAttachments(clientId); session.clearTurnAttachments(clientId);
if (clientId) session.releaseClient(clientId); if (clientId) session.releaseClient(clientId);
session.setCodexState({ busy: false, threadId, turnId }); session.setCodexState({ busy: false, threadId, turnId });
@@ -171,7 +183,10 @@ export function startHttpServer() {
res.json({ ok: true }); res.json({ ok: true });
}); });
app.use((_req, res) => res.status(404).json({ ok: false, error: "not found" })); 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.use((error: Error, req: Request, res: Response, _next: NextFunction) => {
logger.error("HTTP request failed", { method: req.method, path: req.path, error });
res.status(500).json({ ok: false, error: error.message });
});
app.listen(port, "127.0.0.1", () => { app.listen(port, "127.0.0.1", () => {
console.log("Infinite Canvas Agent"); console.log("Infinite Canvas Agent");
@@ -180,6 +195,8 @@ export function startHttpServer() {
console.log("Codex MCP is not installed by this command."); console.log("Codex MCP is not installed by this command.");
console.log("Optional MCP add: codex mcp add infinite-canvas -- npx -y @basketikun/canvas-agent mcp"); console.log("Optional MCP add: codex mcp add infinite-canvas -- npx -y @basketikun/canvas-agent mcp");
console.log("Remove manually added MCP: codex mcp remove infinite-canvas"); console.log("Remove manually added MCP: codex mcp remove infinite-canvas");
if (logger.enabled) console.log(`Debug log: ${logger.filePath}`);
logger.info("Canvas Agent started", { url: config.url, workspace: ensureSiteWorkspace(config).workspacePath, debugLog: logger.filePath });
}); });
} }
+7
View File
@@ -0,0 +1,7 @@
/** 将日期格式化为适合文件名使用的本地日期字符串。 */
export function formatDateForFilename(date = new Date()) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
}
+77
View File
@@ -0,0 +1,77 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import {inspect} from "node:util";
import winston, {format, transports, type Logger as WinstonLogger} from "winston";
import {formatDateForFilename} from "./date.js";
/** 管理 Canvas Agent 的终端与文件 Debug 日志。 */
export class Logger {
readonly enabled = process.argv.includes("--debug");
readonly filePath = this.enabled ? path.join(os.homedir(), ".infinite-canvas", "logs", `canvas-agent-${formatDateForFilename()}.log`) : "";
private readonly logger: WinstonLogger | null;
/** 根据命令行 Debug 参数初始化日志输出。 */
constructor() {
if (!this.enabled) {
this.logger = null;
return;
}
fs.mkdirSync(path.dirname(this.filePath), {recursive: true});
const line = format.printf(({level, message, timestamp, details}) => `[${level.toUpperCase()}][${timestamp}] ${message}${formatDetails(details)}`);
this.logger = winston.createLogger({
level: "debug",
transports: [
new transports.Console({format: format.combine(format.colorize(), format.timestamp({format: "HH:mm:ss"}), line)}),
new transports.File({filename: this.filePath, format: format.combine(format.timestamp({format: "HH:mm:ss"}), line)}),
],
});
}
/** 输出 Debug 级别日志。 */
debug(message: string, details?: unknown) {
if (details === undefined) this.logger?.debug(message);
else this.logger?.debug(message, {details: sanitize(details)});
}
/** 输出 Info 级别日志。 */
info(message: string, details?: unknown) {
if (details === undefined) this.logger?.info(message);
else this.logger?.info(message, {details: sanitize(details)});
}
/** 输出 Warn 级别日志。 */
warn(message: string, details?: unknown) {
if (details === undefined) this.logger?.warn(message);
else this.logger?.warn(message, {details: sanitize(details)});
}
/** 输出 Error 级别日志。 */
error(message: string, details?: unknown) {
if (details === undefined) this.logger?.error(message);
else this.logger?.error(message, {details: sanitize(details)});
}
}
/** 将日志详情格式化为紧凑的单行文本。 */
function formatDetails(details: unknown) {
if (details === undefined) return "";
if (!details || typeof details !== "object" || Array.isArray(details)) return ` ${inspect(details, {depth: null, breakLength: Infinity})}`;
return ` ${Object.entries(details).map(([key, value]) => `${key}=${inspect(value, {depth: null, breakLength: Infinity})}`).join(" ")}`;
}
/** 清理日志内容中的敏感数据和不可序列化引用。 */
function sanitize(value: unknown, key = "", seen = new WeakSet<object>()): unknown {
if (/token|authorization|api.?key|dataurl/i.test(key)) return "[REDACTED]";
if (typeof value === "string" && value.startsWith("data:")) return `[DATA URL ${value.length} chars]`;
if (value instanceof Error) return {name: value.name, message: value.message, stack: value.stack};
if (!value || typeof value !== "object") return value;
if (seen.has(value)) return "[CIRCULAR]";
seen.add(value);
if (Array.isArray(value)) return value.map((item) => sanitize(item, key, seen));
return Object.fromEntries(Object.entries(value as Record<string, unknown>).map(([field, item]) => [field, sanitize(item, field, seen)]));
}
export const logger = new Logger();
@@ -5,6 +5,10 @@ description: 当前版本已实现但仍需人工验证的变更项
# 待测试 # 待测试
- Canvas Agent Debug:使用 `npx -y @basketikun/canvas-agent --debug` 启动后,终端应以 `[DEBUG][HH:mm:ss]` 等传统格式输出带级别颜色的详细日志并显示日志文件路径,`~/.infinite-canvas/logs/` 下应按启动日期生成不含颜色代码的 `canvas-agent-YYYY-MM-DD.log`,同一天多次启动应追加到同一文件;普通启动保持原有简洁输出,日志中不应出现连接 token 或图片 Data URL 原文。
- Agent 回复实时显示:在右侧 Agent 发送消息后,用户消息下方应立即出现 GPT 图标和 `working...`,任务运行期间不应闪退;Codex 的回复应在当前对话中持续显示,实时事件缺失时也应在任务完成后自动同步完整内容,无需切换到历史或日志再返回对话。
- Agent 历史记录:点击记录卡片应直接进入对应对话,不再显示「进入」按钮;可勾选单条或全选多条记录并批量删除,删除当前对话后聊天内容应清空。
- Agent 工作目录指令:`canvas-agent/agent-instructions.md` 应作为独立维护源;重启 Canvas Agent 后,当前工作目录应自动生成 `AGENTS.md`;新建对话发送消息时,Codex 日志中的用户消息只包含本轮请求和必要的附件上下文,不再重复整段 Infinite Canvas 前置提示词,画布及工作台工具仍可正常调用。
- 画布文本设置:文本节点和生成配置节点切换到文本模式后应显示推理强度设置,可选择自动、低、中、高、极高;选择自动时默认 OpenAI Responses 请求不应携带 `reasoning`,选择其他档位时应携带所选强度,刷新画布后节点设置应保留;文本模型自定义调用脚本应能读取 `reasoningEffort`OpenAI 模板应按自动或指定档位正确组装请求。 - 画布文本设置:文本节点和生成配置节点切换到文本模式后应显示推理强度设置,可选择自动、低、中、高、极高;选择自动时默认 OpenAI Responses 请求不应携带 `reasoning`,选择其他档位时应携带所选强度,刷新画布后节点设置应保留;文本模型自定义调用脚本应能读取 `reasoningEffort`OpenAI 模板应按自动或指定档位正确组装请求。
- 生图工作台参考图:将一张或多张图片拖入参考图区域后应直接上传并显示缩略图;拖入非图片文件应忽略,拖动过程中区域应显示高亮提示,放下文件不应导致浏览器打开或替换当前页面。 - 生图工作台参考图:将一张或多张图片拖入参考图区域后应直接上传并显示缩略图;拖入非图片文件应忽略,拖动过程中区域应显示高亮提示,放下文件不应导致浏览器打开或替换当前页面。
- 视频创作台参考资产:将图片、MP4/MOV 视频或 MP3/WAV 音频拖入任一参考资产区域后,应按文件类型自动上传到对应列表;当前拖入区域应显示高亮提示,原有数量、大小、时长和格式限制应继续生效。 - 视频创作台参考资产:将图片、MP4/MOV 视频或 MP3/WAV 音频拖入任一参考资产区域后,应按文件类型自动上传到对应列表;当前拖入区域应显示高亮提示,原有数量、大小、时长和格式限制应继续生效。
@@ -1,6 +1,6 @@
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useNavigate, useSearchParams } from "react-router-dom"; import { useNavigate, useSearchParams } from "react-router-dom";
import { App, Button, Input, Segmented, Tooltip } from "antd"; import { App, Button, Checkbox, Input, Segmented, Tooltip } from "antd";
import copyToClipboard from "copy-to-clipboard"; import copyToClipboard from "copy-to-clipboard";
import { ChevronDown, Copy, FolderOpen, History, KeyRound, Link2, LoaderCircle, MessageSquare, PlugZap, Plus, RefreshCw, Square, Terminal, Trash2 } from "lucide-react"; import { ChevronDown, Copy, FolderOpen, History, KeyRound, Link2, LoaderCircle, MessageSquare, PlugZap, Plus, RefreshCw, Square, Terminal, Trash2 } from "lucide-react";
@@ -49,6 +49,7 @@ type AgentThreadResponse = { ok?: boolean; workspace?: AgentWorkspace; thread?:
type AgentConfigResponse = { ok?: boolean; url?: string; token?: string; hasToken?: boolean }; type AgentConfigResponse = { ok?: boolean; url?: string; token?: string; hasToken?: boolean };
type AgentCodexState = { busy?: boolean; threadId?: string; turnId?: string }; type AgentCodexState = { busy?: boolean; threadId?: string; turnId?: string };
type AgentHelloEvent = { ok?: boolean; clientId?: string; codex?: AgentCodexState }; type AgentHelloEvent = { ok?: boolean; clientId?: string; codex?: AgentCodexState };
type AgentHealth = { codexBusy?: boolean };
type AgentWorkspaceEvent = { activeThreadId?: string; threadId?: string; emptyThread?: boolean }; type AgentWorkspaceEvent = { activeThreadId?: string; threadId?: string; emptyThread?: boolean };
type AgentChatEvent = { threadId?: string; sourceClientId?: string; message?: AgentChatItem }; type AgentChatEvent = { threadId?: string; sourceClientId?: string; message?: AgentChatItem };
@@ -102,6 +103,7 @@ export function CanvasLocalAgentPanel({ embedded, headless, autoConnect }: { emb
const attachmentUrlsRef = useRef(new Set<string>()); const attachmentUrlsRef = useRef(new Set<string>());
const clientIdRef = useRef(randomId()); const clientIdRef = useRef(randomId());
const loadThreadsSequenceRef = useRef(0); const loadThreadsSequenceRef = useRef(0);
const turnSyncSequenceRef = useRef(0);
const endpoint = useMemo(() => url.trim().replace(/\/$/, ""), [url]); const endpoint = useMemo(() => url.trim().replace(/\/$/, ""), [url]);
const urlAgentAutoConnect = searchParams.has("agentUrl") && searchParams.has("agentToken"); const urlAgentAutoConnect = searchParams.has("agentUrl") && searchParams.has("agentToken");
const loadThreads = useCallback(async (skipHistory = false) => { const loadThreads = useCallback(async (skipHistory = false) => {
@@ -124,6 +126,21 @@ export function CanvasLocalAgentPanel({ embedded, headless, autoConnect }: { emb
if (sequence === loadThreadsSequenceRef.current) setAgentState({ loadingThreads: false }); if (sequence === loadThreadsSequenceRef.current) setAgentState({ loadingThreads: false });
} }
}, [endpoint, setAgentState, token]); }, [endpoint, setAgentState, token]);
const syncTurnMessages = async (sequence: number) => {
while (sequence === turnSyncSequenceRef.current && useAgentStore.getState().connected) {
try {
const health = await fetchAgentJson<AgentHealth>(endpoint, token, "/health");
if (!health.codexBusy) {
await wait(300);
if (sequence !== turnSyncSequenceRef.current) return;
await loadThreads();
if (sequence === turnSyncSequenceRef.current) setAgentState({ waiting: false, sending: false, activity: "完成" });
return;
}
} catch {}
await wait(500);
}
};
// canvasContext 命令式订阅:保持 ref 最新,并在快照变化时防抖上报,全程不触发面板重渲染。 // canvasContext 命令式订阅:保持 ref 最新,并在快照变化时防抖上报,全程不触发面板重渲染。
useEffect(() => { useEffect(() => {
@@ -313,7 +330,9 @@ export function CanvasLocalAgentPanel({ embedded, headless, autoConnect }: { emb
URL.revokeObjectURL(item.url); URL.revokeObjectURL(item.url);
attachmentUrlsRef.current.delete(item.url); attachmentUrlsRef.current.delete(item.url);
}); });
setAgentState({ prompt: "", attachments: [] }); setAgentState({ prompt: "", attachments: [], sending: false, waiting: true, activity: "Codex 正在运行" });
const sequence = ++turnSyncSequenceRef.current;
void syncTurnMessages(sequence);
} catch (error) { } catch (error) {
const text = error instanceof Error ? error.message : "发送失败"; const text = error instanceof Error ? error.message : "发送失败";
const busy = text.includes("Codex 正在运行"); const busy = text.includes("Codex 正在运行");
@@ -512,6 +531,7 @@ export function CanvasLocalAgentPanel({ embedded, headless, autoConnect }: { emb
function clearAgentSession(patch: Parameters<typeof setAgentState>[0] = {}) { function clearAgentSession(patch: Parameters<typeof setAgentState>[0] = {}) {
loadThreadsSequenceRef.current += 1; loadThreadsSequenceRef.current += 1;
turnSyncSequenceRef.current += 1;
setAgentState({ setAgentState({
messages: [], messages: [],
threads: [], threads: [],
@@ -554,18 +574,19 @@ export function CanvasLocalAgentPanel({ embedded, headless, autoConnect }: { emb
} }
}; };
const deleteThread = async (threadId: string) => { const deleteThreads = async (threadIds: string[]) => {
if (!connected || !threadId || sending || waiting) return; if (!connected || !threadIds.length || sending || waiting) return;
setAgentState({ loadingThreads: true }); setAgentState({ loadingThreads: true });
try { try {
await fetchAgentJson(endpoint, token, `/agent/codex/threads/${encodeURIComponent(threadId)}/delete`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({}) }); await Promise.all(threadIds.map((threadId) => fetchAgentJson(endpoint, token, `/agent/codex/threads/${encodeURIComponent(threadId)}/delete`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({}) })));
const current = useAgentStore.getState(); const current = useAgentStore.getState();
const deleted = new Set(threadIds);
setAgentState({ setAgentState({
threads: current.threads.filter((thread) => thread.id !== threadId), threads: current.threads.filter((thread) => !deleted.has(thread.id)),
activeThreadId: current.activeThreadId === threadId ? "" : current.activeThreadId, activeThreadId: deleted.has(current.activeThreadId) ? "" : current.activeThreadId,
messages: current.activeThreadId === threadId ? [] : current.messages, messages: deleted.has(current.activeThreadId) ? [] : current.messages,
}); });
message.success("记录已删除"); message.success(`已删除 ${threadIds.length} 条记录`);
} catch (error) { } catch (error) {
addEventLog("删除对话失败", error); addEventLog("删除对话失败", error);
message.error(error instanceof Error ? error.message : "删除对话失败"); message.error(error instanceof Error ? error.message : "删除对话失败");
@@ -574,15 +595,14 @@ export function CanvasLocalAgentPanel({ embedded, headless, autoConnect }: { emb
} }
}; };
const confirmDeleteThread = (thread: AgentThreadSummary) => { const confirmDeleteThreads = (threadIds: string[]) => {
const label = thread.name || thread.preview || "未命名对话";
modal.confirm({ modal.confirm({
title: "删除对话记录", title: `删除 ${threadIds.length} 条对话记录`,
content: `确定删除「${label.length > 48 ? `${label.slice(0, 48)}...` : label}」吗?`, content: "删除后无法恢复,确定继续吗?",
okText: "删除", okText: "删除",
okType: "danger", okType: "danger",
cancelText: "取消", cancelText: "取消",
onOk: () => deleteThread(thread.id), onOk: () => deleteThreads(threadIds),
}); });
}; };
@@ -616,10 +636,21 @@ export function CanvasLocalAgentPanel({ embedded, headless, autoConnect }: { emb
const handleAgentEvent = (event: AgentEventPayload) => { const handleAgentEvent = (event: AgentEventPayload) => {
if (shouldLogAgentEvent(event)) addEventLog(eventTitle(event), event, event); if (shouldLogAgentEvent(event)) addEventLog(eventTitle(event), event, event);
if (event.type === "thread.started" && event.thread_id) setAgentState({ activeThreadId: event.thread_id }); if (event.type === "thread.started" && event.thread_id) setAgentState({ activeThreadId: event.thread_id });
if (event.type === "item.completed" && event.item?.type === "agent_message" && event.item.id) {
const currentMessages = useAgentStore.getState().messages;
const index = currentMessages.findIndex((message) => message.streamId === event.item?.id);
if (index >= 0) {
const text = stringText(event.item.text);
setAgentState({ messages: currentMessages.map((message, i) => (i === index ? { ...message, text: text || message.text, meta: usageText(event) || message.meta, streamId: undefined } : message)) });
return;
}
}
if (event.type === "turn.completed") setAgentState({ messages: useAgentStore.getState().messages.map((message) => (message.streamId ? { ...message, streamId: undefined } : message)) });
const item = formatAgentEvent(event); const item = formatAgentEvent(event);
if (item) addMessage(item); if (item) addMessage(item);
}; };
const streaming = messages.some((message) => message.streamId);
const content = ( const content = (
<> <>
<AgentPanelTabs <AgentPanelTabs
@@ -669,7 +700,7 @@ export function CanvasLocalAgentPanel({ embedded, headless, autoConnect }: { emb
onRefresh={() => void loadThreads()} onRefresh={() => void loadThreads()}
onNewThread={() => void startNewThread()} onNewThread={() => void startNewThread()}
onResumeThread={(threadId) => void resumeThread(threadId)} onResumeThread={(threadId) => void resumeThread(threadId)}
onDeleteThread={confirmDeleteThread} onDeleteThreads={confirmDeleteThreads}
/> />
) : activeTab === "log" ? ( ) : activeTab === "log" ? (
<AgentLogView <AgentLogView
@@ -696,7 +727,7 @@ export function CanvasLocalAgentPanel({ embedded, headless, autoConnect }: { emb
onApprove={approvePendingTool} onApprove={approvePendingTool}
/> />
) : null} ) : null}
{waiting && !pendingTool ? <AgentWorkingMessage theme={theme} /> : null} {(sending || waiting) && !streaming && !pendingTool ? <AgentWorkingMessage theme={theme} /> : null}
</div> </div>
{showScrollToBottom ? ( {showScrollToBottom ? (
<Tooltip title="滚动到底部" placement="left"> <Tooltip title="滚动到底部" placement="left">
@@ -967,7 +998,7 @@ function AgentHistoryView({
onRefresh, onRefresh,
onNewThread, onNewThread,
onResumeThread, onResumeThread,
onDeleteThread, onDeleteThreads,
}: { }: {
theme: (typeof canvasThemes)[keyof typeof canvasThemes]; theme: (typeof canvasThemes)[keyof typeof canvasThemes];
threads: AgentThreadSummary[]; threads: AgentThreadSummary[];
@@ -979,8 +1010,18 @@ function AgentHistoryView({
onRefresh: () => void; onRefresh: () => void;
onNewThread: () => void; onNewThread: () => void;
onResumeThread: (threadId: string) => void; onResumeThread: (threadId: string) => void;
onDeleteThread: (thread: AgentThreadSummary) => void; onDeleteThreads: (threadIds: string[]) => void;
}) { }) {
const [selectedIds, setSelectedIds] = useState(() => new Set<string>());
const selectedThreads = threads.filter((thread) => selectedIds.has(thread.id));
const allSelected = Boolean(threads.length) && selectedThreads.length === threads.length;
const toggleThread = (threadId: string) => {
setSelectedIds((current) => {
const next = new Set(current);
next.has(threadId) ? next.delete(threadId) : next.add(threadId);
return next;
});
};
return ( return (
<div className="thin-scrollbar min-h-0 flex-1 overflow-y-auto p-3"> <div className="thin-scrollbar min-h-0 flex-1 overflow-y-auto p-3">
<div className="space-y-3"> <div className="space-y-3">
@@ -992,10 +1033,16 @@ function AgentHistoryView({
</span> </span>
</div> </div>
<div className="flex flex-wrap items-center justify-between gap-2"> <div className="flex flex-wrap items-center justify-between gap-2">
<div className="text-sm" style={{ color: theme.node.muted }}> <div className="flex items-center gap-2 text-sm" style={{ color: theme.node.muted }}>
{threads.length ? `${threads.length} 条历史` : connected ? "暂无历史" : "未连接"} {threads.length ? <Checkbox checked={allSelected} indeterminate={Boolean(selectedThreads.length) && !allSelected} disabled={loading || busy} onChange={() => setSelectedIds(allSelected ? new Set() : new Set(threads.map((thread) => thread.id)))} /> : null}
<span>{selectedThreads.length ? `已选 ${selectedThreads.length}` : threads.length ? `${threads.length} 条历史` : connected ? "暂无历史" : "未连接"}</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{selectedThreads.length ? (
<Button size="small" danger type="text" icon={<Trash2 className="size-3.5" />} disabled={loading || busy} onClick={() => onDeleteThreads(selectedThreads.map((thread) => thread.id))}>
{selectedThreads.length}
</Button>
) : null}
<Button size="small" icon={<RefreshCw className={`size-3.5 ${loading ? "animate-spin" : ""}`} />} disabled={!connected || loading} onClick={onRefresh}> <Button size="small" icon={<RefreshCw className={`size-3.5 ${loading ? "animate-spin" : ""}`} />} disabled={!connected || loading} onClick={onRefresh}>
</Button> </Button>
@@ -1008,8 +1055,28 @@ function AgentHistoryView({
{threads.map((thread) => { {threads.map((thread) => {
const active = thread.id === activeThreadId; const active = thread.id === activeThreadId;
return ( return (
<div key={thread.id} className="rounded-lg border px-2.5 py-1.5 transition" style={{ borderColor: active ? theme.node.text : theme.node.stroke, background: "transparent", color: theme.node.text }}> <div
key={thread.id}
role="button"
tabIndex={0}
className="cursor-pointer rounded-lg border px-2.5 py-2 transition hover:bg-black/5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-current/20 dark:hover:bg-white/10"
style={{ borderColor: active ? theme.node.text : theme.node.stroke, background: "transparent", color: theme.node.text }}
onClick={() => onResumeThread(thread.id)}
onKeyDown={(event) => {
if (event.key !== "Enter" && event.key !== " ") return;
event.preventDefault();
onResumeThread(thread.id);
}}
>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Checkbox
checked={selectedIds.has(thread.id)}
disabled={loading || busy}
aria-label={`选择${thread.name || thread.preview || "未命名对话"}`}
onClick={(event) => event.stopPropagation()}
onKeyDown={(event) => event.stopPropagation()}
onChange={() => toggleThread(thread.id)}
/>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="flex min-w-0 items-center gap-1.5"> <div className="flex min-w-0 items-center gap-1.5">
{active ? ( {active ? (
@@ -1021,15 +1088,7 @@ function AgentHistoryView({
</div> </div>
<div className="truncate text-[11px] leading-4 opacity-65">{thread.preview || thread.id}</div> <div className="truncate text-[11px] leading-4 opacity-65">{thread.preview || thread.id}</div>
</div> </div>
<div className="flex shrink-0 items-center gap-1"> <span className="shrink-0 text-[10px] opacity-55">{formatThreadTime(thread.updatedAt || thread.createdAt)}</span>
<span className="text-[10px] opacity-55">{formatThreadTime(thread.updatedAt || thread.createdAt)}</span>
<Button size="small" className="!h-6 !px-2" disabled={loading || busy} onClick={() => onResumeThread(thread.id)}>
</Button>
<Tooltip title="删除记录">
<Button size="small" danger type="text" className="!h-6 !w-6 !min-w-6" disabled={loading || busy} icon={<Trash2 className="size-3.5" />} onClick={() => onDeleteThread(thread)} />
</Tooltip>
</div>
</div> </div>
</div> </div>
); );
@@ -1076,7 +1135,7 @@ function agentAttachmentToChatAttachment(item: AgentAttachment): CanvasAgentChat
function formatAgentEvent(event: AgentEventPayload): Omit<AgentChatItem, "id"> | null { function formatAgentEvent(event: AgentEventPayload): Omit<AgentChatItem, "id"> | null {
const item = event.item; const item = event.item;
if (event.type === "item.completed" && item?.type === "error") return { role: "error", title: "错误", text: normalizeText(item.message), detail: item }; if (event.type === "item.completed" && item?.type === "error") return { role: "error", title: "错误", text: normalizeText(item.message), detail: item };
if ((event.type === "item.updated" || event.type === "item.completed") && item?.type === "agent_message") return { role: "assistant", title: "Codex", text: stringText(item.text), meta: usageText(event), streamId: item.id }; if ((event.type === "item.updated" || event.type === "item.completed") && item?.type === "agent_message") return { role: "assistant", title: "Codex", text: stringText(item.text), meta: usageText(event), streamId: event.type === "item.updated" ? item.id : undefined };
if (event.type === "item.completed" && isMcpToolItem(item) && isReadTool(String(item?.tool || ""))) return { role: "tool", title: `${toolName(String(item?.tool || ""))}完成`, text: item?.error?.message || toolSummary(item), detail: toolDetail(item) }; if (event.type === "item.completed" && isMcpToolItem(item) && isReadTool(String(item?.tool || ""))) return { role: "tool", title: `${toolName(String(item?.tool || ""))}完成`, text: item?.error?.message || toolSummary(item), detail: toolDetail(item) };
const text = eventText(event); const text = eventText(event);
if (text) return { role: "assistant", title: "Codex", text, meta: usageText(event) }; if (text) return { role: "assistant", title: "Codex", text, meta: usageText(event) };
@@ -1343,6 +1402,7 @@ function normalizeHistoryMessages(messages: AgentChatItem[]) {
...item, ...item,
id: item.id || `history-${index}`, id: item.id || `history-${index}`,
text: normalizeText(item.text), text: normalizeText(item.text),
streamId: undefined,
})) }))
.filter((item) => item.text); .filter((item) => item.text);
} }
@@ -1356,6 +1416,10 @@ function createId() {
return randomId(); return randomId();
} }
function wait(ms: number) {
return new Promise((resolve) => window.setTimeout(resolve, ms));
}
function clamp(value: number, min: number, max: number) { function clamp(value: number, min: number, max: number) {
return Math.min(max, Math.max(min, value)); return Math.min(max, Math.max(min, value));
} }