dsh-plugin-deck-studio 0.2.6 → 0.2.8

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 (45) hide show
  1. package/cordis.example.yml +85 -7
  2. package/cordis.patch.yml +78 -7
  3. package/index.js +176 -25
  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/review-agent.js +1 -1
  18. package/lib/review-personas.js +49 -2
  19. package/lib/review-protocol.js +411 -0
  20. package/lib/review-readers.js +300 -0
  21. package/lib/review-score.js +198 -0
  22. package/lib/title-chain.js +122 -0
  23. package/lib/tools.js +971 -77
  24. package/package.json +2 -1
  25. package/seeds/docs/assets/screenshots/f186-01-agent-style-create.png +0 -0
  26. package/seeds/docs/assets/screenshots/f186-02-designs-card.png +0 -0
  27. package/seeds/docs/assets/screenshots/f187-01-review-readers.png +0 -0
  28. package/seeds/docs/assets/screenshots/f187-02-gap-report.png +0 -0
  29. package/seeds/docs/assets/screenshots/i14-01-create.png +0 -0
  30. package/seeds/docs/assets/screenshots/i14-02-first-draft.png +0 -0
  31. package/seeds/docs/assets/screenshots/i14-03-rendered.png +0 -0
  32. package/seeds/docs/assets/screenshots/i14-04-image-review.png +0 -0
  33. package/seeds/docs/assets/screenshots/i14-05-p01-page.png +0 -0
  34. package/seeds/docs/install.en.md +13 -3
  35. package/seeds/docs/install.md +13 -4
  36. package/seeds/docs/sample-deck.en.md +1 -1
  37. package/seeds/docs/sample-deck.md +1 -1
  38. package/seeds/docs/styles.en.md +2 -1
  39. package/seeds/docs/styles.md +14 -4
  40. package/seeds/samples/DSA-01.jpg +0 -0
  41. package/seeds/samples/DSA-02.jpg +0 -0
  42. package/seeds/samples/DSA-03.jpg +0 -0
  43. package/seeds/styles/DSA.json +22 -0
  44. package/ui/workbench.css +5 -0
  45. package/ui/workbench.html +98 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-deck-studio",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "private": false,
5
5
  "description": "Deckline:DSH 的 image deck / PPT 工作台插件——原生 Chat + 整页图片 deck 工作区 + 设计风格库;用对话做 PPT、按页出图、版本可追踪、导出 PDF/PPTX。",
6
6
  "license": "MIT",
@@ -9,6 +9,7 @@
9
9
  "exports": {
10
10
  ".": "./index.js",
11
11
  "./client": "./client.js",
12
+ "./capabilities": "./lib/capabilities.js",
12
13
  "./package.json": "./package.json"
13
14
  },
