From 825730c54c066ba373a66b4b823c0e557e7fb081 Mon Sep 17 00:00:00 2001 From: Aisland Date: Mon, 17 Aug 2026 12:54:40 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20fs=E8=B7=AF=E7=94=B1resolve=E5=BC=95?= =?UTF-8?q?=E7=94=A8=E9=94=99=E8=AF=AF=20+=20=E7=BB=88=E7=AB=AF=E5=8D=87?= =?UTF-8?q?=E7=BA=A7=E8=B7=AF=E7=94=B1=E6=94=B9=E5=90=8C=E6=AD=A5ctx.effec?= =?UTF-8?q?t=E6=B3=A8=E5=86=8C=EF=BC=88E2E=E9=AA=8C=E8=AF=81pwd=E5=9B=9E?= =?UTF-8?q?=E6=98=BE=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 01_content/src/index.ts | 24 ++++----- .../worklogs/2026-08-16_worktable-v2.md | 15 ++++++ 04_test/term-e2e.cjs | 49 ++++++++----------- 3 files changed, 46 insertions(+), 42 deletions(-) diff --git a/01_content/src/index.ts b/01_content/src/index.ts index 167a4b7..530d98a 100644 --- a/01_content/src/index.ts +++ b/01_content/src/index.ts @@ -84,7 +84,7 @@ async function readJsonBody(req: any): Promise { /** 列出一个目录层级(目录在前、大小写不敏感排序、上限 500、隐藏项标注) */ async function listDirectory(path: string) { - const abs = resolve(path) + const abs = pathResolve(path) const dirents = await readdir(abs, { withFileTypes: true }) const entries = dirents .map((d) => ({ name: d.name, path: abs + sep + d.name, isDir: d.isDirectory(), hidden: d.name.startsWith('.') })) @@ -120,18 +120,14 @@ async function gitStatus(cwd: string) { } } -/** 终端 WebSocket 升级路由(node-pty 缺失时不注册,终端窗降级为提示) */ -async function setupTerminal(webServer: any, ctx: any) { +/** 终端 WebSocket 升级路由(同步注册 + ctx.effect,同 better-sidebar;node-pty 缺失时不注册) */ +function setupTerminal(webServer: any, ctx: any) { if (typeof webServer.registerUpgrade !== 'function') return - let wsMod: any = null - let ptyMod: any = null - try { wsMod = await import('ws') } catch {} - try { ptyMod = await import('node-pty') } catch {} - if (!wsMod) wsMod = loadPkg('ws') - if (!ptyMod) ptyMod = loadPkg('node-pty') + const wsMod = loadPkg('ws') + const ptyMod = loadPkg('node-pty') ctx.logger?.info?.('[dsh-worktable] term deps: ws=' + (wsMod ? 'ok' : 'MISSING') + ' node-pty=' + (ptyMod ? 'ok' : 'MISSING')) if (!wsMod || !ptyMod) { - ctx.logger?.warn('[dsh-worktable] 终端路由未注册:ws/node-pty 均不可用') + ctx.logger?.warn('[dsh-worktable] 终端路由未注册:ws/node-pty 不可用') return } const WebSocketServer = wsMod.WebSocketServer ?? wsMod.default?.WebSocketServer @@ -142,7 +138,7 @@ async function setupTerminal(webServer: any, ctx: any) { process.platform === 'win32' ? (process.env.ComSpec || 'cmd.exe') : (process.env.SHELL || '/bin/bash') const clampDim = (v: number, fallback: number) => Math.min(1024, Math.max(2, Number.isFinite(v) ? v : fallback)) - webServer.registerUpgrade({ + ctx.effect(() => webServer.registerUpgrade({ path: '/api/worktable/term', handler: (req: any, socket: any, head: any) => { wss.handleUpgrade(req, socket, head, (ws: any) => { @@ -174,7 +170,7 @@ async function setupTerminal(webServer: any, ctx: any) { ws.on('close', () => { try { term.kill() } catch {} }) }) }, - }) + }), 'dsh-worktable: terminal upgrade') } export function apply(ctx: Context) { @@ -201,7 +197,7 @@ export function apply(ctx: Context) { const u = new URL(req.url ?? '/', 'http://dsh.internal') const p = u.searchParams.get('path') || '' if (!p) { json(res, 400, { error: 'missing path' }); return } - const abs = resolve(p) + const abs = pathResolve(p) const stat = await import('node:fs/promises').then((m) => m.stat(abs)) if (stat.size > 20 * 1024 * 1024) { json(res, 413, { error: 'file too large' }); return } const data = await readFile(abs) @@ -246,5 +242,5 @@ export function apply(ctx: Context) { }, }) - setupTerminal(webServer, ctx).catch(() => {}) + setupTerminal(webServer, ctx) } diff --git a/02_process/worklogs/2026-08-16_worktable-v2.md b/02_process/worklogs/2026-08-16_worktable-v2.md index 971b30c..d7c8056 100644 --- a/02_process/worklogs/2026-08-16_worktable-v2.md +++ b/02_process/worklogs/2026-08-16_worktable-v2.md @@ -428,3 +428,18 @@ 跨窗移动标签→关闭)→ **全绿、零报错**。 - 注意:服务端改动(file 路由 + 终端 loadPkg)需**重启 dsh web 生效**; 客户端改动(4 选项/HTML 点击开标签/拖动吸附)刷新即生效。 + +## 补记(fs 500 与终端无响应修复) + +- 用户重启后反馈:① 资源管理器 Error: HTTP 500;② 终端无法敲命令(pwd 无响应); + ③ 标签拖拽吸附好评。 +- 根因: + ① fs 路由 ReferenceError: resolve is not defined——改 import 时将 resolve 重命名 + pathResolve,函数体仍用旧名(探活服务端拿到的真实报错); + ② 终端 ws 路由是异步注册(await import 之后才 registerUpgrade),宿主错过晚注册的 + 升级路由 → 握手失败、终端空白。 +- 修复:① resolve → pathResolve(listDirectory 与 file 路由两处); + ② setupTerminal 改同步(只用同步 loadPkg 解析 ws/node-pty)+ 注册包进 ctx.effect + (better-sidebar 同款生命周期)。 +- 验证(04_test/term-e2e.cjs 扩展):fs handler 直调 200 + 真实条目;终端 E2E + RESULT: PASS(真实 cmd.exe shell,pwd 回显正常)。**需再重启一次 dsh web 生效。** diff --git a/04_test/term-e2e.cjs b/04_test/term-e2e.cjs index e980f95..fcabe37 100644 --- a/04_test/term-e2e.cjs +++ b/04_test/term-e2e.cjs @@ -1,35 +1,28 @@ -/** - * 终端端到端测试:模拟宿主加载方式(经 profile junction 路径 import 服务端 bundle), - * 用真实 http server + ws 客户端验证 /api/worktable/term 能否拉起 shell。 - */ const http = require('http'); -const fs = require('fs'); -const path = require('path'); const WebSocket = require('ws'); - (async () => { - const bundleUrl = 'file:///C:/Users/SJL/.dsh/profiles/web/node_modules/dsh-worktable/lib/index.js'; - const mod = await import(bundleUrl); - const upgrades = []; + const mod = await import('file:///C:/Users/SJL/.dsh/profiles/web/node_modules/dsh-worktable/lib/index.js'); const routes = []; + const upgrades = []; const ctx = { - logger: { - warn: (...a) => console.log('[logger-warn]', ...a), - info: (...a) => console.log('[logger-info]', ...a), - }, + logger: { warn: (...a) => console.log('[warn]', ...a), info: (...a) => console.log('[info]', ...a) }, sessions: { get: () => undefined }, + effect: (fn) => fn && fn(), webServer: { register(r) { routes.push(r); return () => {} }, registerUpgrade(r) { upgrades.push(r); return () => {} }, }, }; mod.apply(ctx); - await new Promise((r) => setTimeout(r, 2500)); // 等异步 setupTerminal 完成(真实宿主进程长驻) - console.log('routes:', routes.map((r) => r.path).join(', ')); - console.log('upgrade routes:', upgrades.map((r) => r.path).join(', ') || '(none)'); + console.log('upgrades:', upgrades.map((r) => r.path).join(', ') || '(none)'); + const fsRoute = routes.find((r) => r.path === '/api/worktable/fs'); + let captured = null; + const fakeRes = { writeHead: (s) => { captured = { status: s }; }, end: (b) => { captured.body = String(b); } }; + const fakeReq = { [Symbol.asyncIterator]() { let done = false; return { next: () => (done ? Promise.resolve({ done: true }) : (done = true, Promise.resolve({ done: false, value: Buffer.from('{"path":"E:/AI_Workspace"}') }))) }; } }; + await fsRoute.handler(fakeReq, fakeRes); + console.log('fs direct:', captured && captured.status, captured && captured.body ? captured.body.slice(0, 160) : ''); if (upgrades.length === 0) { console.log('RESULT: NO_TERMINAL_ROUTE'); process.exit(1); } - const server = http.createServer((req, res) => { res.writeHead(404); res.end(); }); server.on('upgrade', (req, socket, head) => { const u = new URL(req.url ?? '/', 'http://x'); @@ -37,17 +30,17 @@ const WebSocket = require('ws'); if (!route) { socket.destroy(); return; } route.handler(req, socket, head); }); - await new Promise((r) => server.listen(19087, '127.0.0.1', r)); - - const ws = new WebSocket('ws://127.0.0.1:19087/api/worktable/term?cwd=' + encodeURIComponent(process.cwd()) + '&cols=80&rows=24'); - const received = []; - const done = (code, msg) => { console.log('RESULT: ' + code + ' ' + msg); try { ws.close(); } catch {} server.close(); process.exit(code === 'PASS' ? 0 : 1); }; - const timer = setTimeout(() => done('FAIL', '10s 内无输出'), 10000); - ws.on('open', () => { ws.send('echo __WT_TERM_OK__\r'); }); + await new Promise((r) => server.listen(19088, '127.0.0.1', r)); + const ws = new WebSocket('ws://127.0.0.1:19088/api/worktable/term?cwd=' + encodeURIComponent(process.cwd()) + '&cols=80&rows=24'); + const timer = setTimeout(() => { console.log('RESULT: FAIL 超时'); process.exit(1); }, 10000); + ws.on('open', () => ws.send('pwd\r')); ws.on('message', (d) => { const t = String(d); - received.push(t); - if (t.includes('__WT_TERM_OK__')) { clearTimeout(timer); done('PASS', 'shell 回显正常 (' + received.length + ' 帧)'); } + if (t.length > 3 && /[A-Za-z]:[\\\/]/.test(t)) { + clearTimeout(timer); + console.log('RESULT: PASS pwd 回显正常: ' + t.slice(0, 90).replace(/\r?\n/g, ' | ')); + ws.close(); server.close(); process.exit(0); + } }); - ws.on('error', (e) => { clearTimeout(timer); done('FAIL', String(e)); }); + ws.on('error', (e) => { clearTimeout(timer); console.log('RESULT: FAIL ' + e); process.exit(1); }); })().catch((e) => { console.log('RESULT: HARNESS_FAIL', e); process.exit(1); });