dsh-plugin-deck-studio 0.0.0-stage → 0.2.3

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 (133) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +47 -2
  3. package/client.js +500 -0
  4. package/cordis.example.yml +163 -0
  5. package/cordis.patch.yml +147 -0
  6. package/index.js +2340 -0
  7. package/lib/config.js +216 -0
  8. package/lib/content-density.js +98 -0
  9. package/lib/deck-brief.js +87 -0
  10. package/lib/deck-docs.js +1095 -0
  11. package/lib/deck-rules.js +124 -0
  12. package/lib/deck-templates.js +149 -0
  13. package/lib/deck-workspace.js +480 -0
  14. package/lib/design-style.js +157 -0
  15. package/lib/designs.js +175 -0
  16. package/lib/diff.js +88 -0
  17. package/lib/exporter.js +336 -0
  18. package/lib/faq.js +300 -0
  19. package/lib/git-commit.js +87 -0
  20. package/lib/home-catalog.js +249 -0
  21. package/lib/png.js +275 -0
  22. package/lib/proposal-decision.js +204 -0
  23. package/lib/render-nodes.js +171 -0
  24. package/lib/render-settings.js +213 -0
  25. package/lib/render.js +287 -0
  26. package/lib/review-personas.js +258 -0
  27. package/lib/script-style.js +55 -0
  28. package/lib/script-writer.js +165 -0
  29. package/lib/source-adapter.js +327 -0
  30. package/lib/store.js +212 -0
  31. package/lib/tools.js +2594 -0
  32. package/lib/watermark.js +137 -0
  33. package/package.json +67 -4
  34. package/seeds/docs/assets/screenshots/00-hero.png +0 -0
  35. package/seeds/docs/assets/screenshots/01-home.png +0 -0
  36. package/seeds/docs/assets/screenshots/02-deck-pages.png +0 -0
  37. package/seeds/docs/assets/screenshots/03-script.png +0 -0
  38. package/seeds/docs/assets/screenshots/04-design.png +0 -0
  39. package/seeds/docs/assets/screenshots/05-governance.png +0 -0
  40. package/seeds/docs/assets/screenshots/06-settings.png +0 -0
  41. package/seeds/docs/assets/screenshots/07-new-deck.png +0 -0
  42. package/seeds/docs/assets/screenshots/manual/01-home.png +0 -0
  43. package/seeds/docs/assets/screenshots/manual/02-deck-pages.png +0 -0
  44. package/seeds/docs/assets/screenshots/manual/03-script.png +0 -0
  45. package/seeds/docs/assets/screenshots/manual/04-design.png +0 -0
  46. package/seeds/docs/assets/screenshots/manual/05-governance.png +0 -0
  47. package/seeds/docs/assets/screenshots/manual/06-settings.png +0 -0
  48. package/seeds/docs/assets/screenshots/manual/07-new-deck.png +0 -0
  49. package/seeds/docs/assets/screenshots/manual/legend.json +12 -0
  50. package/seeds/docs/compat.md +134 -0
  51. package/seeds/docs/install.en.md +252 -0
  52. package/seeds/docs/install.md +304 -0
  53. package/seeds/docs/manual.en.md +294 -0
  54. package/seeds/docs/manual.md +258 -0
  55. package/seeds/docs/productization.en.md +212 -0
  56. package/seeds/docs/productization.md +193 -0
  57. package/seeds/docs/sample-deck.en.md +114 -0
  58. package/seeds/docs/sample-deck.md +104 -0
  59. package/seeds/docs/styles.en.md +112 -0
  60. package/seeds/docs/styles.md +100 -0
  61. package/seeds/resources/deckline-logo.png +0 -0
  62. package/seeds/resources/deckline-logo.svg +12 -0
  63. package/seeds/resources/deckline-mark.png +0 -0
  64. package/seeds/resources/index.json +19 -0
  65. package/seeds/samples/BIZ-01.jpg +0 -0
  66. package/seeds/samples/BIZ-02.jpg +0 -0
  67. package/seeds/samples/BIZ-03.jpg +0 -0
  68. package/seeds/samples/EDT-01.jpg +0 -0
  69. package/seeds/samples/EDT-02.jpg +0 -0
  70. package/seeds/samples/EDT-03.jpg +0 -0
  71. package/seeds/samples/FYI-01.jpg +0 -0
  72. package/seeds/samples/FYI-02.jpg +0 -0
  73. package/seeds/samples/FYI-03.jpg +0 -0
  74. package/seeds/samples/GRD-01.jpg +0 -0
  75. package/seeds/samples/GRD-02.jpg +0 -0
  76. package/seeds/samples/GRD-03.jpg +0 -0
  77. package/seeds/samples/JPN-01.jpg +0 -0
  78. package/seeds/samples/JPN-02.jpg +0 -0
  79. package/seeds/samples/JPN-03.jpg +0 -0
  80. package/seeds/samples/SKT-01.jpg +0 -0
  81. package/seeds/samples/SKT-02.jpg +0 -0
  82. package/seeds/samples/SKT-03.jpg +0 -0
  83. package/seeds/samples/SWS-01.jpg +0 -0
  84. package/seeds/samples/SWS-02.jpg +0 -0
  85. package/seeds/samples/SWS-03.jpg +0 -0
  86. package/seeds/samples/TEC-01.jpg +0 -0
  87. package/seeds/samples/TEC-02.jpg +0 -0
  88. package/seeds/samples/TEC-03.jpg +0 -0
  89. package/seeds/samples/deckline-intro/README.md +41 -0
  90. package/seeds/samples/deckline-intro/assets/deckline-logo.png +0 -0
  91. package/seeds/samples/deckline-intro/assets/deckline-logo.svg +12 -0
  92. package/seeds/samples/deckline-intro/assets/deckline-mark.png +0 -0
  93. package/seeds/samples/deckline-intro/deckline-intro.pptx +0 -0
  94. package/seeds/samples/deckline-intro/intent.md +48 -0
  95. package/seeds/samples/deckline-intro/materials/project-summary.md +90 -0
  96. package/seeds/samples/deckline-intro/materials/screenshots/01-workbench-deck.png +0 -0
  97. package/seeds/samples/deckline-intro/materials/screenshots/02-home-decks.png +0 -0
  98. package/seeds/samples/deckline-intro/materials/screenshots/03-home-designs.png +0 -0
  99. package/seeds/samples/deckline-intro/materials/screenshots/04-new-deck-popup.png +0 -0
  100. package/seeds/samples/deckline-intro/materials.md +110 -0
  101. package/seeds/samples/deckline-intro/pages/p01.png +0 -0
  102. package/seeds/samples/deckline-intro/pages/p02.png +0 -0
  103. package/seeds/samples/deckline-intro/pages/p03.png +0 -0
  104. package/seeds/samples/deckline-intro/pages/p04.png +0 -0
  105. package/seeds/samples/deckline-intro/pages/p05.png +0 -0
  106. package/seeds/samples/deckline-intro/pages/p06.png +0 -0
  107. package/seeds/samples/deckline-intro/pages/p07.png +0 -0
  108. package/seeds/samples/deckline-intro/pages/p08.png +0 -0
  109. package/seeds/samples/deckline-intro/pages/p09.png +0 -0
  110. package/seeds/samples/deckline-intro/pages/p10.png +0 -0
  111. package/seeds/samples/deckline-intro/popup.md +87 -0
  112. package/seeds/samples/deckline-intro/prompts/p01.txt +4 -0
  113. package/seeds/samples/deckline-intro/prompts/p02.txt +6 -0
  114. package/seeds/samples/deckline-intro/prompts/p03.txt +6 -0
  115. package/seeds/samples/deckline-intro/prompts/p04.txt +6 -0
  116. package/seeds/samples/deckline-intro/prompts/p05.txt +6 -0
  117. package/seeds/samples/deckline-intro/prompts/p06.txt +6 -0
  118. package/seeds/samples/deckline-intro/prompts/p07.txt +6 -0
  119. package/seeds/samples/deckline-intro/prompts/p08.txt +6 -0
  120. package/seeds/samples/deckline-intro/prompts/p09.txt +6 -0
  121. package/seeds/samples/deckline-intro/prompts/p10.txt +6 -0
  122. package/seeds/samples/deckline-intro/script.md +88 -0
  123. package/seeds/styles/BIZ.json +22 -0
  124. package/seeds/styles/EDT.json +22 -0
  125. package/seeds/styles/FYI.json +22 -0
  126. package/seeds/styles/GRD.json +22 -0
  127. package/seeds/styles/JPN.json +22 -0
  128. package/seeds/styles/SKT.json +22 -0
  129. package/seeds/styles/SWS.json +22 -0
  130. package/seeds/styles/TEC.json +22 -0
  131. package/ui/logo.svg +20 -0
  132. package/ui/workbench.css +687 -0
  133. package/ui/workbench.html +4554 -0
