dsh-plugin-deck-studio 0.2.6 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/cordis.example.yml +85 -7
  2. package/cordis.patch.yml +78 -7
  3. package/index.js +367 -27
  4. package/lib/capabilities.js +234 -0
  5. package/lib/citation-check.js +93 -0
  6. package/lib/config.js +27 -0
  7. package/lib/deck-brief.js +35 -1
  8. package/lib/deck-docs.js +45 -7
  9. package/lib/deck-templates.js +8 -1
  10. package/lib/deck-workspace.js +27 -1
  11. package/lib/designs.js +15 -0
  12. package/lib/fact-labels.js +81 -0
  13. package/lib/first-draft.js +119 -0
  14. package/lib/image-review.js +523 -0
  15. package/lib/page-contract.js +112 -0
  16. package/lib/proposal-decision.js +249 -75
  17. package/lib/render.js +101 -10
  18. package/lib/review-agent.js +1 -1
  19. package/lib/review-personas.js +49 -2
  20. package/lib/review-protocol.js +411 -0
  21. package/lib/review-readers.js +300 -0
  22. package/lib/review-score.js +198 -0
  23. package/lib/title-chain.js +122 -0
  24. package/lib/tools.js +973 -78
  25. package/lib/update-check.js +166 -0
  26. package/package.json +12 -11
  27. package/seeds/docs/assets/screenshots/f186-01-agent-style-create.png +0 -0
  28. package/seeds/docs/assets/screenshots/f186-02-designs-card.png +0 -0
  29. package/seeds/docs/assets/screenshots/f187-01-review-readers.png +0 -0
  30. package/seeds/docs/assets/screenshots/f187-02-gap-report.png +0 -0
  31. package/seeds/docs/assets/screenshots/f188-01-header-buttons.png +0 -0
  32. package/seeds/docs/assets/screenshots/f188-02-update-toast.png +0 -0
  33. package/seeds/docs/assets/screenshots/f188-03-review-doc-tabs.png +0 -0
  34. package/seeds/docs/assets/screenshots/f188-04-script-doc-nochips.png +0 -0
  35. package/seeds/docs/assets/screenshots/i14-01-create.png +0 -0
  36. package/seeds/docs/assets/screenshots/i14-02-first-draft.png +0 -0
  37. package/seeds/docs/assets/screenshots/i14-03-rendered.png +0 -0
  38. package/seeds/docs/assets/screenshots/i14-04-image-review.png +0 -0
  39. package/seeds/docs/assets/screenshots/i14-05-p01-page.png +0 -0
  40. package/seeds/docs/install.en.md +13 -3
  41. package/seeds/docs/install.md +13 -4
  42. package/seeds/docs/manual.en.md +17 -0
  43. package/seeds/docs/manual.md +17 -0
  44. package/seeds/docs/sample-deck.en.md +1 -1
  45. package/seeds/docs/sample-deck.md +1 -1
  46. package/seeds/docs/styles.en.md +2 -1
  47. package/seeds/docs/styles.md +14 -4
  48. package/seeds/samples/DSA-01.jpg +0 -0
  49. package/seeds/samples/DSA-02.jpg +0 -0
  50. package/seeds/samples/DSA-03.jpg +0 -0
  51. package/seeds/styles/DSA.json +22 -0
  52. package/ui/workbench.css +25 -3
  53. package/ui/workbench.html +286 -10
