feat: custom pane defaults to send-to-session mode (order swapped) + native folder-picker for project folder
This commit is contained in:
@@ -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]
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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); });
|
||||
@@ -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); });
|
||||
Reference in New Issue
Block a user