package/lib/config.js ADDED
@@ -0,0 +1,216 @@
1
+ /**
2
+ * 插件配置解析与路径解析。纯函数、无 IO——冒烟测试可直接喂假 env。
3
+ *
4
+ * 两个挂载点共用本文件(ADR-0006 D2):
5
+ * - profile 根挂载:`tools: false`,只开观测路由 + 客户端面板;
6
+ * - preset 子树挂载:`tools: true` + `decks[]`(受控源路径等)。
7
+ *
8
+ * 源路径纪律(ADR-0006 D5):客户源目录只从 config / env 来,绝不写死在工具逻辑里。
9
+ * @module dsh-plugin-deck-studio/config
10
+ */
11
+
12
+ import { existsSync } from "node:fs";
13
+ import { dirname, join, resolve } from "node:path";
14
+ import { DEFAULT_MAX_REVIEWERS, HARD_MAX_REVIEWERS } from "./review-personas.js";
15
+ import { fileURLToPath } from "node:url";
16
+
17
+ /** 插件包根目录(lib/ 的上一级——本文件住在 lib/ 里)。 */
18
+ export const PLUGIN_DIR = dirname(dirname(fileURLToPath(import.meta.url)));
19
+
20
+ /**
21
+ * 资产根:开发仓库根 `<repo>/plugin/deck-studio/` 的上一级。
22
+ *
23
+ * npm 安装时插件在 `<profile>/node_modules/dsh-plugin-deck-studio`,上两级是 node_modules,
24
+ * 那里没有 samples / docs;公开包把手册与示例 deck 一起打进包内 `seeds/`(prepack 生成),
25
+ * 这种情况回退到 `seeds/`,否则「使用手册」和首次启动的示例 deck 在 npm 安装下是空的。
26
+ */
27
+ export function repoRootFrom(pluginDir = PLUGIN_DIR) {
28
+ const repo = resolve(pluginDir, "..", "..");
29
+ const looksLikeRepo = existsSync(join(repo, "samples", "deckline-intro", "script.md")) || existsSync(join(repo, "docs", "manual.md"));
30
+ if (looksLikeRepo) return repo;
31
+ const bundled = join(pluginDir, "seeds");
32
+ if (existsSync(join(bundled, "docs", "manual.md"))) return bundled;
33
+ return repo;
34
+ }
35
+
36
+ /**
37
+ * 默认渲染配置。
38
+ * 出图节点地址**不设默认值**:provider 中立,节点只来自 profile patch / env / 本机 key 文件,
39
+ * 仓库里不携带任何厂商或个人中转地址(real 模式缺 endpoint 时 `lib/render.js` 会明确报错)。
40
+ * API key 永不出现在配置或仓库里,只从 env / key 文件读。
41
+ */
42
+ export const DEFAULT_RENDER = {
43
+ mode: "fake",
44
+ model: "gpt-image-2.5-sunburst",
45
+ endpoint: "",
46
+ size: "2560x1440",
47
+ apiKeyEnv: "DSH_DECK_IMAGE_API_KEY",
48
+ apiKeyFile: "",
49
+ // F-110:2560x1440 的整页图在慢网络下会超过 3 分钟(真机 p01 出图在 180s 被 abort)。
50
+ // 2560x1440 实测 219s 成功、慢的时候 300s 还没回来;给到 7 分钟,超时文案会提示重试。
51
+ timeoutMs: 420_000,
52
+ /** 多节点配置(ADR-0010):nodes 存在时优先;没有时回退到上面的单节点字段。 */
53
+ nodes: [],
54
+ /** 缺省选择:nodeId/modelId;留空用 nodes[0].models[0]。 */
55
+ default: "",
56
+ };
57
+
58
+ /** 默认视觉 preset(写进每页 prompt 头部;可在 profile 里用 styleBrief 覆盖)。 */
59
+ export const DEFAULT_STYLE_BRIEF =
60
+ "品牌信息图风格:深青 #06363B 主色、米白纸张底、暖金点缀,纸张纹理背景;" +
61
+ "信息密度高、克制、知识型;版式干净、层级清楚;不要卡通、不要 3D 渲染、不要照片素材。";
62
+
63
+ /** 展示名(status / 面板横幅用)。 */
64
+ export const DEFAULT_DISPLAY_NAME = "Deckline";
65
+
66
+ /**
67
+ * 展开一个 deck 源条目:config 里的 path 支持 `~`;缺 id 拒绝。
68
+ * @param {unknown} raw - config.decks[] 的一项。
69
+ * @param {NodeJS.ProcessEnv} env - 展开 `~` 与 env 覆盖用。
70
+ */
71
+ function normalizeDeck(raw, env) {
72
+ if (!raw || typeof raw !== "object") return null;
73
+ const sourceRoot = typeof raw.sourceRoot === "string" ? expandHome(raw.sourceRoot, env) : "";
74
+ if (!sourceRoot) return null;
75
+ const id = typeof raw.id === "string" && raw.id.trim() ? raw.id.trim() : "";
76
+ if (!id || !/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(id)) return null;
77
+ return {
78
+ id,
79
+ label: typeof raw.label === "string" && raw.label.trim() ? raw.label.trim() : id,
80
+ sourceRoot,
81
+ };
82
+ }
83
+
84
+ /** 展开 `~/` 前缀;其余原样。 */
85
+ /** 默认数据根:安装用户的可写位置(不落进插件包 / node_modules)。开发仓库可在 profile 里显式配 dataRoot。 */
86
+ export function defaultDataRoot(env = process.env) {
87
+ return join(expandHome(env.DSH_HOME || "~/.dsh", env), "deckline");
88
+ }
89
+
90
+ export function expandHome(value, env = process.env) {
91
+ if (typeof value !== "string") return "";
92
+ if (value === "~") return env.HOME ?? value;
93
+ if (value.startsWith("~/")) return join(env.HOME ?? "", value.slice(2));
94
+ return value;
95
+ }
96
+
97
+ /**
98
+ * 合并 loader config ← 默认值 ← env 覆盖。
99
+ * @param {Record<string, unknown>|undefined} raw - apply(ctx, config) 的 config。
100
+ * @param {{pluginDir?: string, env?: NodeJS.ProcessEnv}} [options]
101
+ */
102
+ export function mergeConfig(raw, options = {}) {
103
+ const env = options.env ?? process.env;
104
+ const pluginDir = options.pluginDir ?? PLUGIN_DIR;
105
+ const input = raw && typeof raw === "object" ? raw : {};
106
+ const positive = (value, fallback) =>
107
+ Number.isFinite(Number(value)) && Number(value) > 0 ? Number(value) : fallback;
108
+
109
+ const renderInput = input.render && typeof input.render === "object" ? input.render : {};
110
+ const mode = renderInput.mode === "real" || env.DSH_DECK_RENDER_MODE === "real" ? "real" : "fake";
111
+ const render = {
112
+ ...DEFAULT_RENDER,
113
+ ...renderInput,
114
+ mode,
115
+ // 配置优先;env 别名与 docs/install.md 一致(DSH_DECK_IMAGE_*),保留 GPT_IMAGE_* 兼容旧用法。
116
+ model:
117
+ renderInput.model || env.DSH_DECK_IMAGE_MODEL || env.GPT_IMAGE_MODEL || DEFAULT_RENDER.model,
118
+ size: renderInput.size || env.DSH_DECK_IMAGE_SIZE || env.GPT_IMAGE_SIZE || DEFAULT_RENDER.size,
119
+ endpoint: renderInput.endpoint || env.DSH_DECK_IMAGE_ENDPOINT || DEFAULT_RENDER.endpoint,
120
+ apiKeyFile: expandHome(
121
+ renderInput.apiKeyFile || env.DSH_DECK_IMAGE_API_KEY_FILE || DEFAULT_RENDER.apiKeyFile,
122
+ env,
123
+ ),
124
+ timeoutMs: positive(renderInput.timeoutMs, DEFAULT_RENDER.timeoutMs),
125
+ };
126
+
127
+ const decksRaw = Array.isArray(input.decks) ? input.decks : [];
128
+ const decks = decksRaw.map((deck) => normalizeDeck(deck, env)).filter(Boolean);
129
+
130
+ const singleSource = expandHome(
131
+ typeof input.sourceRoot === "string" ? input.sourceRoot : (env.DSH_DECK_SOURCE_ROOT ?? ""),
132
+ env,
133
+ );
134
+ if (singleSource && !decks.some((deck) => deck.sourceRoot === singleSource)) {
135
+ decks.unshift({
136
+ id: typeof input.sourceDeckId === "string" && input.sourceDeckId.trim() ? input.sourceDeckId.trim() : "sample-deck",
137
+ label: typeof input.sourceDeckLabel === "string" && input.sourceDeckLabel.trim() ? input.sourceDeckLabel.trim() : "示例受控源",
138
+ sourceRoot: singleSource,
139
+ });
140
+ }
141
+
142
+ return {
143
+ /** 只在 preset 子树挂载点为 true;根挂载只开路由与面板。 */
144
+ tools: input.tools === true,
145
+ /** preset 子树挂载点设 false(根挂载已注册同路径路由,避免双重注册)。 */
146
+ routes: input.routes !== false,
147
+ dataRoot:
148
+ expandHome(
149
+ typeof input.dataRoot === "string" && input.dataRoot
150
+ ? input.dataRoot
151
+ : (env.DSH_DECK_DATA_ROOT ?? ""),
152
+ env,
153
+ ) || defaultDataRoot(env),
154
+ /**
155
+ * 新建 deck 项目的落盘根(2026-10-21 负责人:以后写的 deck 都放
156
+ * `00_company/sharing/`)。留空 = 旧的内部 `.deck/projects/<id>/source`。
157
+ * 配置了这个根时,新项目直接建在 `<projectsRoot>/<目录名>/`
158
+ * (script.md / intent.md / style-reference.md / prompts/ 同层,和已有 deck 源结构一致)。
159
+ */
160
+ projectsRoot: expandHome(
161
+ typeof input.projectsRoot === "string" && input.projectsRoot.trim()
162
+ ? input.projectsRoot.trim()
163
+ : (env.DSH_DECK_PROJECTS_ROOT ?? ""),
164
+ env,
165
+ ),
166
+ decks,
167
+ render,
168
+ styleBrief:
169
+ typeof input.styleBrief === "string" && input.styleBrief.trim()
170
+ ? input.styleBrief.trim()
171
+ : DEFAULT_STYLE_BRIEF,
172
+ displayName:
173
+ typeof input.displayName === "string" && input.displayName.trim()
174
+ ? input.displayName.trim()
175
+ : DEFAULT_DISPLAY_NAME,
176
+ /**
177
+ * 是否把 DSH 外壳左侧栏默认收起隐藏(F-69)。
178
+ * 默认 false:只有专门的产品 profile(如 deck-studio)显式打开才生效,
179
+ * 开发/其他 profile 的 DSH 界面不受影响。
180
+ */
181
+ hideShellSidebar: input.hideShellSidebar === true,
182
+ /**
183
+ * 新建 Deck 弹窗默认预选的登记风格(F-112):填 design 缩写(如 GRD)。
184
+ * 留空 = 不预选。只在 UI 预选,不改变任何已有 deck 的关联风格。
185
+ */
186
+ defaultStyleAbbr:
187
+ typeof input.defaultStyleAbbr === "string" && input.defaultStyleAbbr.trim()
188
+ ? input.defaultStyleAbbr.trim().toUpperCase()
189
+ : "",
190
+ /**
191
+ * 首次启动自举(F-157):新装 profile 自动登记示例 deck、拷一份资源库、导入预设风格。
192
+ * 默认 false(通用插件不强加内容);产品 bundle patch 里打开,关掉填 false。
193
+ */
194
+ bootstrap: input.bootstrap === true,
195
+ /**
196
+ * 听众评审(F-159):审稿人由 intent 的「听众」动态生成,不是写死的固定 role。
197
+ * presetId 指向只读评审 preset(cordis.patch.yml 的 preset-deck-critic);
198
+ * maxReviewers 是成本上限(每个审稿人 = 一次子 agent 调用),默认 3。
199
+ */
200
+ review: {
201
+ presetId:
202
+ typeof input.review?.presetId === "string" && input.review.presetId.trim()
203
+ ? input.review.presetId.trim()
204
+ : "deck-critic",
205
+ maxReviewers: Math.min(
206
+ HARD_MAX_REVIEWERS,
207
+ positive(input.review?.maxReviewers, DEFAULT_MAX_REVIEWERS),
208
+ ),
209
+ },
210
+ /** 首页问候语里的用户名(默认空 = 只显示「欢迎你」;可在 profile 配置里改)。 */
211
+ userName:
212
+ typeof input.userName === "string" && input.userName.trim()
213
+ ? input.userName.trim()
214
+ : "",
215
+ };
216
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * deck 内容密度(F-124):一页写多少字、画面要求写多细。
3
+ *
4
+ * 背景:同一份 deck,有人要「一页 300 字、图里信息密集」的自阅稿,有人要「一页 80 字、
5
+ * 一页一件事」的演讲稿。密度是 deck 级设置(manifest.contentDensity),落在:
6
+ * - 新建 Deck 弹窗 / deck 工具栏「密度」下拉(用户改);
7
+ * - deck_source_overview、deck_script_read(Agent 每次写 script 前读到目标字数);
8
+ * - persona 第 23 条(按设置写,auto 时自己判断并说明理由)。
9
+ *
10
+ * 数字口径(负责人 2026-10-23 定):
11
+ * - 信息密集 dense:正文 200–500 字/页,画面要求 800–900 字
12
+ * - 适中 medium:正文 100–200 字/页,画面要求 400–600 字
13
+ * - 简洁 concise:正文 50–100 字/页,画面要求 200–300 字
14
+ * - 自动 auto:**不设固定字数**——由 Agent 按这一页/这份 deck 的内容自己判断每一页该多长
15
+ * (该密则密、该简则简),但要说明判断理由;不是「套一个 100–200 的区间」
16
+ *
17
+ * 注意:正文长 ≠ 画面字多。画面里的字仍由风格与视觉规则约束(信息密集也不许堆小字)。
18
+ */
19
+
20
+ export const CONTENT_DENSITY_KEYS = ["auto", "concise", "medium", "dense"];
21
+
22
+ export const CONTENT_DENSITIES = {
23
+ auto: {
24
+ key: "auto",
25
+ label: "自动",
26
+ hint: "不设固定字数:由 Agent 按每页内容自己判断该多长,并说明理由",
27
+ bodyMin: null,
28
+ bodyMax: null,
29
+ briefMin: null,
30
+ briefMax: null,
31
+ },
32
+ concise: {
33
+ key: "concise",
34
+ label: "简洁",
35
+ hint: "一页一件事,标题即结论,正文只留支撑句",
36
+ bodyMin: 50,
37
+ bodyMax: 100,
38
+ briefMin: 200,
39
+ briefMax: 300,
40
+ },
41
+ medium: {
42
+ key: "medium",
43
+ label: "适中",
44
+ hint: "结论 + 2–3 句支撑 + 一句收口",
45
+ bodyMin: 100,
46
+ bodyMax: 200,
47
+ briefMin: 400,
48
+ briefMax: 600,
49
+ },
50
+ dense: {
51
+ key: "dense",
52
+ label: "信息密集",
53
+ hint: "自阅型:一页给足论据 / 数据 / 步骤,读者不用问讲者",
54
+ bodyMin: 200,
55
+ bodyMax: 500,
56
+ briefMin: 800,
57
+ briefMax: 900,
58
+ },
59
+ };
60
+
61
+ /** 归一化:未知值一律当 auto(老 deck 没有这个字段)。 */
62
+ export function normalizeContentDensity(value) {
63
+ const key = String(value ?? "").trim().toLowerCase();
64
+ return CONTENT_DENSITY_KEYS.includes(key) ? key : "auto";
65
+ }
66
+
67
+ export function densitySpec(value) {
68
+ return CONTENT_DENSITIES[normalizeContentDensity(value)];
69
+ }
70
+
71
+ /** UI 下拉用的选项(含目标字数,便于用户看懂差别;auto 不写死区间)。 */
72
+ export function contentDensityOptions() {
73
+ return CONTENT_DENSITY_KEYS.map((key) => {
74
+ const spec = CONTENT_DENSITIES[key];
75
+ const fixed = spec.bodyMin != null;
76
+ return {
77
+ key,
78
+ label: spec.label,
79
+ body: fixed ? `${spec.bodyMin}–${spec.bodyMax} 字/页` : "每页由 Agent 判断(不固定)",
80
+ brief: fixed ? `${spec.briefMin}–${spec.briefMax} 字` : "每页由 Agent 判断(不固定)",
81
+ // 数值口径也带上:工作台要按它逐页标「字数是否达标」,不能只靠字符串解析
82
+ bodyMin: spec.bodyMin,
83
+ bodyMax: spec.bodyMax,
84
+ briefMin: spec.briefMin,
85
+ briefMax: spec.briefMax,
86
+ hint: spec.hint,
87
+ };
88
+ });
89
+ }
90
+
91
+ /** 给 Agent 看的一行(工具结果 / 脚本页都用它,保证口径一致)。 */
92
+ export function densityLine(value) {
93
+ const spec = densitySpec(value);
94
+ if (spec.key === "auto") {
95
+ return "内容密度:自动(不设固定字数——按这一页的内容自己判断该多长、画面要求该多细,该密则密、该简则简;判断完告诉用户理由,不要套固定区间)";
96
+ }
97
+ return `内容密度:${spec.label}(正文 ${spec.bodyMin}–${spec.bodyMax} 字/页、画面要求 ${spec.briefMin}–${spec.briefMax} 字;${spec.hint})`;
98
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * 读 deck 的「写作上下文」:script.md 头部(title / owner / created)+ intent.md 的结构化字段
3
+ * (听众 / 带走 / 成功标准 / 不讲的)。给两处用(F-158):
4
+ * - deck_script_read / 提案结果:让 Agent 写正文前看到「讲给谁、为什么讲」;
5
+ * - buildPrompt:出图 prompt 带上受众与目的,封面页带作者 / 日期。
6
+ *
7
+ * 只读源文件,纯解析;找不到文件时返回空字段,不抛错。
8
+ * @module dsh-plugin-deck-studio/deck-brief
9
+ */
10
+ import { existsSync, readFileSync } from "node:fs";
11
+ import { join } from "node:path";
12
+
13
+ const clean = (value) => String(value ?? "").replace(/^(待补[^)]*)$/, "").trim();
14
+
15
+ /** 读 `---` front matter(只取顶层 `key: value`)。 */
16
+ export function readFrontMatter(text) {
17
+ const out = {};
18
+ const lines = String(text ?? "").split("\n");
19
+ if (lines[0]?.trim() !== "---") return out;
20
+ for (let index = 1; index < lines.length; index += 1) {
21
+ const line = lines[index];
22
+ if (line.trim() === "---") break;
23
+ const match = line.match(/^([A-Za-z0-9_-]+)\s*:\s*(.*)$/);
24
+ if (match) out[match[1]] = match[2].trim().replace(/^["']|["']$/g, "");
25
+ }
26
+ return out;
27
+ }
28
+
29
+ /** 从 intent.md 的 `- **字段**:值` 行取值。 */
30
+ export function readIntentFields(text) {
31
+ const out = {};
32
+ for (const line of String(text ?? "").split("\n")) {
33
+ const match = line.match(/^\s*[-*]\s*\*\*([^*]+)\*\*\s*[::]\s*(.*)$/);
34
+ if (match) out[match[1].trim()] = clean(match[2]);
35
+ }
36
+ return out;
37
+ }
38
+
39
+ /**
40
+ * @param {string} sourceRoot deck 源根
41
+ * @returns {{title:string, owner:string, created:string, audience:string, topic:string, takeaway:string, success:string, scopeOut:string, rules:string}}
42
+ */
43
+ export function readDeckBrief(sourceRoot) {
44
+ const empty = { title: "", owner: "", created: "", audience: "", topic: "", takeaway: "", success: "", scopeOut: "", rules: "" };
45
+ const root = String(sourceRoot ?? "").trim();
46
+ if (!root) return empty;
47
+ let scriptHead = {};
48
+ try {
49
+ if (existsSync(join(root, "script.md"))) scriptHead = readFrontMatter(readFileSync(join(root, "script.md"), "utf8"));
50
+ } catch {
51
+ scriptHead = {};
52
+ }
53
+ let intent = {};
54
+ try {
55
+ if (existsSync(join(root, "intent.md"))) intent = readIntentFields(readFileSync(join(root, "intent.md"), "utf8"));
56
+ } catch {
57
+ intent = {};
58
+ }
59
+ return {
60
+ title: clean(scriptHead.title || intent["主题"]),
61
+ owner: clean(scriptHead.owner),
62
+ created: clean(scriptHead.created),
63
+ audience: clean(intent["听众"]),
64
+ topic: clean(intent["主题"]),
65
+ takeaway: clean(intent["带走"]),
66
+ success: clean(intent["成功标准"]),
67
+ scopeOut: clean(intent["不讲的"]),
68
+ rules: "",
69
+ };
70
+ }
71
+
72
+ /** 出图 prompt 用的上下文行:受众 / 目的(一句话)。没有就返回空数组。 */
73
+ export function briefPromptLines(brief = {}) {
74
+ const lines = [];
75
+ if (brief.audience) lines.push(`受众:${brief.audience}(术语密度、例子、取舍按这个受众来)`);
76
+ if (brief.takeaway || brief.success) lines.push(`这页服务的目的:${brief.takeaway || brief.success}`);
77
+ return lines;
78
+ }
79
+
80
+ /** 封面页用的作者 / 日期行。日期只取 YYYY-MM-DD。 */
81
+ export function coverPromptLines(brief = {}) {
82
+ const lines = [];
83
+ if (brief.owner) lines.push(`作者:${brief.owner}`);
84
+ const date = String(brief.created ?? "").match(/^\d{4}-\d{2}-\d{2}/)?.[0];
85
+ if (date) lines.push(`日期:${date}`);
86
+ return lines;
87
+ }