@xiaohhhh1/canvas-agent 0.4.4 → 0.4.6

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/dist/index.js CHANGED
@@ -2,9 +2,12 @@
2
2
  import { startHttpServer } from "./server/http.js";
3
3
  import { ensureHttpServer } from "./server/ensure-http.js";
4
4
  import { startMcpServer } from "./server/mcp.js";
5
+ import { startHttpSupervisor } from "./server/supervisor.js";
5
6
  if (process.argv[2] === "mcp") {
6
7
  await ensureHttpServer();
7
8
  await startMcpServer();
8
9
  }
10
+ else if (process.argv[2] === "watch")
11
+ startHttpSupervisor();
9
12
  else
10
13
  startHttpServer();
@@ -1,2 +1,3 @@
1
- /** 插件 MCP 启动时确保本机 HTTP/后台队列服务常驻,无需客户另开终端。 */
1
+ /** Ensure the local HTTP worker is running the same version as the MCP process. */
2
2
  export declare function ensureHttpServer(): Promise<void>;
3
+ export declare function healthVersion(value: unknown): string;
@@ -1,25 +1,57 @@
1
1
  import { spawn } from "node:child_process";
2
- import { loadConfig } from "../config.js";
3
- /** 插件 MCP 启动时确保本机 HTTP/后台队列服务常驻,无需客户另开终端。 */
2
+ import { loadConfig, VERSION } from "../config.js";
3
+ /** Ensure the local HTTP worker is running the same version as the MCP process. */
4
4
  export async function ensureHttpServer() {
5
5
  const config = loadConfig(true);
6
- if (await healthy(config.url))
6
+ const current = await probeHealth(config.url);
7
+ if (current.ok && current.version === VERSION)
7
8
  return;
9
+ if (current.ok) {
10
+ const stopped = await requestShutdown(config.url, config.token);
11
+ if (!stopped)
12
+ throw new Error(`本机 Canvas Agent ${current.version || "旧版本"} 仍在运行,请关闭后重试以升级到 ${VERSION}`);
13
+ for (let attempt = 0; attempt < 40 && (await probeHealth(config.url)).ok; attempt += 1) {
14
+ await new Promise((resolve) => setTimeout(resolve, 100));
15
+ }
16
+ if ((await probeHealth(config.url)).ok)
17
+ throw new Error("旧版 Canvas Agent 未能正常退出,请关闭后重试");
18
+ }
8
19
  const entry = process.argv[1];
9
20
  if (!entry)
10
21
  throw new Error("无法定位 Canvas Agent 启动文件");
11
- const child = spawn(process.execPath, [entry, "serve"], { detached: true, stdio: "ignore", windowsHide: true });
22
+ const child = spawn(process.execPath, [entry, "watch"], { detached: true, stdio: "ignore", windowsHide: true });
12
23
  child.unref();
13
24
  for (let attempt = 0; attempt < 40; attempt += 1) {
14
25
  await new Promise((resolve) => setTimeout(resolve, 250));
15
- if (await healthy(config.url))
26
+ const next = await probeHealth(config.url);
27
+ if (next.ok && next.version === VERSION)
16
28
  return;
17
29
  }
18
30
  throw new Error("本机 Canvas Agent 后台服务启动失败");
19
31
  }
