feat: 文件预览(MD渲染/TXT/PDF/图片 + 标签去重) + 预设3改左二右一对话 + 服务端MIME补pdf/markdown

This commit is contained in:
Aisland
2026-08-17 22:21:13 +08:00
parent 45cde7a761
commit eaaef0945e
11 changed files with 276 additions and 11 deletions
+103
View File
@@ -10,6 +10,7 @@
"license": "MIT",
"devDependencies": {
"esbuild": "^0.25.0",
"markdown-it": "^15.0.0",
"xterm": "^5.3.0"
},
"peerDependencies": {
@@ -471,6 +472,36 @@
"node": ">=18"
}
},
"node_modules/argparse": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/argparse/-/argparse-3.0.0.tgz",
"integrity": "sha512-BOp5NMrHqKxmq/OLr+clzzrRxgOKSLkcjmkWuChp7Irqwn4s74WjOBPIgWfA/HMcBnVkZ5XEuf9uUqzlpfCQ6A==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/puzrin"
},
{
"type": "github",
"url": "https://github.com/sponsors/nodeca"
}
],
"license": "Python-2.0"
},
"node_modules/entities": {
"version": "8.0.0",
"resolved": "https://registry.npmmirror.com/entities/-/entities-8.0.0.tgz",
"integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=20.19.0"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/esbuild": {
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.25.12.tgz",
@@ -513,6 +544,78 @@
"@esbuild/win32-x64": "0.25.12"
}
},
"node_modules/linkify-it": {
"version": "6.1.0",
"resolved": "https://registry.npmmirror.com/linkify-it/-/linkify-it-6.1.0.tgz",
"integrity": "sha512-wJ/TwpSDTLepCrQoYWYIExIKg5Zchex2Nn5yk2mFnB+6PtdkHtyLx742md9csRjjOnGkKIS/RrbY7l8D6gT9Vw==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/puzrin"
},
{
"type": "github",
"url": "https://github.com/sponsors/markdown-it"
}
],
"license": "MIT",
"dependencies": {
"uc.micro": "^3.0.0"
}
},
"node_modules/markdown-it": {
"version": "15.0.0",
"resolved": "https://registry.npmmirror.com/markdown-it/-/markdown-it-15.0.0.tgz",
"integrity": "sha512-Lf8ajvVNdRpzSNB4VegxNy7gjs8gU35l4b4+ET49LrQC5PKYwLZ72u60LeJ9gv3qiaesuYjJWCyVeQmv/QWKQw==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/puzrin"
},
{
"type": "github",
"url": "https://github.com/sponsors/markdown-it"
}
],
"license": "MIT",
"dependencies": {
"argparse": "^3.0.0",
"entities": "^8.0.0",
"linkify-it": "^6.0.0",
"mdurl": "^2.1.0",
"punycode.js": "^2.3.1",
"uc.micro": "^3.0.0"
},
"bin": {
"markdown-it": "bin/markdown-it.mjs"
}
},
"node_modules/mdurl": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/mdurl/-/mdurl-2.1.0.tgz",
"integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==",
"dev": true,
"license": "MIT"
},
"node_modules/punycode.js": {
"version": "2.3.1",
"resolved": "https://registry.npmmirror.com/punycode.js/-/punycode.js-2.3.1.tgz",
"integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/uc.micro": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/uc.micro/-/uc.micro-3.0.0.tgz",
"integrity": "sha512-U3PppEkleoTnIfi8BozMx3yju3qc/L6SwqWo2Sw+54PX+PX0q9I+r1Um5HCmqD7n9VDX5/v3vQH/AjA6deDdtw==",
"dev": true,
"license": "MIT"
},
"node_modules/xterm": {
"version": "5.3.0",
"resolved": "https://registry.npmmirror.com/xterm/-/xterm-5.3.0.tgz",
+3 -2
View File
@@ -36,8 +36,8 @@
},
"peerDependencies": {
"@deepseek-ai/cordis": "*",
"@deepseek-ai/dsh-host-webserver": "*",
"@deepseek-ai/dsh-client-locale": "*"
"@deepseek-ai/dsh-client-locale": "*",
"@deepseek-ai/dsh-host-webserver": "*"
},
"peerDependenciesMeta": {
"@deepseek-ai/cordis": {
@@ -52,6 +52,7 @@
},
"devDependencies": {
"esbuild": "^0.25.0",
"markdown-it": "^15.0.0",
"xterm": "^5.3.0"
}
}
+4 -3
View File
@@ -72,7 +72,7 @@ const OVER_SIDE_PX = 80
const PRESET_DEFS = [
{ id: '2h', leftCount: 0, topCount: 0, contentCount: 1, chatFull: false },
{ id: '3h', leftCount: 0, topCount: 0, contentCount: 2, chatFull: false },
{ id: 'l2', leftCount: 1, topCount: 1, contentCount: 0, chatFull: false },
{ id: 'l2', leftCount: 0, topCount: 1, contentCount: 1, chatFull: true },
{ id: 't2', leftCount: 0, topCount: 1, contentCount: 1, chatFull: false },
{ id: 'grid', leftCount: 0, topCount: 2, contentCount: 1, chatFull: false },
{ id: 't3', leftCount: 0, topCount: 1, contentCount: 2, chatFull: true },
@@ -153,13 +153,14 @@ function presetThumb(defId: string) {
)
}
if (defId === 'l2') {
// 左二右一:左侧上下两个内容窗,右侧整列对话(💬)
return (
<span className="dsh-wt_thumb dsh-wt_thumbCols">
<span className="dsh-wt_thumbCol">{cell(false, 'a')}</span>
<span className="dsh-wt_thumbCol">
<span className="dsh-wt_thumbRow">{cell(false, 'a')}</span>
<span className="dsh-wt_thumbRow">{cell(false, 'b')}</span>
<span className="dsh-wt_thumbRow">{cell(true, 'c')}</span>
</span>
<span className="dsh-wt_thumbCol">{cell(true, 'c')}</span>
</span>
)
}
+6 -2
View File
@@ -61,7 +61,9 @@ export const zh = {
'pane.open': '打开',
'pane.wip': '开发中(后续版本接入)',
'pane.explorerNoCwd': '当前会话没有工作目录',
'pane.openLater': '文件打开将在后续版本支持',
'pane.openLater': '该类型暂不支持预览',
'file.loading': '加载中…',
'file.fail': '文件读取失败',
'pane.gitNotRepo': '当前目录不是 git 仓库',
'pane.gitClean': '工作区干净',
'pane.jobsEmpty': '当前会话暂无后台任务',
@@ -137,7 +139,9 @@ export const en = {
'pane.open': 'Open',
'pane.wip': 'Coming in a later version',
'pane.explorerNoCwd': 'No working directory for the current session',
'pane.openLater': 'Opening files comes in a later version',
'pane.openLater': 'Preview not supported for this type',
'file.loading': 'Loading…',
'file.fail': 'Failed to read file',
'pane.gitNotRepo': 'Not a git repository',
'pane.gitClean': 'Working tree clean',
'pane.jobsEmpty': 'No background jobs in the current session',
+90
View File
@@ -1,6 +1,7 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { Terminal } from 'xterm'
import 'xterm/css/xterm.css'
import MarkdownIt from 'markdown-it'
/**
* dsh-worktable 乐高式工作区 M1:通用分栏引擎(PRD §13)。
@@ -17,6 +18,7 @@ export type BuiltinType = 'browser' | 'explorer' | 'scm' | 'tasks' | 'terminal'
export type SplitContent =
| { kind: 'iframe'; url: string }
| { kind: 'builtin'; type: BuiltinType }
| { kind: 'file'; path: string }
/** 一个内容标签页 */
export type PaneTab = { id: string; title: string; content: SplitContent }
@@ -125,6 +127,7 @@ const BUILTIN_LABEL_KEYS: Record<BuiltinType, string> = {
function tabTitleOf(content: SplitContent): string {
if (content.kind === 'builtin') return T(BUILTIN_LABEL_KEYS[content.type])
if (content.kind === 'file') return basenameOf(content.path)
try {
const u = new URL(content.url)
return u.hostname || content.url
@@ -133,6 +136,20 @@ function tabTitleOf(content: SplitContent): string {
}
}
/** 取路径最后一段作为标签标题 */
function basenameOf(p: string): string {
const parts = String(p).replace(/[\\/]+$/, '').split(/[\\/]/)
return parts[parts.length - 1] || String(p)
}
/** 内容同一性(openTab 去重:同窗内同内容只保留一个标签,再次打开切过去) */
function sameContent(a: SplitContent, b: SplitContent): boolean {
if (a.kind === 'iframe' && b.kind === 'iframe') return a.url === b.url
if (a.kind === 'file' && b.kind === 'file') return a.path === b.path
if (a.kind === 'builtin' && b.kind === 'builtin') return a.type === b.type
return false
}
/** 分栏 UI 文案提供者(由工作台注入 locale t) */
let uiT: ((key: string) => string) | null = null
export function setSplitT(fn: ((key: string) => string) | null) {
@@ -546,6 +563,9 @@ export const splitStore: SplitState = {
if (!spec) return
const mutate = (pane: SplitPane): SplitPane => {
const tabs = [...(pane.tabs ?? [])]
// 去重:同内容已有标签 → 直接激活
const existing = tabs.findIndex((t) => sameContent(t.content, content))
if (existing >= 0) return { ...pane, content: null, tabs, active: existing }
const tab: PaneTab = { id: 't' + Date.now().toString(36), title: tabTitleOf(content), content }
tabs.push(tab)
return { ...pane, content: null, tabs, active: tabs.length - 1 }
@@ -910,6 +930,8 @@ function ExplorerPane(props: { row: PaneRow; index: number }) {
if (e.isDir) { toggle(e.path); return }
if (/\.html?$/i.test(e.name)) {
splitStore.openTab(props.row, props.index, { kind: 'iframe', url: '/api/worktable/file?path=' + encodeURIComponent(e.path) })
} else if (/\.(md|markdown|mdown|txt|log|pdf|png|jpe?g|gif|webp|svg|bmp|ico)$/i.test(e.name)) {
splitStore.openTab(props.row, props.index, { kind: 'file', path: e.path })
} else {
setError(T('pane.openLater'))
}
@@ -1070,12 +1092,80 @@ function TerminalPane() {
return <div ref={hostRef} className="dsh-wt_termHost" />
}
const mdRenderer = new MarkdownIt({ linkify: true })
const IMAGE_EXTS = /[.](png|jpe?g|gif|webp|svg|bmp|ico)$/i
const MD_EXTS = /[.](md|markdown|mdown)$/i
/** 本地文件预览:PDF 走原生 iframe、图片居中展示、MD 渲染预览、其余按纯文本 */
function FileViewer(props: { path: string }) {
const ext = (props.path.split('.').pop() || '').toLowerCase()
const fileUrl = '/api/worktable/file?path=' + encodeURIComponent(props.path)
if (ext === 'pdf') {
return <iframe className="dsh-wt_paneFrame" src={fileUrl} title={basenameOf(props.path)} />
}
if (IMAGE_EXTS.test('.' + ext)) {
return (
<div className="dsh-wt_imgView">
<img src={fileUrl} alt={basenameOf(props.path)} />
</div>
)
}
return <TextViewer path={props.path} fileUrl={fileUrl} isMd={MD_EXTS.test('.' + ext)} />
}
/** MD/TXT 文本预览(fetch 原文 → MD 渲染或 <pre> 等宽展示) */
function TextViewer(props: { path: string; fileUrl: string; isMd: boolean }) {
const [text, setText] = useState<string | null>(null)
const [error, setError] = useState('')
useEffect(() => {
let dead = false
setText(null)
setError('')
fetch(props.fileUrl)
.then((r) => {
if (!r.ok) throw new Error('HTTP ' + r.status)
return r.text()
})
.then((t) => { if (!dead) setText(t) })
.catch((e) => { if (!dead) setError(String(e)) })
return () => { dead = true }
}, [props.fileUrl])
if (error) {
return <div className="dsh-wt_paneWip"><span className="dsh-wt_paneWipText">{T('file.fail')}:{error}</span></div>
}
if (text == null) {
return <div className="dsh-wt_paneWip"><span className="dsh-wt_paneWipText">{T('file.loading')}</span></div>
}
if (props.isMd) {
return (
<div className="dsh-wt_fileView">
<div
className="dsh-wt_md"
dangerouslySetInnerHTML={{ __html: mdRenderer.render(text) }}
onClick={(e: any) => {
const a = e.target && e.target.closest ? (e.target.closest('a') as HTMLAnchorElement | null) : null
if (!a) return
e.preventDefault()
const href = a.getAttribute('href') || ''
if (/^(https?:|mailto:)/i.test(href)) window.open(href, '_blank', 'noopener')
}}
/>
</div>
)
}
return <div className="dsh-wt_fileView"><pre className="dsh-wt_txt">{text}</pre></div>
}
/** 单个标签页的内容渲染 */
function PaneTabBody(props: { tab: PaneTab; row: PaneRow; index: number }) {
const content = props.tab.content
if (content.kind === 'iframe') {
return <iframe className="dsh-wt_paneFrame" src={content.url} title={props.tab.title} />
}
if (content.kind === 'file') {
return <FileViewer path={content.path} />
}
if (content.type === 'browser') return <BrowserPane />
if (content.type === 'explorer') return <ExplorerPane row={props.row} index={props.index} />
if (content.type === 'scm') return <GitPane />
+24
View File
@@ -76,6 +76,30 @@ export const css = [
'.dsh-wt_splitClose{flex:none;display:flex;align-items:center;justify-content:center;width:20px;height:20px;padding:0;border:none;border-radius:4px;background:transparent;color:var(--dsw-alias-label-secondary,#9aa4b2);font-size:12px;cursor:pointer}',
'.dsh-wt_splitClose:hover{color:var(--dsw-alias-label-primary,#e6e8eb);background:var(--dsw-alias-fill-l1,rgba(255,255,255,.06))}',
'.dsh-wt_pane{display:flex;flex-direction:column;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l1,#262b36);border-radius:8px;background:var(--dsw-alias-bg-base,#0b0e14);overflow:hidden;transition:border-color .15s ease,box-shadow .15s ease,transform .15s ease}',
// 文件预览:文本/MD 滚动容器 + MD 排版 + 图片居中
'.dsh-wt_fileView{flex:1;min-height:0;overflow:auto;padding:12px 14px;background:var(--dsw-alias-bg-base,#0b0e14);color:var(--dsw-alias-label-primary,#e6e8eb);font-size:12.5px;line-height:1.7}',
'.dsh-wt_txt{margin:0;font-family:var(--dsw-font-mono,Consolas,Menlo,monospace);font-size:12px;line-height:1.65;white-space:pre-wrap;word-break:break-word}',
'.dsh-wt_md h1,.dsh-wt_md h2,.dsh-wt_md h3,.dsh-wt_md h4{margin:14px 0 8px;line-height:1.3;font-weight:600;color:var(--dsw-alias-label-primary,#e6e8eb)}',
'.dsh-wt_md h1{font-size:20px;border-bottom:1px solid var(--dsw-alias-border-l1,#262b36);padding-bottom:6px}',
'.dsh-wt_md h2{font-size:16.5px;border-bottom:1px solid var(--dsw-alias-border-l1,#262b36);padding-bottom:5px}',
'.dsh-wt_md h3{font-size:14.5px}',
'.dsh-wt_md h4{font-size:13px}',
'.dsh-wt_md p{margin:6px 0}',
'.dsh-wt_md a{color:var(--dsw-alias-state-accent-primary,#4f8ef7);text-decoration:none}',
'.dsh-wt_md a:hover{text-decoration:underline}',
'.dsh-wt_md code{font-family:var(--dsw-font-mono,Consolas,Menlo,monospace);font-size:11.5px;background:var(--dsw-alias-fill-l1,rgba(255,255,255,.06));padding:1px 5px;border-radius:4px}',
'.dsh-wt_md pre{background:var(--dsw-alias-fill-l1,rgba(255,255,255,.04));border:1px solid var(--dsw-alias-border-l1,#262b36);border-radius:8px;padding:10px 12px;overflow:auto;margin:8px 0}',
'.dsh-wt_md pre code{background:transparent;padding:0;font-size:12px}',
'.dsh-wt_md blockquote{margin:8px 0;padding:2px 12px;border-left:3px solid var(--dsw-alias-state-accent-primary,#4f8ef7);background:var(--dsw-alias-fill-l1,rgba(255,255,255,.03));color:var(--dsw-alias-label-secondary,#9aa4b2)}',
'.dsh-wt_md ul,.dsh-wt_md ol{margin:6px 0;padding-left:22px}',
'.dsh-wt_md li{margin:2px 0}',
'.dsh-wt_md table{border-collapse:collapse;margin:8px 0;font-size:12px}',
'.dsh-wt_md th,.dsh-wt_md td{border:1px solid var(--dsw-alias-border-l1,#262b36);padding:4px 10px;text-align:left}',
'.dsh-wt_md th{background:var(--dsw-alias-fill-l1,rgba(255,255,255,.05))}',
'.dsh-wt_md hr{border:none;border-top:1px solid var(--dsw-alias-border-l1,#262b36);margin:12px 0}',
'.dsh-wt_md img{max-width:100%}',
'.dsh-wt_imgView{flex:1;min-height:0;display:flex;align-items:center;justify-content:center;overflow:auto;padding:10px;background:var(--dsw-alias-bg-base,#0b0e14)}',
'.dsh-wt_imgView img{max-width:100%;max-height:100%;object-fit:contain}',
'.dsh-wt_pane[data-drop-hover=true]{border-color:var(--dsw-alias-state-accent-primary,#4f8ef7);box-shadow:0 0 0 2px rgba(79,142,247,.4),var(--dsw-shadow-lv2,0 8px 24px rgba(0,0,0,.4));transform:scale(1.012)}',
'.dsh-wt_paneBar{flex:none;display:flex;align-items:center;height:22px;padding:0 8px;border-bottom:1px solid var(--dsw-alias-border-l1,#262b36);background:var(--dsw-alias-fill-l1,rgba(255,255,255,.02))}',
'.dsh-wt_paneTitle{flex:1;min-width:0;font-size:11px;font-weight:600;color:var(--dsw-alias-label-secondary,#9aa4b2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
+2 -2
View File
@@ -208,8 +208,8 @@ export function apply(ctx: Context) {
const types: Record<string, string> = {
html: 'text/html; charset=utf-8', htm: 'text/html; charset=utf-8',
css: 'text/css; charset=utf-8', js: 'text/javascript; charset=utf-8', mjs: 'text/javascript; charset=utf-8',
json: 'application/json; charset=utf-8', md: 'text/plain; charset=utf-8', txt: 'text/plain; charset=utf-8',
svg: 'image/svg+xml', png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', webp: 'image/webp',
json: 'application/json; charset=utf-8', md: 'text/markdown; charset=utf-8', markdown: 'text/markdown; charset=utf-8', txt: 'text/plain; charset=utf-8', log: 'text/plain; charset=utf-8',
pdf: 'application/pdf', svg: 'image/svg+xml', png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', webp: 'image/webp', bmp: 'image/bmp', ico: 'image/x-icon',
}
res.writeHead(200, { 'content-type': types[ext] ?? 'application/octet-stream', 'cache-control': 'no-store' })
res.end(data)
+3 -2
View File
@@ -335,8 +335,9 @@ type SplitPane = {
视口内钳制;透明遮罩点击关闭),不再使用侧边栏内展开式下拉;面板内容仅「选择布局 + 填名称」
(快捷方式表单暂移除,存量快捷方式条目仍保留展示/删除);
- **拓扑预设六个**(2 行 × 3 列缩略图网格,聊天窗蓝色 💬 标注):
左右两栏 / 三栏横排 / 左一右二 / 上一下二 / 井字四栏 / 左品右聊(2026-08-16 定案);
左一右二 = 左列整高内容窗 + 右列「上内容 + 下聊天」(左列布局聊天固定右下,无 ⇄ 翻转);
左右两栏 / 三栏横排 / 左二右一 / 上一下二 / 井字四栏 / 左品右聊(2026-08-17 更新:
第三个预设由「左一右二」改为「左二右一」);
左二右一 = 左侧上下两个内容窗 + 右侧聊天通高整列(chatFullHeight 几何,聊天可 ⇄ 翻转贴左);
左品右聊 = 左侧品字形(上一个、下两个内容窗)+ 右侧聊天通高整列(chatFullHeight 几何,
聊天可 ⇄ 翻转贴左);
- 保存的布局以「布局条目」形式出现在项目区(布局 = 一种工作台项目),点击打开 tiling 工作区,
@@ -597,3 +597,21 @@
- 打点:git tag checkpoint-2026-08-17-pre-fileviewer(已推远端,b22cd6b)。
- 状态:工作台 UI/交互(卡片统一、图标、弹窗、删除确认、互斥、预设均衡、picker 自适应、
分隔线居中)全部完成并验证;下一个大块 = 文件预览(MD/TXT/PDF 等自定义开发)。
## 补记(文件预览 MD/TXT/PDF/图片 + 预设三改左二右一)
- 用户确认默认方案开工 + 第 3 个预设(左一右二)改为「左边两个 + 右边一个对话」。
- 文件预览实现:
① SplitContent 新增 kind:'file'(path);tabTitleOf 取 basename;openTab 去重
(同内容已有标签 → 直接激活,sameContent 判定);
② FileViewer:pdf → iframe 原生渲染(服务端 MIME application/pdf);图片 → 居中 img;
md/markdown → markdown-it(打进 client.js,linkify 开、内嵌 HTML 关闭,链接点击新标签外开);
其余 → 等宽 <pre> 纯文本;fetch 失败/加载中文案(file.fail/file.loading);
③ 资源管理器点击 .md/.markdown/.mdown/.txt/.log/.pdf/图片 开预览标签(.html 行为不变,
其他类型提示不支持);服务端 file 路由 MIME 补 pdf/markdown/log/bmp/ico;
④ 全套暗色 MD 排版样式(标题/代码块/引用/表格/列表/分割线)。
- 预设 l2 改造:PRESET_DEFS l2 → {top:1, content:1, chatFull:true}(左列上下两内容窗 + 右列通高聊天),
缩略图改为左 2 右 1;PRD §13 预设说明同步更新。
- 验证(functional-diag STEP11):openTab kind:file PRD.md → 标签名 PRD.md、.dsh-wt_md 渲染出
PRD 开头文本;同路径再开 tabsAfterDup=1(去重);package.json → .dsh-wt_txt 展示;
全 STEP 回归 ERROR_COUNT: 0。bundle-eval 补 atob/btoa 桩后 PASS(markdown-it 引入)。
- 备注:客户端改动 F5 生效;服务端 pdf MIME 需手动重启 dsh web 一次后 PDF 预览才生效。
+1
View File
@@ -27,6 +27,7 @@ const sandbox = { self: null,
addEventListener(){}, removeEventListener(){},
},
localStorage: { getItem(){ return null }, setItem(){}, removeItem(){} },
atob: (s) => Buffer.from(s, 'base64').toString('binary'), btoa: (s) => Buffer.from(s, 'binary').toString('base64'),
CustomEvent: class { constructor(t, o){ this.type = t; this.detail = o && o.detail } },
ResizeObserver: class { observe(){} disconnect(){} },
MutationObserver: class { observe(){} disconnect(){} },
+22
View File
@@ -250,6 +250,28 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
})()`);
console.log('STEP10:', JSON.stringify(step10));
// 文件预览:MD 渲染 / TXT 文本 / 标签去重
const step11 = await evaluate(`(async function(){
var out={};
var st=window.__dshWorktable.splitStore;
st.open({id:'t-file',title:'file',top:null,main:[{id:'f1',title:'f',min:200,content:null}],chatWidth:{default:320,min:240,max:600}});
await new Promise(function(r){setTimeout(r,400)});
st.openTab('main',0,{kind:'file',path:'E:\\\\AI_Workspace\\\\DeepseekHarness\\\\Projects\\\\dsh-worktable\\\\02_process\\\\PRD.md'});
await new Promise(function(r){setTimeout(r,1200)});
out.mdTitle=st.spec.main[0].tabs[0].title;
out.mdView=!!document.querySelector('.dsh-wt_md');
out.mdText=document.querySelector('.dsh-wt_md')?String(document.querySelector('.dsh-wt_md').textContent).slice(0,50):null;
st.openTab('main',0,{kind:'file',path:'E:\\\\AI_Workspace\\\\DeepseekHarness\\\\Projects\\\\dsh-worktable\\\\02_process\\\\PRD.md'});
out.tabsAfterDup=st.spec.main[0].tabs.length;
st.openTab('main',0,{kind:'file',path:'E:\\\\AI_Workspace\\\\DeepseekHarness\\\\Projects\\\\dsh-worktable\\\\01_content\\\\package.json'});
await new Promise(function(r){setTimeout(r,900)});
out.txtView=!!document.querySelector('.dsh-wt_txt');
out.tabsNow=st.spec.main[0].tabs.length;
st.close();
return JSON.stringify(out);
})()`);
console.log('STEP11:', JSON.stringify(step11));
const errors = events.filter((e) =>
e.method === 'Runtime.exceptionThrown' ||
(e.method === 'Log.entryAdded' && e.params.entry.level === 'error') ||