dsh-plugin-deck-studio 0.0.0-stage → 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.
Files changed (72) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +47 -2
  3. package/client.js +500 -0
  4. package/cordis.example.yml +163 -0
  5. package/cordis.patch.yml +147 -0
  6. package/index.js +2340 -0
  7. package/lib/config.js +204 -0
  8. package/lib/content-density.js +98 -0
  9. package/lib/deck-brief.js +87 -0
  10. package/lib/deck-docs.js +1095 -0
  11. package/lib/deck-rules.js +124 -0
  12. package/lib/deck-templates.js +149 -0
  13. package/lib/deck-workspace.js +480 -0
  14. package/lib/design-style.js +157 -0
  15. package/lib/designs.js +175 -0
  16. package/lib/diff.js +88 -0
  17. package/lib/exporter.js +336 -0
  18. package/lib/faq.js +300 -0
  19. package/lib/git-commit.js +87 -0
  20. package/lib/home-catalog.js +249 -0
  21. package/lib/png.js +275 -0
  22. package/lib/proposal-decision.js +204 -0
  23. package/lib/render-nodes.js +171 -0
  24. package/lib/render-settings.js +213 -0
  25. package/lib/render.js +287 -0
  26. package/lib/review-personas.js +258 -0
  27. package/lib/script-style.js +55 -0
  28. package/lib/script-writer.js +165 -0
  29. package/lib/source-adapter.js +327 -0
  30. package/lib/store.js +212 -0
  31. package/lib/tools.js +2594 -0
  32. package/lib/watermark.js +137 -0
  33. package/package.json +66 -4
  34. package/seeds/resources/deckline-logo.png +0 -0
  35. package/seeds/resources/deckline-logo.svg +12 -0
  36. package/seeds/resources/deckline-mark.png +0 -0
  37. package/seeds/resources/index.json +19 -0
  38. package/seeds/samples/BIZ-01.jpg +0 -0
  39. package/seeds/samples/BIZ-02.jpg +0 -0
  40. package/seeds/samples/BIZ-03.jpg +0 -0
  41. package/seeds/samples/EDT-01.jpg +0 -0
  42. package/seeds/samples/EDT-02.jpg +0 -0
  43. package/seeds/samples/EDT-03.jpg +0 -0
  44. package/seeds/samples/FYI-01.jpg +0 -0
  45. package/seeds/samples/FYI-02.jpg +0 -0
  46. package/seeds/samples/FYI-03.jpg +0 -0
  47. package/seeds/samples/GRD-01.jpg +0 -0
  48. package/seeds/samples/GRD-02.jpg +0 -0
  49. package/seeds/samples/GRD-03.jpg +0 -0
  50. package/seeds/samples/JPN-01.jpg +0 -0
  51. package/seeds/samples/JPN-02.jpg +0 -0
  52. package/seeds/samples/JPN-03.jpg +0 -0
  53. package/seeds/samples/SKT-01.jpg +0 -0
  54. package/seeds/samples/SKT-02.jpg +0 -0
  55. package/seeds/samples/SKT-03.jpg +0 -0
  56. package/seeds/samples/SWS-01.jpg +0 -0
  57. package/seeds/samples/SWS-02.jpg +0 -0
  58. package/seeds/samples/SWS-03.jpg +0 -0
  59. package/seeds/samples/TEC-01.jpg +0 -0
  60. package/seeds/samples/TEC-02.jpg +0 -0
  61. package/seeds/samples/TEC-03.jpg +0 -0
  62. package/seeds/styles/BIZ.json +22 -0
  63. package/seeds/styles/EDT.json +22 -0
  64. package/seeds/styles/FYI.json +22 -0
  65. package/seeds/styles/GRD.json +22 -0
  66. package/seeds/styles/JPN.json +22 -0
  67. package/seeds/styles/SKT.json +22 -0
  68. package/seeds/styles/SWS.json +22 -0
  69. package/seeds/styles/TEC.json +22 -0
  70. package/ui/logo.svg +20 -0
  71. package/ui/workbench.css +687 -0
  72. package/ui/workbench.html +4554 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Yuna (Deckline)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,48 @@
