@xiaohhhh1/canvas-agent 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -1
- package/agent-instructions.md +21 -0
- package/dist/agent/claude.d.ts +3 -0
- package/dist/agent/claude.js +46 -0
- package/dist/agent/codex-client.d.ts +73 -0
- package/dist/agent/codex-client.js +438 -0
- package/dist/agent/codex-history.d.ts +25 -0
- package/dist/agent/codex-history.js +405 -0
- package/dist/agent/codex-protocol.d.ts +208 -0
- package/dist/{agents.d.ts → agent/codex.d.ts} +34 -31
- package/dist/agent/codex.js +210 -0
- package/dist/agent/types.d.ts +14 -0
- package/dist/agent/types.js +1 -0
- package/dist/canvas/operations.d.ts +13 -0
- package/dist/canvas/operations.js +161 -0
- package/dist/{schemas.d.ts → canvas/schemas.d.ts} +21 -20
- package/dist/{schemas.js → canvas/schemas.js} +1 -0
- package/dist/{canvas-session.d.ts → canvas/session.d.ts} +21 -1
- package/dist/canvas/session.js +256 -0
- package/dist/{tools.d.ts → canvas/tools.d.ts} +13 -8
- package/dist/{tools.js → canvas/tools.js} +5 -0
- package/dist/{types.d.ts → canvas/types.d.ts} +1 -10
- package/dist/canvas/types.js +1 -0
- package/dist/config.d.ts +5 -1
- package/dist/config.js +22 -4
- package/dist/index.js +2 -2
- package/dist/server/http.d.ts +2 -0
- package/dist/{http-server.js → server/http.js} +100 -16
- package/dist/server/mcp.d.ts +2 -0
- package/dist/{mcp-server.js → server/mcp.js} +5 -2
- package/dist/utils/date.d.ts +2 -0
- package/dist/utils/date.js +7 -0
- package/dist/utils/logger.d.ts +17 -0
- package/dist/utils/logger.js +83 -0
- package/dist/utils/value.d.ts +5 -0
- package/dist/utils/value.js +8 -0
- package/package.json +7 -4
- package/dist/agents.js +0 -557
- package/dist/canvas-session.js +0 -391
- package/dist/http-server.d.ts +0 -1
- package/dist/mcp-server.d.ts +0 -1
- /package/dist/{types.js → agent/codex-protocol.js} +0 -0
|
@@ -1,19 +1,27 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { readFile, stat } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
1
4
|
import express from "express";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
5
|
+
import { runClaudeTurn } from "../agent/claude.js";
|
|
6
|
+
import { archiveCodexThread, interruptCodexTurn, isRecoverableThreadError, listCodexThreads, readCodexThread, resolveCodexApproval, resumeCodexThread, runCodexTurn, startCodexThread, summarizeCodexThread, verifyCodexThreadWorkspace } from "../agent/codex.js";
|
|
7
|
+
import { CanvasSession } from "../canvas/session.js";
|
|
8
|
+
import { DEFAULT_PORT, ensureSiteWorkspace, loadConfig, saveConfig, updateSiteWorkspace } from "../config.js";
|
|
9
|
+
import { startRelayBridge } from "../relay-bridge.js";
|
|
10
|
+
import { logger } from "../utils/logger.js";
|
|
11
|
+
/** 启动仅监听本机的 Canvas Agent HTTP 服务。 */
|
|
6
12
|
export function startHttpServer() {
|
|
7
13
|
const config = loadConfig(true);
|
|
8
14
|
const port = Number(process.env.PORT) || Number(new URL(config.url).port) || DEFAULT_PORT;
|
|
9
15
|
config.url = `http://127.0.0.1:${port}`;
|
|
10
16
|
saveConfig(config);
|
|
11
17
|
const session = new CanvasSession();
|
|
18
|
+
/** 将 Agent 事件广播到所属线程或全部网页。 */
|
|
12
19
|
const emit = (type, payload) => {
|
|
13
20
|
const data = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { value: payload };
|
|
14
21
|
const threadId = String(data.threadId || data.thread_id || ensureSiteWorkspace(config).activeThreadId || "");
|
|
15
22
|
threadId ? session.emitThread(type, threadId, data) : session.emitAll(type, data);
|
|
16
23
|
};
|
|
24
|
+
/** 保存并广播当前站点工作空间的活跃线程。 */
|
|
17
25
|
const setActiveThread = (activeThreadId, payload = {}) => {
|
|
18
26
|
const workspace = updateSiteWorkspace(config, { activeThreadId: activeThreadId || undefined });
|
|
19
27
|
session.emitThread("workspace_changed", activeThreadId, { ...payload, activeThreadId });
|
|
@@ -22,6 +30,18 @@ export function startHttpServer() {
|
|
|
22
30
|
const app = express();
|
|
23
31
|
app.disable("x-powered-by");
|
|
24
32
|
app.use(express.json({ limit: "30mb" }));
|
|
33
|
+
app.use((req, res, next) => {
|
|
34
|
+
if (!logger.enabled)
|
|
35
|
+
return next();
|
|
36
|
+
const startedAt = Date.now();
|
|
37
|
+
const url = requestUrl(req, config);
|
|
38
|
+
res.on("finish", () => {
|
|
39
|
+
if (req.method === "OPTIONS" || (res.statusCode < 400 && ["/health", "/canvas/state", "/canvas/activate"].includes(url.pathname)))
|
|
40
|
+
return;
|
|
41
|
+
logger.debug(`HTTP ${req.method} ${url.pathname}`, { status: res.statusCode, durationMs: Date.now() - startedAt });
|
|
42
|
+
});
|
|
43
|
+
next();
|
|
44
|
+
});
|
|
25
45
|
app.use((req, res, next) => {
|
|
26
46
|
const url = requestUrl(req, config);
|
|
27
47
|
if (!setCors(req, res, url, config))
|
|
@@ -58,6 +78,24 @@ export function startHttpServer() {
|
|
|
58
78
|
res.setHeader("Cache-Control", "no-store");
|
|
59
79
|
res.type(attachment.type).send(Buffer.from(data, "base64"));
|
|
60
80
|
}));
|
|
81
|
+
app.post("/agent/local-file/reveal", route(async (req, res) => {
|
|
82
|
+
const filePath = String(req.body?.path || "");
|
|
83
|
+
if (!path.isAbsolute(filePath))
|
|
84
|
+
return res.status(400).json({ ok: false, error: "文件路径必须是绝对路径" });
|
|
85
|
+
const file = await stat(filePath);
|
|
86
|
+
await revealLocalFile(filePath, file.isDirectory());
|
|
87
|
+
res.json({ ok: true });
|
|
88
|
+
}));
|
|
89
|
+
app.post("/agent/local-image", route(async (req, res) => {
|
|
90
|
+
const filePath = String(req.body?.path || "");
|
|
91
|
+
if (!path.isAbsolute(filePath) || !/\.(?:avif|gif|jpe?g|png|webp)$/i.test(filePath))
|
|
92
|
+
return res.status(400).json({ ok: false, error: "图片路径无效" });
|
|
93
|
+
const file = await stat(filePath);
|
|
94
|
+
if (!file.isFile())
|
|
95
|
+
return res.status(400).json({ ok: false, error: "图片文件无效" });
|
|
96
|
+
res.setHeader("Cache-Control", "no-store");
|
|
97
|
+
res.type(path.extname(filePath)).send(await readFile(filePath));
|
|
98
|
+
}));
|
|
61
99
|
app.post("/api/tools", route(async (req, res) => res.json({ ok: true, result: await session.callTool(req.body?.name, req.body?.input || {}) })));
|
|
62
100
|
app.get("/agent/codex/workspace", (_req, res) => {
|
|
63
101
|
const workspace = ensureSiteWorkspace(config);
|
|
@@ -68,15 +106,20 @@ export function startHttpServer() {
|
|
|
68
106
|
const result = await listCodexThreads(emit, { cwd: workspace.workspacePath, searchTerm: String(req.query.searchTerm || "") });
|
|
69
107
|
res.json({ ok: true, workspace, ...result });
|
|
70
108
|
}));
|
|
71
|
-
app.post("/agent/codex/threads/new", route(async (
|
|
109
|
+
app.post("/agent/codex/threads/new", route(async (req, res) => {
|
|
72
110
|
if (session.codexBusy)
|
|
73
111
|
return res.status(409).json({ ok: false, error: "Codex 正在运行,请等待当前任务完成" });
|
|
74
112
|
const workspace = ensureSiteWorkspace(config);
|
|
75
|
-
const thread = await startCodexThread(emit, workspace.workspacePath);
|
|
113
|
+
const thread = await startCodexThread(emit, workspace.workspacePath, permissionMode(req.body?.permissionMode));
|
|
76
114
|
const activeThreadId = String(thread.id || "");
|
|
77
115
|
const nextWorkspace = setActiveThread(activeThreadId, { emptyThread: true });
|
|
78
116
|
res.json({ ok: true, workspace: nextWorkspace, thread: summarizeCodexThread(thread), messages: [] });
|
|
79
117
|
}));
|
|
118
|
+
app.post("/agent/codex/threads/reset", (req, res) => {
|
|
119
|
+
if (session.codexBusy)
|
|
120
|
+
return res.status(409).json({ ok: false, error: "Codex 正在运行,请等待当前任务完成" });
|
|
121
|
+
res.json({ ok: true, workspace: setActiveThread("", { emptyThread: true, draftThread: true }) });
|
|
122
|
+
});
|
|
80
123
|
app.get("/agent/codex/threads/:threadId", route(async (req, res) => {
|
|
81
124
|
const workspace = ensureSiteWorkspace(config);
|
|
82
125
|
const threadId = routeParam(req.params.threadId);
|
|
@@ -94,7 +137,7 @@ export function startHttpServer() {
|
|
|
94
137
|
return res.status(409).json({ ok: false, error: "Codex 正在运行,请等待当前任务完成" });
|
|
95
138
|
const workspace = ensureSiteWorkspace(config);
|
|
96
139
|
const threadId = routeParam(req.params.threadId);
|
|
97
|
-
const result = await resumeCodexThread(emit, threadId, workspace.workspacePath);
|
|
140
|
+
const result = await resumeCodexThread(emit, threadId, workspace.workspacePath, permissionMode(req.body?.permissionMode));
|
|
98
141
|
const nextWorkspace = setActiveThread(threadId);
|
|
99
142
|
res.json({ ok: true, workspace: nextWorkspace, ...result });
|
|
100
143
|
}));
|
|
@@ -116,12 +159,13 @@ export function startHttpServer() {
|
|
|
116
159
|
if (!prompt.trim())
|
|
117
160
|
return res.status(400).json({ ok: false, error: "请输入任务内容" });
|
|
118
161
|
const clientId = String(req.body?.clientId || "");
|
|
162
|
+
logger.info("Codex turn accepted", { threadId: req.body?.threadId, promptLength: prompt.length, attachmentCount: attachments.length });
|
|
119
163
|
session.setCodexState({ busy: true, threadId: String(req.body?.threadId || workspace.activeThreadId || ""), turnId: "" });
|
|
120
164
|
try {
|
|
121
165
|
let threadId = String(req.body?.threadId || workspace.activeThreadId || "");
|
|
122
166
|
let turnId = "";
|
|
123
167
|
if (!threadId) {
|
|
124
|
-
const thread = await startCodexThread(emit, workspace.workspacePath);
|
|
168
|
+
const thread = await startCodexThread(emit, workspace.workspacePath, permissionMode(req.body?.permissionMode));
|
|
125
169
|
threadId = String(thread.id || "");
|
|
126
170
|
setActiveThread(threadId, { emptyThread: true });
|
|
127
171
|
}
|
|
@@ -135,13 +179,15 @@ export function startHttpServer() {
|
|
|
135
179
|
message: { id: String(req.body?.messageId || Date.now()), role: "user", text: String(req.body?.messageText || prompt || `发送了 ${attachments.length} 张图片`) },
|
|
136
180
|
};
|
|
137
181
|
let chatThreadId = "";
|
|
182
|
+
/** 将当前 turn 事件固定广播到实际线程。 */
|
|
138
183
|
const turnEmit = (type, payload) => {
|
|
139
184
|
const data = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { value: payload };
|
|
140
|
-
session.emitThread(type, threadId, data);
|
|
185
|
+
session.emitThread(type, threadId, { ...data, ...(turnId ? { turn_id: turnId } : {}) });
|
|
141
186
|
};
|
|
142
|
-
void runCodexTurn(
|
|
187
|
+
void runCodexTurn(withAttachmentContext(prompt, attachmentRefs), turnEmit, attachments, {
|
|
143
188
|
threadId,
|
|
144
189
|
cwd: workspace.workspacePath,
|
|
190
|
+
permissionMode: permissionMode(req.body?.permissionMode),
|
|
145
191
|
appEmit: emit,
|
|
146
192
|
onStart: clientId ? () => session.bindClient(clientId) : undefined,
|
|
147
193
|
onThread: (actualThreadId) => {
|
|
@@ -157,9 +203,11 @@ export function startHttpServer() {
|
|
|
157
203
|
},
|
|
158
204
|
onTurn: (actualTurnId) => {
|
|
159
205
|
turnId = actualTurnId;
|
|
206
|
+
logger.info("Codex turn started", { threadId, turnId });
|
|
160
207
|
session.setCodexState({ busy: true, threadId, turnId });
|
|
161
208
|
},
|
|
162
209
|
onFinish: () => {
|
|
210
|
+
logger.info("Codex turn finished", { threadId, turnId });
|
|
163
211
|
session.clearTurnAttachments(clientId);
|
|
164
212
|
if (clientId)
|
|
165
213
|
session.releaseClient(clientId);
|
|
@@ -173,16 +221,23 @@ export function startHttpServer() {
|
|
|
173
221
|
throw error;
|
|
174
222
|
}
|
|
175
223
|
}));
|
|
176
|
-
app.post("/agent/codex/
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
224
|
+
app.post("/agent/codex/approval", route(async (req, res) => {
|
|
225
|
+
const decision = String(req.body?.decision || "");
|
|
226
|
+
if (!["accept", "acceptForSession", "decline", "cancel"].includes(decision))
|
|
227
|
+
return res.status(400).json({ ok: false, error: "无效的审批决定" });
|
|
228
|
+
const ok = await resolveCodexApproval(String(req.body?.requestId || ""), decision);
|
|
229
|
+
res.status(ok ? 200 : 409).json({ ok, ...(ok ? {} : { error: "审批请求已失效" }) });
|
|
230
|
+
}));
|
|
231
|
+
app.post("/agent/codex/interrupt", route(async (req, res) => res.json({ ok: await interruptCodexTurn(String(req.body?.threadId || "")) })));
|
|
180
232
|
app.post("/agent/claude/turn", (req, res) => {
|
|
181
|
-
runClaudeTurn(
|
|
233
|
+
runClaudeTurn(String(req.body?.prompt || ""), emit);
|
|
182
234
|
res.json({ ok: true });
|
|
183
235
|
});
|
|
184
236
|
app.use((_req, res) => res.status(404).json({ ok: false, error: "not found" }));
|
|
185
|
-
app.use((error,
|
|
237
|
+
app.use((error, req, res, _next) => {
|
|
238
|
+
logger.error("HTTP request failed", { method: req.method, path: req.path, error });
|
|
239
|
+
res.status(500).json({ ok: false, error: error.message });
|
|
240
|
+
});
|
|
186
241
|
app.listen(port, "127.0.0.1", () => {
|
|
187
242
|
console.log("Infinite Canvas Agent");
|
|
188
243
|
console.log(`Local URL: ${config.url}`);
|
|
@@ -191,17 +246,44 @@ export function startHttpServer() {
|
|
|
191
246
|
console.log("Optional MCP add: codex mcp add infinite-canvas -- npx -y @xiaohhhh1/canvas-agent mcp");
|
|
192
247
|
console.log("Remove manually added MCP: codex mcp remove infinite-canvas");
|
|
193
248
|
startRelayBridge(config);
|
|
249
|
+
if (logger.enabled)
|
|
250
|
+
console.log(`Debug log: ${logger.filePath}`);
|
|
251
|
+
logger.info("Canvas Agent started", { url: config.url, workspace: ensureSiteWorkspace(config).workspacePath, debugLog: logger.filePath });
|
|
194
252
|
});
|
|
195
253
|
}
|
|
254
|
+
/** 将异步 Express 路由异常交给统一错误处理中间件。 */
|
|
196
255
|
function route(handler) {
|
|
197
256
|
return (req, res, next) => void handler(req, res).catch(next);
|
|
198
257
|
}
|
|
258
|
+
/** 从 Express 路由参数中读取单个字符串。 */
|
|
199
259
|
function routeParam(value) {
|
|
200
260
|
return Array.isArray(value) ? value[0] || "" : value;
|
|
201
261
|
}
|
|
262
|
+
function permissionMode(value) {
|
|
263
|
+
return value === "automatic" || value === "full" ? value : "request";
|
|
264
|
+
}
|
|
265
|
+
/** 使用当前操作系统的文件管理器定位本地文件。 */
|
|
266
|
+
function revealLocalFile(filePath, isDirectory) {
|
|
267
|
+
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer.exe" : "xdg-open";
|
|
268
|
+
const args = process.platform === "darwin"
|
|
269
|
+
? ["-R", filePath]
|
|
270
|
+
: process.platform === "win32"
|
|
271
|
+
? [isDirectory ? filePath : `/select,${filePath}`]
|
|
272
|
+
: [isDirectory ? filePath : path.dirname(filePath)];
|
|
273
|
+
return new Promise((resolve, reject) => {
|
|
274
|
+
const child = spawn(command, args, { detached: true, stdio: "ignore" });
|
|
275
|
+
child.once("spawn", () => {
|
|
276
|
+
child.unref();
|
|
277
|
+
resolve();
|
|
278
|
+
});
|
|
279
|
+
child.once("error", reject);
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
/** 结合服务配置解析当前请求 URL。 */
|
|
202
283
|
function requestUrl(req, config) {
|
|
203
284
|
return new URL(req.originalUrl || req.url || "/", config.url);
|
|
204
285
|
}
|
|
286
|
+
/** 设置跨域响应头并记录通过 token 授权的来源。 */
|
|
205
287
|
function setCors(req, res, url, config) {
|
|
206
288
|
const origin = req.headers.origin;
|
|
207
289
|
res.setHeader("Access-Control-Allow-Origin", origin || "*");
|
|
@@ -218,10 +300,12 @@ function setCors(req, res, url, config) {
|
|
|
218
300
|
res.setHeader("Vary", "Origin");
|
|
219
301
|
return config.origins.includes(origin);
|
|
220
302
|
}
|
|
303
|
+
/** 校验请求查询参数或请求头中的连接 token。 */
|
|
221
304
|
function validToken(req, url, token) {
|
|
222
305
|
const header = req.headers["x-canvas-agent-token"];
|
|
223
306
|
return url.searchParams.get("token") === token || header === token || (Array.isArray(header) && header.includes(token));
|
|
224
307
|
}
|
|
308
|
+
/** 向 Agent 提示词追加本轮图片附件引用说明。 */
|
|
225
309
|
function withAttachmentContext(prompt, attachments) {
|
|
226
310
|
if (!attachments.length)
|
|
227
311
|
return prompt;
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { toolDescriptions, toolInputSchemas, toolNames } from "../canvas/schemas.js";
|
|
4
|
+
import { AGENT_PROMPT, loadConfig, VERSION } from "../config.js";
|
|
5
|
+
/** 启动通过标准输入输出通信的 MCP 服务。 */
|
|
5
6
|
export async function startMcpServer() {
|
|
6
7
|
const config = loadConfig(true);
|
|
7
8
|
const server = new McpServer({ name: "canvas-agent", version: VERSION }, { instructions: AGENT_PROMPT });
|
|
8
9
|
toolNames.forEach((name) => registerCanvasTool(server, config, name));
|
|
9
10
|
await server.connect(new StdioServerTransport());
|
|
10
11
|
}
|
|
12
|
+
/** 向 MCP Server 注册单个 Canvas Agent 工具。 */
|
|
11
13
|
function registerCanvasTool(server, config, name) {
|
|
12
14
|
const schema = toolInputSchemas[name];
|
|
13
15
|
server.registerTool(name, { description: toolDescriptions[name], inputSchema: schema.shape }, async (input) => {
|
|
@@ -15,6 +17,7 @@ function registerCanvasTool(server, config, name) {
|
|
|
15
17
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
16
18
|
});
|
|
17
19
|
}
|
|
20
|
+
/** 将 MCP 工具调用转发到本地 Canvas Agent HTTP 服务。 */
|
|
18
21
|
async function postCanvasAgentTool(config, name, input) {
|
|
19
22
|
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 }) });
|
|
20
23
|
const body = (await res.json());
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** 将日期格式化为适合文件名使用的本地日期字符串。 */
|
|
2
|
+
export function formatDateForFilename(date = new Date()) {
|
|
3
|
+
const year = date.getFullYear();
|
|
4
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
5
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
6
|
+
return `${year}-${month}-${day}`;
|
|
7
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** 管理 Canvas Agent 的终端与文件 Debug 日志。 */
|
|
2
|
+
export declare class Logger {
|
|
3
|
+
readonly enabled: boolean;
|
|
4
|
+
readonly filePath: string;
|
|
5
|
+
private readonly logger;
|
|
6
|
+
/** 根据命令行 Debug 参数初始化日志输出。 */
|
|
7
|
+
constructor();
|
|
8
|
+
/** 输出 Debug 级别日志。 */
|
|
9
|
+
debug(message: string, details?: unknown): void;
|
|
10
|
+
/** 输出 Info 级别日志。 */
|
|
11
|
+
info(message: string, details?: unknown): void;
|
|
12
|
+
/** 输出 Warn 级别日志。 */
|
|
13
|
+
warn(message: string, details?: unknown): void;
|
|
14
|
+
/** 输出 Error 级别日志。 */
|
|
15
|
+
error(message: string, details?: unknown): void;
|
|
16
|
+
}
|
|
17
|
+
export declare const logger: Logger;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { inspect } from "node:util";
|
|
5
|
+
import winston, { format, transports } from "winston";
|
|
6
|
+
import { formatDateForFilename } from "./date.js";
|
|
7
|
+
/** 管理 Canvas Agent 的终端与文件 Debug 日志。 */
|
|
8
|
+
export class Logger {
|
|
9
|
+
enabled = process.argv.includes("--debug");
|
|
10
|
+
filePath = this.enabled ? path.join(os.homedir(), ".infinite-canvas", "logs", `canvas-agent-${formatDateForFilename()}.log`) : "";
|
|
11
|
+
logger;
|
|
12
|
+
/** 根据命令行 Debug 参数初始化日志输出。 */
|
|
13
|
+
constructor() {
|
|
14
|
+
if (!this.enabled) {
|
|
15
|
+
this.logger = null;
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
fs.mkdirSync(path.dirname(this.filePath), { recursive: true });
|
|
19
|
+
const line = format.printf(({ level, message, timestamp, details }) => `${timestamp} ${level.toUpperCase()} ${message}${formatDetails(details)}`);
|
|
20
|
+
this.logger = winston.createLogger({
|
|
21
|
+
level: "debug",
|
|
22
|
+
transports: [
|
|
23
|
+
new transports.Console({ format: format.combine(format.timestamp({ format: "HH:mm:ss" }), line) }),
|
|
24
|
+
new transports.File({ filename: this.filePath, format: format.combine(format.timestamp({ format: "HH:mm:ss" }), line) }),
|
|
25
|
+
],
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
/** 输出 Debug 级别日志。 */
|
|
29
|
+
debug(message, details) {
|
|
30
|
+
if (details === undefined)
|
|
31
|
+
this.logger?.debug(message);
|
|
32
|
+
else
|
|
33
|
+
this.logger?.debug(message, { details: sanitize(details) });
|
|
34
|
+
}
|
|
35
|
+
/** 输出 Info 级别日志。 */
|
|
36
|
+
info(message, details) {
|
|
37
|
+
if (details === undefined)
|
|
38
|
+
this.logger?.info(message);
|
|
39
|
+
else
|
|
40
|
+
this.logger?.info(message, { details: sanitize(details) });
|
|
41
|
+
}
|
|
42
|
+
/** 输出 Warn 级别日志。 */
|
|
43
|
+
warn(message, details) {
|
|
44
|
+
if (details === undefined)
|
|
45
|
+
this.logger?.warn(message);
|
|
46
|
+
else
|
|
47
|
+
this.logger?.warn(message, { details: sanitize(details) });
|
|
48
|
+
}
|
|
49
|
+
/** 输出 Error 级别日志。 */
|
|
50
|
+
error(message, details) {
|
|
51
|
+
if (details === undefined)
|
|
52
|
+
this.logger?.error(message);
|
|
53
|
+
else
|
|
54
|
+
this.logger?.error(message, { details: sanitize(details) });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** 将日志详情格式化为紧凑的单行文本。 */
|
|
58
|
+
function formatDetails(details) {
|
|
59
|
+
if (details === undefined)
|
|
60
|
+
return "";
|
|
61
|
+
if (!details || typeof details !== "object" || Array.isArray(details))
|
|
62
|
+
return ` ${inspect(details, { depth: null, breakLength: Infinity })}`;
|
|
63
|
+
const text = Object.entries(details).filter(([, value]) => value !== undefined).map(([key, value]) => `${key}=${inspect(value, { depth: null, breakLength: Infinity })}`).join(" ");
|
|
64
|
+
return text ? ` ${text}` : "";
|
|
65
|
+
}
|
|
66
|
+
/** 清理日志内容中的敏感数据和不可序列化引用。 */
|
|
67
|
+
function sanitize(value, key = "", seen = new WeakSet()) {
|
|
68
|
+
if (/token|authorization|api.?key|dataurl/i.test(key))
|
|
69
|
+
return "[REDACTED]";
|
|
70
|
+
if (typeof value === "string" && value.startsWith("data:"))
|
|
71
|
+
return `[DATA URL ${value.length} chars]`;
|
|
72
|
+
if (value instanceof Error)
|
|
73
|
+
return { name: value.name, message: value.message, stack: value.stack };
|
|
74
|
+
if (!value || typeof value !== "object")
|
|
75
|
+
return value;
|
|
76
|
+
if (seen.has(value))
|
|
77
|
+
return "[CIRCULAR]";
|
|
78
|
+
seen.add(value);
|
|
79
|
+
if (Array.isArray(value))
|
|
80
|
+
return value.map((item) => sanitize(item, key, seen));
|
|
81
|
+
return Object.fromEntries(Object.entries(value).map(([field, item]) => [field, sanitize(item, field, seen)]));
|
|
82
|
+
}
|
|
83
|
+
export const logger = new Logger();
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** 安全读取未知对象中的指定字段。 */
|
|
2
|
+
export function field(value, key) {
|
|
3
|
+
return value && typeof value === "object" ? value[key] : undefined;
|
|
4
|
+
}
|
|
5
|
+
/** 将未知异常转换为可展示的错误信息。 */
|
|
6
|
+
export function errorMessage(error) {
|
|
7
|
+
return error instanceof Error ? error.message : String(error);
|
|
8
|
+
}
|
package/package.json
CHANGED
|
@@ -1,28 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xiaohhhh1/canvas-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
7
7
|
"bin": {
|
|
8
|
-
"canvas-agent": "
|
|
8
|
+
"canvas-agent": "dist/index.js"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"dist",
|
|
12
|
+
"agent-instructions.md",
|
|
12
13
|
"README.md"
|
|
13
14
|
],
|
|
14
15
|
"scripts": {
|
|
15
16
|
"dev": "tsx src/index.ts",
|
|
16
|
-
"
|
|
17
|
+
"debug": "tsx src/index.ts --debug",
|
|
18
|
+
"test": "tsx --test src/canvas/session.test.ts",
|
|
17
19
|
"build": "tsc -p tsconfig.json",
|
|
18
20
|
"start": "node dist/index.js",
|
|
19
21
|
"prepack": "npm run build"
|
|
20
22
|
},
|
|
21
23
|
"dependencies": {
|
|
22
24
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
23
|
-
"@openai/codex": "
|
|
25
|
+
"@openai/codex": "0.145.0",
|
|
24
26
|
"express": "^5.1.0",
|
|
25
27
|
"ws": "^8.18.3",
|
|
28
|
+
"winston": "^3.19.0",
|
|
26
29
|
"zod": "^3.25.0"
|
|
27
30
|
},
|
|
28
31
|
"devDependencies": {
|