fix: 移除 props.useSessions(宿主useSyncExternalStore崩溃),改 ctx.sessions.list 订阅
This commit is contained in:
@@ -229,6 +229,26 @@ function findSidebar(start: HTMLElement | null): HTMLElement | null {
|
||||
/** 子座位注册 id 序列(模块级 store;apply 里订阅 slots 变化写入)。 */
|
||||
const registryStore: { ids: string[]; listeners: Set<() => void> } = { ids: [], listeners: new Set() }
|
||||
|
||||
/** 会话作用域快照(模块级;apply 里订阅 ctx.sessions.list 写入,组件与引擎只读) */
|
||||
const sessionScopeStore: {
|
||||
snapshot: { sessionId: string; cwd: string; jobs: any[] } | null
|
||||
} = { snapshot: null }
|
||||
|
||||
function syncSessionScope(list: any) {
|
||||
try {
|
||||
const snap = list.getSnapshot()
|
||||
const current: string = snap?.current ?? ''
|
||||
const entry = snap?.items?.find((it: any) => it.sessionId === current) ?? null
|
||||
sessionScopeStore.snapshot = {
|
||||
sessionId: current,
|
||||
cwd: entry?.cwd ?? '',
|
||||
jobs: (snap?.jobsBySession?.[current] ?? []) as any[],
|
||||
}
|
||||
} catch {
|
||||
sessionScopeStore.snapshot = { sessionId: '', cwd: '', jobs: [] }
|
||||
}
|
||||
}
|
||||
|
||||
function WorktableSection(props: any) {
|
||||
const wide = props.wide !== false
|
||||
const renderProjectSlot = typeof props.renderSlot === 'function' ? props.renderSlot : null
|
||||
@@ -272,17 +292,15 @@ function WorktableSection(props: any) {
|
||||
)
|
||||
|
||||
// 会话作用域(当前会话 + 工作目录)与后台任务:注入分栏引擎环境
|
||||
const useSessions = typeof props.useSessions === 'function' ? props.useSessions : null
|
||||
const sessionsSnap = useSessions ? useSessions() : null
|
||||
const currentEntry = sessionsSnap?.items?.find((it: any) => it.sessionId === sessionsSnap?.current) ?? null
|
||||
const scopeRef = useRef<{ sessionId: string; cwd: string }>({ sessionId: '', cwd: '' })
|
||||
const jobsRef = useRef<any[]>([])
|
||||
scopeRef.current = { sessionId: sessionsSnap?.current ?? '', cwd: currentEntry?.cwd ?? '' }
|
||||
jobsRef.current = sessionsSnap?.jobsBySession?.[sessionsSnap?.current] ?? []
|
||||
// 注意:不走 props.useSessions hook(其宿主包装在部分版本会触发 useSyncExternalStore
|
||||
// 崩溃),改为 apply 里订阅 ctx.sessions.list 后写入模块级 store,此处直接读取。
|
||||
useEffect(() => {
|
||||
setSplitEnv({
|
||||
getScope: () => scopeRef.current,
|
||||
getJobs: () => jobsRef.current,
|
||||
getScope: () => {
|
||||
const s = sessionScopeStore.snapshot
|
||||
return s ? { sessionId: s.sessionId, cwd: s.cwd } : null
|
||||
},
|
||||
getJobs: () => sessionScopeStore.snapshot?.jobs ?? [],
|
||||
})
|
||||
return () => setSplitEnv(null)
|
||||
}, [])
|
||||
@@ -911,7 +929,7 @@ function WorktableSection(props: any) {
|
||||
)
|
||||
}
|
||||
|
||||
export const inject = ['slots', 'locale']
|
||||
export const inject = ['slots', 'locale', 'sessions']
|
||||
|
||||
export function apply(ctx: any) {
|
||||
ctx.effect(() => {
|
||||
@@ -942,6 +960,14 @@ export function apply(ctx: any) {
|
||||
syncIds()
|
||||
ctx.effect(() => disposeSubscribe, 'dsh-worktable: project registry watch')
|
||||
|
||||
// 会话作用域(当前会话 cwd 与后台任务)→ 功能窗数据源
|
||||
const sessionsList = ctx.sessions?.list
|
||||
if (sessionsList && typeof sessionsList.getSnapshot === 'function') {
|
||||
syncSessionScope(sessionsList)
|
||||
const disposeScope = sessionsList.subscribe(() => syncSessionScope(sessionsList))
|
||||
ctx.effect(() => disposeScope, 'dsh-worktable: session scope watch')
|
||||
}
|
||||
|
||||
// 分栏工作区浮层(M1 通用引擎,shell.overlay 座位)
|
||||
ctx.slots.inject('shell.overlay', () => ctx.slots.register({
|
||||
name: 'shell.overlay',
|
||||
|
||||
@@ -357,3 +357,19 @@
|
||||
未安排自动重启(用户要求,其他窗口仍在工作)。
|
||||
- 待后续:文件打开/编辑、SCM diff/暂存/提交、终端 cwd 信任客户端(better-sidebar 用
|
||||
服务端 header.cwd,后续对齐)、jobs 输出回放。
|
||||
|
||||
## 补记(重启后工作台消失:useSessions 崩溃排查与修复)
|
||||
|
||||
- 现象:用户重启 dsh web 后侧边栏工作台与各项目卡片全部消失(服务端正常、bundle 正常下发)。
|
||||
- 排查:opencli 绑定用户标签页失败(被另一 Chrome 扩展占用调试通道);改用
|
||||
Node + headless Chrome + CDP 自建诊断(04_test/headless-diag.cjs),在真实浏览器引擎
|
||||
中复现并捕获到根因:`slot entry crashed in 'sidebar.footer.action':
|
||||
TypeError: w is not a function`(宿主 useSyncExternalStore 包装层)——触发点是
|
||||
WorktableSection 调用 `props.useSessions()`(GlobalStandardProps 的 selector hook
|
||||
在该宿主版本的 footer.action 座位里崩坏)。
|
||||
- 修复:不再使用 props.useSessions;改为 apply() 里订阅 `ctx.sessions.list`
|
||||
(ObservableSnapshot.getSnapshot/subscribe,client inject 增加 'sessions'),
|
||||
写入模块级 sessionScopeStore;组件与分栏引擎(setSplitEnv getScope/getJobs)直接读该快照。
|
||||
- 回归验证:headless Chrome 重跑——wtSection/railBox 渲染正常、ERRORS_COUNT=0;
|
||||
用户刷新即恢复(客户端实时下发,无需重启)。
|
||||
- 工具沉淀:04_test/headless-diag.cjs(headless Chrome + CDP 页面诊断,可复用)。
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
const fs = require("fs");
|
||||
const vm = require("vm");
|
||||
const code = fs.readFileSync("E:/AI_Workspace/DeepseekHarness/Projects/dsh-worktable/01_content/lib/client.js", "utf8");
|
||||
function makeEl() {
|
||||
return {
|
||||
setAttribute(){}, removeAttribute(){}, appendChild(){}, remove(){}, addEventListener(){},
|
||||
removeEventListener(){}, classList: { add(){}, remove(){}, contains(){ return false } },
|
||||
style: {}, dataset: {}, textContent: "", children: [],
|
||||
};
|
||||
}
|
||||
let registered = null;
|
||||
const moduleLoader = { load(spec){ registered = spec; } };
|
||||
const sandbox = { self: null,
|
||||
console, setTimeout, clearTimeout, setInterval, clearInterval,
|
||||
window: {
|
||||
__ModuleLoader__: moduleLoader,
|
||||
addEventListener(){}, removeEventListener(){}, dispatchEvent(){ return true },
|
||||
innerWidth: 1440, innerHeight: 900, setTimeout, clearTimeout, setInterval, clearInterval,
|
||||
},
|
||||
__ModuleLoader__: moduleLoader,
|
||||
document: {
|
||||
createElement(){ return makeEl() },
|
||||
head: { appendChild(){}, removeChild(){} },
|
||||
body: { appendChild(){} },
|
||||
querySelector(){ return null }, querySelectorAll(){ return [] },
|
||||
addEventListener(){}, removeEventListener(){},
|
||||
},
|
||||
localStorage: { getItem(){ return null }, setItem(){}, removeItem(){} },
|
||||
CustomEvent: class { constructor(t, o){ this.type = t; this.detail = o && o.detail } },
|
||||
ResizeObserver: class { observe(){} disconnect(){} },
|
||||
MutationObserver: class { observe(){} disconnect(){} },
|
||||
navigator: {}, location: { protocol: "http:", host: "127.0.0.1:3080" },
|
||||
fetch: async () => { throw new Error("no fetch") },
|
||||
WebSocket: class {},
|
||||
URL: require("url").URL, URLSearchParams: require("url").URLSearchParams,
|
||||
TextDecoder: require("util").TextDecoder, TextEncoder: require("util").TextEncoder,
|
||||
performance: { now: () => 0 },
|
||||
};
|
||||
sandbox.self = sandbox; try {
|
||||
vm.createContext(sandbox);
|
||||
vm.runInContext(code, sandbox, { filename: "client.js" });
|
||||
console.log("模块作用域执行 OK;已注册:", registered && registered.id);
|
||||
if (registered) {
|
||||
const fakeRequire = (name) => {
|
||||
const stubs = {
|
||||
"react": { useState: (v)=>[v,()=>{}], useEffect: ()=>{}, useLayoutEffect: ()=>{}, useMemo: (f)=>f(), useCallback: (f)=>f, useRef: (v)=>({current:v}), createElement: ()=>null, Fragment: null },
|
||||
"react/jsx-runtime": { jsx: ()=>({}) },
|
||||
"react/jsx-dev-runtime": { jsx: ()=>({}) },
|
||||
"@deepseek-ai/dsh-client-ui-slots": {},
|
||||
"@deepseek-ai/dsh-client-ui-primitives": {},
|
||||
"@deepseek-ai/dsh-client-locale": {},
|
||||
};
|
||||
if (name in stubs) return stubs[name];
|
||||
throw new Error("unexpected require: " + name);
|
||||
};
|
||||
sandbox.self = sandbox; try {
|
||||
const exports = registered.factory(fakeRequire);
|
||||
console.log("工厂执行 OK, exports keys:", Object.keys(exports || {}));
|
||||
} catch (e) {
|
||||
console.log("工厂执行抛错:", e && e.stack ? e.stack.split("\n").slice(0,8).join("\n") : e);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("模块作用域抛错:", e && e.stack ? e.stack.split("\n").slice(0,10).join("\n") : e);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
|
||||
const http = require('http');
|
||||
const { spawn } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const WebSocket = require('ws');
|
||||
|
||||
const PORT = 9333;
|
||||
const candidates = [
|
||||
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
||||
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
|
||||
'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
|
||||
'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe',
|
||||
];
|
||||
const chrome = candidates.find((p) => fs.existsSync(p));
|
||||
if (!chrome) { console.log('NO_BROWSER_FOUND'); process.exit(1); }
|
||||
console.log('browser:', chrome);
|
||||
|
||||
const proc = spawn(chrome, [
|
||||
'--headless=new', '--disable-gpu', '--no-first-run', '--no-default-browser-check',
|
||||
'--remote-debugging-port=' + PORT,
|
||||
'--user-data-dir=C:\\Users\\SJL\\AppData\\Local\\Temp\\wt-chrome-debug',
|
||||
'about:blank',
|
||||
], { stdio: 'ignore' });
|
||||
|
||||
function getJSON(path) {
|
||||
return new Promise((res, rej) => {
|
||||
http.get({ host: '127.0.0.1', port: PORT, path }, (r) => {
|
||||
let d = '';
|
||||
r.on('data', (c) => d += c);
|
||||
r.on('end', () => { try { res(JSON.parse(d)) } catch (e) { rej(e) } });
|
||||
}).on('error', rej);
|
||||
});
|
||||
}
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
(async () => {
|
||||
let list = null;
|
||||
for (let i = 0; i < 40; i++) {
|
||||
try { list = await getJSON('/json/list'); if (list && list.length) break; } catch {}
|
||||
await sleep(500);
|
||||
}
|
||||
if (!list || !list.length) { console.log('NO_DEBUG_TARGET'); proc.kill(); process.exit(1); }
|
||||
const target = list.find((t) => t.type === 'page') || list[0];
|
||||
const ws = new WebSocket(target.webSocketDebuggerUrl);
|
||||
let id = 0;
|
||||
const pending = new Map();
|
||||
const events = [];
|
||||
const send = (method, params) => new Promise((res) => {
|
||||
const mid = ++id;
|
||||
pending.set(mid, res);
|
||||
ws.send(JSON.stringify({ id: mid, method, params }));
|
||||
});
|
||||
ws.on('message', (raw) => {
|
||||
const msg = JSON.parse(String(raw));
|
||||
if (msg.id && pending.has(msg.id)) { pending.get(msg.id)(msg); pending.delete(msg.id); }
|
||||
else if (msg.method) events.push(msg);
|
||||
});
|
||||
await new Promise((r) => ws.on('open', r));
|
||||
await send('Runtime.enable');
|
||||
await send('Log.enable');
|
||||
await send('Page.enable');
|
||||
await send('Page.navigate', { url: 'http://127.0.0.1:3080/' });
|
||||
await sleep(12000);
|
||||
const result = await send('Runtime.evaluate', {
|
||||
expression: "JSON.stringify({ wtSection: !!document.querySelector('.dsh-wt_section'), taCard: !!document.querySelector('.ta_card'), prCard: !!document.querySelector('.pr_card'), taEntry: !!document.querySelector('.dsh-ta_entry'), usageDock: !!document.querySelector('.u_dock'), railBox: !!document.querySelector('.dsh-wt_railBox'), readyState: document.readyState, bodyText: (document.body ? document.body.innerText.slice(0,200) : '') })",
|
||||
returnByValue: true,
|
||||
});
|
||||
console.log('DOM_STATE:', result.result && result.result.result && result.result.result.value);
|
||||
const errors = events.filter((e) =>
|
||||
e.method === 'Runtime.exceptionThrown' ||
|
||||
(e.method === 'Log.entryAdded' && (e.params.entry.level === 'error' || e.params.entry.level === 'warning')) ||
|
||||
(e.method === 'Runtime.consoleAPICalled' && e.params.type === 'error'),
|
||||
).slice(0, 25).map((e) => {
|
||||
if (e.method === 'Runtime.exceptionThrown') {
|
||||
const d = e.params.exceptionDetails;
|
||||
return 'EXCEPTION: ' + (d.exception && d.exception.description || d.text) + ' @ ' + (d.url || '') + ':' + (d.lineNumber || 0);
|
||||
}
|
||||
if (e.method === 'Log.entryAdded') return 'LOG[' + e.params.entry.level + ']: ' + (e.params.entry.text || '').slice(0, 300);
|
||||
return 'CONSOLE_ERROR: ' + (e.params.args || []).map((a) => a.value || a.description || '').join(' ').slice(0, 300);
|
||||
});
|
||||
console.log('ERRORS_COUNT:', errors.length);
|
||||
errors.forEach((x) => console.log(x));
|
||||
ws.close();
|
||||
proc.kill();
|
||||
process.exit(0);
|
||||
})().catch((e) => { console.log('SCRIPT_FAIL:', e); proc.kill(); process.exit(1); });
|
||||
Reference in New Issue
Block a user