1
- # Temporary Holding Version
1
+ # Deckline · `dsh-plugin-deck-studio`
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ DSH(DeepSeek Harness)的 **image deck 工作台插件**:左侧原生 Chat、中间整页图片 deck、右侧资料 / Designs / 资源 / 会话。
4
+ 用对话改稿、按页出图(可点名批量连出)、版本留存、导出 PDF / PPTX / 交接包——不是又一个 PPT 编辑器。
5
+
6
+ ## 安装
7
+
8
+ ```sh
9
+ # 1) 新建一个 profile(基于 DSH 自带 web 模板)
10
+ dsh --profile deckline --from-default-profile web
11
+
12
+ # 2) 安装本插件(安装后会自动注册 bundle,无需手动改配置文件)
13
+ dsh plugin --profile deckline add dsh-plugin-deck-studio@0.2.2
14
+
15
+ # 3) 启动,浏览器打开终端打印的带 token 链接
16
+ dsh --profile deckline --port 13090
17
+ ```
18
+
19
+ 确认装上了(输出 `1` 才对):
20
+
21
+ ```sh
22
+ dsh --profile deckline --dump-config | grep -c "preset-deck-critic"
23
+ ```
24
+
25
+ 从源码 / Git 安装、Windows、以及完整配置(数据目录 / deck 源 / 出图节点 / key)见仓库文档:
26
+
27
+ - [docs/install.md](https://github.com/yuna78/dsh-deck-studio/blob/main/docs/install.md)(中文)
28
+ - [docs/install.en.md](https://github.com/yuna78/dsh-deck-studio/blob/main/docs/install.en.md)(English)
29
+ - [示例 deck 与效果](https://github.com/yuna78/dsh-deck-studio/tree/main/samples/deckline-intro)
30
+
31
+ ## 快速开始(fake 模式,无需 key)
32
+
33
+ 1. 首页点「+ 新建 Deck」,填标题 / 受众 / 一句话意图;
34
+ 2. 在 Chat 里让 Agent 起草 `intent.md` / `script.md`,确认后逐页出图;
35
+ 3. 默认 fake 模式出占位图,能走完整个流程;要看真图再进「设置」填出图节点 endpoint / 模型 / key。
36
+
37
+ 出图节点不内置:real 模式必须自己配 OpenAI 兼容的 `/v1/images/generations` 地址(key 存本机,
38
+ `~/.dsh/deckline/render-settings.json`,0600,不进 git、不回显)。
39
+
40
+ ## 数据在哪
41
+
42
+ - 运行数据(工作副本 / 出图 / 事件):`$DSH_HOME/deckline`(可用 `dataRoot` 覆盖);
43
+ - 受控源 deck:`decks[].sourceRoot` 指向的目录,**只读**,Deckline 永不回写;
44
+ - 新建 deck:`projectsRoot` 或 `<dataRoot>/projects/<deckId>/source`。
45
+
46
+ ## 许可
47
+
48
+ MIT © 2026 Yuna (Deckline)。见仓库 [LICENSE](https://github.com/yuna78/dsh-deck-studio/blob/main/LICENSE)。
package/client.js ADDED
@@ -0,0 +1,500 @@
1
+ /**
2
+ * dsh-plugin-deck-studio —— 观测面板(浏览器半)。
3
+ *
4
+ * 只画界面、只 fetch `/api/deck-studio.*`;不碰文件系统、无外网请求。
5
+ * 数据全部由宿主半聚合好(浏览器半只做展示,不重算业务口径)。
6
+ *
7
+ * 形态:主区「Deck 工作台」面板(iframe 复用 /api/deck-studio.app?embed=1)。
8
+ * 启动后自动打开(零点击进入),左侧 foote 只保留一个「Deckline」入口用于重新打开;
9
+ * 不再注册右侧 dockkit 观测标签页(F-63:入口太多且丑)。
10
+ * 宿主没有 layout 主区面板契约时降级为 main 面板 + sidebar.panellist。
11
+ *
12
+ * Chatbox 说明(ADR-0006 D1):对话入口就是 DSH 原生 Chat——选择
13
+ * 「Deckline · image deck」preset 即进入 Deck Agent 会话;本面板只负责
14
+ * 观测 task / event / proposal / deck 状态。
15
+ */
16
+ window.__ModuleLoader__.load({
17
+ id: "dsh-plugin-deck-studio",
18
+ factory: (require) => {
19
+ "use strict";
20
+ var module = { exports: {} };
21
+ var exports = module.exports;
22
+
23
+ const React = require("react");
24
+ const h = React.createElement;
25
+
26
+ const PLUGIN_ID = "dsh-plugin-deck-studio";
27
+ const KIND = "deck-studio";
28
+ const PANEL_KEY = "deck-studio-panel";
29
+ const WORKBENCH_KEY = "deck-studio-workbench";
30
+ const ROUTE_PREFIX = "/api/deck-studio.";
31
+ const POLL_MS = 5000;
32
+
33
+ /** 路由白名单:getJson 只放行这一族地址。 */
34
+ async function getJson(path) {
35
+ if (!path.startsWith(ROUTE_PREFIX)) throw new Error(`blocked non-plugin route: ${path}`);
36
+ const response = await fetch(path, { headers: { accept: "application/json" } });
37
+ if (!response.ok) throw new Error(`${path} → HTTP ${response.status}`);
38
+ return response.json();
39
+ }
40
+
41
+ /** 样式:结构色走 GUI 令牌并带兜底,不写死白底黑字。 */
42
+ function injectStylesheet() {
43
+ if (document.getElementById("dsh-deck-studio-style")) return;
44
+ const style = document.createElement("style");
45
+ style.id = "dsh-deck-studio-style";
46
+ style.textContent = `
47
+ #dsh-deck-studio-panel{font-size:12px;color:var(--dsw-alias-text-primary,#18343b);line-height:1.55;height:100%;overflow:auto;padding:14px;box-sizing:border-box}
48
+ #dsh-deck-studio-panel .dds-card{border:1px solid var(--dsw-alias-border-primary,#d6d0c4);border-radius:8px;background:var(--dsw-alias-surface-primary,#fffdf8);padding:10px 12px;margin-bottom:10px}
49
+ #dsh-deck-studio-panel .dds-eyebrow{font-size:10px;letter-spacing:.08em;text-transform:uppercase;color:var(--dsw-alias-text-tertiary,#728086);margin:0 0 6px}
50
+ #dsh-deck-studio-panel h3{margin:0 0 6px;font-size:13px}
51
+ #dsh-deck-studio-panel .dds-meta{color:var(--dsw-alias-text-secondary,#5c6464);font-size:11px}
52
+ #dsh-deck-studio-panel .dds-warn{color:#b3591e;font-size:11px}
53
+ #dsh-deck-studio-panel table{width:100%;border-collapse:collapse;font-size:11px}
54
+ #dsh-deck-studio-panel th{ text-align:left;color:var(--dsw-alias-text-tertiary,#728086);font-weight:600;border-bottom:1px solid var(--dsw-alias-border-primary,#d6d0c4);padding:3px 6px;white-space:nowrap}
55
+ #dsh-deck-studio-panel td{padding:3px 6px;border-bottom:1px dashed var(--dsw-alias-border-secondary,#e8e2d6);vertical-align:top}
56
+ #dsh-deck-studio-panel .dds-pill{display:inline-block;padding:1px 7px;border-radius:9px;font-size:10px;border:1px solid var(--dsw-alias-border-primary,#c9c2b4)}
57
+ #dsh-deck-studio-panel .dds-succeeded{color:#1c7c46;border-color:#1c7c46}
58
+ #dsh-deck-studio-panel .dds-failed{color:#b3261e;border-color:#b3261e}
59
+ #dsh-deck-studio-panel .dds-waiting-user{color:#8a6100;border-color:#c5882f;background:color-mix(in srgb,#c5882f 12%,transparent)}
60
+ #dsh-deck-studio-panel .dds-open-workbench{display:inline-block;padding:6px 12px;border-radius:6px;background:#0c4651;color:#faf8f2;font-size:12px;text-decoration:none}
61
+ #dsh-deck-studio-panel .dds-open-workbench:hover{background:#0f6975}
62
+ #dsh-deck-studio-panel .dds-running,#dsh-deck-studio-panel .dds-queued{color:#0f6975;border-color:#1496a8}
63
+ #dsh-deck-studio-panel .dds-cancelled{color:var(--dsw-alias-text-tertiary,#728086)}
64
+ #dsh-deck-studio-panel .dds-foot{margin-top:8px;color:var(--dsw-alias-text-tertiary,#728086);font-size:10px;display:flex;justify-content:space-between;gap:8px}
65
+ #dsh-deck-studio-panel code{font-size:10.5px;color:var(--dsw-alias-text-secondary,#0f6975)}
66
+ `;
67
+ document.head.appendChild(style);
68
+ }
69
+
70
+ /**
71
+ * F-69:把 DSH 外壳左侧栏收起时整列收成 0 宽。
72
+ * 官方 collapsed 在 Web 端仍保留 56px 图标栏;只有 profile 显式配置
73
+ * `hideShellSidebar: true` 才注入,避免影响其他 profile(如开发用 3080)。
74
+ */
75
+ function injectShellSidebarStyle() {
76
+ if (document.getElementById("dsh-deck-studio-shell-sidebar-style")) return;
77
+ const style = document.createElement("style");
78
+ style.id = "dsh-deck-studio-shell-sidebar-style";
79
+ style.textContent = `
80
+ div[data-sidebar-collapsed]{grid-template-columns:0px minmax(0px,1fr) 0px!important}
81
+ `;
82
+ document.head.appendChild(style);
83
+ }
84
+
85
+ /** 状态药丸。 */
86
+ function Pill({ value }) {
87
+ const cls = String(value ?? "").replace(/[^a-z-]/gi, "") || "cancelled";
88
+ return h("span", { className: `dds-pill dds-${cls}` }, String(value ?? "—"));
89
+ }
90
+
91
+ function fmtTime(iso) {
92
+ if (!iso) return "—";
93
+ try {
94
+ return new Date(iso).toLocaleTimeString("zh-CN", { hour12: false });
95
+ } catch {
96
+ return iso;
97
+ }
98
+ }
99
+
100
+ /** 面板体:状态 + 待确认 + 任务 + 事件。 */
101
+ function DeckBody(props) {
102
+ const [state, setState] = React.useState(null);
103
+ const [error, setError] = React.useState(null);
104
+ const [tick, setTick] = React.useState(0);
105
+
106
+ React.useEffect(() => {
107
+ let alive = true;
108
+ async function poll() {
109
+ try {
110
+ const next = await getJson("/api/deck-studio.state");
111
+ if (alive) {
112
+ setState(next);
113
+ setError(null);
114
+ }
115
+ } catch (err) {
116
+ if (alive) setError(err?.message ?? String(err));
117
+ }
118
+ }
119
+ poll();
120
+ const timer = setInterval(poll, POLL_MS);
121
+ return () => {
122
+ alive = false;
123
+ clearInterval(timer);
124
+ };
125
+ }, [tick]);
126
+
127
+ const refresh = () => setTick((value) => value + 1);
128
+
129
+ if (error) {
130
+ return h("div", { id: "dsh-deck-studio-panel" },
131
+ h("div", { className: "dds-card" },
132
+ h("p", { className: "dds-eyebrow" }, "Deckline"),
133
+ h("h3", null, "读不到运行状态"),
134
+ h("p", { className: "dds-warn" }, String(error)),
135
+ h("p", { className: "dds-meta" }, "确认 profile 里已挂载 dsh-plugin-deck-studio 且路由可用。"),
136
+ h("button", { onClick: refresh }, "重试"),
137
+ ));
138
+ }
139
+ if (!state) {
140
+ return h("div", { id: "dsh-deck-studio-panel" }, h("p", { className: "dds-meta" }, "加载中…"));
141
+ }
142
+
143
+ const waiting = state.pendingProposals ?? [];
144
+ const tasks = state.tasks ?? [];
145
+ const events = state.events ?? [];
146
+ const decks = state.decks ?? [];
147
+
148
+ return h("div", { id: "dsh-deck-studio-panel" },
149
+ h("div", { className: "dds-card" },
150
+ h("p", { className: "dds-eyebrow" }, "Deckline · 观测"),
151
+ h("h3", null, state.displayName ?? "dsh-deck-studio"),
152
+ h("p", { className: "dds-meta" },
153
+ `渲染:${state.renderMode} · ${state.renderModel} | 工具:${state.toolsMounted ? "已挂载" : "未挂载(观测模式)"} | 数据:${state.dataRoot}`),
154
+ h("p", { className: "dds-meta" },
155
+ "对话入口 = DSH 原生 Chat,选择「Deckline · image deck」preset 即进入 Deck Agent 会话。"),
156
+ h("p", { style: { marginTop: 10 } },
157
+ h("a", { href: "/api/deck-studio.app", target: "_blank", rel: "noreferrer", className: "dds-open-workbench" },
158
+ "打开完整工作台 UI ↗"),
159
+ h("div", { className: "dds-meta" },
160
+ "三栏 Observation 工作台:左侧 Chat 直连 DSH 会话,中间 storyboard 与右侧 Inspector 读真实 .deck 数据。"),
161
+ ),
162
+ ),
163
+
164
+ h("div", { className: "dds-card" },
165
+ h("p", { className: "dds-eyebrow" }, `受控 deck 源(${decks.length})`),
166
+ decks.length === 0
167
+ ? h("p", { className: "dds-meta" }, "未配置 deck 源——检查 preset 配置里的 decks。")
168
+ : decks.map((deck) => h("div", { key: deck.deckId, style: { marginBottom: 8 } },
169
+ h("div", null, h("b", null, deck.deckId), " ", h("span", { className: "dds-meta" }, `· ${deck.title}`)),
170
+ h("div", { className: "dds-meta" },
171
+ `${deck.pageCount} 页 · ${deck.status || "—"} · 源${deck.sourceAvailable ? "可读" : "不可读"}`),
172
+ ...(deck.warnings ?? []).map((warning, index) => h("div", { key: index, className: "dds-warn" }, `⚠ ${warning}`)),
173
+ )),
174
+ ),
175
+
176
+ h("div", { className: "dds-card" },
177
+ h("p", { className: "dds-eyebrow" }, `待你确认(${waiting.length})`),
178
+ waiting.length === 0
179
+ ? h("p", { className: "dds-meta" }, "暂无 waiting-user 提案。")
180
+ : waiting.map((proposal) => h("div", { key: proposal.proposalId, style: { marginBottom: 8 } },
181
+ h("div", null, h("code", null, proposal.proposalId), " ", h("b", null, `${proposal.pageId} · ${proposal.field}`)),
182
+ proposal.reason ? h("div", { className: "dds-meta" }, `理由:${proposal.reason}`) : null,
183
+ h("div", { className: "dds-meta" }, `创建于 ${fmtTime(proposal.createdAt)} · 在 Chat 里回复确认或驳回即可。`),
184
+ )),
185
+ ),
186
+
187
+ h("div", { className: "dds-card" },
188
+ h("p", { className: "dds-eyebrow" }, `任务(最近 ${tasks.length})`),
189
+ h("table", null,
190
+ h("thead", null, h("tr", null, ["task", "类型", "状态", "deck", "页", "创建"].map((label) => h("th", { key: label }, label)))),
191
+ h("tbody", null,
192
+ tasks.length === 0
193
+ ? h("tr", null, h("td", { colSpan: 6, className: "dds-meta" }, "还没有任务——去 Chat 里开始。"))
194
+ : tasks.map((task) => h("tr", { key: task.taskId },
195
+ h("td", null, h("code", null, task.taskId)),
196
+ h("td", null, task.kind),
197
+ h("td", null, h(Pill, { value: task.status })),
198
+ h("td", null, task.deckId ?? "—"),
199
+ h("td", null, (task.slideIds ?? []).join(",") || "—"),
200
+ h("td", { className: "dds-meta" }, fmtTime(task.createdAt)),
201
+ ))),
202
+ ),
203
+ Object.keys(state.taskCounts ?? {}).length > 0
204
+ ? h("p", { className: "dds-meta", style: { marginTop: 6 } },
205
+ Object.entries(state.taskCounts).map(([key, value]) => `${key}:${value}`).join(" | "))
206
+ : null,
207
+ ),
208
+
209
+ h("div", { className: "dds-card" },
210
+ h("p", { className: "dds-eyebrow" }, `事件流水(最近 ${events.length})`),
211
+ h("table", null,
212
+ h("thead", null, h("tr", null, ["时间", "事件", "状态", "对象", "角色"].map((label) => h("th", { key: label }, label)))),
213
+ h("tbody", null,
214
+ events.length === 0
215
+ ? h("tr", null, h("td", { colSpan: 5, className: "dds-meta" }, "暂无事件。"))
216
+ : events.map((event) => h("tr", { key: event.eventId },
217
+ h("td", { className: "dds-meta" }, fmtTime(event.timestamp)),
218
+ h("td", null, event.type),
219
+ h("td", null, h(Pill, { value: event.status })),
220
+ h("td", { className: "dds-meta" }, [event.deckId, ...(event.slideIds ?? [])].filter(Boolean).join(" / ") || "—"),
221
+ h("td", null, event.actor),
222
+ ))),
223
+ ),
224
+ ),
225
+
226
+ h("div", { className: "dds-foot" },
227
+ h("span", null, `↻ 每 ${Math.round(POLL_MS / 1000)} 秒刷新`),
228
+ h("span", null, `v${state.version ?? "?"} · ${fmtTime(state.generatedAt)}`),
229
+ h("button", { onClick: refresh }, "立即刷新"),
230
+ ),
231
+ props?.onOpenWorkbench
232
+ ? h("div", { style: { marginTop: 10 } },
233
+ h("button", { className: "dds-open-workbench", onClick: props.onOpenWorkbench, style: { border: 0, cursor: "pointer" } }, "打开 Deck 工作台(对话用 DSH 原生 Chat)"))
234
+ : null,
235
+ );
236
+ }
237
+
238
+ /** 侧栏 glyph(降级 panellist / footer 用)。 */
239
+ function DeckGlyph() {
240
+ return h("span", { style: { fontSize: 13 } }, "▦");
241
+ }
242
+
243
+ function MainPanel() {
244
+ return h("div", { style: { height: "100%", minHeight: 0 } }, h(DeckBody));
245
+ }
246
+
247
+ /**
248
+ * 主区「Deck 工作台」:iframe 复用工作台页面本身(不 embed)。
249
+ * 打开先到主页(#/home/decks),点 deck 后是左中右三栏:左原生 Chat / 中间整页图片 / 右资料库。
250
+ * 对话仍走 DSH 原生 Chat(工作台左栏嵌 `/?decklineChat=1`),权限、附件、模型、思考过程、气泡都是原生的。
251
+ */
252
+ function WorkbenchFrame() {
253
+ return h("iframe", {
254
+ id: "dsh-deck-workbench-frame",
255
+ title: "Deckline 工作台",
256
+ src: "/api/deck-studio.app#/home/decks",
257
+ style: { width: "100%", height: "100%", border: 0, display: "block", background: "#faf8f2" },
258
+ // F-102:全屏演示要能用真全屏;跨域 iframe 没有这项授权时 requestFullscreen 会被拒。
259
+ allow: "clipboard-write; fullscreen",
260
+ allowFullScreen: true,
261
+ });
262
+ }
263
+
264
+ /** 客户端插件体:样式 + 槽位注册。 */
265
+ function apply(ctx) {
266
+ injectStylesheet();
267
+ // 工作台左栏会把 DSH 原生 Chat 以 `/?decklineChat=1` 嵌进来(同源 iframe)。
268
+ // 那个嵌套的 DSH 实例不再自动打开工作台,否则会层层嵌套(F-68)。
269
+ const chatEmbed = new URLSearchParams(window.location.search).get("decklineChat") === "1";
270
+ if (chatEmbed) ctx.logger?.info?.("deck-studio client: chat-embed mode (bridge only, no workbench shell)");
271
+ const openTab = () => {
272
+ const sidebarRight = ctx.get?.("sidebarRight") ?? ctx.sidebarRight;
273
+ if (!sidebarRight?.openTab) throw new Error("右侧面板服务(ctx.sidebarRight)未加载");
274
+ sidebarRight.openTab(KIND);
275
+ };
276
+ const layout = ctx.get?.("layout") ?? ctx.layout ?? null;
277
+ /** F-69:是否收起宿主左侧栏 —— 只认 profile 显式配置,默认不动其他 profile 的 DSH 界面。 */
278
+ let hideShellSidebar = false;
279
+ /** F-69:DSH 外壳左侧栏默认收起(Web 端 collapsed 仍留 56px 图标栏,样式里已收成 0px)。 */
280
+ const collapseShellSidebar = () => {
281
+ try {
282
+ if (!layout?.toggleSidebar) return;
283
+ if (document.querySelector("[data-sidebar-collapsed]")) return;
284
+ layout.toggleSidebar();
285
+ } catch (error) {
286
+ ctx.logger?.warn?.("deck-studio: collapse shell sidebar failed", error);
287
+ }
288
+ };
289
+ const openWorkbench = () => {
290
+ if (!layout?.selectPanel) throw new Error("布局服务(ctx.layout)未加载,无法打开工作台");
291
+ layout.selectPanel(WORKBENCH_KEY);
292
+ if (hideShellSidebar) collapseShellSidebar();
293
+ };
294
+ const closeWorkbench = () => layout?.selectPanel?.(null);
295
+
296
+ const currentSessionId = () => {
297
+ try {
298
+ const raw = window.localStorage.getItem("dsh.sessions.current");
299
+ return raw ? JSON.parse(raw).sessionId || "" : "";
300
+ } catch {
301
+ return "";
302
+ }
303
+ };
304
+ const sendToSession = async (sessionId, text) => {
305
+ const sessions = ctx.sessions;
306
+ if (!sessions?.scope) throw new Error("sessions service unavailable");
307
+ if (!sessionId) throw new Error("no target session");
308
+ const scoped = sessions.scope(sessionId);
309
+ if (!scoped) throw new Error(`session ${sessionId} is not retained`);
310
+ // 会话级上下文需要显式 inject 才能拿到 conversation 服务。
311
+ await new Promise((resolve, reject) => {
312
+ let started = false;
313
+ const timer = setTimeout(() => {
314
+ if (!started) reject(new Error("conversation service unavailable in session scope"));
315
+ }, 1500);
316
+ scoped.inject(["conversation"], (scopedCtx) => {
317
+ started = true;
318
+ clearTimeout(timer);
319
+ Promise.resolve(scopedCtx.conversation.send(text)).then(resolve, reject);
320
+ });
321
+ });
322
+ return sessionId;
323
+ };
324
+
325
+
326
+ // 工作台 iframe → 宿主:回到 Conversation(原生对话)
327
+ ctx.effect(() => {
328
+ const onMessage = (event) => {
329
+ if (event?.data?.type === "dsh-deck-studio:close") closeWorkbench();
330
+ };
331
+ window.addEventListener("message", onMessage);
332
+ return () => window.removeEventListener("message", onMessage);
333
+ }, "deck-studio: workbench close bridge");
334
+
335
+ /* 工作台 iframe → 宿主:把一条完整 prompt 发进当前会话的原生 Chat。 */
336
+ ctx.effect(() => {
337
+ const sendToChat = async (text) => {
338
+ const sessionId = currentSessionId();
339
+ if (!sessionId) throw new Error("no current session");
340
+ return sendToSession(sessionId, text);
341
+ };
342
+ const onMessage = (event) => {
343
+ if (event?.data?.type !== "dsh-deck-studio:prompt") return;
344
+ const text = String(event.data.text ?? "");
345
+ if (!text.trim()) return;
346
+ const reply = (payload) => {
347
+ try {
348
+ event.source?.postMessage({ type: "dsh-deck-studio:prompt-ack", ...payload }, "*");
349
+ } catch {
350
+ /* 跨窗口失效就忽略 */
351
+ }
352
+ };
353
+ sendToChat(text)
354
+ .then((sessionId) => reply({ ok: true, sessionId }))
355
+ .catch((error) => {
356
+ ctx.logger?.warn?.("deck-studio: prompt bridge failed", error);
357
+ reply({ ok: false, reason: String(error?.message ?? error) });
358
+ });
359
+ };
360
+ window.addEventListener("message", onMessage);
361
+ return () => window.removeEventListener("message", onMessage);
362
+ }, "deck-studio: prompt bridge");
363
+
364
+ /* 工作台 → 宿主:建/切会话并绑定 deck|design(F-43:每个会话必须有绑定)。
365
+ * - action=open:切到给定 sessionId;
366
+ * - action=ensure:有 sessionId 就切过去;没有就 sessions.create({cwd}) 新建再切,
367
+ * 然后可选地把首条 prompt 发进去(design 会话确认后「chat 开始工作」)。 */
368
+ ctx.effect(() => {
369
+ const onMessage = async (event) => {
370
+ const data = event?.data ?? {};
371
+ if (data.type !== "dsh-deck-studio:conversation") return;
372
+ const reply = (payload) => {
373
+ try {
374
+ event.source?.postMessage({ type: "dsh-deck-studio:conversation-ack", requestId: data.requestId ?? null, ...payload }, "*");
375
+ } catch {
376
+ /* 跨窗口失效忽略 */
377
+ }
378
+ };
379
+ try {
380
+ const uiWorkspace = ctx.uiWorkspace ?? ctx.get?.("uiWorkspace");
381
+ if (!uiWorkspace?.openSession) throw new Error("uiWorkspace service unavailable");
382
+ let sessionId = String(data.sessionId ?? "").trim();
383
+ if (data.action === "ensure" && !sessionId) {
384
+ const sessions = ctx.sessions;
385
+ if (!sessions?.create) throw new Error("sessions service unavailable");
386
+ // 注意:客户端 sessions.create 直接 resolve 新 sessionId,失败才 throw(不是 {ok,value})。
387
+ sessionId = await sessions.create({ cwd: String(data.cwd ?? "") || undefined });
388
+ if (!sessionId) throw new Error("session create returned no id");
389
+ }
390
+ if (!sessionId) throw new Error("没有目标会话");
391
+ if (data.action === "open" || data.action === "ensure") uiWorkspace.openSession(sessionId);
392
+ if (data.prompt && data.action === "ensure") await sendToSession(sessionId, String(data.prompt));
393
+ reply({ ok: true, sessionId });
394
+ } catch (error) {
395
+ ctx.logger?.warn?.("deck-studio: conversation bridge failed", error);
396
+ reply({ ok: false, reason: String(error?.message ?? error) });
397
+ }
398
+ };
399
+ window.addEventListener("message", onMessage);
400
+ return () => window.removeEventListener("message", onMessage);
401
+ }, "deck-studio: conversation bridge");
402
+
403
+ if (!chatEmbed) {
404
+ /* 工作台顶栏「☰ DSH」→ 宿主:临时展开 / 收起左侧栏(需要看会话、插件、设置时用)。 */
405
+ ctx.effect(() => {
406
+ const onMessage = (event) => {
407
+ if (event?.data?.type !== "dsh-deck-studio:toggle-shell-sidebar") return;
408
+ if (!hideShellSidebar) return;
409
+ try {
410
+ layout?.toggleSidebar?.();
411
+ } catch (error) {
412
+ ctx.logger?.warn?.("deck-studio: toggle shell sidebar failed", error);
413
+ }
414
+ };
415
+ window.addEventListener("message", onMessage);
416
+ return () => window.removeEventListener("message", onMessage);
417
+ }, "deck-studio: shell sidebar bridge");
418
+
419
+ /* F-69:读 profile 配置决定是否收起宿主左侧栏(默认 false,其他 profile / 3080 不受影响)。 */
420
+ ctx.effect(() => {
421
+ let cancelled = false;
422
+ getJson(`${ROUTE_PREFIX}status`)
423
+ .then((status) => {
424
+ if (cancelled || status?.hideShellSidebar !== true) return;
425
+ hideShellSidebar = true;
426
+ injectShellSidebarStyle();
427
+ collapseShellSidebar();
428
+ })
429
+ .catch((error) => ctx.logger?.warn?.("deck-studio: hideShellSidebar config unavailable", error));
430
+ return () => {
431
+ cancelled = true;
432
+ };
433
+ }, "deck-studio: hide shell sidebar config");
434
+
435
+ const hasRightbar = Boolean(ctx.sidebarRightTabs?.register && ctx.sidebarRight?.openTab);
436
+
437
+ // 主区工作台面板:DSH 原生 Chat 保留在侧栏,主区显示 deck 观测/编辑。
438
+ if (layout?.selectPanel) {
439
+ ctx.effect(
440
+ () => ctx.slots.inject("main", () => ctx.slots.register({ name: "main", key: WORKBENCH_KEY }, WorkbenchFrame)),
441
+ "deck-studio: workbench main panel",
442
+ );
443
+ }
444
+
445
+ // 启动即打开工作台(F-63:不用先点左侧 Deckline 再点右侧按钮)。
446
+ // 等布局槽位注册完成再打开;若用户已经切到别的面板(如 Plugins)就不抢占。
447
+ ctx.effect(() => {
448
+ let timer = 0;
449
+ let attempts = 0;
450
+ const open = () => {
451
+ try {
452
+ const activePanelId = layout?.panelInfo?.getSnapshot?.()?.activePanelId ?? null;
453
+ if (activePanelId === null) openWorkbench();
454
+ } catch (error) {
455
+ if (attempts++ < 6) timer = window.setTimeout(open, 500);
456
+ else ctx.logger?.warn?.("deck-studio: auto-open workbench failed", error);
457
+ }
458
+ };
459
+ timer = window.setTimeout(open, 700);
460
+ return () => window.clearTimeout(timer);
461
+ }, "deck-studio: auto-open workbench");
462
+
463
+ // 唯一入口:侧栏脚部一个「Deckline」按钮(重新打开工作台);→ 对话用工作台里的「← 回到对话」。
464
+ if (hasRightbar) {
465
+ ctx.effect(
466
+ () => ctx.slots.inject("sidebar.footer.action", () =>
467
+ ctx.slots.register({ name: "sidebar.footer.action", id: `${PLUGIN_ID}-workbench` }, (props) => {
468
+ const button = h("button", { onClick: openWorkbench, title: "打开 Deck 工作台(对话在 DSH 原生 Chat)", style: { cursor: "pointer" } },
469
+ h("span", { style: { fontSize: 13 } }, "▤"), props?.wide ? h("span", { style: { marginLeft: 4 } }, "Deckline") : null);
470
+ return button;
471
+ })),
472
+ "deck-studio: workbench footer button",
473
+ );
474
+ ctx.logger?.info?.("deck-studio client: workbench-only mode");
475
+ } else {
476
+ ctx.effect(
477
+ () => ctx.slots.inject("main", () => ctx.slots.register({ name: "main", key: PANEL_KEY }, () => h(DeckBody, { onOpenWorkbench: openWorkbench }))),
478
+ "deck-studio: main panel (fallback)",
479
+ );
480
+ ctx.effect(
481
+ () => ctx.slots.inject("sidebar.panellist", () =>
482
+ ctx.slots.register({ name: "sidebar.panellist", id: PANEL_KEY, order: 46, label: () => "Deckline" }, DeckGlyph)),
483
+ "deck-studio: sidebar button (fallback)",
484
+ );
485
+ ctx.logger?.info?.("deck-studio client: main-panel fallback mode");
486
+ }
487
+ }
488
+ }
489
+
490
+ exports.apply = apply;
491
+ exports.inject = ["slots", "sidebarRightTabs", "sidebarRight", "layout", "sessions", "conversation", "uiWorkspace"];
492
+ exports.PLUGIN_ID = PLUGIN_ID;
493
+ exports.KIND = KIND;
494
+ exports.PANEL_KEY = PANEL_KEY;
495
+ exports.WORKBENCH_KEY = WORKBENCH_KEY;
496
+ exports.DeckBody = DeckBody;
497
+
498
+ return module.exports;
499
+ },
500
+ });