dsh-pictor 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/NOTICE.md ADDED
@@ -0,0 +1,18 @@
1
+ # NOTICE — 第三方内容声明
2
+
3
+ 本仓库包含以下源自第三方的内容,请在分发或再使用时留意相应许可:
4
+
5
+ ## references/paper/
6
+
7
+ - `2601.23265v1.md`:arXiv 预印本 2601.23265v1 的正文提取(仅文本,不含 PDF),
8
+ 作为研究背景资料随源码仓库分发。
9
+ - `paperbanana-prompts.md`:同一 arXiv 预印本附录 G、H 中提示词原文的忠实提取与中文翻译。
10
+
11
+ 两者均**不随发布包分发**(package.json 的 files 仅含 `references/domain`),
12
+ 只存在于源码仓库。再分发请以 arXiv 原文及其作者声明的许可为准。
13
+
14
+ ## 免责声明
15
+
16
+ 除上述文件外,本仓库其余内容(agents/ 蓝图、references/domain/ 模板与规范、
17
+ src/、fixtures/ 示例文档)均为作者原创。样式与布局模板的早期版本
18
+ 曾参考 PaperBanana(arXiv:2601.23265v1)的设计思路,但经重写已不包含其原文。
package/README.md CHANGED
@@ -20,6 +20,10 @@ Replace `<profile>` with the target profile name (e.g. `desktop`, `web`) and sta
20
20
 
21
21
  > Install the Pictor plugin from the npm package `dsh-pictor` into the current profile, restart if needed, and confirm when a Pictor button appears at the bottom left.
22
22
 
23
+ ## Supported dsh version
24
+
25
+ Pictor is built and tested against **DSH Desktop 0.7.2** (bundled Harness **0.1.2-alpha.1**). The DeepSeek Harness client API moves quickly — the workspace/session services it exposes keep changing — so this plugin intentionally tracks one harness generation. Other versions (older or newer) may work or may break without notice, and are outside our support scope. If something stops working after a DSH update, update dsh to the version above rather than debugging the mismatch.
26
+
23
27
  ## Usage
24
28
 
25
29
  1. Open the workbench via the **Pictor** footer button and click **New project**: upload a file (`md` / `txt` / `docx` / `pdf` / image) or paste content (from Word or the web; formatting is kept, stored as sanitised HTML).
@@ -47,10 +51,15 @@ Replace `<profile>` with the target profile name (e.g. `desktop`, `web`) and sta
47
51
  ```bash
48
52
  npm run build # esbuild build of host + bundled client
49
53
  npm run verify # L1 unit + L2 host integration (mock ctx, 15 items)
50
- npm run verify:integration # L3 transport smoke against a real dsh web
54
+ npm run verify:integration # L3 transport smoke against a real dsh web (hermetic temp home)
55
+ npm run test:pack # package integrity: tarball carries lib/, agents/, references/, assets/
56
+ npm run test:install # L5: README install path (dsh plugin add) → real dsh web boot
51
57
  npm run test:e2e # L4a browser e2e (fixture data, deterministic)
58
+ npm run hooks:install # one-time: enable the pre-push hook (git config core.hooksPath .githooks)
52
59
  ```
53
60
 
61
+ The pre-push hook runs `npm run verify` + `npm run test:install` before every push; all smoke layers are hermetic (temp `DSH_HOME`, never the live `~/.dsh`).
62
+
54
63
  Test-layer definitions and rationale: [docs/DESIGN.zh.md](docs/DESIGN.zh.md) §7.
55
64
 
56
65
  ### Layout
package/README.zh-cn.md CHANGED
@@ -20,6 +20,10 @@ dsh plugin --profile <profile> add dsh-pictor
20
20
 
21
21
  > 帮我把 Pictor 插件装上,从 npm 包 dsh-pictor 装到当前 profile,需要的话重启,装好告诉我左下角有没有 Pictor 入口。
22
22
 
23
+ ## 适配的 DSH 版本
24
+
25
+ Pictor 以 **DSH Desktop 0.7.2**(内置 Harness **0.1.2-alpha.1**)为适配与测试基准。DeepSeek Harness 的客户端 API(工作区/会话服务)演进很快,本月两个版本之间可能就面目全非,所以本插件只跟住一个 harness 世代。其他版本——无论更旧更新——可能能跑、也可能无预告地坏掉,都不在我们的支持范围内。DSH 升级后插件不工作,请先回到上面这个版本,而不是去排查版本差异。
26
+
23
27
  ## 使用
24
28
 
