dsh-plugin-deck-studio 0.2.14 → 0.2.25
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 +380 -12
- package/lib/capabilities.js +3 -2
- package/lib/deck-templates.js +6 -1
- package/lib/pdf-import.js +270 -0
- package/lib/present-native.js +17 -0
- package/lib/review-agent.js +2 -1
- package/package.json +1 -1
- package/ui/workbench.css +17 -1
- package/ui/workbench.html +464 -25
package/ui/workbench.html
CHANGED
|
@@ -128,6 +128,7 @@
|
|
|
128
128
|
<section id="homeView" class="home">
|
|
129
129
|
<aside class="home-nav">
|
|
130
130
|
<button class="btn rail-new home-new-deck" id="newDeckBtn">+ 新建 Deck</button>
|
|
131
|
+
<button class="btn alt rail-new home-new-deck" id="importPdfBtn" title="把一份 PDF 的每一页转成整页图,建一本 deck">⇪ 导入 PDF</button>
|
|
131
132
|
<nav class="home-nav-list" id="homeNav">
|
|
132
133
|
<button class="home-nav-item" data-section="decks"><span class="hn-icon">▦</span><span class="hn-label">Decks</span><em id="navCountDecks">0</em></button>
|
|
133
134
|
<button class="home-nav-item" data-section="designs"><span class="hn-icon">◈</span><span class="hn-label">Designs</span><em id="navCountDesigns">0</em></button>
|
|
@@ -177,6 +178,11 @@
|
|
|
177
178
|
<button class="modal-close" id="ndClose" title="关闭">✕</button>
|
|
178
179
|
</div>
|
|
179
180
|
<form class="modal-body newdeck-body" id="newDeckForm">
|
|
181
|
+
<div class="nd-mode" id="ndMode" role="radiogroup" aria-label="开工方式">
|
|
182
|
+
<label class="nd-mode-opt"><input type="radio" name="ndMode" value="form" checked><span><b>表单模式</b><em>受众 / 意图 / 页数先定清楚</em></span></label>
|
|
183
|
+
<label class="nd-mode-opt"><input type="radio" name="ndMode" value="explore"><span><b>探索模式</b><em>只有一句模糊想法也能开工,边聊边定</em></span></label>
|
|
184
|
+
</div>
|
|
185
|
+
<p class="engine-note nd-mode-note" id="ndModeNote" hidden>探索模式只留「标题 + 风格」:其余字段先隐藏。建完 Agent 会在对话里一次问清听众 / 想让人带走什么 / 场合 / 材料 / 边界,再给你一版故事线草案——问清之前不出图、不留 TODO。</p>
|
|
180
186
|
<p class="nd-tip">先定「讲给谁、讲什么、想带走什么」+ 视觉风格。创建后会开出源目录与 9 份文档骨架,然后在对话里继续补资料、改 script,直到可以出图。<br>新项目保存位置:<code id="ndStoragePath">正在读取配置…</code>。<span id="ndStorageNote"></span></p>
|
|
181
187
|
<label class="field"><span>主题 / 标题</span><input id="ndTitle" required placeholder="例:XX 业务场景的 AI 落地分享"></label>
|
|
182
188
|
<label class="field"><span>项目目录名(可选,英文小写中划线;留空自动生成)</span><input id="ndDirName" placeholder="例:talk-ai-transformation-6-stages"></label>
|
|
@@ -210,6 +216,42 @@
|
|
|
210
216
|
</div>
|
|
211
217
|
</div>
|
|
212
218
|
|
|
219
|
+
<div id="importPdfModal" class="modal" hidden>
|
|
220
|
+
<div class="modal-card newdeck-card">
|
|
221
|
+
<div class="modal-head">
|
|
222
|
+
<div><div class="eyebrow">Import</div><h2>导入 PDF</h2></div>
|
|
223
|
+
<button class="modal-close" id="impClose" title="关闭">✕</button>
|
|
224
|
+
</div>
|
|
225
|
+
<div class="modal-body newdeck-body">
|
|
226
|
+
<p class="nd-tip">点一份 PDF:每一页会转成一张整页图,建一本新 deck。
|
|
227
|
+
<br>同一个文件夹里有 md 就顺手抽(页数对得上当脚本,其余进支撑材料),没有就只导入图。
|
|
228
|
+
<br>只支持 PDF(本机 poppler 的 <code>pdftoppm</code>);单次上限 40 页。</p>
|
|
229
|
+
<div class="imp-picker">
|
|
230
|
+
<div class="imp-picker-bar">
|
|
231
|
+
<button type="button" class="btn ghost" id="impUp">↑ 上一级</button>
|
|
232
|
+
<code id="impPath">—</code>
|
|
233
|
+
</div>
|
|
234
|
+
<div class="imp-picker-list" id="impList"><div class="dir-picker-empty">正在读目录…</div></div>
|
|
235
|
+
</div>
|
|
236
|
+
<p class="engine-note" id="impSelected">还没有选 PDF:点上面的 📄 文件即可。</p>
|
|
237
|
+
<div class="nd-foot">
|
|
238
|
+
<label class="imp-dpi">清晰度
|
|
239
|
+
<select id="impDpi">
|
|
240
|
+
<option value="96">96 · 快</option>
|
|
241
|
+
<option value="120" selected>120 · 默认</option>
|
|
242
|
+
<option value="200">200 · 更清</option>
|
|
243
|
+
<option value="300">300 · 最清最慢</option>
|
|
244
|
+
</select>
|
|
245
|
+
</label>
|
|
246
|
+
<span class="spacer"></span>
|
|
247
|
+
<button type="button" class="btn alt" id="impCancel">取消</button>
|
|
248
|
+
<button type="button" class="btn" id="impSubmit" disabled>开始导入</button>
|
|
249
|
+
</div>
|
|
250
|
+
<p class="engine-note" id="impState"></p>
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
|
|
213
255
|
<div id="confirmModal" class="modal" hidden>
|
|
214
256
|
<div class="modal-card confirm-card">
|
|
215
257
|
<div class="modal-head">
|
|
@@ -513,6 +555,17 @@
|
|
|
513
555
|
.dir-picker-tag{font-size:10.5px;color:#0f6975}
|
|
514
556
|
.dir-picker-count{font-size:10.5px;color:#8a7a58}
|
|
515
557
|
.dir-picker-list{border:1px solid #e3e1d8;border-radius:10px;max-height:42vh;overflow:auto;background:#fff}
|
|
558
|
+
/* F-203:导入 PDF —— 直接在弹层里点文件,不让人手打目录 / 文件名 */
|
|
559
|
+
.imp-picker{border:1px solid #e4ded0;border-radius:10px;overflow:hidden;margin:8px 0 10px;background:#fff}
|
|
560
|
+
.imp-picker-bar{display:flex;align-items:center;gap:10px;padding:8px 10px;border-bottom:1px solid #f2efe6;background:#faf8f3}
|
|
561
|
+
.imp-picker-bar code{flex:1;min-width:0;font-size:11.5px;color:#6d7f83;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
562
|
+
.imp-picker-list{max-height:280px;overflow:auto;padding:2px}
|
|
563
|
+
.imp-picker-list .dir-picker-row.selected{background:#eaf4f1;box-shadow:inset 0 0 0 1px #0d6b62}
|
|
564
|
+
.imp-picker-list .dir-picker-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
565
|
+
.imp-picker-list .imp-find{background:#fbf7ec;border-top:1px solid #f2efe6;color:#8a5b12}
|
|
566
|
+
.imp-picker-list .imp-find:hover{background:#f7f1df}
|
|
567
|
+
.imp-dpi{display:flex;align-items:center;gap:6px;font-size:12px;color:#6d7f83}
|
|
568
|
+
.imp-dpi select{font-size:12px;padding:4px 6px;border-radius:8px;border:1px solid #dcd5c6;background:#fff}
|
|
516
569
|
.dir-picker-row{display:flex;gap:10px;align-items:center;width:100%;padding:8px 12px;border:0;border-bottom:1px solid #f2efe6;background:transparent;text-align:left;font-size:12.5px;color:#18343b;cursor:pointer}
|
|
517
570
|
.dir-picker-row:last-child{border-bottom:0}
|
|
518
571
|
.dir-picker-row:hover{background:#f6f9f8}
|
|
@@ -599,6 +652,59 @@ const DESKTOP_HOST = !/^(https?:)$/.test(location.protocol)
|
|
|
599
652
|
|
|
600
653
|
const esc = (v) => String(v ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
|
|
601
654
|
const getEl = (id) => document.getElementById(id);
|
|
655
|
+
|
|
656
|
+
/* 0.2.21:宿主「点左侧会话 → 打开这本 deck」的指令常常比本脚本启动还早(面板刚挂载就开始发),
|
|
657
|
+
旧实现把监听注册在 boot 末尾,消息就直接丢了——表现成「工作区弹出来了,但停在首页」。
|
|
658
|
+
这里一启动就注册监听:没 boot 完先暂存,boot 完再执行,并回 ack 让宿主确认送达。 */
|
|
659
|
+
const OPEN_BINDING = { pending: null, booted: false };
|
|
660
|
+
|
|
661
|
+
function ackOpenBinding(data, result) {
|
|
662
|
+
try {
|
|
663
|
+
window.parent?.postMessage({ type: "dsh-deck-studio:open-binding-ack", id: data?.id ?? "", kind: data?.kind ?? "", ...result }, "*");
|
|
664
|
+
} catch {
|
|
665
|
+
/* 独立打开(无宿主)时忽略 */
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function applyOpenBinding(data) {
|
|
670
|
+
if (!data || typeof data !== "object") return;
|
|
671
|
+
if (data.notice) {
|
|
672
|
+
flash(data.notice);
|
|
673
|
+
ackOpenBinding(data, { ok: true, action: "notice" });
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
if (!data.id) return;
|
|
677
|
+
if (data.kind === "design") {
|
|
678
|
+
const project = (STATE.data?.designProjects || []).find((item) => item.designId === data.id);
|
|
679
|
+
const canvasId = project?.canvasDeckId || "";
|
|
680
|
+
if (!canvasId) {
|
|
681
|
+
flash(`找不到这个风格的画布:${data.id}`, "error");
|
|
682
|
+
ackOpenBinding(data, { ok: false, reason: "design canvas not found" });
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
showDeck(canvasId)
|
|
686
|
+
.then(() => ackOpenBinding(data, { ok: true, deckId: canvasId }))
|
|
687
|
+
.catch((error) => {
|
|
688
|
+
flash(`打开风格画布失败:${error?.message ?? error}`, "error");
|
|
689
|
+
ackOpenBinding(data, { ok: false, reason: String(error?.message ?? error) });
|
|
690
|
+
});
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
showDeck(data.id)
|
|
694
|
+
.then(() => ackOpenBinding(data, { ok: true, deckId: data.id }))
|
|
695
|
+
.catch((error) => {
|
|
696
|
+
flash(`打开 deck 失败:${error?.message ?? error}`, "error");
|
|
697
|
+
ackOpenBinding(data, { ok: false, reason: String(error?.message ?? error) });
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
window.addEventListener("message", (event) => {
|
|
702
|
+
if (event.source !== window.parent) return;
|
|
703
|
+
const data = event.data ?? {};
|
|
704
|
+
if (data.type !== "dsh-deck-studio:open-binding") return;
|
|
705
|
+
if (OPEN_BINDING.booted) applyOpenBinding(data);
|
|
706
|
+
else OPEN_BINDING.pending = data;
|
|
707
|
+
});
|
|
602
708
|
const fmtBytes = (n) => (n ? (n > 1048576 ? (n / 1048576).toFixed(1) + " MB" : Math.round(n / 1024) + " KB") : "");
|
|
603
709
|
const fmtTime = (iso) => String(iso ?? "").replace("T", " ").replace(/\.\d+Z$/, "Z").replace("Z", "").slice(5);
|
|
604
710
|
|
|
@@ -1138,15 +1244,18 @@ function openDirPicker(options = {}) {
|
|
|
1138
1244
|
const modal = getEl("dirPickerModal");
|
|
1139
1245
|
if (!modal) return;
|
|
1140
1246
|
const dd = readDataDir(DIAG_DATA || STATE.data);
|
|
1141
|
-
|
|
1247
|
+
const targetId = options.targetId || "frDataRoot";
|
|
1248
|
+
// 只读闸只针对「选数据目录」;导入 PDF 选的是别人的目录,不该被 env 覆盖挡住。
|
|
1249
|
+
const dataDirTarget = targetId === "frDataRoot" || targetId === "frProjectsRoot";
|
|
1250
|
+
if (dataDirTarget && dd.envOverride.active) {
|
|
1142
1251
|
// env 覆盖时目录是只读的,选也没用——直接说清楚,不弹一个点不动的界面。
|
|
1143
1252
|
flash(ENV_READONLY_TEXT + ",改环境变量后重启 DSH", "error");
|
|
1144
1253
|
return;
|
|
1145
1254
|
}
|
|
1146
1255
|
DIR_PICKER = {
|
|
1147
1256
|
mode: options.mode === "switch" ? "switch" : "fill",
|
|
1148
|
-
targetId
|
|
1149
|
-
path: options.path || dd.dataRoot.value || dd.dataRoot.effective || "",
|
|
1257
|
+
targetId,
|
|
1258
|
+
path: options.path || (dataDirTarget ? dd.dataRoot.value || dd.dataRoot.effective || "" : ""),
|
|
1150
1259
|
data: null,
|
|
1151
1260
|
loading: true,
|
|
1152
1261
|
error: "",
|
|
@@ -1239,6 +1348,16 @@ async function confirmDirPicker() {
|
|
|
1239
1348
|
const path = st.data.path;
|
|
1240
1349
|
if (st.mode === "fill") {
|
|
1241
1350
|
const targetId = st.targetId;
|
|
1351
|
+
// 非数据目录的填写目标(导入 PDF 的目录)只往输入框里填,不借首次引导那张表单。
|
|
1352
|
+
if (targetId !== "frDataRoot" && targetId !== "frProjectsRoot") {
|
|
1353
|
+
closeDirPicker();
|
|
1354
|
+
const input = getEl(targetId);
|
|
1355
|
+
if (input) {
|
|
1356
|
+
input.value = path;
|
|
1357
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
1358
|
+
}
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1242
1361
|
closeDirPicker();
|
|
1243
1362
|
const wizard = getEl("firstRunModal");
|
|
1244
1363
|
const wizardOpen = wizard && wizard.hidden === false;
|
|
@@ -1648,8 +1767,8 @@ function renderHomeDecks(decks, active, gov) {
|
|
|
1648
1767
|
${deck.deletable ? `<button type="button" class="design-tool danger deck-tool" data-deck-delete="${esc(deck.deckId)}" title="删除这个 deck(本机新建的才可以)">${TRASH_ICON}</button>` : ""}
|
|
1649
1768
|
<div class="home-block-media">${homeThumb(deck)}</div>
|
|
1650
1769
|
<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>
|
|
1770
|
+
<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>
|
|
1771
|
+
<div class="home-block-meta">${deck.unmounted ? "源未挂载 · 只剩工作副本" : deck.kind === "design-canvas" ? "风格画布" : `${esc(String(deck.pageCount ?? 0))} 页`}${score != null ? ` · 治理 ${score}/100` : ""}</div>
|
|
1653
1772
|
${styleChip || docTags || rulesChip ? `<div class="home-block-tags">${styleChip}${rulesChip}${docTags}</div>` : ""}
|
|
1654
1773
|
</div>
|
|
1655
1774
|
</div>`;
|
|
@@ -4657,6 +4776,41 @@ function newDeckStorageInfo(config = {}) {
|
|
|
4657
4776
|
return { path: "暂时无法确定", note: "请等待工作台配置加载后重试;未配置 projectsRoot 时使用 <dataRoot>/projects/<deck-id>/source/。" };
|
|
4658
4777
|
}
|
|
4659
4778
|
|
|
4779
|
+
/** 0.2.18:探索模式要藏起来的表单字段(标题 / 风格 / 参考资料之外的「先填清楚」项)。 */
|
|
4780
|
+
const ND_FORM_ONLY = ["ndDirName", "ndPages", "ndDensity", "ndAudience", "ndTopic", "ndIntent", "ndOccasion", "ndSuccess", "ndScopeOut", "ndRefs"];
|
|
4781
|
+
|
|
4782
|
+
/** 当前开工方式:form(表单)/ explore(探索,主意还没定清楚)。 */
|
|
4783
|
+
function newDeckMode() {
|
|
4784
|
+
return document.querySelector('#ndMode input[name="ndMode"]:checked')?.value === "explore" ? "explore" : "form";
|
|
4785
|
+
}
|
|
4786
|
+
|
|
4787
|
+
/**
|
|
4788
|
+
* 切换开工方式:探索式只留「标题 + 风格」,其余字段隐藏(值保留,切回表单还在);
|
|
4789
|
+
* 隐藏的字段不能用 hidden 属性就完事——.newdeck-body .field 是 display:flex,会盖掉浏览器默认样式。
|
|
4790
|
+
*/
|
|
4791
|
+
function applyNewDeckMode() {
|
|
4792
|
+
const explore = newDeckMode() === "explore";
|
|
4793
|
+
for (const id of ND_FORM_ONLY) {
|
|
4794
|
+
const field = getEl(id)?.closest(".field");
|
|
4795
|
+
if (!field) continue;
|
|
4796
|
+
field.hidden = explore;
|
|
4797
|
+
field.style.display = explore ? "none" : "";
|
|
4798
|
+
}
|
|
4799
|
+
// 成对的行(场合 / 成功标准)两个字段都藏起来后,行本身也要收掉,否则会留一条空栅格。
|
|
4800
|
+
document.querySelectorAll("#newDeckForm .nd-row").forEach((row) => {
|
|
4801
|
+
const allHidden = [...row.querySelectorAll(".field")].every((field) => field.hidden);
|
|
4802
|
+
row.hidden = explore && allHidden;
|
|
4803
|
+
row.style.display = explore && allHidden ? "none" : "";
|
|
4804
|
+
});
|
|
4805
|
+
const pages = getEl("ndPages");
|
|
4806
|
+
if (pages) pages.required = !explore;
|
|
4807
|
+
getEl("ndModeNote").hidden = !explore;
|
|
4808
|
+
const button = getEl("ndCreate");
|
|
4809
|
+
if (button) button.textContent = explore ? "开始探索 →" : "创建并开始对话";
|
|
4810
|
+
getEl("newDeckModal").dataset.mode = explore ? "explore" : "form";
|
|
4811
|
+
return explore;
|
|
4812
|
+
}
|
|
4813
|
+
|
|
4660
4814
|
function openNewDeckModal() {
|
|
4661
4815
|
// F-112:profile 里配了 defaultStyleAbbr 就默认预选这个风格(用户仍可改/取消)。
|
|
4662
4816
|
STATE.ndStyleAbbr = String(STATE.data?.config?.defaultStyleAbbr || "");
|
|
@@ -4666,6 +4820,7 @@ function openNewDeckModal() {
|
|
|
4666
4820
|
getEl("ndStoragePath").textContent = storage.path;
|
|
4667
4821
|
getEl("ndStorageNote").textContent = storage.note;
|
|
4668
4822
|
getEl("newDeckModal").hidden = false;
|
|
4823
|
+
applyNewDeckMode();
|
|
4669
4824
|
renderDeckStyleChoices();
|
|
4670
4825
|
setTimeout(() => getEl("ndTitle").focus(), 30);
|
|
4671
4826
|
}
|
|
@@ -4678,26 +4833,33 @@ getEl("newDeckBtn").addEventListener("click", openNewDeckModal);
|
|
|
4678
4833
|
getEl("ndClose").addEventListener("click", closeNewDeckModal);
|
|
4679
4834
|
getEl("ndCancel").addEventListener("click", closeNewDeckModal);
|
|
4680
4835
|
getEl("newDeckModal").addEventListener("click", (event) => { if (event.target === getEl("newDeckModal")) closeNewDeckModal(); });
|
|
4836
|
+
getEl("ndMode").addEventListener("change", applyNewDeckMode);
|
|
4681
4837
|
getEl("newDeckForm").addEventListener("submit", async (event) => {
|
|
4682
4838
|
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
|
-
},
|
|
4839
|
+
const explore = newDeckMode() === "explore";
|
|
4840
|
+
const style = {
|
|
4841
|
+
abbr: STATE.ndStyleAbbr || "",
|
|
4842
|
+
// preset 选中的话,description 仍带完整风格定义(只是不给用户看)
|
|
4843
|
+
description: pickedDeckStyle()?.description || getEl("ndStyle").value.trim(),
|
|
4700
4844
|
};
|
|
4845
|
+
// 0.2.18:探索式只提交「标题 + 风格」——被隐藏字段里可能还留着旧值,不能偷偷带上;
|
|
4846
|
+
// 页数先占 1 页占位,等对话里把故事线聊清楚再追加。
|
|
4847
|
+
const payload = explore
|
|
4848
|
+
? { title: getEl("ndTitle").value.trim(), mode: "explore", pages: 1, style }
|
|
4849
|
+
: {
|
|
4850
|
+
title: getEl("ndTitle").value.trim(),
|
|
4851
|
+
dirName: getEl("ndDirName").value.trim(),
|
|
4852
|
+
pages: Math.min(Math.max(Number.parseInt(getEl("ndPages").value, 10) || 6, 1), 40),
|
|
4853
|
+
contentDensity: getEl("ndDensity").value || "auto",
|
|
4854
|
+
audience: getEl("ndAudience").value.trim(),
|
|
4855
|
+
topic: getEl("ndTopic").value.trim(),
|
|
4856
|
+
intent: getEl("ndIntent").value.trim(),
|
|
4857
|
+
occasion: getEl("ndOccasion").value.trim(),
|
|
4858
|
+
success: getEl("ndSuccess").value.trim(),
|
|
4859
|
+
scopeOut: getEl("ndScopeOut").value.trim(),
|
|
4860
|
+
references: getEl("ndRefs").value.trim(),
|
|
4861
|
+
style,
|
|
4862
|
+
};
|
|
4701
4863
|
if (!payload.title) return;
|
|
4702
4864
|
getEl("ndCreate").disabled = true;
|
|
4703
4865
|
getEl("ndState").textContent = "正在创建 deck 项目(源目录 + 9 份文档骨架)…";
|
|
@@ -4716,6 +4878,18 @@ getEl("newDeckForm").addEventListener("submit", async (event) => {
|
|
|
4716
4878
|
// 停在 home(loadWorkbench 在 home 视图下只会重渲染首页,用户会以为「没自动打开」)。
|
|
4717
4879
|
await showDeck(deckId);
|
|
4718
4880
|
getEl("renderStatus").textContent = `已创建 ${label};对话在 DSH 原生 Chat 里继续。`;
|
|
4881
|
+
if (explore) {
|
|
4882
|
+
sendPrompt(
|
|
4883
|
+
`我刚新建了 deck「${label}」(${deckId}),用的是**探索模式**:我一开始只有个模糊想法,没有现成简报。\n` +
|
|
4884
|
+
`请按这个顺序把我带清楚,不要留 TODO、不要出图:\n` +
|
|
4885
|
+
`1) 先一次问完 3–5 个关键问题(讲给谁 / 听完要带走什么 / 什么场合和时长 / 手上有没有现成材料 / 有没有必须避开的边界),别一条一条挤牙膏;\n` +
|
|
4886
|
+
`2) 我回答后,用 deck_doc_update(docId=intent)把听众 / 带走 / 场合 / 成功标准 / 不讲的写回去;没依据的字段写成「待确认:缺依据,已列问题」,不要编,也不要留模板占位;\n` +
|
|
4887
|
+
`3) 再给一版故事线草案:分几段、每段回答什么、大概几页(不确定就给区间),等我确认;\n` +
|
|
4888
|
+
`4) 我确认后再用 deck_script_add_page / 提案改 script;要出图我会明确说。\n` +
|
|
4889
|
+
`材料里没有的数字 / 客户 / 案例 / URL 不要编。`,
|
|
4890
|
+
"探索式新建 · 首条消息发给 Agent",
|
|
4891
|
+
);
|
|
4892
|
+
} else {
|
|
4719
4893
|
sendPrompt(
|
|
4720
4894
|
`我刚新建了 deck「${label}」(${deckId})。\n` +
|
|
4721
4895
|
`页数:已按 ${payload.pages} 页占位(P01–P${String(payload.pages).padStart(2, "0")});页数要改就在对话里说,用 deck_script_add_page 追加。\n` +
|
|
@@ -4737,6 +4911,7 @@ getEl("newDeckForm").addEventListener("submit", async (event) => {
|
|
|
4737
4911
|
`材料里没有的数字 / 客户 / 案例 / URL 不要编。做完复述你补了什么、还缺什么,再等我确认后起 script 草案,不要直接出图。`,
|
|
4738
4912
|
"新建 deck · 下一步发给 Agent",
|
|
4739
4913
|
);
|
|
4914
|
+
}
|
|
4740
4915
|
} catch (error) {
|
|
4741
4916
|
getEl("ndState").textContent = "创建失败:" + error.message;
|
|
4742
4917
|
} finally {
|
|
@@ -4744,6 +4919,239 @@ getEl("newDeckForm").addEventListener("submit", async (event) => {
|
|
|
4744
4919
|
}
|
|
4745
4920
|
});
|
|
4746
4921
|
|
|
4922
|
+
/* ---------------- 导入 PDF:点一份 PDF → 每页整页图建 deck;同目录 md 顺手抽(F-203) ----------------
|
|
4923
|
+
* 交互目标(负责人反馈):不要填目录 / 文件名 / 标题——直接浏览、点那份 PDF,同文件夹的 md 有就抽、没有拉倒。
|
|
4924
|
+
* 目录列表:GET /api/deck-studio.dir-browse?files=1(只回 pdf / md)
|
|
4925
|
+
* 子目录搜索:GET /api/deck-studio.dir-browse?find=pdf&path=…(深度 ≤3、最多 60 份,解决「这层只有文件夹」)
|
|
4926
|
+
* 导入:POST /api/deck-studio.deck-import { pdf: <绝对路径>, dpi } */
|
|
4927
|
+
const IMPORT_ERR_TEXT = {
|
|
4928
|
+
"pdftoppm-missing": "本机缺 poppler 的 pdftoppm:macOS 执行 `brew install poppler`,Linux 装 poppler-utils,装完重启 DSH。",
|
|
4929
|
+
"no-pdf": "这个文件夹里没有 PDF(或者这份 PDF 读不到)。",
|
|
4930
|
+
"not-found": "这个路径不存在,检查一下。",
|
|
4931
|
+
"bad-dir": "这个路径不对:要绝对路径,单次最多 40 页。",
|
|
4932
|
+
};
|
|
4933
|
+
|
|
4934
|
+
function importErrText(code, message) {
|
|
4935
|
+
return IMPORT_ERR_TEXT[code] || message || "导入失败";
|
|
4936
|
+
}
|
|
4937
|
+
|
|
4938
|
+
const IMP = {
|
|
4939
|
+
path: "",
|
|
4940
|
+
mode: "browse", // browse=看当前目录;found=子目录搜索结果的平铺列表
|
|
4941
|
+
dirs: [],
|
|
4942
|
+
pdfs: [],
|
|
4943
|
+
mdCount: 0,
|
|
4944
|
+
foundFiles: [],
|
|
4945
|
+
foundTruncated: false,
|
|
4946
|
+
stale: false, // 后端还是旧版(dir-browse 没返回 files)——必须重启 DSH
|
|
4947
|
+
loading: false,
|
|
4948
|
+
error: "",
|
|
4949
|
+
selected: null,
|
|
4950
|
+
busy: false,
|
|
4951
|
+
};
|
|
4952
|
+
|
|
4953
|
+
function impSize(bytes) {
|
|
4954
|
+
const n = Number(bytes) || 0;
|
|
4955
|
+
if (n >= 1024 * 1024) return `${(n / 1024 / 1024).toFixed(1)} MB`;
|
|
4956
|
+
return `${Math.max(1, Math.round(n / 1024))} KB`;
|
|
4957
|
+
}
|
|
4958
|
+
|
|
4959
|
+
function impSelectedText() {
|
|
4960
|
+
if (!IMP.selected) return "还没有选 PDF:点上面的 📄 文件即可。";
|
|
4961
|
+
const md = IMP.mdCount ? `同目录 ${IMP.mdCount} 份 md 会一起抽` : "同目录没有 md —— 只导入整页图";
|
|
4962
|
+
return `已选:${IMP.selected.name} · ${md}`;
|
|
4963
|
+
}
|
|
4964
|
+
|
|
4965
|
+
function impFileRow(file, label) {
|
|
4966
|
+
const selected = IMP.selected?.path === file.path;
|
|
4967
|
+
return `<button type="button" class="dir-picker-row imp-file${selected ? " selected" : ""}" data-imp-pdf="${esc(file.path)}" title="${esc(file.path)}"><span class="dir-picker-name">📄 ${esc(label)}</span><span class="dir-picker-meta">${impSize(file.sizeBytes)}</span></button>`;
|
|
4968
|
+
}
|
|
4969
|
+
|
|
4970
|
+
function renderImportPicker() {
|
|
4971
|
+
const list = getEl("impList");
|
|
4972
|
+
if (!list) return;
|
|
4973
|
+
getEl("impPath").textContent = IMP.mode === "found" ? `${IMP.path}(含子目录)` : IMP.path || "—";
|
|
4974
|
+
getEl("impPath").title = IMP.path || "";
|
|
4975
|
+
getEl("impUp").disabled = IMP.loading || !IMP.path || IMP.mode === "found";
|
|
4976
|
+
getEl("impSubmit").disabled = !IMP.selected || IMP.busy;
|
|
4977
|
+
getEl("impSelected").textContent = impSelectedText();
|
|
4978
|
+
|
|
4979
|
+
if (IMP.loading) {
|
|
4980
|
+
list.innerHTML = `<div class="dir-picker-empty">正在读目录…</div>`;
|
|
4981
|
+
return;
|
|
4982
|
+
}
|
|
4983
|
+
if (IMP.error) {
|
|
4984
|
+
list.innerHTML = `<div class="dir-picker-empty">${esc(IMP.error)}</div>`;
|
|
4985
|
+
return;
|
|
4986
|
+
}
|
|
4987
|
+
if (IMP.stale) {
|
|
4988
|
+
list.innerHTML = `<div class="dir-picker-empty">这个弹层是新版,但**正在运行的 Deckline 后端还是旧版**:<br>接口没返回文件列表,所以只看得到文件夹。<br><br>重启 DSH(<code>dsh --profile deck-studio</code> 或 DSH 桌面端)后再打开这里。</div>`;
|
|
4989
|
+
return;
|
|
4990
|
+
}
|
|
4991
|
+
|
|
4992
|
+
if (IMP.mode === "found") {
|
|
4993
|
+
const rows = IMP.foundFiles.length
|
|
4994
|
+
? IMP.foundFiles.map((file) => impFileRow(file, file.path.replace(`${IMP.path.replace(/[\\/]+$/, "")}/`, ""))).join("")
|
|
4995
|
+
: `<div class="dir-picker-empty">子目录里也没找到 PDF(只看 3 层)。</div>`;
|
|
4996
|
+
list.innerHTML = `<button type="button" class="dir-picker-row" data-imp-back="1"><span class="dir-picker-name">↩ 回到目录列表</span></button>${rows}${IMP.foundTruncated ? `<div class="dir-picker-empty">只列了前 60 份,缩小范围再找。</div>` : ""}`;
|
|
4997
|
+
return;
|
|
4998
|
+
}
|
|
4999
|
+
|
|
5000
|
+
const dirs = (IMP.dirs || []).filter((item) => !item.hidden);
|
|
5001
|
+
const pdfs = IMP.pdfs || [];
|
|
5002
|
+
const findRow = dirs.length
|
|
5003
|
+
? `<button type="button" class="dir-picker-row imp-find" data-imp-find="1"><span class="dir-picker-name">🔍 ${pdfs.length ? "这层也有 PDF;在子目录里再找一遍" : "这层没有 PDF —— 在子目录里找(≤3 层)"}</span></button>`
|
|
5004
|
+
: "";
|
|
5005
|
+
const body = [
|
|
5006
|
+
...pdfs.map((file) => impFileRow(file, file.name)),
|
|
5007
|
+
...dirs.map((dir) => `<button type="button" class="dir-picker-row" data-imp-dir="${esc(dir.path)}" title="${esc(dir.path)}"><span class="dir-picker-name">📁 ${esc(dir.name)}</span></button>`),
|
|
5008
|
+
].join("");
|
|
5009
|
+
if (!body) {
|
|
5010
|
+
list.innerHTML = `<div class="dir-picker-empty">这个文件夹里没有子目录,也没有 PDF。点「↑ 上一级」换个位置。</div>`;
|
|
5011
|
+
return;
|
|
5012
|
+
}
|
|
5013
|
+
list.innerHTML = body + findRow;
|
|
5014
|
+
}
|
|
5015
|
+
|
|
5016
|
+
async function impFetchJson(url) {
|
|
5017
|
+
const res = await fetch(url, { headers: { accept: "application/json" } });
|
|
5018
|
+
let payload = {};
|
|
5019
|
+
try { payload = await res.json(); } catch { /* 非 JSON 兜底 */ }
|
|
5020
|
+
if (!res.ok || !payload?.ok) {
|
|
5021
|
+
const failure = new Error(payload?.error?.message || `HTTP ${res.status}`);
|
|
5022
|
+
failure.code = payload?.error?.code || "";
|
|
5023
|
+
throw failure;
|
|
5024
|
+
}
|
|
5025
|
+
return payload;
|
|
5026
|
+
}
|
|
5027
|
+
|
|
5028
|
+
async function impLoad(pathValue) {
|
|
5029
|
+
IMP.loading = true;
|
|
5030
|
+
IMP.error = "";
|
|
5031
|
+
IMP.selected = null;
|
|
5032
|
+
IMP.mode = "browse";
|
|
5033
|
+
IMP.stale = false;
|
|
5034
|
+
renderImportPicker();
|
|
5035
|
+
try {
|
|
5036
|
+
const query = pathValue ? `?files=1&path=${encodeURIComponent(pathValue)}` : "?files=1";
|
|
5037
|
+
const payload = await impFetchJson("/api/deck-studio.dir-browse" + query);
|
|
5038
|
+
const dir = payload.dir || {};
|
|
5039
|
+
IMP.path = dir.path || pathValue || "";
|
|
5040
|
+
IMP.dirs = dir.dirs || [];
|
|
5041
|
+
// 旧版后端不会返回 files:明确说「要重启」,别让人以为目录里真没 PDF。
|
|
5042
|
+
IMP.stale = !Array.isArray(dir.files);
|
|
5043
|
+
const files = Array.isArray(dir.files) ? dir.files : [];
|
|
5044
|
+
IMP.pdfs = files.filter((file) => file.kind === "pdf");
|
|
5045
|
+
IMP.mdCount = files.filter((file) => file.kind === "md").length;
|
|
5046
|
+
} catch (error) {
|
|
5047
|
+
IMP.error = importErrText(error?.code, error?.message);
|
|
5048
|
+
IMP.dirs = [];
|
|
5049
|
+
IMP.pdfs = [];
|
|
5050
|
+
IMP.mdCount = 0;
|
|
5051
|
+
} finally {
|
|
5052
|
+
IMP.loading = false;
|
|
5053
|
+
renderImportPicker();
|
|
5054
|
+
}
|
|
5055
|
+
}
|
|
5056
|
+
|
|
5057
|
+
async function impFindPdfs() {
|
|
5058
|
+
IMP.loading = true;
|
|
5059
|
+
IMP.error = "";
|
|
5060
|
+
renderImportPicker();
|
|
5061
|
+
try {
|
|
5062
|
+
const payload = await impFetchJson(`/api/deck-studio.dir-browse?find=pdf&path=${encodeURIComponent(IMP.path)}`);
|
|
5063
|
+
IMP.foundFiles = payload.found?.files || [];
|
|
5064
|
+
IMP.foundTruncated = Boolean(payload.found?.truncated);
|
|
5065
|
+
IMP.mode = "found";
|
|
5066
|
+
} catch (error) {
|
|
5067
|
+
IMP.error = importErrText(error?.code, error?.message);
|
|
5068
|
+
} finally {
|
|
5069
|
+
IMP.loading = false;
|
|
5070
|
+
renderImportPicker();
|
|
5071
|
+
}
|
|
5072
|
+
}
|
|
5073
|
+
|
|
5074
|
+
const IMP_LAST_DIR_KEY = "deckImportLastDir";
|
|
5075
|
+
|
|
5076
|
+
function openImportPdfModal() {
|
|
5077
|
+
IMP.busy = false;
|
|
5078
|
+
IMP.selected = null;
|
|
5079
|
+
IMP.stale = false;
|
|
5080
|
+
getEl("impState").textContent = "";
|
|
5081
|
+
getEl("impDpi").value = "120";
|
|
5082
|
+
getEl("importPdfModal").hidden = false;
|
|
5083
|
+
let start = "";
|
|
5084
|
+
try { start = localStorage.getItem(IMP_LAST_DIR_KEY) || ""; } catch { start = ""; }
|
|
5085
|
+
if (!start) start = String(STATE.data?.config?.dataRoot || "").trim();
|
|
5086
|
+
impLoad(start);
|
|
5087
|
+
}
|
|
5088
|
+
|
|
5089
|
+
function closeImportPdfModal() {
|
|
5090
|
+
getEl("importPdfModal").hidden = true;
|
|
5091
|
+
}
|
|
5092
|
+
|
|
5093
|
+
getEl("importPdfBtn").addEventListener("click", openImportPdfModal);
|
|
5094
|
+
getEl("impClose").addEventListener("click", closeImportPdfModal);
|
|
5095
|
+
getEl("impCancel").addEventListener("click", closeImportPdfModal);
|
|
5096
|
+
getEl("importPdfModal").addEventListener("click", (event) => { if (event.target === getEl("importPdfModal")) closeImportPdfModal(); });
|
|
5097
|
+
getEl("impUp").addEventListener("click", () => {
|
|
5098
|
+
if (!IMP.path || IMP.mode === "found") return;
|
|
5099
|
+
const parent = IMP.path.replace(/[\\/]+$/, "").replace(/[\\/][^\\/]+$/, "");
|
|
5100
|
+
impLoad(parent || "/");
|
|
5101
|
+
});
|
|
5102
|
+
getEl("impList").addEventListener("click", (event) => {
|
|
5103
|
+
if (event.target.closest("[data-imp-back]")) {
|
|
5104
|
+
impLoad(IMP.path);
|
|
5105
|
+
return;
|
|
5106
|
+
}
|
|
5107
|
+
if (event.target.closest("[data-imp-find]")) {
|
|
5108
|
+
impFindPdfs();
|
|
5109
|
+
return;
|
|
5110
|
+
}
|
|
5111
|
+
const dirHit = event.target.closest("[data-imp-dir]");
|
|
5112
|
+
if (dirHit) {
|
|
5113
|
+
impLoad(dirHit.dataset.impDir);
|
|
5114
|
+
return;
|
|
5115
|
+
}
|
|
5116
|
+
const pdfHit = event.target.closest("[data-imp-pdf]");
|
|
5117
|
+
if (pdfHit) {
|
|
5118
|
+
IMP.selected = [...IMP.pdfs, ...IMP.foundFiles].find((file) => file.path === pdfHit.dataset.impPdf) || null;
|
|
5119
|
+
renderImportPicker();
|
|
5120
|
+
}
|
|
5121
|
+
});
|
|
5122
|
+
getEl("impSubmit").addEventListener("click", async () => {
|
|
5123
|
+
if (!IMP.selected || IMP.busy) return;
|
|
5124
|
+
IMP.busy = true;
|
|
5125
|
+
getEl("impSubmit").disabled = true;
|
|
5126
|
+
getEl("impState").textContent = `正在把 ${IMP.selected.name} 每页转成整页图(页数多会慢一点,别关页面)…`;
|
|
5127
|
+
try {
|
|
5128
|
+
const res = await fetch("/api/deck-studio.deck-import", {
|
|
5129
|
+
method: "POST",
|
|
5130
|
+
headers: { "content-type": "application/json" },
|
|
5131
|
+
body: JSON.stringify({ pdf: IMP.selected.path, dpi: Number(getEl("impDpi").value) || 120 }),
|
|
5132
|
+
});
|
|
5133
|
+
const result = await res.json().catch(() => ({}));
|
|
5134
|
+
if (!res.ok || !result.ok) {
|
|
5135
|
+
const failure = new Error(result?.error?.message || `HTTP ${res.status}`);
|
|
5136
|
+
failure.code = result?.error?.code || "";
|
|
5137
|
+
throw failure;
|
|
5138
|
+
}
|
|
5139
|
+
try { localStorage.setItem(IMP_LAST_DIR_KEY, IMP.path); } catch { /* 隐私模式忽略 */ }
|
|
5140
|
+
const parts = [`已导入 ${result.pages} 页(${result.dpi} dpi)`];
|
|
5141
|
+
if (result.markdown?.length) parts.push(`抽到 md:${result.markdown.join("、")}`);
|
|
5142
|
+
if (result.warnings?.length) parts.push(`注意:${result.warnings.join(";")}`);
|
|
5143
|
+
getEl("impState").textContent = parts.join(" · ");
|
|
5144
|
+
closeImportPdfModal();
|
|
5145
|
+
flash(`已导入「${result.deck?.label || ""}」:${result.pages} 页整页图`, "ok");
|
|
5146
|
+
if (result.deck?.deckId) await showDeck(result.deck.deckId);
|
|
5147
|
+
} catch (error) {
|
|
5148
|
+
getEl("impState").textContent = "导入失败:" + importErrText(error?.code, error?.message);
|
|
5149
|
+
} finally {
|
|
5150
|
+
IMP.busy = false;
|
|
5151
|
+
getEl("impSubmit").disabled = !IMP.selected;
|
|
5152
|
+
}
|
|
5153
|
+
});
|
|
5154
|
+
|
|
4747
5155
|
// F-63:标签是动态渲染的,用事件委托
|
|
4748
5156
|
getEl("observeTabs").addEventListener("click", (event) => {
|
|
4749
5157
|
const btn = event.target.closest(".observe-tab");
|
|
@@ -5107,6 +5515,8 @@ function requestConversation(payload) {
|
|
|
5107
5515
|
cwd: STATE.data?.cwd || "",
|
|
5108
5516
|
workspacePath: STATE.data?.workspacePath || "",
|
|
5109
5517
|
sourceRoot: STATE.data?.active?.sourceRoot || "",
|
|
5518
|
+
// 0.2.16:宿主要按「deck 自己的项目目录」建会话工作区(桌面里 cwd 是 profile 目录,用不了)。
|
|
5519
|
+
sessionWorkspacePath: STATE.data?.active?.sessionWorkspacePath || "",
|
|
5110
5520
|
...payload,
|
|
5111
5521
|
// F-202B:桌面 Dock 模式默认不抢主区(open:false);显式 takeMain → open:true 切宿主主区。
|
|
5112
5522
|
// web 不传 open(undefined → 宿主按原语义 openSession)。
|
|
@@ -5184,8 +5594,11 @@ async function ensureDeckConversation(deckId) {
|
|
|
5184
5594
|
});
|
|
5185
5595
|
if (!result.ok) {
|
|
5186
5596
|
getEl("chatBindChip").textContent = `会话打开失败:${result.reason || "未知原因"}`;
|
|
5597
|
+
flash(`会话打开失败:${result.reason || "未知原因"}`, "error");
|
|
5187
5598
|
return result;
|
|
5188
5599
|
}
|
|
5600
|
+
// 0.2.20:会话建好了但首条指令没发出去时,宿主会带 notice——别让它悄悄消失。
|
|
5601
|
+
if (result.notice) flash(result.notice);
|
|
5189
5602
|
if (!binding || binding.sessionId !== result.sessionId) {
|
|
5190
5603
|
await bindConversation({ sessionId: result.sessionId, kind: "deck", id: deckId, title: label });
|
|
5191
5604
|
}
|
|
@@ -5591,23 +6004,33 @@ function renderChatHeader() {
|
|
|
5591
6004
|
/** + 新建会话:建完立即绑定当前 deck / design(不再产生未绑定会话)。 */
|
|
5592
6005
|
async function newChatForTarget() {
|
|
5593
6006
|
const target = activeChatTarget();
|
|
5594
|
-
if (!target)
|
|
6007
|
+
if (!target) {
|
|
6008
|
+
// 0.2.16:以前这里静默 return——点了 + 什么都不发生,用户只能猜。
|
|
6009
|
+
flash("当前没有可绑定的 deck / 风格:先打开一本 deck 再新建会话", "error");
|
|
6010
|
+
return;
|
|
6011
|
+
}
|
|
5595
6012
|
const button = getEl("newChatBtn");
|
|
5596
6013
|
button.disabled = true;
|
|
5597
6014
|
try {
|
|
5598
6015
|
const result = await requestConversation({ action: "ensure", kind: target.kind, id: target.id, title: target.label });
|
|
5599
6016
|
if (!result.ok || !result.sessionId) {
|
|
6017
|
+
const reason = result.reason || "未知原因";
|
|
5600
6018
|
// F-202B:失败要用户可见——之前只写按钮 title,Dock 模式下看起来像「点了没反应」。
|
|
6019
|
+
// 0.2.16:再加一条常驻 toast,光靠 chip 文案还是容易被当成没反应。
|
|
6020
|
+
flash(`新建会话失败:${reason}`, "error");
|
|
5601
6021
|
getEl("chatBindChip").textContent = "会话新建失败";
|
|
5602
|
-
getEl("chatBindChip").title = `新建会话失败:${
|
|
5603
|
-
getEl("chatSessionBtn").title = `新建会话失败:${
|
|
6022
|
+
getEl("chatBindChip").title = `新建会话失败:${reason};点这里重试`;
|
|
6023
|
+
getEl("chatSessionBtn").title = `新建会话失败:${reason}`;
|
|
5604
6024
|
return;
|
|
5605
6025
|
}
|
|
5606
6026
|
STATE.chatSessionId = result.sessionId;
|
|
6027
|
+
if (result.notice) flash(result.notice);
|
|
5607
6028
|
await bindConversation({ sessionId: result.sessionId, kind: target.kind, id: target.id, title: target.label });
|
|
5608
6029
|
await refreshSessions();
|
|
5609
6030
|
renderChatHeader();
|
|
5610
6031
|
// 桌面:新建就是要点开聊——切到宿主主区的原生会话。
|
|
6032
|
+
} catch (error) {
|
|
6033
|
+
flash(`新建会话失败:${error?.message ?? error}`, "error");
|
|
5611
6034
|
} finally {
|
|
5612
6035
|
button.disabled = false;
|
|
5613
6036
|
}
|
|
@@ -5900,6 +6323,22 @@ function backToManual(box) {
|
|
|
5900
6323
|
showHome();
|
|
5901
6324
|
}
|
|
5902
6325
|
setInterval(() => loadWorkbench(STATE.deckId), 5000);
|
|
6326
|
+
|
|
6327
|
+
/**
|
|
6328
|
+
* 0.2.21:boot 完成 → 把启动期间暂存的「打开某本 deck」执行掉,再向宿主报 ready。
|
|
6329
|
+
* 顺序很关键:先 booted=true 再执行,避免执行期间又来一条指令被丢进 pending 里没人取。
|
|
6330
|
+
*/
|
|
6331
|
+
OPEN_BINDING.booted = true;
|
|
6332
|
+
if (OPEN_BINDING.pending) {
|
|
6333
|
+
const pending = OPEN_BINDING.pending;
|
|
6334
|
+
OPEN_BINDING.pending = null;
|
|
6335
|
+
applyOpenBinding(pending);
|
|
6336
|
+
}
|
|
6337
|
+
try {
|
|
6338
|
+
window.parent?.postMessage({ type: "dsh-deck-studio:workbench-ready" }, "*");
|
|
6339
|
+
} catch {
|
|
6340
|
+
/* 直接打开(无宿主)时没有 parent */
|
|
6341
|
+
}
|
|
5903
6342
|
})();
|
|
5904
6343
|
</script>
|
|
5905
6344
|
</body>
|