dsh-plugin-deck-studio 0.2.14 → 0.2.24
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 +331 -5
- package/index.js +81 -7
- package/lib/deck-templates.js +6 -1
- package/package.json +1 -1
- package/ui/workbench.css +17 -1
- package/ui/workbench.html +167 -22
package/client.js
CHANGED
|
@@ -339,8 +339,62 @@ div[data-sidebar-collapsed]{grid-template-columns:0px minmax(0px,1fr) 0px!import
|
|
|
339
339
|
const session = useSession((value) => value);
|
|
340
340
|
const active = useConversation((value) => value).activeTargets;
|
|
341
341
|
const phase = (!session.blank || session.running || active.size > 0) ? "active" : "hero";
|
|
342
|
+
/**
|
|
343
|
+
* 0.2.23:**回退** 0.2.22 的 sessionId 透传。
|
|
344
|
+
*
|
|
345
|
+
* 为什么当时要透传:宿主的 composer 链是
|
|
346
|
+
* useSessionStatus(s => s.get(sessionId)?.pendingInteraction) → renderSlotChain(...)
|
|
347
|
+
* 审批面板要靠 pendingInteraction 才渲染,我们没传 sessionId 就永远不弹 ask。
|
|
348
|
+
*
|
|
349
|
+
* 为什么回退:透传之后真机上「点会话 → 工作台闪出 → 不到 1s 整片变灰」——
|
|
350
|
+
* 说明在 shell.overlay 这个上下文里让 composer 链去查宿主的主区 pendingInteraction 会直接崩掉
|
|
351
|
+
* 整棵 Dock 子树(外面看不到原因,只看到灰屏)。宁可先保住对话可用,再想别的办法把 ask 露出来。
|
|
352
|
+
*/
|
|
342
353
|
return renderFactorySlot("conversation.content", { variant: "embedded", phase, hero: phase === "hero" }, { slots: { views: DeckChatView } });
|
|
343
354
|
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* 0.2.23:Dock 子树的错误边界。
|
|
358
|
+
*
|
|
359
|
+
* 之前 slot 里任何一个组件抛错,整个 overlay 会变成一片空白灰,用户完全不知道发生了什么
|
|
360
|
+
* (这次灰屏就是)。用边界把错误收住,至少把原因写在屏幕上 + 给重试和「去主区对话」的出路。
|
|
361
|
+
*/
|
|
362
|
+
class DockErrorBoundary extends React.Component {
|
|
363
|
+
constructor(props) {
|
|
364
|
+
super(props);
|
|
365
|
+
this.state = { error: null };
|
|
366
|
+
}
|
|
367
|
+
static getDerivedStateFromError(error) {
|
|
368
|
+
return { error };
|
|
369
|
+
}
|
|
370
|
+
componentDidCatch(error) {
|
|
371
|
+
try {
|
|
372
|
+
ctx.logger?.warn?.("deck-studio: chat dock crashed", error);
|
|
373
|
+
} catch {
|
|
374
|
+
/* 日志不可用时忽略 */
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
render() {
|
|
378
|
+
if (!this.state.error) return this.props.children;
|
|
379
|
+
const message = String(this.state.error?.message ?? this.state.error ?? "未知错误");
|
|
380
|
+
return h(
|
|
381
|
+
"div",
|
|
382
|
+
{ style: { height: "100%", padding: 16, overflow: "auto", background: "#fdf7f3", color: "#7a3b2e", fontSize: 12, lineHeight: 1.7 } },
|
|
383
|
+
h("div", { style: { fontWeight: 600, marginBottom: 6 } }, "对话区加载失败"),
|
|
384
|
+
h("div", { style: { color: "#5c6464", wordBreak: "break-word" } }, message),
|
|
385
|
+
h(
|
|
386
|
+
"button",
|
|
387
|
+
{
|
|
388
|
+
type: "button",
|
|
389
|
+
style: { marginTop: 10, marginRight: 8, padding: "4px 10px", fontSize: 12, cursor: "pointer" },
|
|
390
|
+
onClick: () => this.setState({ error: null }),
|
|
391
|
+
},
|
|
392
|
+
"重试",
|
|
393
|
+
),
|
|
394
|
+
h("div", { style: { marginTop: 10, color: "#5c6464" } }, "也可以点顶栏「☰ DSH」去宿主主区的对话里继续。"),
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
344
398
|
/** Dock:测量工作台对话占位矩形;用 retain 的会话引用喂 SessionProvider。 */
|
|
345
399
|
function DeckChatDock({ SessionProvider, renderSlot }) {
|
|
346
400
|
const sessionId = React.useSyncExternalStore(subscribeDockSession, () => dockState.sessionId);
|
|
@@ -389,10 +443,18 @@ div[data-sidebar-collapsed]{grid-template-columns:0px minmax(0px,1fr) 0px!import
|
|
|
389
443
|
try { frameEl.contentWindow.postMessage({ type: "dsh-deck-studio:chat-dock" }, "*"); } catch { /* 不可达时降级卡留着 */ }
|
|
390
444
|
}, [rect, reference]);
|
|
391
445
|
if (!SessionProvider || !renderSlot || !reference || !rect) return null;
|
|
446
|
+
// 0.2.23:把覆盖层夹在可视区内。测量一旦拿到「整窗」尺寸(面板刚切换、iframe 还没布局),
|
|
447
|
+
// 这个 fixed 覆盖层会把整个应用盖住,看起来就是「整片灰、啥也没有」。
|
|
448
|
+
const viewportW = typeof window !== "undefined" ? window.innerWidth : rect.width;
|
|
449
|
+
const viewportH = typeof window !== "undefined" ? window.innerHeight : rect.height;
|
|
450
|
+
const left = Math.max(0, Math.min(rect.left, Math.max(0, viewportW - 120)));
|
|
451
|
+
const top = Math.max(0, Math.min(rect.top, Math.max(0, viewportH - 120)));
|
|
452
|
+
const width = Math.max(120, Math.min(rect.width, viewportW - left));
|
|
453
|
+
const height = Math.max(160, Math.min(rect.height, viewportH - top));
|
|
392
454
|
return h("div", {
|
|
393
455
|
"data-deck-studio-chat-dock": "",
|
|
394
|
-
style: { position: "fixed", left: `${
|
|
395
|
-
}, h(SessionProvider, { session: reference }, renderSlot(CHAT_SLOT, {})));
|
|
456
|
+
style: { position: "fixed", left: `${left}px`, top: `${top}px`, width: `${width}px`, height: `${height}px`, zIndex: 40, display: "flex", flexDirection: "column", minHeight: 0, overflow: "hidden", background: "var(--paper,#faf8f2)", borderRight: "1px solid var(--line,#e8e2d6)", pointerEvents: "auto" },
|
|
457
|
+
}, h(DockErrorBoundary, null, h(SessionProvider, { session: reference }, renderSlot(CHAT_SLOT, {}))));
|
|
396
458
|
}
|
|
397
459
|
// 工作台左栏会把 DSH 原生 Chat 以 `/?decklineChat=1` 嵌进来(同源 iframe)。
|
|
398
460
|
// 那个嵌套的 DSH 实例不再自动打开工作台,否则会层层嵌套(F-68)。
|
|
@@ -423,6 +485,140 @@ div[data-sidebar-collapsed]{grid-template-columns:0px minmax(0px,1fr) 0px!import
|
|
|
423
485
|
};
|
|
424
486
|
const closeWorkbench = () => layout?.selectPanel?.(null);
|
|
425
487
|
|
|
488
|
+
/**
|
|
489
|
+
* 0.2.17:从 DSH 侧栏的会话直接拉起 Deckline 工作区(对齐 html-deck-studio 的同款交互)。
|
|
490
|
+
*
|
|
491
|
+
* 宿主把「当前主区会话」记在 `sessions.list` 的 `retainedBy.mainView` 上。切到一本
|
|
492
|
+
* 已绑定 deck / design 的会话时,自动打开工作台并切到那本 deck —— 省掉「先点 Deckline
|
|
493
|
+
* 面板、再选 deck」两步;会话行「⋯」菜单里也给一个显式入口。
|
|
494
|
+
*/
|
|
495
|
+
const activeMainSessionId = () => {
|
|
496
|
+
try {
|
|
497
|
+
const list = ctx.sessions?.list?.getSnapshot?.();
|
|
498
|
+
if (!list?.byId) return "";
|
|
499
|
+
return Object.values(list.byId).find((row) => (row?.retainedBy?.mainView ?? 0) > 0)?.id ?? "";
|
|
500
|
+
} catch {
|
|
501
|
+
return "";
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
const activePanelIdOf = () => {
|
|
505
|
+
try {
|
|
506
|
+
return layout?.panelInfo?.getSnapshot?.()?.activePanelId ?? null;
|
|
507
|
+
} catch {
|
|
508
|
+
return null;
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
/** 会话 → 绑定实体(服务端绑定表;3s 内复用,列表频繁变化时不重复打接口)。 */
|
|
512
|
+
let bindingsCache = { at: 0, rows: [] };
|
|
513
|
+
const readSessionBindings = async () => {
|
|
514
|
+
if (Date.now() - bindingsCache.at < 3000) return bindingsCache.rows;
|
|
515
|
+
try {
|
|
516
|
+
const res = await fetch("/api/deck-studio.session-bindings", { headers: { accept: "application/json" } });
|
|
517
|
+
const data = await res.json();
|
|
518
|
+
bindingsCache = { at: Date.now(), rows: Array.isArray(data?.bindings) ? data.bindings : [] };
|
|
519
|
+
} catch {
|
|
520
|
+
bindingsCache = { at: Date.now(), rows: bindingsCache.rows };
|
|
521
|
+
}
|
|
522
|
+
return bindingsCache.rows;
|
|
523
|
+
};
|
|
524
|
+
/**
|
|
525
|
+
* 待发往工作台 iframe 的「打开某个 deck / design」指令。
|
|
526
|
+
*
|
|
527
|
+
* 0.2.21:发出去**不清空**——面板刚挂载时 iframe 里的脚本还没起来,早发的那几帧会被丢掉,
|
|
528
|
+
* 表现成「工作区弹出来了但停在首页」。现在等 iframe 回 `open-binding-ack` 才算送达,
|
|
529
|
+
* 期间由 ready 消息 + 多档定时重试兜底;20s 还没 ack 就放弃,避免下次误打开旧 deck。
|
|
530
|
+
*/
|
|
531
|
+
let pendingOpen = null;
|
|
532
|
+
const flushPendingOpen = () => {
|
|
533
|
+
const pending = pendingOpen;
|
|
534
|
+
const win = frameState.el?.contentWindow;
|
|
535
|
+
if (!pending || !win) return;
|
|
536
|
+
try {
|
|
537
|
+
win.postMessage({ type: "dsh-deck-studio:open-binding", ...pending }, "*");
|
|
538
|
+
} catch {
|
|
539
|
+
/* iframe 还没就绪:等 ready 消息或下一次重试 */
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
const scheduleFlush = () => {
|
|
543
|
+
[60, 220, 600, 1400, 2600, 4200].forEach((delay) => window.setTimeout(flushPendingOpen, delay));
|
|
544
|
+
};
|
|
545
|
+
const openBindingInWorkbench = (target) => {
|
|
546
|
+
if (!target || (!target.id && !target.notice)) return false;
|
|
547
|
+
openWorkbench();
|
|
548
|
+
pendingOpen = { kind: target.kind ?? "deck", id: target.id ?? "", title: target.title ?? "", notice: target.notice ?? "" };
|
|
549
|
+
flushPendingOpen();
|
|
550
|
+
scheduleFlush();
|
|
551
|
+
const expected = pendingOpen;
|
|
552
|
+
window.setTimeout(() => {
|
|
553
|
+
if (pendingOpen === expected) pendingOpen = null;
|
|
554
|
+
}, 20000);
|
|
555
|
+
return true;
|
|
556
|
+
};
|
|
557
|
+
/** 侧栏点了一个会话:已绑定就直接进那本 deck;没绑定就进工作台并说明怎么绑。 */
|
|
558
|
+
const openSessionInWorkbench = async (sessionId, options = {}) => {
|
|
559
|
+
const id = String(sessionId ?? "").trim();
|
|
560
|
+
if (!id) return false;
|
|
561
|
+
const rows = await readSessionBindings();
|
|
562
|
+
const binding = rows.find((row) => row?.sessionId === id) ?? null;
|
|
563
|
+
if (!binding) {
|
|
564
|
+
if (options.auto) return false;
|
|
565
|
+
return openBindingInWorkbench({
|
|
566
|
+
kind: "none",
|
|
567
|
+
id: "",
|
|
568
|
+
notice: "这个会话还没有绑定 deck / 风格:先选一本 deck,用「+」新建或切换绑定。",
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
return openBindingInWorkbench({
|
|
572
|
+
kind: binding.kind === "design" ? "design" : "deck",
|
|
573
|
+
id: binding.id,
|
|
574
|
+
title: binding.title ?? "",
|
|
575
|
+
});
|
|
576
|
+
};
|
|
577
|
+
/** 会话行「⋯」菜单项:在 Deckline 里打开这个会话绑定的 deck(未绑定给可见提示)。 */
|
|
578
|
+
function OpenDecklineMenuItem({ sessionId, useMenuOpenState }) {
|
|
579
|
+
const menu = typeof useMenuOpenState === "function" ? useMenuOpenState : () => [false, () => {}];
|
|
580
|
+
const [, setMenuOpen] = menu();
|
|
581
|
+
const [busy, setBusy] = React.useState(false);
|
|
582
|
+
const label = busy ? "正在打开 Deckline…" : "在 Deckline 打开";
|
|
583
|
+
const onSelect = () => {
|
|
584
|
+
try {
|
|
585
|
+
setMenuOpen(false);
|
|
586
|
+
} catch {
|
|
587
|
+
/* 菜单状态不可用时仍然执行动作 */
|
|
588
|
+
}
|
|
589
|
+
if (busy) return;
|
|
590
|
+
setBusy(true);
|
|
591
|
+
void openSessionInWorkbench(sessionId).finally(() => setBusy(false));
|
|
592
|
+
};
|
|
593
|
+
return h(
|
|
594
|
+
"button",
|
|
595
|
+
{
|
|
596
|
+
type: "button",
|
|
597
|
+
role: "menuitem",
|
|
598
|
+
"aria-label": label,
|
|
599
|
+
"data-deck-studio-open": "",
|
|
600
|
+
disabled: busy,
|
|
601
|
+
style: {
|
|
602
|
+
width: "100%",
|
|
603
|
+
minHeight: 32,
|
|
604
|
+
padding: "0 12px",
|
|
605
|
+
fontSize: 13,
|
|
606
|
+
display: "flex",
|
|
607
|
+
alignItems: "center",
|
|
608
|
+
justifyContent: "flex-start",
|
|
609
|
+
textAlign: "left",
|
|
610
|
+
border: 0,
|
|
611
|
+
background: "transparent",
|
|
612
|
+
color: "var(--dsw-alias-label-primary)",
|
|
613
|
+
borderRadius: "var(--dsw-radius-sm)",
|
|
614
|
+
cursor: busy ? "default" : "pointer",
|
|
615
|
+
},
|
|
616
|
+
onClick: onSelect,
|
|
617
|
+
},
|
|
618
|
+
label,
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
|
|
426
622
|
const currentSessionId = () => {
|
|
427
623
|
try {
|
|
428
624
|
const raw = window.localStorage.getItem("dsh.sessions.current");
|
|
@@ -435,7 +631,20 @@ div[data-sidebar-collapsed]{grid-template-columns:0px minmax(0px,1fr) 0px!import
|
|
|
435
631
|
const sessions = ctx.sessions;
|
|
436
632
|
if (!sessions?.scope) throw new Error("sessions service unavailable");
|
|
437
633
|
if (!sessionId) throw new Error("no target session");
|
|
438
|
-
|
|
634
|
+
/**
|
|
635
|
+
* 0.2.20:刚建出来的会话还没被 Dock retain 过,`sessions.scope()` 就是 null——
|
|
636
|
+
* 以前直接抛 `session … is not retained`,于是「新建 deck 后点发送」整条链路失败:
|
|
637
|
+
* 会话建出来了却没绑定、工作区也不弹。这里先自己 retain 一次再取 scope。
|
|
638
|
+
*/
|
|
639
|
+
let scoped = sessions.scope(sessionId);
|
|
640
|
+
if (!scoped && typeof sessions.retain === "function") {
|
|
641
|
+
try {
|
|
642
|
+
sessions.retain(sessionId, { source: WORKBENCH_KEY });
|
|
643
|
+
} catch (error) {
|
|
644
|
+
ctx.logger?.warn?.(`deck-studio: retain before send failed: ${error?.message ?? error}`);
|
|
645
|
+
}
|
|
646
|
+
scoped = sessions.scope(sessionId);
|
|
647
|
+
}
|
|
439
648
|
if (!scoped) throw new Error(`session ${sessionId} is not retained`);
|
|
440
649
|
// 会话级上下文需要显式 inject 才能拿到 conversation 服务。
|
|
441
650
|
await new Promise((resolve, reject) => {
|
|
@@ -545,6 +754,28 @@ div[data-sidebar-collapsed]{grid-template-columns:0px minmax(0px,1fr) 0px!import
|
|
|
545
754
|
const sourceRoot = String(data.sourceRoot ?? "").trim();
|
|
546
755
|
const sourceWorkspace = sourceRoot ? containing(sourceRoot) : null;
|
|
547
756
|
if (sourceWorkspace) return sourceWorkspace.workspaceId;
|
|
757
|
+
/**
|
|
758
|
+
* 0.2.16:deck 自己的项目目录(工作台载荷里的 sessionWorkspacePath)。
|
|
759
|
+
* 桌面宿主里上面几级会全落空——cwd 是 profile 目录被守卫挡掉、源目录还没登记成工作区、
|
|
760
|
+
* 客户端 workspaces 服务也没有 initializeDefault——结果是「从没有会话的 deck」
|
|
761
|
+
* 永远建不出会话、也就永远绑不上(点了 + 没反应)。这里按 deck 项目目录建/复用工作区,
|
|
762
|
+
* 保证每本 deck 都有落点;工作区名只是侧栏文案,命名失败不影响建会话。
|
|
763
|
+
*/
|
|
764
|
+
const projectPath = String(data.sessionWorkspacePath ?? "").trim();
|
|
765
|
+
if (projectPath) {
|
|
766
|
+
const existing = containing(projectPath);
|
|
767
|
+
if (existing) return existing.workspaceId;
|
|
768
|
+
const projectWorkspaceId = await register(projectPath);
|
|
769
|
+
const title = String(data.title ?? "").trim().slice(0, 60);
|
|
770
|
+
if (title && typeof workspaces.rename === "function") {
|
|
771
|
+
try {
|
|
772
|
+
await workspaces.rename(projectWorkspaceId, `Deckline · ${title}`);
|
|
773
|
+
} catch {
|
|
774
|
+
/* 工作区重名等失败只影响侧栏文案,会话照建 */
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
return projectWorkspaceId;
|
|
778
|
+
}
|
|
548
779
|
const fallback = await workspaces.initializeDefault?.();
|
|
549
780
|
if (fallback?.workspaceId) return fallback.workspaceId;
|
|
550
781
|
throw new Error("无法确定启动工作区,未创建会话;请配置 workspacePath 后重试");
|
|
@@ -614,10 +845,18 @@ div[data-sidebar-collapsed]{grid-template-columns:0px minmax(0px,1fr) 0px!import
|
|
|
614
845
|
if (lastError) throw lastError;
|
|
615
846
|
if (data.takeMain === true && DESKTOP_SHELL) closeWorkbench();
|
|
616
847
|
}
|
|
848
|
+
let notice = "";
|
|
617
849
|
if (data.prompt && data.action === "ensure" && sessionId !== String(data.sessionId ?? "")) {
|
|
618
|
-
|
|
850
|
+
// 0.2.20:首条指令发不出去不该让整条 ensure 失败——会话已经建好,
|
|
851
|
+
// 绑定必须照写(否则 deck 变「未绑定」,工作区也不会自动弹出来)。
|
|
852
|
+
try {
|
|
853
|
+
await sendToSession(sessionId, String(data.prompt));
|
|
854
|
+
} catch (error) {
|
|
855
|
+
notice = `会话已建好并绑定,但首条指令没发出去:${error?.message ?? error};在工作台里再发一次即可。`;
|
|
856
|
+
ctx.logger?.warn?.(`deck-studio: prompt send failed after ensure: ${error?.message ?? error}`);
|
|
857
|
+
}
|
|
619
858
|
}
|
|
620
|
-
reply({ ok: true, sessionId });
|
|
859
|
+
reply({ ok: true, sessionId, ...(notice ? { notice } : {}) });
|
|
621
860
|
} catch (error) {
|
|
622
861
|
ctx.logger?.warn?.("deck-studio: conversation bridge failed", error);
|
|
623
862
|
reply({ ok: false, reason: String(error?.message ?? error) });
|
|
@@ -720,6 +959,78 @@ div[data-sidebar-collapsed]{grid-template-columns:0px minmax(0px,1fr) 0px!import
|
|
|
720
959
|
return () => window.clearTimeout(timer);
|
|
721
960
|
}, "deck-studio: auto-open workbench");
|
|
722
961
|
|
|
962
|
+
/**
|
|
963
|
+
* 0.2.17:切到「已绑定 deck / design 的会话」时自动拉起工作区。
|
|
964
|
+
* 只在会话真的换了、且主区没有停在别的插件面板时动作;启动时先只记住当前会话不抢视图,
|
|
965
|
+
* 工作台自己打开的那一下不算用户切换(pendingOpen / 自身面板判定都挡掉)。
|
|
966
|
+
*/
|
|
967
|
+
ctx.effect(() => {
|
|
968
|
+
const store = ctx.sessions?.list;
|
|
969
|
+
if (!store?.subscribe) return undefined;
|
|
970
|
+
let primed = false;
|
|
971
|
+
let lastActive = "";
|
|
972
|
+
let timer = null;
|
|
973
|
+
let token = 0;
|
|
974
|
+
const onChange = () => {
|
|
975
|
+
const list = store.getSnapshot?.();
|
|
976
|
+
if (!list || list.phase !== "ready") return;
|
|
977
|
+
const active = activeMainSessionId();
|
|
978
|
+
if (!primed) {
|
|
979
|
+
primed = true;
|
|
980
|
+
lastActive = active;
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
if (active === lastActive) return;
|
|
984
|
+
lastActive = active;
|
|
985
|
+
if (!active) return;
|
|
986
|
+
// 会话变化的那一刻取面板:宿主随后会 selectPanel(null),晚一步就分不清原来停在哪。
|
|
987
|
+
const panelAtChange = activePanelIdOf();
|
|
988
|
+
const mine = ++token;
|
|
989
|
+
window.clearTimeout(timer);
|
|
990
|
+
timer = window.setTimeout(() => {
|
|
991
|
+
if (mine !== token || activeMainSessionId() !== active) return;
|
|
992
|
+
if (panelAtChange !== null && panelAtChange !== WORKBENCH_KEY) return;
|
|
993
|
+
const panel = activePanelIdOf();
|
|
994
|
+
if (panel !== null && panel !== WORKBENCH_KEY) return;
|
|
995
|
+
void openSessionInWorkbench(active, { auto: true }).catch((error) =>
|
|
996
|
+
ctx.logger?.warn?.(`deck-studio: auto-open bound session failed: ${error?.message ?? error}`));
|
|
997
|
+
}, 260);
|
|
998
|
+
};
|
|
999
|
+
const dispose = store.subscribe(onChange);
|
|
1000
|
+
onChange();
|
|
1001
|
+
return () => {
|
|
1002
|
+
window.clearTimeout(timer);
|
|
1003
|
+
token += 1;
|
|
1004
|
+
if (typeof dispose === "function") dispose();
|
|
1005
|
+
};
|
|
1006
|
+
}, "deck-studio: session → workbench");
|
|
1007
|
+
|
|
1008
|
+
// 工作台 iframe 就绪:把还没送出去的「打开某本 deck」补发一次。
|
|
1009
|
+
ctx.effect(() => {
|
|
1010
|
+
const onReady = (event) => {
|
|
1011
|
+
if (event.data?.type !== "dsh-deck-studio:workbench-ready") return;
|
|
1012
|
+
if (event.source !== (frameState.el?.contentWindow ?? null)) return;
|
|
1013
|
+
flushPendingOpen();
|
|
1014
|
+
};
|
|
1015
|
+
window.addEventListener("message", onReady);
|
|
1016
|
+
return () => window.removeEventListener("message", onReady);
|
|
1017
|
+
}, "deck-studio: workbench ready bridge");
|
|
1018
|
+
|
|
1019
|
+
// 工作台确认收到「打开某本 deck」→ 才算送达,清掉 pending(没 ack 就继续重试)。
|
|
1020
|
+
ctx.effect(() => {
|
|
1021
|
+
const onAck = (event) => {
|
|
1022
|
+
const data = event.data ?? {};
|
|
1023
|
+
if (data.type !== "dsh-deck-studio:open-binding-ack") return;
|
|
1024
|
+
if (event.source !== (frameState.el?.contentWindow ?? null)) return;
|
|
1025
|
+
if (!pendingOpen || String(data.id ?? "") !== String(pendingOpen.id ?? "")) return;
|
|
1026
|
+
const failed = data.ok === false;
|
|
1027
|
+
pendingOpen = null;
|
|
1028
|
+
if (failed) ctx.logger?.warn?.(`deck-studio: open binding failed: ${data.reason ?? "unknown"}`);
|
|
1029
|
+
};
|
|
1030
|
+
window.addEventListener("message", onAck);
|
|
1031
|
+
return () => window.removeEventListener("message", onAck);
|
|
1032
|
+
}, "deck-studio: open-binding ack bridge");
|
|
1033
|
+
|
|
723
1034
|
// 唯一入口:侧栏面板行「Deckline」(与 plugins / schedules 等插件同一种形态,带图标);
|
|
724
1035
|
// → 对话用工作台里的「← 回到对话」。
|
|
725
1036
|
if (hasRightbar) {
|
|
@@ -729,6 +1040,21 @@ div[data-sidebar-collapsed]{grid-template-columns:0px minmax(0px,1fr) 0px!import
|
|
|
729
1040
|
ctx.slots.register({ name: "sidebar.panellist", id: WORKBENCH_KEY, order: 46, label: () => "Deckline" }, DeckGlyph)),
|
|
730
1041
|
"deck-studio: workbench sidebar entry",
|
|
731
1042
|
);
|
|
1043
|
+
// 会话行「⋯」菜单:把当前会话绑定的 deck 在工作台里打开(未绑定给可见提示)。
|
|
1044
|
+
// 该槽位是宿主可选契约,旧版本没有时只记一条警告,不能影响面板本身注册。
|
|
1045
|
+
try {
|
|
1046
|
+
ctx.effect(
|
|
1047
|
+
() => ctx.slots.inject("sidebar.workspaces.session.menu.item", () =>
|
|
1048
|
+
ctx.slots.register({
|
|
1049
|
+
name: "sidebar.workspaces.session.menu.item",
|
|
1050
|
+
id: "dsh-plugin-deck-studio.open-binding",
|
|
1051
|
+
order: 150,
|
|
1052
|
+
}, OpenDecklineMenuItem)),
|
|
1053
|
+
"deck-studio: session menu entry",
|
|
1054
|
+
);
|
|
1055
|
+
} catch (error) {
|
|
1056
|
+
ctx.logger?.warn?.(`deck-studio: session menu slot unavailable: ${error?.message ?? error}`);
|
|
1057
|
+
}
|
|
732
1058
|
}
|
|
733
1059
|
ctx.logger?.info?.("deck-studio client: workbench-only mode");
|
|
734
1060
|
} else {
|
package/index.js
CHANGED
|
@@ -395,9 +395,38 @@ export function createService(config, logger, env = process.env) {
|
|
|
395
395
|
// F-71:标出「来自注册表」——preset 工具实例靠它区分静态源,运行时删除的 deck 才能被移除。
|
|
396
396
|
config.decks.push({ id: entry.id, label: entry.label || entry.id, sourceRoot: entry.sourceRoot, kind: entry.kind ?? "deck", designId: entry.designId ?? null, fromRegistry: true });
|
|
397
397
|
}
|
|
398
|
+
/**
|
|
399
|
+
* 0.2.15:补挂「工作副本里有 manifest、但注册表里没有条目」的 deck(ghost deck)。
|
|
400
|
+
*
|
|
401
|
+
* 以前这类 deck 只在 status() 的兜底分支里露一个标题:没有 adapter → 源里已出好的整页图
|
|
402
|
+
* baseline、资源库、封面、9 份文档全部拿不到,工作台打开就是一屏「未出图」的空白页,
|
|
403
|
+
* 看起来像「资源都在就是没显示」。这里在 adapters 之前补挂:manifest 里的源还可读
|
|
404
|
+
* (script.md 在)就造一个只读 adapter;读不到的留给 status() 标「未挂载」。
|
|
405
|
+
*/
|
|
406
|
+
function adoptManifestDecks() {
|
|
407
|
+
const adopted = [];
|
|
408
|
+
for (const deckId of workspace.listDeckIds()) {
|
|
409
|
+
if (config.decks.some((deck) => deck.id === deckId)) continue;
|
|
410
|
+
const manifest = workspace.manifest(deckId);
|
|
411
|
+
const root = String(manifest?.source?.root ?? "").trim();
|
|
412
|
+
if (!root || !existsSync(join(root, "script.md"))) continue;
|
|
413
|
+
const kind = manifest?.kind === "design-canvas" ? "design-canvas" : "deck";
|
|
414
|
+
config.decks.push({
|
|
415
|
+
id: deckId,
|
|
416
|
+
label: String(manifest?.title ?? "").trim() || deckId,
|
|
417
|
+
sourceRoot: root,
|
|
418
|
+
kind,
|
|
419
|
+
designId: manifest?.designId ?? null,
|
|
420
|
+
fromManifest: true,
|
|
421
|
+
});
|
|
422
|
+
adopted.push(deckId);
|
|
423
|
+
}
|
|
424
|
+
return adopted;
|
|
425
|
+
}
|
|
398
426
|
// F-157 首次启动自举:新装 profile 打开 bootstrap 时,自动登记示例 deck / 拷资源库 / 导入预设风格,
|
|
399
427
|
// 避免「装完四个页面全是空的」。可用 profile patch 里 bootstrap: false 关掉。
|
|
400
428
|
// 示例 deck 必须在 adapters 之前登记;资源和 fashioned 风格放后面(依赖 adapters/workspace)。
|
|
429
|
+
const adoptedDeckIds = adoptManifestDecks();
|
|
401
430
|
if (config.bootstrap === true) bootstrapSampleDeck();
|
|
402
431
|
const adapters = config.decks.map((deck) => createLocalSourceAdapter(deck));
|
|
403
432
|
|
|
@@ -517,9 +546,12 @@ export function createService(config, logger, env = process.env) {
|
|
|
517
546
|
const success = String(payload.success ?? "").trim();
|
|
518
547
|
const scopeOut = String(payload.scopeOut ?? "").trim();
|
|
519
548
|
if (!title) throw new Error("deck 标题不能为空");
|
|
549
|
+
// 0.2.18:开工方式。表单式(默认)要求先把受众 / 意图 / 页数定清楚;
|
|
550
|
+
// 探索式只要一个标题——主意还模糊时先建本 deck,简报随对话补充(对齐 html deck-studio 的 explore)。
|
|
551
|
+
const creationMode = payload.mode === "explore" ? "explore" : "form";
|
|
520
552
|
// 空白画布(design 会话用):0 页,等 Agent 把风格样张沉淀到 designs/<ABBR>/。
|
|
521
553
|
const blank = Boolean(payload.blank);
|
|
522
|
-
const pageTitles = newDeckPageTitles(payload, blank);
|
|
554
|
+
const pageTitles = creationMode === "explore" ? ["待定 · 边聊边定"] : newDeckPageTitles(payload, blank);
|
|
523
555
|
const pad = (n) => String(n).padStart(2, "0");
|
|
524
556
|
const now = new Date();
|
|
525
557
|
const id = `deck-${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}-${Math.random().toString(36).slice(2, 6)}`;
|
|
@@ -553,13 +585,16 @@ export function createService(config, logger, env = process.env) {
|
|
|
553
585
|
"confidential: client",
|
|
554
586
|
"render: image",
|
|
555
587
|
"deck-type: talk",
|
|
588
|
+
...(creationMode === "explore" ? ["creation-mode: explore"] : []),
|
|
556
589
|
`pages: ${pageTitles.length}`,
|
|
557
590
|
"status: draft",
|
|
558
591
|
"---",
|
|
559
592
|
"",
|
|
560
593
|
`# ${title}`,
|
|
561
594
|
"",
|
|
562
|
-
|
|
595
|
+
creationMode === "explore"
|
|
596
|
+
? "> 由「新建 Deck · 探索模式」创建:还没定简报,先和 Agent 把听众 / 想让人带走什么 / 场合聊清楚,再补 script;未确认前不出图。"
|
|
597
|
+
: "> 由「新建 Deck」创建:先补意图与资料,再改 script;未确认前不出图。",
|
|
563
598
|
"",
|
|
564
599
|
"## 故事线",
|
|
565
600
|
"",
|
|
@@ -594,6 +629,7 @@ export function createService(config, logger, env = process.env) {
|
|
|
594
629
|
scopeOut,
|
|
595
630
|
created: now.toISOString(),
|
|
596
631
|
owner: config.userName,
|
|
632
|
+
creationMode,
|
|
597
633
|
}),
|
|
598
634
|
"utf8",
|
|
599
635
|
);
|
|
@@ -672,6 +708,8 @@ export function createService(config, logger, env = process.env) {
|
|
|
672
708
|
style: { abbr: styleAbbr, description: styleDescription },
|
|
673
709
|
kind: payload.kind ?? "deck",
|
|
674
710
|
designId: payload.designId ?? null,
|
|
711
|
+
// 0.2.18:探索式建档在注册表里也留痕(重启后 UI 能显示「探索式」)。
|
|
712
|
+
creationMode,
|
|
675
713
|
});
|
|
676
714
|
writeFileSync(decksRegistryFile, `${JSON.stringify({ decks: entries }, null, 2)}\n`, "utf8");
|
|
677
715
|
|
|
@@ -700,7 +738,8 @@ export function createService(config, logger, env = process.env) {
|
|
|
700
738
|
designId: payload.designId ?? null,
|
|
701
739
|
style: { abbr: styleAbbr, description: styleDescription },
|
|
702
740
|
pages: pageTitles.length,
|
|
703
|
-
|
|
741
|
+
creationMode,
|
|
742
|
+
note: `初始化:script ${pageTitles.length} 页 + 意图 / 风格 / 参考资料清单 / 文档骨架${creationMode === "explore" ? "(探索式:简报随对话补充)" : ""}`,
|
|
704
743
|
},
|
|
705
744
|
});
|
|
706
745
|
return {
|
|
@@ -837,9 +876,16 @@ export function createService(config, logger, env = process.env) {
|
|
|
837
876
|
return { designId: design.designId, added, design: designProjectView(design) };
|
|
838
877
|
}
|
|
839
878
|
|
|
840
|
-
/**
|
|
879
|
+
/**
|
|
880
|
+
* F-157:把仓库自带的示例 deck 登记成只读源。
|
|
881
|
+
*
|
|
882
|
+
* 0.2.15:判定从「`config.decks` 非空就跳过」改成「这份示例没被挂上才登记」。
|
|
883
|
+
* 旧口径会被「导入预设风格时自动建的 8 本风格画布」顶掉——装过风格的机器上示例 deck
|
|
884
|
+
* 永远不会再登记,而它的工作副本还在 `decks/<id>/deck.json`,于是变成 ghost deck:
|
|
885
|
+
* 列表里有 10 页标题,打开却一页图都没有。
|
|
886
|
+
*/
|
|
841
887
|
function bootstrapSampleDeck() {
|
|
842
|
-
if (config.decks.
|
|
888
|
+
if (config.decks.some((deck) => deck.id === "sample-deckline-intro")) return;
|
|
843
889
|
const sampleRoot = join(REPO_ROOT, "samples", "deckline-intro");
|
|
844
890
|
if (!existsSync(join(sampleRoot, "script.md"))) return;
|
|
845
891
|
config.decks.push({ id: "sample-deckline-intro", label: "Deckline 示例手册", sourceRoot: sampleRoot, kind: "deck", designId: null, fromBootstrap: true });
|
|
@@ -1874,6 +1920,29 @@ export function createService(config, logger, env = process.env) {
|
|
|
1874
1920
|
};
|
|
1875
1921
|
}
|
|
1876
1922
|
|
|
1923
|
+
/**
|
|
1924
|
+
* 0.2.16:这本 deck 该用哪个目录当「会话工作区」。
|
|
1925
|
+
*
|
|
1926
|
+
* 背景:桌面宿主的 shell cwd 是 profile 目录(被工作区守卫挡掉),宿主客户端的
|
|
1927
|
+
* workspaces 服务又没有 initializeDefault,源目录也常常还没登记成工作区——三者叠加,
|
|
1928
|
+
* 「+ 新建会话」永远解析不到工作区,于是**从没有会话的 deck 永远建不出会话**(点了没反应)。
|
|
1929
|
+
* 这里给宿主桥一个明确的落点:deck 自己的项目目录优先;源落在插件安装位(node_modules)
|
|
1930
|
+
* 或 App 包里时不拿来当工作区,退回这本 deck 的工作副本目录 <dataRoot>/decks/<id>。
|
|
1931
|
+
*/
|
|
1932
|
+
function sessionWorkspacePathOf(deckId) {
|
|
1933
|
+
const sourceRoot = String(adapterOf(deckId)?.sourceRoot ?? "").trim();
|
|
1934
|
+
const installed = /[/\\]node_modules[/\\]/.test(sourceRoot) || sourceRoot.startsWith("/Applications/");
|
|
1935
|
+
if (sourceRoot && !installed) return sourceRoot;
|
|
1936
|
+
try {
|
|
1937
|
+
const dir = workspace.deckDir(deckId);
|
|
1938
|
+
// 工作区必须指向真实存在的目录;工作副本还没建出来时先建空的(没有 deck.json,不会被当成 deck 列出)。
|
|
1939
|
+
mkdirSync(dir, { recursive: true });
|
|
1940
|
+
return dir;
|
|
1941
|
+
} catch {
|
|
1942
|
+
return "";
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1877
1946
|
/** media 路由解析:只放行白名单目录的图片。 */
|
|
1878
1947
|
function mediaFile(deckId, scope, path) {
|
|
1879
1948
|
const adapter = adapterOf(deckId);
|
|
@@ -1951,8 +2020,11 @@ export function createService(config, logger, env = process.env) {
|
|
|
1951
2020
|
}
|
|
1952
2021
|
for (const deckId of workspace.listDeckIds()) {
|
|
1953
2022
|
if (decks.some((deck) => deck.deckId === deckId)) continue;
|
|
2023
|
+
// 0.2.15:走到这里说明这本 deck 的源没挂上(目录不在了 / script.md 丢了)。
|
|
2024
|
+
// 明确标 unmounted + sourceAvailable:false,前端据此给「重新登记 / 换源」的入口,
|
|
2025
|
+
// 而不是列一屏看起来正常、点开全是空白页的 deck。
|
|
1954
2026
|
const manifest = workspace.manifest(deckId);
|
|
1955
|
-
decks.push({ deckId, label: manifest?.title ?? deckId, sourceRoot: manifest?.source?.root ?? "", sourceAvailable:
|
|
2027
|
+
decks.push({ deckId, label: manifest?.title ?? deckId, sourceRoot: manifest?.source?.root ?? "", sourceAvailable: false, unmounted: true, title: manifest?.title ?? deckId, pageCount: manifest?.slides?.length ?? 0, status: manifest?.status ?? "", warnings: [] });
|
|
1956
2028
|
}
|
|
1957
2029
|
const tasks = store.taskList(200);
|
|
1958
2030
|
const counts = {};
|
|
@@ -2276,10 +2348,12 @@ export function createService(config, logger, env = process.env) {
|
|
|
2276
2348
|
// 当前 deck 生效的出图节点/模型(下拉可换)
|
|
2277
2349
|
renderNodes: renderState(activeId),
|
|
2278
2350
|
// 首页/标签用:deck 列表、风格(designs/<ABBR>/ 样张)、资源库
|
|
2279
|
-
decks: decks.map((item) => ({ deckId: item.deckId, label: item.label, pageCount: item.pageCount, status: item.status ?? "", sourceRoot: item.sourceRoot ?? "", kind: item.kind ?? "deck" })),
|
|
2351
|
+
decks: decks.map((item) => ({ deckId: item.deckId, label: item.label, pageCount: item.pageCount, status: item.status ?? "", sourceRoot: item.sourceRoot ?? "", kind: item.kind ?? "deck", sourceAvailable: item.sourceAvailable !== false, unmounted: item.unmounted === true })),
|
|
2280
2352
|
designs: designSamples(activeId),
|
|
2281
2353
|
...homeCatalog(activeId),
|
|
2282
2354
|
sourceRoot: adapter?.sourceRoot ?? deck?.source?.root ?? "",
|
|
2355
|
+
// 0.2.16:给宿主桥注册 DSH 工作区用的项目目录(见 sessionWorkspacePathOf)。
|
|
2356
|
+
sessionWorkspacePath: sessionWorkspacePathOf(activeId),
|
|
2283
2357
|
// P0:黄条与 Inspector 用的数据目录状态(active 级别)
|
|
2284
2358
|
dataDir: activeDataDir,
|
|
2285
2359
|
dataDirMismatch: needsBanner,
|
package/lib/deck-templates.js
CHANGED
|
@@ -20,7 +20,7 @@ const orPlaceholder = (value, hint) => (pad(value) ? pad(value) : `(待补:$
|
|
|
20
20
|
* intent.md 初始内容。
|
|
21
21
|
* @param {{title:string, audience?:string, topic?:string, intent?:string, references?:string, created?:string, owner?:string}} input
|
|
22
22
|
*/
|
|
23
|
-
export function buildIntentMarkdown({ title, audience = "", topic = "", intent = "", references = "", occasion = "", success = "", scopeOut = "", argument = "", skeletonType = "", pageOrder = "", keepThree = "", created = "", owner = "" } = {}) {
|
|
23
|
+
export function buildIntentMarkdown({ title, audience = "", topic = "", intent = "", references = "", occasion = "", success = "", scopeOut = "", argument = "", skeletonType = "", pageOrder = "", keepThree = "", created = "", owner = "", creationMode = "" } = {}) {
|
|
24
24
|
const heading = pad(title) || "(deck 标题)";
|
|
25
25
|
return [
|
|
26
26
|
"---",
|
|
@@ -28,10 +28,15 @@ export function buildIntentMarkdown({ title, audience = "", topic = "", intent =
|
|
|
28
28
|
`owner: ${owner}`,
|
|
29
29
|
...(created ? [`created: ${created}`] : []),
|
|
30
30
|
"status: draft",
|
|
31
|
+
// 0.2.18:探索式建档留痕——重启 / 重读 intent 时仍然知道这本是「边聊边定」的。
|
|
32
|
+
...(creationMode === "explore" ? ["creation-mode: explore"] : []),
|
|
31
33
|
"---",
|
|
32
34
|
"",
|
|
33
35
|
"# 意图",
|
|
34
36
|
"",
|
|
37
|
+
...(creationMode === "explore"
|
|
38
|
+
? ["- **创建模式**:探索式 —— 简报随对话补充;下面「待补」的字段由 Agent 逐条问清楚,不是漏填表单。"]
|
|
39
|
+
: []),
|
|
35
40
|
`- **听众**:${orPlaceholder(audience, "讲给谁:角色 / 部门 / 层级 / 对 AI 的熟悉度")}`,
|
|
36
41
|
`- **主题**:${orPlaceholder(topic || heading, "一句话说清这场讲什么")}`,
|
|
37
42
|
`- **带走**:${orPlaceholder(intent, "听完他们应该能带走什么(1–3 条,可验证)")}`,
|
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.24",
|
|
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",
|
package/ui/workbench.css
CHANGED
|
@@ -371,6 +371,19 @@ body.resizing{user-select:none;cursor:col-resize}
|
|
|
371
371
|
.newdeck-body .nd-row{display:grid;grid-template-columns:1fr 1fr;gap:10px}
|
|
372
372
|
.newdeck-body .field > span{font-size:10.5px;color:#5c6464}
|
|
373
373
|
.newdeck-body input,.newdeck-body textarea,.newdeck-body select{width:100%;font:inherit;font-size:12px;padding:7px 9px;border:1px solid var(--line);border-radius:5px;background:#fffdf8;color:var(--ink);resize:vertical}
|
|
374
|
+
/* 0.2.18 / 0.2.19:表单 / 探索两种开工方式。
|
|
375
|
+
必须写成 .newdeck-body .nd-mode-opt 这种权重,并且放在上面那条基础 input 规则之后——
|
|
376
|
+
否则 radio 会被 `.newdeck-body input{width:100%;padding:7px 9px}` 接管:撑满整行、把文字挤成一字一行
|
|
377
|
+
(0.2.18 的排版事故就是同权重 + 顺序靠前导致的)。 */
|
|
378
|
+
.newdeck-body .nd-mode{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin:0 0 10px}
|
|
379
|
+
.newdeck-body .nd-mode-opt{display:flex;align-items:flex-start;gap:8px;padding:8px 10px;border:1px solid var(--line);border-radius:6px;background:#fffdf8;cursor:pointer;font-size:12px;color:var(--ink);min-width:0}
|
|
380
|
+
.newdeck-body .nd-mode-opt input[type="radio"]{width:auto;height:auto;flex:0 0 auto;margin:2px 0 0;padding:0;border:0;background:none;accent-color:#0c4651}
|
|
381
|
+
.newdeck-body .nd-mode-opt > span{flex:1 1 auto;display:flex;flex-direction:column;gap:2px;min-width:0}
|
|
382
|
+
.newdeck-body .nd-mode-opt b{font-weight:600;font-size:12px;line-height:1.35}
|
|
383
|
+
.newdeck-body .nd-mode-opt em{font-style:normal;font-size:11px;line-height:1.45;color:#728086;overflow-wrap:anywhere}
|
|
384
|
+
.newdeck-body .nd-mode-opt:has(input:checked){border-color:#0c4651;box-shadow:inset 0 0 0 1px #0c4651}
|
|
385
|
+
.newdeck-body .nd-mode-opt:has(input:checked) em{color:#48605f}
|
|
386
|
+
.newdeck-body .nd-mode-note{margin:0 0 10px}
|
|
374
387
|
/* 新建 Deck · 风格选择:样张预览卡片(F-45) */
|
|
375
388
|
.nd-styles{display:grid;grid-template-columns:repeat(auto-fill,minmax(158px,1fr));gap:8px;margin-bottom:8px}
|
|
376
389
|
.nd-style{display:flex;flex-direction:column;padding:0;border:1px solid var(--line);border-radius:8px;background:#fffdf8;cursor:pointer;font:inherit;text-align:left;overflow:hidden;transition:border-color .12s,box-shadow .12s}
|
|
@@ -677,7 +690,10 @@ body.has-chat .layout{grid-template-columns:var(--nav-w,430px) 5px minmax(0,1fr)
|
|
|
677
690
|
body.presenting{overflow:hidden}
|
|
678
691
|
.present-stage{width:100%;height:100%;display:flex;align-items:center;justify-content:center;padding:22px 22px 82px;cursor:pointer}
|
|
679
692
|
.present-slide{margin:0;width:100%;height:100%;display:flex;align-items:center;justify-content:center}
|
|
680
|
-
.
|
|
693
|
+
/* 0.2.24:放映时整页图必须随屏幕放大——以前只写 max-width/max-height,图小于画布时永远保持
|
|
694
|
+
自然尺寸(1280×720 的页图在 2K/4K 屏上就缩在中间一小块)。改成 width/height:100% + object-fit:contain,
|
|
695
|
+
大图缩小、小图放大都按比例铺满;阴影改用 drop-shadow,跟着图片轮廓而不是整块元素。 */
|
|
696
|
+
.present-slide img{width:100%;height:100%;max-width:none;max-height:none;object-fit:contain;border-radius:0;background:transparent;filter:drop-shadow(0 22px 62px #000b);user-select:none;-webkit-user-drag:none}
|
|
681
697
|
.present-empty{width:min(900px,82vw);aspect-ratio:16/9;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;border:1px dashed #ffffff2e;border-radius:8px;color:#c9c2b4;font-size:13px;text-align:center;padding:24px}
|
|
682
698
|
.present-empty b{font-size:20px;color:#f3ede1}
|
|
683
699
|
.present-bar{position:absolute;left:50%;bottom:18px;transform:translateX(-50%);display:flex;align-items:center;gap:10px;padding:6px 10px 6px 6px;border-radius:999px;background:#0c4651d9;border:1px solid #ffffff24;box-shadow:0 12px 34px #0009;font-size:12px;color:#e9e4d8;max-width:calc(100% - 28px);backdrop-filter:blur(8px)}
|
package/ui/workbench.html
CHANGED
|
@@ -177,6 +177,11 @@
|
|
|
177
177
|
<button class="modal-close" id="ndClose" title="关闭">✕</button>
|
|
178
178
|
</div>
|
|
179
179
|
<form class="modal-body newdeck-body" id="newDeckForm">
|
|
180
|
+
<div class="nd-mode" id="ndMode" role="radiogroup" aria-label="开工方式">
|
|
181
|
+
<label class="nd-mode-opt"><input type="radio" name="ndMode" value="form" checked><span><b>表单模式</b><em>受众 / 意图 / 页数先定清楚</em></span></label>
|
|
182
|
+
<label class="nd-mode-opt"><input type="radio" name="ndMode" value="explore"><span><b>探索模式</b><em>只有一句模糊想法也能开工,边聊边定</em></span></label>
|
|
183
|
+
</div>
|
|
184
|
+
<p class="engine-note nd-mode-note" id="ndModeNote" hidden>探索模式只留「标题 + 风格」:其余字段先隐藏。建完 Agent 会在对话里一次问清听众 / 想让人带走什么 / 场合 / 材料 / 边界,再给你一版故事线草案——问清之前不出图、不留 TODO。</p>
|
|
180
185
|
<p class="nd-tip">先定「讲给谁、讲什么、想带走什么」+ 视觉风格。创建后会开出源目录与 9 份文档骨架,然后在对话里继续补资料、改 script,直到可以出图。<br>新项目保存位置:<code id="ndStoragePath">正在读取配置…</code>。<span id="ndStorageNote"></span></p>
|
|
181
186
|
<label class="field"><span>主题 / 标题</span><input id="ndTitle" required placeholder="例:XX 业务场景的 AI 落地分享"></label>
|
|
182
187
|
<label class="field"><span>项目目录名(可选,英文小写中划线;留空自动生成)</span><input id="ndDirName" placeholder="例:talk-ai-transformation-6-stages"></label>
|
|
@@ -599,6 +604,59 @@ const DESKTOP_HOST = !/^(https?:)$/.test(location.protocol)
|
|
|
599
604
|
|
|
600
605
|
const esc = (v) => String(v ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
|
|
601
606
|
const getEl = (id) => document.getElementById(id);
|
|
607
|
+
|
|
608
|
+
/* 0.2.21:宿主「点左侧会话 → 打开这本 deck」的指令常常比本脚本启动还早(面板刚挂载就开始发),
|
|
609
|
+
旧实现把监听注册在 boot 末尾,消息就直接丢了——表现成「工作区弹出来了,但停在首页」。
|
|
610
|
+
这里一启动就注册监听:没 boot 完先暂存,boot 完再执行,并回 ack 让宿主确认送达。 */
|
|
611
|
+
const OPEN_BINDING = { pending: null, booted: false };
|
|
612
|
+
|
|
613
|
+
function ackOpenBinding(data, result) {
|
|
614
|
+
try {
|
|
615
|
+
window.parent?.postMessage({ type: "dsh-deck-studio:open-binding-ack", id: data?.id ?? "", kind: data?.kind ?? "", ...result }, "*");
|
|
616
|
+
} catch {
|
|
617
|
+
/* 独立打开(无宿主)时忽略 */
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function applyOpenBinding(data) {
|
|
622
|
+
if (!data || typeof data !== "object") return;
|
|
623
|
+
if (data.notice) {
|
|
624
|
+
flash(data.notice);
|
|
625
|
+
ackOpenBinding(data, { ok: true, action: "notice" });
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
if (!data.id) return;
|
|
629
|
+
if (data.kind === "design") {
|
|
630
|
+
const project = (STATE.data?.designProjects || []).find((item) => item.designId === data.id);
|
|
631
|
+
const canvasId = project?.canvasDeckId || "";
|
|
632
|
+
if (!canvasId) {
|
|
633
|
+
flash(`找不到这个风格的画布:${data.id}`, "error");
|
|
634
|
+
ackOpenBinding(data, { ok: false, reason: "design canvas not found" });
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
showDeck(canvasId)
|
|
638
|
+
.then(() => ackOpenBinding(data, { ok: true, deckId: canvasId }))
|
|
639
|
+
.catch((error) => {
|
|
640
|
+
flash(`打开风格画布失败:${error?.message ?? error}`, "error");
|
|
641
|
+
ackOpenBinding(data, { ok: false, reason: String(error?.message ?? error) });
|
|
642
|
+
});
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
showDeck(data.id)
|
|
646
|
+
.then(() => ackOpenBinding(data, { ok: true, deckId: data.id }))
|
|
647
|
+
.catch((error) => {
|
|
648
|
+
flash(`打开 deck 失败:${error?.message ?? error}`, "error");
|
|
649
|
+
ackOpenBinding(data, { ok: false, reason: String(error?.message ?? error) });
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
window.addEventListener("message", (event) => {
|
|
654
|
+
if (event.source !== window.parent) return;
|
|
655
|
+
const data = event.data ?? {};
|
|
656
|
+
if (data.type !== "dsh-deck-studio:open-binding") return;
|
|
657
|
+
if (OPEN_BINDING.booted) applyOpenBinding(data);
|
|
658
|
+
else OPEN_BINDING.pending = data;
|
|
659
|
+
});
|
|
602
660
|
const fmtBytes = (n) => (n ? (n > 1048576 ? (n / 1048576).toFixed(1) + " MB" : Math.round(n / 1024) + " KB") : "");
|
|
603
661
|
const fmtTime = (iso) => String(iso ?? "").replace("T", " ").replace(/\.\d+Z$/, "Z").replace("Z", "").slice(5);
|
|
604
662
|
|
|
@@ -1648,8 +1706,8 @@ function renderHomeDecks(decks, active, gov) {
|
|
|
1648
1706
|
${deck.deletable ? `<button type="button" class="design-tool danger deck-tool" data-deck-delete="${esc(deck.deckId)}" title="删除这个 deck(本机新建的才可以)">${TRASH_ICON}</button>` : ""}
|
|
1649
1707
|
<div class="home-block-media">${homeThumb(deck)}</div>
|
|
1650
1708
|
<div class="home-block-body">
|
|
1651
|
-
<div class="home-block-title">${esc(homeDeckTitle(deck))}${deck.kind === "design-canvas" ? `<span class="pill canvas-pill">风格画布</span>` : ""}</div>
|
|
1652
|
-
<div class="home-block-meta">${deck.kind === "design-canvas" ? "风格画布" : `${esc(String(deck.pageCount ?? 0))} 页`}${score != null ? ` · 治理 ${score}/100` : ""}</div>
|
|
1709
|
+
<div class="home-block-title">${esc(homeDeckTitle(deck))}${deck.kind === "design-canvas" ? `<span class="pill canvas-pill">风格画布</span>` : ""}${deck.unmounted ? `<span class="pill canvas-pill" title="这份 deck 的源目录不在了(只剩工作副本):点开在右侧 Inspector 换源 / 重新登记">源未挂载</span>` : ""}</div>
|
|
1710
|
+
<div class="home-block-meta">${deck.unmounted ? "源未挂载 · 只剩工作副本" : deck.kind === "design-canvas" ? "风格画布" : `${esc(String(deck.pageCount ?? 0))} 页`}${score != null ? ` · 治理 ${score}/100` : ""}</div>
|
|
1653
1711
|
${styleChip || docTags || rulesChip ? `<div class="home-block-tags">${styleChip}${rulesChip}${docTags}</div>` : ""}
|
|
1654
1712
|
</div>
|
|
1655
1713
|
</div>`;
|
|
@@ -4657,6 +4715,41 @@ function newDeckStorageInfo(config = {}) {
|
|
|
4657
4715
|
return { path: "暂时无法确定", note: "请等待工作台配置加载后重试;未配置 projectsRoot 时使用 <dataRoot>/projects/<deck-id>/source/。" };
|
|
4658
4716
|
}
|
|
4659
4717
|
|
|
4718
|
+
/** 0.2.18:探索模式要藏起来的表单字段(标题 / 风格 / 参考资料之外的「先填清楚」项)。 */
|
|
4719
|
+
const ND_FORM_ONLY = ["ndDirName", "ndPages", "ndDensity", "ndAudience", "ndTopic", "ndIntent", "ndOccasion", "ndSuccess", "ndScopeOut", "ndRefs"];
|
|
4720
|
+
|
|
4721
|
+
/** 当前开工方式:form(表单)/ explore(探索,主意还没定清楚)。 */
|
|
4722
|
+
function newDeckMode() {
|
|
4723
|
+
return document.querySelector('#ndMode input[name="ndMode"]:checked')?.value === "explore" ? "explore" : "form";
|
|
4724
|
+
}
|
|
4725
|
+
|
|
4726
|
+
/**
|
|
4727
|
+
* 切换开工方式:探索式只留「标题 + 风格」,其余字段隐藏(值保留,切回表单还在);
|
|
4728
|
+
* 隐藏的字段不能用 hidden 属性就完事——.newdeck-body .field 是 display:flex,会盖掉浏览器默认样式。
|
|
4729
|
+
*/
|
|
4730
|
+
function applyNewDeckMode() {
|
|
4731
|
+
const explore = newDeckMode() === "explore";
|
|
4732
|
+
for (const id of ND_FORM_ONLY) {
|
|
4733
|
+
const field = getEl(id)?.closest(".field");
|
|
4734
|
+
if (!field) continue;
|
|
4735
|
+
field.hidden = explore;
|
|
4736
|
+
field.style.display = explore ? "none" : "";
|
|
4737
|
+
}
|
|
4738
|
+
// 成对的行(场合 / 成功标准)两个字段都藏起来后,行本身也要收掉,否则会留一条空栅格。
|
|
4739
|
+
document.querySelectorAll("#newDeckForm .nd-row").forEach((row) => {
|
|
4740
|
+
const allHidden = [...row.querySelectorAll(".field")].every((field) => field.hidden);
|
|
4741
|
+
row.hidden = explore && allHidden;
|
|
4742
|
+
row.style.display = explore && allHidden ? "none" : "";
|
|
4743
|
+
});
|
|
4744
|
+
const pages = getEl("ndPages");
|
|
4745
|
+
if (pages) pages.required = !explore;
|
|
4746
|
+
getEl("ndModeNote").hidden = !explore;
|
|
4747
|
+
const button = getEl("ndCreate");
|
|
4748
|
+
if (button) button.textContent = explore ? "开始探索 →" : "创建并开始对话";
|
|
4749
|
+
getEl("newDeckModal").dataset.mode = explore ? "explore" : "form";
|
|
4750
|
+
return explore;
|
|
4751
|
+
}
|
|
4752
|
+
|
|
4660
4753
|
function openNewDeckModal() {
|
|
4661
4754
|
// F-112:profile 里配了 defaultStyleAbbr 就默认预选这个风格(用户仍可改/取消)。
|
|
4662
4755
|
STATE.ndStyleAbbr = String(STATE.data?.config?.defaultStyleAbbr || "");
|
|
@@ -4666,6 +4759,7 @@ function openNewDeckModal() {
|
|
|
4666
4759
|
getEl("ndStoragePath").textContent = storage.path;
|
|
4667
4760
|
getEl("ndStorageNote").textContent = storage.note;
|
|
4668
4761
|
getEl("newDeckModal").hidden = false;
|
|
4762
|
+
applyNewDeckMode();
|
|
4669
4763
|
renderDeckStyleChoices();
|
|
4670
4764
|
setTimeout(() => getEl("ndTitle").focus(), 30);
|
|
4671
4765
|
}
|
|
@@ -4678,26 +4772,33 @@ getEl("newDeckBtn").addEventListener("click", openNewDeckModal);
|
|
|
4678
4772
|
getEl("ndClose").addEventListener("click", closeNewDeckModal);
|
|
4679
4773
|
getEl("ndCancel").addEventListener("click", closeNewDeckModal);
|
|
4680
4774
|
getEl("newDeckModal").addEventListener("click", (event) => { if (event.target === getEl("newDeckModal")) closeNewDeckModal(); });
|
|
4775
|
+
getEl("ndMode").addEventListener("change", applyNewDeckMode);
|
|
4681
4776
|
getEl("newDeckForm").addEventListener("submit", async (event) => {
|
|
4682
4777
|
event.preventDefault();
|
|
4683
|
-
const
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
audience: getEl("ndAudience").value.trim(),
|
|
4689
|
-
topic: getEl("ndTopic").value.trim(),
|
|
4690
|
-
intent: getEl("ndIntent").value.trim(),
|
|
4691
|
-
occasion: getEl("ndOccasion").value.trim(),
|
|
4692
|
-
success: getEl("ndSuccess").value.trim(),
|
|
4693
|
-
scopeOut: getEl("ndScopeOut").value.trim(),
|
|
4694
|
-
references: getEl("ndRefs").value.trim(),
|
|
4695
|
-
style: {
|
|
4696
|
-
abbr: STATE.ndStyleAbbr || "",
|
|
4697
|
-
// preset 选中的话,description 仍带完整风格定义(只是不给用户看)
|
|
4698
|
-
description: pickedDeckStyle()?.description || getEl("ndStyle").value.trim(),
|
|
4699
|
-
},
|
|
4778
|
+
const explore = newDeckMode() === "explore";
|
|
4779
|
+
const style = {
|
|
4780
|
+
abbr: STATE.ndStyleAbbr || "",
|
|
4781
|
+
// preset 选中的话,description 仍带完整风格定义(只是不给用户看)
|
|
4782
|
+
description: pickedDeckStyle()?.description || getEl("ndStyle").value.trim(),
|
|
4700
4783
|
};
|
|
4784
|
+
// 0.2.18:探索式只提交「标题 + 风格」——被隐藏字段里可能还留着旧值,不能偷偷带上;
|
|
4785
|
+
// 页数先占 1 页占位,等对话里把故事线聊清楚再追加。
|
|
4786
|
+
const payload = explore
|
|
4787
|
+
? { title: getEl("ndTitle").value.trim(), mode: "explore", pages: 1, style }
|
|
4788
|
+
: {
|
|
4789
|
+
title: getEl("ndTitle").value.trim(),
|
|
4790
|
+
dirName: getEl("ndDirName").value.trim(),
|
|
4791
|
+
pages: Math.min(Math.max(Number.parseInt(getEl("ndPages").value, 10) || 6, 1), 40),
|
|
4792
|
+
contentDensity: getEl("ndDensity").value || "auto",
|
|
4793
|
+
audience: getEl("ndAudience").value.trim(),
|
|
4794
|
+
topic: getEl("ndTopic").value.trim(),
|
|
4795
|
+
intent: getEl("ndIntent").value.trim(),
|
|
4796
|
+
occasion: getEl("ndOccasion").value.trim(),
|
|
4797
|
+
success: getEl("ndSuccess").value.trim(),
|
|
4798
|
+
scopeOut: getEl("ndScopeOut").value.trim(),
|
|
4799
|
+
references: getEl("ndRefs").value.trim(),
|
|
4800
|
+
style,
|
|
4801
|
+
};
|
|
4701
4802
|
if (!payload.title) return;
|
|
4702
4803
|
getEl("ndCreate").disabled = true;
|
|
4703
4804
|
getEl("ndState").textContent = "正在创建 deck 项目(源目录 + 9 份文档骨架)…";
|
|
@@ -4716,6 +4817,18 @@ getEl("newDeckForm").addEventListener("submit", async (event) => {
|
|
|
4716
4817
|
// 停在 home(loadWorkbench 在 home 视图下只会重渲染首页,用户会以为「没自动打开」)。
|
|
4717
4818
|
await showDeck(deckId);
|
|
4718
4819
|
getEl("renderStatus").textContent = `已创建 ${label};对话在 DSH 原生 Chat 里继续。`;
|
|
4820
|
+
if (explore) {
|
|
4821
|
+
sendPrompt(
|
|
4822
|
+
`我刚新建了 deck「${label}」(${deckId}),用的是**探索模式**:我一开始只有个模糊想法,没有现成简报。\n` +
|
|
4823
|
+
`请按这个顺序把我带清楚,不要留 TODO、不要出图:\n` +
|
|
4824
|
+
`1) 先一次问完 3–5 个关键问题(讲给谁 / 听完要带走什么 / 什么场合和时长 / 手上有没有现成材料 / 有没有必须避开的边界),别一条一条挤牙膏;\n` +
|
|
4825
|
+
`2) 我回答后,用 deck_doc_update(docId=intent)把听众 / 带走 / 场合 / 成功标准 / 不讲的写回去;没依据的字段写成「待确认:缺依据,已列问题」,不要编,也不要留模板占位;\n` +
|
|
4826
|
+
`3) 再给一版故事线草案:分几段、每段回答什么、大概几页(不确定就给区间),等我确认;\n` +
|
|
4827
|
+
`4) 我确认后再用 deck_script_add_page / 提案改 script;要出图我会明确说。\n` +
|
|
4828
|
+
`材料里没有的数字 / 客户 / 案例 / URL 不要编。`,
|
|
4829
|
+
"探索式新建 · 首条消息发给 Agent",
|
|
4830
|
+
);
|
|
4831
|
+
} else {
|
|
4719
4832
|
sendPrompt(
|
|
4720
4833
|
`我刚新建了 deck「${label}」(${deckId})。\n` +
|
|
4721
4834
|
`页数:已按 ${payload.pages} 页占位(P01–P${String(payload.pages).padStart(2, "0")});页数要改就在对话里说,用 deck_script_add_page 追加。\n` +
|
|
@@ -4737,6 +4850,7 @@ getEl("newDeckForm").addEventListener("submit", async (event) => {
|
|
|
4737
4850
|
`材料里没有的数字 / 客户 / 案例 / URL 不要编。做完复述你补了什么、还缺什么,再等我确认后起 script 草案,不要直接出图。`,
|
|
4738
4851
|
"新建 deck · 下一步发给 Agent",
|
|
4739
4852
|
);
|
|
4853
|
+
}
|
|
4740
4854
|
} catch (error) {
|
|
4741
4855
|
getEl("ndState").textContent = "创建失败:" + error.message;
|
|
4742
4856
|
} finally {
|
|
@@ -5107,6 +5221,8 @@ function requestConversation(payload) {
|
|
|
5107
5221
|
cwd: STATE.data?.cwd || "",
|
|
5108
5222
|
workspacePath: STATE.data?.workspacePath || "",
|
|
5109
5223
|
sourceRoot: STATE.data?.active?.sourceRoot || "",
|
|
5224
|
+
// 0.2.16:宿主要按「deck 自己的项目目录」建会话工作区(桌面里 cwd 是 profile 目录,用不了)。
|
|
5225
|
+
sessionWorkspacePath: STATE.data?.active?.sessionWorkspacePath || "",
|
|
5110
5226
|
...payload,
|
|
5111
5227
|
// F-202B:桌面 Dock 模式默认不抢主区(open:false);显式 takeMain → open:true 切宿主主区。
|
|
5112
5228
|
// web 不传 open(undefined → 宿主按原语义 openSession)。
|
|
@@ -5184,8 +5300,11 @@ async function ensureDeckConversation(deckId) {
|
|
|
5184
5300
|
});
|
|
5185
5301
|
if (!result.ok) {
|
|
5186
5302
|
getEl("chatBindChip").textContent = `会话打开失败:${result.reason || "未知原因"}`;
|
|
5303
|
+
flash(`会话打开失败:${result.reason || "未知原因"}`, "error");
|
|
5187
5304
|
return result;
|
|
5188
5305
|
}
|
|
5306
|
+
// 0.2.20:会话建好了但首条指令没发出去时,宿主会带 notice——别让它悄悄消失。
|
|
5307
|
+
if (result.notice) flash(result.notice);
|
|
5189
5308
|
if (!binding || binding.sessionId !== result.sessionId) {
|
|
5190
5309
|
await bindConversation({ sessionId: result.sessionId, kind: "deck", id: deckId, title: label });
|
|
5191
5310
|
}
|
|
@@ -5591,23 +5710,33 @@ function renderChatHeader() {
|
|
|
5591
5710
|
/** + 新建会话:建完立即绑定当前 deck / design(不再产生未绑定会话)。 */
|
|
5592
5711
|
async function newChatForTarget() {
|
|
5593
5712
|
const target = activeChatTarget();
|
|
5594
|
-
if (!target)
|
|
5713
|
+
if (!target) {
|
|
5714
|
+
// 0.2.16:以前这里静默 return——点了 + 什么都不发生,用户只能猜。
|
|
5715
|
+
flash("当前没有可绑定的 deck / 风格:先打开一本 deck 再新建会话", "error");
|
|
5716
|
+
return;
|
|
5717
|
+
}
|
|
5595
5718
|
const button = getEl("newChatBtn");
|
|
5596
5719
|
button.disabled = true;
|
|
5597
5720
|
try {
|
|
5598
5721
|
const result = await requestConversation({ action: "ensure", kind: target.kind, id: target.id, title: target.label });
|
|
5599
5722
|
if (!result.ok || !result.sessionId) {
|
|
5723
|
+
const reason = result.reason || "未知原因";
|
|
5600
5724
|
// F-202B:失败要用户可见——之前只写按钮 title,Dock 模式下看起来像「点了没反应」。
|
|
5725
|
+
// 0.2.16:再加一条常驻 toast,光靠 chip 文案还是容易被当成没反应。
|
|
5726
|
+
flash(`新建会话失败:${reason}`, "error");
|
|
5601
5727
|
getEl("chatBindChip").textContent = "会话新建失败";
|
|
5602
|
-
getEl("chatBindChip").title = `新建会话失败:${
|
|
5603
|
-
getEl("chatSessionBtn").title = `新建会话失败:${
|
|
5728
|
+
getEl("chatBindChip").title = `新建会话失败:${reason};点这里重试`;
|
|
5729
|
+
getEl("chatSessionBtn").title = `新建会话失败:${reason}`;
|
|
5604
5730
|
return;
|
|
5605
5731
|
}
|
|
5606
5732
|
STATE.chatSessionId = result.sessionId;
|
|
5733
|
+
if (result.notice) flash(result.notice);
|
|
5607
5734
|
await bindConversation({ sessionId: result.sessionId, kind: target.kind, id: target.id, title: target.label });
|
|
5608
5735
|
await refreshSessions();
|
|
5609
5736
|
renderChatHeader();
|
|
5610
5737
|
// 桌面:新建就是要点开聊——切到宿主主区的原生会话。
|
|
5738
|
+
} catch (error) {
|
|
5739
|
+
flash(`新建会话失败:${error?.message ?? error}`, "error");
|
|
5611
5740
|
} finally {
|
|
5612
5741
|
button.disabled = false;
|
|
5613
5742
|
}
|
|
@@ -5900,6 +6029,22 @@ function backToManual(box) {
|
|
|
5900
6029
|
showHome();
|
|
5901
6030
|
}
|
|
5902
6031
|
setInterval(() => loadWorkbench(STATE.deckId), 5000);
|
|
6032
|
+
|
|
6033
|
+
/**
|
|
6034
|
+
* 0.2.21:boot 完成 → 把启动期间暂存的「打开某本 deck」执行掉,再向宿主报 ready。
|
|
6035
|
+
* 顺序很关键:先 booted=true 再执行,避免执行期间又来一条指令被丢进 pending 里没人取。
|
|
6036
|
+
*/
|
|
6037
|
+
OPEN_BINDING.booted = true;
|
|
6038
|
+
if (OPEN_BINDING.pending) {
|
|
6039
|
+
const pending = OPEN_BINDING.pending;
|
|
6040
|
+
OPEN_BINDING.pending = null;
|
|
6041
|
+
applyOpenBinding(pending);
|
|
6042
|
+
}
|
|
6043
|
+
try {
|
|
6044
|
+
window.parent?.postMessage({ type: "dsh-deck-studio:workbench-ready" }, "*");
|
|
6045
|
+
} catch {
|
|
6046
|
+
/* 直接打开(无宿主)时没有 parent */
|
|
6047
|
+
}
|
|
5903
6048
|
})();
|
|
5904
6049
|
</script>
|
|
5905
6050
|
</body>
|