14
15
  "keywords": [
@@ -6,7 +6,17 @@ assets / conversations on the right. It is not a standalone app — it runs on a
6
6
 
7
7
  > Target users: people who build content and presentations with DSH. After setup you can edit a deck
8
8
  > from its script, render slide images on request (single page or a named batch), manage governance documents, register your own
9
- > design styles (with sample images) and start from 8 preset styles.
9
+ > design styles (with sample images) and start from 9 preset styles.
10
+
11
+ ## Assistant capabilities (0.2.7+)
12
+
13
+ The Deckline preset explicitly mounts Web search/fetch, scoped file/image reading and search, scratch-file write/edit, a read-only research subagent, questions, todos, file delivery, time context and compaction. No shell, arbitrary-preset delegation or developer skills; internal audience reviewers remain tool-free.
14
+
15
+ Search uses the host Web settings. The default DeepSeek backend requires its account/`DEEPSEEK_API_KEY`; a third-party chat key does not configure search. HTTP fetch reads public destinations. Missing credentials must produce an explicit error, not fabricated results.
16
+
17
+ Generic reads are limited to the session workspace and explicit `deck-capabilities.config.readRoots`; secret paths and symlinks are denied. Authorize exact external material paths rather than an entire home directory. DSH image attachments use explicit `attachmentRoots` within its attachment objects subtree, for `read_image` only; this does not authorize the whole DSH directory. Image input also requires a vision-capable model route. Generic writes/edits are restricted to `.deck-assistant-scratch` under the workspace. Formal scripts, intent and revisions must use deck tools. Narrow directory searches to clean material directories if secret/symlink checks reject a broad scan.
18
+
19
+ If a handwritten profile overrides `preset-deck-studio.config.plugins`, merge the capability entries from the updated `cordis.example.yml`, restart and check a new session. Do not mount the whole standard preset or shell as a workaround.
10
20
 
11
21
  ## 0. Prerequisites
12
22
 
@@ -229,9 +239,9 @@ Refresh the page afterwards. png / jpg / webp are supported; names are free-form
229
239
 
230
240
  ### Use preset styles (no prompt writing needed)
231
241
 
232
- **+ Import preset styles** on the Designs page imports all 8 bundled styles (business consulting /
242
+ **+ Import preset styles** on the Designs page imports all 9 bundled styles (business consulting /
233
243
  premium dark / Japanese minimal / magazine editorial / tech blueprint / hand-drawn whiteboard /
234
- Swiss minimal / aurora gradient), each with samples. See [styles.md](styles.md).
244
+ Swiss minimal / aurora gradient / deep-sea amber), each with samples. See [styles.md](styles.md).
235
245
 
236
246
  ## 6. Troubleshooting
237
247
 
@@ -7,7 +7,16 @@ Deckline(`dsh-plugin-deck-studio`)是 DeepSeek Harness(DSH)的 **image d
7
7
  它不是独立 App,必须挂在 DSH 的 profile 上运行。
8
8
 
9
9
  > 目标用户:用 DSH 做内容/演示的人。装完后你可以:读 script 改 deck、按页出图(可点名批量连出)、管理治理文档、
10
- > 登记自己的设计风格(上传样张),并用 8 个预设风格直接开工。
10
+ > 登记自己的设计风格(上传样张),并用 9 个预设风格直接开工。
11
+
12
+ ## 主助手基础能力(0.2.7起)
13
+
14
+ Deckline制作模式显式提供联网搜索/抓取、文件/图片读取与检索、辅助草稿写编辑、只读研究子Agent、提问/待办/文件交付、时间与上下文压缩。不开放shell、任意preset委派或开发技能;内部听众审稿仍零工具。
15
+
16
+ - `web_search`使用DSH宿主的搜索设置;默认DeepSeek后端需相应账户或`DEEPSEEK_API_KEY`。使用第三方LLM key不代表搜索已配置。`web_fetch`默认读取公开HTTP(S)网页。缺凭证/失败明确报错,不能靠加提示词修复。
17
+ - 通用读只允许当前会话工作区与`deck-capabilities.config.readRoots`显式授权路径;秘密文件和symlink拒绝。读取其他资料请在profile精确授权,勿授权整个home;DSH图片附件使用`attachmentRoots`精确授权其objects子树下的图片文件/目录,仅`read_image`可用,不授权整个DSH目录。视觉读取还要求当前模型路由支持image输入。
18
+ - 通用`write/edit`仅限工作区`.deck-assistant-scratch`辅助目录;脚本/意图/版本须用deck工具,不可绕过确认。宽泛目录搜索因秘密文件/链接被拒时缩小到干净资料目录。
19
+ - 手写profile若覆盖`preset-deck-studio.config.plugins`,请合并新版`cordis.example.yml`的基础能力条目,重启后用新会话核对工具。不要把完整标准preset或Bash直接挂进来。
11
20
 
12
21
  ## 0. 前置条件
13
22
 
@@ -285,8 +294,8 @@ export DSH_DECK_IMAGE_API_KEY="sk-…" # 只放在你的 shell / 密钥
285
294
 
286
295
  ### 用预设风格(不用自己写 prompt)
287
296
 
288
- Designs 页 **+ 导入预设风格** 会一次导入 8 个自带风格(商务咨询 / 高端深色 fancy / 日系极简 /
289
- 杂志编辑 / 科技蓝图 / 手绘白板 / 瑞士极简 / 渐变极光),每个都带样张,见 [docs/styles.md](styles.md)。
297
+ Designs 页 **+ 导入预设风格** 会一次导入 9 个自带风格(商务咨询 / 高端深色 fancy / 日系极简 /
298
+ 杂志编辑 / 科技蓝图 / 手绘白板 / 瑞士极简 / 渐变极光 / 深海琥珀),每个都带样张,见 [docs/styles.md](styles.md)。
290
299
 
291
300
  ## 6. 排障
292
301
 
@@ -319,7 +328,7 @@ dsh --profile deck-studio --no-open # 本仓开发 profile(端口
319
328
  | 左侧 DSH 外壳栏(工作区 / 会话) | 收起隐藏,只留 Deckline 导航 | profile 里 `hideShellSidebar: false` |
320
329
  | Decks 页 | 自动登记 **Deckline 示例手册**(仓库 `samples/deckline-intro`,只读) | 配了任意 `decks[]` 就不登记;或 `bootstrap: false` |
321
330
  | 资源库 | 自动拷入品牌素材(logo / mark)到 `<dataRoot>/resources` | 该目录非空时不动;或 `bootstrap: false` |
322
- | Designs 页 | 自动导入 8 个预设风格(含样张) | 已有风格时不重复;或 `bootstrap: false` |
331
+ | Designs 页 | 自动导入 9 个预设风格(含样张) | 已有风格时不重复;或 `bootstrap: false` |
323
332
  | 问候语 / 新 deck 作者 | 首次启动弹「先记个名字」,存 `<dataRoot>/user.json`;首页显示「XX,欢迎你」,新 deck 的 owner / 封面作者默认用它 | 点「稍后」不写;之后可在 profile 里用 `userName` 覆盖 |
324
333
 
325
334
  自举只在「空」的时候发生,重复启动不会覆盖你已有的内容;想彻底关掉,在 profile patch 里给根挂载加
@@ -44,7 +44,7 @@ Field cheat sheet (copy-paste ready): [`samples/deckline-intro/popup.md`](../sam
44
44
  CHANGELOG.md —— v0.1.0 能力清单与边界
45
45
  docs/productization.md —— 产品化定位、路线图、发布范围、一期边界
46
46
  docs/install.md —— 安装、profile 配置、出图节点与 key 放置、fake 模式
47
- docs/styles.md —— 8 个预设风格 + 自定义风格方法
47
+ docs/styles.md —— 9 个预设风格 + 自定义风格方法
48
48
  docs/sample-deck.md —— 用示例 deck 跑通流程(本页步骤来源)
49
49
 
50
50
  【2. 代码(真实链接)】
@@ -41,7 +41,7 @@
41
41
  CHANGELOG.md —— v0.1.0 能力清单与边界
42
42
  docs/productization.md —— 产品化定位、路线图、发布范围、一期边界
43
43
  docs/install.md —— 安装、profile 配置、出图节点与 key 放置、fake 模式
44
- docs/styles.md —— 8 个预设风格 + 自定义风格方法
44
+ docs/styles.md —— 9 个预设风格 + 自定义风格方法
45
45
  docs/sample-deck.md —— 用示例 deck 跑通流程(本页步骤来源)
46
46
 
47
47
  【2. 代码(真实链接)】
@@ -6,7 +6,7 @@ language for the images.
6
6
 
7
7
  ## 1. Use preset styles (recommended starting point)
8
8
 
9
- Designs page → **+ Import preset styles** imports the 8 styles below in one click (each ships with
9
+ Designs page → **+ Import preset styles** imports the 9 styles below in one click (each ships with
10
10
  3 real baseline samples: cover / info page / data page). It is idempotent — existing abbreviations are
11
11
  skipped, never duplicated.
12
12
 
@@ -20,6 +20,7 @@ skipped, never duplicated.
20
20
  | SKT | Hand-drawn whiteboard (workshop) | Whiteboard + markers + sticky notes + handwritten headings | Workshops, co-creation, training |
21
21
  | SWS | Swiss minimal (black/white/red) | Pure white + oversized black type + true red + generous whitespace | Minimal manifestos, big-type posters |
22
22
  | GRD | Aurora gradient (modern SaaS) | Violet-cyan aurora gradients + glow + 3D abstraction + glass cards | Product launches, internet style |
23
+ | DSA | Deep Sea Amber (deep teal + gold hairlines) | Near-ink teal base + champagne-gold hairlines + serif headlines + generous whitespace | Premium talks, dark narratives, big-type covers |
23
24
 
24
25
  Definitions: `plugin/deck-studio/seeds/styles/<ABBR>.json`; samples:
25
26
  `plugin/deck-studio/seeds/samples/<ABBR>-0{1,2,3}.jpg` (3 baseline samples, 1280-wide previews).
@@ -7,7 +7,7 @@ Agent 出图时会以这份描述作为画面语言的基础。
7
7
 
8
8
  ## 一、用预设风格(推荐先从这里开始)
9
9
 
10
- Designs 页 → **+ 导入预设风格**:一次导入下面 8 个风格(每个都带 3 张真实基线样张:封面 / 信息页 / 数据页),幂等——
10
+ Designs 页 → **+ 导入预设风格**:一次导入下面 9 个风格(每个都带 3 张真实基线样张:封面 / 信息页 / 数据页),幂等——
11
11
  已存在的缩写自动跳过,不会重复创建。
12
12
 
13
13
  | 缩写 | 名称 | 画面语言 | 适合 |
@@ -20,12 +20,22 @@ Designs 页 → **+ 导入预设风格**:一次导入下面 8 个风格(
20
20
  | SKT | 手绘白板(工作坊) | 白板 + 马克笔 + 便签 + 手写标题 | 研讨、共创、培训 |
21
21
  | SWS | 瑞士极简(黑白红) | 纯白 + 超大黑体 + 正红 + 大留白 | 极简宣言、大字报 |
22
22
  | GRD | 渐变极光(深色玻璃 · 编辑感) | 近黑炭底 + 紫青极光丝带 + 玻璃面板 + 香槟金细线 | 深色封面、编辑感信息页 |
23
+ | DSA | 深海琥珀(深青 + 香槟金细线) | 深青近墨底 + 香槟金细线 + 衬线大标题 + 大留白 | 高端分享、深色叙事、封面大标题 |
23
24
 
24
25
  定义文件:`plugin/deck-studio/seeds/styles/<ABBR>.json`;样张:`plugin/deck-studio/seeds/samples/<ABBR>-0{1,2,3}.jpg`
25
26
  (3 张基线样张,1280 宽预览)。导入逻辑幂等:再点一次只会跳过已存在的风格。
26
27
 
27
28
  ## 二、自己加一个风格
28
29
 
30
+ ### 方式 0:普通会话里直接说(F-186,做完才发现风格时最省事)
31
+
32
+ 没先选风格也做了 deck,觉得这套视觉语言想复用?直接在 Deckline 会话里说「这个风格好看,帮我登记一下」:
33
+
34
+ 1. Agent 先读页面实际视觉,写一套风格提示词(颜色 / 材质 / 字体 / 版式);
35
+ 2. `deck_style_create` 建 design(缩写不用你给,自动生成),并把已出图页面的**成品图复制**成样张——默认挑 3 张(封面 / 信息页 / 数据对比页各一,三张才说明风格在各类版面上都成立;不重新出图、不花额度);
36
+ 3. 登记完即可在新建 Deck 时选到,也可以用 `deck_design_bind` 把当前 deck 关联过去;
37
+ 4. 想给已有风格再加样张:让它传 `designId` + `pages` 补(只加样张,不动名称和提示词)。
38
+
29
39
  ### 方式 1:界面里加(可以贴图)
30
40
 
31
41
  1. Designs 页 → **+ 添加风格**;
@@ -59,7 +69,7 @@ Designs 页 → **+ 导入预设风格**:一次导入下面 8 个风格(
59
69
  ```
60
70
 
61
71
  - `styleBrief`:**风格提示词本体**。Agent 出样张时自动带上(`composeSamplePrompt`),只需要再补「这张要试的画面」(brief)。
62
- 预设包里的 8 个风格都已按真实样张补齐;改风格用 `deck_style_update` / 界面「复制」后改。
72
+ 预设包里的 9 个风格都已按真实样张补齐;改风格用 `deck_style_update` / 界面「复制」后改。
63
73
  注意:`reference` 只给人看,不进 image prompt——写机构 / 客户品牌名会触发 image adapter 内容过滤(表现为 180s 超时)。
64
74
  - `baselineBriefs`:预设自带的 3 张基线样张的版面 brief(封面 / 信息页 / 数据页),供重制脚本与「导入预设」使用。
65
75
 
@@ -83,11 +93,11 @@ Designs 页 → **+ 导入预设风格**:一次导入下面 8 个风格(
83
93
 
84
94
  ## 五、预设基线样张(3 张打底)与重制
85
95
 
86
- 8 个预设风格每个都有 3 张基线样张(封面 / 信息页 / 数据页),作用是:**一眼看清这个风格能做成什么样**,
96
+ 9 个预设风格每个都有 3 张基线样张(封面 / 信息页 / 数据页),作用是:**一眼看清这个风格能做成什么样**,
87
97
  同时给 Agent 一个「同风格但不同版面」的参照。重制流程:
88
98
 
89
99
  ```bash
90
- # 8 个预设 × 3 张(1920×1080 出图 → 压 1280 宽 JPEG 回写 seeds/samples)
100
+ # 9 个预设 × 3 张(1920×1080 出图 → 压 1280 宽 JPEG 回写 seeds/samples)
91
101
  node scripts/f74-regenerate-presets.mjs
92
102
  # 只重制一个风格 / 只试一张
93
103
  node scripts/f74-regenerate-presets.mjs --only=BIZ --limit=1
Binary file
Binary file
Binary file
@@ -0,0 +1,22 @@
1
+ {
2
+ "abbr": "DSA",
3
+ "name": "深海琥珀(深青 + 香槟金细线)",
4
+ "description": "深青近墨底 + 香槟金细线 + 衬线中文大标题 + 大面积留白 + 右侧竖排英文小字;信息页用暖米色直角卡片承载要点。克制、单一层次。",
5
+ "constraints": "不要亮色 / 高饱和色块;不要渐变、光晕、玻璃拟态;不要金色填充块或粗金边;不要无衬线做大标题;不要图标堆砌、插画、3D;不要满版密排或多栏密集网格;不要重投影卡片。",
6
+ "reference": "高端分享会 / 深色咨询风(深海青底 + 香槟金细线),非客户素材",
7
+ "styleBrief": "深青(deep teal)近墨底铺满整页,无渐变、无明显纹理,压暗整体明度。香槟金(champagne gold)极细线条只做结构与框定:1px 页边细框、细横线分隔,不用金色填充块、不做粗金边。中文大标题用高对比衬线(宋体系),字重中等偏粗、字号大、行距紧,左对齐,是页面唯一视觉重心;正文用低对比灰青小字,行距舒展。大面积留白不少于 40%,元素集中在左半区或上下角,不做满版密排。英文小字竖排在页面右侧边缘,字号小、字距拉开、自上而下,作版式标注。信息页用暖米色(warm ivory)直角卡片承载要点,无阴影或极弱阴影,卡片内字用深青 / 近黑。整体克制、单一层次,靠留白与细线建立秩序,不靠图标与插画。",
8
+ "baselineBriefs": [
9
+ {
10
+ "title": "封面(深青金线)",
11
+ "brief": "深青近墨底满版铺底,无渐变;1px 香槟金页边细框 + 一条细金横线。左对齐两行中文衬线大标题(第二行略小、含一个金色词或金色细下划线),标题上方一枚加宽字距的金色小标签;右下角一行低对比灰青小字注记与页码;右侧边缘竖排英文小字;留白不少于 40%,不放大图、不放插画。"
12
+ },
13
+ {
14
+ "title": "数据对比页",
15
+ "brief": "深青近墨底 + 金色细框;左上两行中文衬线标题 + 一行灰青小字结论;中部并排两块暖米色直角卡片(各含一个大数字 / 短标签 / 两行说明,数字用深青近黑),两块卡片之间一条金色细竖线分隔,卡片无阴影或极弱阴影;底部细金横线 + 页码;右侧竖排英文小字。"
16
+ },
17
+ {
18
+ "title": "要点 / 决策页",
19
+ "brief": "深青近墨底 + 金色细框;左上中文衬线大标题,下方 3 组要点用细金横线分隔,每组一行加宽字距的小标签 + 两行低对比灰青说明,不用图标、不用色块;右侧竖排英文小字标注;页面下部大面积留白,底部金线 + 页码。"
20
+ }
21
+ ]
22
+ }
package/ui/workbench.css CHANGED
@@ -86,6 +86,11 @@
86
86
  .doc-head{display:flex;align-items:flex-start;gap:12px;flex-wrap:wrap;margin-bottom:10px}
87
87
  .doc-head h2{margin:6px 0 0;font-family:"Noto Serif SC",Georgia,serif;font-size:22px;color:var(--ink)}
88
88
  .doc-head .doc-file{margin-left:auto;color:#8a9599;font-size:10.5px;align-self:center}
89
+ /* F-187:审稿页的相关文件切换(听众读后感 / 意图差距 / 工程口径 …) */
90
+ .doc-head .doc-files{margin-left:auto;display:flex;gap:6px;flex-wrap:wrap;justify-content:flex-end;max-width:70%}
91
+ .doc-file-chip{border:1px solid #d7dee0;background:#fff;color:#5a6a70;border-radius:999px;padding:3px 10px;font-size:10.5px;cursor:pointer}
92
+ .doc-file-chip:hover{border-color:#0f6f7a;color:#0f6f7a}
93
+ .doc-file-chip.active{background:#0f6f7a;border-color:#0f6f7a;color:#fff}
89
94
  .doc-status{font-size:10.5px;padding:2px 9px;border-radius:9px;border:1px solid var(--line);color:#6c7a7e;white-space:nowrap}
90
95
  .doc-status.ok{color:#2f7a52;border-color:color-mix(in srgb,#2f7a52 40%,transparent);background:#eef6f0}
91
96
  .doc-status.partial,.doc-status.draft{color:var(--gold);border-color:color-mix(in srgb,var(--gold) 45%,transparent);background:#fbf4e8}
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}
@@ -1399,10 +1411,21 @@ function docHeader(doc, extra = "") {
1399
1411
  <div><div class="eyebrow">Deck Document</div><h2>${esc(doc.label)}</h2></div>
1400
1412
  ${docBadge(doc.status)}
1401
1413
  <span class="small doc-file">${esc(doc.primary || "无文件")}</span>
1414
+ ${(doc.files || []).length > 1 ? `<div class="doc-files">${doc.files.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>` : ""}
1402
1415
  ${extra}
1403
1416
  </div>`;
1404
1417
  }
1405
1418
 
1419
+ /** F-187:审稿页有读者本 / 差距本 / 工程本,点 chip 切换正文。 */
1420
+ async function openDocFile(docId, file) {
1421
+ const deckId = STATE.data?.active?.deckId ?? "";
1422
+ const res = await fetch(`/api/deck-studio.doc?` + new URLSearchParams({ deckId, docId, file }));
1423
+ const doc = await res.json().catch(() => null);
1424
+ if (!res.ok || !doc || doc.ok === false) return;
1425
+ STATE.docCache[docId] = doc;
1426
+ renderDocTab(docId);
1427
+ }
1428
+
1406
1429
  function docFooter(doc, content) {
1407
1430
  return `<details class="doc-raw"><summary>查看原始 markdown</summary><div class="md"><pre>${esc(content || "(空)")}</pre></div></details>`;
1408
1431
  }
@@ -1557,7 +1580,25 @@ function renderDoc(doc, index) {
1557
1580
  const name = kind === "body" ? "正文" : "画面要求";
1558
1581
  return `<span class="density-count${off ? " is-off" : ""}" title="当前密度「${esc(densityTarget.label)}」:${name}目标 ${min}–${max} 字">${name} ${chars} 字${off ? ` · 目标 ${min}–${max}` : " · 达标"}</span>`;
1559
1582
  };
1560
- return head + styleNote + densityNote + briefBanner + docIssues(doc) + storyOverview + banner + `<div class="page-cards">${pages.map((page) => {
1583
+ // F-176 / I-18:评审意见清单——带 reviewRef / severity / confidence 的提案合成一个可勾选清单,
1584
+ // 勾完点「接受并更新」→ 一条 revision(不是一条提案一条)。C 类(原文找不到)根本不进提案。
1585
+ const reviewBatch = actionablePending.filter((proposal) => proposal.reviewRef || proposal.severity || proposal.confidence);
1586
+ const reviewRow = (proposal) => {
1587
+ const confidence = String(proposal.confidence || "").toUpperCase();
1588
+ const selectable = confidence !== "C";
1589
+ const tag = [proposal.reviewRef, proposal.severity, confidence ? `证据 ${confidence}` : ""].filter(Boolean).join(" · ");
1590
+ return `<label class="review-row${selectable ? "" : " review-row-off"}">
1591
+ <input type="checkbox" data-review-pick="${esc(proposal.proposalId)}"${selectable ? (confidence === "D" ? "" : " checked") : " disabled"}>
1592
+ <span class="review-tag">${esc(tag || "评审意见")}</span>
1593
+ <span class="review-why">${esc(String(proposal.reason || proposal.evidence || "按评审意见修改").slice(0, 80))}</span>
1594
+ <span class="review-page">${esc(proposal.pageId)} · ${esc(proposalFieldLabel(proposal.field))}</span>
1595
+ <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>
1596
+ </label>`;
1597
+ };
1598
+ const reviewPanel = reviewBatch.length
1599
+ ? `<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>`
1600
+ : "";
1601
+ return head + styleNote + densityNote + briefBanner + docIssues(doc) + storyOverview + banner + reviewPanel + `<div class="page-cards">${pages.map((page) => {
1561
1602
  const pageProposals = pendingByPage.get(Number(page.page)) || [];
1562
1603
  const slide = slideById.get(page.id) || {};
1563
1604
  // 已落版值(manifest)优先:确认过的 title/question/body 必须立刻显示,不能被源文档快照盖住。
@@ -1678,6 +1719,12 @@ function renderDoc(doc, index) {
1678
1719
  if (doc.id === "changelog") {
1679
1720
  const feed = doc.parsed.feed ?? [];
1680
1721
  const fmtWhen = (value) => String(value || "").replace("T", " ").slice(0, 16).replace(/-/g, "/");
1722
+ // F-173:最新一版是首稿自动落版时,给出「这版没经过确认」+ 一键回滚。
1723
+ const latestRevision = feed.map((row) => row.revision).filter(Boolean).sort().pop() || "";
1724
+ const autoRow = feed.find((row) => row.autoland && row.autoland.revision === latestRevision && !row.autoland.rolledBack);
1725
+ const rollbackBar = autoRow
1726
+ ? `<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>`
1727
+ : "";
1681
1728
  const list = feed.length
1682
1729
  ? `<div class="change-feed">${feed.map((row) => `
1683
1730
  <div class="change-item">
@@ -1691,7 +1738,7 @@ function renderDoc(doc, index) {
1691
1738
  <div class="change-note">${row.note ? mdInline(row.note) : `<span class="small">系统记录,未写说明</span>`}</div>
1692
1739
  </div>`).join("")}</div>`
1693
1740
  : `<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);
1741
+ return head + docIssues(doc) + rollbackBar + list + `<p class="engine-note">这张日志由系统从版本与事件自动生成;Agent 每次改完 deck 只用补一句「为什么」(工具 <code>deck_note_change</code>),不需要手抄历史。</p>` + docFooter(doc, content);
1695
1742
  }
1696
1743
  if (doc.id === "feedback") {
1697
1744
  const rows = doc.parsed.rows ?? [];
@@ -1822,7 +1869,7 @@ function flash(text, kind = "ok") {
1822
1869
  }
1823
1870
 
1824
1871
  /** F-108:在工作台直接落版 / 驳回提案(与 Agent 的 deck_revision_confirm 同一条写入路径)。 */
1825
- async function decideProposals(proposalIds, confirmed) {
1872
+ async function decideProposals(proposalIds, confirmed, batch = false) {
1826
1873
  const ids = (proposalIds || []).filter(Boolean);
1827
1874
  if (!ids.length) return;
1828
1875
  const res = await fetch("/api/deck-studio.proposal-decide", {
@@ -1831,7 +1878,8 @@ async function decideProposals(proposalIds, confirmed) {
1831
1878
  body: JSON.stringify({
1832
1879
  proposalIds: ids,
1833
1880
  confirmed,
1834
- quote: confirmed ? "(工作台:用户点了「确认落版」)" : "(工作台:用户点了「驳回」)",
1881
+ ...(batch ? { batch: true } : {}),
1882
+ quote: confirmed ? (batch ? "(工作台:用户点了「接受并更新」)" : "(工作台:用户点了「确认落版」)") : "(工作台:用户点了「驳回」)",
1835
1883
  }),
1836
1884
  });
1837
1885
  let payload = {};
@@ -1840,6 +1888,13 @@ async function decideProposals(proposalIds, confirmed) {
1840
1888
  flash(`操作失败:${payload?.error?.message || `HTTP ${res.status}`}`, "error");
1841
1889
  return;
1842
1890
  }
1891
+ if (payload.batch) {
1892
+ flash(`已接受并更新:${payload.decided} 条意见合成 revision ${payload.revision}(旧版保留,可回滚)`, "ok");
1893
+ STATE.docCache = {};
1894
+ await loadWorkbench(STATE.deckId);
1895
+ if (["script", "design", "intent", "changelog"].includes(STATE.tab)) await renderDocTab(STATE.tab);
1896
+ return;
1897
+ }
1843
1898
  const failed = (payload.results || []).filter((item) => !item.ok);
1844
1899
  const expired = failed.filter((item) => /已过期|工作副本已漂移/.test(String(item.error || "")));
1845
1900
  let message;
@@ -1853,7 +1908,32 @@ async function decideProposals(proposalIds, confirmed) {
1853
1908
  if (["script", "design", "intent", "changelog"].includes(STATE.tab)) await renderDocTab(STATE.tab);
1854
1909
  }
1855
1910
 
1911
+ /** F-173:回滚首稿(回到自动落版前的内容,追加新 revision)。 */
1912
+ async function rollbackFirstDraft(revision) {
1913
+ if (!STATE.deckId) return;
1914
+ const res = await fetch("/api/deck-studio.draft-rollback", {
1915
+ method: "POST",
1916
+ headers: { "content-type": "application/json" },
1917
+ body: JSON.stringify({ deckId: STATE.deckId, note: "(工作台:用户点了「回滚首稿」)" }),
1918
+ });
1919
+ let payload = {};
1920
+ try { payload = await res.json(); } catch { /* 非 JSON 走兜底 */ }
1921
+ if (!res.ok || !payload.ok) {
1922
+ flash(`回滚失败:${payload?.error?.message || `HTTP ${res.status}`}`, "error");
1923
+ return;
1924
+ }
1925
+ flash(`已回滚首稿:${payload.beforeRevision || revision} → ${payload.revision}(旧版保留)`, "ok");
1926
+ STATE.docCache = {};
1927
+ await loadWorkbench(STATE.deckId);
1928
+ if (["script", "changelog"].includes(STATE.tab)) await renderDocTab(STATE.tab);
1929
+ }
1930
+
1856
1931
  function bindDocActions(root) {
1932
+ root.querySelectorAll("[data-doc-file]").forEach((btn) => btn.addEventListener("click", () => {
1933
+ const file = btn.getAttribute("data-doc-file") || "";
1934
+ if (file) openDocFile(STATE.tab, file);
1935
+ }));
1936
+ root.querySelectorAll("[data-rollback-draft]").forEach((btn) => btn.addEventListener("click", () => rollbackFirstDraft(btn.getAttribute("data-rollback-draft") || "")));
1857
1937
  root.querySelectorAll("[data-assoc-bind]").forEach((btn) => btn.addEventListener("click", () => bindDeckDesign(getEl("assocSelect")?.value || "")));
1858
1938
  root.querySelectorAll("[data-assoc-switch]").forEach((btn) => btn.addEventListener("click", openAssocSwitchPanel));
1859
1939
  root.querySelectorAll("[data-assoc-switch-confirm]").forEach((btn) => btn.addEventListener("click", confirmAssocSwitch));
@@ -1879,11 +1959,12 @@ function bindDocActions(root) {
1879
1959
  }));
1880
1960
  root.querySelectorAll("[data-assoc-unbind]").forEach((btn) => btn.addEventListener("click", () => bindDeckDesign("")));
1881
1961
  root.querySelectorAll("[data-doc-fill]").forEach((btn) => btn.addEventListener("click", () => askAgentForDoc(btn.dataset.docFill)));
1882
- // F-159:听众评审入口(脚本页)。评审只出意见,不改稿;改哪几条由用户说。
1962
+ // F-159 / F-172:听众评审入口(脚本页)——两段式(盲审 → 冻结 → 对照)。
1963
+ // 评审只出意见、不改稿;用户点这里是要「审完给结论再决定」,所以列完问题必须先问是否接受并更新。
1883
1964
  root.querySelectorAll("[data-script-review]").forEach((btn) =>
1884
1965
  btn.addEventListener("click", () =>
1885
1966
  sendPrompt(
1886
- `请调用 deck_script_review 评审 deck ${STATE.data.active.deckId} 的整份 script:按 intent 里的听众生成审稿人,只报告 P0/P1 与具体改法,不要直接改稿、不要出图。\n评审完先把 P0 逐条列给我(带页号),等我指定改哪几条,再用 deck_script_propose_edits 提案。`,
1967
+ `请调用 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
1968
  ),
1888
1969
  ),
1889
1970
  );
@@ -1910,6 +1991,17 @@ function bindDocActions(root) {
1910
1991
  decideProposals(ids, true);
1911
1992
  }));
1912
1993
  root.querySelectorAll("[data-confirm-all]").forEach((btn) => btn.addEventListener("click", () => decideProposals(freshProposalIds(), true)));
1994
+ // F-176 / I-18:勾选的评审意见一次接受并更新(一条 revision)。
1995
+ root.querySelectorAll("[data-accept-review]").forEach((btn) =>
1996
+ btn.addEventListener("click", () => {
1997
+ const picked = [...root.querySelectorAll("[data-review-pick]:checked")].map((input) => input.dataset.reviewPick).filter(Boolean);
1998
+ if (!picked.length) {
1999
+ flash("先勾选要采纳的意见(C 类无法核实,不能选)", "error");
2000
+ return;
2001
+ }
2002
+ decideProposals(picked, true, true);
2003
+ }),
2004
+ );
1913
2005
  root.querySelectorAll("[data-reject-all]").forEach((btn) => btn.addEventListener("click", () => decideProposals(allPendingProposalIds(), false)));
1914
2006
  root.querySelectorAll("[data-reject-stale]").forEach((btn) => btn.addEventListener("click", () => decideProposals(String(btn.dataset.rejectStale || "").split(",").filter(Boolean), false)));
1915
2007
  root.querySelectorAll("[data-reject-expired-all]").forEach((btn) => btn.addEventListener("click", () => decideProposals(deckPendingProposals().filter((proposal) => proposal.stale).map((proposal) => proposal.proposalId), false)));