@gavinc-cn/touchstone-dsh 0.2.0 → 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/LICENSE +0 -0
- package/README.md +23 -15
- package/README.zh.md +17 -12
- package/auth.py +0 -0
- package/backend/lib/access.js +44 -0
- package/backend/lib/agents.js +341 -0
- package/backend/lib/args.js +94 -0
- package/backend/lib/auth.js +195 -0
- package/backend/lib/board.js +6154 -0
- package/backend/lib/builtin-assets.js +1177 -0
- package/backend/lib/casescan.js +456 -0
- package/backend/lib/chat.js +2053 -0
- package/backend/lib/cookie.js +76 -0
- package/backend/lib/db.js +2260 -0
- package/backend/lib/dshdriver.js +1148 -0
- package/backend/lib/dshevents.js +851 -0
- package/backend/lib/export-cases.js +261 -0
- package/backend/lib/feishu.js +1738 -0
- package/backend/lib/feishu_cfg.js +228 -0
- package/backend/lib/feishu_conv.js +1484 -0
- package/backend/lib/feishu_device.js +428 -0
- package/backend/lib/feishu_inbound.js +1674 -0
- package/backend/lib/feishu_provision.js +855 -0
- package/backend/lib/feishu_rest.js +398 -0
- package/backend/lib/feishu_slash.js +568 -0
- package/backend/lib/feishu_ws.js +950 -0
- package/backend/lib/http.js +312 -0
- package/backend/lib/lib.js +973 -0
- package/backend/lib/loadcase.js +749 -0
- package/backend/lib/loadgen.js +1407 -0
- package/backend/lib/localbus.js +164 -0
- package/backend/lib/lock.js +405 -0
- package/backend/lib/proc.js +278 -0
- package/backend/lib/prompts.js +1021 -0
- package/backend/lib/pybase64.js +114 -0
- package/backend/lib/pyjson.js +214 -0
- package/backend/lib/query.js +125 -0
- package/backend/lib/rag.js +1310 -0
- package/backend/lib/runner.js +4520 -0
- package/backend/lib/servercaps.js +182 -0
- package/backend/lib/serverstate.js +237 -0
- package/backend/lib/sessparse.js +1176 -0
- package/backend/lib/sse.js +207 -0
- package/backend/lib/static.js +84 -0
- package/backend/lib/taskjson.js +302 -0
- package/backend/lib/waitq.js +2114 -0
- package/backend/lib/worktree.js +588 -0
- package/backend/lib/zstd-frames.js +218 -0
- package/backend/routes/admin.js +553 -0
- package/backend/routes/agents.js +149 -0
- package/backend/routes/assets.js +145 -0
- package/backend/routes/board.js +1220 -0
- package/backend/routes/feishu.js +269 -0
- package/backend/routes/feishu_app.js +215 -0
- package/backend/routes/feishu_hook.js +358 -0
- package/backend/routes/index.js +297 -0
- package/backend/routes/projects.js +1059 -0
- package/backend/routes/sessions.js +946 -0
- package/backend/routes/tasks.js +1530 -0
- package/backend/schema/fixtures/ancient.sql +189 -0
- package/backend/schema/fixtures/pre_alter.sql +177 -0
- package/backend/schema/migrations.json +154 -0
- package/backend/schema/schema.json +1451 -0
- package/backend/schema/schema.sql +216 -0
- package/backend/server.js +498 -0
- package/board.py +30 -1
- package/builtin_assets.py +0 -0
- package/builtin_prompts/free_style/flow.md +0 -0
- package/builtin_prompts/free_style/load_test.md +0 -0
- package/builtin_prompts/free_style/templates/bug_report_template.md +0 -0
- package/builtin_prompts/free_style/templates/case_template.md +0 -0
- package/builtin_prompts/free_style/templates/index_template.md +0 -0
- package/builtin_prompts/free_style/templates/load_charts_template.md +0 -0
- package/builtin_prompts/free_style/templates/load_index_template.md +0 -0
- package/builtin_prompts/free_style/templates/load_plan_template.md +0 -0
- package/builtin_prompts/free_style/templates/load_scenario_template.md +0 -0
- package/builtin_prompts/free_style/templates/status_template.md +0 -0
- package/db.py +294 -129
- package/dsh-plugin/cordis.patch.yml +0 -0
- package/dsh-plugin/install.sh +22 -8
- package/dsh-plugin/lib/client.bundle.js +0 -0
- package/dsh-plugin/lib/index.js +299 -29
- package/dsh-plugin/lib/pack-closure.mjs +291 -0
- package/dsh-plugin/scripts/build.mjs +0 -0
- package/dsh-plugin/scripts/prepack-check.mjs +30 -2
- package/dsh-plugin/scripts/select-bundle.mjs +0 -0
- package/dsh-plugin/src/client.js +0 -0
- package/dshevents.py +0 -0
- package/export_cases.py +0 -0
- package/extensions/dsh-autocommit/asset.json +0 -0
- package/extensions/dsh-autocommit/pkg/lib/index.mjs +0 -0
- package/extensions/dsh-autocommit/pkg/package.json +0 -0
- package/feishu_conv.py +0 -0
- package/lib.py +0 -0
- package/lifecycle.py +116 -13
- package/loadcase.py +0 -0
- package/loadgen.py +0 -0
- package/localbus.py +0 -0
- package/migrate_p7b.py +0 -0
- package/package.json +6 -1
- package/platcompat.py +0 -0
- package/prompts.py +0 -0
- package/rag.py +0 -0
- package/requirements.txt +0 -0
- package/touchstone.py +0 -0
- package/waitq.py +0 -0
- package/webui/dist-plugin/assets/index-ChP3sg6y.css +1 -0
- package/webui/dist-plugin/assets/{index-JLwyZZxC.js → index-UrMyeAXF.js} +110 -110
- package/webui/dist-plugin/index.html +2 -2
- package/worktree.py +0 -0
- package/webui/dist-plugin/assets/index-tJ0TlvQ3.css +0 -1
package/dsh-plugin/lib/index.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Touchstone dsh 插件 — Node 薄壳(服务端半)
|
|
3
3
|
* 职责四件(路线 A 起,见 doc_ai/spec/dsh_plugin/dsh插件形态.md):
|
|
4
|
-
* 1. spawn Touchstone
|
|
4
|
+
* 1. spawn Touchstone 后端子进程(127.0.0.1 随机端口 + 插件版前端 + 免登信任头):
|
|
5
|
+
* 缺省 Node(`backend/server.js`,2026-10-15 M8 翻)/ `config.backend: python` 时 Python(`server.py`)
|
|
5
6
|
* 2. 向 dsh webServer 注册 /touchstone 前缀路由, 全量反代到子进程(流式管道, SSE 不缓冲)
|
|
6
7
|
* 3. agent 驱动(路线 A): 进程内会话池 + 会话事件 SSE 推送, 挂 /touchstone-agent 前缀;
|
|
7
8
|
* 驱动地址与一次性令牌经环境变量下发给 server.py(见 lib/agent-driver.js)
|
|
8
9
|
* 4. 插件停用时注销路由、断开 SSE、释放常驻会话并 SIGTERM 子进程(server.py 侧有优雅退出)
|
|
9
|
-
* 静态页/SPA 回退/REST/SSE
|
|
10
|
+
* 静态页/SPA 回退/REST/SSE 全部由后端自身承载, 薄壳本身不实现业务
|
|
11
|
+
* (两版后端共用同一份 apply() 其余逻辑: 端口解析、日志落盘、stopBackend、驱动下发都不分叉)。
|
|
10
12
|
*
|
|
11
13
|
* 生命周期自愈(2026-10-04,修「Plugins 面板停用不回收」):
|
|
12
14
|
* dsh 热重载换下条目时**不保证**走到本插件的 disposer。真机实测(隔离 profile):
|
|
@@ -92,6 +94,61 @@ const PYTHON_PROBE_TIMEOUT_MS = 5000;
|
|
|
92
94
|
*/
|
|
93
95
|
const PYTHON_PROBE_CODE = 'pass # touchstone-python-probe';
|
|
94
96
|
|
|
97
|
+
/**
|
|
98
|
+
* 后端实现选择(2026-10-11 M0 起为 `config.backend`;**2026-10-15 M8 翻缺省**):
|
|
99
|
+
* `config.backend` = `'node'`(**缺省**)| `'python'`(显式,仍完整可用)。
|
|
100
|
+
*
|
|
101
|
+
* 为什么是配置键而不是「有 Python 就用 Python、没有就自动用 Node」:换后端是**验收动作**,
|
|
102
|
+
* 必须能一行改回;自动探测会让「对拍还没过的版本」在用户机器上被静默启用。
|
|
103
|
+
*
|
|
104
|
+
* 缺省值变迁(M8,用户 2026-10-15 裁决「默认可以改为 nodejs 后台,但 python 版现在不能删」):
|
|
105
|
+
* - 翻转前:缺省 python(Node 是影子/可选路径,未验收前谁都不受影响);
|
|
106
|
+
* - 翻转后:缺省 node(Node 已在 M0–M7.5 逐项对拍过关,Gate 1 的全量对拍判据已达成);
|
|
107
|
+
* - Python 版**不退役**:`server.py` 与包内 `*.py` 照旧随包分发,`backend: python`
|
|
108
|
+
* 是观测期唯一的回滚动作(**必须显式写 `python`,删键现在等于 node**)。
|
|
109
|
+
*
|
|
110
|
+
* `backend: 'node'` 时整条 Python 链(解释器探测 + 依赖预检 + 缺依赖提示页)**一律跳过** ——
|
|
111
|
+
* 那正是换 Node 的收益本身(不再要求用户机器上有 Python 与 pip)。改由 Node 侧自己的能力探测
|
|
112
|
+
* (`node:sqlite`,见 `renderNodeRuntimeHint`)兜「宿主 Node 太老」这一类失败。
|
|
113
|
+
*/
|
|
114
|
+
const BACKEND_PYTHON = 'python';
|
|
115
|
+
const BACKEND_NODE = 'node';
|
|
116
|
+
|
|
117
|
+
/** Node 后端入口(相对 repoDir)。进程契约见方案 §3.2,两版 argv 只差入口路径与 node 选项。 */
|
|
118
|
+
const NODE_ENTRY_REL = 'backend/server.js';
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 只给 Node 后端的进程选项:`node:sqlite` 在 22.x 是实验特性,每次启动往 stderr 打
|
|
122
|
+
* `ExperimentalWarning`(R1c)——而 `plugin-backend.log` 是排障唯一证据源,噪声必须压掉。
|
|
123
|
+
*/
|
|
124
|
+
const NODE_EXTRA_FLAGS = ['--disable-warning=ExperimentalWarning'];
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 宿主 Node 能力探测(M8 新增)。判据是**能力**而不是版本号比较:
|
|
128
|
+
* `node:sqlite` 从 22.5 才有、且 22.19 之前必须显式 `--experimental-sqlite` 才 import 得到
|
|
129
|
+
* (方案 R1 实测:22.19 unflag 可用),而自编译/裁剪过的 Node 也可能没编进 sqlite。
|
|
130
|
+
* 所以真 spawn 一个 `process.execPath`(=随后要拉起后端的那一个二进制)探一次,
|
|
131
|
+
* 探测用的 flags 与 env 与随后 spawn **同一份** —— 与「缺省解释器改真 spawn 探一次」同一条口径。
|
|
132
|
+
*
|
|
133
|
+
* 探到 `DatabaseSync` 不是函数也算失败:import 成功但 API 缺失(22.5-22.18 带 flag 的形态)
|
|
134
|
+
* 会在建库那一刻才炸,那时用户看到的只是「后端起不来」。
|
|
135
|
+
*/
|
|
136
|
+
const NODE_PROBE_CODE = 'import("node:sqlite").then((m)=>{'
|
|
137
|
+
+ 'if(typeof m.DatabaseSync!=="function"){console.error("node:sqlite 无 DatabaseSync");process.exit(4)}'
|
|
138
|
+
+ 'console.log("node:sqlite ok");process.exit(0)},(e)=>{'
|
|
139
|
+
+ 'console.error(String((e&&e.message)||e));process.exit(3)})';
|
|
140
|
+
/** 探测超时(与 Python 侧 PREFLIGHT_TIMEOUT_MS 同量级:探不动就放行,绝不卡住激活) */
|
|
141
|
+
const NODE_PROBE_TIMEOUT_MS = 5000;
|
|
142
|
+
|
|
143
|
+
/** 解析 `config.backend`;未知值 warn 一句后按缺省 node 处理(不静默、不猜测) */
|
|
144
|
+
function resolveBackend(config, logger) {
|
|
145
|
+
const raw = config.backend;
|
|
146
|
+
if (raw === undefined || raw === null || raw === '') return BACKEND_NODE;
|
|
147
|
+
if (raw === BACKEND_PYTHON || raw === BACKEND_NODE) return raw;
|
|
148
|
+
logger.warn(`touchstone: 未知的 config.backend=${JSON.stringify(raw)}(只认 python/node),按缺省 node 处理`);
|
|
149
|
+
return BACKEND_NODE;
|
|
150
|
+
}
|
|
151
|
+
|
|
95
152
|
/** 子进程句柄与解析出的后端端口(每 profile 一个插件实例, 模块级单例即可) */
|
|
96
153
|
let child = null;
|
|
97
154
|
let backendPort = 0;
|
|
@@ -245,6 +302,75 @@ function preflightDeps(pythonPath, repoDir, logger, env) {
|
|
|
245
302
|
});
|
|
246
303
|
}
|
|
247
304
|
|
|
305
|
+
/**
|
|
306
|
+
* 真 spawn 探一次宿主 Node 能力(判据与理由见 `NODE_PROBE_CODE`),返回
|
|
307
|
+
* `{ ok, skipped, detail }`:
|
|
308
|
+
* ok=true —— 能力齐备(`detail` 是子进程 stdout 那一行,进日志当证据);
|
|
309
|
+
* ok=false —— 探到了且不满足(`detail` = 子进程 stderr / 退出码)⇒ 调用方**不 spawn 后端**;
|
|
310
|
+
* skipped —— 探测本身没跑成(超时 / 无法 spawn / 无输出)⇒ **照常启动后端**:
|
|
311
|
+
* 与 Python 依赖预检同一口径(预检不确定不许阻断一个本来能跑的部署),
|
|
312
|
+
* 真起不来还有「起来就崩」提示页兜底。
|
|
313
|
+
*
|
|
314
|
+
* env 必须与随后 spawn 后端同一份(profile 的 extraEnv 可能改 `NODE_OPTIONS`/`PATH`),
|
|
315
|
+
* flags 也同一份(`NODE_EXTRA_FLAGS`)——「探测用 A、启动用 B」的分裂是 Python 侧踩过的真 bug。
|
|
316
|
+
*/
|
|
317
|
+
function probeNodeRuntime(execPath, flags, env) {
|
|
318
|
+
return new Promise((resolve) => {
|
|
319
|
+
let settled = false;
|
|
320
|
+
const settle = (value) => {
|
|
321
|
+
if (settled) return;
|
|
322
|
+
settled = true;
|
|
323
|
+
resolve(value);
|
|
324
|
+
};
|
|
325
|
+
const skip = (why) => settle({ ok: false, skipped: true, detail: why });
|
|
326
|
+
let proc = null;
|
|
327
|
+
try {
|
|
328
|
+
proc = spawn(execPath, [...flags, '-e', NODE_PROBE_CODE], {
|
|
329
|
+
cwd: homedir(), env, stdio: ['ignore', 'pipe', 'pipe'],
|
|
330
|
+
});
|
|
331
|
+
} catch (error) {
|
|
332
|
+
skip(error && error.message ? error.message : String(error));
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
let stdout = '';
|
|
336
|
+
let stderr = '';
|
|
337
|
+
const timer = setTimeout(() => {
|
|
338
|
+
skip(`超过 ${NODE_PROBE_TIMEOUT_MS}ms`);
|
|
339
|
+
try {
|
|
340
|
+
proc.kill('SIGKILL');
|
|
341
|
+
} catch { /* 已退出:忽略 */ }
|
|
342
|
+
}, NODE_PROBE_TIMEOUT_MS);
|
|
343
|
+
if (timer.unref) timer.unref();
|
|
344
|
+
proc.stdout.on('data', (buf) => { stdout += String(buf); });
|
|
345
|
+
proc.stderr.on('data', (buf) => { stderr += String(buf); });
|
|
346
|
+
proc.on('error', (err) => {
|
|
347
|
+
clearTimeout(timer);
|
|
348
|
+
skip(err.message);
|
|
349
|
+
});
|
|
350
|
+
// 用 close 而不是 exit:exit 可能早于 stdout 最后一个数据块(Python 预检的同一教训)。
|
|
351
|
+
proc.on('close', (code) => {
|
|
352
|
+
clearTimeout(timer);
|
|
353
|
+
const out = stdout.trim().split('\n').pop() || '';
|
|
354
|
+
const err = stderr.trim().split('\n').pop() || '';
|
|
355
|
+
if (code === 0) {
|
|
356
|
+
if (!out) { skip('无输出'); return; }
|
|
357
|
+
settle({ ok: true, skipped: false, detail: out });
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
settle({ ok: false, skipped: false, detail: err || `探测进程退出 code=${code}` });
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* 能力探测的测试缝(`dev-check-backend.mjs` 场景 G 用;生产路径恒为 null ⇒ 走真探测)。
|
|
367
|
+
* 「宿主 Node 太老」这一支在真机上造不出来(本机是能力齐备的 Node),不给缝就只能是未验项。
|
|
368
|
+
*/
|
|
369
|
+
let nodeProbeForTest = null;
|
|
370
|
+
export function _setNodeRuntimeProbeForTest(fn) {
|
|
371
|
+
nodeProbeForTest = fn;
|
|
372
|
+
}
|
|
373
|
+
|
|
248
374
|
/**
|
|
249
375
|
* 依赖缺失时的面板提示页(代替 503 JSON): 缺什么 + 一条可直接复制的修复命令
|
|
250
376
|
* + 修完怎么让插件重来(停用再启用即会重跑 apply, 按 spec §8.8 不必重启 dsh web)。
|
|
@@ -295,6 +421,88 @@ function renderNoPythonHint() {
|
|
|
295
421
|
+ '</body></html>';
|
|
296
422
|
}
|
|
297
423
|
|
|
424
|
+
/**
|
|
425
|
+
* Node 后端入口缺失时的提示页(缺省实现;也可显式配 `backend: node`)。
|
|
426
|
+
* 与「找不到 Python」并列的一类失败:装的是老包(包里还没有 `backend/` 目录)、
|
|
427
|
+
* 或 repoDir 指到了老检出。这条路径**不早退**——照常注册路由,面板因此能看到原因,
|
|
428
|
+
* 而不是一个 404 白页。
|
|
429
|
+
*/
|
|
430
|
+
function renderNoNodeBackendHint(repoDir) {
|
|
431
|
+
return '<!doctype html><html lang="zh"><head><meta charset="utf-8">'
|
|
432
|
+
+ '<title>Touchstone 后端未启动</title></head>'
|
|
433
|
+
+ '<body style="margin:0;padding:2rem;font:14px/1.75 ui-monospace,Menlo,Consolas,monospace;'
|
|
434
|
+
+ 'background:#14161a;color:#e6e6e6">'
|
|
435
|
+
+ '<h2 style="color:#ffb454;margin:0 0 .9rem">Touchstone 后端未启动:Node 后端入口缺失</h2>'
|
|
436
|
+
+ `<p>没找到 <code>${escapeHtml(`${repoDir}/backend/server.js`)}</code>,`
|
|
437
|
+
+ '而本插件走的是 Node 后端(<b>缺省实现</b>,也可显式配 <code>backend: node</code>)。</p>'
|
|
438
|
+
+ '<p>两种常见原因:① 装的是不含 Node 后端的旧包 —— 重跑 <code>dsh-plugin/install.sh</code>;'
|
|
439
|
+
+ '② profile patch 里的 <code>repoDir</code> 指向了另一个检出 —— 改成含 '
|
|
440
|
+
+ '<code>backend/server.js</code> 的那个目录,或删掉该项用缺省(包自身目录)。</p>'
|
|
441
|
+
+ '<p style="color:#8b949e">改完把本插件<b>停用再启用</b>即会重试,不必重启 dsh web。'
|
|
442
|
+
+ '要退回 Python 后端:把 <code>backend</code> <b>显式写成</b> <code>python</code>'
|
|
443
|
+
+ '(包里的 <code>server.py</code> 仍在,Python 版未退役;注意<b>删掉该键现在等于 node</b>,'
|
|
444
|
+
+ '不能靠删键回退)。</p>'
|
|
445
|
+
+ '</body></html>';
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* 宿主 Node 能力不足时的提示页(M8 新增)。为什么需要:缺省翻成 node 之后,
|
|
450
|
+
* 「宿主 Node 太老」从「可选路径的边角情况」变成**默认路径上的头号失败**——
|
|
451
|
+
* `node:sqlite` 在 22.5 才有、22.19 之前要显式 `--experimental-sqlite`,更老的 Node 直接没有这个 builtin。
|
|
452
|
+
* 那时后端会在 import 期就退出,面板只会显示 503 JSON 或一段日志尾巴,用户不知道该升级什么、
|
|
453
|
+
* 也不知道其实**有一条一行配置的退路**。
|
|
454
|
+
*/
|
|
455
|
+
function renderNodeRuntimeHint({ execPath, version, detail, logPath }) {
|
|
456
|
+
return '<!doctype html><html lang="zh"><head><meta charset="utf-8">'
|
|
457
|
+
+ '<title>Touchstone 后端未启动</title></head>'
|
|
458
|
+
+ '<body style="margin:0;padding:2rem;font:14px/1.75 ui-monospace,Menlo,Consolas,monospace;'
|
|
459
|
+
+ 'background:#14161a;color:#e6e6e6">'
|
|
460
|
+
+ '<h2 style="color:#ffb454;margin:0 0 .9rem">Touchstone 后端未启动:宿主 Node 不满足要求</h2>'
|
|
461
|
+
+ `<p>当前运行 dsh 的 Node 是 <code>${escapeHtml(String(version))}</code>`
|
|
462
|
+
+ `(<code>${escapeHtml(String(execPath))}</code>),探测 <code>node:sqlite</code> 未通过:</p>`
|
|
463
|
+
+ `<pre style="background:#0d0f12;border:1px solid #2a2f36;border-radius:6px;padding:.8rem 1rem;`
|
|
464
|
+
+ `overflow:auto">${escapeHtml(String(detail || ''))}</pre>`
|
|
465
|
+
+ '<p>Node 后端要求 <b>Node >= 22.19</b>(该版本起 <code>node:sqlite</code> 免 flag 可用;'
|
|
466
|
+
+ '22.5–22.18 需要 <code>--experimental-sqlite</code>,更老的版本没有这个内建模块)。'
|
|
467
|
+
+ '两条出路任选一条:</p>'
|
|
468
|
+
+ '<p>① <b>升级 Node 到 22.19+</b>,重启 dsh web(后端用的是宿主那个 Node,'
|
|
469
|
+
+ '与 PATH 上的 <code>node</code> 无关);<br>'
|
|
470
|
+
+ '② <b>改回 Python 后端</b>:在 profile patch 的 <code>touchstone-dsh</code> 条目里'
|
|
471
|
+
+ ' <b>显式</b>写 <code>backend: python</code>(注意<b>删掉该键等于 node</b>),'
|
|
472
|
+
+ '然后停用再启用本插件 —— Python 版随包分发、未退役。</p>'
|
|
473
|
+
+ '<p style="color:#8b949e">后端日志:<code>'
|
|
474
|
+
+ `${escapeHtml(logPath || '<库目录>/plugin-backend.log')}</code></p>`
|
|
475
|
+
+ '</body></html>';
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* `backend: node` 且进程**起来就崩**时的提示页(M0 评审 I3)。
|
|
480
|
+
* 为什么需要:「入口缺失」只是新开关失败的一种;更常见的是入口在、跑不起来
|
|
481
|
+
* (宿主 Node 版本不认 `--disable-warning`、`dist-plugin` 缺失、被执行策略拦、脚本自身报错)。
|
|
482
|
+
* 那时 `backendPort` 一直是 0,proxy 只会回通用 503 JSON —— 面板里显示一段 JSON 原文,
|
|
483
|
+
* 用户既不知道发生了什么、也不知道去哪看。提示页给三件事:失败原因、日志路径、怎么回退。
|
|
484
|
+
*/
|
|
485
|
+
function renderBackendCrashHint({ entryPath, logPath, code, signal }) {
|
|
486
|
+
return '<!doctype html><html lang="zh"><head><meta charset="utf-8">'
|
|
487
|
+
+ '<title>Touchstone 后端未启动</title></head>'
|
|
488
|
+
+ '<body style="margin:0;padding:2rem;font:14px/1.75 ui-monospace,Menlo,Consolas,monospace;'
|
|
489
|
+
+ 'background:#14161a;color:#e6e6e6">'
|
|
490
|
+
+ '<h2 style="color:#ffb454;margin:0 0 .9rem">Touchstone 后端未启动:Node 后端进程退出</h2>'
|
|
491
|
+
+ `<p>入口 <code>${escapeHtml(entryPath)}</code> 起来了但没有报出监听端口就退出`
|
|
492
|
+
+ `(code=${escapeHtml(String(code))}${signal ? `, signal=${escapeHtml(String(signal))}` : ''})。</p>`
|
|
493
|
+
+ '<p>下一步:看后端日志里的最后几行(启动失败原因就在那里)——</p>'
|
|
494
|
+
+ `<pre style="background:#0d0f12;border:1px solid #2a2f36;border-radius:6px;padding:.8rem 1rem;`
|
|
495
|
+
+ `overflow:auto">tail -n 40 ${escapeHtml(logPath || '<库目录>/plugin-backend.log')}</pre>`
|
|
496
|
+
+ '<p>常见原因:<code>dist-plugin</code> 构建产物缺失、被执行策略拦下、入口脚本自身报错。'
|
|
497
|
+
+ '若日志里是 <code>node:sqlite</code> / <code>ERR_UNKNOWN_BUILTIN_MODULE</code> 一类,'
|
|
498
|
+
+ '那是宿主 Node 太老 —— 升级到 22.19+ 或改回 <code>backend: python</code>。'
|
|
499
|
+
+ '改完把本插件<b>停用再启用</b>即会重试。</p>'
|
|
500
|
+
+ '<p style="color:#8b949e">要退回 Python 后端:把 profile 里的 <code>backend</code> '
|
|
501
|
+
+ '<b>显式写成</b> <code>python</code>(Python 版随包分发、未退役;'
|
|
502
|
+
+ '注意<b>删掉该键现在等于 node</b>)。</p>'
|
|
503
|
+
+ '</body></html>';
|
|
504
|
+
}
|
|
505
|
+
|
|
298
506
|
/**
|
|
299
507
|
* 插件形态后端日志路径:与子进程用的**同一个库**同目录(`<库目录>/plugin-backend.log`)。
|
|
300
508
|
* 不落 node_modules 下的 .run:装机目录可能只读、重装即清,而且日志跟着库走才对得上
|
|
@@ -445,9 +653,16 @@ export async function apply(ctx, config = {}) {
|
|
|
445
653
|
// repoDir 缺省 = 包自身目录(自包含包: 包里就有 server.py 与 webui/dist-plugin), 用户零配置即可跑;
|
|
446
654
|
// 显式 config.repoDir 仍优先 —— 要指向开发中的工作副本、或包被拆开放置时才需要写。
|
|
447
655
|
const repoDir = config.repoDir || PACKAGE_DIR;
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
656
|
+
const backend = resolveBackend(config, logger);
|
|
657
|
+
const entryRel = backend === BACKEND_NODE ? NODE_ENTRY_REL : 'server.py';
|
|
658
|
+
const entryPath = join(repoDir, entryRel);
|
|
659
|
+
const entryMissing = !existsSync(entryPath);
|
|
660
|
+
if (entryMissing) {
|
|
661
|
+
logger.warn(`touchstone: 找不到 ${entryPath}(repoDir 配置有误, 或安装包不完整)`);
|
|
662
|
+
// Python 侧行为与改动前一致:不注册路由、直接退出(存量用户不受影响)。
|
|
663
|
+
// Node 侧改为继续往下走:注册路由 + backendHint,让新开关的失败在面板里自解释。
|
|
664
|
+
if (backend !== BACKEND_NODE) return;
|
|
665
|
+
backendHint = renderNoNodeBackendHint(repoDir);
|
|
451
666
|
}
|
|
452
667
|
// ① 启用即回收:上一份壳若没走到 disposer(热重载换下不 dispose),它的路由与
|
|
453
668
|
// server.py 子进程都还在。必须先回收干净再注册,否则本实例 register 抛
|
|
@@ -478,8 +693,9 @@ export async function apply(ctx, config = {}) {
|
|
|
478
693
|
const driverUrl = webServer && webServer.port
|
|
479
694
|
? `http://127.0.0.1:${webServer.port}/touchstone-agent` : '';
|
|
480
695
|
|
|
696
|
+
// 两个后端共用的参数尾巴(方案 §3.2 的进程契约):只差入口路径与 node 的进程选项,
|
|
697
|
+
// 因此从入口之后的部分**逐字相同** —— 薄壳不需要为哪个后端分叉。
|
|
481
698
|
const args = [
|
|
482
|
-
`${repoDir}/server.py`,
|
|
483
699
|
'--host', '127.0.0.1', // 只绑回环: 信任头等价 admin, 不得对外网开放
|
|
484
700
|
'--port', '0', // 随机端口, 按 stdout 的 TOUCHSTONE_LISTEN 行解析
|
|
485
701
|
'--web-dir', `${repoDir}/webui/dist-plugin`, // 插件版前端(base=/touchstone/)
|
|
@@ -507,35 +723,78 @@ export async function apply(ctx, config = {}) {
|
|
|
507
723
|
// 解释器(2026-10-11 改): 配置优先; 没配就按 `python` → `python3` 真探一次
|
|
508
724
|
// (Windows 上没有 python3 这个名字, 写死它会直接 ENOENT——那样连缺依赖都报不出来)。
|
|
509
725
|
// 探测用「与随后 spawn 后端同一份 env」, extraEnv 里改过 PATH 时两者才一致。
|
|
726
|
+
//
|
|
727
|
+
// 这一段按 backend 分叉**只到「决定用什么命令拉起」为止**:探测/预检属 Python 专有,
|
|
728
|
+
// 之后 spawn、日志落盘、端口解析、退出处理全部共用(见下面的 launch 分支)。
|
|
510
729
|
const childEnv = { ...process.env, ...extraEnv };
|
|
511
|
-
const pythonPath = config.pythonPath || await resolveDefaultPython(childEnv);
|
|
512
|
-
if (!pythonPath) {
|
|
513
|
-
backendHint = renderNoPythonHint();
|
|
514
|
-
logger.warn(`touchstone: 找不到 Python 解释器(依次试过 ${PYTHON_CANDIDATES.join(', ')}), 插件不启动后端`);
|
|
515
|
-
logger.warn('touchstone: 请安装 Python 3.12+ 或在本插件 config 里配 pythonPath, 然后停用再启用');
|
|
516
|
-
return;
|
|
517
|
-
}
|
|
518
|
-
const viaDefault = !config.pythonPath;
|
|
519
|
-
if (viaDefault) logger.info(`touchstone: 未配置 pythonPath, 缺省探到解释器: ${pythonPath}`);
|
|
520
|
-
const deps = await preflightDeps(pythonPath, repoDir, logger, childEnv);
|
|
521
730
|
let myChild = null;
|
|
522
731
|
/** 本次壳的开的后端日志流(teardown 关闭;null = 不可用/未启动) */
|
|
523
732
|
let myLog = null;
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
733
|
+
/** 待拉起的命令(null = 本次不拉后端:入口缺失/缺依赖/缺解释器,三种都有提示页) */
|
|
734
|
+
let launch = null;
|
|
735
|
+
if (backend === BACKEND_NODE) {
|
|
736
|
+
// Node 后端:不探测解释器、不跑依赖预检、不会有缺依赖提示页。
|
|
737
|
+
// 用 process.execPath(= 当前 dsh 宿主跑的那个 node)而不是 PATH 上的 `node`:
|
|
738
|
+
// 用户机器上可能根本没有 node 在 PATH 里(dsh 自己是打包运行的),
|
|
739
|
+
// 而且这样能保证「探测的 Node」与「拉起后端的 Node」是同一个二进制(同一个 runtime)。
|
|
740
|
+
if (!entryMissing) {
|
|
741
|
+
// 能力探测(M8):探不到的常见形态是 import 期就退出的后端 —— 那时日志里只有一行
|
|
742
|
+
// `ERR_UNKNOWN_BUILTIN_MODULE: No such built-in module: node:sqlite`,用户无从下手。
|
|
743
|
+
// 探测本身不确定(超时/无法 spawn)**不阻断启动**,见 probeNodeRuntime。
|
|
744
|
+
const probe = nodeProbeForTest
|
|
745
|
+
? await nodeProbeForTest()
|
|
746
|
+
: await probeNodeRuntime(process.execPath, NODE_EXTRA_FLAGS, childEnv);
|
|
747
|
+
const logPath = backendLogPath(extraEnv);
|
|
748
|
+
if (probe.ok) {
|
|
749
|
+
backendHint = '';
|
|
750
|
+
// 观测期(Gate 1)要靠这行判「这次到底跑的是哪一版、跑在什么 Node 上」。
|
|
751
|
+
logger.info(`touchstone: 宿主 Node ${process.versions.node} 能力探测通过(${probe.detail})`);
|
|
752
|
+
launch = { cmd: process.execPath, argv: [...NODE_EXTRA_FLAGS, entryPath, ...args] };
|
|
753
|
+
} else if (probe.skipped) {
|
|
754
|
+
backendHint = '';
|
|
755
|
+
logger.warn(`touchstone: 宿主 Node 能力探测未完成(${probe.detail}), 照常启动后端`);
|
|
756
|
+
launch = { cmd: process.execPath, argv: [...NODE_EXTRA_FLAGS, entryPath, ...args] };
|
|
757
|
+
} else {
|
|
758
|
+
backendHint = renderNodeRuntimeHint({
|
|
759
|
+
execPath: process.execPath, version: process.versions.node, detail: probe.detail, logPath,
|
|
760
|
+
});
|
|
761
|
+
logger.warn(`touchstone: 后端未启动 —— 宿主 Node ${process.versions.node} 不满足要求: ${probe.detail}`);
|
|
762
|
+
logger.warn('touchstone: 两条出路: ① 升级 Node 到 >= 22.19 后重启 dsh web; '
|
|
763
|
+
+ '② 在本插件 config 里显式写 backend: python(Python 版随包分发、未退役)后停用再启用');
|
|
764
|
+
}
|
|
765
|
+
}
|
|
529
766
|
} else {
|
|
530
|
-
|
|
531
|
-
if (
|
|
532
|
-
|
|
767
|
+
const pythonPath = config.pythonPath || await resolveDefaultPython(childEnv);
|
|
768
|
+
if (!pythonPath) {
|
|
769
|
+
backendHint = renderNoPythonHint();
|
|
770
|
+
logger.warn(`touchstone: 找不到 Python 解释器(依次试过 ${PYTHON_CANDIDATES.join(', ')}), 插件不启动后端`);
|
|
771
|
+
logger.warn('touchstone: 请安装 Python 3.12+ 或在本插件 config 里配 pythonPath, 然后停用再启用');
|
|
772
|
+
return;
|
|
533
773
|
}
|
|
534
|
-
|
|
774
|
+
const viaDefault = !config.pythonPath;
|
|
775
|
+
if (viaDefault) logger.info(`touchstone: 未配置 pythonPath, 缺省探到解释器: ${pythonPath}`);
|
|
776
|
+
const deps = await preflightDeps(pythonPath, repoDir, logger, childEnv);
|
|
777
|
+
if (deps.fatal.length) {
|
|
778
|
+
backendHint = renderDepHint(pythonPath, repoDir, deps.fatal, viaDefault);
|
|
779
|
+
logger.warn(`touchstone: 后端未启动 —— Python 运行依赖缺失: ${deps.fatal.join(', ')}`);
|
|
780
|
+
logger.warn(`touchstone: 修复: ${pythonPath} -m pip install -r ${join(repoDir, 'requirements.txt')}`);
|
|
781
|
+
logger.warn('touchstone: 装完把本插件停用再启用即会重试(不必重启 dsh web)');
|
|
782
|
+
} else {
|
|
783
|
+
backendHint = '';
|
|
784
|
+
if (deps.advisory.length) {
|
|
785
|
+
logger.warn(`touchstone: 可选依赖未安装(只影响对应功能, 不影响启动): ${deps.advisory.join(', ')}`);
|
|
786
|
+
}
|
|
787
|
+
launch = { cmd: pythonPath, argv: [entryPath, ...args] };
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
if (launch) {
|
|
792
|
+
logger.info(`touchstone: 启动后端(${backend}) ${launch.cmd} ${launch.argv.join(' ')}`);
|
|
535
793
|
// stdio[0] 必须是 pipe 且**永不写入**:这是父死感知的 A 通道——dsh 进程无论
|
|
536
|
-
// 优雅退出还是被 kill -9,OS
|
|
537
|
-
//
|
|
538
|
-
|
|
794
|
+
// 优雅退出还是被 kill -9,OS 都会关闭该管道写端,后端读到 EOF 即自主退出
|
|
795
|
+
// (Python 侧实测父 kill -9 后 1.94s 退出;Node 侧走 process.stdin 的 end/close)。
|
|
796
|
+
// 改成 'ignore' 会让该通道失效。
|
|
797
|
+
myChild = spawn(launch.cmd, launch.argv, {
|
|
539
798
|
cwd: repoDir,
|
|
540
799
|
env: childEnv,
|
|
541
800
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -549,25 +808,36 @@ export async function apply(ctx, config = {}) {
|
|
|
549
808
|
if (!myLog) return;
|
|
550
809
|
try { myLog.write(line + '\n'); } catch { /* 流已坏:忽略,绝不打断读取 */ }
|
|
551
810
|
};
|
|
811
|
+
let sawListen = false; // 是否报出过监听端口(判「起来就崩」用,见 renderBackendCrashHint)
|
|
552
812
|
pumpLines(myChild.stdout, (line) => {
|
|
553
813
|
writeLog(line);
|
|
554
814
|
const m = line.match(/^TOUCHSTONE_LISTEN (\d+)/);
|
|
555
815
|
if (m) {
|
|
816
|
+
sawListen = true;
|
|
556
817
|
backendPort = Number(m[1]);
|
|
557
818
|
logger.info(`touchstone: 后端就绪 127.0.0.1:${backendPort}`);
|
|
558
819
|
}
|
|
559
820
|
});
|
|
560
821
|
pumpLines(myChild.stderr, (line) => {
|
|
561
822
|
writeLog(line);
|
|
562
|
-
if (line.trim()) logger.warn(`touchstone[py]: ${line.trim()}`);
|
|
823
|
+
if (line.trim()) logger.warn(`touchstone[${backend === BACKEND_NODE ? 'node' : 'py'}]: ${line.trim()}`);
|
|
563
824
|
});
|
|
825
|
+
// 「起来就崩」不再只留一行 dsh 日志:Node 分支给面板一个可读提示页(Python 分支行为不动)。
|
|
826
|
+
const crashed = (code, sig) => {
|
|
827
|
+
if (sawListen || backend !== BACKEND_NODE) return; // 只在 Node 分支、且从未就绪时给提示页
|
|
828
|
+
backendHint = renderBackendCrashHint({
|
|
829
|
+
entryPath, logPath: backendLogPath(extraEnv), code, signal: sig,
|
|
830
|
+
});
|
|
831
|
+
};
|
|
564
832
|
myChild.on('exit', (code, sig) => {
|
|
565
833
|
writeLog(`[touchstone] 后端退出 code=${code} sig=${sig}`);
|
|
566
834
|
logger.warn(`touchstone: 后端退出 code=${code} sig=${sig}`);
|
|
835
|
+
crashed(code, sig);
|
|
567
836
|
});
|
|
568
837
|
myChild.on('error', (err) => {
|
|
569
838
|
writeLog(`[touchstone] 后端拉起失败: ${err.message}`);
|
|
570
839
|
logger.warn(`touchstone: 后端拉起失败: ${err.message}`);
|
|
840
|
+
crashed('spawn-error', null);
|
|
571
841
|
});
|
|
572
842
|
}
|
|
573
843
|
|