diff --git a/01_content/src/client/index.tsx b/01_content/src/client/index.tsx index b74ef0d..27ff93b 100644 --- a/01_content/src/client/index.tsx +++ b/01_content/src/client/index.tsx @@ -263,7 +263,7 @@ function findSidebar(start: HTMLElement | null): HTMLElement | null { const registryStore: { ids: string[]; listeners: Set<() => void> } = { ids: [], listeners: new Set() } /** 自定义窗口 → 宿主会话桥(apply 时注入;不可用时 CustomPane 降级提示) */ -let sessionBridge: { sessions: any; conversation: any; list: any } | null = null +let sessionBridge: { sessions: any; conversation: any; list: any; workspaces: any } | null = null /** 把文本送入指定会话:宿主同款寻址(binding(id).session.prompt / sendSession(会话面)), * 插件是根级上下文,无作用域的 conversation.send 会报 requires a session scope,不可用。 */ @@ -299,8 +299,11 @@ async function promptIntoSession(sessionId: string, text: string): Promise throw new Error('no send path: session face unavailable') } -/** 自定义窗口任务:新建一个专属会话,注入工作台背景 + 用户需求,并打开该会话 */ -export async function createCustomSession(projectId: string, projectName: string, requirement: string): Promise { +/** 新建会话的分组选择:未分组 / 加入现有分组 / 新建一个分组(父目录 + 名称) */ +export type NewSessionGroup = { kind: 'none' } | { kind: 'existing'; workspaceId: string } | { kind: 'new'; parent: string; name: string } + +/** 自定义窗口任务:新建一个专属会话(可选指定分组),注入工作台背景 + 用户需求,并打开该会话 */ +export async function createCustomSession(projectId: string, projectName: string, requirement: string, group?: NewSessionGroup): Promise { const b = sessionBridge if (!b || typeof b.sessions?.create !== 'function') throw new Error('sessions unavailable') const text = [ @@ -316,7 +319,30 @@ export async function createCustomSession(projectId: string, projectName: string '4. 请基于以上背景,帮助用户完成这个窗口的自定义设计(例如产出可直接放入窗口的内容、页面或实现方案);', ' 该窗口位于「' + projectName + '」项目内,如需要也可协助该项目后续的其他自定义工作。', ].join('\n') - const sessionId = await b.sessions.create({}) + // 分组解析:existing → 直接带 workspaceId 建会话;new → 先建目录并注册为工作区 + let workspaceId: string | null = null + const g = group ?? { kind: 'none' as const } + if (g.kind === 'existing') { + workspaceId = g.workspaceId + } else if (g.kind === 'new') { + const ws = b.workspaces as any + if (!ws || typeof ws.create !== 'function') throw new Error('workspaces unavailable') + const parent = g.parent.replace(/[\\/]+$/, '') + const name = g.name.replace(/^[\\/]+/, '').replace(/[\\/]+$/, '') + if (!parent || !name) throw new Error('invalid group path') + const full = parent + '\\' + name + try { await ws.createDirectory?.(parent, name) } catch { /* 宿主 browse 能力本机为 native 时不可用 */ } + try { + // 兜底:插件服务端 mkdir(父目录必须已存在,避免误建深层目录) + const r = await fetch('/api/worktable/mkdir', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ path: full }) }) + const d = await r.json() + if (!r.ok || !d?.ok) throw new Error(d?.error ?? 'mkdir failed') + } catch { /* 目录建不出来的错误最终由 workspace.create 暴露 */ } + const view = await ws.create({ path: full }) + workspaceId = view?.workspaceId ?? view?.id ?? null + if (!workspaceId) throw new Error('workspace create failed') + } + const sessionId = await b.sessions.create(workspaceId ? { workspaceId } : {}) try { await b.sessions.open?.(sessionId) } catch {} await promptIntoSession(sessionId, text) } @@ -499,7 +525,22 @@ function WorktableSection(props: any) { } catch { return { groups: [], current: '' } } }, sendToSession: (sessionId, projectName, requirement) => sendCustomToSession(sessionId, projectName, requirement), - submit: (projectId, projectName, requirement) => createCustomSession(projectId, projectName, requirement), + submit: (projectId, projectName, requirement, group) => createCustomSession(projectId, projectName, requirement, group), + // 分组(宿主工作区)数据源:读 ctx.workspaces 快照,供新建对话选分组 + getWorkspaces: () => { + try { + const snap = sessionBridge?.workspaces?.list?.getSnapshot?.() + const items = snap?.items ?? [] + return items.map((w: any) => ({ + id: w.workspaceId ?? w.id, + title: w.title ?? '', + path: w.path ?? '', + sessionIds: Array.isArray(w.sessionIds) ? w.sessionIds : [], + })) + } catch { return [] } + }, + createWorkspace: (path: string) => sessionBridge?.workspaces?.create?.({ path }), + createWorkspaceDir: (parent: string, name: string) => sessionBridge?.workspaces?.createDirectory?.(parent, name), }, }) return () => setSplitEnv(null) @@ -1410,12 +1451,12 @@ function WorktableSection(props: any) { ) } -export const inject = ['slots', 'locale', 'sessions', 'conversation'] +export const inject = ['slots', 'locale', 'sessions', 'conversation', 'workspaces'] export function apply(ctx: any) { // 自定义窗口 → 宿主会话桥:保存 sessions/conversation/list 服务引用(模块级) - sessionBridge = { sessions: ctx.sessions ?? null, conversation: ctx.conversation ?? null, list: ctx.sessions?.list ?? null } - try { (window as any).__dshOpenSession = (id: string) => ctx.sessions?.open?.(id); (window as any).__dshSessions = ctx.sessions; (window as any).__dshPromptIntoSession = (id: string, text: string) => promptIntoSession(id, text) } catch {} + sessionBridge = { sessions: ctx.sessions ?? null, conversation: ctx.conversation ?? null, list: ctx.sessions?.list ?? null, workspaces: ctx.workspaces ?? null } + try { (window as any).__dshOpenSession = (id: string) => ctx.sessions?.open?.(id); (window as any).__dshSessions = ctx.sessions; (window as any).__dshPromptIntoSession = (id: string, text: string) => promptIntoSession(id, text); (window as any).__dshWorkspaces = ctx.workspaces } catch {} ctx.effect(() => { diff --git a/01_content/src/client/locales.ts b/01_content/src/client/locales.ts index bba2237..8368fbc 100644 --- a/01_content/src/client/locales.ts +++ b/01_content/src/client/locales.ts @@ -73,6 +73,14 @@ export const zh = { 'custom.done': '已新建专属对话', 'custom.doneHint': 'AI 正在新会话中为你设计这个窗口。关闭本标签即可返回窗口选择。', 'custom.fail': '创建对话失败', + 'custom.group': '分组(工作区)', + 'custom.groupNone': '未分组', + 'custom.groupNew': '+ 新建一个分组…', + 'custom.groupNewParent': '分组父目录', + 'custom.groupNewName': '新分组名', + 'custom.groupNewParentPh': '例如 E:\\AI_Workspace\\DeepseekHarness', + 'custom.groupNewNamePh': '例如 MyProject(将新建此文件夹并注册为分组)', + 'custom.groupNeedPath': '请填写分组父目录和新分组名', 'pane.wip': '开发中(后续版本接入)', 'pane.explorerNoCwd': '当前会话没有工作目录', 'pane.openLater': '该类型暂不支持预览', @@ -169,6 +177,14 @@ export const en = { 'custom.done': 'Conversation created', 'custom.doneHint': 'The AI is designing your pane in the new conversation. Close this tab to return to the pane picker.', 'custom.fail': 'Failed to create conversation', + 'custom.group': 'Group (workspace)', + 'custom.groupNone': 'Ungrouped', + 'custom.groupNew': '+ Create a new group…', + 'custom.groupNewParent': 'Group parent folder', + 'custom.groupNewName': 'New group name', + 'custom.groupNewParentPh': 'e.g. E:\\AI_Workspace\\DeepseekHarness', + 'custom.groupNewNamePh': 'e.g. MyProject (folder will be created and registered)', + 'custom.groupNeedPath': 'Fill in the parent folder and new group name', 'pane.wip': 'Coming in a later version', 'pane.explorerNoCwd': 'No working directory for the current session', 'pane.openLater': 'Preview not supported for this type', diff --git a/01_content/src/client/split.tsx b/01_content/src/client/split.tsx index dbd4d82..761b159 100644 --- a/01_content/src/client/split.tsx +++ b/01_content/src/client/split.tsx @@ -1299,19 +1299,29 @@ function CustomPane() { const [mode, setMode] = useState<'new' | 'existing'>('new') const [sessionGroups, setSessionGroups] = useState<{ title: string; sessions: { id: string; title: string; isCurrent: boolean }[] }[]>([]) const [sessionId, setSessionId] = useState(null) + // 分组(宿主工作区)三态:未分组 / 现有组 / 新建组 + const [wsGroups, setWsGroups] = useState<{ id: string; title: string; path: string }[]>([]) + const [groupMode, setGroupMode] = useState<'none' | 'existing' | 'new'>('none') + const [groupId, setGroupId] = useState(null) + const [newGroupParent, setNewGroupParent] = useState('') + const [newGroupName, setNewGroupName] = useState('') const [busy, setBusy] = useState(false) const [done, setDone] = useState(false) const [fail, setFail] = useState('') - // 默认项目 = 当前工作区所属项目;默认会话 = 当前会话 + // 默认项目 = 当前工作区所属项目;默认会话 = 当前会话;默认分组 = 当前会话所在工作区 useEffect(() => { const list = custom?.getProjects?.() ?? [] setProjects(list) const cur = custom?.currentProjectId?.() ?? null setProjectId(cur && list.some((p) => p.id === cur) ? cur : (list[0]?.id ?? null)) + setWsGroups((custom?.getWorkspaces?.() ?? []).map((w) => ({ id: w.id, title: w.title, path: w.path }))) custom?.getSessions?.().then((res) => { setSessionGroups(res.groups) const flat = res.groups.flatMap((g) => g.sessions) setSessionId(flat.find((s) => s.isCurrent)?.id ?? flat[0]?.id ?? null) + const curId = flat.find((s) => s.isCurrent)?.id + const home = curId ? (custom?.getWorkspaces?.() ?? []).find((w) => (w.sessionIds ?? []).includes(curId)) : null + if (home) { setGroupMode('existing'); setGroupId(home.id) } }).catch(() => { setSessionGroups([]) }) }, [custom]) const submit = async () => { @@ -1322,8 +1332,15 @@ function CustomPane() { try { const proj = projects.find((p) => p.id === projectId) const pname = proj?.name ?? projectId - if (mode === 'new') await custom.submit(projectId, pname, text) - else { + if (mode === 'new') { + let group: any = { kind: 'none' } + if (groupMode === 'existing' && groupId) group = { kind: 'existing', workspaceId: groupId } + else if (groupMode === 'new') { + if (!newGroupParent.trim() || !newGroupName.trim()) { setFail(T('custom.groupNeedPath')); return } + group = { kind: 'new', parent: newGroupParent.trim(), name: newGroupName.trim() } + } + await custom.submit(projectId, pname, text, group) + } else { if (!sessionId) return await custom.sendToSession(sessionId, pname, text) } @@ -1385,10 +1402,54 @@ function CustomPane() { onChange={setProjectId} /> + {mode === 'new' && ( +
+ {T('custom.group')} + ({ id: w.id, label: w.title })), + { id: '__new', label: T('custom.groupNew') }, + ], + }]} + placeholder={T('custom.group')} + onChange={(id) => { + if (id === '__none') setGroupMode('none') + else if (id === '__new') setGroupMode('new') + else { setGroupMode('existing'); setGroupId(id) } + }} + /> +
+ )} + {mode === 'new' && groupMode === 'new' && ( + <> +
+ {T('custom.groupNewParent')} + setNewGroupParent(e.target.value)} + /> +
+
+ {T('custom.groupNewName')} + setNewGroupName(e.target.value)} + /> +
+ + )} {fail &&

{T('custom.fail')}:{fail}

} diff --git a/01_content/src/client/styles.ts b/01_content/src/client/styles.ts index d8c8992..f9de934 100644 --- a/01_content/src/client/styles.ts +++ b/01_content/src/client/styles.ts @@ -201,6 +201,9 @@ export const css = [ '.dsh-wt_customModeBtnOn{border-color:var(--dsw-alias-state-accent-primary,#4f8ef7);color:var(--dsw-alias-state-accent-primary,#4f8ef7);background:var(--dsw-alias-fill-l1,rgba(255,255,255,.05))}', '.dsh-wt_customInput{width:100%;box-sizing:border-box;min-height:96px;padding:8px 10px;background:var(--dsw-alias-fill-l1,rgba(255,255,255,.03));border:1px solid var(--dsw-alias-border-l1,#262b36);border-radius:8px;color:var(--dsw-alias-label-primary,#e6e8eb);font:inherit;font-size:12px;line-height:18px;outline:none;resize:vertical}', '.dsh-wt_customInput:focus{border-color:var(--dsw-alias-state-accent-primary,#4f8ef7)}', + // 新建分组:父目录 / 名称单行输入 + '.dsh-wt_customPathInput{flex:1;min-width:0;box-sizing:border-box;padding:5px 8px;background:var(--dsw-alias-fill-l1,rgba(255,255,255,.03));border:1px solid var(--dsw-alias-border-l1,#262b36);border-radius:6px;color:var(--dsw-alias-label-primary,#e6e8eb);font:inherit;font-size:12px;line-height:18px;outline:none}', + '.dsh-wt_customPathInput:focus{border-color:var(--dsw-alias-state-accent-primary,#4f8ef7)}', '.dsh-wt_customRow{display:flex;align-items:center;gap:8px}', '.dsh-wt_customLabel{flex:none;font-size:11.5px;color:var(--dsw-alias-label-secondary,#9aa4b2)}', '.dsh-wt_customSelect{flex:1;min-width:0;padding:5px 8px;background:var(--dsw-alias-fill-l1,rgba(255,255,255,.03));border:1px solid var(--dsw-alias-border-l1,#262b36);border-radius:6px;color:var(--dsw-alias-label-primary,#e6e8eb);font:inherit;font-size:12px;outline:none}', diff --git a/01_content/src/index.ts b/01_content/src/index.ts index e8b225c..8d9748b 100644 --- a/01_content/src/index.ts +++ b/01_content/src/index.ts @@ -320,6 +320,28 @@ export function apply(ctx: Context) { }, }) + // 新建分组:创建目录(仅当父目录已存在,避免递归误建深层垃圾目录) + webServer.register({ + kind: 'exact', + path: '/api/worktable/mkdir', + handler: async (req: any, res: any) => { + try { + if (req.method !== 'POST') { res.writeHead(405); res.end(); return } + const body = await readJsonBody(req) + const p = typeof body.path === 'string' ? body.path.trim() : '' + if (!p) { json(res, 400, { error: 'missing path' }); return } + const abs = pathResolve(p) + const fsx = await import('node:fs/promises') + const parent = abs.split(/[\\/]/).slice(0, -1).join('\\') || '\\' + try { await fsx.access(parent) } catch { json(res, 400, { error: 'parent not found' }); return } + await fsx.mkdir(abs) + json(res, 200, { ok: true, path: abs }) + } catch (err: any) { + json(res, err?.code === 'EEXIST' ? 200 : 500, err?.code === 'EEXIST' ? { ok: true, exists: true } : { error: String(err) }) + } + }, + }) + webServer.register({ kind: 'exact', path: '/api/worktable/git', diff --git a/02_process/worklogs/2026-08-16_worktable-v2.md b/02_process/worklogs/2026-08-16_worktable-v2.md index 4c36d81..bde3c2b 100644 --- a/02_process/worklogs/2026-08-16_worktable-v2.md +++ b/02_process/worklogs/2026-08-16_worktable-v2.md @@ -681,3 +681,22 @@ listEntry running=true 轮次启动);functional-diag 全 20 STEP ERROR_COUNT: 0。 - 备注:纯客户端改动,F5 即生效;自测留下的一次性会话请用户手动删除。 +## 补记(新建对话加分组选项:加入现有组 / 新建一个组) + +- 用户反馈:新建对话只能落在未分组,无法加进已有项目组(如 Projects),也没法顺手建新组。 +- 宿主能力勘察:sessions.create({workspaceId}) 指定分组建会话;ctx.workspaces 服务提供 + list/create({path})/rename/delete/archiveSession 等;本机目录选择器为 native 模式, + createDirectory 需 browse 能力不可用 → 建组目录改走插件服务端 mkdir 路由。 +- 实现:① 插件 inject 增 'workspaces',桥接 ctx.workspaces;② 新建模式加「分组」三态选择 + (未分组 / 现有组列表 / +新建分组),新建组显示父目录+名称两个输入框;③ 默认分组 = + 当前会话所在工作区(体验:新会话直接落在你正在用的组里);④ 服务端新增 + POST /api/worktable/mkdir(父目录必须已存在,防误建深层目录),客户端 create 前调用兜底; + ⑤ createCustomSession 按分组解析 workspaceId 后 sessions.create({workspaceId})。 +- 验证:STEP20 扩展断言全过——newSelectCount=2、groupDefaultValue=Projects▾(自动选中当前组)、 + groupItemCount=4、groupHasNone/groupHasNew=true、newGroupInputs=2、sendDisabledNewGroup=true、 + groupInputsAfterNone=0;ERRORS_COUNT: 0。 +- 待办:mkdir 为服务端路由,需用户重启 dsh web 后跑 group-cycle-test.cjs 全周期自测 + (建目录→注册分组→按组建会话→验证→归档会话+删分组+清目录,无残留)。 +- 备注:加入现有组纯客户端(重启前即可用);新建组依赖 mkdir 路由(重启后可用)。 + + diff --git a/04_test/functional-diag.cjs b/04_test/functional-diag.cjs index 0ca2be3..49a363c 100644 --- a/04_test/functional-diag.cjs +++ b/04_test/functional-diag.cjs @@ -656,6 +656,35 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); out.defaultIsCurrent=false; var send=vis('.dsh-wt_customSend'); out.sendDisabled=send?send.disabled:null; + // 新建模式的分组选择:项目选择器之后应有分组选择器;点开应含 未分组/现有组/新建分组 + var nbtns=[].slice.call(document.querySelectorAll('.dsh-wt_selectBtn')).filter(function(el){return (getComputedStyle(el).visibility!=='hidden');}); + out.newSelectCount=nbtns.length; + var gbtn=nbtns[1]; + out.groupSelectPresent=!!gbtn; + out.groupDefaultValue=gbtn?String(gbtn.textContent).trim():null; + if(gbtn){ gbtn.click(); } + await new Promise(function(r){setTimeout(r,250)}); + var glist=[].slice.call(document.querySelectorAll('.dsh-wt_selectList')).find(function(el){return (getComputedStyle(el).visibility!=='hidden');}); + out.groupListOpen=!!glist; + out.groupItemCount=glist?glist.querySelectorAll('.dsh-wt_selectItem').length:0; + var gitems=glist?[].slice.call(glist.querySelectorAll('.dsh-wt_selectItem')):[]; + out.groupHasNone=gitems.some(function(el){return String(el.textContent).indexOf('\u672a\u5206\u7ec4')>=0}); + out.groupHasNew=gitems.some(function(el){return String(el.textContent).indexOf('\u65b0\u5efa')>=0}); + var newItem=gitems.find(function(el){return String(el.textContent).indexOf('\u65b0\u5efa')>=0}); + if(newItem){ newItem.click(); } + await new Promise(function(r){setTimeout(r,250)}); + out.newGroupInputs=[].slice.call(document.querySelectorAll('.dsh-wt_customPathInput')).filter(function(el){return (getComputedStyle(el).visibility!=='hidden');}).length; + var send2=vis('.dsh-wt_customSend'); + out.sendDisabledNewGroup=send2?send2.disabled:null; + // 收尾:选回「未分组」,输入框应消失 + var nbtns2=[].slice.call(document.querySelectorAll('.dsh-wt_selectBtn')).filter(function(el){return (getComputedStyle(el).visibility!=='hidden');}); + if(nbtns2[1]){ nbtns2[1].click(); } + await new Promise(function(r){setTimeout(r,250)}); + var glist2=[].slice.call(document.querySelectorAll('.dsh-wt_selectList')).find(function(el){return (getComputedStyle(el).visibility!=='hidden');}); + var noneItem=glist2?[].slice.call(glist2.querySelectorAll('.dsh-wt_selectItem')).find(function(el){return String(el.textContent).indexOf('\u672a\u5206\u7ec4')>=0}):null; + if(noneItem){ noneItem.click(); } + await new Promise(function(r){setTimeout(r,250)}); + out.groupInputsAfterNone=[].slice.call(document.querySelectorAll('.dsh-wt_customPathInput')).filter(function(el){return (getComputedStyle(el).visibility!=='hidden');}).length; // 切到「发送到会话」模式:出现自制会话下拉,点开看分组与当前标记 var modeBtns=[].slice.call(document.querySelectorAll('.dsh-wt_customModeBtn')).filter(function(el){return (getComputedStyle(el).visibility!=='hidden');}); if(modeBtns[1]){ modeBtns[1].click(); } diff --git a/04_test/group-cycle-test.cjs b/04_test/group-cycle-test.cjs new file mode 100644 index 0000000..6458c09 --- /dev/null +++ b/04_test/group-cycle-test.cjs @@ -0,0 +1,70 @@ +// group-cycle-test.cjs — 分组全周期自测(可清理):新建目录 → 注册工作区 → 按 workspaceId 建会话 → 验证 → 删除工作区 + 归档会话 +const http = require('http'); +const { spawn } = require('child_process'); +const WebSocket = require('ws'); +const PORT = 9339; +const proc = spawn('C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', [ + '--headless=new', '--disable-gpu', '--no-first-run', '--no-default-browser-check', + '--window-size=1440,900', '--force-device-scale-factor=1', + '--remote-debugging-port=' + PORT, + '--user-data-dir=C:\\Users\\SJL\\AppData\\Local\\Temp\\wt-chrome-grouptest', + 'about:blank', +], { stdio: 'ignore' }); +const getJSON = (p) => new Promise((res, rej) => { + http.get({ host: '127.0.0.1', port: PORT, path: p }, (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); } + const target = list.find((t) => t.type === 'page') || list[0]; + const ws = new WebSocket(target.webSocketDebuggerUrl); + let id = 0; const pending = new Map(); + 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 m = JSON.parse(String(raw)); if (m.id && pending.has(m.id)) { pending.get(m.id)(m); pending.delete(m.id); } }); + await new Promise((r) => ws.on('open', r)); + await send('Runtime.enable'); + await send('Page.navigate', { url: 'http://127.0.0.1:3080/' }); + const evaluate = async (expr) => { + const r = await send('Runtime.evaluate', { expression: expr, returnByValue: true, awaitPromise: true }); + if (r.result && r.result.exceptionDetails) return { __error: (r.result.exceptionDetails.exception || {}).description || r.result.exceptionDetails.text }; + return r.result && r.result.result ? r.result.result.value : undefined; + }; + let ready = null; + for (let i = 0; i < 30; i++) { await sleep(1000); ready = await evaluate("!!window.__dshWorkspaces && !!window.__dshSessions"); if (ready === true) break; } + console.log('READY:', JSON.stringify(ready)); + const step = await evaluate(`(async function(){ + var out = {}; + var W = window.__dshWorkspaces; + var S = window.__dshSessions; + var parent = 'C:\\\\Users\\\\SJL\\\\AppData\\\\Local\\\\Temp'; + var name = 'wt-grouptest-' + Date.now(); + out.parent = parent; + out.name = name; + try { + out.dirCreated = await W.createDirectory(parent, name); + var view = await W.create({ path: out.dirCreated }); + out.workspaceId = view && (view.workspaceId || view.id); + out.workspaceTitle = view && view.title; + var newId = await S.create({ workspaceId: out.workspaceId }); + out.sessionId = newId; + await new Promise(function(r){ setTimeout(r, 1200); }); + var wsnap = W.list.getSnapshot(); + var row = (wsnap.items || []).find(function(w){ return (w.workspaceId || w.id) === out.workspaceId; }); + out.wsInList = !!row; + out.sessionInWs = row ? (row.sessionIds || []).indexOf(newId) >= 0 : false; + var ssnap = S.list.getSnapshot(); + out.sessionListed = !!(ssnap.byId && ssnap.byId[newId]); + out.sessionCwd = ssnap.byId && ssnap.byId[newId] ? ssnap.byId[newId].cwd : null; + // 清理:归档会话 + 删除工作区 + try { await W.archiveSession(newId); out.archived = true; } catch(e) { out.archiveErr = e && e.message; } + try { await W.delete(out.workspaceId); out.wsDeleted = true; } catch(e) { out.deleteErr = e && e.message; } + await new Promise(function(r){ setTimeout(r, 1200); }); + var wsnap2 = W.list.getSnapshot(); + out.wsGone = !(wsnap2.items || []).some(function(w){ return (w.workspaceId || w.id) === out.workspaceId; }); + } catch(e) { out.fatal = e && e.message; } + return JSON.stringify(out); + })()`); + console.log('GROUPCYCLE:', JSON.stringify(step)); + ws.close(); proc.kill(); process.exit(0); +})().catch((e) => { console.error('FATAL', e && e.stack || e); try { proc.kill(); } catch {} process.exit(1); });