feat: anim pane type + bind button redesign (aligned double-circle icon, status popup) + auto-bind rule + workspaces route BOM tolerance
This commit is contained in:
@@ -382,7 +382,7 @@ async function promptIntoSession(sessionId: string, text: 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> {
|
||||
export async function createCustomSession(projectId: string, projectName: string, requirement: string, group?: NewSessionGroup): Promise<string> {
|
||||
const b = sessionBridge
|
||||
if (!b || typeof b.sessions?.create !== 'function') throw new Error('sessions unavailable')
|
||||
const text = [
|
||||
@@ -424,6 +424,7 @@ export async function createCustomSession(projectId: string, projectName: string
|
||||
const sessionId = await b.sessions.create(workspaceId ? { workspaceId } : {})
|
||||
try { await b.sessions.open?.(sessionId) } catch {}
|
||||
await promptIntoSession(sessionId, text)
|
||||
return sessionId
|
||||
}
|
||||
|
||||
/** 把自定义需求直接发到用户选定的已有会话(默认当前会话) */
|
||||
@@ -553,7 +554,7 @@ function WorktableSection(props: any) {
|
||||
// 注意:不走 props.useSessions hook(其宿主包装在部分版本会触发 useSyncExternalStore
|
||||
// 崩溃),改为 apply 里订阅 ctx.sessions.list 后写入模块级 store,此处直接读取。
|
||||
useEffect(() => {
|
||||
setSplitEnv({
|
||||
const env = {
|
||||
getScope: () => {
|
||||
const s = sessionScopeStore.snapshot
|
||||
return s ? { sessionId: s.sessionId, cwd: s.cwd } : null
|
||||
@@ -587,9 +588,20 @@ function WorktableSection(props: any) {
|
||||
},
|
||||
createWorkspace: (path: string) => sessionBridge?.workspaces?.create?.({ path }),
|
||||
createWorkspaceDir: (parent: string, name: string) => sessionBridge?.workspaces?.createDirectory?.(parent, name),
|
||||
// 自动绑定规则:当前项目未绑定 → 绑定到刚新建/发送的会话;已绑定 → 不改(返回 'kept')
|
||||
autoBind: (sessionId: string): 'auto' | 'kept' | 'none' => {
|
||||
const pid = splitStore.active && splitStore.spec ? splitStore.spec.id : null
|
||||
if (!pid || typeof sessionId !== 'string' || !sessionId) return 'none'
|
||||
const already = projectsRef.current.projects.bindings[pid]
|
||||
if (already) return 'kept'
|
||||
persistProjects((prev) => (prev.bindings[pid] ? prev : { ...prev, bindings: { ...prev.bindings, [pid]: sessionId } }))
|
||||
return 'auto'
|
||||
},
|
||||
},
|
||||
})
|
||||
return () => setSplitEnv(null)
|
||||
}
|
||||
setSplitEnv(env)
|
||||
try { (window as any).__dshCustomEnv = env } catch {}
|
||||
return () => { setSplitEnv(null); try { (window as any).__dshCustomEnv = null } catch {} }
|
||||
}, [])
|
||||
const floatRef = useRef<FloatRect | null>(null)
|
||||
|
||||
@@ -742,6 +754,15 @@ function WorktableSection(props: any) {
|
||||
}
|
||||
}, [])
|
||||
|
||||
/** 绑定会话信息:文件夹(工作区)+ 会话标题(供绑定弹窗状态行展示) */
|
||||
function bindInfoOf(groups: { title: string; sessions: { id: string; title: string; isCurrent: boolean }[] }[], sid: string): { folder: string; title: string } {
|
||||
for (const g of groups) {
|
||||
const s = g.sessions.find((x) => x.id === sid)
|
||||
if (s) return { folder: g.title || '未分组', title: s.title }
|
||||
}
|
||||
return { folder: '', title: sid }
|
||||
}
|
||||
|
||||
/** 自定义布局提示词模板(复制到剪贴板,可发给任意 dsh 会话让 agent 实现) */
|
||||
function buildCustomLayoutPrompt(req: string): string {
|
||||
return [
|
||||
@@ -1102,7 +1123,10 @@ function buildCustomLayoutPrompt(req: string): string {
|
||||
bindBtn = document.createElement('span')
|
||||
bindBtn.className = 'dsh-wt_bindBtn'
|
||||
bindBtn.setAttribute('role', 'button')
|
||||
bindBtn.textContent = '🔗'
|
||||
const circles = document.createElement('span')
|
||||
circles.className = 'dsh-wt_bindCircles'
|
||||
circles.setAttribute('aria-hidden', 'true')
|
||||
bindBtn.appendChild(circles)
|
||||
el.appendChild(bindBtn)
|
||||
const cs = getComputedStyle(el)
|
||||
if (cs.position === 'static') el.style.position = 'relative'
|
||||
@@ -1110,7 +1134,7 @@ function buildCustomLayoutPrompt(req: string): string {
|
||||
bindBtn.setAttribute('data-wt-bind', id)
|
||||
const bound = projects.bindings[id]
|
||||
bindBtn.setAttribute('data-bound', bound ? 'true' : 'false')
|
||||
bindBtn.title = t('bind.title')
|
||||
bindBtn.title = bound ? t('bind.titleBound') : t('bind.titleUnbound')
|
||||
const icon = el.children[0] as HTMLElement | null
|
||||
if (icon) {
|
||||
const ovr = projects.iconOverrides[id]
|
||||
@@ -1542,7 +1566,25 @@ function buildCustomLayoutPrompt(req: string): string {
|
||||
{bindPick && (
|
||||
<div className="dsh-wt_menu dsh-wt_pop dsh-wt_bindPop" style={{ position: 'fixed', left: bindPick.x, top: bindPick.y, width: 280, zIndex: 84 }}>
|
||||
<span className="dsh-wt_menuLabel">{t('bind.title')}</span>
|
||||
<div className="dsh-wt_bindProjName">{projects.nameOverrides[bindPick.id] ?? metas[bindPick.id]?.name ?? projects.layouts.find((l) => l.id === bindPick.id)?.title ?? bindPick.id}</div>
|
||||
{(() => {
|
||||
const sid = projects.bindings[bindPick.id]
|
||||
if (!sid) {
|
||||
return (
|
||||
<div className="dsh-wt_bindStatus dsh-wt_bindStatusNone">
|
||||
<span className="dsh-wt_bindMini" aria-hidden><span className="dsh-wt_bindCircles" /></span>
|
||||
<span className="dsh-wt_bindNoneText">{t('bind.unbound')}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const info = bindInfoOf(bindGroups, sid)
|
||||
return (
|
||||
<div className="dsh-wt_bindStatus">
|
||||
<span className="dsh-wt_bindFolder" title={info.folder}>📂 {info.folder}</span>
|
||||
<span className="dsh-wt_bindSep" aria-hidden>|</span>
|
||||
<span className="dsh-wt_bindConv" title={info.title}>{info.title}</span>
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
<p className="dsh-wt_bindHint">{t('bind.hint')}</p>
|
||||
{projects.bindings[bindPick.id] && (
|
||||
<button type="button" className="dsh-wt_bindUnbind" onClick={() => setProjectBinding(bindPick.id, null)}>{t('bind.unbind')} ✕</button>
|
||||
@@ -1622,7 +1664,7 @@ function buildCustomLayoutPrompt(req: string): string {
|
||||
type="button"
|
||||
className="dsh-wt_layout"
|
||||
data-on={activeSplitId === l.id ? 'true' : 'false'}
|
||||
style={{ order: effectiveOrder.indexOf(l.id) + 1000 }}
|
||||
style={{ order: effectiveOrder.indexOf(l.id) + 1000, position: 'relative' }}
|
||||
onClick={() => { openSplit(l); reportUsed(l.id) }}
|
||||
>
|
||||
<span
|
||||
@@ -1640,9 +1682,9 @@ function buildCustomLayoutPrompt(req: string): string {
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
data-bound={projects.bindings[l.id] ? 'true' : 'false'}
|
||||
title={t('bind.title')}
|
||||
title={projects.bindings[l.id] ? t('bind.titleBound') : t('bind.titleUnbound')}
|
||||
onClick={(e) => { e.stopPropagation(); openBindPick(l.id, e.currentTarget as HTMLElement) }}
|
||||
>🔗</span>
|
||||
><span className="dsh-wt_bindCircles" aria-hidden /></span>
|
||||
<span className="dsh-wt_layoutArrow" aria-hidden>›</span>
|
||||
</button>
|
||||
))}
|
||||
|
||||
@@ -58,6 +58,8 @@ export const zh = {
|
||||
'pane.tasks': '任务管理',
|
||||
'pane.terminal': '终端',
|
||||
'pane.custom': '自定义',
|
||||
'pane.anim': '动画',
|
||||
'pane.animUrlPh': '粘贴动画站点地址后回车',
|
||||
'custom.title': '自定义这个窗口',
|
||||
'custom.hint': '发送后将自动新建一个会话,专门负责这个窗口的自定义设计。',
|
||||
'custom.placeholder': '告诉 AI 你想让这个窗口做什么,例如:做一个展示我旅行足迹的时间线页面…',
|
||||
@@ -83,7 +85,10 @@ export const zh = {
|
||||
'custom.groupNeedPath': '请填写分组父目录和新分组名',
|
||||
'bind.title': '绑定对话',
|
||||
'bind.unbind': '解绑',
|
||||
'bind.hint': '选择后,打开该项目时右侧对话窗自动切换到这个会话;解绑后不再切换。',
|
||||
'bind.unbound': '未绑定对话',
|
||||
'bind.titleUnbound': '绑定对话(未绑定 ○○):点击选择要跟随的对话',
|
||||
'bind.titleBound': '已绑定对话(●●):点击更换或解绑',
|
||||
'bind.hint': '一个项目只能绑定一个对话。选择后,打开该项目时右侧对话窗会自动切换过去;已绑定的项目不会因新建/发送对话而自动改绑。',
|
||||
'customLayout.add': '自定义',
|
||||
'customLayout.addTitle': '自定义一个布局(生成提示词)',
|
||||
'customLayout.title': '自定义布局',
|
||||
@@ -91,6 +96,8 @@ export const zh = {
|
||||
'customLayout.copy': '复制提示词到剪贴板',
|
||||
'customLayout.copied': '提示词已复制到剪贴板。去任意一个 DSH 对话里粘贴发送,对方的 dsh 就知道要做什么了。',
|
||||
'customLayout.copyFail': '复制失败,请手动全选输入框内容复制。',
|
||||
'custom.autoBound': '已自动把这个项目绑定到这个对话(卡片上的 ●● 按钮可更换或解绑)。',
|
||||
'custom.keptBinding': '该项目已有绑定的对话,按规则未自动更改。',
|
||||
'pane.wip': '开发中(后续版本接入)',
|
||||
'pane.explorerNoCwd': '当前会话没有工作目录',
|
||||
'pane.openLater': '该类型暂不支持预览',
|
||||
@@ -172,6 +179,8 @@ export const en = {
|
||||
'pane.tasks': 'Tasks',
|
||||
'pane.terminal': 'Terminal',
|
||||
'pane.custom': 'Custom',
|
||||
'pane.anim': 'Animation',
|
||||
'pane.animUrlPh': 'Paste the animation site URL and press Enter',
|
||||
'custom.title': 'Customize this pane',
|
||||
'custom.hint': 'Sending will create a new conversation dedicated to designing this pane.',
|
||||
'custom.placeholder': 'Tell the AI what you want this pane to do, e.g. a timeline page of my travels…',
|
||||
@@ -197,7 +206,10 @@ export const en = {
|
||||
'custom.groupNeedPath': 'Fill in the parent folder and new group name',
|
||||
'bind.title': 'Bind conversation',
|
||||
'bind.unbind': 'Unbind',
|
||||
'bind.hint': 'When bound, opening this project switches the right chat pane to this conversation. Unbind to stop switching.',
|
||||
'bind.unbound': 'No conversation bound',
|
||||
'bind.titleUnbound': 'Bind conversation (unbound ○○): pick the conversation to follow',
|
||||
'bind.titleBound': 'Conversation bound (●●): click to change or unbind',
|
||||
'bind.hint': 'A project can bind exactly one conversation. When bound, opening the project switches the right chat pane to it; an existing binding is never auto-changed by creating/sending conversations.',
|
||||
'customLayout.add': 'Custom',
|
||||
'customLayout.addTitle': 'Customize a layout (generate a prompt)',
|
||||
'customLayout.title': 'Custom layout',
|
||||
@@ -205,6 +217,8 @@ export const en = {
|
||||
'customLayout.copy': 'Copy prompt to clipboard',
|
||||
'customLayout.copied': 'Prompt copied to clipboard. Paste it into any DSH conversation and send — that dsh will know what to do.',
|
||||
'customLayout.copyFail': 'Copy failed — select the text above and copy it manually.',
|
||||
'custom.autoBound': 'This project was auto-bound to this conversation (use the ●● button on its card to change or unbind).',
|
||||
'custom.keptBinding': 'This project already has a bound conversation, so it was not auto-changed.',
|
||||
'pane.wip': 'Coming in a later version',
|
||||
'pane.explorerNoCwd': 'No working directory for the current session',
|
||||
'pane.openLater': 'Preview not supported for this type',
|
||||
|
||||
@@ -23,7 +23,7 @@ hljs.registerLanguage('json', hljsJson)
|
||||
* 内容与 chatSide 的变更经 onSpecMutated 回调交给工作台持久化(布局条目)。
|
||||
*/
|
||||
|
||||
export type BuiltinType = 'browser' | 'explorer' | 'scm' | 'tasks' | 'terminal' | 'custom'
|
||||
export type BuiltinType = 'browser' | 'anim' | 'explorer' | 'scm' | 'tasks' | 'terminal' | 'custom'
|
||||
|
||||
export type SplitContent =
|
||||
| { kind: 'iframe'; url: string; title?: string }
|
||||
@@ -123,6 +123,7 @@ const PERSIST_KEY = 'dsh.worktable.split.v2'
|
||||
/** 内置内容窗图标 */
|
||||
const BUILTIN_ICONS: Record<BuiltinType, string> = {
|
||||
browser: '🌐',
|
||||
anim: '🎬',
|
||||
explorer: '📁',
|
||||
scm: '🔀',
|
||||
tasks: '✅',
|
||||
@@ -132,6 +133,7 @@ const BUILTIN_ICONS: Record<BuiltinType, string> = {
|
||||
|
||||
const BUILTIN_LABEL_KEYS: Record<BuiltinType, string> = {
|
||||
browser: 'pane.browser',
|
||||
anim: 'pane.anim',
|
||||
explorer: 'pane.explorer',
|
||||
scm: 'pane.scm',
|
||||
tasks: 'pane.tasks',
|
||||
@@ -861,6 +863,31 @@ function BrowserPane() {
|
||||
)
|
||||
}
|
||||
|
||||
/** 动画播放窗:iframe 壳 + 地址栏(站内自带项目/场景列表、播放、画幅切换、导出等全部控件) */
|
||||
function AnimPane() {
|
||||
const [url, setUrl] = useState('')
|
||||
const [src, setSrc] = useState('about:blank')
|
||||
const go = () => {
|
||||
const u = url.trim()
|
||||
setSrc(/^(\/|https?:\/\/)/i.test(u) ? u : 'about:blank')
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className="dsh-wt_browserBar">
|
||||
<input
|
||||
className="dsh-wt_browserInput"
|
||||
value={url}
|
||||
placeholder={T('pane.animUrlPh')}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') go() }}
|
||||
/>
|
||||
<button type="button" className="dsh-wt_browserGo" onClick={go}>↗</button>
|
||||
</div>
|
||||
<iframe className="dsh-wt_paneFrame" src={src} title="anim" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/** 文件夹图标(重绘 SVG,与 better-sidebar 同款风格) */
|
||||
function FolderIcon() {
|
||||
return (
|
||||
@@ -1313,6 +1340,7 @@ function CustomPane() {
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [done, setDone] = useState(false)
|
||||
const [fail, setFail] = useState('')
|
||||
const [bindNote, setBindNote] = useState<'auto' | 'kept' | 'none'>('none')
|
||||
// 默认项目 = 当前工作区所属项目;默认会话 = 当前会话;默认分组 = 当前会话所在工作区
|
||||
useEffect(() => {
|
||||
const list = custom?.getProjects?.() ?? []
|
||||
@@ -1344,10 +1372,12 @@ function CustomPane() {
|
||||
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)
|
||||
const sid = await custom.submit(projectId, pname, text, group)
|
||||
setBindNote((custom.autoBind?.(sid) ?? 'none') as any)
|
||||
} else {
|
||||
if (!sessionId) return
|
||||
await custom.sendToSession(sessionId, pname, text)
|
||||
setBindNote((custom.autoBind?.(sessionId) ?? 'none') as any)
|
||||
}
|
||||
setDone(true)
|
||||
} catch (e) {
|
||||
@@ -1365,6 +1395,9 @@ function CustomPane() {
|
||||
<span className="dsh-wt_customDone" aria-hidden>✅</span>
|
||||
<p className="dsh-wt_customDoneText">{mode === 'new' ? T('custom.done') : T('custom.sent')}</p>
|
||||
<p className="dsh-wt_customDoneHint">{T('custom.doneHint')}</p>
|
||||
{bindNote !== 'none' && (
|
||||
<p className="dsh-wt_customDoneBind">{bindNote === 'auto' ? T('custom.autoBound') : T('custom.keptBinding')}</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1473,6 +1506,7 @@ function PaneTabBody(props: { tab: PaneTab; row: PaneRow; index: number }) {
|
||||
return <FileViewer path={content.path} />
|
||||
}
|
||||
if (content.type === 'browser') return <BrowserPane />
|
||||
if (content.type === 'anim') return <AnimPane />
|
||||
if (content.type === 'explorer') return <ExplorerPane row={props.row} index={props.index} />
|
||||
if (content.type === 'scm') return <GitPane />
|
||||
if (content.type === 'tasks') return <JobsPane />
|
||||
@@ -1547,6 +1581,9 @@ function PanePicker(props: { row: PaneRow; index: number }) {
|
||||
<button type="button" className="dsh-wt_panePick" onClick={() => pick({ kind: 'builtin', type: 'browser' })}>
|
||||
<span aria-hidden>🌐</span>{T('pane.browser')}
|
||||
</button>
|
||||
<button type="button" className="dsh-wt_panePick" onClick={() => pick({ kind: 'builtin', type: 'anim' })}>
|
||||
<span aria-hidden>🎬</span>{T('pane.anim')}
|
||||
</button>
|
||||
<button type="button" className="dsh-wt_panePick" onClick={() => pick({ kind: 'builtin', type: 'explorer' })}>
|
||||
<span aria-hidden>📁</span>{T('pane.explorer')}
|
||||
</button>
|
||||
|
||||
@@ -153,11 +153,16 @@ export const css = [
|
||||
'.dsh-wt_presetAdd{border-style:dashed;color:var(--dsw-alias-state-accent-primary,#4f8ef7)}',
|
||||
'.dsh-wt_presetAddIcon{font-size:18px;line-height:20px}',
|
||||
'.dsh-wt_presetAddText{font-size:10px;line-height:13px}',
|
||||
// 项目卡片上的对话绑定按钮(布局卡内联 / 入驻卡由桥注入,绝对定位于中间偏右)
|
||||
'.dsh-wt_bindBtn{flex:none;display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;margin-left:auto;border-radius:5px;font-size:12px;line-height:1;color:var(--dsw-alias-label-tertiary,#6b7280);cursor:pointer;user-select:none}',
|
||||
'.dsh-wt_bindBtn:hover{background:var(--dsw-alias-fill-l2,rgba(255,255,255,.06));color:var(--dsw-alias-label-primary,#e6e8eb)}',
|
||||
// 项目卡片上的对话绑定按钮(○○ 空心 = 未绑定 / ●● 实心 = 已绑定;CSS 双圆单色绘制;
|
||||
// 布局卡与入驻卡统一绝对定位于卡片中间偏右、垂直居中,保证所有卡片竖向对齐)
|
||||
'.dsh-wt_bindBtn{position:absolute;right:26px;top:50%;transform:translateY(-50%);width:18px;height:14px;margin:0;display:block;color:var(--dsw-alias-label-tertiary,#6b7280);cursor:pointer;user-select:none}',
|
||||
'.dsh-wt_bindBtn:hover{color:var(--dsw-alias-label-primary,#e6e8eb)}',
|
||||
'.dsh-wt_bindBtn[data-bound=true]{color:var(--dsw-alias-state-accent-primary,#4f8ef7)}',
|
||||
'button[data-wt-id] > .dsh-wt_bindBtn{position:absolute;right:26px;top:50%;transform:translateY(-50%);margin:0}',
|
||||
'.dsh-wt_bindCircles{position:absolute;left:0;top:50%;width:18px;height:9px;transform:translateY(-50%)}',
|
||||
'.dsh-wt_bindCircles::before,.dsh-wt_bindCircles::after{content:\'\';position:absolute;top:50%;width:8px;height:8px;margin-top:-4px;border-radius:50%;box-sizing:border-box;border:1.4px solid currentColor;background:transparent}',
|
||||
'.dsh-wt_bindCircles::before{left:0}',
|
||||
'.dsh-wt_bindCircles::after{left:9px}',
|
||||
'.dsh-wt_bindBtn[data-bound=true] .dsh-wt_bindCircles::before,.dsh-wt_bindBtn[data-bound=true] .dsh-wt_bindCircles::after{background:currentColor}',
|
||||
|
||||
// 布局条目卡片
|
||||
'.dsh-wt_layout{display:flex;align-items:center;gap:7px;padding:6px 8px;border:1px solid var(--dsw-alias-border-l1,#262b36);border-radius:8px;background:var(--dsw-alias-fill-l1,rgba(255,255,255,.02));color:var(--dsw-alias-label-primary,#e6e8eb);font:inherit;font-size:12px;line-height:18px;cursor:pointer;text-align:left}',
|
||||
@@ -221,9 +226,17 @@ export const css = [
|
||||
'.dsh-wt_customLayoutBtn:hover{background:var(--dsw-alias-fill-l1,rgba(255,255,255,.05))}',
|
||||
'.dsh-wt_customLayoutToast{margin:8px 0 0;font-size:11.5px;line-height:17px;color:var(--dsw-alias-state-success,#3fb950)}',
|
||||
'.dsh-wt_customLayoutToastFail{color:var(--dsw-alias-state-danger,#f85149)}',
|
||||
// 对话绑定弹窗
|
||||
'.dsh-wt_bindProjName{font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary,#e6e8eb);margin-top:6px}',
|
||||
'.dsh-wt_bindHint{margin:4px 0 6px;font-size:11px;line-height:16px;color:var(--dsw-alias-label-secondary,#9aa4b2)}',
|
||||
// 对话绑定弹窗:状态行(未绑定 / 📂文件夹 | [对话框])
|
||||
'.dsh-wt_bindStatus{display:flex;align-items:center;gap:6px;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}',
|
||||
'.dsh-wt_bindStatusNone{color:var(--dsw-alias-label-secondary,#9aa4b2)}',
|
||||
'.dsh-wt_bindMini{position:relative;display:inline-block;width:18px;height:14px;flex:none}',
|
||||
'.dsh-wt_bindNoneText{font-size:12px;line-height:18px}',
|
||||
'.dsh-wt_bindFolder{flex:none;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary,#9aa4b2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:40%}',
|
||||
'.dsh-wt_bindSep{flex:none;color:var(--dsw-alias-border-l2,#3a4150);font-size:11px}',
|
||||
'.dsh-wt_bindConv{flex:1;min-width:0;padding:2px 8px;border:1px solid var(--dsw-alias-border-l2,#3a4150);border-radius:6px;background:var(--dsw-alias-fill-l2,rgba(255,255,255,.05));font-size:12px;line-height:18px;color:var(--dsw-alias-label-primary,#e6e8eb);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
||||
'.dsh-wt_bindHint{margin:6px 0 6px;font-size:11px;line-height:16px;color:var(--dsw-alias-label-secondary,#9aa4b2)}',
|
||||
// 自定义窗完成页:自动绑定提示
|
||||
'.dsh-wt_customDoneBind{margin:6px 0 0;font-size:11px;line-height:16px;color:var(--dsw-alias-state-accent-primary,#4f8ef7)}',
|
||||
'.dsh-wt_bindUnbind{display:inline-flex;align-items:center;gap:4px;margin-bottom:6px;padding:3px 8px;border:1px solid var(--dsw-alias-border-l1,#262b36);border-radius:6px;background:transparent;color:var(--dsw-alias-label-secondary,#9aa4b2);font:inherit;font-size:11px;line-height:16px;cursor:pointer}',
|
||||
'.dsh-wt_bindUnbind:hover{color:var(--dsw-alias-state-danger,#f85149);border-color:var(--dsw-alias-state-danger,#f85149)}',
|
||||
'.dsh-wt_bindList{max-height:280px;overflow:auto;position:static}',
|
||||
|
||||
@@ -292,7 +292,8 @@ export function apply(ctx: Context) {
|
||||
try {
|
||||
const file = pathResolve(homedir(), '.dsh', 'storages', 'workspace.json')
|
||||
const raw = await readFile(file, 'utf8')
|
||||
json(res, 200, JSON.parse(raw))
|
||||
// 容忍 BOM(外部工具改写可能带 EF BB BF,JSON.parse 会抛错)
|
||||
json(res, 200, JSON.parse(raw.charCodeAt(0) === 0xFEFF ? raw.slice(1) : raw))
|
||||
} catch (err) {
|
||||
json(res, 404, { error: String(err) })
|
||||
}
|
||||
|
||||
@@ -724,6 +724,28 @@
|
||||
- 重启后 functional-diag 全 20 STEP 复跑 ERROR_COUNT: 0(site/write/workspaces 路由全部就绪)。
|
||||
- 备注:之前 send-self-test 遗留的一次性会话 session-91a6adea 用户未删,STEP20 itemCount 13→14
|
||||
即该测试会话;再次提醒用户删除即可,不影响功能。
|
||||
## 补记(动画窗类型 + 绑定按钮改版 + 自动绑定规则 + 会话硬删除)
|
||||
|
||||
- 用户要求:① 删掉归档的一次性会话 session-91a6adea;② 新增「动画」窗类型(iframe 壳 + 地址栏,
|
||||
独立 🎬 图标,站内自带控件);③ 绑定按钮重画:竖向对齐 + ○○(未绑定)/●●(已绑定)双圆单色图标;
|
||||
④ 绑定弹窗改版:未绑定显示「未绑定对话」,已绑定显示「📂文件夹 | [对话名框]」,去掉照抄项目名的
|
||||
第二行;⑤ 自动绑定规则:子窗口新建/发送到会话且当前项目未绑定 → 自动绑定该会话;已绑定 → 永不
|
||||
自动改绑;⑥ 资深 PM 式 hover 提示。
|
||||
- 实现:① 会话目录 + workspace.json 引用删除(备份后清理,宿主已采纳);② BuiltinType 增 'anim'、
|
||||
BUILTIN_ICONS 🎬、AnimPane(浏览器窗同款壳、默认 about:blank)、选择器第 2 位;③ 绑定按钮改为
|
||||
绝对定位 right:26px/top:50% 统一对齐(布局卡与入驻卡一致),图标由 CSS 双圆绘制(::before/::after,
|
||||
data-bound 切换空心/实心、单色 currentColor);④ 弹窗状态行(bindInfoOf 查文件夹+标题,对话名带框)
|
||||
+ 提示文案「一个项目只能绑定一个对话…」;⑤ custom env 增 autoBind('auto'/'kept'/'none'),
|
||||
createCustomSession 返回会话 id,CustomPane 完成页显示自动绑定/未改绑提示。
|
||||
- 验证(probe-batch5/autobind-mini):pickCount=5、tabType=anim、地址栏+iframe ✓;3 个绑定按钮
|
||||
全部 absolute/delta=0 对齐、8px 空心圆 ✓;弹窗未绑定态「未绑定对话」无对话框 ✓,绑定后
|
||||
📂未分组 | [dsh-usage] 带框 ✓;autoBind 未绑定→'auto'+写入、再调→'kept'+不变 ✓;functional-diag
|
||||
全 20 STEP ERROR_COUNT: 0。
|
||||
- 事故与修复:删除会话时用 PowerShell Set-Content 重写 workspace.json 带上了 UTF-8 BOM,宿主路由
|
||||
JSON.parse 抛错 → /api/worktable/workspaces 404、会话分组退化为平铺(STEP20 groupHeaders 0)。
|
||||
已去 BOM 修复,并在服务端路由加 BOM 容忍(下次重启生效)。
|
||||
- 备注:动画窗/绑定按钮纯客户端,F5 即生效;服务端 BOM 容忍改动需下次重启 dsh web 后生效。
|
||||
|
||||
## 补记(第 7/8 布局 + +自定义磁贴 + 窗口改名 + 对话绑定)
|
||||
|
||||
- 用户要求:① 新增第 7 个预设「田字格」——右聊天通高整列、左侧 2×2 四窗默认等大,横向贯通、
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
// autobind-mini.cjs — 未绑定项目:autoBind 应返回 'auto' 并写入;二次调用 'kept' 不改
|
||||
const http = require('http');
|
||||
const { spawn } = require('child_process');
|
||||
const WebSocket = require('ws');
|
||||
const PORT = 9344;
|
||||
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-autobind',
|
||||
'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.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:{}})); } }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.__dshCustomEnv"); if (ready === true) break; }
|
||||
await sleep(1200);
|
||||
const step = await evaluate(`(async function(){
|
||||
var out = {};
|
||||
var st = window.__dshWorktable.splitStore;
|
||||
var env = window.__dshCustomEnv;
|
||||
st.open(JSON.parse(localStorage.getItem('dsh.worktable.projects.v1')).layouts[0]);
|
||||
await new Promise(function(r){ setTimeout(r, 600); });
|
||||
var pid = st.spec && st.spec.id;
|
||||
out.pid = pid;
|
||||
out.r1 = env.custom.autoBind('fake-session-1');
|
||||
await new Promise(function(r){ setTimeout(r, 400); });
|
||||
out.binding1 = JSON.parse(localStorage.getItem('dsh.worktable.projects.v1')).bindings[pid];
|
||||
out.r2 = env.custom.autoBind('fake-session-2');
|
||||
await new Promise(function(r){ setTimeout(r, 400); });
|
||||
out.binding2 = JSON.parse(localStorage.getItem('dsh.worktable.projects.v1')).bindings[pid];
|
||||
out.unchanged = out.binding2 === out.binding1;
|
||||
st.close();
|
||||
var j = JSON.parse(localStorage.getItem('dsh.worktable.projects.v1'));
|
||||
delete j.bindings[pid];
|
||||
localStorage.setItem('dsh.worktable.projects.v1', JSON.stringify(j));
|
||||
return JSON.stringify(out);
|
||||
})()`);
|
||||
console.log('AUTOBIND:', 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); });
|
||||
@@ -0,0 +1,80 @@
|
||||
// bindpop-mini.cjs — 只测绑定弹窗打开,捕获 React 异常
|
||||
const http = require('http');
|
||||
const { spawn } = require('child_process');
|
||||
const WebSocket = require('ws');
|
||||
const PORT = 9343;
|
||||
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-bindmini',
|
||||
'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:{}})); } }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"); if (ready === true) break; }
|
||||
await sleep(1500);
|
||||
const step0 = await evaluate(`(async function(){
|
||||
var st = window.__dshWorktable.splitStore;
|
||||
st.open({id:'t-anim',title:'anim',top:null,main:[{id:'p1',title:'W',min:200,content:null}],chatWidth:{default:320,min:240,max:600}});
|
||||
await new Promise(function(r){ setTimeout(r, 900); });
|
||||
var picks = [].slice.call(document.querySelectorAll('.dsh-wt_panePick')).filter(function(el){ return (getComputedStyle(el).visibility!=='hidden'); });
|
||||
var ap = picks.find(function(el){ return String(el.textContent).indexOf('\u52a8\u753b') >= 0; });
|
||||
if (ap) { ap.click(); }
|
||||
await new Promise(function(r){ setTimeout(r, 400); });
|
||||
st.close();
|
||||
await new Promise(function(r){ setTimeout(r, 300); });
|
||||
return JSON.stringify({ opened: true, tab: st.spec ? null : 'closed' });
|
||||
})()`);
|
||||
console.log('STEP0:', JSON.stringify(step0));
|
||||
const step1 = await evaluate(`(function(){
|
||||
var out = {};
|
||||
out.cards = document.querySelectorAll('.dsh-wt_layout').length;
|
||||
out.ls = localStorage.getItem('dsh.worktable.projects.v1');
|
||||
var b = document.querySelector('.dsh-wt_layout .dsh-wt_bindBtn');
|
||||
out.btn = !!b;
|
||||
if (b) { b.click(); }
|
||||
return JSON.stringify(out);
|
||||
})()`);
|
||||
console.log('STEP1:', JSON.stringify(step1));
|
||||
await sleep(800);
|
||||
const step2 = await evaluate(`(function(){
|
||||
var out = {};
|
||||
out.pop = !!document.querySelector('.dsh-wt_bindPop');
|
||||
out.pops = document.querySelectorAll('.dsh-wt_bindPop').length;
|
||||
out.backdrops = document.querySelectorAll('.dsh-wt_popBackdrop').length;
|
||||
var p = document.querySelector('.dsh-wt_bindPop');
|
||||
out.visHidden = p ? getComputedStyle(p).visibility : null;
|
||||
if (p) { var r = p.getBoundingClientRect(); out.rect = {x:r.x, y:r.y, w:r.width, h:r.height}; }
|
||||
out.err = window.__lastErr || null;
|
||||
return JSON.stringify(out);
|
||||
})()`);
|
||||
console.log('STEP2:', JSON.stringify(step2));
|
||||
const errs = events.filter((e) => e.method === 'Runtime.exceptionThrown').slice(-5)
|
||||
.map((e) => ((e.params.exceptionDetails.exception || {}).description || '').slice(0, 200));
|
||||
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); });
|
||||
@@ -0,0 +1,134 @@
|
||||
// probe-batch5.cjs — 动画窗类型 + 绑定按钮对齐/双圆图标 + 弹窗状态行 + 自动绑定规则
|
||||
const http = require('http');
|
||||
const { spawn } = require('child_process');
|
||||
const WebSocket = require('ws');
|
||||
const PORT = 9342;
|
||||
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-batch5',
|
||||
'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{ window.__seedRan = (window.__seedRan||0)+1; 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:{}})); window.__seedDone = true; } }catch(e){ window.__seedErr = String(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.__dshCustomEnv"); if (ready === true) break; }
|
||||
await sleep(1500);
|
||||
console.log('READY:', JSON.stringify(ready));
|
||||
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 st = window.__dshWorktable.splitStore;
|
||||
try {
|
||||
// A. 动画窗类型:打开工作区 → 6选1选择器多一项 → 打开 anim 标签 → 地址栏+iframe
|
||||
st.open({id:'t-anim',title:'anim',top:null,main:[{id:'p1',title:'\u7a97\u53e31',min:200,content:null}],chatWidth:{default:320,min:240,max:600}});
|
||||
await sleep(900);
|
||||
var picks = [].slice.call(document.querySelectorAll('.dsh-wt_panePick')).filter(function(el){ return (getComputedStyle(el).visibility!=='hidden'); });
|
||||
out.pickCount = picks.length;
|
||||
out.pickHasAnim = picks.some(function(el){ return String(el.textContent).indexOf('\u52a8\u753b') >= 0; });
|
||||
var animPick = picks.find(function(el){ return String(el.textContent).indexOf('\u52a8\u753b') >= 0; });
|
||||
if (animPick) { animPick.click(); }
|
||||
await sleep(400);
|
||||
out.tabType = st.spec.main[0].tabs && st.spec.main[0].tabs[0].content.type;
|
||||
out.animBar = !!vis('.dsh-wt_browserBar');
|
||||
out.animIframe = !!vis('.dsh-wt_paneFrame');
|
||||
st.close();
|
||||
await sleep(300);
|
||||
// B. 绑定按钮:双圆 CSS 图标 + 竖向对齐(所有卡片按钮中心与各自卡片中心偏差 <= 2px)
|
||||
var btns = [].slice.call(document.querySelectorAll('.dsh-wt_bindBtn')).filter(function(el){ return el.getBoundingClientRect().height > 0; });
|
||||
out.bindBtnCount = btns.length;
|
||||
out.aligns = btns.map(function(b){
|
||||
var card = b.closest('button');
|
||||
var br = b.getBoundingClientRect(); var cr = card ? card.getBoundingClientRect() : null;
|
||||
var ps = getComputedStyle(b);
|
||||
var b4 = getComputedStyle(b.querySelector('.dsh-wt_bindCircles'), '::before');
|
||||
return { pos: ps.position, right: ps.right, w: Math.round(br.width), h: Math.round(br.height), delta: card ? Math.round((br.top+br.height/2) - (cr.top+cr.height/2)) : null, circleW: b4.width, circleBorder: b4.borderTopWidth, circleFill: b4.backgroundColor };
|
||||
});
|
||||
out.allAbsAligned = out.aligns.every(function(a){ return a.pos === 'absolute' && Math.abs(a.delta) <= 2; });
|
||||
out.circleStyle = out.aligns[0] || null;
|
||||
// C. 绑定弹窗:未绑定状态行 → 绑定后状态行(📂 | [框])
|
||||
out.lsBeforeC = localStorage.getItem('dsh.worktable.projects.v1');
|
||||
out.seedRan = window.__seedRan; out.seedDone = window.__seedDone; out.seedErr = window.__seedErr || null;
|
||||
out.layoutCards = document.querySelectorAll('.dsh-wt_layout').length;
|
||||
var card0 = document.querySelector('.dsh-wt_layout');
|
||||
var bbtn = card0 && card0.querySelector('.dsh-wt_bindBtn');
|
||||
if (bbtn) { bbtn.click(); }
|
||||
await sleep(900);
|
||||
out.popup = !!vis('.dsh-wt_bindPop');
|
||||
out.popupRaw = document.querySelectorAll('.dsh-wt_bindPop').length;
|
||||
var pp = document.querySelector('.dsh-wt_bindPop');
|
||||
if (pp) { var prr = pp.getBoundingClientRect(); out.popupRect = { x: Math.round(prr.x), y: Math.round(prr.y), w: Math.round(prr.width), h: Math.round(prr.height), vis: getComputedStyle(pp).visibility }; }
|
||||
out.unboundText = vis('.dsh-wt_bindNoneText') ? String(vis('.dsh-wt_bindNoneText').textContent).trim() : null;
|
||||
out.noConvBoxWhenUnbound = !vis('.dsh-wt_bindConv');
|
||||
var items = [].slice.call(document.querySelectorAll('.dsh-wt_bindList .dsh-wt_selectItem'));
|
||||
out.bindItemCount = items.length;
|
||||
if (items[0]) { items[0].click(); }
|
||||
await sleep(900);
|
||||
var lid = JSON.parse(localStorage.getItem('dsh.worktable.projects.v1')).layouts[0].id;
|
||||
var boundVal = JSON.parse(localStorage.getItem('dsh.worktable.projects.v1')).bindings[lid];
|
||||
out.boundSaved = typeof boundVal === 'string' && boundVal.length > 0;
|
||||
if (bbtn) { bbtn.click(); }
|
||||
await sleep(900);
|
||||
out.popup2 = !!vis('.dsh-wt_bindPop');
|
||||
out.folderShown = vis('.dsh-wt_bindFolder') ? String(vis('.dsh-wt_bindFolder').textContent).trim() : null;
|
||||
out.sepShown = !!vis('.dsh-wt_bindSep');
|
||||
var conv = vis('.dsh-wt_bindConv');
|
||||
out.convBox = conv ? { text: String(conv.textContent).trim(), border: getComputedStyle(conv).borderTopWidth, bg: getComputedStyle(conv).backgroundColor } : null;
|
||||
out.noNoneTextWhenBound = !vis('.dsh-wt_bindNoneText');
|
||||
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);
|
||||
// D. 自动绑定规则:直接调 env.autoBind
|
||||
var env = window.__dshCustomEnv;
|
||||
st.open(JSON.parse(localStorage.getItem('dsh.worktable.projects.v1')).layouts[0]);
|
||||
await sleep(600);
|
||||
var pid = st.spec && st.spec.id;
|
||||
out.autoPid = pid;
|
||||
var r1 = env.custom.autoBind('fake-session-1');
|
||||
out.auto1 = r1;
|
||||
out.bindingAfter1 = JSON.parse(localStorage.getItem('dsh.worktable.projects.v1')).bindings[pid];
|
||||
var r2 = env.custom.autoBind('fake-session-2');
|
||||
out.auto2 = r2;
|
||||
out.bindingAfter2 = JSON.parse(localStorage.getItem('dsh.worktable.projects.v1')).bindings[pid];
|
||||
st.close();
|
||||
// 清理:解绑
|
||||
var layouts2 = JSON.parse(localStorage.getItem('dsh.worktable.projects.v1'));
|
||||
delete layouts2.bindings[pid];
|
||||
localStorage.setItem('dsh.worktable.projects.v1', JSON.stringify(layouts2));
|
||||
// 恢复原会话
|
||||
var S = window.__dshSessions;
|
||||
var snap0 = S.list.getSnapshot();
|
||||
if (snap0.current) { window.__dshOpenSession(snap0.current); }
|
||||
} catch(e) { out.err = String(e); }
|
||||
return JSON.stringify(out);
|
||||
})()`);
|
||||
console.log('BATCH5:', JSON.stringify(step));
|
||||
const errs = events.filter((e) => e.method === 'Runtime.exceptionThrown').slice(-6)
|
||||
.map((e) => ((e.params.exceptionDetails.exception || {}).description || '').slice(0, 300));
|
||||
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