25
29
  1. 打开工作台,点「新建项目」:上传文件(md/txt/docx/pdf/图片)或粘贴内容(从 Word/网页粘贴,格式保留,落盘为消毒 HTML)。
@@ -47,10 +51,15 @@ dsh plugin --profile <profile> add dsh-pictor
47
51
  ```bash
48
52
  npm run build # esbuild 构建 host + 拼接 client
49
53
  npm run verify # L1 单元 + L2 host 集成(mock ctx,15 项)
50
- npm run verify:integration # L3 transport 冒烟(真实 dsh web)
54
+ npm run verify:integration # L3 transport 冒烟(真实 dsh web,封闭临时 DSH_HOME)
55
+ npm run test:pack # 打包完整性:tarball 必带 lib/、agents/、references/、assets/
56
+ npm run test:install # L5:README 安装路径(dsh plugin add)→ 真实 dsh web 启动
51
57
  npm run test:e2e # L4a 浏览器 e2e(fixture 数据,确定性)
58
+ npm run hooks:install # 一次性启用 pre-push 钩子(git config core.hooksPath .githooks)
52
59
  ```
53
60
 
61
+ pre-push 钩子在每次 push 前依次跑 `npm run verify` 与 `npm run test:install`;所有冒烟层都是封闭式(临时 `DSH_HOME`,不碰真实 `~/.dsh`)。
62
+
54
63
  测试分层的定义与理由见 [docs/DESIGN.zh.md](docs/DESIGN.zh.md) 第 7 节。
55
64
 
56
65
  ## 目录