20
- async function healthy(url) {
32
+ export function healthVersion(value) {
33
+ if (!value || typeof value !== "object" || Array.isArray(value))
34
+ return "";
35
+ return typeof value.version === "string" ? String(value.version) : "";
36
+ }
37
+ async function probeHealth(url) {
21
38
  try {
22
39
  const response = await fetch(new URL("/health", url), { signal: AbortSignal.timeout(800) });
40
+ if (!response.ok)
41
+ return { ok: false, version: "" };
42
+ return { ok: true, version: healthVersion(await response.json()) };
43
+ }
44
+ catch {
45
+ return { ok: false, version: "" };
46
+ }
47
+ }
48
+ async function requestShutdown(url, token) {
49
+ try {
50
+ const response = await fetch(new URL("/agent/shutdown", url), {
51
+ method: "POST",
52
+ headers: { "x-canvas-agent-token": token },
53
+ signal: AbortSignal.timeout(1500),
54
+ });
23
55
  return response.ok;
24
56
  }
25
57
  catch {
@@ -5,11 +5,12 @@ import express from "express";
5
5
  import { runClaudeTurn } from "../agent/claude.js";
6
6
  import { archiveCodexThread, interruptCodexTurn, isRecoverableThreadError, listCodexThreads, readCodexThread, resolveCodexApproval, resumeCodexThread, runCodexTurn, startCodexThread, summarizeCodexThread, verifyCodexThreadWorkspace } from "../agent/codex.js";
7
7
  import { CanvasSession } from "../canvas/session.js";
8
- import { DEFAULT_PORT, ensureSiteWorkspace, loadConfig, saveConfig, updateSiteWorkspace } from "../config.js";
8
+ import { DEFAULT_PORT, ensureSiteWorkspace, loadConfig, saveConfig, updateSiteWorkspace, VERSION } from "../config.js";
9
9
  import { startRelayBridge } from "../relay-bridge.js";
10
10
  import { logger } from "../utils/logger.js";
11
11
  import { windowsRootExecutable, windowsSystemExecutable } from "../utils/windows.js";
12
12
  import { WorkflowManager } from "../workflow/manager.js";
13
+ import { AGENT_REPLACED_EXIT_CODE } from "./supervisor.js";
13
14
  /** 启动仅监听本机的 Canvas Agent HTTP 服务。 */
14
15
  export function startHttpServer() {
15
16
  const config = loadConfig(true);
@@ -53,13 +54,21 @@ export function startHttpServer() {
53
54
  return void res.json({});
54
55
  next();
55
56
  });
56
- app.get("/health", (_req, res) => res.json(session.health()));
57
+ app.get("/health", (_req, res) => res.json({ ...session.health(), version: VERSION }));
57
58
  app.get("/config", (_req, res) => res.json({ ok: true, url: config.url, hasToken: true }));
58
59
  app.use((req, res, next) => {
59
60
  if (validToken(req, requestUrl(req, config), config.token))
60
61
  return next();
61
62
  res.status(401).json({ ok: false, error: "invalid token" });
62
63
  });
64
+ let httpServer;
65
+ app.post("/agent/shutdown", (_req, res) => {
66
+ res.json({ ok: true, version: VERSION });
67
+ setTimeout(() => {
68
+ httpServer?.close(() => process.exit(AGENT_REPLACED_EXIT_CODE));
69
+ setTimeout(() => process.exit(AGENT_REPLACED_EXIT_CODE), 2000).unref();
70
+ }, 50).unref();
71
+ });
63
72
  app.get("/events", (req, res) => session.openEvents(requestUrl(req, config), res));
64
73
  app.post("/canvas/state", (req, res) => {
65
74
  session.updateState(req.body, String(req.query.clientId || "") || undefined);
@@ -259,7 +268,7 @@ export function startHttpServer() {
259
268
  logger.error("HTTP request failed", { method: req.method, path: req.path, error });
260
269
  res.status(500).json({ ok: false, error: error.message });
261
270
  });
262
- app.listen(port, "127.0.0.1", () => {
271
+ httpServer = app.listen(port, "127.0.0.1", () => {
263
272
  console.log("Infinite Canvas Agent");
264
273
  console.log(`Local URL: ${config.url}`);
265
274
  console.log(`Connect token: ${config.token}`);
@@ -0,0 +1,4 @@
1
+ export declare const AGENT_REPLACED_EXIT_CODE = 75;
2
+ export declare function shouldRestartAgent(code: number | null, stopping: boolean): boolean;
3
+ /** Keep the local HTTP worker alive, but step aside when a newer package replaces it. */
4
+ export declare function startHttpSupervisor(): void;
@@ -0,0 +1,38 @@
1
+ import { spawn } from "node:child_process";
2
+ export const AGENT_REPLACED_EXIT_CODE = 75;
3
+ const RESTART_DELAY_MS = 1000;
4
+ export function shouldRestartAgent(code, stopping) {
5
+ return !stopping && code !== AGENT_REPLACED_EXIT_CODE;
6
+ }
7
+ /** Keep the local HTTP worker alive, but step aside when a newer package replaces it. */
8
+ export function startHttpSupervisor() {
9
+ const entry = process.argv[1];
10
+ if (!entry)
11
+ throw new Error("无法定位 Canvas Agent 启动文件");
12
+ let child;
13
+ let stopping = false;
14
+ let restartTimer;
15
+ const launch = () => {
16
+ if (stopping)
17
+ return;
18
+ child = spawn(process.execPath, [entry, "serve"], { stdio: "ignore", windowsHide: true });
19
+ child.once("exit", (code) => {
20
+ child = undefined;
21
+ if (!shouldRestartAgent(code, stopping))
22
+ return void process.exit(0);
23
+ restartTimer = setTimeout(launch, RESTART_DELAY_MS);
24
+ });
25
+ };
26
+ const stop = () => {
27
+ if (stopping)
28
+ return;
29
+ stopping = true;
30
+ if (restartTimer)
31
+ clearTimeout(restartTimer);
32
+ child?.kill();
33
+ setTimeout(() => process.exit(0), 2000).unref();
34
+ };
35
+ process.once("SIGINT", stop);
36
+ process.once("SIGTERM", stop);
37
+ launch();
38
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xiaohhhh1/canvas-agent",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",