dsh-plugin-deck-studio 0.2.12 → 0.2.14
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/client.js +204 -20
- package/package.json +2 -2
- package/seeds/docs/assets/screenshots/f197-01-deckline-entry.png +0 -0
- package/seeds/docs/assets/screenshots/f197-02-deckline-icon.png +0 -0
- package/seeds/docs/install.en.md +2 -2
- package/seeds/docs/install.md +4 -4
- package/ui/workbench.css +47 -0
- package/ui/workbench.html +274 -15
package/client.js
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* 数据全部由宿主半聚合好(浏览器半只做展示,不重算业务口径)。
|
|
6
6
|
*
|
|
7
7
|
* 形态:主区「Deck 工作台」面板(iframe 复用 /api/deck-studio.app?embed=1)。
|
|
8
|
-
*
|
|
9
|
-
* 不再注册右侧 dockkit 观测标签页(F-63
|
|
8
|
+
* 启动后自动打开(零点击进入),侧栏只保留一个带图标的「Deckline」面板入口(与其它插件同形态);
|
|
9
|
+
* 不再注册右侧 dockkit 观测标签页(F-63:入口太多且丑),也不再占用侧栏脚部按钮。
|
|
10
10
|
* 宿主没有 layout 主区面板契约时降级为 main 面板 + sidebar.panellist。
|
|
11
11
|
*
|
|
12
12
|
* Chatbox 说明(ADR-0006 D1):对话入口就是 DSH 原生 Chat——选择
|
|
@@ -29,6 +29,14 @@ window.__ModuleLoader__.load({
|
|
|
29
29
|
const WORKBENCH_KEY = "deck-studio-workbench";
|
|
30
30
|
const ROUTE_PREFIX = "/api/deck-studio.";
|
|
31
31
|
const POLL_MS = 5000;
|
|
32
|
+
/**
|
|
33
|
+
* F-202B(ADR-0027):桌面宿主的 shell 起源是自定义协议(如 dsh-app://),嵌 DSH 页面的
|
|
34
|
+
* 子帧拿不到 boot 注入、流式端点也不可达 —— 桌面里对话改用「原生会话 Dock」;
|
|
35
|
+
* web(http)宿主继续走工作台内嵌的 DSH iframe。
|
|
36
|
+
*/
|
|
37
|
+
const DESKTOP_SHELL = !/^(https?:)$/.test(location.protocol);
|
|
38
|
+
/** 桌面 Dock 渲染会话用的子弹槽名(shell.overlay 条目下的 session 作用域子槽)。 */
|
|
39
|
+
const CHAT_SLOT = `${WORKBENCH_KEY}.chat`;
|
|
32
40
|
|
|
33
41
|
/** 路由白名单:getJson 只放行这一族地址。 */
|
|
34
42
|
async function getJson(path) {
|
|
@@ -235,9 +243,34 @@ div[data-sidebar-collapsed]{grid-template-columns:0px minmax(0px,1fr) 0px!import
|
|
|
235
243
|
);
|
|
236
244
|
}
|
|
237
245
|
|
|
238
|
-
/**
|
|
239
|
-
function DeckGlyph() {
|
|
240
|
-
|
|
246
|
+
/** 侧栏图标(panellist 行用):叠卡片 + 星芒,单色 currentColor,跟随主题深浅色。 */
|
|
247
|
+
function DeckGlyph({ size }) {
|
|
248
|
+
const edge = size || 16;
|
|
249
|
+
return h("svg", {
|
|
250
|
+
width: edge,
|
|
251
|
+
height: edge,
|
|
252
|
+
viewBox: "0 0 16 16",
|
|
253
|
+
fill: "none",
|
|
254
|
+
"aria-hidden": "true",
|
|
255
|
+
style: { display: "block" },
|
|
256
|
+
},
|
|
257
|
+
h("rect", {
|
|
258
|
+
x: 4.8, y: 2.4, width: 8.2, height: 6.2, rx: 1.5,
|
|
259
|
+
stroke: "currentColor", strokeWidth: 1.2, opacity: 0.55,
|
|
260
|
+
}),
|
|
261
|
+
h("rect", {
|
|
262
|
+
x: 2.4, y: 5.2, width: 9.2, height: 6.8, rx: 1.6,
|
|
263
|
+
stroke: "currentColor", strokeWidth: 1.35,
|
|
264
|
+
}),
|
|
265
|
+
h("path", {
|
|
266
|
+
d: "M4.6 7.6h4.6M4.6 9.6h3",
|
|
267
|
+
stroke: "currentColor", strokeWidth: 1.2, strokeLinecap: "round",
|
|
268
|
+
}),
|
|
269
|
+
h("path", {
|
|
270
|
+
d: "M12.5 9.9l.45 1.15 1.15.45-1.15.45-.45 1.15-.45-1.15-1.15-.45 1.15-.45z",
|
|
271
|
+
fill: "currentColor",
|
|
272
|
+
}),
|
|
273
|
+
);
|
|
241
274
|
}
|
|
242
275
|
|
|
243
276
|
function MainPanel() {
|
|
@@ -249,9 +282,21 @@ div[data-sidebar-collapsed]{grid-template-columns:0px minmax(0px,1fr) 0px!import
|
|
|
249
282
|
* 打开先到主页(#/home/decks),点 deck 后是左中右三栏:左原生 Chat / 中间整页图片 / 右资料库。
|
|
250
283
|
* 对话仍走 DSH 原生 Chat(工作台左栏嵌 `/?decklineChat=1`),权限、附件、模型、思考过程、气泡都是原生的。
|
|
251
284
|
*/
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* 桌面 Dock(F-202B / ADR-0027 升级):桌面里不再嵌 DSH 页面,用宿主 `shell.overlay` +
|
|
288
|
+
* SessionProvider + `sessions.retain` 把**原生会话**渲染到工作台对话列的位置
|
|
289
|
+
* (multichat / html-deck-studio 同款做法,注册形状已在该宿主验证)。
|
|
290
|
+
* 工作台(iframe)把当前绑定会话经 `dsh-deck-studio:chat-session` 报上来;
|
|
291
|
+
* Dock 测量工作台 iframe 里 `#chatDockArea` 的矩形后固定覆盖其上。
|
|
292
|
+
*/
|
|
293
|
+
/** 客户端插件体:样式 + 槽位注册。 */
|
|
294
|
+
function apply(ctx) {
|
|
295
|
+
injectStylesheet();
|
|
252
296
|
function WorkbenchFrame() {
|
|
253
297
|
return h("iframe", {
|
|
254
298
|
id: "dsh-deck-workbench-frame",
|
|
299
|
+
ref: (node) => setWorkbenchFrame(node),
|
|
255
300
|
title: "Deckline 工作台",
|
|
256
301
|
src: "/api/deck-studio.app#/home/decks",
|
|
257
302
|
style: { width: "100%", height: "100%", border: 0, display: "block", background: "#faf8f2" },
|
|
@@ -261,9 +306,94 @@ div[data-sidebar-collapsed]{grid-template-columns:0px minmax(0px,1fr) 0px!import
|
|
|
261
306
|
});
|
|
262
307
|
}
|
|
263
308
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
309
|
+
const dockState = { sessionId: "", listeners: new Set() };
|
|
310
|
+
function publishDockSession(sessionId) {
|
|
311
|
+
const next = typeof sessionId === "string" ? sessionId : "";
|
|
312
|
+
if (dockState.sessionId === next) return;
|
|
313
|
+
dockState.sessionId = next;
|
|
314
|
+
for (const listener of dockState.listeners) listener();
|
|
315
|
+
}
|
|
316
|
+
function subscribeDockSession(listener) {
|
|
317
|
+
dockState.listeners.add(listener);
|
|
318
|
+
return () => dockState.listeners.delete(listener);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** 工作台 iframe 元素引用(React ref);Dock 靠它测矩形 / 报「Dock 已就绪」。 */
|
|
322
|
+
const frameState = { el: null, listeners: new Set() };
|
|
323
|
+
function setWorkbenchFrame(node) {
|
|
324
|
+
const next = node ?? null;
|
|
325
|
+
if (frameState.el === next) return;
|
|
326
|
+
frameState.el = next;
|
|
327
|
+
for (const listener of frameState.listeners) listener();
|
|
328
|
+
}
|
|
329
|
+
function subscribeWorkbenchFrame(listener) {
|
|
330
|
+
frameState.listeners.add(listener);
|
|
331
|
+
return () => frameState.listeners.delete(listener);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** 会话内容视图:复用宿主 `conversation.session` 槽(不是复制品)。 */
|
|
335
|
+
function DeckChatView(props) {
|
|
336
|
+
return props.renderSlot("conversation.session", { view: "chat" });
|
|
337
|
+
}
|
|
338
|
+
function DeckChatBody({ useSession, useConversation, renderFactorySlot }) {
|
|
339
|
+
const session = useSession((value) => value);
|
|
340
|
+
const active = useConversation((value) => value).activeTargets;
|
|
341
|
+
const phase = (!session.blank || session.running || active.size > 0) ? "active" : "hero";
|
|
342
|
+
return renderFactorySlot("conversation.content", { variant: "embedded", phase, hero: phase === "hero" }, { slots: { views: DeckChatView } });
|
|
343
|
+
}
|
|
344
|
+
/** Dock:测量工作台对话占位矩形;用 retain 的会话引用喂 SessionProvider。 */
|
|
345
|
+
function DeckChatDock({ SessionProvider, renderSlot }) {
|
|
346
|
+
const sessionId = React.useSyncExternalStore(subscribeDockSession, () => dockState.sessionId);
|
|
347
|
+
// 主区面板卸载/重建时 iframe 元素会换,Dock 必须跟着重算位置,否则切走再切回来就不见了。
|
|
348
|
+
const frameEl = React.useSyncExternalStore(subscribeWorkbenchFrame, () => frameState.el);
|
|
349
|
+
const [rect, setRect] = React.useState(null);
|
|
350
|
+
const [reference, setReference] = React.useState(null);
|
|
351
|
+
React.useEffect(() => {
|
|
352
|
+
const measure = () => {
|
|
353
|
+
let next = null;
|
|
354
|
+
try {
|
|
355
|
+
const outer = frameEl?.getBoundingClientRect?.();
|
|
356
|
+
// 只盖住对话框的内容区(#chatDockArea),把上面的绑定/+/会话菜单那一行留出来。
|
|
357
|
+
const inner = frameEl?.contentDocument?.querySelector("#chatDockArea")?.getBoundingClientRect?.();
|
|
358
|
+
if (outer && inner && outer.width > 0 && inner.width > 4 && inner.height > 4) {
|
|
359
|
+
next = {
|
|
360
|
+
left: Math.round(outer.left + inner.left),
|
|
361
|
+
top: Math.round(outer.top + inner.top),
|
|
362
|
+
width: Math.round(inner.width),
|
|
363
|
+
height: Math.round(inner.height),
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
} catch {
|
|
367
|
+
next = null;
|
|
368
|
+
}
|
|
369
|
+
setRect((prev) => (prev && next && prev.left === next.left && prev.top === next.top && prev.width === next.width && prev.height === next.height) ? prev : next);
|
|
370
|
+
};
|
|
371
|
+
measure();
|
|
372
|
+
if (!frameEl) { setRect(null); return undefined; }
|
|
373
|
+
const timer = setInterval(measure, 800);
|
|
374
|
+
window.addEventListener("resize", measure);
|
|
375
|
+
return () => { clearInterval(timer); window.removeEventListener("resize", measure); };
|
|
376
|
+
}, [frameEl]);
|
|
377
|
+
// 引用放 state(不是 ref):StrictMode 的 effect 重放与 ref 释放会让提交渲染时 ref 为空。
|
|
378
|
+
React.useEffect(() => {
|
|
379
|
+
const sessions = ctx.sessions;
|
|
380
|
+
if (!sessionId || typeof sessions?.retain !== "function") { setReference(null); return undefined; }
|
|
381
|
+
let held = null;
|
|
382
|
+
try { held = sessions.retain(sessionId, { source: WORKBENCH_KEY }); } catch (error) { ctx.logger?.warn?.(`deck-studio: retain failed: ${error?.message ?? error}`); }
|
|
383
|
+
setReference(held ?? null);
|
|
384
|
+
return () => { try { held?.release?.(); } catch { /* 已释放 */ } };
|
|
385
|
+
}, [sessionId]);
|
|
386
|
+
// Dock 已在渲染:告诉工作台 iframe 收起降级卡(#chatHostCard)。
|
|
387
|
+
React.useEffect(() => {
|
|
388
|
+
if (!rect || !reference || !frameEl?.contentWindow) return;
|
|
389
|
+
try { frameEl.contentWindow.postMessage({ type: "dsh-deck-studio:chat-dock" }, "*"); } catch { /* 不可达时降级卡留着 */ }
|
|
390
|
+
}, [rect, reference]);
|
|
391
|
+
if (!SessionProvider || !renderSlot || !reference || !rect) return null;
|
|
392
|
+
return h("div", {
|
|
393
|
+
"data-deck-studio-chat-dock": "",
|
|
394
|
+
style: { position: "fixed", left: `${rect.left}px`, top: `${rect.top}px`, width: `${rect.width}px`, height: `${rect.height}px`, zIndex: 40, display: "flex", flexDirection: "column", minHeight: 0, overflow: "hidden", background: "var(--paper,#faf8f2)", borderRight: "1px solid var(--line,#e8e2d6)", pointerEvents: "auto" },
|
|
395
|
+
}, h(SessionProvider, { session: reference }, renderSlot(CHAT_SLOT, {})));
|
|
396
|
+
}
|
|
267
397
|
// 工作台左栏会把 DSH 原生 Chat 以 `/?decklineChat=1` 嵌进来(同源 iframe)。
|
|
268
398
|
// 那个嵌套的 DSH 实例不再自动打开工作台,否则会层层嵌套(F-68)。
|
|
269
399
|
const chatEmbed = new URLSearchParams(window.location.search).get("decklineChat") === "1";
|
|
@@ -406,7 +536,11 @@ div[data-sidebar-collapsed]{grid-template-columns:0px minmax(0px,1fr) 0px!import
|
|
|
406
536
|
// 不把插件安装目录或 DSH 私有数据目录登记成用户工作区。
|
|
407
537
|
const path = normalize(cwd);
|
|
408
538
|
const internal = /(^|\/)node_modules(\/|$)|(^|\/)\.dsh(\/|$)/.test(path);
|
|
409
|
-
|
|
539
|
+
// F-202B:桌面宿主里 shell 的 process.cwd 是 App 包目录(/Applications/DeepSeek Harness.app)
|
|
540
|
+
// ——把它登记成用户工作区是错的(web 宿主的 cwd 才是启动目录)。这类系统路径视为不可信,
|
|
541
|
+
// 落到 sourceRoot / 默认工作区兜底,而不是新建一个垃圾工作区。
|
|
542
|
+
const systemPath = path === "/" || path.startsWith("/Applications/") || /^(\/System(\/|$)|\/Library(\/|$)|\/private\/var|\/usr\/|\/opt\/|\/Volumes\/)/.test(path);
|
|
543
|
+
if (path && !internal && !systemPath) return register(cwd);
|
|
410
544
|
}
|
|
411
545
|
const sourceRoot = String(data.sourceRoot ?? "").trim();
|
|
412
546
|
const sourceWorkspace = sourceRoot ? containing(sourceRoot) : null;
|
|
@@ -461,7 +595,25 @@ div[data-sidebar-collapsed]{grid-template-columns:0px minmax(0px,1fr) 0px!import
|
|
|
461
595
|
let sessionId = String(data.sessionId ?? "").trim();
|
|
462
596
|
if (data.action === "ensure") sessionId = await ensureConversationSession(data);
|
|
463
597
|
if (!sessionId) throw new Error("没有目标会话");
|
|
464
|
-
|
|
598
|
+
/**
|
|
599
|
+
* F-202B(ADR-0027):`open !== false` = 把会话切到宿主主区(web 内嵌 iframe 的原语义,
|
|
600
|
+
* 桌面「在主区打开」的显式入口);`open:false` = 桌面 Dock 模式——会话已在工作台
|
|
601
|
+
* 左栏原生渲染,只把会话准备好并回 id,不抢宿主主区。会话目录未就绪时短重试
|
|
602
|
+
* (`sessions.retain: unknown session...` 是自愈错误,不是会话丢失)。
|
|
603
|
+
*/
|
|
604
|
+
if ((data.action === "open" || data.action === "ensure") && data.open !== false) {
|
|
605
|
+
const delays = [0, 300, 700, 1500, 2500];
|
|
606
|
+
let lastError = null;
|
|
607
|
+
for (const delay of delays) {
|
|
608
|
+
if (delay) await new Promise((resolve) => setTimeout(resolve, delay));
|
|
609
|
+
try { await uiWorkspace.openSession(sessionId); lastError = null; break; } catch (error) {
|
|
610
|
+
lastError = error;
|
|
611
|
+
if (!/sessions\.retain|unknown session/i.test(String(error?.message ?? error))) throw error;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
if (lastError) throw lastError;
|
|
615
|
+
if (data.takeMain === true && DESKTOP_SHELL) closeWorkbench();
|
|
616
|
+
}
|
|
465
617
|
if (data.prompt && data.action === "ensure" && sessionId !== String(data.sessionId ?? "")) {
|
|
466
618
|
await sendToSession(sessionId, String(data.prompt));
|
|
467
619
|
}
|
|
@@ -475,6 +627,39 @@ div[data-sidebar-collapsed]{grid-template-columns:0px minmax(0px,1fr) 0px!import
|
|
|
475
627
|
return () => window.removeEventListener("message", onMessage);
|
|
476
628
|
}, "deck-studio: conversation bridge");
|
|
477
629
|
|
|
630
|
+
/* 桌面 Dock 的会话源:工作台(iframe)周期性把当前绑定会话报上来。 */
|
|
631
|
+
ctx.effect(() => {
|
|
632
|
+
const onMessage = (event) => {
|
|
633
|
+
const data = event?.data ?? {};
|
|
634
|
+
if (data?.type !== "dsh-deck-studio:chat-session" || typeof data.sessionId !== "string") return;
|
|
635
|
+
if (event.source !== (frameState.el?.contentWindow ?? null)) return;
|
|
636
|
+
publishDockSession(data.sessionId);
|
|
637
|
+
};
|
|
638
|
+
window.addEventListener("message", onMessage);
|
|
639
|
+
return () => window.removeEventListener("message", onMessage);
|
|
640
|
+
}, "deck-studio: chat-session bridge");
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* F-202B:桌面「原生会话 Dock」——shell.overlay 容器 + session 作用域内容槽
|
|
644
|
+
* (multichat / html-deck-studio 同款注册形状,SessionProvider 由宿主注入)。
|
|
645
|
+
* web(http 起源)不用 Dock:工作台 iframe 里的 DSH 对话本来就是好的。
|
|
646
|
+
*/
|
|
647
|
+
if (DESKTOP_SHELL) {
|
|
648
|
+
ctx.effect(
|
|
649
|
+
() => ctx.slots.inject("shell.overlay", () => ctx.slots.register({
|
|
650
|
+
name: "shell.overlay",
|
|
651
|
+
id: `${WORKBENCH_KEY}.chat`,
|
|
652
|
+
order: 58,
|
|
653
|
+
children: { [CHAT_SLOT]: { kind: "single", scope: "session" } },
|
|
654
|
+
}, DeckChatDock)),
|
|
655
|
+
"deck-studio: chat dock",
|
|
656
|
+
);
|
|
657
|
+
ctx.effect(
|
|
658
|
+
() => ctx.slots.inject(CHAT_SLOT, () => ctx.slots.register({ name: CHAT_SLOT }, DeckChatBody)),
|
|
659
|
+
"deck-studio: chat dock body",
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
|
|
478
663
|
if (!chatEmbed) {
|
|
479
664
|
/* 工作台顶栏「☰ DSH」→ 宿主:临时展开 / 收起左侧栏(需要看会话、插件、设置时用)。 */
|
|
480
665
|
ctx.effect(() => {
|
|
@@ -535,17 +720,16 @@ div[data-sidebar-collapsed]{grid-template-columns:0px minmax(0px,1fr) 0px!import
|
|
|
535
720
|
return () => window.clearTimeout(timer);
|
|
536
721
|
}, "deck-studio: auto-open workbench");
|
|
537
722
|
|
|
538
|
-
//
|
|
723
|
+
// 唯一入口:侧栏面板行「Deckline」(与 plugins / schedules 等插件同一种形态,带图标);
|
|
724
|
+
// → 对话用工作台里的「← 回到对话」。
|
|
539
725
|
if (hasRightbar) {
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
ctx.slots.
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
"deck-studio: workbench footer button",
|
|
548
|
-
);
|
|
726
|
+
if (layout?.selectPanel) {
|
|
727
|
+
ctx.effect(
|
|
728
|
+
() => ctx.slots.inject("sidebar.panellist", () =>
|
|
729
|
+
ctx.slots.register({ name: "sidebar.panellist", id: WORKBENCH_KEY, order: 46, label: () => "Deckline" }, DeckGlyph)),
|
|
730
|
+
"deck-studio: workbench sidebar entry",
|
|
731
|
+
);
|
|
732
|
+
}
|
|
549
733
|
ctx.logger?.info?.("deck-studio client: workbench-only mode");
|
|
550
734
|
} else {
|
|
551
735
|
ctx.effect(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-deck-studio",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.14",
|
|
4
4
|
"description": "Deckline:DSH 的 image deck / PPT 工作台插件——原生 Chat + 整页图片 deck 工作区 + 设计风格库;用对话做 PPT、按页出图、版本可追踪、导出 PDF/PPTX。",
|
|
5
5
|
"icon": "assets/icon.svg",
|
|
6
6
|
"homepage": "https://github.com/yuna78/dsh-deck-studio#readme",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
},
|
|
46
46
|
"scripts": {
|
|
47
47
|
"verify": "node verify/smoke.mjs",
|
|
48
|
-
"prepublishOnly": "node verify/smoke.mjs",
|
|
48
|
+
"prepublishOnly": "node -e \"const fs=require('fs');const{execSync}=require('child_process');if(fs.existsSync('verify/smoke.mjs')){execSync('node verify/smoke.mjs',{stdio:'inherit'})}else{console.log('[prepublishOnly] verify/ 不在发布目录(公开包白名单不含),跳过;发布前请在开发仓跑 npm run smoke')}\"",
|
|
49
49
|
"prepack": "node scripts/prepack-assets.mjs"
|
|
50
50
|
},
|
|
51
51
|
"dsh": {
|
|
Binary file
|
|
Binary file
|
package/seeds/docs/install.en.md
CHANGED
|
@@ -43,7 +43,7 @@ node -v && pnpm -v && dsh --version
|
|
|
43
43
|
dsh --profile deckline --from-default-profile web
|
|
44
44
|
|
|
45
45
|
# 2) install the plugin; dsh plugin add adds the bundle to the profile and enables it
|
|
46
|
-
dsh plugin --profile deckline add dsh-plugin-deck-studio@0.2.
|
|
46
|
+
dsh plugin --profile deckline add dsh-plugin-deck-studio@0.2.13
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
> Do not skip step 1: the `deckline` profile is **never created automatically**. Running
|
|
@@ -181,7 +181,7 @@ From zero to 13090, run these three commands in order (options for steps 1–2 a
|
|
|
181
181
|
dsh --profile deckline --from-default-profile web
|
|
182
182
|
|
|
183
183
|
# 2) install the plugin (adds it to that profile and enables it)
|
|
184
|
-
dsh plugin --profile deckline add dsh-plugin-deck-studio@0.2.
|
|
184
|
+
dsh plugin --profile deckline add dsh-plugin-deck-studio@0.2.13
|
|
185
185
|
|
|
186
186
|
# 3) start
|
|
187
187
|
dsh --profile deckline --port 13090
|
package/seeds/docs/install.md
CHANGED
|
@@ -45,7 +45,7 @@ node -v && pnpm -v && dsh --version
|
|
|
45
45
|
dsh --profile deckline --from-default-profile web
|
|
46
46
|
|
|
47
47
|
# 2) 装插件;dsh plugin add 会自动把组合包加入 profile 并默认启用
|
|
48
|
-
dsh plugin --profile deckline add dsh-plugin-deck-studio@0.2.
|
|
48
|
+
dsh plugin --profile deckline add dsh-plugin-deck-studio@0.2.13
|
|
49
49
|
```
|
|
50
50
|
|
|
51
51
|
> 第 1 步不能省:`deckline` profile **不会**被自动创建。跳过第 1 步直接 `dsh --profile deckline --port 13090`,
|
|
@@ -54,7 +54,7 @@ dsh plugin --profile deckline add dsh-plugin-deck-studio@0.2.12
|
|
|
54
54
|
|
|
55
55
|
> 国内网络直连 `registry.npmjs.org` 可能很慢(20 MB 包实测约 19 KB/s,要十几分钟)。换镜像会快很多:
|
|
56
56
|
> ```sh
|
|
57
|
-
> npm_config_registry=https://registry.npmmirror.com dsh plugin --profile deckline add dsh-plugin-deck-studio@0.2.
|
|
57
|
+
> npm_config_registry=https://registry.npmmirror.com dsh plugin --profile deckline add dsh-plugin-deck-studio@0.2.13
|
|
58
58
|
> ```
|
|
59
59
|
> 或一次性 `npm config set registry https://registry.npmmirror.com`。
|
|
60
60
|
|
|
@@ -75,7 +75,7 @@ DSH 目前没有官方插件市场,市场是「索引源」模式:添加下
|
|
|
75
75
|
https://raw.githubusercontent.com/yuna78/dsh-deck-studio/main/market-index.json
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
-
3. 搜索 `Deckline` 或 `PPT` → **安装**,会安装 `dsh-plugin-deck-studio@0.2.
|
|
78
|
+
3. 搜索 `Deckline` 或 `PPT` → **安装**,会安装 `dsh-plugin-deck-studio@0.2.13`(索引里声明了国内镜像 registry)。
|
|
79
79
|
4. 之后同 A 的第 3 步启动。
|
|
80
80
|
|
|
81
81
|
索引文件就是仓库根目录的 [`market-index.json`](../market-index.json)(格式 1),新版本发布后更新版本号即可。
|
|
@@ -210,7 +210,7 @@ profile "deckline" does not exist
|
|
|
210
210
|
dsh --profile deckline --from-default-profile web
|
|
211
211
|
|
|
212
212
|
# 2) 装插件(自动加进该 profile 并启用)
|
|
213
|
-
dsh plugin --profile deckline add dsh-plugin-deck-studio@0.2.
|
|
213
|
+
dsh plugin --profile deckline add dsh-plugin-deck-studio@0.2.13
|
|
214
214
|
|
|
215
215
|
# 3) 启动
|
|
216
216
|
dsh --profile deckline --port 13090
|
package/ui/workbench.css
CHANGED
|
@@ -238,6 +238,44 @@ body.resizing{user-select:none;cursor:col-resize}
|
|
|
238
238
|
.hn-label{flex:1}
|
|
239
239
|
.home-nav-item em{font-style:normal;font-size:10px;color:#93a0a3;background:#fffdf8;border:1px solid var(--line);border-radius:9px;padding:0 5px}
|
|
240
240
|
.home-nav-item.active em{color:var(--teal);border-color:#bfe0dc}
|
|
241
|
+
/* F-193/F-194:左栏「关于」单入口(版本 chip + 有新版小圆点) */
|
|
242
|
+
.home-nav-sep{height:1px;background:var(--line);margin:10px 4px 0}
|
|
243
|
+
.hn-dot{width:6px;height:6px;border-radius:50%;background:var(--gold);box-shadow:0 0 0 3px color-mix(in srgb,var(--gold) 18%,transparent)}
|
|
244
|
+
.hn-dot[hidden]{display:none}
|
|
245
|
+
/* F-194:「关于」单页——从上到下:作者 → 公众号 → 版本 → 开源 → 其他项目 */
|
|
246
|
+
.about-stack{display:flex;flex-direction:column;gap:12px;max-width:880px}
|
|
247
|
+
.about-card{border:1px solid var(--line);background:#fffdf8;border-radius:10px;padding:16px 18px;min-width:0}
|
|
248
|
+
.about-card h3{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap;margin:0 0 8px;font-family:"Noto Serif SC",Georgia,serif;font-size:16px;color:var(--ink)}
|
|
249
|
+
.about-role{font-family:Inter,-apple-system,sans-serif;font-size:10.5px;font-weight:400;color:#93a0a3;letter-spacing:.02em}
|
|
250
|
+
.about-hero{display:flex;flex-direction:column;gap:14px;background:linear-gradient(180deg,#fffdf8 0%,#faf5e9 100%)}
|
|
251
|
+
.about-profile{display:flex;gap:14px;align-items:center;min-width:0}
|
|
252
|
+
.about-wechat{display:flex;align-items:center;gap:14px;border-top:1px solid var(--line);padding-top:13px}
|
|
253
|
+
.about-wechat-text{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
|
|
254
|
+
.about-wechat b{font-size:13px;color:var(--teal);line-height:1.5}
|
|
255
|
+
.about-wechat .eyebrow{color:#93a0a3}
|
|
256
|
+
.about-wechat .about-hint{margin:2px 0 0}
|
|
257
|
+
.about-wechat .about-qr{flex:0 0 auto;width:104px;max-width:104px;border-radius:7px}
|
|
258
|
+
.about-avatar{flex:0 0 auto;width:58px;height:58px;border-radius:14px;display:flex;align-items:center;justify-content:center;font-family:"Noto Serif SC",Georgia,serif;font-size:24px;color:#fffdf8;background:linear-gradient(145deg,#0e5563,#08343d);box-shadow:inset 0 0 0 1px #ffffff22}
|
|
259
|
+
.about-profile-body{min-width:0;max-width:560px}
|
|
260
|
+
.about-desc{margin:0 0 8px;color:#5d7276;font-size:12px;line-height:1.65}
|
|
261
|
+
.about-line{margin:0 0 4px;color:#728086;font-size:11.5px}
|
|
262
|
+
.about-line b{color:var(--teal)}
|
|
263
|
+
.about-line code{font-size:11px;color:#5d7276;background:#f3efe4;border:1px solid var(--line);border-radius:5px;padding:1px 5px}
|
|
264
|
+
.about-hint{margin:8px 0 0;color:#93a0a3;font-size:10.5px;line-height:1.6;word-break:break-word}
|
|
265
|
+
.about-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px}
|
|
266
|
+
.about-actions .btn{font-size:11px}
|
|
267
|
+
.about-state{margin:8px 0 0;color:var(--gold)}
|
|
268
|
+
.about-qr{display:block;width:140px;max-width:140px;aspect-ratio:1;object-fit:contain;border:1px solid var(--line);border-radius:8px;background:#fff}
|
|
269
|
+
.about-qr-empty{display:flex;align-items:center;justify-content:center;width:140px;aspect-ratio:1;color:#93a0a3;font-size:11px;border:1px dashed var(--line);border-radius:8px;background:#f8f5ed;text-align:center;padding:8px}
|
|
270
|
+
.about-project-list{list-style:none;margin:2px 0 0;padding:0}
|
|
271
|
+
.about-project-row{display:flex;gap:14px;align-items:flex-start;justify-content:space-between;padding:12px 0;border-top:1px solid var(--line)}
|
|
272
|
+
.about-project-row:first-child{border-top:0;padding-top:2px}
|
|
273
|
+
.about-project-main{flex:1;min-width:0}
|
|
274
|
+
.about-project-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
275
|
+
.about-project-head b{font-size:13px;color:var(--ink)}
|
|
276
|
+
.about-badge{flex:0 0 auto;font-size:10px;color:var(--teal);background:#e2f1ef;border:1px solid #bfe0dc;border-radius:9px;padding:1px 7px}
|
|
277
|
+
.about-project-row .about-desc{margin:4px 0 0}
|
|
278
|
+
.about-project-row .btn{flex:0 0 auto}
|
|
241
279
|
.home-main{padding:26px 32px;overflow-y:auto;min-width:0}
|
|
242
280
|
.home-greet{padding-bottom:16px;border-bottom:1px solid var(--line);margin-bottom:18px}
|
|
243
281
|
.home-greet h1{margin:5px 0 4px;font-family:"Noto Serif SC",Georgia,serif;font-size:30px;color:var(--ink);letter-spacing:.01em}
|
|
@@ -441,6 +479,15 @@ body.has-chat .layout{grid-template-columns:var(--nav-w,430px) 5px minmax(0,1fr)
|
|
|
441
479
|
.chat-session-item span{font-size:9.5px;color:#8a9599}
|
|
442
480
|
.chat-session-empty{margin:6px 8px 8px;font-size:10.5px;color:#8a9599;line-height:1.6}
|
|
443
481
|
#nativeChatFrame{flex:1;min-height:0;width:100%;border:0;background:var(--paper)}
|
|
482
|
+
/* F-202B:桌面宿主(dsh-app://app)不嵌 DSH 页面——工作台对话列留一个 Dock 占位,
|
|
483
|
+
* 宿主用 shell.overlay 把原生会话精确盖在这里;原生 Dock 不可达时回退 #chatHostCard。 */
|
|
484
|
+
#chatDockArea{flex:1;min-height:0;background:var(--paper)}
|
|
485
|
+
#chatDockArea[hidden]{display:none}
|
|
486
|
+
#chatDockArea:empty::before{content:"还没有绑定会话——点上方 + 新建";margin:44px auto;display:block;width:80%;text-align:center;color:#8a9599;font-size:11px}
|
|
487
|
+
#chatHostCard{flex:1;min-height:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;padding:26px 20px;overflow:auto}
|
|
488
|
+
#chatHostCard[hidden]{display:none}
|
|
489
|
+
#chatHostCard .chat-host-hint{margin:0;font-size:11px;line-height:1.7;color:var(--ink);opacity:.78;text-align:center;max-width:30em}
|
|
490
|
+
#chatHostCard .btn{min-width:180px;text-align:center}
|
|
444
491
|
|
|
445
492
|
/* 叙事结构概览(脚本页顶部,从 script 派生) */
|
|
446
493
|
.story-overview{margin:10px 0 18px;border:1px solid var(--line);border-radius:7px;background:#f8f6f0;padding:10px 14px}
|
package/ui/workbench.html
CHANGED
|
@@ -134,6 +134,11 @@
|
|
|
134
134
|
<button class="home-nav-item" data-section="resources"><span class="hn-icon">◇</span><span class="hn-label">资源库</span><em id="navCountResources">0</em></button>
|
|
135
135
|
<button class="home-nav-item" data-section="conversations"><span class="hn-icon">☰</span><span class="hn-label">Conversations</span><em id="navCountConvs">0</em></button>
|
|
136
136
|
<button class="home-nav-item" data-section="manual"><span class="hn-icon">?</span><span class="hn-label">使用手册</span></button>
|
|
137
|
+
<!-- F-193/F-194:关于只留一页入口;版本 chip + 有新版小圆点仍常驻,升级入口不藏在顶栏图标里 -->
|
|
138
|
+
<div class="home-nav-sep" aria-hidden="true"></div>
|
|
139
|
+
<button class="home-nav-item" data-section="about" id="aboutNavBtn" title="关于 Deckline:版本 / 公众号 / 开源地址 / 其他项目">
|
|
140
|
+
<span class="hn-icon">☺</span><span class="hn-label">关于</span><em id="navUpdateVer">—</em><i class="hn-dot" id="navUpdateDot" hidden></i>
|
|
141
|
+
</button>
|
|
137
142
|
</nav>
|
|
138
143
|
</aside>
|
|
139
144
|
<main class="home-main">
|
|
@@ -281,6 +286,11 @@
|
|
|
281
286
|
</div>
|
|
282
287
|
<div class="chat-session-menu" id="chatSessionMenu" hidden></div>
|
|
283
288
|
</div>
|
|
289
|
+
<div id="chatDockArea" hidden title="这里由宿主渲染原生会话(桌面 Dock)"></div>
|
|
290
|
+
<div id="chatHostCard" hidden>
|
|
291
|
+
<p class="chat-host-hint">桌面 App 里内嵌 DSH 页面无法启动(子帧拿不到 boot 注入)。宿主原生会话 Dock 没有就绪——对话将<b>在宿主主区</b>打开,点 ☰ 或左侧栏 Deckline 行切回工作台。</p>
|
|
292
|
+
<button type="button" class="btn alt" id="chatHostOpen">在宿主主区打开对话 →</button>
|
|
293
|
+
</div>
|
|
284
294
|
<iframe id="nativeChatFrame" title="DSH 原生对话"></iframe>
|
|
285
295
|
</aside>
|
|
286
296
|
<div class="splitter" data-split="nav" title="拖动调整对话栏宽度" hidden></div>
|
|
@@ -578,6 +588,14 @@ let SETTINGS_FORM_DIRTY = false;
|
|
|
578
588
|
|
|
579
589
|
const STATE = { sid: null, sessionTitle: "", data: null, selected: null, running: false, demo: false, tab: "deck", entityKind: "", sig: "", docCache: {}, docLoading: null, view: "home", deckId: null, rightTab: "", sessions: [], editorPage: null, ndStyleAbbr: "", designCards: [] };
|
|
580
590
|
const EMBED = new URLSearchParams(location.search).get("embed") === "1";
|
|
591
|
+
/**
|
|
592
|
+
* F-202B:桌面宿主(自定义协议 `dsh-app://app`)的 DSH 子帧拿不到 boot 注入(`__DSH_BOOT_READY__`
|
|
593
|
+
* 只注入主帧),流式端点也在子帧 404 —— `/?decklineChat=1` 嵌套实例永远停在「Loading plugins…」。
|
|
594
|
+
* 桌面上对话改走宿主原生(client.js 的 conversation 桥 → 宿主 uiWorkspace.openSession,会话开在宿主主区);
|
|
595
|
+
* web 版(http 起源,如 13090 / 3080)保持 iframe 内嵌不动。
|
|
596
|
+
*/
|
|
597
|
+
const DESKTOP_HOST = !/^(https?:)$/.test(location.protocol)
|
|
598
|
+
|| new URLSearchParams(location.search).get("chatHost") === "panel";
|
|
581
599
|
|
|
582
600
|
const esc = (v) => String(v ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
|
|
583
601
|
const getEl = (id) => document.getElementById(id);
|
|
@@ -1449,8 +1467,8 @@ async function copyDataDirDiagnostics() {
|
|
|
1449
1467
|
|
|
1450
1468
|
/* ---------------- 首页(左侧导航 + 右侧分区 blocks) ---------------- */
|
|
1451
1469
|
|
|
1452
|
-
const HOME_SECTIONS = ["decks", "designs", "resources", "conversations", "manual"];
|
|
1453
|
-
const HOME_TITLES = { decks: "Decks", designs: "Designs", resources: "资源库", conversations: "Conversations" };
|
|
1470
|
+
const HOME_SECTIONS = ["decks", "designs", "resources", "conversations", "manual", "about"];
|
|
1471
|
+
const HOME_TITLES = { decks: "Decks", designs: "Designs", resources: "资源库", conversations: "Conversations", manual: "使用手册", about: "关于" };
|
|
1454
1472
|
|
|
1455
1473
|
function homeDateText() {
|
|
1456
1474
|
const now = new Date();
|
|
@@ -1531,6 +1549,7 @@ function renderHome() {
|
|
|
1531
1549
|
resources: `${resources.length} 个资源(logo / 素材);点开预览。`,
|
|
1532
1550
|
conversations: `${sessions.length} 个历史会话;点一个继续聊。`,
|
|
1533
1551
|
manual: "从新建到导出:截图 + 步骤;找不到就直接在 Chat 里问。",
|
|
1552
|
+
about: "作者 / 公众号 / 版本更新 / 开源地址 / 其他项目,都在这一页。",
|
|
1534
1553
|
}[section];
|
|
1535
1554
|
getEl("homeGreetSub").textContent = greet;
|
|
1536
1555
|
|
|
@@ -1550,8 +1569,22 @@ function renderHome() {
|
|
|
1550
1569
|
if (STATE.manualDoc) openManualDoc(box, STATE.manualDoc);
|
|
1551
1570
|
else renderManualHome(box);
|
|
1552
1571
|
}
|
|
1572
|
+
else if (section === "about") {
|
|
1573
|
+
box.innerHTML = renderHomeAbout(resources);
|
|
1574
|
+
paintUpdate(); // 版本 chip / 更新状态在关于页也要跟着顶栏走(F-193)
|
|
1575
|
+
}
|
|
1553
1576
|
else box.innerHTML = renderHomeConversations(sessions, bindings);
|
|
1554
1577
|
|
|
1578
|
+
// F-193:「关于」分区里的按钮(检查更新 / 外链)——外链直接开新标签。
|
|
1579
|
+
box.querySelectorAll("[data-about-check]").forEach((node) => node.addEventListener("click", () => checkPluginUpdate()));
|
|
1580
|
+
box.querySelectorAll("[data-about-external]").forEach((node) => node.addEventListener("click", () => {
|
|
1581
|
+
const links = aboutLinks();
|
|
1582
|
+
const kind = node.dataset.aboutExternal;
|
|
1583
|
+
if (kind === "repo") openExternalUrl(links.repo, "还没拿到仓库地址");
|
|
1584
|
+
else if (kind === "issues") openExternalUrl(links.issues, "还没拿到提建议地址");
|
|
1585
|
+
else if (kind === "profile") openExternalUrl(links.profile, "还没拿到 GitHub 主页地址");
|
|
1586
|
+
}));
|
|
1587
|
+
|
|
1555
1588
|
box.querySelectorAll("[data-home-deck]").forEach((node) => node.addEventListener("click", async () => {
|
|
1556
1589
|
const deckId = node.dataset.homeDeck;
|
|
1557
1590
|
// 先等 deck 切完(iframe 里的 Chat 也要加载完),再选默认会话——否则 postMessage 发早了会落到旧会话。
|
|
@@ -1711,6 +1744,90 @@ function renderHomeResources(resources) {
|
|
|
1711
1744
|
</button>`).join("")}</div>`;
|
|
1712
1745
|
}
|
|
1713
1746
|
|
|
1747
|
+
/* ---------------- F-193 / F-194:首页「关于」单页(作者 → 公众号 → 版本 → 开源 → 其他项目) ---------------- */
|
|
1748
|
+
|
|
1749
|
+
/** 公众号二维码从资源库现取(个人二维码不进仓库);拿不到就降级成说明,不显示破图。 */
|
|
1750
|
+
function aboutWechatQr(resources = []) {
|
|
1751
|
+
return resources.find((resource) => /二维码|qr|微信|wechat/i.test(`${resource?.name || ""} ${resource?.description || ""} ${resource?.url || ""}`)) || null;
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
function renderHomeAbout(resources = []) {
|
|
1755
|
+
const links = aboutLinks();
|
|
1756
|
+
const qr = aboutWechatQr(resources);
|
|
1757
|
+
const versionLine = UPDATE.current ? `v${UPDATE.current}` : "读取中…";
|
|
1758
|
+
const installNote = UPDATE.installMode === "linked"
|
|
1759
|
+
? "本机是开发链接安装(link:/file:):更新走仓库,界面不执行 pnpm。"
|
|
1760
|
+
: UPDATE.installMode === "npm"
|
|
1761
|
+
? "npm 安装:有新版可以在这里直接更新,重启 DSH 生效。"
|
|
1762
|
+
: "没识别出安装方式时,可手动 dsh plugin add。";
|
|
1763
|
+
const repoShort = links.repo.replace(/^https?:\/\//, "");
|
|
1764
|
+
const projectRows = [
|
|
1765
|
+
{ name: "Deckline · dsh-deck-studio", badge: "本插件", external: "repo", desc: "DSH 的 image deck / PPT 工作台:故事线、逐页 prompt、AI 出图与版本留存、风格库、导出 PDF/PPTX。" },
|
|
1766
|
+
];
|
|
1767
|
+
if (links.profile) {
|
|
1768
|
+
projectRows.push({
|
|
1769
|
+
name: `GitHub @${links.profile.split("/").pop()}`,
|
|
1770
|
+
badge: "全部项目",
|
|
1771
|
+
external: "profile",
|
|
1772
|
+
desc: "我的开源主页:插件、实验和工具会陆续挂在这里。",
|
|
1773
|
+
});
|
|
1774
|
+
}
|
|
1775
|
+
return `<div class="section-head"><h2>关于</h2><span class="small">作者 · 公众号 · 版本更新 · 开源</span></div>
|
|
1776
|
+
<div class="about-stack">
|
|
1777
|
+
<section class="about-card about-hero">
|
|
1778
|
+
<div class="about-profile">
|
|
1779
|
+
<div class="about-avatar" aria-hidden="true">Y</div>
|
|
1780
|
+
<div class="about-profile-body">
|
|
1781
|
+
<h3>${esc(ABOUT.author)}<span class="about-role">${esc(ABOUT.role)}</span></h3>
|
|
1782
|
+
<p class="about-desc">${esc(ABOUT.intro)}</p>
|
|
1783
|
+
</div>
|
|
1784
|
+
</div>
|
|
1785
|
+
<div class="about-wechat">
|
|
1786
|
+
<div class="about-wechat-text">
|
|
1787
|
+
<div class="eyebrow">微信公众号</div>
|
|
1788
|
+
<b>${esc(ABOUT.account)}</b>
|
|
1789
|
+
<p class="about-hint">${qr ? "扫码关注 / 加我微信" : `把二维码图片放进数据目录的 <code>resources/</code>(资源库),这里会自动显示。`}</p>
|
|
1790
|
+
</div>
|
|
1791
|
+
${qr
|
|
1792
|
+
? `<img class="about-qr" src="${esc(qr.url)}" alt="${esc(qr.name || "公众号二维码")}" loading="lazy">`
|
|
1793
|
+
: `<div class="about-qr about-qr-empty">二维码未收录</div>`}
|
|
1794
|
+
</div>
|
|
1795
|
+
</section>
|
|
1796
|
+
|
|
1797
|
+
<section class="about-card">
|
|
1798
|
+
<h3>版本与更新</h3>
|
|
1799
|
+
<p class="about-line">当前版本 <b>${esc(versionLine)}</b></p>
|
|
1800
|
+
<p class="about-desc">${esc(installNote)}</p>
|
|
1801
|
+
<div class="about-actions">
|
|
1802
|
+
<button class="btn" data-about-check>检查新版本</button>
|
|
1803
|
+
</div>
|
|
1804
|
+
<p class="small about-state" data-about-state></p>
|
|
1805
|
+
</section>
|
|
1806
|
+
<section class="about-card">
|
|
1807
|
+
<h3>开源地址与反馈</h3>
|
|
1808
|
+
<p class="about-line"><code>${esc(repoShort)}</code></p>
|
|
1809
|
+
<p class="about-desc">用着不对、想要什么功能,直接提 issue;仓库里能看到版本记录与路线。</p>
|
|
1810
|
+
<div class="about-actions">
|
|
1811
|
+
<button class="btn ghost" data-about-external="repo">仓库地址</button>
|
|
1812
|
+
<button class="btn alt" data-about-external="issues">提交建议 / 问题</button>
|
|
1813
|
+
</div>
|
|
1814
|
+
</section>
|
|
1815
|
+
<section class="about-card">
|
|
1816
|
+
<h3>其他开源项目</h3>
|
|
1817
|
+
<ul class="about-project-list">${projectRows.map((row) => `
|
|
1818
|
+
<li class="about-project-row">
|
|
1819
|
+
<div class="about-project-main">
|
|
1820
|
+
<div class="about-project-head"><b>${esc(row.name)}</b><span class="about-badge">${esc(row.badge)}</span></div>
|
|
1821
|
+
<p class="about-desc">${esc(row.desc)}</p>
|
|
1822
|
+
</div>
|
|
1823
|
+
<button class="btn ghost" data-about-external="${esc(row.external)}">打开</button>
|
|
1824
|
+
</li>`).join("")}
|
|
1825
|
+
</ul>
|
|
1826
|
+
<p class="about-hint">最新列表以 GitHub 主页为准;想第一时间看到更新,关注公众号「${esc(ABOUT.account)}」。</p>
|
|
1827
|
+
</section>
|
|
1828
|
+
</div>`;
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1714
1831
|
function renderHomeConversations(sessions, bindings = []) {
|
|
1715
1832
|
const bound = new Map((bindings || []).map((binding) => [binding.sessionId, binding]));
|
|
1716
1833
|
const targetLabel = (binding) => {
|
|
@@ -1813,19 +1930,55 @@ function dlToast(text, ms = 4200) {
|
|
|
1813
1930
|
/** F-188:更新状态(版本 / 安装方式 / 仓库链接)由 workbench payload 带下来。 */
|
|
1814
1931
|
const UPDATE = { current: "", latest: "", hasUpdate: false, installMode: "", repo: "", suggest: "", note: "" };
|
|
1815
1932
|
|
|
1933
|
+
/** F-193:左栏「关于」分组用的作者 / 公众号信息;仓库链接优先用包里的 repository,兜底写死公开地址。 */
|
|
1934
|
+
const ABOUT = {
|
|
1935
|
+
author: "Yuna",
|
|
1936
|
+
role: "Deckline 作者",
|
|
1937
|
+
account: "Yuna 的 AI 修炼手册",
|
|
1938
|
+
intro: "把 AI 落到工作现场:用对话把分享、培训、汇报做成整页图片 deck。",
|
|
1939
|
+
fallbackRepo: "https://github.com/yuna78/dsh-deck-studio",
|
|
1940
|
+
};
|
|
1941
|
+
|
|
1942
|
+
function aboutLinks() {
|
|
1943
|
+
const repo = UPDATE.repo || ABOUT.fallbackRepo;
|
|
1944
|
+
const issues = UPDATE.suggest || UPDATE.issues || `${repo.replace(/\/+$/, "")}/issues/new/choose`;
|
|
1945
|
+
const profile = repo.replace(/^https?:\/\/(?:www\.)?github\.com\/([^/]+).*$/i, "https://github.com/$1");
|
|
1946
|
+
return { repo, issues, profile: profile === repo ? "" : profile };
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
/** 外链统一走这里:拿不到地址时给 toast,不让按钮静默没反应。 */
|
|
1950
|
+
function openExternalUrl(url, missingMessage) {
|
|
1951
|
+
if (!url) {
|
|
1952
|
+
dlToast(missingMessage || "链接还没拿到:插件包或运行环境里没有配置地址", 5000);
|
|
1953
|
+
return;
|
|
1954
|
+
}
|
|
1955
|
+
window.open(url, "_blank", "noopener");
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1816
1958
|
function paintUpdate() {
|
|
1817
1959
|
const ver = getEl("updateVer");
|
|
1818
1960
|
if (ver) ver.textContent = UPDATE.current ? `v${UPDATE.current}` : "—";
|
|
1819
1961
|
const dot = getEl("updateDot");
|
|
1820
1962
|
if (dot) dot.hidden = !UPDATE.hasUpdate;
|
|
1963
|
+
const title = UPDATE.hasUpdate
|
|
1964
|
+
? `有新版 v${UPDATE.latest}(当前 v${UPDATE.current})`
|
|
1965
|
+
: UPDATE.installMode === "linked"
|
|
1966
|
+
? "本机是开发链接安装:更新走 git pull"
|
|
1967
|
+
: "检查更新";
|
|
1821
1968
|
const btn = getEl("updateBtn");
|
|
1822
|
-
if (btn)
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1969
|
+
if (btn) btn.title = title;
|
|
1970
|
+
// F-193:左栏「检查新版本」与顶栏 ↻ 共用同一份状态(版本 chip + 有新版小圆点)
|
|
1971
|
+
const navVer = getEl("navUpdateVer");
|
|
1972
|
+
if (navVer) navVer.textContent = UPDATE.current ? `v${UPDATE.current}` : "—";
|
|
1973
|
+
const navDot = getEl("navUpdateDot");
|
|
1974
|
+
if (navDot) navDot.hidden = !UPDATE.hasUpdate;
|
|
1975
|
+
const aboutNavBtn = getEl("aboutNavBtn");
|
|
1976
|
+
if (aboutNavBtn) aboutNavBtn.title = UPDATE.hasUpdate ? `关于 Deckline · ${title}` : "关于 Deckline:版本 / 公众号 / 开源地址 / 其他项目";
|
|
1977
|
+
const aboutState = document.querySelectorAll("[data-about-state]");
|
|
1978
|
+
aboutState.forEach((node) => {
|
|
1979
|
+
// 只在「有新版」时占用这行;「当前版本」上面已经写过了,别重复。
|
|
1980
|
+
node.textContent = UPDATE.hasUpdate ? `发现新版 v${UPDATE.latest}(当前 v${UPDATE.current})` : "";
|
|
1981
|
+
});
|
|
1829
1982
|
}
|
|
1830
1983
|
|
|
1831
1984
|
function absorbUpdate(payload) {
|
|
@@ -4758,6 +4911,7 @@ function decorateNativeChat(doc) {
|
|
|
4758
4911
|
|
|
4759
4912
|
function setupNativeChat() {
|
|
4760
4913
|
if (EMBED) return; // embed 模式下宿主 shell 本身就有原生 Chat
|
|
4914
|
+
if (DESKTOP_HOST) return setupHostChatColumn(); // F-202B:桌面不嵌 DSH 页面
|
|
4761
4915
|
const column = getEl("chatCol");
|
|
4762
4916
|
const frame = getEl("nativeChatFrame");
|
|
4763
4917
|
const splitter = document.querySelector('.splitter[data-split="nav"]');
|
|
@@ -4797,6 +4951,92 @@ function setupNativeChat() {
|
|
|
4797
4951
|
});
|
|
4798
4952
|
}
|
|
4799
4953
|
|
|
4954
|
+
/**
|
|
4955
|
+
* 桌面(dsh-app://app)的对话列:不嵌 DSH 页面,把头部保留成「绑定 + 会话切换」,
|
|
4956
|
+
* 动作经宿主桥(client.js)在宿主主区打开原生会话;打开成功后关闭工作台面板。
|
|
4957
|
+
*/
|
|
4958
|
+
function setupHostChatColumn() {
|
|
4959
|
+
const column = getEl("chatCol");
|
|
4960
|
+
const frame = getEl("nativeChatFrame");
|
|
4961
|
+
const splitter = document.querySelector('.splitter[data-split="nav"]');
|
|
4962
|
+
if (!column || !frame) return;
|
|
4963
|
+
column.hidden = false;
|
|
4964
|
+
if (splitter) splitter.hidden = false;
|
|
4965
|
+
document.body.classList.add("has-chat");
|
|
4966
|
+
frame.hidden = true;
|
|
4967
|
+
getEl("chatDockArea").hidden = false;
|
|
4968
|
+
STATE.dockActive = false;
|
|
4969
|
+
// 降级卡:宿主原生 Dock 没接上(少见;比如 shell.overlay 契约缺失)时,退回「在主区打开」。
|
|
4970
|
+
const fallbackTimer = setTimeout(() => {
|
|
4971
|
+
if (!STATE.dockActive) getEl("chatHostCard").hidden = false;
|
|
4972
|
+
}, 6000);
|
|
4973
|
+
// 心跳把当前绑定会话报给宿主:Dock 据此用原生会话盖住 #chatDockArea。
|
|
4974
|
+
let lastPublished = "§init§";
|
|
4975
|
+
const publishChatSession = () => {
|
|
4976
|
+
const sessionId = currentChatSessionId();
|
|
4977
|
+
if (sessionId === lastPublished) return;
|
|
4978
|
+
lastPublished = sessionId;
|
|
4979
|
+
try { window.parent?.postMessage({ type: "dsh-deck-studio:chat-session", sessionId }, "*"); } catch { /* 宿主桥不可用 */ }
|
|
4980
|
+
};
|
|
4981
|
+
setInterval(publishChatSession, 1200);
|
|
4982
|
+
try { window.parent?.postMessage({ type: "dsh-deck-studio:chat-session", sessionId: lastPublished = currentChatSessionId() }, "*"); } catch { /* 同上 */ }
|
|
4983
|
+
window.addEventListener("message", (event) => {
|
|
4984
|
+
if (event.source !== window.parent) return;
|
|
4985
|
+
if (event.data?.type !== "dsh-deck-studio:chat-dock") return;
|
|
4986
|
+
STATE.dockActive = true;
|
|
4987
|
+
getEl("chatHostCard").hidden = true;
|
|
4988
|
+
clearInterval(fallbackTimer);
|
|
4989
|
+
});
|
|
4990
|
+
getEl("chatBindChip").addEventListener("click", () => {
|
|
4991
|
+
if (!getEl("chatBindChip").classList.contains("unbound")) return;
|
|
4992
|
+
bindCurrentChatToTarget().catch(() => {});
|
|
4993
|
+
});
|
|
4994
|
+
getEl("newChatBtn").addEventListener("click", () => {
|
|
4995
|
+
newChatForTarget().catch(() => {});
|
|
4996
|
+
});
|
|
4997
|
+
// ↗(在 DSH 里打开完整对话):桌面=显式把会话开到宿主主区;web=开独立窗口。
|
|
4998
|
+
getEl("expandChatBtn").addEventListener("click", () => takeMainAreaOnHost());
|
|
4999
|
+
getEl("chatHostOpen").addEventListener("click", () => takeMainAreaOnHost());
|
|
5000
|
+
getEl("chatSessionBtn").addEventListener("click", (event) => {
|
|
5001
|
+
event.stopPropagation();
|
|
5002
|
+
toggleChatSessionMenu();
|
|
5003
|
+
});
|
|
5004
|
+
document.addEventListener("click", (event) => {
|
|
5005
|
+
const menu = getEl("chatSessionMenu");
|
|
5006
|
+
if (menu.hidden) return;
|
|
5007
|
+
if (menu.contains(event.target) || event.target.closest?.("#chatSessionBtn")) return;
|
|
5008
|
+
menu.hidden = true;
|
|
5009
|
+
});
|
|
5010
|
+
document.addEventListener("keydown", (event) => {
|
|
5011
|
+
if (event.key === "Escape") getEl("chatSessionMenu").hidden = true;
|
|
5012
|
+
});
|
|
5013
|
+
document.body.classList.add("desktop-host");
|
|
5014
|
+
refreshSessions().then(renderChatHeader).catch(() => renderChatHeader());
|
|
5015
|
+
}
|
|
5016
|
+
|
|
5017
|
+
/** 桌面:显式把会话开到宿主主区(优先当前会话,其次最近绑定,再不行 ensure 一个)。
|
|
5018
|
+
* 成功后宿主关闭工作台面板——这是唯一的「跳出工作台」路径。 */
|
|
5019
|
+
async function takeMainAreaOnHost() {
|
|
5020
|
+
const target = activeChatTarget();
|
|
5021
|
+
const current = currentChatSessionId();
|
|
5022
|
+
const target_ = target;
|
|
5023
|
+
const bound = target_ ? chatSessionsForTarget(target_) : [];
|
|
5024
|
+
const sessionId = current || bound[0]?.sessionId || "";
|
|
5025
|
+
return requestConversation({
|
|
5026
|
+
action: sessionId ? "open" : "ensure",
|
|
5027
|
+
sessionId,
|
|
5028
|
+
kind: target_?.kind,
|
|
5029
|
+
id: target_?.id,
|
|
5030
|
+
title: target_?.label,
|
|
5031
|
+
takeMain: true,
|
|
5032
|
+
prompt: sessionId
|
|
5033
|
+
? undefined
|
|
5034
|
+
: target_
|
|
5035
|
+
? `本对话绑定 ${target_.kind === "deck" ? `deck ${target_.id}(${target_.label})` : `风格设计 ${target_.label}`}。等我的指令,不要直接出图。`
|
|
5036
|
+
: undefined,
|
|
5037
|
+
});
|
|
5038
|
+
}
|
|
5039
|
+
|
|
4800
5040
|
function copyText(text) {
|
|
4801
5041
|
try {
|
|
4802
5042
|
navigator.clipboard.writeText(text);
|
|
@@ -4806,13 +5046,13 @@ function copyText(text) {
|
|
|
4806
5046
|
}
|
|
4807
5047
|
}
|
|
4808
5048
|
|
|
4809
|
-
/** 工作台 → 原生 Chat 的窗口:非 embed 用左栏自己的 iframe
|
|
5049
|
+
/** 工作台 → 原生 Chat 的窗口:非 embed 用左栏自己的 iframe;embed / 桌面用宿主 DSH 壳。 */
|
|
4810
5050
|
function nativeChatWindow() {
|
|
4811
|
-
if (!EMBED) {
|
|
5051
|
+
if (!EMBED && !DESKTOP_HOST) {
|
|
4812
5052
|
const frame = getEl("nativeChatFrame");
|
|
4813
5053
|
return frame && frame.contentWindow ? frame.contentWindow : null;
|
|
4814
5054
|
}
|
|
4815
|
-
return window.parent !== window ? window.parent : null; // embed
|
|
5055
|
+
return window.parent !== window ? window.parent : null; // embed / 桌面:宿主 DSH 壳
|
|
4816
5056
|
}
|
|
4817
5057
|
|
|
4818
5058
|
function requestNativeChat(text) {
|
|
@@ -4868,6 +5108,9 @@ function requestConversation(payload) {
|
|
|
4868
5108
|
workspacePath: STATE.data?.workspacePath || "",
|
|
4869
5109
|
sourceRoot: STATE.data?.active?.sourceRoot || "",
|
|
4870
5110
|
...payload,
|
|
5111
|
+
// F-202B:桌面 Dock 模式默认不抢主区(open:false);显式 takeMain → open:true 切宿主主区。
|
|
5112
|
+
// web 不传 open(undefined → 宿主按原语义 openSession)。
|
|
5113
|
+
open: DESKTOP_HOST ? payload.takeMain === true : undefined,
|
|
4871
5114
|
}, "*");
|
|
4872
5115
|
setTimeout(() => {
|
|
4873
5116
|
if (settled) return;
|
|
@@ -4935,6 +5178,8 @@ async function ensureDeckConversation(deckId) {
|
|
|
4935
5178
|
kind: "deck",
|
|
4936
5179
|
id: deckId,
|
|
4937
5180
|
title: label,
|
|
5181
|
+
// 桌面:点 deck 是进工作台,不是切出去聊天——后台 ensure,不切换宿主视图。
|
|
5182
|
+
|
|
4938
5183
|
prompt: `本对话绑定 deck ${deckId}(${label})。后续所有修改都在这个 deck 上进行;先读 intent 与 script 概要,等我的指令,不要直接出图。`,
|
|
4939
5184
|
});
|
|
4940
5185
|
if (!result.ok) {
|
|
@@ -4987,6 +5232,12 @@ async function openBoundConversation(sessionId) {
|
|
|
4987
5232
|
getEl("homeGreetSub").textContent = `没能切到会话:${result.reason || "未知原因"}`;
|
|
4988
5233
|
return;
|
|
4989
5234
|
}
|
|
5235
|
+
if (DESKTOP_HOST) {
|
|
5236
|
+
// 桌面:用户点会话就是要对话;宿主主区已切到该会话,收起工作台(回工作台时再对齐到绑定的 deck)。
|
|
5237
|
+
STATE.chatSessionId = sessionId;
|
|
5238
|
+
showDeck(binding?.kind === "deck" ? binding.id : null);
|
|
5239
|
+
return;
|
|
5240
|
+
}
|
|
4990
5241
|
if (binding?.kind === "deck") {
|
|
4991
5242
|
showDeck(binding.id);
|
|
4992
5243
|
return;
|
|
@@ -5030,8 +5281,11 @@ function activeChatTarget() {
|
|
|
5030
5281
|
return chatTargetForBinding({ kind: "deck", id: active.deckId });
|
|
5031
5282
|
}
|
|
5032
5283
|
|
|
5033
|
-
/** 当前 Chat 所在会话 id:优先读 iframe 里 shell 的 dsh.sessions.current(同源),退回本地记录。
|
|
5284
|
+
/** 当前 Chat 所在会话 id:优先读 iframe 里 shell 的 dsh.sessions.current(同源),退回本地记录。
|
|
5285
|
+
* F-202B:桌面宿主不读——隐藏 iframe 继承 dsh-app origin,localStorage 就是
|
|
5286
|
+
* 宿主 shell 的存储,读到的是「宿主当前会话」而不是本 deck 绑定会话(Dock 被它顶掉)。 */
|
|
5034
5287
|
function currentChatSessionId() {
|
|
5288
|
+
if (DESKTOP_HOST) return STATE.chatSessionId || "";
|
|
5035
5289
|
try {
|
|
5036
5290
|
const win = getEl("nativeChatFrame")?.contentWindow;
|
|
5037
5291
|
const raw = win?.localStorage?.getItem("dsh.sessions.current");
|
|
@@ -5144,6 +5398,7 @@ function renderChatSessionMenu(sessions, target) {
|
|
|
5144
5398
|
STATE.chatSessionId = sessionId;
|
|
5145
5399
|
await refreshSessions();
|
|
5146
5400
|
renderChatHeader();
|
|
5401
|
+
// 桌面:会话已在宿主主区打开,把工作台面板让出来(web 版留在内嵌 iframe 里)。
|
|
5147
5402
|
}));
|
|
5148
5403
|
}
|
|
5149
5404
|
|
|
@@ -5342,6 +5597,9 @@ async function newChatForTarget() {
|
|
|
5342
5597
|
try {
|
|
5343
5598
|
const result = await requestConversation({ action: "ensure", kind: target.kind, id: target.id, title: target.label });
|
|
5344
5599
|
if (!result.ok || !result.sessionId) {
|
|
5600
|
+
// F-202B:失败要用户可见——之前只写按钮 title,Dock 模式下看起来像「点了没反应」。
|
|
5601
|
+
getEl("chatBindChip").textContent = "会话新建失败";
|
|
5602
|
+
getEl("chatBindChip").title = `新建会话失败:${result.reason || "未知原因"};点这里重试`;
|
|
5345
5603
|
getEl("chatSessionBtn").title = `新建会话失败:${result.reason || "未知原因"}`;
|
|
5346
5604
|
return;
|
|
5347
5605
|
}
|
|
@@ -5349,6 +5607,7 @@ async function newChatForTarget() {
|
|
|
5349
5607
|
await bindConversation({ sessionId: result.sessionId, kind: target.kind, id: target.id, title: target.label });
|
|
5350
5608
|
await refreshSessions();
|
|
5351
5609
|
renderChatHeader();
|
|
5610
|
+
// 桌面:新建就是要点开聊——切到宿主主区的原生会话。
|
|
5352
5611
|
} finally {
|
|
5353
5612
|
button.disabled = false;
|
|
5354
5613
|
}
|
|
@@ -5621,7 +5880,7 @@ function backToManual(box) {
|
|
|
5621
5880
|
getEl("updateBtn").addEventListener("click", checkPluginUpdate);
|
|
5622
5881
|
getEl("repoBtn").addEventListener("click", openRepo);
|
|
5623
5882
|
await refreshSessions();
|
|
5624
|
-
getEl("save").textContent = EMBED ? "对话在 DSH 原生 Chat" : "对话在左侧 · DSH 原生";
|
|
5883
|
+
getEl("save").textContent = EMBED ? "对话在 DSH 原生 Chat" : DESKTOP_HOST ? "对话 · 原生(桌面 Dock)" : "对话在左侧 · DSH 原生";
|
|
5625
5884
|
// F-69:只有 profile 开了 hideShellSidebar,宿主左侧栏才会默认收起;此时顶栏给一个切换入口。
|
|
5626
5885
|
if (!EMBED && window.parent !== window && STATE.data?.hideShellSidebar === true) {
|
|
5627
5886
|
const shellBtn = getEl("shellSidebarBtn");
|
|
@@ -5636,7 +5895,7 @@ function backToManual(box) {
|
|
|
5636
5895
|
getEl("workspaceView").style.display = "grid";
|
|
5637
5896
|
switchChatToBoundSession().catch(() => {});
|
|
5638
5897
|
} else {
|
|
5639
|
-
const match = location.hash.match(/^#\/home\/(decks|designs|resources|conversations|manual)/);
|
|
5898
|
+
const match = location.hash.match(/^#\/home\/(decks|designs|resources|conversations|manual|about)/);
|
|
5640
5899
|
STATE.homeSection = match ? match[1] : "decks";
|
|
5641
5900
|
showHome();
|
|
5642
5901
|
}
|