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 +18 -0
- package/README.md +10 -1
- package/README.zh-cn.md +10 -1
- package/lib/client.js +117 -60
- package/lib/index.js +46 -0
- package/package.json +9 -2
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:
|
|
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:
|
|
59
|
-
border-radius:
|
|
60
|
-
.pt-nav-new:hover {
|
|
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:
|
|
96
|
-
font-weight:
|
|
97
|
-
.pt-btn:hover {
|
|
98
|
-
.pt-btn:
|
|
99
|
-
.pt-btn-
|
|
100
|
-
.pt-btn
|
|
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:
|
|
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:
|
|
126
|
-
.pt-input
|
|
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:
|
|
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
|
-
|
|
372
|
-
try {
|
|
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:
|
|
377
|
+
val: readBandungLang(),
|
|
375
378
|
subs: new Set(),
|
|
376
379
|
emit() { for (const f of this.subs) f() },
|
|
377
|
-
set(v) {
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
//
|
|
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' &&
|
|
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
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
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.
|
|
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",
|