feat: group option in new-conversation pane (join existing / create new workspace via mkdir route)
This commit is contained in:
@@ -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<void>
|
||||
throw new Error('no send path: session face unavailable')
|
||||
}
|
||||
|
||||
/** 自定义窗口任务:新建一个专属会话,注入工作台背景 + 用户需求,并打开该会话 */
|
||||
export async function createCustomSession(projectId: string, projectName: string, requirement: string): Promise<void> {
|
||||
/** 新建会话的分组选择:未分组 / 加入现有分组 / 新建一个分组(父目录 + 名称) */
|
||||
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<void> {
|
||||
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(() => {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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<string | null>(null)
|
||||
// 分组(宿主工作区)三态:未分组 / 现有组 / 新建组
|
||||
const [wsGroups, setWsGroups] = useState<{ id: string; title: string; path: string }[]>([])
|
||||
const [groupMode, setGroupMode] = useState<'none' | 'existing' | 'new'>('none')
|
||||
const [groupId, setGroupId] = useState<string | null>(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}
|
||||
/>
|
||||
</div>
|
||||
{mode === 'new' && (
|
||||
<div className="dsh-wt_customRow">
|
||||
<span className="dsh-wt_customLabel">{T('custom.group')}</span>
|
||||
<SelectPop
|
||||
value={groupMode === 'none' ? '__none' : groupMode === 'new' ? '__new' : groupId}
|
||||
groups={[{
|
||||
title: '',
|
||||
items: [
|
||||
{ id: '__none', label: T('custom.groupNone') },
|
||||
...wsGroups.map((w) => ({ 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) }
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{mode === 'new' && groupMode === 'new' && (
|
||||
<>
|
||||
<div className="dsh-wt_customRow">
|
||||
<span className="dsh-wt_customLabel">{T('custom.groupNewParent')}</span>
|
||||
<input
|
||||
className="dsh-wt_customPathInput"
|
||||
placeholder={T('custom.groupNewParentPh')}
|
||||
value={newGroupParent}
|
||||
onChange={(e) => setNewGroupParent(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="dsh-wt_customRow">
|
||||
<span className="dsh-wt_customLabel">{T('custom.groupNewName')}</span>
|
||||
<input
|
||||
className="dsh-wt_customPathInput"
|
||||
placeholder={T('custom.groupNewNamePh')}
|
||||
value={newGroupName}
|
||||
onChange={(e) => setNewGroupName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="dsh-wt_customSend"
|
||||
disabled={busy || !requirement.trim() || !projectId || (mode === 'existing' && !sessionId)}
|
||||
disabled={busy || !requirement.trim() || !projectId || (mode === 'existing' && !sessionId) || (mode === 'new' && groupMode === 'new' && (!newGroupParent.trim() || !newGroupName.trim()))}
|
||||
onClick={submit}
|
||||
>{busy ? '…' : (mode === 'new' ? T('custom.send') : T('custom.sendToSession'))}</button>
|
||||
{fail && <p className="dsh-wt_customFail">{T('custom.fail')}:{fail}</p>}
|
||||
|
||||
@@ -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}',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 路由(重启后可用)。
|
||||
|
||||
|
||||
|
||||
@@ -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(); }
|
||||
|
||||
@@ -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); });
|
||||
Reference in New Issue
Block a user