package/ui/workbench.html CHANGED
@@ -30,6 +30,18 @@
30
30
  .brief-style-badge.is-unknown{border-color:#cdd6d4;background:#f4f7f6;color:#6b7b7a}
31
31
  .brief-cover{margin-left:6px;color:#4a5c5a}
32
32
  .brief-banner{border-color:#e0b96a;background:#fdf7e8}
33
+ .review-panel{margin:10px 0 12px;padding:10px 12px;border:1px solid #cfd9e8;background:#f6f9fd;border-radius:10px}
34
+ .review-row{display:grid;grid-template-columns:18px 132px 1fr 130px 1.2fr;gap:8px;align-items:center;padding:5px 0;border-top:1px dashed #dde5ef;font-size:12px}
35
+ .review-row:first-of-type{border-top:none}
36
+ .review-row-off{opacity:.55}
37
+ .review-tag{color:#25507f;font-weight:600;font-size:11px}
38
+ .review-why{color:#4a5464}
39
+ .review-page{color:#7a8595;font-size:11px}
40
+ .review-diff{display:flex;gap:6px;align-items:center;overflow:hidden}
41
+ .review-diff .proposal-before{max-width:45%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#8a8a8a}
42
+ .review-diff .proposal-value{max-width:55%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#1d5c2e}
43
+ .review-actions{display:flex;gap:10px;align-items:center;justify-content:flex-end;margin-top:8px}
44
+ .review-hint{color:#7a8595;font-size:11px}
33
45
  .assoc-switch-panel{margin-top:10px;padding:12px 14px;border:1px dashed #d8b46a;border-radius:10px;background:#fffdf6}
34
46
  .assoc-switch-panel label{display:block;margin:6px 0;font-size:13px;color:#3c4a4c}
35
47
  .assoc-switch-panel .panel-actions{display:flex;gap:8px;margin-top:10px}
@@ -80,6 +92,19 @@
80
92
  <span class="project">AI Image Deck Studio</span>
81
93
  <span class="spacer"></span>
82
94
  <span id="save">DSH 会话 · 连接中…</span>
95
+ <!-- F-188:更新 / 仓库 / 设置入口(点刷新查 npm 最新版,有新版带小圆点) -->
96
+ <button id="settingsBtn" class="dl-icon-btn" title="设置(出图节点 / 模型 / 尺寸)" aria-label="设置">
97
+ <svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true"><path fill="currentColor" d="M19.4 13a7.7 7.7 0 0 0 0-2l2-1.6-2-3.4-2.4 1a7.7 7.7 0 0 0-1.7-1L15 3.5h-4l-.3 2.5a7.7 7.7 0 0 0-1.7 1l-2.4-1-2 3.4 2 1.6a7.7 7.7 0 0 0 0 2l-2 1.6 2 3.4 2.4-1a7.7 7.7 0 0 0 1.7 1l.3 2.5h4l.3-2.5a7.7 7.7 0 0 0 1.7-1l2.4 1 2-3.4-2-1.6zM12 15.2A3.2 3.2 0 1 1 12 8.8a3.2 3.2 0 0 1 0 6.4z"/></svg>
98
+ </button>
99
+ <button id="updateBtn" class="dl-icon-btn" title="检查更新" aria-label="检查更新">
100
+ <svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true"><path fill="currentColor" d="M12 4V1.5L7.5 6l4.5 4.5V8a5 5 0 1 1-5 5H5a7 7 0 1 0 7-9z"/></svg>
101
+ <span class="dl-icon-ver" id="updateVer">—</span>
102
+ <i class="dl-icon-dot" id="updateDot" hidden></i>
103
+ </button>
104
+ <button id="repoBtn" class="dl-icon-btn" title="GitHub 仓库 / 提建议" aria-label="GitHub 仓库">
105
+ <svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true"><path fill="currentColor" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg>
106
+ </button>
107
+ <div id="dlToast" class="dl-toast" hidden></div>
83
108
  <button id="shellSidebarBtn" title="显示 / 收起 DSH 侧栏(会话 · 插件 · 设置)" hidden>☰ DSH</button>
84
109
  </header>
85
110
 
@@ -1032,6 +1057,137 @@ async function postJson(routeName, body) {
1032
1057
  return data;
1033
1058
  }
1034
1059
 
1060
+ /** GET 一个插件路由(失败抛后端 message)。 */
1061
+ async function getJson(routeName) {
1062
+ const res = await fetch("/api/deck-studio." + routeName, { headers: { accept: "application/json" } });
1063
+ const data = await res.json().catch(() => ({}));
1064
+ if (!res.ok || data?.ok === false) throw new Error(data?.error?.message || "HTTP " + res.status);
1065
+ return data;
1066
+ }
1067
+
1068
+ /** 顶栏临时提示(不打断操作)。 */
1069
+ let DL_TOAST_TIMER = null;
1070
+ function dlToast(text, ms = 4200) {
1071
+ const box = getEl("dlToast");
1072
+ if (!box) return;
1073
+ box.textContent = text;
1074
+ box.hidden = false;
1075
+ box.classList.add("show");
1076
+ clearTimeout(DL_TOAST_TIMER);
1077
+ DL_TOAST_TIMER = setTimeout(() => {
1078
+ box.classList.remove("show");
1079
+ box.hidden = true;
1080
+ }, ms);
1081
+ }
1082
+
1083
+ /** F-188:更新状态(版本 / 安装方式 / 仓库链接)由 workbench payload 带下来。 */
1084
+ const UPDATE = { current: "", latest: "", hasUpdate: false, installMode: "", repo: "", suggest: "", note: "" };
1085
+
1086
+ function paintUpdate() {
1087
+ const ver = getEl("updateVer");
1088
+ if (ver) ver.textContent = UPDATE.current ? `v${UPDATE.current}` : "—";
1089
+ const dot = getEl("updateDot");
1090
+ if (dot) dot.hidden = !UPDATE.hasUpdate;
1091
+ const btn = getEl("updateBtn");
1092
+ if (btn) {
1093
+ btn.title = UPDATE.hasUpdate
1094
+ ? `有新版 v${UPDATE.latest}(当前 v${UPDATE.current})`
1095
+ : UPDATE.installMode === "linked"
1096
+ ? "本机是开发链接安装:更新走 git pull"
1097
+ : "检查更新";
1098
+ }
1099
+ }
1100
+
1101
+ function absorbUpdate(payload) {
1102
+ const brief = payload?.update ?? {};
1103
+ if (brief.currentVersion) UPDATE.current = brief.currentVersion;
1104
+ if (brief.installMode) UPDATE.installMode = brief.installMode;
1105
+ if (brief.repo) UPDATE.repo = brief.repo;
1106
+ if (brief.suggest) UPDATE.suggest = brief.suggest;
1107
+ if (brief.issues) UPDATE.issues = brief.issues;
1108
+ paintUpdate();
1109
+ }
1110
+
1111
+ async function runPluginUpdate() {
1112
+ try {
1113
+ const data = await postJson("update", {});
1114
+ UPDATE.current = data.to || UPDATE.current;
1115
+ UPDATE.hasUpdate = false;
1116
+ paintUpdate();
1117
+ openConfirm({
1118
+ title: `已更新到 v${data.to}`,
1119
+ desc: `${data.restartHint || "重启 DSH 后生效。"}\n\n${data.output ? String(data.output).slice(-600) : ""}`,
1120
+ okLabel: "知道了",
1121
+ onOk: null,
1122
+ });
1123
+ } catch (error) {
1124
+ dlToast(String(error?.message || error), 8000);
1125
+ }
1126
+ }
1127
+
1128
+ /** 点刷新:先查 npm 最新版,再决定是提示「已是最新」、解释开发链接、还是弹更新确认。 */
1129
+ async function checkPluginUpdate() {
1130
+ const btn = getEl("updateBtn");
1131
+ if (btn) btn.classList.add("busy");
1132
+ try {
1133
+ const data = await getJson("update-check");
1134
+ UPDATE.latest = data.latestVersion || "";
1135
+ UPDATE.hasUpdate = Boolean(data.hasUpdate);
1136
+ UPDATE.installMode = data.installMode || UPDATE.installMode;
1137
+ if (data.note) UPDATE.note = data.note;
1138
+ paintUpdate();
1139
+ if (data.error) {
1140
+ dlToast(data.error, 6000);
1141
+ return;
1142
+ }
1143
+ if (!UPDATE.hasUpdate) {
1144
+ dlToast(`已是最新版本 v${data.currentVersion}`);
1145
+ return;
1146
+ }
1147
+ if (data.installMode === "linked") {
1148
+ dlToast(data.note || "本机是开发链接安装,更新走仓库(git pull)。", 7000);
1149
+ return;
1150
+ }
1151
+ if (data.installMode !== "npm") {
1152
+ dlToast("没识别出 profile 的安装方式,无法一键更新;请手动 dsh plugin add。", 7000);
1153
+ return;
1154
+ }
1155
+ openConfirm({
1156
+ title: `更新到 v${data.latestVersion}`,
1157
+ desc: `当前 v${data.currentVersion} → v${data.latestVersion}。会在 profile 里执行 pnpm add,下载完成后重启 DSH 生效。`,
1158
+ okLabel: "更新",
1159
+ onOk: runPluginUpdate,
1160
+ });
1161
+ } catch (error) {
1162
+ dlToast(`检查更新失败:${String(error?.message || error)}`, 6000);
1163
+ } finally {
1164
+ if (btn) btn.classList.remove("busy");
1165
+ }
1166
+ }
1167
+
1168
+ /** 顶栏齿轮:直接切到右栏「设置」(首页时先回到当前 deck 的工作台)。 */
1169
+ async function openSettingsPanel() {
1170
+ const deckId = STATE.data?.active?.deckId;
1171
+ if (!deckId) {
1172
+ dlToast("先在 Decks 里打开一本 deck,右栏就有设置(出图节点 / 模型 / 尺寸)", 6000);
1173
+ return;
1174
+ }
1175
+ if (STATE.view === "home") await showDeck(deckId);
1176
+ activateRightTab("settings");
1177
+ const panel = getEl("settings");
1178
+ if (panel) panel.scrollIntoView({ block: "start" });
1179
+ }
1180
+
1181
+ /** 打开仓库 / 提建议页(新标签)。 */
1182
+ function openRepo() {
1183
+ const url = UPDATE.suggest || UPDATE.issues || UPDATE.repo;
1184
+ if (!url) {
1185
+ dlToast("插件包里没配 repository 链接", 5000);
1186
+ return;
1187
+ }
1188
+ window.open(url, "_blank", "noopener");
1189
+ }
1190
+
1035
1191
  let CONFIRM_OK = null;
1036
1192
 
1037
1193
  /** 通用确认弹层(删除等破坏性操作用)。 */
@@ -1395,14 +1551,37 @@ function emptyDoc(doc) {
1395
1551
  }
1396
1552
 
1397
1553
  function docHeader(doc, extra = "") {
1554
+ // 只有「多个可切换的文档」才出 chip:目录(prompts/ 之类)、不存在的相关文件都不算。
1555
+ // 以前脚本页右侧也挂着 script.md / prompts/ 两个 chip,看着像能切,其实没内容可切。
1556
+ const switchable = (doc.files || []).filter((file) => !String(file.name || "").endsWith("/") && file.exists !== false);
1557
+ const chips = switchable.length > 1
1558
+ ? `<div class="doc-files">${switchable.map((file) => `<button type="button" class="doc-file-chip${file.name === doc.primary ? " active" : ""}" data-doc-file="${esc(file.name)}" title="${esc(file.name)}">${esc(file.name.replace(/^docs\//, ""))}</button>`).join("")}</div>`
1559
+ : "";
1560
+ // 两行:第一行标题 + 徽标,右上角是主文件路径与草案说明 / 动作;第二行只有文件 chips(左对齐,可换行)。
1561
+ // 一轮审稿会有 review.md / readers / gap / experts / images / scores 六个 chip,跟标题挤一行会被挤到右侧折成两截,很难读。
1398
1562
  return `<div class="doc-head">
1399
- <div><div class="eyebrow">Deck Document</div><h2>${esc(doc.label)}</h2></div>
1400
- ${docBadge(doc.status)}
1401
- <span class="small doc-file">${esc(doc.primary || "无文件")}</span>
1402
- ${extra}
1563
+ <div class="doc-head-top">
1564
+ <div><div class="eyebrow">Deck Document</div><h2>${esc(doc.label)}</h2></div>
1565
+ ${docBadge(doc.status)}
1566
+ <span class="doc-head-side">
1567
+ <span class="small doc-file">${esc(doc.primary || "无文件")}</span>
1568
+ ${extra ? `<span class="doc-head-extra">${extra}</span>` : ""}
1569
+ </span>
1570
+ </div>
1571
+ ${chips ? `<div class="doc-head-row">${chips}</div>` : ""}
1403
1572
  </div>`;
1404
1573
  }
1405
1574
 
1575
+ /** F-187:审稿页有读者本 / 差距本 / 工程本,点 chip 切换正文。 */
1576
+ async function openDocFile(docId, file) {
1577
+ const deckId = STATE.data?.active?.deckId ?? "";
1578
+ const res = await fetch(`/api/deck-studio.doc?` + new URLSearchParams({ deckId, docId, file }));
1579
+ const doc = await res.json().catch(() => null);
1580
+ if (!res.ok || !doc || doc.ok === false) return;
1581
+ STATE.docCache[docId] = doc;
1582
+ renderDocTab(docId);
1583
+ }
1584
+
1406
1585
  function docFooter(doc, content) {
1407
1586
  return `<details class="doc-raw"><summary>查看原始 markdown</summary><div class="md"><pre>${esc(content || "(空)")}</pre></div></details>`;
1408
1587
  }
@@ -1557,7 +1736,25 @@ function renderDoc(doc, index) {
1557
1736
  const name = kind === "body" ? "正文" : "画面要求";
1558
1737
  return `<span class="density-count${off ? " is-off" : ""}" title="当前密度「${esc(densityTarget.label)}」:${name}目标 ${min}–${max} 字">${name} ${chars} 字${off ? ` · 目标 ${min}–${max}` : " · 达标"}</span>`;
1559
1738
  };
1560
- return head + styleNote + densityNote + briefBanner + docIssues(doc) + storyOverview + banner + `<div class="page-cards">${pages.map((page) => {
1739
+ // F-176 / I-18:评审意见清单——带 reviewRef / severity / confidence 的提案合成一个可勾选清单,
1740
+ // 勾完点「接受并更新」→ 一条 revision(不是一条提案一条)。C 类(原文找不到)根本不进提案。
1741
+ const reviewBatch = actionablePending.filter((proposal) => proposal.reviewRef || proposal.severity || proposal.confidence);
1742
+ const reviewRow = (proposal) => {
1743
+ const confidence = String(proposal.confidence || "").toUpperCase();
1744
+ const selectable = confidence !== "C";
1745
+ const tag = [proposal.reviewRef, proposal.severity, confidence ? `证据 ${confidence}` : ""].filter(Boolean).join(" · ");
1746
+ return `<label class="review-row${selectable ? "" : " review-row-off"}">
1747
+ <input type="checkbox" data-review-pick="${esc(proposal.proposalId)}"${selectable ? (confidence === "D" ? "" : " checked") : " disabled"}>
1748
+ <span class="review-tag">${esc(tag || "评审意见")}</span>
1749
+ <span class="review-why">${esc(String(proposal.reason || proposal.evidence || "按评审意见修改").slice(0, 80))}</span>
1750
+ <span class="review-page">${esc(proposal.pageId)} · ${esc(proposalFieldLabel(proposal.field))}</span>
1751
+ <span class="review-diff"><span class="proposal-before">${esc(proposalCurrent(proposal) || "(空)")}</span><span class="proposal-arrow">→</span><span class="proposal-value">${esc(proposalPreview(proposal) || "(空值)")}</span></span>
1752
+ </label>`;
1753
+ };
1754
+ const reviewPanel = reviewBatch.length
1755
+ ? `<div class="review-panel"><p class="proposal-strip-note">评审意见 <b>${reviewBatch.length}</b> 条 · 勾选后点「接受并更新」合成<b>一条</b> revision(旧版保留,可回滚);不点就什么都不改</p>${reviewBatch.map(reviewRow).join("")}<div class="review-actions"><button class="btn solid" data-accept-review="1">接受并更新所选</button><span class="review-hint">A=原文可定位;B=转述或支撑偏弱;D=原文在但推不出结论(默认不勾,只作参考);C=无法核实,不进清单</span></div></div>`
1756
+ : "";
1757
+ return head + styleNote + densityNote + briefBanner + docIssues(doc) + storyOverview + banner + reviewPanel + `<div class="page-cards">${pages.map((page) => {
1561
1758
  const pageProposals = pendingByPage.get(Number(page.page)) || [];
1562
1759
  const slide = slideById.get(page.id) || {};
1563
1760
  // 已落版值(manifest)优先:确认过的 title/question/body 必须立刻显示,不能被源文档快照盖住。
@@ -1609,6 +1806,7 @@ function renderDoc(doc, index) {
1609
1806
  })()}
1610
1807
  ${bodyText ? `<div class="page-content md${placeholderContent ? " is-placeholder" : ""}">${placeholderContent ? esc(bodyText) : mdToHtml(bodyText)}</div><p class="density-line">${densityBadge("body", bodyText)}</p>` : `<p class="engine-note">(这页没有正文)</p>`}
1611
1808
  <details class="prompt-details"><summary>提示词(${placeholderPrompt ? "待补" : `${String(promptText).length} 字符`}${densityTarget && !placeholderPrompt ? ` · 目标 ${densityTarget.briefMin}–${densityTarget.briefMax} 字` : ""})</summary><div class="prompt-quote">${esc(promptText || "(缺提示词)")}${placeholderPrompt ? pendingHint(["visualBrief"]) : ""}</div></details>
1809
+ <details class="prompt-details" data-assembled-prompt="${esc(slide.id)}"><summary>出图时拼装的完整 prompt(含图上文字清单,展开时读取)</summary><div class="prompt-quote" data-assembled-body="${esc(slide.id)}">展开读取…</div></details>
1612
1810
  ${proposalBlock}
1613
1811
  </div></div>`;
1614
1812
  }).join("")}</div>` + docFooter(doc, content);
@@ -1678,6 +1876,12 @@ function renderDoc(doc, index) {
1678
1876
  if (doc.id === "changelog") {
1679
1877
  const feed = doc.parsed.feed ?? [];
1680
1878
  const fmtWhen = (value) => String(value || "").replace("T", " ").slice(0, 16).replace(/-/g, "/");
1879
+ // F-173:最新一版是首稿自动落版时,给出「这版没经过确认」+ 一键回滚。
1880
+ const latestRevision = feed.map((row) => row.revision).filter(Boolean).sort().pop() || "";
1881
+ const autoRow = feed.find((row) => row.autoland && row.autoland.revision === latestRevision && !row.autoland.rolledBack);
1882
+ const rollbackBar = autoRow
1883
+ ? `<div class="proposal-banner"><div><b>${esc(autoRow.autoland.badge || "首稿(自动落版,未经确认)")}</b><span>Agent 自改 ${esc(String(autoRow.autoland.selfReview?.rounds ?? 0))} 轮后直接落版,没有逐条等你确认。${autoRow.autoland.selfReview?.summary ? `主要改了:${esc(autoRow.autoland.selfReview.summary)}。` : ""}不满意可以回到落版前。</span></div><div class="proposal-banner-actions"><button class="btn alt" data-rollback-draft="${esc(autoRow.autoland.revision)}">回滚首稿</button></div></div>`
1884
+ : "";
1681
1885
  const list = feed.length
1682
1886
  ? `<div class="change-feed">${feed.map((row) => `
1683
1887
  <div class="change-item">
@@ -1691,7 +1895,7 @@ function renderDoc(doc, index) {
1691
1895
  <div class="change-note">${row.note ? mdInline(row.note) : `<span class="small">系统记录,未写说明</span>`}</div>
1692
1896
  </div>`).join("")}</div>`
1693
1897
  : `<p class="engine-note">还没有变更记录:改一次 script / 出一次图就会自动出现在这里。</p>`;
1694
- return head + docIssues(doc) + list + `<p class="engine-note">这张日志由系统从版本与事件自动生成;Agent 每次改完 deck 只用补一句「为什么」(工具 <code>deck_note_change</code>),不需要手抄历史。</p>` + docFooter(doc, content);
1898
+ return head + docIssues(doc) + rollbackBar + list + `<p class="engine-note">这张日志由系统从版本与事件自动生成;Agent 每次改完 deck 只用补一句「为什么」(工具 <code>deck_note_change</code>),不需要手抄历史。</p>` + docFooter(doc, content);
1695
1899
  }
1696
1900
  if (doc.id === "feedback") {
1697
1901
  const rows = doc.parsed.rows ?? [];
@@ -1822,7 +2026,7 @@ function flash(text, kind = "ok") {
1822
2026
  }
1823
2027
 
1824
2028
  /** F-108:在工作台直接落版 / 驳回提案(与 Agent 的 deck_revision_confirm 同一条写入路径)。 */
1825
- async function decideProposals(proposalIds, confirmed) {
2029
+ async function decideProposals(proposalIds, confirmed, batch = false) {
1826
2030
  const ids = (proposalIds || []).filter(Boolean);
1827
2031
  if (!ids.length) return;
1828
2032
  const res = await fetch("/api/deck-studio.proposal-decide", {
@@ -1831,7 +2035,8 @@ async function decideProposals(proposalIds, confirmed) {
1831
2035
  body: JSON.stringify({
1832
2036
  proposalIds: ids,
1833
2037
  confirmed,
1834
- quote: confirmed ? "(工作台:用户点了「确认落版」)" : "(工作台:用户点了「驳回」)",
2038
+ ...(batch ? { batch: true } : {}),
2039
+ quote: confirmed ? (batch ? "(工作台:用户点了「接受并更新」)" : "(工作台:用户点了「确认落版」)") : "(工作台:用户点了「驳回」)",
1835
2040
  }),
1836
2041
  });
1837
2042
  let payload = {};
@@ -1840,6 +2045,13 @@ async function decideProposals(proposalIds, confirmed) {
1840
2045
  flash(`操作失败:${payload?.error?.message || `HTTP ${res.status}`}`, "error");
1841
2046
  return;
1842
2047
  }
2048
+ if (payload.batch) {
2049
+ flash(`已接受并更新:${payload.decided} 条意见合成 revision ${payload.revision}(旧版保留,可回滚)`, "ok");
2050
+ STATE.docCache = {};
2051
+ await loadWorkbench(STATE.deckId);
2052
+ if (["script", "design", "intent", "changelog"].includes(STATE.tab)) await renderDocTab(STATE.tab);
2053
+ return;
2054
+ }
1843
2055
  const failed = (payload.results || []).filter((item) => !item.ok);
1844
2056
  const expired = failed.filter((item) => /已过期|工作副本已漂移/.test(String(item.error || "")));
1845
2057
  let message;
@@ -1853,7 +2065,55 @@ async function decideProposals(proposalIds, confirmed) {
1853
2065
  if (["script", "design", "intent", "changelog"].includes(STATE.tab)) await renderDocTab(STATE.tab);
1854
2066
  }
1855
2067
 
2068
+ /** F-173:回滚首稿(回到自动落版前的内容,追加新 revision)。 */
2069
+ async function rollbackFirstDraft(revision) {
2070
+ if (!STATE.deckId) return;
2071
+ const res = await fetch("/api/deck-studio.draft-rollback", {
2072
+ method: "POST",
2073
+ headers: { "content-type": "application/json" },
2074
+ body: JSON.stringify({ deckId: STATE.deckId, note: "(工作台:用户点了「回滚首稿」)" }),
2075
+ });
2076
+ let payload = {};
2077
+ try { payload = await res.json(); } catch { /* 非 JSON 走兜底 */ }
2078
+ if (!res.ok || !payload.ok) {
2079
+ flash(`回滚失败:${payload?.error?.message || `HTTP ${res.status}`}`, "error");
2080
+ return;
2081
+ }
2082
+ flash(`已回滚首稿:${payload.beforeRevision || revision} → ${payload.revision}(旧版保留)`, "ok");
2083
+ STATE.docCache = {};
2084
+ await loadWorkbench(STATE.deckId);
2085
+ if (["script", "changelog"].includes(STATE.tab)) await renderDocTab(STATE.tab);
2086
+ }
2087
+
1856
2088
  function bindDocActions(root) {
2089
+ root.querySelectorAll("details[data-assembled-prompt]").forEach((details) => {
2090
+ details.addEventListener("toggle", async () => {
2091
+ if (!details.open) return;
2092
+ const pageId = details.getAttribute("data-assembled-prompt");
2093
+ const body = details.querySelector("[data-assembled-body]");
2094
+ if (!pageId || !body || body.dataset.loaded === "1") return;
2095
+ const deckId = STATE.data?.active?.deckId;
2096
+ if (!deckId) return;
2097
+ body.textContent = "读取中…";
2098
+ try {
2099
+ const res = await fetch(`/api/deck-studio.page-prompt?` + new URLSearchParams({ deckId, pageId }), { headers: { accept: "application/json" } });
2100
+ const data = await res.json().catch(() => ({}));
2101
+ if (res.ok && data?.ok && data.prompt) {
2102
+ body.textContent = data.prompt;
2103
+ body.dataset.loaded = "1";
2104
+ } else {
2105
+ body.textContent = `读取失败:${data?.error?.message || "未知错误"}`;
2106
+ }
2107
+ } catch (error) {
2108
+ body.textContent = `读取失败:${String(error?.message ?? error)}`;
2109
+ }
2110
+ });
2111
+ });
2112
+ root.querySelectorAll("[data-doc-file]").forEach((btn) => btn.addEventListener("click", () => {
2113
+ const file = btn.getAttribute("data-doc-file") || "";
2114
+ if (file) openDocFile(STATE.tab, file);
2115
+ }));
2116
+ root.querySelectorAll("[data-rollback-draft]").forEach((btn) => btn.addEventListener("click", () => rollbackFirstDraft(btn.getAttribute("data-rollback-draft") || "")));
1857
2117
  root.querySelectorAll("[data-assoc-bind]").forEach((btn) => btn.addEventListener("click", () => bindDeckDesign(getEl("assocSelect")?.value || "")));
1858
2118
  root.querySelectorAll("[data-assoc-switch]").forEach((btn) => btn.addEventListener("click", openAssocSwitchPanel));
1859
2119
  root.querySelectorAll("[data-assoc-switch-confirm]").forEach((btn) => btn.addEventListener("click", confirmAssocSwitch));
@@ -1879,11 +2139,12 @@ function bindDocActions(root) {
1879
2139
  }));
1880
2140
  root.querySelectorAll("[data-assoc-unbind]").forEach((btn) => btn.addEventListener("click", () => bindDeckDesign("")));
1881
2141
  root.querySelectorAll("[data-doc-fill]").forEach((btn) => btn.addEventListener("click", () => askAgentForDoc(btn.dataset.docFill)));
1882
- // F-159:听众评审入口(脚本页)。评审只出意见,不改稿;改哪几条由用户说。
2142
+ // F-159 / F-172:听众评审入口(脚本页)——两段式(盲审 → 冻结 → 对照)。
2143
+ // 评审只出意见、不改稿;用户点这里是要「审完给结论再决定」,所以列完问题必须先问是否接受并更新。
1883
2144
  root.querySelectorAll("[data-script-review]").forEach((btn) =>
1884
2145
  btn.addEventListener("click", () =>
1885
2146
  sendPrompt(
1886
- `请调用 deck_script_review 评审 deck ${STATE.data.active.deckId} 的整份 script:按 intent 里的听众生成审稿人,只报告 P0/P1 与具体改法,不要直接改稿、不要出图。\n评审完先把 P0 逐条列给我(带页号),等我指定改哪几条,再用 deck_script_propose_edits 提案。`,
2147
+ `请调用 deck_script_review(phase=full)评审 deck ${STATE.data.active.deckId} 的整份 script:按 intent 里的听众先生成审稿人做盲审(不给意图、逐页四问、结论冻结),再对照意图做三方差分;不要直接改稿、不要出图。\n评审完先告诉我:盲审覆盖率、判定「否 / 部分」的页(带页号 + 原文证据 + 改法)、可删可合并的页。然后按稿子状态分开处理:\n① 如果这份 deck 还一次都没落过版(首稿):按「否 / 部分」自己改,最多 2 轮,然后用 deck_script_propose_edits 带 autoland: true + selfReviewRounds + selfReviewSummary 直接落版,落版后告我自改摘要和「可回滚」;\n② 如果已经有已落版 revision:停下来问我「是否接受并更新稿件」;我同意后按可采纳清单逐条提案(每条带 reviewRef/severity/confidence/evidence,C 类不提),变成脚本页的「评审意见」清单,我勾选后点「接受并更新」合成一条 revision。`,
1887
2148
  ),
1888
2149
  ),
1889
2150
  );
@@ -1910,6 +2171,17 @@ function bindDocActions(root) {
1910
2171
  decideProposals(ids, true);
1911
2172
  }));
1912
2173
  root.querySelectorAll("[data-confirm-all]").forEach((btn) => btn.addEventListener("click", () => decideProposals(freshProposalIds(), true)));
2174
+ // F-176 / I-18:勾选的评审意见一次接受并更新(一条 revision)。
2175
+ root.querySelectorAll("[data-accept-review]").forEach((btn) =>
2176
+ btn.addEventListener("click", () => {
2177
+ const picked = [...root.querySelectorAll("[data-review-pick]:checked")].map((input) => input.dataset.reviewPick).filter(Boolean);
2178
+ if (!picked.length) {
2179
+ flash("先勾选要采纳的意见(C 类无法核实,不能选)", "error");
2180
+ return;
2181
+ }
2182
+ decideProposals(picked, true, true);
2183
+ }),
2184
+ );
1913
2185
  root.querySelectorAll("[data-reject-all]").forEach((btn) => btn.addEventListener("click", () => decideProposals(allPendingProposalIds(), false)));
1914
2186
  root.querySelectorAll("[data-reject-stale]").forEach((btn) => btn.addEventListener("click", () => decideProposals(String(btn.dataset.rejectStale || "").split(",").filter(Boolean), false)));
1915
2187
  root.querySelectorAll("[data-reject-expired-all]").forEach((btn) => btn.addEventListener("click", () => decideProposals(deckPendingProposals().filter((proposal) => proposal.stale).map((proposal) => proposal.proposalId), false)));
@@ -4586,6 +4858,10 @@ function backToManual(box) {
4586
4858
  }
4587
4859
  getEl("embedBack").addEventListener("click", () => window.parent?.postMessage({ type: "dsh-deck-studio:close" }, "*"));
4588
4860
  await loadWorkbench();
4861
+ absorbUpdate(STATE.data);
4862
+ getEl("settingsBtn").addEventListener("click", openSettingsPanel);
4863
+ getEl("updateBtn").addEventListener("click", checkPluginUpdate);
4864
+ getEl("repoBtn").addEventListener("click", openRepo);
4589
4865
  await refreshSessions();
4590
4866
  getEl("save").textContent = EMBED ? "对话在 DSH 原生 Chat" : "对话在左侧 · DSH 原生";
4591
4867
  // F-69:只有 profile 开了 hideShellSidebar,宿主左侧栏才会默认收起;此时顶栏给一个切换入口。