infinite-canvas-local-proxy 0.2.0 → 0.2.1

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 CHANGED
@@ -69,4 +69,41 @@ http://127.0.0.1:23210/https://api.openai.com/v1/models
69
69
 
70
70
  ## 环境要求
71
71
 
72
- Node.js >= 18。
72
+ Node.js >= 18。不需要 Python、Visual Studio 编译工具,也不需要任何原生模块通过编译——安装阶段不会触发 `node-gyp`。
73
+
74
+ ### 任务存储的三种后端
75
+
76
+ 代理会按顺序自动挑选,不需要手动配置:
77
+
78
+ | 后端 | 条件 | 任务是否持久化 |
79
+ |---|---|---|
80
+ | `better-sqlite3` | 安装成功且与当前 Node 的 ABI 匹配 | 是 |
81
+ | `node:sqlite` | Node 22.5 及以上(内置) | 是 |
82
+ | `memory` | 上面两者都不可用 | 否,重启后清空 |
83
+
84
+ 启动日志和根路径 `GET /` 会显示当前使用的后端,例如:
85
+
86
+ ```json
87
+ {"taskStore":"node:sqlite","persistentTasks":true,"node":"22.22.2"}
88
+ ```
89
+
90
+ `better-sqlite3` 是可选依赖。某些 Node 版本(如 24)没有对应的预编译产物,安装时会尝试本地编译并失败,但 **npm 会跳过它并继续**,代理随后自动改用内置的 `node:sqlite`,功能不受影响。
91
+
92
+ Node 22.5 及以上首次加载内置 SQLite 时会打印一行 `ExperimentalWarning: SQLite is an experimental feature`,这是 Node 自身的提示,不影响使用。
93
+
94
+ ### 安装报错的排查
95
+
96
+ 如果看到 `prebuild-install warn install No prebuilt binaries found` 并跟着一长串 `gyp ERR! find Python`,说明当前 Node 版本没有 `better-sqlite3` 的预编译产物,于是退回到本地编译,而机器上又没装 Python。这种情况在新版本 Node 上很常见。
97
+
98
+ 0.2.1 起这个错误不会再中断安装。如果仍想彻底消除,任选其一:
99
+
100
+ - 升级到 Node 22 LTS 或更高,走内置 `node:sqlite`,完全不需要编译
101
+ - 或者安装 Python 3 与 Visual Studio Build Tools,让 `better-sqlite3` 能编译
102
+
103
+ 清除安装残留:
104
+
105
+ ```bash
106
+ rmdir /s /q "%LocalAppData%\npm-cache\_npx" # Windows
107
+ npm cache clean --force
108
+ ```
109
+
package/index.js CHANGED
@@ -226,6 +226,10 @@ async function forward(req, res, target) {
226
226
  export function createProxyServer(options = {}) {
227
227
  const dataDir = options.dataDir || process.env.CANVAS_PROXY_DATA_DIR || path.join(os.homedir(), ".canvas-proxy");
228
228
  const store = createTaskStore(dataDir);
229
+ if (!store.persistent) {
230
+ console.warn(`任务存储: 内存模式(未找到可用的 SQLite),重启后任务记录不会保留。升级到 Node 22.5+ 可自动启用内置 SQLite。`);
231
+ console.warn(`当前 Node ${process.versions.node}。`);
232
+ }
229
233
  const now = Date.now();
230
234
  store.recover(now);
231
235
  const taskTimeoutMs = positiveInteger(options.taskTimeoutMs ?? process.env.CANVAS_PROXY_TASK_TIMEOUT_MS, TASK_TIMEOUT_MS);
@@ -774,7 +778,7 @@ export function createProxyServer(options = {}) {
774
778
 
775
779
  const target = readTarget(req.url || "/");
776
780
  if (!target) {
777
- sendJson(res, 200, { app: "infinite-canvas", proxy: pkg.name, version: pkg.version, taskApiVersion: 1, usage: "/<full-target-url>" });
781
+ sendJson(res, 200, { app: "infinite-canvas", proxy: pkg.name, version: pkg.version, taskApiVersion: 1, taskStore: store.backend, persistentTasks: store.persistent, node: process.versions.node, usage: "/<full-target-url>" });
778
782
  return;
779
783
  }
780
784
  const startedAt = Date.now();
@@ -0,0 +1,132 @@
1
+ /**
2
+ * 内存版任务存储。只在 better-sqlite3 与 node:sqlite 都不可用时启用,
3
+ * 目的是让代理仍然能启动、转发照常工作,代价是重启后任务记录不再保留。
4
+ * 接口与基于 SQLite 的实现保持一致,主程序无需区分。
5
+ */
6
+ export function createMemoryTaskStore(resultDir) {
7
+ const tasks = new Map();
8
+
9
+ function toTask(row) {
10
+ if (!row) return null;
11
+ let metadata;
12
+ if (row.metadata_json) {
13
+ metadata = JSON.parse(row.metadata_json);
14
+ }
15
+ return {
16
+ id: row.id,
17
+ status: row.status,
18
+ createdAt: row.created_at,
19
+ updatedAt: row.updated_at,
20
+ expiresAt: row.expires_at,
21
+ responseStatus: row.response_status,
22
+ responseContentType: row.response_content_type,
23
+ responseFile: row.response_file,
24
+ error: row.error,
25
+ ...(row.client_request_id ? { clientRequestId: row.client_request_id } : {}),
26
+ ...(row.kind !== "request" ? { kind: row.kind } : {}),
27
+ ...(row.source ? { source: row.source } : {}),
28
+ ...(metadata ? { metadata } : {}),
29
+ };
30
+ }
31
+
32
+ function insert(task) {
33
+ const row = {
34
+ id: task.id,
35
+ status: "queued",
36
+ created_at: task.now,
37
+ updated_at: task.now,
38
+ expires_at: task.expiresAt,
39
+ response_status: null,
40
+ response_content_type: null,
41
+ response_file: null,
42
+ error: null,
43
+ client_request_id: task.clientRequestId || null,
44
+ kind: task.kind || "request",
45
+ source: task.source || null,
46
+ metadata_json: task.metadata ? JSON.stringify(task.metadata) : null,
47
+ };
48
+ tasks.set(row.id, row);
49
+ return row;
50
+ }
51
+
52
+ function findByClientRequestId(clientRequestId) {
53
+ if (!clientRequestId) return null;
54
+ for (const row of tasks.values()) {
55
+ if (row.client_request_id === clientRequestId) return row;
56
+ }
57
+ return null;
58
+ }
59
+
60
+ return {
61
+ resultDir,
62
+ backend: "memory",
63
+ persistent: false,
64
+ create(id, now, expiresAt) {
65
+ insert({ id, now, expiresAt, clientRequestId: null, kind: "request", source: null, metadata: null });
66
+ },
67
+ createOrGet(task) {
68
+ const resolved = { kind: "request", source: null, metadata: null, ...task };
69
+ if (resolved.clientRequestId) {
70
+ const existing = findByClientRequestId(resolved.clientRequestId);
71
+ if (existing) return { task: toTask(existing), created: false };
72
+ }
73
+ const row = insert(resolved);
74
+ return { task: toTask(row), created: true };
75
+ },
76
+ setStatus(id, status, now, error = null) {
77
+ const row = tasks.get(id);
78
+ if (!row) return;
79
+ row.status = status;
80
+ row.updated_at = now;
81
+ row.error = error;
82
+ },
83
+ setSucceeded(id, now, status, contentType, file) {
84
+ const row = tasks.get(id);
85
+ if (!row) return;
86
+ row.status = "succeeded";
87
+ row.updated_at = now;
88
+ row.response_status = status;
89
+ row.response_content_type = contentType;
90
+ row.response_file = file;
91
+ row.error = null;
92
+ },
93
+ get(id) {
94
+ return toTask(tasks.get(id));
95
+ },
96
+ getByClientRequestId(clientRequestId) {
97
+ return toTask(findByClientRequestId(clientRequestId));
98
+ },
99
+ listActive(now, limit = 100) {
100
+ const active = ["queued", "running", "submitting", "polling"];
101
+ return [...tasks.values()]
102
+ .filter((row) => row.expires_at > now && active.includes(row.status))
103
+ .sort((a, b) => b.created_at - a.created_at)
104
+ .slice(0, limit)
105
+ .map(toTask);
106
+ },
107
+ recover(now) {
108
+ const active = ["queued", "running", "submitting", "polling"];
109
+ let changes = 0;
110
+ for (const row of tasks.values()) {
111
+ if (!active.includes(row.status)) continue;
112
+ row.status = "unknown";
113
+ row.updated_at = now;
114
+ row.error = "Proxy restarted while the upstream request was running; its outcome is unknown.";
115
+ changes += 1;
116
+ }
117
+ return changes;
118
+ },
119
+ removeExpired(now) {
120
+ const finished = ["succeeded", "failed", "unknown"];
121
+ return [...tasks.values()]
122
+ .filter((row) => row.expires_at <= now && finished.includes(row.status))
123
+ .map((row) => ({ id: row.id, response_file: row.response_file }));
124
+ },
125
+ remove(id) {
126
+ tasks.delete(id);
127
+ },
128
+ close() {
129
+ tasks.clear();
130
+ },
131
+ };
132
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infinite-canvas-local-proxy",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Minimal local forwarding proxy that lets the Infinite Canvas web app call AI provider APIs without CORS restrictions.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -11,6 +11,8 @@
11
11
  "files": [
12
12
  "index.js",
13
13
  "task-store.js",
14
+ "sqlite-backend.js",
15
+ "memory-task-store.js",
14
16
  "script-runner.js",
15
17
  "README.md"
16
18
  ],
@@ -21,7 +23,7 @@
21
23
  "engines": {
22
24
  "node": ">=18"
23
25
  },
24
- "dependencies": {
26
+ "optionalDependencies": {
25
27
  "better-sqlite3": "^11.8.1"
26
28
  },
27
29
  "publishConfig": {
@@ -0,0 +1,102 @@
1
+ import { createRequire } from "node:module";
2
+
3
+ const require = createRequire(import.meta.url);
4
+
5
+ /**
6
+ * 尽量拿到一个可用的 SQLite 实现,按优先级:
7
+ * 1. better-sqlite3 —— Node 18/20/22 有预编译产物,装上即用
8
+ * 2. node:sqlite —— Node 22.5+ 内置,不需要任何依赖
9
+ * 都不可用时返回 null,由调用方降级为内存存储,保证代理仍能启动。
10
+ *
11
+ * 返回的对象只暴露 task-store 用到的最小接口:
12
+ * pragma(sql) / exec(sql) / prepare(sql).run|get|all / transaction(fn) / close()
13
+ */
14
+
15
+ export function openSqlite(file) {
16
+ const better = loadBetterSqlite3();
17
+ if (better) {
18
+ try {
19
+ return { db: new better(file), backend: "better-sqlite3" };
20
+ } catch {
21
+ // 二进制与当前 Node ABI 不匹配等情况,继续尝试内置实现
22
+ }
23
+ }
24
+
25
+ const nodeSqlite = loadNodeSqlite();
26
+ if (nodeSqlite) {
27
+ try {
28
+ return { db: new NodeSqliteAdapter(file, nodeSqlite), backend: "node:sqlite" };
29
+ } catch {
30
+ // 内置实现不可用,走内存降级
31
+ }
32
+ }
33
+
34
+ return null;
35
+ }
36
+
37
+ function loadBetterSqlite3() {
38
+ try {
39
+ const mod = require("better-sqlite3");
40
+ return typeof mod === "function" ? mod : mod.default;
41
+ } catch {
42
+ return null;
43
+ }
44
+ }
45
+
46
+ function loadNodeSqlite() {
47
+ try {
48
+ const mod = require("node:sqlite");
49
+ return mod && mod.DatabaseSync ? mod : null;
50
+ } catch {
51
+ return null;
52
+ }
53
+ }
54
+
55
+ /** 把 node:sqlite 的 DatabaseSync 适配成 better-sqlite3 的调用习惯 */
56
+ class NodeSqliteAdapter {
57
+ #db;
58
+
59
+ constructor(file, { DatabaseSync }) {
60
+ this.#db = new DatabaseSync(file);
61
+ }
62
+
63
+ pragma(statement) {
64
+ this.#db.exec(`PRAGMA ${statement}`);
65
+ }
66
+
67
+ exec(sql) {
68
+ this.#db.exec(sql);
69
+ }
70
+
71
+ prepare(sql) {
72
+ const statement = this.#db.prepare(sql);
73
+ return {
74
+ run: (...args) => statement.run(...args),
75
+ get: (...args) => statement.get(...args),
76
+ all: (...args) => statement.all(...args),
77
+ };
78
+ }
79
+
80
+ transaction(fn) {
81
+ const db = this.#db;
82
+ return (...args) => {
83
+ db.exec("BEGIN");
84
+ try {
85
+ const result = fn(...args);
86
+ db.exec("COMMIT");
87
+ return result;
88
+ } catch (error) {
89
+ try {
90
+ db.exec("ROLLBACK");
91
+ } catch {
92
+ // 事务可能已经失效,保留原始错误
93
+ }
94
+ throw error;
95
+ }
96
+ };
97
+ }
98
+
99
+ close() {
100
+ this.#db.close();
101
+ }
102
+ }
package/task-store.js CHANGED
@@ -1,15 +1,17 @@
1
1
  import { mkdirSync } from "node:fs";
2
- import { createRequire } from "node:module";
3
2
  import path from "node:path";
4
-
5
- const require = createRequire(import.meta.url);
6
- const Database = require("better-sqlite3");
3
+ import { openSqlite } from "./sqlite-backend.js";
4
+ import { createMemoryTaskStore } from "./memory-task-store.js";
7
5
 
8
6
  export function createTaskStore(dataDir) {
9
7
  mkdirSync(dataDir, { recursive: true, mode: 0o700 });
10
8
  const resultDir = path.join(dataDir, "results");
11
9
  mkdirSync(resultDir, { recursive: true, mode: 0o700 });
12
- const db = new Database(path.join(dataDir, "tasks.sqlite"));
10
+
11
+ const opened = openSqlite(path.join(dataDir, "tasks.sqlite"));
12
+ if (!opened) return createMemoryTaskStore(resultDir);
13
+ const { db, backend } = opened;
14
+
13
15
  db.pragma("journal_mode = WAL");
14
16
  db.pragma("synchronous = FULL");
15
17
  db.exec(`
@@ -100,6 +102,8 @@ export function createTaskStore(dataDir) {
100
102
 
101
103
  return {
102
104
  resultDir,
105
+ backend,
106
+ persistent: true,
103
107
  create(id, now, expiresAt) {
104
108
  insert.run({ id, now, expiresAt, clientRequestId: null, kind: "request", source: null, metadataJson: null });
105
109
  },