package/lib/client.js CHANGED
@@ -53,11 +53,11 @@ const STYLE = `
53
53
  background: var(--dsw-alias-bg-layer-1, #fbfbfa); display: flex; flex-direction: column; overflow-y: auto; }
54
54
  .pt-nav-head { padding: 18px 16px 12px; display: flex; align-items: center; justify-content: space-between; gap: 8px; }
55
55
  .pt-nav-titles { flex: 1; min-width: 0; }
56
- .pt-nav-title { font-size: 18px; font-weight: 700; letter-spacing: -0.01em; margin: 0; }
56
+ .pt-nav-title { font-size: 15px; font-weight: 650; letter-spacing: -0.15px; margin: 0; }
57
57
  .pt-nav-sub { font-size: 13px; color: var(--dsw-alias-label-caption, #8a8a8a); margin: 2px 0 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
58
- .pt-nav-new { border: 0; background: var(--dsw-alias-state-business-primary, #4f7cff); color: #fff;
59
- border-radius: 10px; padding: 7px 12px; font-size: 13px; font-weight: 600; cursor: pointer; white-space: nowrap; flex: none; }
60
- .pt-nav-new:hover { filter: brightness(.95); }
58
+ .pt-root .pt-nav-new { border: 1px solid transparent; background: var(--dsw-alias-button-primary-fill, #4f7cff); color: var(--dsw-alias-label-primary-foreground, #fff);
59
+ border-radius: 8px; padding: 5px 12px; font-size: 13.5px; font-weight: 550; cursor: pointer; white-space: nowrap; flex: none; box-shadow: 0 1px 2px rgba(0,0,0,0.12); transition: background 140ms ease; }
60
+ .pt-root .pt-nav-new:hover { background: var(--dsw-alias-button-primary-hover, #3a6ae0); }
61
61
  .pt-nav-list { flex: 1; overflow-y: auto; padding: 4px 8px 12px; min-height: 0; }
62
62
  .pt-nav-item { display: flex; align-items: center; gap: 10px; width: 100%; text-align: left;
63
63
  padding: 9px 10px; border-radius: 10px; border: 0; background: transparent; cursor: pointer;
@@ -78,13 +78,6 @@ const STYLE = `
78
78
  .pt-nav-settings:hover { border-color: var(--dsw-alias-border-l1, #b0b0b0); filter: brightness(.98); }
79
79
  .pt-nav-settings .glyph { color: var(--dsw-alias-state-business-primary, #4f7cff); font-size: 16px; }
80
80
  .pt-nav-settings:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(0,0,0,.05)); }
81
- .pt-lang { display: flex; align-items: center; gap: 8px; padding: 8px 10px 0; font-size: 13px;
82
- color: var(--dsw-alias-label-caption, #888); }
83
- .pt-lang-opts { display: inline-flex; border: 1px solid var(--dsw-alias-border-l2, #d8d8d8);
84
- border-radius: 999px; overflow: hidden; }
85
- .pt-lang-opt { border: 0; background: transparent; padding: 2px 10px; font-size: 12.5px; cursor: pointer;
86
- color: var(--dsw-alias-label-secondary, #777); }
87
- .pt-lang-opt.on { background: var(--dsw-alias-state-business-primary, #4f7cff); color: #fff; font-weight: 600; }
88
81
 
89
82
  /* ---- 主区 ---- */
90
83
  .pt-main { flex: 1; min-width: 0; overflow-y: auto; padding: 22px 28px 40px; }
@@ -92,12 +85,16 @@ const STYLE = `
92
85
  .pt-title { font-size: 28px; font-weight: 700; letter-spacing: -0.01em; margin: 0 0 4px; }
93
86
  .pt-subtitle { margin: 0 0 24px; font-size: 15px; color: var(--dsw-alias-label-secondary, #666); }
94
87
  .pt-btn { border: 1px solid var(--dsw-alias-border-l2, #d8d8d8); background: var(--dsw-alias-bg-layer-2, #fff);
95
- color: var(--dsw-alias-label-primary, #1a1a1a); border-radius: 12px; padding: 9px 18px; font-size: 15px;
96
- font-weight: 550; cursor: pointer; transition: filter 150ms, background 150ms; white-space: nowrap; }
97
- .pt-btn:hover { filter: brightness(0.96); }
98
- .pt-btn:disabled { opacity: 0.45; cursor: not-allowed; }
99
- .pt-btn-primary { background: var(--dsw-alias-accent, #4f7cff); border-color: var(--dsw-alias-accent, #4f7cff); color: #fff; }
100
- .pt-btn-ghost { background: transparent; border-color: transparent; color: var(--dsw-alias-label-secondary, #666); }
88
+ color: var(--dsw-alias-label-primary, #1a1a1a); border-radius: 8px; padding: 7px 14px; font-size: 14px;
89
+ font-weight: 500; cursor: pointer; transition: background 140ms ease, border-color 140ms ease, color 140ms ease, box-shadow 140ms ease; user-select: none; white-space: nowrap; }
90
+ .pt-btn:hover:not(:disabled) { background: var(--dsw-alias-bg-layer-3, var(--dsw-alias-interactive-bg-hover, rgba(0,0,0,0.05))); border-color: var(--dsw-alias-border-l3, #ccc); }
91
+ .pt-btn:active { transform: translateY(0.5px); }
92
+ .pt-btn:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary, #4f7cff); outline-offset: 2px; }
93
+ .pt-btn:disabled { opacity: 0.45; cursor: not-allowed; box-shadow: none; }
94
+ .pt-btn-primary { background: var(--dsw-alias-button-primary-fill, #4f7cff); border-color: transparent; color: var(--dsw-alias-label-primary-foreground, #fff); font-weight: 550; box-shadow: 0 1px 2px rgba(0,0,0,0.12); }
95
+ .pt-btn-primary:hover:not(:disabled) { background: var(--dsw-alias-button-primary-hover, #3a6ae0); }
96
+ .pt-btn-ghost { background: transparent; border-color: transparent; color: var(--dsw-alias-label-dimmed, #777); }
97
+ .pt-btn-ghost:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover, rgba(0,0,0,0.05)); color: var(--dsw-alias-label-primary, #1a1a1a); border-color: transparent; }
101
98
  .pt-card { background: var(--dsw-alias-bg-layer-1, #fff); border: 1px solid var(--dsw-alias-border-l2, #e2e2e2);
102
99
  border-radius: 16px; padding: 18px 20px; }
103
100
  .pt-card-row { display: flex; align-items: center; gap: 14px; justify-content: space-between; }
@@ -120,10 +117,12 @@ const STYLE = `
120
117
  .pt-badge-gated { color: #2563eb; }
121
118
  .pt-badge-done { color: #15803d; }
122
119
  .pt-badge-idle { color: var(--dsw-alias-label-secondary, #777); }
123
- .pt-input { width: 100%; border: 1px solid var(--dsw-alias-border-l2, #d8d8d8); border-radius: 12px;
120
+ .pt-input { width: 100%; border: 1px solid var(--dsw-alias-border-l2, #d8d8d8); border-radius: 8px;
124
121
  background: var(--dsw-alias-bg-layer-2, #fff); color: var(--dsw-alias-label-primary, #1a1a1a);
125
- padding: 10px 14px; font-size: 15px; outline: none; }
126
- .pt-input:focus, .pt-rich:focus { border-color: var(--dsw-alias-accent, #4f7cff); }
122
+ padding: 8px 12px; font-size: 14px; outline: none; transition: border-color 140ms ease, box-shadow 140ms ease; }
123
+ .pt-input::placeholder { color: var(--dsw-alias-label-caption, #888); }
124
+ .pt-input:hover { border-color: var(--dsw-alias-border-l3, #c5c5c5); }
125
+ .pt-input:focus, .pt-rich:focus { border-color: var(--dsw-alias-brand-primary, #4f7cff); box-shadow: 0 0 0 3px color-mix(in srgb, var(--dsw-alias-brand-primary, #4f7cff) 18%, transparent); }
127
126
  .pt-textarea { min-height: 90px; resize: vertical; }
128
127
  .pt-rich { min-height: 180px; max-height: 320px; overflow-y: auto; padding: 12px 14px; }
129
128
  .pt-rich:empty::before { content: attr(data-placeholder); color: var(--dsw-alias-label-caption, #999); }
@@ -206,7 +205,7 @@ const STYLE = `
206
205
  .pt-tab.active { background: var(--dsw-alias-bg-layer-2, #fff); color: var(--dsw-alias-label-primary, #1a1a1a);
207
206
  border-color: var(--dsw-alias-accent, #4f7cff); }
208
207
  .pt-field { margin-bottom: 14px; }
209
- .pt-field-label { display: block; font-size: 13.5px; font-weight: 550; color: var(--dsw-alias-label-secondary, #666);
208
+ .pt-field-label { display: block; font-size: 13.5px; font-weight: 500; color: var(--dsw-alias-label-primary, #1a1a1a);
210
209
  margin-bottom: 6px; }
211
210
  .pt-modal-backdrop { position: fixed; inset: 0; z-index: 60; background: rgba(0,0,0,.38);
212
211
  display: flex; align-items: center; justify-content: center; padding: 24px; }
@@ -368,13 +367,22 @@ const I18N = {
368
367
  send: 'Send', langLabel: 'Language', loading: 'Loading…',
369
368
  },
370
369
  }
371
- let savedLang = 'zh'
372
- try { savedLang = typeof localStorage !== 'undefined' ? localStorage.getItem('pictor-lang') : null } catch { /* 沙箱可能没有 storage */ }
370
+ function readBandungLang() {
371
+ try {
372
+ if (typeof window !== 'undefined' && window.__dshAppDock__ && window.__dshAppDock__.lang) return window.__dshAppDock__.lang.get()
373
+ return typeof localStorage !== 'undefined' ? (localStorage.getItem('bandung-lang') === 'en' ? 'en' : 'zh') : 'zh'
374
+ } catch { return 'zh' }
375
+ }
373
376
  const langStore = {
374
- val: savedLang === 'en' ? 'en' : 'zh',
377
+ val: readBandungLang(),
375
378
  subs: new Set(),
376
379
  emit() { for (const f of this.subs) f() },
377
- set(v) { this.val = v === 'en' ? 'en' : 'zh'; try { localStorage.setItem('pictor-lang', this.val) } catch { /* ignore */ } this.emit() },
380
+ set(v) {
381
+ this.val = v === 'en' ? 'en' : 'zh'
382
+ try { localStorage.setItem('bandung-lang', this.val) } catch { /* ignore */ }
383
+ if (typeof window !== 'undefined' && window.__dshAppDock__ && window.__dshAppDock__.lang) window.__dshAppDock__.lang.set(this.val)
384
+ this.emit()
385
+ },
378
386
  subscribe(f) { this.subs.add(f); return () => { this.subs.delete(f) } },
379
387
  }
380
388
  function useLang() {
@@ -387,16 +395,6 @@ function t(key, vars) {
387
395
  if (vars) { for (const k of Object.keys(vars)) text = text.replace('{' + k + '}', String(vars[k])) }
388
396
  return text
389
397
  }
390
- function LangSwitch() {
391
- const lang = useLang()
392
- const opt = (code, label) => h('button', {
393
- className: 'pt-lang-opt' + (lang === code ? ' on' : ''),
394
- onClick: () => langStore.set(code),
395
- }, label)
396
- return h('div', { className: 'pt-lang' },
397
- h('span', { className: 'pt-lang-label' }, t('langLabel')),
398
- h('div', { className: 'pt-lang-opts' }, opt('zh', '中文'), opt('en', 'English')))
399
- }
400
398
 
401
399
  // ---------- 工具 ----------
402
400
 
@@ -487,10 +485,23 @@ function sanitizePaste(html) {
487
485
  // 会话创建与驱动走 dsh 的 workspace 流程(照 pomasa 的 driveSession):
488
486
  // 宿主只出 prompt,客户端经 workspaces.connectWorkspace 建会话(自然归入
489
487
  // 「Pictor」工作区、继承 profile 默认模型),再经 sessions.prompt 驱动。
488
+ // dsh ≥ 0.1.2-alpha.1(DSH Desktop 0.7.2)把 connectWorkspace 从
489
+ // `workspaces` 服务移到了 `uiWorkspace` 服务上:这里两个形态都尝试,哪个
490
+ // 在就调哪个,老版本前端不受影响。
491
+ function sessionConnectFn(ctx, ws) {
492
+ let uiWs = null
493
+ try { uiWs = ctx.uiWorkspace || ((ctx.get && ctx.get('uiWorkspace')) || null) } catch { /* ignore */ }
494
+ return (uiWs && typeof uiWs.connectWorkspace === 'function')
495
+ ? uiWs.connectWorkspace.bind(uiWs)
496
+ : (ws && typeof ws.connectWorkspace === 'function') ? ws.connectWorkspace.bind(ws) : null
497
+ }
498
+
490
499
  async function pictorWorkspace(ctx) {
491
500
  const ws = ctx.workspaces || (ctx.get && ctx.get('workspaces'))
492
501
  const base = await rpc(ctx, 'config.get').then((c) => (c && c.dataRoot) || '')
493
- if (!ws || typeof ws.connectWorkspace !== 'function') throw new Error('会话服务不可用(workspaces)')
502
+ // 这里只用 list/create/delete/rename,新旧 services 都有;connectWorkspace
503
+ // 的可用性由 driveProjectSession 检查。
504
+ if (!ws) throw new Error('会话服务不可用(workspaces)')
494
505
  const items = () => {
495
506
  try {
496
507
  const snap = ws.list && typeof ws.list.getSnapshot === 'function' ? ws.list.getSnapshot() : null
@@ -531,14 +542,29 @@ async function pictorWorkspace(ctx) {
531
542
  async function driveProjectSession(ctx, prompt) {
532
543
  const ws = ctx.workspaces || (ctx.get && ctx.get('workspaces'))
533
544
  const sessionsSvc = ctx.sessions || (ctx.get && ctx.get('sessions'))
545
+ const connect = sessionConnectFn(ctx, ws)
546
+ const canCreate = !!(sessionsSvc && typeof sessionsSvc.create === 'function')
547
+ if (!(connect || canCreate) || !(sessionsSvc && typeof sessionsSvc.binding === 'function')) {
548
+ let uiWs = null
549
+ try { uiWs = ctx.uiWorkspace || ((ctx.get && ctx.get('uiWorkspace')) || null) } catch { /* ignore */ }
550
+ const diag = { ws: !!ws, ses: !!sessionsSvc && typeof sessionsSvc.binding === 'function', uiws: !!uiWs, conn: !!connect, create: canCreate }
551
+ console.warn('[pictor] session services unavailable', diag)
552
+ try { window.__pictor_diag = diag } catch { /* ignore */ }
553
+ try { pictorDiag(ctx, 'drive:fail') } catch { /* ignore */ }
554
+ throw new Error(`会话服务不可用(workspaces) [ws:${diag.ws},ses:${diag.ses},uiws:${diag.uiws},conn:${diag.conn},create:${diag.create}]`)
555
+ }
534
556
  const wid = await pictorWorkspace(ctx)
535
557
  let sessionId
536
- try { sessionId = await ws.connectWorkspace(wid) }
558
+ try {
559
+ const created = connect ? await connect(wid) : await sessionsSvc.create({ workspaceId: wid })
560
+ sessionId = created && typeof created === 'object' && created.id ? created.id : created
561
+ }
537
562
  catch (e) { throw new Error('创建会话失败:' + String((e && e.message) || e)) }
538
563
  const bound = sessionsSvc && typeof sessionsSvc.binding === 'function' ? sessionsSvc.binding(sessionId) : null
539
564
  const sess = bound && bound.session
540
565
  if (!sess || typeof sess.prompt !== 'function') throw new Error('会话无 prompt 通道')
541
566
  await sess.prompt([{ type: 'text', text: String(prompt || '') }], 'queue')
567
+ try { pictorDiag(ctx, 'drive:ok') } catch { /* ignore */ }
542
568
  return sessionId
543
569
  }
544
570
 
@@ -945,7 +971,17 @@ function fileSlug(s) {
945
971
  return String(s || '').toLowerCase().trim().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40) || 'infographic'
946
972
  }
947
973
 
948
- function ResultsPanel({ outputs, images, renderErrors, proposals, lay, renderBatches }) {
974
+ // 结果图直出走同源 HTTP(data URL 在新版 DSH Desktop WebView 可能被拦),
975
+ // 失败时回退到 RPC 返回的 data URL。
976
+ const imgUrlFor = (projectId, name) => {
977
+ const base = '/pictor/asset/project/' + encodeURIComponent(projectId) + '/' + encodeURIComponent(name)
978
+ try {
979
+ const tok = new URLSearchParams(window.location.search || '').get('token')
980
+ return tok ? base + '?token=' + encodeURIComponent(tok) : base
981
+ } catch { return base }
982
+ }
983
+
984
+ function ResultsPanel({ projectId, outputs, images, renderErrors, proposals, lay, renderBatches }) {
949
985
  const [big, setBig] = React.useState(null)
950
986
  const [collapsed, setCollapsed] = React.useState({})
951
987
  if (!outputs.length && !renderErrors.length) {
@@ -967,7 +1003,7 @@ function ResultsPanel({ outputs, images, renderErrors, proposals, lay, renderBat
967
1003
  h('div', null, h('h3', { className: 'pt-option-title', style: { margin: 0 } }, bigInfo.title)),
968
1004
  h('button', { className: 'pt-modal-close', onClick: () => setBig(null), title: t('close'), 'aria-label': t('close') }, '×')),
969
1005
  h('div', { className: 'pt-modal-body', style: { textAlign: 'center' } },
970
- h('img', { src: images[big], alt: bigInfo.title, style: { maxWidth: '100%', maxHeight: '70vh', borderRadius: 10 } }),
1006
+ h('img', { src: imgUrlFor(projectId, big), alt: bigInfo.title, style: { maxWidth: '100%', maxHeight: '70vh', borderRadius: 10 }, onError: (e) => { if (e && e.currentTarget) e.currentTarget.src = images[big] } }),
971
1007
  bigInfo.actualLayout ? h('p', { className: 'pt-meta', style: { marginTop: 10 } }, t('layoutLabel') + ': ' + layoutLabel(bigInfo.actualLayout)) : null)))
972
1008
  : null
973
1009
 
@@ -991,7 +1027,7 @@ function ResultsPanel({ outputs, images, renderErrors, proposals, lay, renderBat
991
1027
  const dlName = info.prop ? (fileSlug(info.prop.title || layoutLabel(info.actualLayout || info.prop.suggested_layout)) + '-' + info.pid + '.png') : name
992
1028
  return h('figure', { key: name, className: 'pt-img-card' },
993
1029
  images[name]
994
- ? h('img', { className: 'pt-img', src: images[name], alt: info.title, style: { cursor: 'zoom-in' }, onClick: () => setBig(name) })
1030
+ ? h('img', { className: 'pt-img', src: imgUrlFor(projectId, name), alt: info.title, style: { cursor: 'zoom-in' }, onClick: () => setBig(name), onError: (e) => { if (e && e.currentTarget) e.currentTarget.src = images[name] } })
995
1031
  : h('div', { className: 'thumb', style: { width: '100%', height: 140 } }),
996
1032
  h('figcaption', null,
997
1033
  h('div', { className: 'pt-fig-title' }, info.title),
@@ -1405,7 +1441,7 @@ function DetailPane(props) {
1405
1441
  ratios, setRatio, onRender: renderSelected, renderBusy,
1406
1442
  hasImageKey: Boolean(imageCfg && imageCfg.hasKey), running: Boolean(data.running),
1407
1443
  renderState: data.renderState, renderRunning: Boolean(data.renderRunning) }),
1408
- render: h(ResultsPanel, { outputs: data.outputs, images, renderErrors, proposals, lay: layOv, renderBatches: data.renderBatches }),
1444
+ render: h(ResultsPanel, { projectId, outputs: data.outputs, images, renderErrors, proposals, lay: layOv, renderBatches: data.renderBatches }),
1409
1445
  }[step]
1410
1446
 
1411
1447
  return h('div', null,
@@ -1628,7 +1664,7 @@ function Workbench(props) {
1628
1664
  h('button', { className: 'pt-nav-settings', onClick: () => setView('settings') },
1629
1665
  h('span', { className: 'glyph' }, '⚙'),
1630
1666
  t('navSettings')),
1631
- h(LangSwitch, null)))
1667
+ ))
1632
1668
 
1633
1669
  return h('div', { className: 'pt-root pt-workbench' },
1634
1670
  nav,
@@ -1670,7 +1706,30 @@ function selectedLabel(ids) {
1670
1706
  return ids.join('、')
1671
1707
  }
1672
1708
 
1709
+ // 最佳努力诊断:上报此 ctx 上工作区/会话服务的暴露情况(DSH Desktop 0.7.2
1710
+ // 调整了客户端 API),host 落盘 ~/.pictor/diag.jsonl,无需 DevTools 即可排查。
1711
+ function pictorDiag(ctx, phase) {
1712
+ let ws = null, ses = null, ui = null
1713
+ try { ws = ctx && (ctx.workspaces || (ctx.get && ctx.get('workspaces'))) } catch { /* ignore */ }
1714
+ try { ses = ctx && (ctx.sessions || (ctx.get && ctx.get('sessions'))) } catch { /* ignore */ }
1715
+ try { ui = ctx && (ctx.uiWorkspace || (ctx.get && ctx.get('uiWorkspace'))) } catch { /* ignore */ }
1716
+ const body = {
1717
+ phase,
1718
+ ws: !!ws, ses: !!ses, ui: !!ui,
1719
+ wsMethods: ws ? Object.keys(ws).slice(0, 40) : null,
1720
+ sesMethods: ses ? Object.keys(ses).slice(0, 40) : null,
1721
+ uiMethods: ui ? Object.keys(ui).slice(0, 40) : null,
1722
+ connectWs: !!(ws && typeof ws.connectWorkspace === 'function'),
1723
+ connectUi: !!(ui && typeof ui.connectWorkspace === 'function'),
1724
+ bind: !!(ses && typeof ses.binding === 'function'),
1725
+ href: typeof location !== 'undefined' ? location.href.slice(0, 80) : '',
1726
+ }
1727
+ try { fetch('/pictor/diag', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) }).catch(() => {}) } catch { /* ignore */ }
1728
+ console.warn('[pictor] diag', body)
1729
+ }
1730
+
1673
1731
  function apply(ctx) {
1732
+ try { pictorDiag(ctx, 'apply') } catch { /* ignore */ }
1674
1733
  console.info('[dsh-pictor] apply 被调用', { hasSlots: Boolean(ctx && ctx.slots) })
1675
1734
  if (typeof document !== 'undefined') {
1676
1735
  const styleId = 'dsh-pictor-style'
@@ -1690,12 +1749,12 @@ function apply(ctx) {
1690
1749
  panel.close = () => { if (panel.open) { panel.open = false; panel.emit() } }
1691
1750
  panel.subscribe = (fn) => { panel.subs.add(fn); return () => { panel.subs.delete(fn) } }
1692
1751
  // 点选 dsh 左侧会话(或面板外任意处)自动收起工作台:
1693
- // 只排除面板本身与 Pictor 启动按钮;侧栏会话此刻在面板左侧始终可点。
1752
+ // 只排除面板本身;侧栏会话此刻在面板左侧始终可点(入口已交给应用坞)。
1694
1753
  if (typeof document !== 'undefined') {
1695
1754
  document.addEventListener('mousedown', (e) => {
1696
1755
  if (!panel.open) return
1697
1756
  const t = e.target
1698
- if (t && typeof t.closest === 'function' && (t.closest('.pt-shell-panel') || t.closest('.pt-footer-action'))) return
1757
+ if (t && typeof t.closest === 'function' && t.closest('.pt-shell-panel')) return
1699
1758
  panel.close()
1700
1759
  })
1701
1760
  }
@@ -1737,24 +1796,22 @@ function apply(ctx) {
1737
1796
  h(Workbench, { ctx, key: 'shell' })))
1738
1797
  }
1739
1798
 
1740
- function FooterAction() {
1741
- useLang() // footer 文案随语言刷新
1742
- const open = usePanelOpen()
1743
- return h('div', {
1744
- className: 'pt-footer-action' + (open ? ' on' : ''),
1745
- role: 'button',
1746
- tabIndex: 0,
1747
- onClick: () => panel.toggle(),
1748
- onKeyDown: (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); panel.toggle() } },
1749
- title: open ? t('close') : t('open'),
1750
- 'aria-expanded': open ? 'true' : 'false',
1751
- }, h('span', { className: 'glyph' }, '◈'), 'Pictor')
1799
+ // 入坞:dsh-app-dock 是依赖,入口交给坞(含 ready 延迟注册),自占 footer 槽移除。
1800
+ const registerWithDock = () => {
1801
+ if (typeof window === 'undefined' || !window.__dshAppDock__) return
1802
+ window.__dshAppDock__.register({ id: 'dsh-pictor', label: 'Pictor', icon: '◈', order: 20, onToggle: () => panel.toggle() })
1803
+ if (window.__dshAppDock__.lang) {
1804
+ window.__dshAppDock__.lang.subscribe(() => {
1805
+ const v = window.__dshAppDock__.lang.get()
1806
+ if (langStore.val !== v) { langStore.val = v; langStore.emit() }
1807
+ })
1808
+ }
1752
1809
  }
1810
+ if (typeof window !== 'undefined' && !window.__dshAppDock__) {
1811
+ window.addEventListener('dsh-app-dock:ready', registerWithDock, { once: true })
1812
+ }
1813
+ registerWithDock()
1753
1814
 
1754
- slots.inject('sidebar.footer.action', () => slots.register(
1755
- { name: 'sidebar.footer.action', id: 'dsh-pictor', order: 20, label: 'Pictor' },
1756
- () => h(FooterAction, null),
1757
- ))
1758
1815
  slots.inject('shell.overlay', () => slots.register(
1759
1816
  { name: 'shell.overlay', id: 'dsh-pictor', order: 10, label: 'Pictor' },
1760
1817
  () => h(WorkbenchPanel, null),
package/lib/index.js CHANGED
@@ -702,6 +702,52 @@ function apply(ctx, config = {}) {
702
702
  } });
703
703
  }
704
704
  }
705
+ const PREFIX = "/pictor/asset/project";
706
+ wsA.register({ kind: "prefix", path: PREFIX, handler: (req, res) => {
707
+ try {
708
+ const pathname = new URL(req.url || "/", "http://x").pathname;
709
+ const rest = pathname.slice(PREFIX.length + 1);
710
+ const slash = rest.indexOf("/");
711
+ if (slash <= 0) {
712
+ res.writeHead(404);
713
+ res.end("not found");
714
+ return;
715
+ }
716
+ const id = rest.slice(0, slash);
717
+ const file = rest.slice(slash + 1);
718
+ if (id.includes("..") || /[\\/]/.test(id) || !/^[0-9a-zA-Z_.-]+$/.test(file) || !file.endsWith(".png")) {
719
+ res.writeHead(400);
720
+ res.end("bad request");
721
+ return;
722
+ }
723
+ const buf = readFileSync(join(projectDir(id), "output", file));
724
+ res.writeHead(200, { "content-type": "image/png", "cache-control": "public, max-age=300" });
725
+ res.end(buf);
726
+ } catch {
727
+ res.writeHead(404);
728
+ res.end("not found");
729
+ }
730
+ } });
731
+ wsA.register({ kind: "exact", path: "/pictor/diag", handler: (req, res) => {
732
+ let body = "";
733
+ try {
734
+ req.on("data", (c) => {
735
+ body += c;
736
+ });
737
+ req.on("end", () => {
738
+ try {
739
+ mkdirSync(base, { recursive: true });
740
+ appendFileSync(join(base, "diag.jsonl"), JSON.stringify({ t: Date.now(), ...JSON.parse(body || "{}") }) + "\n");
741
+ } catch {
742
+ }
743
+ res.writeHead(204);
744
+ res.end();
745
+ });
746
+ } catch {
747
+ res.writeHead(204);
748
+ res.end();
749
+ }
750
+ } });
705
751
  }
706
752
  return { dataRoot: base };
707
753
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-pictor",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Pictor:文档转信息图工作台。每项目一个连贯 dsh 会话,GUI 为 shell.overlay 工作台(左栏项目、右栏三步:提取/方案/渲染),行为全部交给 DSH 智能运行时。",
5
5
  "type": "module",
6
6
  "repository": {
@@ -46,17 +46,24 @@
46
46
  "docs",
47
47
  "README.md",
48
48
  "LICENSE",
49
- "assets"
49
+ "assets",
50
+ "NOTICE.md"
50
51
  ],
51
52
  "scripts": {
52
53
  "build": "node scripts/build.mjs",
53
54
  "verify": "node scripts/build.mjs && node verify.mjs",
54
55
  "verify:integration": "bash scripts/transport-smoke.sh",
56
+ "test:pack": "bash scripts/package-integrity.sh",
57
+ "test:install": "bash scripts/install-smoke.sh",
58
+ "hooks:install": "git config core.hooksPath .githooks",
55
59
  "test:e2e": "playwright test --config e2e/playwright.config.ts --project chromium",
56
60
  "verify:live": "playwright test --config e2e/playwright.config.ts --project live",
57
61
  "prepare": "node scripts/build.mjs"
58
62
  },
59
63
  "license": "MIT",
64
+ "dependencies": {
65
+ "dsh-app-dock": "^0.1.2"
66
+ },
60
67
  "peerDependencies": {
61
68
  "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
62
69
  "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.6",