feat: console glow cards, icon theme toggle, cold preview warmup, unified naming
This commit is contained in:
@@ -312,10 +312,10 @@ const CONSOLE_ID = 'wt-console'
|
||||
const CONSOLE_ICON = '🖥️'
|
||||
|
||||
/** 控制室默认布局:单一锁死大窗格(项目卡片网格)+ 右侧对话 */
|
||||
function buildConsoleSpec(): LayoutSpec {
|
||||
function buildConsoleSpec(t: (key: string) => string): LayoutSpec {
|
||||
return {
|
||||
id: CONSOLE_ID,
|
||||
title: '工作台',
|
||||
title: t('console.name'),
|
||||
icon: CONSOLE_ICON,
|
||||
top: null,
|
||||
main: [{
|
||||
@@ -348,6 +348,70 @@ function listWorkspaces(): { id: string; title: string; path: string }[] {
|
||||
} catch { return [] }
|
||||
}
|
||||
|
||||
/** 冷会话最近消息缓存与预热:宿主 history 只读通道(face.history 为运行期内建方法,非公开接口,
|
||||
* 只读侦察确认可用;拉取是带宽成本不是 Token 成本,模型不参与)。 */
|
||||
const previewCache = new Map<string, string>()
|
||||
const previewFetching = new Set<string>()
|
||||
let previewSweepBusy = false
|
||||
let previewTimer: number | null = null
|
||||
|
||||
/** 从 history 事件流尾部提取最近一条成品消息文本(优先 text 块,回退任意文本) */
|
||||
async function coldPreviewOf(face: any): Promise<string> {
|
||||
if (!face || typeof face.history !== 'function') return ''
|
||||
const r1 = await face.history({ maxMessages: 2 })
|
||||
const evs = r1?.result?.value?.events
|
||||
if (!Array.isArray(evs)) return ''
|
||||
const textOf = (blocks: any): string => {
|
||||
let fallback = ''
|
||||
if (!Array.isArray(blocks)) return ''
|
||||
for (const b of blocks) {
|
||||
const s = typeof b?.text === 'string' ? b.text.trim().replace(/\s+/g, ' ') : ''
|
||||
if (!s) continue
|
||||
if (b?.type === 'text') return s.slice(0, 140)
|
||||
if (!fallback) fallback = s.slice(0, 140)
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
for (let i = evs.length - 1; i >= 0; i--) {
|
||||
const ev = evs[i]?.event
|
||||
if (!ev) continue
|
||||
const d = ev.data ?? {}
|
||||
if (ev.type === 'user/message') {
|
||||
const s = textOf(d.content ?? d.blocks)
|
||||
if (s) return s
|
||||
} else if (ev.type === 'assistant/message') {
|
||||
const m = d.message ?? d
|
||||
const s = textOf(m.content ?? m.blocks)
|
||||
if (s) return s
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
/** 预热所有已绑定会话的最近消息(逐个只读拉取;已在拉/已拉过未完成的跳过,失败静默回退内存路径) */
|
||||
async function sweepPreviews() {
|
||||
if (previewSweepBusy) return
|
||||
previewSweepBusy = true
|
||||
try {
|
||||
const sids = Array.from(new Set(Object.values(projectBindingsRef.current).filter((x): x is string => !!x)))
|
||||
for (const sid of sids) {
|
||||
if (previewFetching.has(sid)) continue
|
||||
previewFetching.add(sid)
|
||||
try {
|
||||
const face = sessionBridge?.sessions?.binding?.(sid)?.session
|
||||
const txt = await coldPreviewOf(face)
|
||||
if (txt) { previewCache.set(sid, txt); notifyConsole() }
|
||||
} catch {} finally { previewFetching.delete(sid) }
|
||||
}
|
||||
} finally { previewSweepBusy = false }
|
||||
}
|
||||
|
||||
/** 防抖调度:控制室开着且会话快照变化时刷新预览(合并 6s 内的连续变化) */
|
||||
function schedulePreviewSweep() {
|
||||
if (previewTimer != null) return
|
||||
previewTimer = window.setTimeout(() => { previewTimer = null; sweepPreviews() }, 6000)
|
||||
}
|
||||
|
||||
/** 从会话快照节点里提取最近一条文本(纯读内存镜像,零 Token;无则 '') */
|
||||
function lastTextOf(sid: string): string {
|
||||
try {
|
||||
@@ -815,6 +879,8 @@ function WorktableSection(props: any) {
|
||||
)
|
||||
/** 控制室动作引用(每渲染同步,供 env 闭包读取) */
|
||||
const actionsRef = useRef<{ openSplit: (spec: LayoutSpec) => void; openConsole: (explicitBound?: string | null) => void } | null>(null)
|
||||
/** 提醒确认动作引用(每渲染同步;env 闭包用) */
|
||||
const ackRef = useRef<((projectId: string) => void) | null>(null)
|
||||
/** 视图状态最新快照(供 env 闭包读取控制室主题) */
|
||||
const viewRef = useRef(view)
|
||||
viewRef.current = view
|
||||
@@ -881,16 +947,21 @@ function WorktableSection(props: any) {
|
||||
} catch {}
|
||||
return null
|
||||
}
|
||||
const ackMap = loadNotifyAck()
|
||||
const make = (id: string, name: string, icon: string, self: boolean): ConsoleCardData => {
|
||||
const sid = pr.projects.bindings[id]
|
||||
const status = statusOf(sid)
|
||||
// 发光 = 完成/待决且本轮未被确认;点卡片确认后熄灭
|
||||
const glow = !!sid && ((status === 'done' && ackMap[sid] !== 'done') || (status === 'need' && ackMap[sid] !== 'need'))
|
||||
return {
|
||||
id, name, icon,
|
||||
status: statusOf(sid),
|
||||
status,
|
||||
runtimeMs: runtimeOf(sid),
|
||||
kids: sid ? kidsSetOf(sid).size : 0,
|
||||
preview: sid ? lastTextOf(sid) : '',
|
||||
preview: sid ? (previewCache.get(sid) ?? lastTextOf(sid)) : '',
|
||||
bound: !!sid,
|
||||
self,
|
||||
glow,
|
||||
}
|
||||
}
|
||||
const cards: ConsoleCardData[] = []
|
||||
@@ -965,6 +1036,11 @@ function WorktableSection(props: any) {
|
||||
getCards: () => getConsoleCards(),
|
||||
getTheme: () => viewRef.current.consoleTheme ?? 'system',
|
||||
setTheme: (th: 'dark' | 'light' | 'system') => persistView({ consoleTheme: th }),
|
||||
onAck: (id) => { ackRef.current?.(id); setNotifyTick((t) => t + 1); notifyConsole() },
|
||||
refreshPreviews: () => {
|
||||
if (previewTimer != null) { window.clearTimeout(previewTimer); previewTimer = null }
|
||||
sweepPreviews()
|
||||
},
|
||||
onOpen: (id) => {
|
||||
if (id === CONSOLE_ID) return
|
||||
const pr = projectsRef.current.projects
|
||||
@@ -1226,7 +1302,7 @@ function buildCustomLayoutPrompt(req: string): string {
|
||||
|
||||
/** 打开「工作台」控制室:已绑定 → 打开并切换绑定对话;explicitBound 供强制绑定流程传入刚绑定的会话 */
|
||||
const openConsole = useCallback((explicitBound?: string | null) => {
|
||||
const spec = projects.views[CONSOLE_ID] ?? buildConsoleSpec()
|
||||
const spec = projects.views[CONSOLE_ID] ?? buildConsoleSpec((k) => t(k as WorktableKey))
|
||||
engineIdsRef.current.add(CONSOLE_ID)
|
||||
splitStore.open(spec)
|
||||
if (splitStore.active && splitStore.spec?.id === CONSOLE_ID) {
|
||||
@@ -1238,8 +1314,10 @@ function buildCustomLayoutPrompt(req: string): string {
|
||||
if (bound) { try { sessionBridge?.sessions?.open?.(bound) } catch {} }
|
||||
ackProjectNotify(CONSOLE_ID)
|
||||
try { notifyConsole() } catch {}
|
||||
// 打开即预热所有绑定会话的最近消息(冷会话走 history 只读通道)
|
||||
try { sweepPreviews() } catch {}
|
||||
}
|
||||
}, [projects.views])
|
||||
}, [projects.views, t])
|
||||
|
||||
/** 控制室卡片点击:已绑定 → 打开控制室;未绑定 → 强制绑定弹窗 */
|
||||
const clickConsoleCard = (anchor: HTMLElement) => {
|
||||
@@ -1366,9 +1444,13 @@ function buildCustomLayoutPrompt(req: string): string {
|
||||
copyToastTimerRef.current = window.setTimeout(() => setCopiedToast(null), 6000)
|
||||
}
|
||||
|
||||
// 会话快照变化 → 重渲染(驱动项目卡片的完成/待决提醒镜像 + 控制室面板)
|
||||
// 会话快照变化 → 重渲染(驱动项目卡片的完成/待决提醒镜像 + 控制室面板 + 预览防抖刷新)
|
||||
useEffect(() => {
|
||||
const l = () => { setNotifyTick((t) => t + 1); notifyConsole() }
|
||||
const l = () => {
|
||||
setNotifyTick((t) => t + 1)
|
||||
notifyConsole()
|
||||
if (splitStore.active && splitStore.spec?.id === CONSOLE_ID) schedulePreviewSweep()
|
||||
}
|
||||
sessionsSnapshotStore.listeners.add(l)
|
||||
return () => { sessionsSnapshotStore.listeners.delete(l) }
|
||||
}, [])
|
||||
@@ -1493,6 +1575,7 @@ function buildCustomLayoutPrompt(req: string): string {
|
||||
if (st === 'done') saveNotifyAck(sid, 'done')
|
||||
}
|
||||
}
|
||||
ackRef.current = ackProjectNotify
|
||||
|
||||
// ── 有效排序 ──
|
||||
// 手动:持久化 order(过滤已卸载 id)→ 新注册 id 与布局 id 追加尾部;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
/** Simplified Chinese dictionary (the key-set source of truth). */
|
||||
export const zh = {
|
||||
'title': '工作台',
|
||||
'title': '控制室',
|
||||
'rail.title': '工作台(展开侧边栏查看)',
|
||||
'handle.title': '按住拖动位置 · 双击复位',
|
||||
'handle.aria': '拖动工作台位置',
|
||||
@@ -113,7 +113,7 @@ export const zh = {
|
||||
'customLayout.copyFail': '复制失败,请手动全选输入框内容复制。',
|
||||
'custom.autoBound': '已自动把这个项目绑定到这个对话(卡片上的 ●● 按钮可更换或解绑)。',
|
||||
'custom.keptBinding': '该项目已有绑定的对话,按规则未自动更改。',
|
||||
'console.name': '工作台',
|
||||
'console.name': '控制室',
|
||||
'console.title': '控制室',
|
||||
'console.themeLabel': '面板主题',
|
||||
'console.themeDark': '深色',
|
||||
@@ -167,7 +167,7 @@ export type WorktableKey = keyof typeof zh
|
||||
|
||||
/** English dictionary, checked complete against the zh key set. */
|
||||
export const en = {
|
||||
'title': 'Worktable',
|
||||
'title': 'Console',
|
||||
'rail.title': 'Worktable (expand sidebar to view)',
|
||||
'handle.title': 'Drag to move · double-click to reset',
|
||||
'handle.aria': 'Drag worktable position',
|
||||
@@ -274,7 +274,7 @@ export const en = {
|
||||
'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.',
|
||||
'console.name': 'Worktable',
|
||||
'console.name': 'Console',
|
||||
'console.title': 'Console',
|
||||
'console.themeLabel': 'Panel theme',
|
||||
'console.themeDark': 'Dark',
|
||||
|
||||
@@ -206,6 +206,8 @@ export type ConsoleCardData = {
|
||||
bound: boolean
|
||||
/** 是否为「工作台」自己(卡片点击无操作) */
|
||||
self: boolean
|
||||
/** 完成/待决且未被确认:卡片发对应色光(点击确认后熄灭) */
|
||||
glow: boolean
|
||||
}
|
||||
|
||||
type SplitEnv = {
|
||||
@@ -226,6 +228,10 @@ type SplitEnv = {
|
||||
getCards: () => ConsoleCardData[]
|
||||
onOpen: (id: string) => void
|
||||
onJump: (id: string) => void
|
||||
/** 点发光卡片:确认(熄灭光)再打开 */
|
||||
onAck: (id: string) => void
|
||||
/** 冷会话消息预热(打开控制室时拉最近消息) */
|
||||
refreshPreviews: () => void
|
||||
getTheme: () => 'dark' | 'light' | 'system'
|
||||
setTheme: (th: 'dark' | 'light' | 'system') => void
|
||||
}
|
||||
@@ -975,6 +981,10 @@ function ConsolePane() {
|
||||
try { mq.addEventListener('change', f) } catch { try { (mq as any).addListener(f) } catch {} }
|
||||
return () => { try { mq.removeEventListener('change', f) } catch { try { (mq as any).removeListener(f) } catch {} } }
|
||||
}, [])
|
||||
// 打开控制室 = 预热所有绑定会话的最近消息(冷会话走宿主 history 只读通道)
|
||||
useEffect(() => {
|
||||
splitEnv?.console?.refreshPreviews?.()
|
||||
}, [])
|
||||
// 运行时长的分钟级刷新:每秒重渲染一次(仅控制室开着时存在)
|
||||
useEffect(() => {
|
||||
const iv = window.setInterval(() => setNow(Date.now()), 1000)
|
||||
@@ -1001,10 +1011,10 @@ function ConsolePane() {
|
||||
return h > 0 ? h + ':' + String(m).padStart(2, '0') + ':' + String(ss).padStart(2, '0') : m + ':' + String(ss).padStart(2, '0')
|
||||
}
|
||||
const statusLabel: Record<string, string> = { idle: T('console.idle'), busy: T('console.busy'), need: T('console.need'), done: T('console.done') }
|
||||
const themeOpts: { mode: 'dark' | 'light' | 'system'; key: string }[] = [
|
||||
{ mode: 'dark', key: 'console.themeDark' },
|
||||
{ mode: 'light', key: 'console.themeLight' },
|
||||
{ mode: 'system', key: 'console.themeSystem' },
|
||||
const themeOpts: { mode: 'dark' | 'light' | 'system'; icon: string; key: string }[] = [
|
||||
{ mode: 'dark', icon: '🌙', key: 'console.themeDark' },
|
||||
{ mode: 'light', icon: '☀️', key: 'console.themeLight' },
|
||||
{ mode: 'system', icon: '🖥️', key: 'console.themeSystem' },
|
||||
]
|
||||
return (
|
||||
<div className="dsh-wt_console" data-wt-theme={resolvedTheme}>
|
||||
@@ -1016,8 +1026,10 @@ function ConsolePane() {
|
||||
key={o.mode}
|
||||
type="button"
|
||||
className={'dsh-wt_consoleThemeBtn' + (themeMode === o.mode ? ' dsh-wt_consoleThemeBtnOn' : '')}
|
||||
title={T(o.key)}
|
||||
aria-label={T(o.key)}
|
||||
onClick={() => setTheme(o.mode)}
|
||||
>{T(o.key)}</button>
|
||||
><span aria-hidden>{o.icon}</span></button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1027,18 +1039,27 @@ function ConsolePane() {
|
||||
key={c.id}
|
||||
role={c.self ? undefined : 'button'}
|
||||
tabIndex={c.self ? -1 : 0}
|
||||
className={'dsh-wt_consoleCard' + (c.self ? ' dsh-wt_consoleCardSelf' : '')}
|
||||
className={'dsh-wt_consoleCard' + (c.self ? ' dsh-wt_consoleCardSelf' : '')
|
||||
+ (c.status === 'busy' ? ' dsh-wt_consoleCard-busy' : '')
|
||||
+ (c.glow && c.status === 'done' ? ' dsh-wt_consoleCard-glowDone' : '')
|
||||
+ (c.glow && c.status === 'need' ? ' dsh-wt_consoleCard-glowNeed' : '')}
|
||||
title={c.name}
|
||||
onClick={() => { if (!c.self && env) env.onOpen(c.id) }}
|
||||
onClick={() => {
|
||||
if (c.self || !env) return
|
||||
if (c.glow) env.onAck?.(c.id) // 点发光卡片:先确认熄光,再进入
|
||||
env.onOpen(c.id)
|
||||
}}
|
||||
>
|
||||
<div className="dsh-wt_consoleCardHead">
|
||||
<span className="dsh-wt_consoleIcon" aria-hidden>{c.icon}</span>
|
||||
<span className="dsh-wt_consoleName">{c.name}</span>
|
||||
<span className={'dsh-wt_consoleDot dsh-wt_consoleDot-' + c.status} aria-hidden />
|
||||
</div>
|
||||
<div className="dsh-wt_consoleStatusRow">
|
||||
<span className={'dsh-wt_consoleStatus dsh-wt_consoleStatus-' + c.status}>{statusLabel[c.status]}</span>
|
||||
{c.runtimeMs != null && <span className="dsh-wt_consoleRuntime">⏱ {fmtDur(c.runtimeMs)}</span>}
|
||||
</div>
|
||||
<div className="dsh-wt_consoleBadges">
|
||||
<span className={'dsh-wt_consoleBadge dsh-wt_consoleBadge-' + c.status}>{statusLabel[c.status]}</span>
|
||||
{c.runtimeMs != null && <span className="dsh-wt_consoleBadge">⏱ {fmtDur(c.runtimeMs)}</span>}
|
||||
<span className="dsh-wt_consoleBadge">{T('console.kids')} {c.kids}</span>
|
||||
</div>
|
||||
<div className={'dsh-wt_consolePreview' + (c.preview ? '' : ' dsh-wt_consolePreviewNone')} title={c.preview}>
|
||||
|
||||
@@ -387,30 +387,48 @@ export const css = xtermCss + '\n' + [
|
||||
'.dsh-wt_consoleHead{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:12px}',
|
||||
'.dsh-wt_consoleTitle{font-size:14px;font-weight:700;line-height:20px;color:var(--wt-text)}',
|
||||
'.dsh-wt_consoleTheme{display:inline-flex;align-items:center;padding:2px;border:1px solid var(--wt-border);border-radius:999px;background:var(--wt-card)}',
|
||||
'.dsh-wt_consoleThemeBtn{flex:none;padding:2px 10px;border:none;border-radius:999px;background:transparent;color:var(--wt-text2);font:inherit;font-size:11px;line-height:18px;cursor:pointer}',
|
||||
'.dsh-wt_consoleThemeBtnOn{background:var(--wt-chip);color:var(--wt-text);font-weight:600}',
|
||||
'.dsh-wt_consoleGrid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;align-content:start}',
|
||||
'.dsh-wt_consoleCard{position:relative;display:flex;flex-direction:column;gap:8px;min-height:96px;padding:12px 14px;background:var(--wt-card);border:1px solid var(--wt-border);border-radius:12px;cursor:pointer;transition:transform .12s ease,border-color .12s ease,box-shadow .12s ease}',
|
||||
'.dsh-wt_consoleCard:hover{transform:translateY(-1px);border-color:var(--wt-borderHover);box-shadow:var(--wt-shadow)}',
|
||||
'.dsh-wt_consoleCardSelf{cursor:default;opacity:.96}',
|
||||
'.dsh-wt_consoleCardHead{display:flex;align-items:center;gap:8px;min-width:0}',
|
||||
'.dsh-wt_consoleIcon{flex:none;font-size:20px;line-height:1}',
|
||||
'.dsh-wt_consoleName{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px;font-weight:600;line-height:20px;color:var(--wt-text)}',
|
||||
'.dsh-wt_consoleDot{flex:none;width:8px;height:8px;border-radius:50%;background:var(--wt-text3)}',
|
||||
'.dsh-wt_consoleDot-busy{background:var(--dsw-alias-state-accent-primary,#4f8ef7);box-shadow:0 0 0 3px rgba(79,142,247,.15)}',
|
||||
'.dsh-wt_consoleDot-need{background:var(--dsw-alias-state-warning,#d29922);box-shadow:0 0 0 3px rgba(210,153,34,.15)}',
|
||||
'.dsh-wt_consoleDot-done{background:var(--dsw-alias-state-success,#3fb950);box-shadow:0 0 0 3px rgba(63,185,80,.15)}',
|
||||
'.dsh-wt_consoleThemeBtn{flex:none;padding:3px 9px;border:none;border-radius:999px;background:transparent;color:var(--wt-text3);font:inherit;font-size:12px;line-height:18px;cursor:pointer;opacity:.85}',
|
||||
'.dsh-wt_consoleThemeBtn:hover{opacity:1;background:var(--wt-chip)}',
|
||||
'.dsh-wt_consoleThemeBtnOn{background:var(--wt-chip);color:var(--wt-text);opacity:1}',
|
||||
'.dsh-wt_consoleGrid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px;align-content:start}',
|
||||
'.dsh-wt_consoleCard{position:relative;display:flex;flex-direction:column;gap:10px;aspect-ratio:1/1;min-height:150px;padding:16px;background:var(--wt-card);border:1px solid var(--wt-border);border-radius:14px;cursor:pointer;transition:transform .12s ease,border-color .12s ease,box-shadow .12s ease;overflow:hidden}',
|
||||
'.dsh-wt_consoleCard:hover{transform:translateY(-2px);border-color:var(--wt-borderHover);box-shadow:var(--wt-shadow)}',
|
||||
'.dsh-wt_consoleCardSelf{cursor:default;opacity:.97}',
|
||||
'.dsh-wt_consoleCardHead{display:flex;align-items:center;gap:9px;min-width:0}',
|
||||
'.dsh-wt_consoleIcon{flex:none;font-size:24px;line-height:1}',
|
||||
'.dsh-wt_consoleName{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:14.5px;font-weight:700;line-height:22px;color:var(--wt-text)}',
|
||||
'.dsh-wt_consoleDot{flex:none;width:9px;height:9px;border-radius:50%;background:var(--wt-text3)}',
|
||||
'.dsh-wt_consoleDot-busy{background:var(--dsw-alias-state-accent-primary,#4f8ef7);box-shadow:0 0 0 4px rgba(79,142,247,.16)}',
|
||||
'.dsh-wt_consoleDot-need{background:var(--dsw-alias-state-warning,#d29922);box-shadow:0 0 0 4px rgba(210,153,34,.16)}',
|
||||
'.dsh-wt_consoleDot-done{background:var(--dsw-alias-state-success,#3fb950);box-shadow:0 0 0 4px rgba(63,185,80,.16)}',
|
||||
'.dsh-wt_consoleStatusRow{display:flex;align-items:baseline;gap:8px;min-width:0}',
|
||||
'.dsh-wt_consoleStatus{flex:none;font-size:17px;font-weight:700;line-height:24px;color:var(--wt-text2)}',
|
||||
'.dsh-wt_consoleStatus-busy{color:var(--dsw-alias-state-accent-primary,#4f8ef7)}',
|
||||
'.dsh-wt_consoleStatus-need{color:var(--dsw-alias-state-warning,#d29922)}',
|
||||
'.dsh-wt_consoleStatus-done{color:var(--dsw-alias-state-success,#3fb950)}',
|
||||
'.dsh-wt_consoleRuntime{flex:none;font-size:13px;font-weight:600;line-height:20px;color:var(--wt-text2);font-variant-numeric:tabular-nums}',
|
||||
'.dsh-wt_consoleBadges{display:flex;align-items:center;gap:6px;flex-wrap:wrap}',
|
||||
'.dsh-wt_consoleBadge{flex:none;padding:1px 7px;border-radius:999px;font-size:10.5px;line-height:16px;color:var(--wt-text2);background:var(--wt-chip)}',
|
||||
'.dsh-wt_consoleBadge-busy{color:var(--dsw-alias-state-accent-primary,#4f8ef7)}',
|
||||
'.dsh-wt_consoleBadge-need{color:var(--dsw-alias-state-warning,#d29922)}',
|
||||
'.dsh-wt_consoleBadge-done{color:var(--dsw-alias-state-success,#3fb950)}',
|
||||
'.dsh-wt_consolePreview{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:11px;line-height:16px;color:var(--wt-text2)}',
|
||||
'.dsh-wt_consolePreviewNone{color:var(--wt-text3);font-style:italic}',
|
||||
'.dsh-wt_consoleJump{position:absolute;top:10px;right:10px;width:22px;height:22px;padding:0;border:none;border-radius:6px;background:transparent;color:var(--wt-text3);font-size:12px;line-height:1;cursor:pointer;opacity:0;transition:opacity .12s ease}',
|
||||
'.dsh-wt_consoleBadge{flex:none;padding:2px 9px;border-radius:999px;font-size:12px;font-weight:600;line-height:18px;color:var(--wt-text2);background:var(--wt-chip)}',
|
||||
'.dsh-wt_consolePreview{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;font-size:12.5px;line-height:18px;color:var(--wt-text2);word-break:break-word}',
|
||||
'.dsh-wt_consolePreviewNone{color:var(--wt-text3);font-style:italic;-webkit-line-clamp:1}',
|
||||
// 三色光效:完成=绿光、待决=黄光(背景泛光,点击确认后熄灭);工作=蓝色描边光晕顺时针旋转
|
||||
'.dsh-wt_consoleCard-glowDone{border-color:rgba(63,185,80,.45);box-shadow:0 0 22px rgba(63,185,80,.22)}',
|
||||
'.dsh-wt_consoleCard-glowDone::after{content:"";position:absolute;inset:0;border-radius:inherit;background:radial-gradient(130% 130% at 50% 0%,rgba(63,185,80,.16),transparent 62%);pointer-events:none}',
|
||||
'.dsh-wt_consoleCard-glowNeed{border-color:rgba(210,153,34,.5);box-shadow:0 0 22px rgba(210,153,34,.22)}',
|
||||
'.dsh-wt_consoleCard-glowNeed::after{content:"";position:absolute;inset:0;border-radius:inherit;background:radial-gradient(130% 130% at 50% 0%,rgba(210,153,34,.17),transparent 62%);pointer-events:none}',
|
||||
'@property --consoleAngle{syntax:"<angle>";initial-value:0deg;inherits:false}',
|
||||
'.dsh-wt_consoleCard-busy::before{content:"";position:absolute;inset:-2px;border-radius:16px;padding:2px;background:conic-gradient(from var(--consoleAngle),transparent 0deg,rgba(79,142,247,0) 30deg,rgba(79,142,247,.95) 80deg,rgba(79,142,247,0) 130deg,transparent 200deg);-webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);-webkit-mask-composite:xor;mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);mask-composite:exclude;animation:consoleAngleSpin 1.8s linear infinite;pointer-events:none}',
|
||||
'@keyframes consoleAngleSpin{to{--consoleAngle:360deg}}',
|
||||
'.dsh-wt_consoleJump{position:absolute;top:12px;right:12px;width:24px;height:24px;padding:0;border:none;border-radius:7px;background:transparent;color:var(--wt-text3);font-size:13px;line-height:1;cursor:pointer;opacity:0;transition:opacity .12s ease}',
|
||||
'.dsh-wt_consoleCard:hover .dsh-wt_consoleJump{opacity:1}',
|
||||
'.dsh-wt_consoleJump:hover{color:var(--dsw-alias-state-accent-primary,#4f8ef7);background:var(--wt-chip)}',
|
||||
'.dsh-wt_consoleEmpty{padding:40px 0;text-align:center;font-size:12px;color:var(--wt-text3)}',
|
||||
// 控制室入口卡:统筹主程序的差异化(渐变底 + 青色描边 + 微光,克制不喧宾夺主)
|
||||
'.dsh-wt_projects .dsh-wt_consoleEntry{background:linear-gradient(135deg,rgba(79,142,247,.14),rgba(139,92,246,.10));border-color:rgba(79,142,247,.4);box-shadow:0 0 12px rgba(79,142,247,.14)}',
|
||||
'.dsh-wt_projects .dsh-wt_consoleEntry:hover{background:linear-gradient(135deg,rgba(79,142,247,.2),rgba(139,92,246,.14));border-color:rgba(79,142,247,.6);box-shadow:0 0 16px rgba(79,142,247,.22)}',
|
||||
'.dsh-wt_projects .dsh-wt_consoleEntry[data-on=true]{border-color:rgba(79,142,247,.7);box-shadow:0 0 16px rgba(79,142,247,.28)}',
|
||||
'.dsh-wt_projects .dsh-wt_consoleEntry .dsh-wt_layoutIcon{font-size:15px}',
|
||||
'.dsh-wt_projects .dsh-wt_consoleEntry .dsh-wt_layoutName{font-weight:600;color:var(--dsw-alias-label-primary,#e6e8eb)}',
|
||||
// 控制室强制绑定弹窗:左加入现有 / 右新建对话
|
||||
'.dsh-wt_consoleBindCols{display:flex;gap:12px}',
|
||||
'.dsh-wt_consoleBindCol{flex:1;min-width:0;display:flex;flex-direction:column;gap:6px}',
|
||||
|
||||
@@ -1173,3 +1173,32 @@
|
||||
新建对话路径(分组默认无)✓;主题深/白/跟随切换 + data-wt-theme 落定 + 持久化 ✓;
|
||||
functional-diag 全 20 STEP(布局卡选择器收窄排除控制室卡)ERROR_COUNT: 0。
|
||||
- 备注:纯客户端改动,F5 即生效;AGENTS.md 已补控制室规则。
|
||||
|
||||
## 补记(控制室 UI 打磨:光效卡片 + 正方形布局 + 主题图标 + 冷会话预览预热 + 统一命名)
|
||||
|
||||
- 用户反馈:① 卡片太长信息太少 → 改正方形(1:1 圆角卡),信息放大(状态 17px 大字、
|
||||
子代理 12px 徽章、预览 12.5px 两行截断);② 状态可视化:工作 = 蓝色描边光晕顺时针
|
||||
绕卡旋转(conic-gradient + @property --consoleAngle 动画)、完成 = 背景绿光、待决 =
|
||||
背景黄光(radial 泛光 + 外发光),点发光卡片先确认熄光再进入;③ 命名统一「控制室」
|
||||
(侧栏区块标题 + 默认项目卡 + 面板标题);④ 入口卡差异化(渐变底 + 青蓝描边 + 微光,
|
||||
统筹主程序感);⑤ 主题三选一改图标按钮(🌙/☀️/🖥️,title/aria 保留文字);⑥ 预览
|
||||
方案 A:冷会话最近消息预热。
|
||||
- 冷读调研(probe-preview-cold/cold2/dbg.cjs):binding() 实例化冷会话 nodes 为空;
|
||||
loadOlder 无效;**face.history({maxMessages:N})(运行期内建方法)返回 {ok,value:
|
||||
{events,hasMore}} 原始事件流**——尾部扫 user/message(data.content)与
|
||||
assistant/message(data.message.content)可提取最近一条 text 块文本(实测拿
|
||||
到真实文本)✓。纯只读拉取,无副作用不激活 agent。
|
||||
- 实现:index.tsx 新增 previewCache/previewFetching/coldPreviewOf/sweepPreviews/
|
||||
schedulePreviewSweep;打开控制室即 sweep(openConsole + ConsolePane mount 双触发),
|
||||
控制室开着时会话快照变化防抖 6s 再扫;getConsoleCards 预览 = cache ?? lastTextOf。
|
||||
卡片 glow 字段 = done/need 且未 ack;env.console 增 onAck(ackProjectNotify +
|
||||
setNotifyTick + notifyConsole)与 refreshPreviews;ackRef 每渲染同步。ConsolePane:
|
||||
状态行大字号、正方形卡、三光效类接线、发光卡点击先 ack。样式:--wt-* 主题作用域内
|
||||
新增状态色/徽章/两行截断/radial 泛光/旋转描边(@property + mask 挖空描边环)/入口卡
|
||||
渐变描边/主题图标按钮。
|
||||
- 验证(probe-batch16 扩展):cardAspect=1、statusFont 17px、busy 旋转动画
|
||||
consoleAngleSpin + conic 描边 ✓、绿/黄泛光与外发光 rgba 值 ✓、入口卡渐变+青蓝描边 ✓、
|
||||
主题按钮 🌙☀️🖥️ ✓、冷会话预热 selfPreviewFilled(真实文本「新包已生效…」)✓;
|
||||
三阶段全流程与主题切换全 ✓;functional-diag 全 20 STEP ERROR_COUNT: 0。
|
||||
- 备注:纯客户端改动,F5 即生效;face.history 为非公开接口(只读、失败静默回退内存路径),
|
||||
AGENTS.md 已注明。
|
||||
|
||||
@@ -59,7 +59,7 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
await sleep(400);
|
||||
var rows = document.querySelectorAll('.dsh-wt_settings .dsh-wt_manageRow');
|
||||
out.manageRows = rows.length;
|
||||
out.consoleInManage = [].slice.call(rows).some(function(r){ return (r.textContent||'').indexOf('工作台') >= 0; });
|
||||
out.consoleInManage = [].slice.call(rows).some(function(r){ return (r.textContent||'').indexOf('控制室') >= 0; });
|
||||
var done = document.querySelector('.dsh-wt_manageDone');
|
||||
if (done) { done.dispatchEvent(new MouseEvent('click', {bubbles:true, cancelable:true})); }
|
||||
await sleep(300);
|
||||
@@ -75,7 +75,39 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
var labels = bpop.querySelectorAll('.dsh-wt_consoleBindLabel');
|
||||
out.leftLabel = labels[0] ? labels[0].textContent : null;
|
||||
out.rightLabel = labels[1] ? labels[1].textContent : null;
|
||||
var item = bpop.querySelector('.dsh-wt_consoleBindList .dsh-wt_selectItem');
|
||||
// 选一个「确实有历史文本」的会话(逐个冷拉 2 条消息验证;保证预热断言可测)
|
||||
var svc = window.__dshSessions;
|
||||
var snap = svc && svc.list ? svc.list.getSnapshot() : null;
|
||||
var byId = snap && snap.byId ? snap.byId : {};
|
||||
var ids = snap && snap.ids ? snap.ids : [];
|
||||
var chosen = null;
|
||||
for (var k = 0; k < Math.min(ids.length, 12) && !chosen; k++) {
|
||||
var sid = ids[k];
|
||||
try {
|
||||
var face = svc.binding(sid).session;
|
||||
var r1 = await face.history({ maxMessages: 2 });
|
||||
var evs = (r1 && r1.result && r1.result.value && r1.result.value.events) || [];
|
||||
for (var i = evs.length - 1; i >= 0 && !chosen; i--) {
|
||||
var ev = evs[i] && evs[i].event;
|
||||
var d = ev && ev.data ? ev.data : {};
|
||||
var blocks = null;
|
||||
if (ev && ev.type === 'user/message') blocks = d.content || d.blocks;
|
||||
else if (ev && ev.type === 'assistant/message') { var mm = d.message || d; blocks = mm.content || mm.blocks; }
|
||||
if (Array.isArray(blocks)) {
|
||||
for (var b = 0; b < blocks.length; b++) {
|
||||
if (blocks[b] && typeof blocks[b].text === 'string' && blocks[b].text.trim()) {
|
||||
var titleOf = byId[sid] && (byId[sid].title || byId[sid].displayTitle);
|
||||
chosen = { title: titleOf, sid: sid };
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
out.chosenTitle = chosen ? chosen.title : null;
|
||||
var item = chosen ? [].slice.call(bpop.querySelectorAll('.dsh-wt_consoleBindList .dsh-wt_selectItem')).find(function(it){ return String(it.textContent) === String(chosen.title); }) : null;
|
||||
if (!item) item = bpop.querySelector('.dsh-wt_consoleBindList .dsh-wt_selectItem');
|
||||
if (item) { item.dispatchEvent(new MouseEvent('click', {bubbles:true, cancelable:true})); }
|
||||
await sleep(900);
|
||||
}
|
||||
@@ -99,6 +131,40 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
out.firstCardHasPreview = !!(c0 && c0.querySelector('.dsh-wt_consolePreview'));
|
||||
out.anyLayoutCard = [].slice.call(cards).some(function(c){ return (c.textContent||'').indexOf('绑定测试') >= 0; });
|
||||
out.layoutCardHasJump = [].slice.call(cards).some(function(c){ return (c.textContent||'').indexOf('绑定测试') >= 0 && !!c.querySelector('.dsh-wt_consoleJump'); });
|
||||
// 新版 UI:正方形比例、大字号状态、光效 CSS 接线(注入类验证,不依赖真实会话状态)
|
||||
var cardEl = c0;
|
||||
if (cardEl) {
|
||||
var cr = cardEl.getBoundingClientRect();
|
||||
out.cardAspect = Math.round((cr.width / Math.max(1, cr.height)) * 100) / 100;
|
||||
var statusEl = cardEl.querySelector('.dsh-wt_consoleStatus');
|
||||
out.statusFont = statusEl ? getComputedStyle(statusEl).fontSize : null;
|
||||
cardEl.classList.add('dsh-wt_consoleCard-busy');
|
||||
var bs = getComputedStyle(cardEl, '::before');
|
||||
out.busyBgImage = bs.backgroundImage ? bs.backgroundImage.slice(0, 60) : null;
|
||||
out.busyAnim = bs.animationName;
|
||||
cardEl.classList.remove('dsh-wt_consoleCard-busy');
|
||||
cardEl.classList.add('dsh-wt_consoleCard-glowDone');
|
||||
await sleep(300);
|
||||
var gs = getComputedStyle(cardEl, '::after');
|
||||
out.glowDoneBg = gs.backgroundImage ? gs.backgroundImage.slice(0, 60) : null;
|
||||
out.glowDoneShadow = getComputedStyle(cardEl).boxShadow.slice(0, 60);
|
||||
cardEl.classList.remove('dsh-wt_consoleCard-glowDone');
|
||||
cardEl.classList.add('dsh-wt_consoleCard-glowNeed');
|
||||
await sleep(300);
|
||||
out.glowNeedShadow = getComputedStyle(cardEl).boxShadow.slice(0, 60);
|
||||
cardEl.classList.remove('dsh-wt_consoleCard-glowNeed');
|
||||
}
|
||||
var entry = vis('.dsh-wt_projects .dsh-wt_consoleEntry');
|
||||
if (entry) {
|
||||
var ecs = getComputedStyle(entry);
|
||||
out.entryBgGradient = ecs.backgroundImage.indexOf('linear-gradient') >= 0;
|
||||
out.entryBorder = ecs.borderColor;
|
||||
}
|
||||
// 冷会话预热:等 sweep 拉取后,自己卡片预览应有真实文本
|
||||
await sleep(4200);
|
||||
var selfPrev2 = c0 && c0.querySelector('.dsh-wt_consolePreview');
|
||||
out.selfPreviewAfterSweep = selfPrev2 ? selfPrev2.textContent.slice(0, 60) : null;
|
||||
out.selfPreviewFilled = !!(selfPrev2 && !selfPrev2.classList.contains('dsh-wt_consolePreviewNone'));
|
||||
var lcard = [].slice.call(cards).find(function(c){ return (c.textContent||'').indexOf('绑定测试') >= 0; });
|
||||
if (lcard) { lcard.dispatchEvent(new MouseEvent('click', {bubbles:true, cancelable:true})); }
|
||||
await sleep(800);
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
// preview-cold.cjs — 冷会话文本可读性实测:绑定→binding() 实例化后 nodes 是否有文本;无则试 loadOlder
|
||||
const http = require('http');
|
||||
const { spawn } = require('child_process');
|
||||
const WebSocket = require('ws');
|
||||
const PORT = 9375;
|
||||
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-pvcold',
|
||||
'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.__dshWorktable"); if (ready === true) break; }
|
||||
await sleep(1500);
|
||||
const out = await evaluate(`(async function(){
|
||||
var out = {};
|
||||
var sleep = function(ms){ return new Promise(function(r){ setTimeout(r, ms); }); };
|
||||
try {
|
||||
// 宿主会话列表快照:window.__dshWorktable 不暴露;改从 worktable 页面内部可见线索拿不到——
|
||||
// 改用宿主侧全局:dsh 的 session list store 常挂在 window 下的模块图里。
|
||||
// 兜底:直接探测 binding 是否存在(从控制室绑定一个已知真实会话 id 太难),
|
||||
// 因此这里改为:读取 window 上可能的会话入口。
|
||||
out.probeKeys = Object.keys(window).filter(function(k){ return /session|Session/.test(k); }).slice(0, 20);
|
||||
// 尝试从 dsh 全局取 session list(宿主暴露与否取决于实现)
|
||||
var anySid = null;
|
||||
try {
|
||||
var st = window.__dshWorktable;
|
||||
// 用控制室 env 的 getCards 前需要绑定;先拿会话列表:宿主 ctx.sessions 不在 window。
|
||||
// 通过 fetchSessionGroups 的网络接口 /api/worktable/workspaces 拿会话 id 不可行(无会话列表)。
|
||||
// 改用 localStorage 已存绑定(本机真实用户的浏览器里才有;这个 headless 是空档)→ 跳过。
|
||||
} catch (e) { out.err1 = String(e); }
|
||||
// 最终方案:给控制室项目绑定弹窗左列里有一个「当前」会话——用 DOM 拿它的 data 不可行,
|
||||
// 因此改为在页面内调用 __dshWorktable 的 bind 流程拿一个真实会话 id(走绑定弹窗 UI)。
|
||||
var vis = function(sel){ return [].slice.call(document.querySelectorAll(sel)).find(function(el){ return (getComputedStyle(el).visibility!=='hidden'&&el.getBoundingClientRect().height>0); }); };
|
||||
var st2 = window.__dshWorktable.splitStore;
|
||||
var card = vis('.dsh-wt_projects .dsh-wt_consoleEntry');
|
||||
card.dispatchEvent(new MouseEvent('click', {bubbles:true, cancelable:true}));
|
||||
await sleep(600);
|
||||
var bpop = vis('.dsh-wt_consoleBindPop');
|
||||
var items = bpop ? bpop.querySelectorAll('.dsh-wt_consoleBindList .dsh-wt_selectItem') : [];
|
||||
out.itemCount = items.length;
|
||||
var title = items[0] ? items[0].textContent : null;
|
||||
// 点击第一个会话 → 绑定并打开控制室
|
||||
if (items[0]) { items[0].dispatchEvent(new MouseEvent('click', {bubbles:true, cancelable:true})); }
|
||||
await sleep(1400);
|
||||
var j = JSON.parse(localStorage.getItem('dsh.worktable.projects.v1'));
|
||||
var sid = j.bindings['wt-console'];
|
||||
out.boundSid = sid;
|
||||
out.boundTitle = title;
|
||||
// 读会话面快照(binding() 实例化后的冷读结果)
|
||||
out.faceInfo = null;
|
||||
try {
|
||||
var faces = window.__dshWorktable;
|
||||
// 尝试 window 上的会话桥(插件内部 sessionBridge 不导出)
|
||||
out.bridgeOnWindow = typeof window.__dshSessionBridge;
|
||||
} catch (e) { out.err2 = String(e); }
|
||||
// 看控制室自己卡片的预览是否非空(lastTextOf 走 binding() 冷读路径)
|
||||
var grid = vis('.dsh-wt_consoleGrid');
|
||||
var selfCard = grid && [].slice.call(grid.querySelectorAll('.dsh-wt_consoleCard')).find(function(c){ return c.classList.contains('dsh-wt_consoleCardSelf'); });
|
||||
var prev = selfCard && selfCard.querySelector('.dsh-wt_consolePreview');
|
||||
out.selfPreviewText = prev ? prev.textContent : null;
|
||||
out.selfPreviewIsEmpty = prev ? (prev.classList.contains('dsh-wt_consolePreviewNone')) : null;
|
||||
await sleep(2500);
|
||||
out.selfPreviewAfterWait = prev ? prev.textContent : null;
|
||||
st2.close(); await sleep(300);
|
||||
} catch (e) { out.err = String(e && e.stack || e); }
|
||||
return out;
|
||||
})()`);
|
||||
console.log('RESULT ' + JSON.stringify(out));
|
||||
try { ws.close(); } catch {}
|
||||
proc.kill();
|
||||
})().catch((e) => { console.error('PROBE FAIL', e); try { proc.kill(); } catch {} });
|
||||
@@ -0,0 +1,146 @@
|
||||
// preview-cold2.cjs — 冷会话 binding() 面对象方法侦察
|
||||
const http = require('http');
|
||||
const { spawn } = require('child_process');
|
||||
const WebSocket = require('ws');
|
||||
const PORT = 9376;
|
||||
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-pvcold2',
|
||||
'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.__dshSessions"); if (ready === true) break; }
|
||||
await sleep(1200);
|
||||
const out = await evaluate(`(async function(){
|
||||
var out = {};
|
||||
var sleep = function(ms){ return new Promise(function(r){ setTimeout(r, ms); }); };
|
||||
try {
|
||||
var svc = window.__dshSessions;
|
||||
out.svcKeys = Object.getOwnPropertyNames(svc ? Object.getPrototypeOf(svc) : {}).slice(0, 40);
|
||||
var listSnap = svc.list && svc.list.getSnapshot ? svc.list.getSnapshot() : null;
|
||||
out.ids = listSnap && listSnap.ids ? listSnap.ids.slice(0, 6) : null;
|
||||
out.byIdKeys = listSnap && listSnap.byId ? Object.keys(listSnap.byId).slice(0, 3) : null;
|
||||
var sid = listSnap && listSnap.ids ? listSnap.ids[0] : null;
|
||||
out.sid = sid;
|
||||
if (sid) {
|
||||
var b = svc.binding(sid);
|
||||
out.hasBinding = !!b;
|
||||
var face = b && b.session;
|
||||
out.faceKeys = face ? Object.getOwnPropertyNames(Object.getPrototypeOf(face)).slice(0, 40) : null;
|
||||
var snap = face && face.getSnapshot ? face.getSnapshot() : null;
|
||||
out.nodesLen = snap && snap.nodes ? snap.nodes.length : null;
|
||||
out.hasLoadOlder = !!(face && typeof face.loadOlder === 'function');
|
||||
// 试 loadOlder
|
||||
if (face && typeof face.loadOlder === 'function') {
|
||||
try {
|
||||
await face.loadOlder();
|
||||
await sleep(1500);
|
||||
var snap2 = face.getSnapshot();
|
||||
out.nodesLenAfterLoadOlder = snap2 && snap2.nodes ? snap2.nodes.length : null;
|
||||
out.hasMore = snap2 ? snap2.hasMore : null;
|
||||
} catch (e) { out.loadOlderErr = String(e); }
|
||||
}
|
||||
// 试原型上的 history 方法(非公开接口,只读侦察)
|
||||
out.historyProbe = null;
|
||||
if (face && typeof face.history === 'function') {
|
||||
try {
|
||||
var r1 = await face.history({ maxMessages: 100 });
|
||||
var res = r1 && r1.result;
|
||||
out.historyProbe = { type: typeof r1, isArray: Array.isArray(r1), len: r1 && r1.length != null ? r1.length : null, keys: r1 && typeof r1 === 'object' && !Array.isArray(r1) ? Object.keys(r1).slice(0, 10) : null };
|
||||
out.historyResult = null;
|
||||
try {
|
||||
if (res && res.ok !== false) {
|
||||
var val = res.value != null ? res.value : res;
|
||||
var items = val.items != null ? val.items : (val.entries != null ? val.entries : (val.messages != null ? val.messages : (Array.isArray(val) ? val : null)));
|
||||
out.historyResult = {
|
||||
resKeys: Object.keys(res).slice(0, 12),
|
||||
valType: val === null ? 'null' : typeof val,
|
||||
valKeys: val && typeof val === 'object' && !Array.isArray(val) ? Object.keys(val).slice(0, 12) : null,
|
||||
valSample: JSON.stringify(val).slice(0, 600),
|
||||
itemsType: items ? (Array.isArray(items) ? 'array' : typeof items) : null,
|
||||
itemsLen: items && items.length != null ? items.length : null,
|
||||
first: items && items[0] ? JSON.stringify(items[0]).slice(0, 400) : null,
|
||||
last: items && items.length ? JSON.stringify(items[items.length - 1]).slice(0, 400) : null,
|
||||
};
|
||||
// 事件类型分布 + 尾部寻找成品消息文本
|
||||
try {
|
||||
var evs = val && Array.isArray(val.events) ? val.events : [];
|
||||
var types = {};
|
||||
evs.forEach(function(ev){ var tt = ev && ev.event && ev.event.type; types[tt] = (types[tt]||0)+1; });
|
||||
out.eventTypes = types;
|
||||
out.eventsLen = evs.length;
|
||||
var tailTexts = [];
|
||||
for (var i = evs.length - 1; i >= 0 && tailTexts.length < 4; i--) {
|
||||
var ev = evs[i] && evs[i].event;
|
||||
if (!ev) continue;
|
||||
var d = ev.data || {};
|
||||
if (ev.type === 'user/message' || ev.type === 'user') {
|
||||
var blocks = d.content || d.blocks || [];
|
||||
var s = '';
|
||||
if (Array.isArray(blocks)) blocks.forEach(function(b){ if (b && typeof b.text === 'string') s += b.text; });
|
||||
if (s) tailTexts.push({ type: ev.type, text: s.slice(0, 80) });
|
||||
} else if (ev.type === 'assistant/message' || ev.type === 'assistant') {
|
||||
var b2 = d.message || d;
|
||||
var bl2 = b2.content || b2.blocks || [];
|
||||
var s2 = '';
|
||||
if (Array.isArray(bl2)) bl2.forEach(function(b){ if (b && typeof b.text === 'string') s2 += b.text; });
|
||||
if (s2) tailTexts.push({ type: ev.type, text: s2.slice(0, 80) });
|
||||
}
|
||||
}
|
||||
out.tailTexts = tailTexts;
|
||||
// 成品消息事件样本
|
||||
var samples = [];
|
||||
for (var j = evs.length - 1; j >= 0 && samples.length < 2; j--) {
|
||||
var evj = evs[j] && evs[j].event;
|
||||
if (evj && (evj.type === 'user/message' || evj.type === 'assistant/message')) {
|
||||
samples.push(JSON.stringify(evj).slice(0, 500));
|
||||
}
|
||||
}
|
||||
out.msgSamples = samples;
|
||||
} catch (e4) { out.tailErr = String(e4); }
|
||||
} else {
|
||||
out.historyResult = { resKeys: Object.keys(res || {}).slice(0, 8), err: res && (res.error || res.code || JSON.stringify(res).slice(0, 120)) };
|
||||
}
|
||||
} catch (e3) { out.historyResultErr = String(e3); }
|
||||
} catch (e2) { out.historyErr = String(e2); }
|
||||
}
|
||||
// 找 history 类方法(原型链上)
|
||||
var protoNames = [];
|
||||
var p = Object.getPrototypeOf(face);
|
||||
var depth = 0;
|
||||
while (p && depth < 4) {
|
||||
protoNames = protoNames.concat(Object.getOwnPropertyNames(p));
|
||||
p = Object.getPrototypeOf(p); depth++;
|
||||
}
|
||||
out.historyLike = protoNames.filter(function(n){ return /histor|load|fetch|more|page/i.test(n); });
|
||||
}
|
||||
} catch (e) { out.err = String(e && e.stack || e); }
|
||||
return out;
|
||||
})()`);
|
||||
console.log('RESULT ' + JSON.stringify(out));
|
||||
try { ws.close(); } catch {}
|
||||
proc.kill();
|
||||
})().catch((e) => { console.error('PROBE FAIL', e); try { proc.kill(); } catch {} });
|
||||
@@ -0,0 +1,77 @@
|
||||
// preview-dbg.cjs — 对绑定会话直接拉 history,看消息事件形态
|
||||
const http = require('http');
|
||||
const { spawn } = require('child_process');
|
||||
const WebSocket = require('ws');
|
||||
const PORT = 9377;
|
||||
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-pvdbg',
|
||||
'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.__dshSessions"); if (ready === true) break; }
|
||||
await sleep(1200);
|
||||
const out = await evaluate(`(async function(){
|
||||
var out = {};
|
||||
try {
|
||||
var svc = window.__dshSessions;
|
||||
var snap = svc.list.getSnapshot();
|
||||
// 对所有会话逐个拉 2 条消息,统计哪些能提取到文本
|
||||
var sids = (snap.ids || []).slice(0, 8);
|
||||
var results = [];
|
||||
for (var k = 0; k < sids.length; k++) {
|
||||
var sid = sids[k];
|
||||
try {
|
||||
var face = svc.binding(sid).session;
|
||||
var r1 = await face.history({ maxMessages: 2 });
|
||||
var evs = (r1 && r1.result && r1.result.value && r1.result.value.events) || [];
|
||||
var userShape = null, asstShape = null, anyText = '';
|
||||
for (var i = evs.length - 1; i >= 0; i--) {
|
||||
var ev = evs[i] && evs[i].event;
|
||||
if (!ev) continue;
|
||||
var d = ev.data || {};
|
||||
if (ev.type === 'user/message' && !userShape) userShape = JSON.stringify(d).slice(0, 260);
|
||||
if (ev.type === 'assistant/message' && !asstShape) asstShape = JSON.stringify(d).slice(0, 260);
|
||||
var blocks = null;
|
||||
if (ev.type === 'user/message') blocks = d.content || d.blocks;
|
||||
else if (ev.type === 'assistant/message') { var m = d.message || d; blocks = m.content || m.blocks; }
|
||||
if (Array.isArray(blocks) && !anyText) {
|
||||
for (var b = 0; b < blocks.length; b++) {
|
||||
if (blocks[b] && typeof blocks[b].text === 'string' && blocks[b].text.trim()) { anyText = blocks[b].text.trim().slice(0, 50); break; }
|
||||
}
|
||||
}
|
||||
}
|
||||
results.push({ sid: sid.slice(-8), evsLen: evs.length, userShape: userShape, asstShape: asstShape, anyText: anyText });
|
||||
} catch (e) { results.push({ sid: sid.slice(-8), err: String(e).slice(0, 90) }); }
|
||||
}
|
||||
out.results = results;
|
||||
} catch (e) { out.err = String(e && e.stack || e); }
|
||||
return out;
|
||||
})()`);
|
||||
console.log('RESULT ' + JSON.stringify(out));
|
||||
try { ws.close(); } catch {}
|
||||
proc.kill();
|
||||
})().catch((e) => { console.error('PROBE FAIL', e); try { proc.kill(); } catch {} });
|
||||
@@ -90,10 +90,21 @@ node --check lib/index.js
|
||||
consoleListeners(项目/会话快照变化推送)+ 面板每秒 tick。
|
||||
- 卡片动作:点卡片 = 打开该项目(openSplit 或入驻项目切绑定对话);💬 = 跳绑定对话
|
||||
(markPluginSessionOpen 豁免联动,控制室保持打开);工作台自己的卡片点击无操作。
|
||||
- 主题:面板三选一开关(深色/白色/跟随系统,存 view.v1 consoleTheme);system 读宿主
|
||||
html 的 color-scheme(DSH 深色/白色/跟随系统设置都会反映到它)+ prefers-color-scheme
|
||||
兜底;落成 .dsh-wt_console[data-wt-theme=dark|light] 作用域变量 --wt-*(宿主不发布
|
||||
--dsw-alias-*,工作台全站一直靠回退色渲染——控制室自带主题作用域,不受其影响)。
|
||||
- 主题:面板三选一开关(图标按钮 🌙/☀️/🖥️,title/aria 保留文字;存 view.v1 consoleTheme);
|
||||
system 读宿主 html 的 color-scheme(DSH 深色/白色/跟随系统设置都会反映到它)+
|
||||
prefers-color-scheme 兜底;落成 .dsh-wt_console[data-wt-theme=dark|light] 作用域变量
|
||||
--wt-*(宿主不发布 --dsw-alias-*,工作台全站一直靠回退色渲染——控制室自带主题作用域,
|
||||
不受其影响)。
|
||||
- 状态光效:工作=蓝色描边光晕顺时针绕卡旋转(.dsh-wt_consoleCard-busy ::before
|
||||
conic-gradient + @property --consoleAngle + consoleAngleSpin);完成=绿光、待决=黄光
|
||||
(-glowDone/-glowNeed:radial 泛光 + 外发光;glow 字段 = done/need 且本轮未 ack,
|
||||
点卡片先 onAck 熄光再进入,与提醒 ack 生命周期一致)。
|
||||
- 命名统一「控制室」:侧栏区块标题、默认项目卡名、面板标题均为 console.name/title 系列
|
||||
locale(不再是「工作台」)。
|
||||
- 冷会话消息预览(方案 A):binding() 对冷会话不载入文本;预热走 face.history({maxMessages:2})
|
||||
(运行期内建方法、非公开接口,只读无副作用)尾部扫 user/message 与 assistant/message 的
|
||||
text 块 → previewCache;sweepPreviews 在打开控制室时 + 控制室开着且会话快照变化防抖 6s
|
||||
触发;失败静默回退内存路径 lastTextOf。拉取是带宽成本不是 Token 成本。
|
||||
|
||||
## 安装 / 重启
|
||||
|
||||
|
||||
Reference in New Issue
Block a user