dsh-pictor 0.2.0 → 0.2.1

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
@@ -487,10 +487,23 @@ function sanitizePaste(html) {
487
487
  // 会话创建与驱动走 dsh 的 workspace 流程(照 pomasa 的 driveSession):
488
488
  // 宿主只出 prompt,客户端经 workspaces.connectWorkspace 建会话(自然归入
489
489
  // 「Pictor」工作区、继承 profile 默认模型),再经 sessions.prompt 驱动。
490
+ // dsh ≥ 0.1.2-alpha.1(DSH Desktop 0.7.2)把 connectWorkspace 从
491
+ // `workspaces` 服务移到了 `uiWorkspace` 服务上:这里两个形态都尝试,哪个
492
+ // 在就调哪个,老版本前端不受影响。
493
+ function sessionConnectFn(ctx, ws) {
494
+ let uiWs = null
495
+ try { uiWs = ctx.uiWorkspace || ((ctx.get && ctx.get('uiWorkspace')) || null) } catch { /* ignore */ }
496
+ return (uiWs && typeof uiWs.connectWorkspace === 'function')
497
+ ? uiWs.connectWorkspace.bind(uiWs)
498
+ : (ws && typeof ws.connectWorkspace === 'function') ? ws.connectWorkspace.bind(ws) : null
499
+ }
500
+
490
501
  async function pictorWorkspace(ctx) {
491
502
  const ws = ctx.workspaces || (ctx.get && ctx.get('workspaces'))
492
503
  const base = await rpc(ctx, 'config.get').then((c) => (c && c.dataRoot) || '')
493
- if (!ws || typeof ws.connectWorkspace !== 'function') throw new Error('会话服务不可用(workspaces)')
504
+ // 这里只用 list/create/delete/rename,新旧 services 都有;connectWorkspace
505
+ // 的可用性由 driveProjectSession 检查。
506
+ if (!ws) throw new Error('会话服务不可用(workspaces)')
494
507
  const items = () => {
495
508
  try {
496
509
  const snap = ws.list && typeof ws.list.getSnapshot === 'function' ? ws.list.getSnapshot() : null
@@ -531,14 +544,29 @@ async function pictorWorkspace(ctx) {
531
544
  async function driveProjectSession(ctx, prompt) {
532
545
  const ws = ctx.workspaces || (ctx.get && ctx.get('workspaces'))
533
546
  const sessionsSvc = ctx.sessions || (ctx.get && ctx.get('sessions'))
547
+ const connect = sessionConnectFn(ctx, ws)
548
+ const canCreate = !!(sessionsSvc && typeof sessionsSvc.create === 'function')
549
+ if (!(connect || canCreate) || !(sessionsSvc && typeof sessionsSvc.binding === 'function')) {
550
+ let uiWs = null
551
+ try { uiWs = ctx.uiWorkspace || ((ctx.get && ctx.get('uiWorkspace')) || null) } catch { /* ignore */ }
552
+ const diag = { ws: !!ws, ses: !!sessionsSvc && typeof sessionsSvc.binding === 'function', uiws: !!uiWs, conn: !!connect, create: canCreate }
553
+ console.warn('[pictor] session services unavailable', diag)
554
+ try { window.__pictor_diag = diag } catch { /* ignore */ }
555
+ try { pictorDiag(ctx, 'drive:fail') } catch { /* ignore */ }
556
+ throw new Error(`会话服务不可用(workspaces) [ws:${diag.ws},ses:${diag.ses},uiws:${diag.uiws},conn:${diag.conn},create:${diag.create}]`)
557
+ }
534
558
  const wid = await pictorWorkspace(ctx)
535
559
  let sessionId
536
- try { sessionId = await ws.connectWorkspace(wid) }
560
+ try {
561
+ const created = connect ? await connect(wid) : await sessionsSvc.create({ workspaceId: wid })
562
+ sessionId = created && typeof created === 'object' && created.id ? created.id : created
563
+ }
537
564
  catch (e) { throw new Error('创建会话失败:' + String((e && e.message) || e)) }
538
565
  const bound = sessionsSvc && typeof sessionsSvc.binding === 'function' ? sessionsSvc.binding(sessionId) : null
539
566
  const sess = bound && bound.session
540
567
  if (!sess || typeof sess.prompt !== 'function') throw new Error('会话无 prompt 通道')
541
568
  await sess.prompt([{ type: 'text', text: String(prompt || '') }], 'queue')
569
+ try { pictorDiag(ctx, 'drive:ok') } catch { /* ignore */ }
542
570
  return sessionId
543
571
  }
544
572
 
@@ -945,7 +973,17 @@ function fileSlug(s) {
945
973
  return String(s || '').toLowerCase().trim().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40) || 'infographic'
946
974
  }
947
975
 
948
- function ResultsPanel({ outputs, images, renderErrors, proposals, lay, renderBatches }) {
976
+ // 结果图直出走同源 HTTP(data URL 在新版 DSH Desktop WebView 可能被拦),
977
+ // 失败时回退到 RPC 返回的 data URL。
978
+ const imgUrlFor = (projectId, name) => {
979
+ const base = '/pictor/asset/project/' + encodeURIComponent(projectId) + '/' + encodeURIComponent(name)
980
+ try {
981
+ const tok = new URLSearchParams(window.location.search || '').get('token')
982
+ return tok ? base + '?token=' + encodeURIComponent(tok) : base
983
+ } catch { return base }
984
+ }
985
+
986
+ function ResultsPanel({ projectId, outputs, images, renderErrors, proposals, lay, renderBatches }) {
949
987
  const [big, setBig] = React.useState(null)
950
988
  const [collapsed, setCollapsed] = React.useState({})
951
989
  if (!outputs.length && !renderErrors.length) {
@@ -967,7 +1005,7 @@ function ResultsPanel({ outputs, images, renderErrors, proposals, lay, renderBat
967
1005
  h('div', null, h('h3', { className: 'pt-option-title', style: { margin: 0 } }, bigInfo.title)),
968
1006
  h('button', { className: 'pt-modal-close', onClick: () => setBig(null), title: t('close'), 'aria-label': t('close') }, '×')),
969
1007
  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 } }),
1008
+ 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
1009
  bigInfo.actualLayout ? h('p', { className: 'pt-meta', style: { marginTop: 10 } }, t('layoutLabel') + ': ' + layoutLabel(bigInfo.actualLayout)) : null)))
972
1010
  : null
973
1011
 
@@ -991,7 +1029,7 @@ function ResultsPanel({ outputs, images, renderErrors, proposals, lay, renderBat
991
1029
  const dlName = info.prop ? (fileSlug(info.prop.title || layoutLabel(info.actualLayout || info.prop.suggested_layout)) + '-' + info.pid + '.png') : name
992
1030
  return h('figure', { key: name, className: 'pt-img-card' },
993
1031
  images[name]
994
- ? h('img', { className: 'pt-img', src: images[name], alt: info.title, style: { cursor: 'zoom-in' }, onClick: () => setBig(name) })
1032
+ ? 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
1033
  : h('div', { className: 'thumb', style: { width: '100%', height: 140 } }),
996
1034
  h('figcaption', null,
997
1035
  h('div', { className: 'pt-fig-title' }, info.title),
@@ -1405,7 +1443,7 @@ function DetailPane(props) {
1405
1443
  ratios, setRatio, onRender: renderSelected, renderBusy,
1406
1444
  hasImageKey: Boolean(imageCfg && imageCfg.hasKey), running: Boolean(data.running),
1407
1445
  renderState: data.renderState, renderRunning: Boolean(data.renderRunning) }),
1408
- render: h(ResultsPanel, { outputs: data.outputs, images, renderErrors, proposals, lay: layOv, renderBatches: data.renderBatches }),
1446
+ render: h(ResultsPanel, { projectId, outputs: data.outputs, images, renderErrors, proposals, lay: layOv, renderBatches: data.renderBatches }),
1409
1447
  }[step]
1410
1448
 
1411
1449
  return h('div', null,
@@ -1670,7 +1708,30 @@ function selectedLabel(ids) {
1670
1708
  return ids.join('、')
1671
1709
  }
1672
1710
 
1711
+ // 最佳努力诊断:上报此 ctx 上工作区/会话服务的暴露情况(DSH Desktop 0.7.2
1712
+ // 调整了客户端 API),host 落盘 ~/.pictor/diag.jsonl,无需 DevTools 即可排查。
1713
+ function pictorDiag(ctx, phase) {
1714
+ let ws = null, ses = null, ui = null
1715
+ try { ws = ctx && (ctx.workspaces || (ctx.get && ctx.get('workspaces'))) } catch { /* ignore */ }
1716
+ try { ses = ctx && (ctx.sessions || (ctx.get && ctx.get('sessions'))) } catch { /* ignore */ }
1717
+ try { ui = ctx && (ctx.uiWorkspace || (ctx.get && ctx.get('uiWorkspace'))) } catch { /* ignore */ }
1718
+ const body = {
1719
+ phase,
1720
+ ws: !!ws, ses: !!ses, ui: !!ui,
1721
+ wsMethods: ws ? Object.keys(ws).slice(0, 40) : null,
1722
+ sesMethods: ses ? Object.keys(ses).slice(0, 40) : null,
1723
+ uiMethods: ui ? Object.keys(ui).slice(0, 40) : null,
1724
+ connectWs: !!(ws && typeof ws.connectWorkspace === 'function'),
1725
+ connectUi: !!(ui && typeof ui.connectWorkspace === 'function'),
1726
+ bind: !!(ses && typeof ses.binding === 'function'),
1727
+ href: typeof location !== 'undefined' ? location.href.slice(0, 80) : '',
1728
+ }
1729
+ try { fetch('/pictor/diag', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) }).catch(() => {}) } catch { /* ignore */ }
1730
+ console.warn('[pictor] diag', body)
1731
+ }
1732
+
1673
1733
  function apply(ctx) {
1734
+ try { pictorDiag(ctx, 'apply') } catch { /* ignore */ }
1674
1735
  console.info('[dsh-pictor] apply 被调用', { hasSlots: Boolean(ctx && ctx.slots) })
1675
1736
  if (typeof document !== 'undefined') {
1676
1737
  const styleId = 'dsh-pictor-style'
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.1",
4
4
  "description": "Pictor:文档转信息图工作台。每项目一个连贯 dsh 会话,GUI 为 shell.overlay 工作台(左栏项目、右栏三步:提取/方案/渲染),行为全部交给 DSH 智能运行时。",
5
5
  "type": "module",
6
6
  "repository": {
@@ -46,12 +46,16 @@
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"