feat: custom pane defaults to send-to-session mode (order swapped) + native folder-picker for project folder

This commit is contained in:
Aisland
2026-08-19 21:48:32 +08:00
parent ce449c6620
commit 04b984a246
8 changed files with 249 additions and 98 deletions
+33 -65
View File
@@ -586,20 +586,15 @@ function WorktableSection(props: any) {
const [wsPreset, setWsPreset] = useState<string>('2h')
const [wsName, setWsName] = useState('')
const [wsError, setWsError] = useState(false)
// 新建项目强制工作文件夹:父目录(必填)+ 名称(留空 = 用项目名)
// 新建项目强制工作文件夹:经「选择位置…」弹窗选定(系统资源管理器式选择窗)
const [wsFolderParent, setWsFolderParent] = useState('')
const [wsFolderName, setWsFolderName] = useState('')
const [wsFolderError, setWsFolderError] = useState(false)
/** 图标选择器:kind + 目标 id + 弹窗锚点坐标(fixed 定位) */
const [iconPick, setIconPick] = useState<{ kind: 'layout' | 'shortcut' | 'project'; id: string; x: number; y: number } | null>(null)
/** 对话绑定弹窗:项目 id + 锚点坐标;bindGroups = 打开时抓取的会话分组 */
const [bindPick, setBindPick] = useState<{ id: string; x: number; y: number } | null>(null)
const [bindGroups, setBindGroups] = useState<{ title: string; sessions: { id: string; title: string; isCurrent: boolean }[] }[]>([])
/** 绑定弹窗内的项目文件夹编辑区 */
const [bindFolderEdit, setBindFolderEdit] = useState(false)
const [bindFolderParent, setBindFolderParent] = useState('')
const [bindFolderName, setBindFolderName] = useState('')
const [bindFolderError, setBindFolderError] = useState(false)
/** 自定义布局弹窗(预设网格末尾的 + 磁贴):描述 → 复制提示词到剪贴板 */
const [customOpen, setCustomOpen] = useState(false)
const [customLayoutText, setCustomLayoutText] = useState('')
@@ -912,26 +907,26 @@ function buildCustomLayoutPrompt(req: string): string {
const y = clamp(Math.round(r.top), 8, window.innerHeight - 420)
setBindPick({ id, x, y })
setBindGroups([])
setBindFolderEdit(false)
setBindFolderError(false)
fetchSessionGroups().then((res) => setBindGroups(res.groups)).catch(() => setBindGroups([]))
}, [])
/** 保存绑定弹窗里的项目文件夹(建目录 + 持久化) */
const saveBindFolder = async () => {
if (!bindPick) return
const parent = bindFolderParent.trim()
const name = bindFolderName.trim()
if (!parent || !name) { setBindFolderError(true); return }
const folderPath = parent.replace(/[\\/]+$/, '') + '\\' + name.replace(/[\\/]+/g, '')
/** 弹出系统文件夹选择窗(宿主 pickDirectory);选中后回调 */
const pickFolder = async (apply: (p: string) => void) => {
try {
const r = await fetch('/api/worktable/mkdir', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ path: folderPath }) })
const d = await r.json()
if (!r.ok || !d?.ok) { setBindFolderError(true); return }
} catch { setBindFolderError(true); return }
persistProjects((prev) => ({ ...prev, folders: { ...prev.folders, [bindPick.id]: folderPath } }))
setBindFolderEdit(false)
setBindFolderError(false)
const ws = sessionBridge?.workspaces as any
if (ws && typeof ws.pickDirectory === 'function') {
const p = await ws.pickDirectory()
if (p && typeof p === 'string') apply(p)
}
} catch { /* 取消或不可用 */ }
}
/** 绑定弹窗「更改」:弹窗选择文件夹,直接作为项目文件夹 */
const changeBindFolder = () => {
if (!bindPick) return
pickFolder((p) => {
persistProjects((prev) => ({ ...prev, folders: { ...prev.folders, [bindPick.id]: p } }))
})
}
/** 绑定 / 解绑会话 */
@@ -1114,22 +1109,19 @@ function buildCustomLayoutPrompt(req: string): string {
persistProjects((prev) => ({ ...prev, shortcuts: prev.shortcuts.filter((s) => s.id !== id) }))
}
// ── 布局(新建工作区) ── 强制项目文件夹:父目录必填,文件夹名留空 = 用项目名;保存时建目录
// ── 布局(新建工作区) ── 强制项目文件夹:必须经「选择位置…」选定;路径即为项目文件夹
const saveLayout = async () => {
const name = wsName.trim()
if (!name) { setWsError(true); return }
const parent = wsFolderParent.trim()
if (!parent) { setWsFolderError(true); return }
const folderName = (wsFolderName.trim() || name).replace(/[\\/]+/g, '')
const folderPath = parent.replace(/[\\/]+$/, '') + '\\' + folderName
const folderPath = wsFolderParent.trim()
if (!folderPath) { setWsFolderError(true); return }
try {
const r = await fetch('/api/worktable/mkdir', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ path: folderPath }) })
const d = await r.json()
if (!r.ok || !d?.ok) { setWsFolderError(true); return }
} catch { setWsFolderError(true); return }
// 轻量兜底:选中的路径理应存在,mkdir 幂等无害
await fetch('/api/worktable/mkdir', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ path: folderPath }) })
} catch { /* 非致命 */ }
const layout = buildLayout(wsPreset, name)
persistProjects((prev) => ({ ...prev, layouts: [...prev.layouts, layout], folders: { ...prev.folders, [layout.id]: folderPath } }))
setWsName(''); setWsFolderParent(''); setWsFolderName(''); setWsError(false); setWsFolderError(false)
setWsName(''); setWsFolderParent(''); setWsError(false); setWsFolderError(false)
setAddOpen(false)
openSplit(layout)
reportUsed(layout.id)
@@ -1550,13 +1542,10 @@ function buildCustomLayoutPrompt(req: string): string {
onChange={(e) => { setWsName(e.target.value); setWsError(false) }} />
<div className="dsh-wt_addFolderRow">
<span className="dsh-wt_customLabel">{t('add.folderParent')}</span>
<input className="dsh-wt_customPathInput" type="text" placeholder={t('add.folderParentPh')} value={wsFolderParent}
onChange={(e) => { setWsFolderParent(e.target.value); setWsFolderError(false) }} />
</div>
<div className="dsh-wt_addFolderRow">
<span className="dsh-wt_customLabel">{t('add.folderName')}</span>
<input className="dsh-wt_customPathInput" type="text" placeholder={t('add.folderNamePh')} value={wsFolderName}
onChange={(e) => { setWsFolderName(e.target.value); setWsFolderError(false) }} />
<span className={'dsh-wt_addFolderPath' + (wsFolderParent ? '' : ' dsh-wt_addFolderPathNone')} title={wsFolderParent || ''}>
{wsFolderParent || t('add.folderNone')}
</span>
<button type="button" className="dsh-wt_bindFolderChange" onClick={() => { pickFolder((p) => { setWsFolderParent(p); setWsFolderError(false) }); setWsError(false) }}>{t('add.folderPick')}</button>
</div>
<button type="button" className="dsh-wt_addBtn" onClick={saveLayout}>{t('add.layoutSave')}</button>
</div>
@@ -1728,32 +1717,11 @@ function buildCustomLayoutPrompt(req: string): string {
<div className="dsh-wt_bindFolderBox">
<div className="dsh-wt_bindFolderRow">
<span className="dsh-wt_bindFolderLabel">📁 {t('bind.folder')}</span>
<button
type="button"
className="dsh-wt_bindFolderChange"
onClick={() => {
const cur = projects.folders[bindPick.id] ?? ''
setBindFolderParent(cur.replace(/[\\/][^\\/]*$/, ''))
setBindFolderName(cur.split(/[\\/]/).pop() ?? '')
setBindFolderEdit((v) => !v)
setBindFolderError(false)
}}
>{t('bind.folderChange')}</button>
<button type="button" className="dsh-wt_bindFolderChange" onClick={changeBindFolder}>{t('bind.folderChange')}</button>
</div>
<div className={'dsh-wt_bindFolderPath' + (projects.folders[bindPick.id] ? '' : ' dsh-wt_bindFolderPathNone')} title={projects.folders[bindPick.id] ?? ''}>
{projects.folders[bindPick.id] ?? t('bind.folderNone')}
</div>
{bindFolderEdit ? (
<div className="dsh-wt_bindFolderForm">
<input className="dsh-wt_customPathInput" type="text" placeholder={t('add.folderParentPh')} value={bindFolderParent}
onChange={(e) => { setBindFolderParent(e.target.value); setBindFolderError(false) }} />
<input className="dsh-wt_customPathInput" type="text" placeholder={t('add.folderNamePh')} value={bindFolderName}
onChange={(e) => { setBindFolderName(e.target.value); setBindFolderError(false) }} />
<button type="button" className="dsh-wt_customLayoutBtn" onClick={saveBindFolder}>{t('add.layoutSave')}</button>
{bindFolderError && <p className="dsh-wt_addError">{t('add.folderRequired')}</p>}
</div>
) : (
<div className={'dsh-wt_bindFolderPath' + (projects.folders[bindPick.id] ? '' : ' dsh-wt_bindFolderPathNone')} title={projects.folders[bindPick.id] ?? ''}>
{projects.folders[bindPick.id] ?? t('bind.folderNone')}
</div>
)}
</div>
{(() => {
const sid = projects.bindings[bindPick.id]
+8 -10
View File
@@ -40,11 +40,10 @@ export const zh = {
'shortcut.badge': '本地',
'add.chooseLayout': '选择布局模式',
'add.layoutNamePh': '布局名称',
'add.folderParent': '项目文件夹(父目录)',
'add.folderParentPh': '例如 E:\\AI_Workspace\\DeepseekHarness\\Projects',
'add.folderName': '文件夹名',
'add.folderNamePh': '留空 = 用项目名',
'add.folderRequired': '请填写项目文件夹的父目录(保存时将创建「父目录\项目名」)',
'add.folderParent': '项目文件夹',
'add.folderPick': '选择位置…',
'add.folderNone': '未选择(点右侧「选择位置…」在资源管理器窗口里挑一个文件夹)',
'add.folderRequired': '请先选择项目文件夹的位置',
'add.layoutSave': '进入工作区',
'add.layoutInvalid': '请填写布局名称',
'manage.deleteLayout': '删除布局',
@@ -171,11 +170,10 @@ export const en = {
'shortcut.badge': 'Local',
'add.chooseLayout': 'Choose a layout',
'add.layoutNamePh': 'Layout name',
'add.folderParent': 'Project folder (parent dir)',
'add.folderParentPh': 'e.g. E:\\AI_Workspace\\DeepseekHarness\\Projects',
'add.folderName': 'Folder name',
'add.folderNamePh': 'Blank = use project name',
'add.folderRequired': 'Fill in the project folder parent (the folder parent\\project-name will be created)',
'add.folderParent': 'Project folder',
'add.folderPick': 'Choose…',
'add.folderNone': 'Not chosen (click Choose… to pick a folder in the explorer window)',
'add.folderRequired': 'Pick the project folder location first',
'add.layoutSave': 'Enter workspace',
'add.layoutInvalid': 'Enter a layout name',
'manage.deleteLayout': 'Delete layout',
+2 -2
View File
@@ -1330,7 +1330,7 @@ function CustomPane(props: { paneTitle?: string }) {
const [requirement, setRequirement] = useState('')
const [projectId, setProjectId] = useState<string | null>(null)
const [projects, setProjects] = useState<{ id: string; name: string }[]>(() => custom?.getProjects?.() ?? [])
const [mode, setMode] = useState<'new' | 'existing'>('new')
const [mode, setMode] = useState<'new' | 'existing'>('existing')
const [sessionGroups, setSessionGroups] = useState<{ title: string; sessions: { id: string; title: string; isCurrent: boolean }[] }[]>([])
const [sessionId, setSessionId] = useState<string | null>(null)
// 分组(宿主工作区)三态:未分组 / 现有组 / 新建组
@@ -1408,8 +1408,8 @@ function CustomPane(props: { paneTitle?: string }) {
<div className="dsh-wt_customCard">
<span className="dsh-wt_customTitle">✨ {T('custom.title')}</span>
<div className="dsh-wt_customModes">
<button type="button" className={'dsh-wt_customModeBtn' + (mode === 'new' ? ' dsh-wt_customModeBtnOn' : '')} onClick={() => setMode('new')}>{T('custom.modeNew')}</button>
<button type="button" className={'dsh-wt_customModeBtn' + (mode === 'existing' ? ' dsh-wt_customModeBtnOn' : '')} onClick={() => setMode('existing')}>{T('custom.modeSend')}</button>
<button type="button" className={'dsh-wt_customModeBtn' + (mode === 'new' ? ' dsh-wt_customModeBtnOn' : '')} onClick={() => setMode('new')}>{T('custom.modeNew')}</button>
</div>
<p className="dsh-wt_customHint">{mode === 'new' ? T('custom.hint') : T('custom.hintSend')}</p>
<textarea
+2
View File
@@ -227,6 +227,8 @@ export const css = [
'.dsh-wt_customPathInput:focus{border-color:var(--dsw-alias-state-accent-primary,#4f8ef7)}',
// 新建项目强制文件夹:两行(父目录/名称)
'.dsh-wt_addFolderRow{display:flex;align-items:center;gap:8px;margin-top:6px}',
'.dsh-wt_addFolderPath{flex:1;min-width:0;padding:4px 8px;border:1px solid var(--dsw-alias-border-l1,#262b36);border-radius:6px;background:var(--dsw-alias-fill-l1,rgba(255,255,255,.03));font-size:11.5px;line-height:17px;color:var(--dsw-alias-label-primary,#e6e8eb);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
'.dsh-wt_addFolderPathNone{color:var(--dsw-alias-label-tertiary,#6b7280);border-style:dashed}',
'.dsh-wt_addFolderRow .dsh-wt_customLabel{width:86px;text-align:right}',
// 绑定弹窗内的项目文件夹区
'.dsh-wt_bindFolderBox{margin-top:8px;padding:6px 8px;background:var(--dsw-alias-fill-l1,rgba(255,255,255,.03));border:1px solid var(--dsw-alias-border-l1,#262b36);border-radius:8px}',
@@ -863,6 +863,22 @@
- 备注:纯客户端改动,F5 即生效;测试目录已清理。用户贴的接收会话记录(glob 超时/反复 Read
插件源码)即知识包要解决的痛点。
## 补记(自定义窗默认模式对调 + 文件夹弹窗选择)
- 用户反馈:① 自定义窗默认应为「发送到会话」且排第一,「新建对话」第二;② 项目文件夹的
父目录+名称双输入太麻烦——要像 DSH 加工作区那样弹资源管理器式位置选择窗。
- 实现:① CustomPane 默认 mode='existing'、按钮顺序对调(发送到会话在前);② 文件夹选择
改用宿主 ctx.workspaces.pickDirectory()(本机为原生 IFileDialog,真实 Windows 文件夹选择窗,
与 DSH 加工作区同款):新建项目面板 = 路径显示 + 「选择位置…」按钮(必选,留空不可保存);
绑定弹窗「更改」= 直接弹窗选择、选中路径即项目文件夹;取消手输表单与 mkdir 依赖。
- 验证(probe-batch7.cjs,桩 pickDirectory):默认模式按钮0=发送到会话且选中、会话行默认可见、
分组行默认隐藏 ✓;新建面板路径显示+选择按钮、桩返回路径回填、保存后 folders 持久化 ✓;
绑定弹窗「更改」写入选中路径 ✓;functional-diag 全 20 STEP(STEP20 按新顺序重写)
ERROR_COUNT: 0。
- 备注:纯客户端改动,F5 即生效;无头环境 pickDirectory 不弹真实对话框(挂起),真实 GUI 中
由用户手势触发原生 Windows 文件夹选择窗。
## 补记(箭头再上移 0.5px)
+20 -21
View File
@@ -651,12 +651,30 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
out.tabOpened=st.spec.main[0].tabs.length===1;
out.tabType=st.spec.main[0].tabs[0].content.type;
out.dialog=!!vis('.dsh-wt_customCard');
// 默认模式 = 发送到会话:第一个选择器是会话下拉
var sel=vis('.dsh-wt_selectBtn');
out.selectValue=sel?String(sel.textContent).trim():null;
out.defaultIsCurrent=false;
var send=vis('.dsh-wt_customSend');
out.sendDisabled=send?send.disabled:null;
// 新建模式的分组选择:项目选择器之后应有分组选择器;点开应含 未分组/现有组/新建分组
var sbtns=[].slice.call(document.querySelectorAll('.dsh-wt_selectBtn')).filter(function(el){return (getComputedStyle(el).visibility!=='hidden');});
out.selectBtnCount=sbtns.length;
out.sessionSelect=sbtns.length>=2;
if(sbtns[0]){ sbtns[0].click(); }
await new Promise(function(r){setTimeout(r,250)});
var list=[].slice.call(document.querySelectorAll('.dsh-wt_selectList')).find(function(el){return (getComputedStyle(el).visibility!=='hidden');});
out.listOpen=!!list;
out.groupHeaders=list?list.querySelectorAll('.dsh-wt_selectGroup').length:0;
var gh=list?list.querySelector('.dsh-wt_selectGroup'):null;
out.groupFontSize=gh?getComputedStyle(gh).fontSize:null;
out.groupWeight=gh?getComputedStyle(gh).fontWeight:null;
out.groupBorderLeft=gh?getComputedStyle(gh).borderLeftWidth:null;
out.dividers=list?list.querySelectorAll('.dsh-wt_selectDivider').length:0;
out.itemCount=list?list.querySelectorAll('.dsh-wt_selectItem').length:0;
out.currentBadge=list?list.querySelectorAll('.dsh-wt_selectCurrent').length:0;
// 切到第二个模式「新建对话」:出现分组选择
var modeBtns=[].slice.call(document.querySelectorAll('.dsh-wt_customModeBtn')).filter(function(el){return (getComputedStyle(el).visibility!=='hidden');});
if(modeBtns[1]){ modeBtns[1].click(); }
await new Promise(function(r){setTimeout(r,250)});
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];
@@ -685,25 +703,6 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
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(); }
await new Promise(function(r){setTimeout(r,250)});
var sbtns=[].slice.call(document.querySelectorAll('.dsh-wt_selectBtn')).filter(function(el){return (getComputedStyle(el).visibility!=='hidden');});
out.selectBtnCount=sbtns.length;
out.sessionSelect=sbtns.length>=2;
if(sbtns[0]){ sbtns[0].click(); }
await new Promise(function(r){setTimeout(r,250)});
var list=[].slice.call(document.querySelectorAll('.dsh-wt_selectList')).find(function(el){return (getComputedStyle(el).visibility!=='hidden');});
out.listOpen=!!list;
out.groupHeaders=list?list.querySelectorAll('.dsh-wt_selectGroup').length:0;
var gh=list?list.querySelector('.dsh-wt_selectGroup'):null;
out.groupFontSize=gh?getComputedStyle(gh).fontSize:null;
out.groupWeight=gh?getComputedStyle(gh).fontWeight:null;
out.groupBorderLeft=gh?getComputedStyle(gh).borderLeftWidth:null;
out.dividers=list?list.querySelectorAll('.dsh-wt_selectDivider').length:0;
out.itemCount=list?list.querySelectorAll('.dsh-wt_selectItem').length:0;
out.currentBadge=list?list.querySelectorAll('.dsh-wt_selectCurrent').length:0;
st.close();
}catch(err){ out.err=String(err) }
return JSON.stringify(out);
+59
View File
@@ -0,0 +1,59 @@
// pickdir-probe.cjs — 探测 ctx.workspaces.pickDirectory 在 Web GUI 的行为
const http = require('http');
const { spawn } = require('child_process');
const WebSocket = require('ws');
const PORT = 9350;
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-pickdir',
'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.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"); if (ready === true) break; }
await sleep(1000);
// 发起 pickDirectory(不 await 结果,先观察 UI 是否弹窗)
const fire = await evaluate(`(function(){
var W = window.__dshWorkspaces;
if (!W || typeof W.pickDirectory !== 'function') return 'no-pick';
window.__pickResult = 'pending';
W.pickDirectory().then(function(p){ window.__pickResult = 'resolved:' + String(p); }).catch(function(e){ window.__pickResult = 'rejected:' + String(e && e.message || e); });
return 'fired';
})()`);
console.log('FIRE:', JSON.stringify(fire));
await sleep(2500);
const check = await evaluate(`(function(){
var out = { pick: window.__pickResult };
// 是否出现宿主内嵌目录浏览 UI(常见的类名试探)
out.dialogCount = document.querySelectorAll('[role=dialog], .dsh-dialog, [class*=picker], [class*=Picker]').length;
out.bodyTextTail = document.body ? document.body.innerText.slice(-200) : '';
return JSON.stringify(out);
})()`);
console.log('CHECK:', JSON.stringify(check));
await sleep(3000);
const check2 = await evaluate("JSON.stringify({ pick: window.__pickResult })");
console.log('CHECK2:', JSON.stringify(check2));
ws.close(); proc.kill(); process.exit(0);
})().catch((e) => { console.error('FATAL', e && e.stack || e); try { proc.kill(); } catch {} process.exit(1); });
+109
View File
@@ -0,0 +1,109 @@
// probe-batch7.cjs — 自定义窗默认模式对调 + 文件夹弹窗选择(桩 pickDirectory)
const http = require('http');
const { spawn } = require('child_process');
const WebSocket = require('ws');
const PORT = 9351;
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-batch7',
'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 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 m = JSON.parse(String(raw)); if (m.id && pending.has(m.id)) { pending.get(m.id)(m); pending.delete(m.id); } else if (m.method) events.push(m); });
await new Promise((r) => ws.on('open', r));
await send('Runtime.enable');
await send('Page.enable');
await send('Page.addScriptToEvaluateOnNewDocument', {
source: "try{ if(window===window.top && location.origin==='http://127.0.0.1:3080'){ localStorage.setItem('dsh.worktable.projects.v1', JSON.stringify({order:[],lastUsed:{},hidden:[],nameOverrides:{},shortcuts:[],layouts:[{id:'t-layout',title:'L',top:null,left:null,main:[{id:'p1',title:'W',min:200,content:null}],chatWidth:{default:320,min:240,max:600},chatSide:'right',chatFullHeight:false}],bindings:{},folders:{}})); } }catch(e){}",
});
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.__dshWorktable && !!window.__dshWorkspaces"); if (ready === true) break; }
await sleep(1500);
const step = await evaluate(`(async function(){
var out = {};
var sleep = function(ms){ return new Promise(function(r){ setTimeout(r, ms); }); };
var vis = function(sel){ return [].slice.call(document.querySelectorAll(sel)).find(function(el){ return (getComputedStyle(el).visibility!=='hidden'&&el.getBoundingClientRect().height>0); }); };
var setInput = function(el, v){ var s = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype,'value').set; s.call(el, v); el.dispatchEvent(new Event('input',{bubbles:true})); };
var st = window.__dshWorktable.splitStore;
try {
// 桩住 pickDirectory(避免无头环境拉起真实系统对话框)
var W = window.__dshWorkspaces;
try { W.pickDirectory = function(){ return Promise.resolve('C:\\\\Picked\\\\ProjFolder'); }; } catch(e) { out.stubErr = String(e); }
// A. 自定义窗默认模式 = 发送到会话
st.open({id:'t-cust',title:'c',top:null,main:[{id:'p1',title:'\u7a97\u53e31',min:200,content:null}],chatWidth:{default:320,min:240,max:600}});
await sleep(400);
var pick = [].slice.call(document.querySelectorAll('.dsh-wt_panePick')).find(function(el){ var t=el.textContent||''; return t.indexOf('\u81ea\u5b9a\u4e49')>=0; });
if (pick) { pick.click(); }
await sleep(400);
var modeBtns = [].slice.call(document.querySelectorAll('.dsh-wt_customModeBtn'));
out.modeBtn0 = modeBtns[0] ? String(modeBtns[0].textContent).trim() : null;
out.modeBtn0On = modeBtns[0] ? modeBtns[0].className.indexOf('dsh-wt_customModeBtnOn') >= 0 : null;
out.modeBtn1 = modeBtns[1] ? String(modeBtns[1].textContent).trim() : null;
out.sessionRowDefault = !!vis('.dsh-wt_customRow') && !!vis('.dsh-wt_selectBtn');
out.groupRowHiddenDefault = !document.querySelector('.dsh-wt_customRow .dsh-wt_selectBtn') ? null : true;
st.close();
await sleep(300);
// B. 新建项目面板:路径显示 + 选择位置…按钮(桩返回路径)
document.querySelectorAll('.dsh-wt_actions .dsh-wt_iconBtn')[2].click();
await sleep(350);
out.addPathShown = !!vis('.dsh-wt_addFolderPath');
var pickBtn = [].slice.call(document.querySelectorAll('.dsh-wt_bindFolderChange')).find(function(el){ return el.textContent.indexOf('\u9009\u62e9\u4f4d\u7f6e') >= 0; });
out.pickBtn = !!pickBtn;
if (pickBtn) { pickBtn.click(); }
await sleep(500);
out.addPathAfterPick = vis('.dsh-wt_addFolderPath') ? String(vis('.dsh-wt_addFolderPath').textContent).trim() : null;
var inps = [].slice.call(document.querySelectorAll('.dsh-wt_addForm input'));
out.addInputs = inps.length;
setInput(inps[0], 'PICK测试');
await sleep(200);
document.querySelector('.dsh-wt_addBtn').click();
await sleep(700);
var saved = JSON.parse(localStorage.getItem('dsh.worktable.projects.v1'));
var lid = saved.layouts[saved.layouts.length-1].id;
out.folderSaved = saved.folders[lid];
st.close();
await sleep(300);
// C. 绑定弹窗「更改」→ 桩路径直接写入
var card0 = document.querySelectorAll('.dsh-wt_layout')[0];
var bbtn = card0 && card0.querySelector('.dsh-wt_bindBtn');
if (bbtn) { bbtn.click(); }
await sleep(900);
var changeBtn = vis('.dsh-wt_bindFolderChange');
if (changeBtn) { changeBtn.click(); }
await sleep(500);
var saved2 = JSON.parse(localStorage.getItem('dsh.worktable.projects.v1'));
out.bindFolderSaved = saved2.folders['t-layout'];
var bd = [].slice.call(document.querySelectorAll('.dsh-wt_popBackdrop')).find(function(el){ return (getComputedStyle(el).visibility!=='hidden'&&el.getBoundingClientRect().height>0); });
if (bd) { bd.click(); }
await sleep(300);
// 清理
var j = JSON.parse(localStorage.getItem('dsh.worktable.projects.v1'));
j.layouts = j.layouts.filter(function(l){ return l.id !== lid; });
delete j.folders[lid]; delete j.folders['t-layout'];
localStorage.setItem('dsh.worktable.projects.v1', JSON.stringify(j));
} catch(e) { out.err = String(e); }
return JSON.stringify(out);
})()`);
console.log('BATCH7:', JSON.stringify(step));
const errs = events.filter((e) => e.method === 'Runtime.exceptionThrown').slice(-4).map((e) => ((e.params.exceptionDetails.exception || {}).description || '').slice(0, 150));
console.log('EXCEPTIONS:', JSON.stringify(errs));
ws.close(); proc.kill(); process.exit(0);
})().catch((e) => { console.error('FATAL', e && e.stack || e); try { proc.kill(); } catch {} process.exit(1); });