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.
- package/LICENSE +21 -0
- package/README.md +47 -2
- package/client.js +500 -0
- package/cordis.example.yml +163 -0
- package/cordis.patch.yml +147 -0
- package/index.js +2340 -0
- package/lib/config.js +216 -0
- package/lib/content-density.js +98 -0
- package/lib/deck-brief.js +87 -0
- package/lib/deck-docs.js +1095 -0
- package/lib/deck-rules.js +124 -0
- package/lib/deck-templates.js +149 -0
- package/lib/deck-workspace.js +480 -0
- package/lib/design-style.js +157 -0
- package/lib/designs.js +175 -0
- package/lib/diff.js +88 -0
- package/lib/exporter.js +336 -0
- package/lib/faq.js +300 -0
- package/lib/git-commit.js +87 -0
- package/lib/home-catalog.js +249 -0
- package/lib/png.js +275 -0
- package/lib/proposal-decision.js +204 -0
- package/lib/render-nodes.js +171 -0
- package/lib/render-settings.js +213 -0
- package/lib/render.js +287 -0
- package/lib/review-personas.js +258 -0
- package/lib/script-style.js +55 -0
- package/lib/script-writer.js +165 -0
- package/lib/source-adapter.js +327 -0
- package/lib/store.js +212 -0
- package/lib/tools.js +2594 -0
- package/lib/watermark.js +137 -0
- package/package.json +67 -4
- package/seeds/docs/assets/screenshots/00-hero.png +0 -0
- package/seeds/docs/assets/screenshots/01-home.png +0 -0
- package/seeds/docs/assets/screenshots/02-deck-pages.png +0 -0
- package/seeds/docs/assets/screenshots/03-script.png +0 -0
- package/seeds/docs/assets/screenshots/04-design.png +0 -0
- package/seeds/docs/assets/screenshots/05-governance.png +0 -0
- package/seeds/docs/assets/screenshots/06-settings.png +0 -0
- package/seeds/docs/assets/screenshots/07-new-deck.png +0 -0
- package/seeds/docs/assets/screenshots/manual/01-home.png +0 -0
- package/seeds/docs/assets/screenshots/manual/02-deck-pages.png +0 -0
- package/seeds/docs/assets/screenshots/manual/03-script.png +0 -0
- package/seeds/docs/assets/screenshots/manual/04-design.png +0 -0
- package/seeds/docs/assets/screenshots/manual/05-governance.png +0 -0
- package/seeds/docs/assets/screenshots/manual/06-settings.png +0 -0
- package/seeds/docs/assets/screenshots/manual/07-new-deck.png +0 -0
- package/seeds/docs/assets/screenshots/manual/legend.json +12 -0
- package/seeds/docs/compat.md +134 -0
- package/seeds/docs/install.en.md +252 -0
- package/seeds/docs/install.md +304 -0
- package/seeds/docs/manual.en.md +294 -0
- package/seeds/docs/manual.md +258 -0
- package/seeds/docs/productization.en.md +212 -0
- package/seeds/docs/productization.md +193 -0
- package/seeds/docs/sample-deck.en.md +114 -0
- package/seeds/docs/sample-deck.md +104 -0
- package/seeds/docs/styles.en.md +112 -0
- package/seeds/docs/styles.md +100 -0
- package/seeds/resources/deckline-logo.png +0 -0
- package/seeds/resources/deckline-logo.svg +12 -0
- package/seeds/resources/deckline-mark.png +0 -0
- package/seeds/resources/index.json +19 -0
- package/seeds/samples/BIZ-01.jpg +0 -0
- package/seeds/samples/BIZ-02.jpg +0 -0
- package/seeds/samples/BIZ-03.jpg +0 -0
- package/seeds/samples/EDT-01.jpg +0 -0
- package/seeds/samples/EDT-02.jpg +0 -0
- package/seeds/samples/EDT-03.jpg +0 -0
- package/seeds/samples/FYI-01.jpg +0 -0
- package/seeds/samples/FYI-02.jpg +0 -0
- package/seeds/samples/FYI-03.jpg +0 -0
- package/seeds/samples/GRD-01.jpg +0 -0
- package/seeds/samples/GRD-02.jpg +0 -0
- package/seeds/samples/GRD-03.jpg +0 -0
- package/seeds/samples/JPN-01.jpg +0 -0
- package/seeds/samples/JPN-02.jpg +0 -0
- package/seeds/samples/JPN-03.jpg +0 -0
- package/seeds/samples/SKT-01.jpg +0 -0
- package/seeds/samples/SKT-02.jpg +0 -0
- package/seeds/samples/SKT-03.jpg +0 -0
- package/seeds/samples/SWS-01.jpg +0 -0
- package/seeds/samples/SWS-02.jpg +0 -0
- package/seeds/samples/SWS-03.jpg +0 -0
- package/seeds/samples/TEC-01.jpg +0 -0
- package/seeds/samples/TEC-02.jpg +0 -0
- package/seeds/samples/TEC-03.jpg +0 -0
- package/seeds/samples/deckline-intro/README.md +41 -0
- package/seeds/samples/deckline-intro/assets/deckline-logo.png +0 -0
- package/seeds/samples/deckline-intro/assets/deckline-logo.svg +12 -0
- package/seeds/samples/deckline-intro/assets/deckline-mark.png +0 -0
- package/seeds/samples/deckline-intro/deckline-intro.pptx +0 -0
- package/seeds/samples/deckline-intro/intent.md +48 -0
- package/seeds/samples/deckline-intro/materials/project-summary.md +90 -0
- package/seeds/samples/deckline-intro/materials/screenshots/01-workbench-deck.png +0 -0
- package/seeds/samples/deckline-intro/materials/screenshots/02-home-decks.png +0 -0
- package/seeds/samples/deckline-intro/materials/screenshots/03-home-designs.png +0 -0
- package/seeds/samples/deckline-intro/materials/screenshots/04-new-deck-popup.png +0 -0
- package/seeds/samples/deckline-intro/materials.md +110 -0
- package/seeds/samples/deckline-intro/pages/p01.png +0 -0
- package/seeds/samples/deckline-intro/pages/p02.png +0 -0
- package/seeds/samples/deckline-intro/pages/p03.png +0 -0
- package/seeds/samples/deckline-intro/pages/p04.png +0 -0
- package/seeds/samples/deckline-intro/pages/p05.png +0 -0
- package/seeds/samples/deckline-intro/pages/p06.png +0 -0
- package/seeds/samples/deckline-intro/pages/p07.png +0 -0
- package/seeds/samples/deckline-intro/pages/p08.png +0 -0
- package/seeds/samples/deckline-intro/pages/p09.png +0 -0
- package/seeds/samples/deckline-intro/pages/p10.png +0 -0
- package/seeds/samples/deckline-intro/popup.md +87 -0
- package/seeds/samples/deckline-intro/prompts/p01.txt +4 -0
- package/seeds/samples/deckline-intro/prompts/p02.txt +6 -0
- package/seeds/samples/deckline-intro/prompts/p03.txt +6 -0
- package/seeds/samples/deckline-intro/prompts/p04.txt +6 -0
- package/seeds/samples/deckline-intro/prompts/p05.txt +6 -0
- package/seeds/samples/deckline-intro/prompts/p06.txt +6 -0
- package/seeds/samples/deckline-intro/prompts/p07.txt +6 -0
- package/seeds/samples/deckline-intro/prompts/p08.txt +6 -0
- package/seeds/samples/deckline-intro/prompts/p09.txt +6 -0
- package/seeds/samples/deckline-intro/prompts/p10.txt +6 -0
- package/seeds/samples/deckline-intro/script.md +88 -0
- package/seeds/styles/BIZ.json +22 -0
- package/seeds/styles/EDT.json +22 -0
- package/seeds/styles/FYI.json +22 -0
- package/seeds/styles/GRD.json +22 -0
- package/seeds/styles/JPN.json +22 -0
- package/seeds/styles/SKT.json +22 -0
- package/seeds/styles/SWS.json +22 -0
- package/seeds/styles/TEC.json +22 -0
- package/ui/logo.svg +20 -0
- package/ui/workbench.css +687 -0
- package/ui/workbench.html +4554 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 本地受控源 adapter(DeckSourceAdapter 最小实现)。
|
|
3
|
+
*
|
|
4
|
+
* 只读:script.md(frontmatter + 逐页表格)、build/deck.yaml(页序 manifest)、
|
|
5
|
+
* CURRENT.md(做到哪)、feedback.md(反馈行)。客户正文只进会话与 `.deck/`,
|
|
6
|
+
* 永不回写源目录、永不复制进 Git。
|
|
7
|
+
*
|
|
8
|
+
* 表格解析按「第一列 = 页号,后接标题 / 回答 / 主张 / 来源」的既有 deck 惯例;
|
|
9
|
+
* 缺单元格补空串,行结构不齐不炸——读不到就如实返回空 + 警告。
|
|
10
|
+
* @module dsh-plugin-deck-studio/source-adapter
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
|
|
16
|
+
/** 读文本文件;不存在返回 null。 */
|
|
17
|
+
function readText(file) {
|
|
18
|
+
try {
|
|
19
|
+
return existsSync(file) ? readFileSync(file, "utf8") : null;
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 解析 frontmatter(`---` 围栏,顶层 `key: value`;嵌套结构忽略)。 */
|
|
26
|
+
export function parseFrontmatter(text) {
|
|
27
|
+
if (typeof text !== "string" || !text.startsWith("---")) return {};
|
|
28
|
+
const end = text.indexOf("\n---", 3);
|
|
29
|
+
if (end < 0) return {};
|
|
30
|
+
const meta = {};
|
|
31
|
+
for (const line of text.slice(4, end).split("\n")) {
|
|
32
|
+
const match = /^([A-Za-z0-9_-]+)\s*:\s*(.*)$/.exec(line);
|
|
33
|
+
if (match && !line.startsWith(" ") && !line.startsWith("-")) meta[match[1]] = match[2].trim();
|
|
34
|
+
}
|
|
35
|
+
return meta;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 把一行 markdown 表格行拆成单元格(去首尾 `|`、去分隔行)。 */
|
|
39
|
+
function splitRow(line) {
|
|
40
|
+
return line.replace(/^\|/, "").replace(/\|\s*$/, "").split("|").map((cell) => cell.trim());
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isSeparator(line) {
|
|
44
|
+
return /^\|[\s:|-]+\|?\s*$/.test(line);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 抓取所有包含指定表头关键词的 markdown 表:返回行数组的数组。 */
|
|
48
|
+
function findTables(text, headerKeywords) {
|
|
49
|
+
if (!text) return [];
|
|
50
|
+
const lines = text.split("\n");
|
|
51
|
+
const tables = [];
|
|
52
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
53
|
+
const line = lines[i];
|
|
54
|
+
if (!line.startsWith("|") || isSeparator(line)) continue;
|
|
55
|
+
const head = splitRow(line).join(" ");
|
|
56
|
+
if (!headerKeywords.every((keyword) => head.includes(keyword))) continue;
|
|
57
|
+
const rows = [];
|
|
58
|
+
for (let j = i + 1; j < lines.length; j += 1) {
|
|
59
|
+
const row = lines[j];
|
|
60
|
+
if (!row.startsWith("|")) break;
|
|
61
|
+
if (isSeparator(row)) continue;
|
|
62
|
+
rows.push(splitRow(row));
|
|
63
|
+
}
|
|
64
|
+
tables.push(rows);
|
|
65
|
+
i += 1 + rows.length; // 跳过已消费的表体(分隔行 + 行)
|
|
66
|
+
}
|
|
67
|
+
return tables;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** 第一张匹配表的行(CURRENT / feedback 用)。 */
|
|
71
|
+
function findTable(text, headerKeywords) {
|
|
72
|
+
return findTables(text, headerKeywords)[0] ?? [];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 从 script.md 抽逐页行:`| N | 标题 | 这页回答… | 主张 | 来源 |`。
|
|
77
|
+
* 正文与附录可能各有一张同构表——全部收集;页号跨表应唯一,重复时保序去重。
|
|
78
|
+
*/
|
|
79
|
+
export function parseStoryPages(scriptText) {
|
|
80
|
+
const pages = [];
|
|
81
|
+
const seen = new Set();
|
|
82
|
+
for (const table of findTables(scriptText, ["页", "标题"])) {
|
|
83
|
+
for (const cells of table) {
|
|
84
|
+
const page = Number.parseInt(String(cells[0] ?? "").replace(/\D/g, ""), 10);
|
|
85
|
+
if (!Number.isFinite(page) || page <= 0 || seen.has(page)) continue;
|
|
86
|
+
seen.add(page);
|
|
87
|
+
pages.push({
|
|
88
|
+
page,
|
|
89
|
+
title: cells[1] ?? "",
|
|
90
|
+
question: cells[2] ?? "",
|
|
91
|
+
claim: cells[3] ?? "",
|
|
92
|
+
content: cells[2] ?? "",
|
|
93
|
+
source: cells[4] ?? "",
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return pages;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 解析「按页分节」的 script(另一种既有 deck 惯例):
|
|
102
|
+
*
|
|
103
|
+
* ### P04 · 案例闪卡 01:商业运营 / 数据分析
|
|
104
|
+
* **标题**:从"拼报告",到"可复用的洞察链路"
|
|
105
|
+
* **页面标签**:案例闪卡 01|商业运营 / 数据分析团队
|
|
106
|
+
*
|
|
107
|
+
* 没有 `|页|标题|` 表格时用它。字段按 `**字段**:值` 收集;列表内容留在 body。
|
|
108
|
+
* 页号来自 `### P<digits>`,标题缺字段时退回分节标题。
|
|
109
|
+
*/
|
|
110
|
+
export function parseSectionedPages(scriptText) {
|
|
111
|
+
if (!scriptText) return [];
|
|
112
|
+
const pageHead = /^#{2,3}\s*P(\d+)\s*[·.\-–—:]?\s*(.*)$/;
|
|
113
|
+
const fieldLine = /^\*\*([^*]+)\*\*\s*[::]\s*(.*)$/;
|
|
114
|
+
const pages = [];
|
|
115
|
+
let current = null;
|
|
116
|
+
const flush = () => {
|
|
117
|
+
if (!current) return;
|
|
118
|
+
const fields = current.fields;
|
|
119
|
+
// 没写「副标题 / 页面标签」的页(总纲、能力线等),拿正文第一段当「这页回答」,
|
|
120
|
+
// 跳过 `**目标一|…**` 这种小标题行和表格行,避免空字段直接漏到卡片上。
|
|
121
|
+
const excerpt = current.body
|
|
122
|
+
.map((line) => line.trim())
|
|
123
|
+
.filter((line) => line && !/^\*\*[^*]+\*\*$/.test(line) && !line.startsWith("|") && !/^-{3,}$/.test(line))
|
|
124
|
+
.map((line) => line.replace(/^[-*+]\s+/, "").replace(/^>\s?/, "").replace(/\*\*/g, "").trim())
|
|
125
|
+
.find((line) => line.length >= 6) ?? "";
|
|
126
|
+
pages.push({
|
|
127
|
+
page: current.page,
|
|
128
|
+
section: current.section,
|
|
129
|
+
fields,
|
|
130
|
+
title: fields["标题"] || current.section || "",
|
|
131
|
+
question: fields["副标题"] || fields["页面标签"] || excerpt.slice(0, 110),
|
|
132
|
+
claim: fields["主张"] || fields["为什么值得 AI 做"] || fields["为什么值得 Agent 做"] || fields["讲述意图"] || fields["画面说明"] || "",
|
|
133
|
+
// 整页正文(标题行以下全部原文):storyline / 脚本标签要能完整展示,不能只剩一句摘录。
|
|
134
|
+
content: current.body.join("\n").replace(/\n{3,}/g, "\n\n").trim(),
|
|
135
|
+
source: "script.md",
|
|
136
|
+
});
|
|
137
|
+
current = null;
|
|
138
|
+
};
|
|
139
|
+
for (const line of scriptText.split("\n")) {
|
|
140
|
+
const head = pageHead.exec(line.trim());
|
|
141
|
+
if (head) {
|
|
142
|
+
flush();
|
|
143
|
+
current = { page: Number(head[1]), section: head[2].trim(), fields: {}, body: [] };
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (!current) continue;
|
|
147
|
+
if (/^#{1,4}\s/.test(line)) {
|
|
148
|
+
flush();
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const field = fieldLine.exec(line.trim());
|
|
152
|
+
if (field) {
|
|
153
|
+
current.fields[field[1].trim()] = field[2].trim();
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
current.body.push(line);
|
|
157
|
+
}
|
|
158
|
+
flush();
|
|
159
|
+
return pages.filter((page) => page.page > 0);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* 解析附录表(`| 页 | 内容 | 来源 |`,页列可能是 `A1` 或 `A3–A10` 区间)。
|
|
164
|
+
* 区间按原样保留,不展开成单页——附录页在源里就是区间登记。
|
|
165
|
+
*/
|
|
166
|
+
export function parseAppendixRows(scriptText) {
|
|
167
|
+
const rows = [];
|
|
168
|
+
for (const table of findTables(scriptText, ["页", "内容"])) {
|
|
169
|
+
for (const cells of table) {
|
|
170
|
+
const id = String(cells[0] ?? "").trim();
|
|
171
|
+
if (!id || /^[0-9]+$/.test(id)) continue; // 正文页号列走 parseStoryPages
|
|
172
|
+
rows.push({ id, content: cells[1] ?? "", source: cells[2] ?? "" });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return rows;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** 展开附录 id(`A1` / `A3–A10`)为页数,用于与 deck.yaml 对账。 */
|
|
179
|
+
export function appendixRowCount(rows) {
|
|
180
|
+
let count = 0;
|
|
181
|
+
for (const row of rows) {
|
|
182
|
+
const range = /^A(\d+)\s*[–-]\s*A(\d+)$/.exec(row.id);
|
|
183
|
+
if (range) count += Math.abs(Number(range[2]) - Number(range[1])) + 1;
|
|
184
|
+
else count += 1;
|
|
185
|
+
}
|
|
186
|
+
return count;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** 解析 build/deck.yaml:顶层 title + `- custom: <path>` 页序。 */
|
|
190
|
+
export function parseDeckYaml(yamlText) {
|
|
191
|
+
if (!yamlText) return { title: "", pages: [] };
|
|
192
|
+
const titleMatch = /^title:\s*(.+)$/m.exec(yamlText);
|
|
193
|
+
const pages = [];
|
|
194
|
+
for (const match of yamlText.matchAll(/^\s*-\s*(?:custom|html|page)\s*:\s*(\S+)\s*$/gm)) {
|
|
195
|
+
pages.push(match[1]);
|
|
196
|
+
}
|
|
197
|
+
return { title: titleMatch ? titleMatch[1].trim().replace(/^["']|["']$/g, "") : "", pages };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* 创建一个只读 deck 源 adapter。
|
|
202
|
+
* @param {{id: string, label: string, sourceRoot: string}} deck - config.decks 的一项。
|
|
203
|
+
*/
|
|
204
|
+
export function createLocalSourceAdapter(deck) {
|
|
205
|
+
const { id, label, sourceRoot } = deck;
|
|
206
|
+
|
|
207
|
+
function scriptPath() {
|
|
208
|
+
return join(sourceRoot, "script.md");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
get id() {
|
|
213
|
+
return id;
|
|
214
|
+
},
|
|
215
|
+
get label() {
|
|
216
|
+
return label;
|
|
217
|
+
},
|
|
218
|
+
get sourceRoot() {
|
|
219
|
+
return sourceRoot;
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
/** 源是否可读(script.md 存在)。 */
|
|
223
|
+
available() {
|
|
224
|
+
return existsSync(scriptPath());
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
/** 全量概览:不抛错,缺什么报什么。 */
|
|
228
|
+
overview() {
|
|
229
|
+
const script = readText(scriptPath());
|
|
230
|
+
const meta = parseFrontmatter(script ?? "");
|
|
231
|
+
const current = readText(join(sourceRoot, "CURRENT.md"));
|
|
232
|
+
const feedback = readText(join(sourceRoot, "feedback.md"));
|
|
233
|
+
const deckYaml = parseDeckYaml(readText(join(sourceRoot, "build", "deck.yaml")));
|
|
234
|
+
const tablePages = parseStoryPages(script ?? "");
|
|
235
|
+
const sectioned = tablePages.length ? [] : parseSectionedPages(script ?? "");
|
|
236
|
+
const pages = tablePages.length ? tablePages : sectioned;
|
|
237
|
+
const appendix = parseAppendixRows(script ?? "");
|
|
238
|
+
const appendixCount = appendixRowCount(appendix);
|
|
239
|
+
const expectedYaml = pages.length + appendixCount;
|
|
240
|
+
return {
|
|
241
|
+
id,
|
|
242
|
+
label,
|
|
243
|
+
sourceRoot,
|
|
244
|
+
available: Boolean(script),
|
|
245
|
+
meta,
|
|
246
|
+
// deck 标题优先取 build manifest(deck.yaml),其次 script frontmatter,最后 label。
|
|
247
|
+
title: deckYaml.title || meta.title || label,
|
|
248
|
+
status: meta.status ?? "",
|
|
249
|
+
updated: meta.updated ?? "",
|
|
250
|
+
pageCount: pages.length,
|
|
251
|
+
appendixCount,
|
|
252
|
+
appendixRows: appendix,
|
|
253
|
+
yamlPageCount: deckYaml.pages.length,
|
|
254
|
+
currentTable: findTable(current ?? "", ["阶段", "状态"]).slice(0, 12),
|
|
255
|
+
feedbackCount: findTable(feedback ?? "", ["日期", "原话"]).length,
|
|
256
|
+
warnings: [
|
|
257
|
+
...(script && pages.length === 0 ? [`${id}: script.md 存在但没解析出逐页(表格或 P 分节均无)`] : []),
|
|
258
|
+
...(script && deckYaml.pages.length > 0 && deckYaml.pages.length !== expectedYaml
|
|
259
|
+
? [`${id}: deck.yaml 页数(${deckYaml.pages.length}) ≠ script 逐页(${pages.length}) + 附录(${appendixCount})`]
|
|
260
|
+
: []),
|
|
261
|
+
],
|
|
262
|
+
};
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
/** 逐页行(升序):优先表格惯例,其次 `### Pxx` 分节惯例。 */
|
|
266
|
+
storyPages() {
|
|
267
|
+
const text = readText(scriptPath()) ?? "";
|
|
268
|
+
const tableRows = parseStoryPages(text);
|
|
269
|
+
const rows = tableRows.length ? tableRows : parseSectionedPages(text);
|
|
270
|
+
return rows.sort((a, b) => a.page - b.page);
|
|
271
|
+
},
|
|
272
|
+
|
|
273
|
+
/** 每页 visualBrief(源侧已写好的 image prompt:`prompts/P01.txt`)。 */
|
|
274
|
+
prompts() {
|
|
275
|
+
const dir = join(sourceRoot, "prompts");
|
|
276
|
+
const out = {};
|
|
277
|
+
if (!existsSync(dir)) return out;
|
|
278
|
+
for (const name of readdirSync(dir)) {
|
|
279
|
+
const match = /^[Pp](\d+)\.(txt|md)$/.exec(name);
|
|
280
|
+
if (!match) continue;
|
|
281
|
+
out[`p${String(Number(match[1])).padStart(2, "0")}`] = readText(join(dir, name)) ?? "";
|
|
282
|
+
}
|
|
283
|
+
return out;
|
|
284
|
+
},
|
|
285
|
+
|
|
286
|
+
/** 源里已出好的整页图(image deck 的 ppt 本体):`outputs/full/final/P01.png` 等。 */
|
|
287
|
+
sourceImages() {
|
|
288
|
+
const out = {};
|
|
289
|
+
const dirs = ["outputs/full/final", "outputs/final", "pages", "images", "exports"];
|
|
290
|
+
for (const dir of dirs) {
|
|
291
|
+
const full = join(sourceRoot, dir);
|
|
292
|
+
if (!existsSync(full)) continue;
|
|
293
|
+
for (const name of readdirSync(full)) {
|
|
294
|
+
const match = /^[Pp](\d+)\.(png|jpg|jpeg|webp)$/i.exec(name);
|
|
295
|
+
if (!match) continue;
|
|
296
|
+
const pageId = `p${String(Number(match[1])).padStart(2, "0")}`;
|
|
297
|
+
if (out[pageId]) continue;
|
|
298
|
+
const file = join(full, name);
|
|
299
|
+
out[pageId] = { path: file, ext: `.${match[2].toLowerCase()}`, sizeBytes: statSync(file).size };
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return out;
|
|
303
|
+
},
|
|
304
|
+
|
|
305
|
+
/** 附录行(区间按源文件原样)。 */
|
|
306
|
+
appendixRows() {
|
|
307
|
+
return parseAppendixRows(readText(scriptPath()) ?? "");
|
|
308
|
+
},
|
|
309
|
+
|
|
310
|
+
/** deck.yaml 页序(与逐页行按下标对应)。 */
|
|
311
|
+
yamlPages() {
|
|
312
|
+
return parseDeckYaml(readText(join(sourceRoot, "build", "deck.yaml"))).pages;
|
|
313
|
+
},
|
|
314
|
+
|
|
315
|
+
/** 原始 script 文本(diff / 工作副本初始化用)。 */
|
|
316
|
+
readScriptRaw() {
|
|
317
|
+
return readText(scriptPath());
|
|
318
|
+
},
|
|
319
|
+
|
|
320
|
+
/** 反馈行。 */
|
|
321
|
+
feedbackRows(limit = 20) {
|
|
322
|
+
return findTable(readText(join(sourceRoot, "feedback.md")) ?? "", ["日期", "原话"])
|
|
323
|
+
.slice(0, limit)
|
|
324
|
+
.map((cells) => ({ date: cells[0] ?? "", target: cells[1] ?? "", quote: cells[2] ?? "", action: cells[3] ?? "", layer: cells[4] ?? "", tags: cells[5] ?? "" }));
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
}
|
package/lib/store.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* task / event / trace 运行时落盘(契约:docs/runtime-trace.md)。
|
|
3
|
+
*
|
|
4
|
+
* 全部 JSONL 追加、末行快照为最新状态(last-wins),不重写历史:
|
|
5
|
+
* - `.deck/tasks.jsonl` —— task 生命周期,每次迁移追加一条完整快照;
|
|
6
|
+
* - `.deck/events.jsonl` —— 事件索引(envelope);
|
|
7
|
+
* - `.deck/events/<eventId>.json` —— 事件载荷(payloadRef 指过去)。
|
|
8
|
+
*
|
|
9
|
+
* 同步 fs:插件低频写,简单可靠优先;冒烟测试直接建临时目录即可。
|
|
10
|
+
* @module dsh-plugin-deck-studio/store
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { appendFileSync, mkdirSync, readFileSync, writeFileSync, existsSync, readdirSync } from "node:fs";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
|
|
16
|
+
/** TASK 生命周期合法迁移(runtime-trace.md)。 */
|
|
17
|
+
const TASK_FLOW = {
|
|
18
|
+
queued: ["running", "cancelled"],
|
|
19
|
+
running: ["waiting-user", "succeeded", "failed", "cancelled"],
|
|
20
|
+
"waiting-user": ["succeeded", "failed", "cancelled"],
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** 事件 envelope 的 status 词表。 */
|
|
24
|
+
export const EVENT_STATUS = ["started", "progress", "waiting-user", "succeeded", "failed"];
|
|
25
|
+
|
|
26
|
+
/** 生成 `prefix_<base36 时间><4 位随机>` 形如 task_lq2... 的 id。 */
|
|
27
|
+
export function makeId(prefix) {
|
|
28
|
+
const time = Date.now().toString(36);
|
|
29
|
+
const rand = Math.random().toString(36).slice(2, 6);
|
|
30
|
+
return `${prefix}_${time}${rand}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** ISO 时间戳(毫秒精度)。 */
|
|
34
|
+
export function nowIso() {
|
|
35
|
+
return new Date().toISOString();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 创建 store。目录惰性创建:读空目录返回空列表,不抛。
|
|
40
|
+
* @param {string} dataRoot - `.deck/` 绝对路径。
|
|
41
|
+
*/
|
|
42
|
+
export function createRuntimeStore(dataRoot) {
|
|
43
|
+
const tasksPath = join(dataRoot, "tasks.jsonl");
|
|
44
|
+
const eventsPath = join(dataRoot, "events.jsonl");
|
|
45
|
+
const eventsDir = join(dataRoot, "events");
|
|
46
|
+
|
|
47
|
+
function ensureDirs() {
|
|
48
|
+
mkdirSync(eventsDir, { recursive: true });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function appendLine(file, value) {
|
|
52
|
+
ensureDirs();
|
|
53
|
+
appendFileSync(file, `${JSON.stringify(value)}\n`, "utf8");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function readLines(file) {
|
|
57
|
+
if (!existsSync(file)) return [];
|
|
58
|
+
const out = [];
|
|
59
|
+
for (const line of readFileSync(file, "utf8").split("\n")) {
|
|
60
|
+
const trimmed = line.trim();
|
|
61
|
+
if (!trimmed) continue;
|
|
62
|
+
try {
|
|
63
|
+
out.push(JSON.parse(trimmed));
|
|
64
|
+
} catch {
|
|
65
|
+
// 损坏行不炸读取;trace 页能看到“有行解析失败”比黑屏诚实。
|
|
66
|
+
out.push({ corrupt: true, line: trimmed.slice(0, 200) });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** 追加事件;payload 一并落 `events/<eventId>.json`。返回 envelope。 */
|
|
73
|
+
function recordEvent({ type, sessionId = null, taskId = null, deckId = null, slideIds = [], actor = "system", status = "succeeded", payload = {} }) {
|
|
74
|
+
const eventId = makeId("evt");
|
|
75
|
+
const envelope = {
|
|
76
|
+
eventId,
|
|
77
|
+
timestamp: nowIso(),
|
|
78
|
+
sessionId,
|
|
79
|
+
taskId,
|
|
80
|
+
deckId,
|
|
81
|
+
slideIds,
|
|
82
|
+
type,
|
|
83
|
+
actor,
|
|
84
|
+
status,
|
|
85
|
+
payloadRef: `events/${eventId}.json`,
|
|
86
|
+
};
|
|
87
|
+
ensureDirs();
|
|
88
|
+
writeFileSync(join(eventsDir, `${eventId}.json`), `${JSON.stringify({ envelope, payload }, null, 2)}\n`, "utf8");
|
|
89
|
+
appendLine(eventsPath, envelope);
|
|
90
|
+
return envelope;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** 读事件(envelope 升序 = 追加序)。 */
|
|
94
|
+
function events(limit = 100) {
|
|
95
|
+
const all = readLines(eventsPath).filter((line) => !line.corrupt);
|
|
96
|
+
return typeof limit === "number" && limit > 0 ? all.slice(-limit) : all;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** 读一个事件的完整载荷(含 envelope);读不到返回 null。 */
|
|
100
|
+
function eventPayload(eventId) {
|
|
101
|
+
const file = join(eventsDir, `${String(eventId).replace(/[^a-zA-Z0-9_]/g, "")}.json`);
|
|
102
|
+
if (!existsSync(file)) return null;
|
|
103
|
+
try {
|
|
104
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
105
|
+
} catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** 创建 task(初始 queued)。 */
|
|
111
|
+
function taskCreate({ kind, deckId = null, slideIds = [], actor = "agent", sessionId = null, inputRefs = [] }) {
|
|
112
|
+
if (!TASK_FLOW.queued) throw new Error("unreachable");
|
|
113
|
+
const record = {
|
|
114
|
+
taskId: makeId("task"),
|
|
115
|
+
sessionId,
|
|
116
|
+
deckId,
|
|
117
|
+
slideIds,
|
|
118
|
+
kind,
|
|
119
|
+
status: "queued",
|
|
120
|
+
actor,
|
|
121
|
+
createdAt: nowIso(),
|
|
122
|
+
startedAt: null,
|
|
123
|
+
finishedAt: null,
|
|
124
|
+
inputRefs,
|
|
125
|
+
outputRefs: [],
|
|
126
|
+
error: null,
|
|
127
|
+
};
|
|
128
|
+
appendLine(tasksPath, record);
|
|
129
|
+
recordEvent({ type: "task.created", sessionId, taskId: record.taskId, deckId, slideIds, actor, status: "started", payload: { kind, inputRefs } });
|
|
130
|
+
return { ...record };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** 追加一条迁移快照(last-wins)。非法迁移抛错,防止 task 状态漂移。 */
|
|
134
|
+
function taskUpdate(taskId, patch) {
|
|
135
|
+
const current = taskGet(taskId);
|
|
136
|
+
if (!current) throw new Error(`task not found: ${taskId}`);
|
|
137
|
+
const nextStatus = patch.status ?? current.status;
|
|
138
|
+
if (nextStatus !== current.status) {
|
|
139
|
+
const allowed = TASK_FLOW[current.status] ?? [];
|
|
140
|
+
if (!allowed.includes(nextStatus)) {
|
|
141
|
+
throw new Error(`illegal task transition ${current.status} → ${nextStatus} (${taskId})`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const next = {
|
|
145
|
+
...current,
|
|
146
|
+
...patch,
|
|
147
|
+
taskId: current.taskId,
|
|
148
|
+
createdAt: current.createdAt,
|
|
149
|
+
status: nextStatus,
|
|
150
|
+
startedAt: patch.startedAt ?? (nextStatus === "running" && !current.startedAt ? nowIso() : current.startedAt),
|
|
151
|
+
finishedAt: patch.finishedAt ?? (["succeeded", "failed", "cancelled"].includes(nextStatus) ? nowIso() : current.finishedAt),
|
|
152
|
+
};
|
|
153
|
+
appendLine(tasksPath, next);
|
|
154
|
+
if (nextStatus !== current.status) {
|
|
155
|
+
recordEvent({
|
|
156
|
+
type: "task.status",
|
|
157
|
+
sessionId: next.sessionId,
|
|
158
|
+
taskId: next.taskId,
|
|
159
|
+
deckId: next.deckId,
|
|
160
|
+
slideIds: next.slideIds,
|
|
161
|
+
actor: patch.actor ?? next.actor,
|
|
162
|
+
status: nextStatus === "succeeded" ? "succeeded" : nextStatus === "failed" ? "failed" : "progress",
|
|
163
|
+
payload: { from: current.status, to: nextStatus, error: next.error ?? null, outputRefs: next.outputRefs },
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
return next;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** 最新快照;不存在返回 null。 */
|
|
170
|
+
function taskGet(taskId) {
|
|
171
|
+
let found = null;
|
|
172
|
+
for (const line of readLines(tasksPath)) {
|
|
173
|
+
if (!line.corrupt && line.taskId === taskId) found = line;
|
|
174
|
+
}
|
|
175
|
+
return found;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** 每个 task 的最新快照,最近创建的在前。 */
|
|
179
|
+
function taskList(limit = 50) {
|
|
180
|
+
const latest = new Map();
|
|
181
|
+
for (const line of readLines(tasksPath)) {
|
|
182
|
+
if (line.corrupt) continue;
|
|
183
|
+
latest.set(line.taskId, line);
|
|
184
|
+
}
|
|
185
|
+
const all = [...latest.values()].sort((a, b) => (a.createdAt < b.createdAt ? 1 : -1));
|
|
186
|
+
return typeof limit === "number" && limit > 0 ? all.slice(0, limit) : all;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** `.deck/` 内现有 deck id(有 decks/<id>/deck.json 的)。 */
|
|
190
|
+
function listDeckIds() {
|
|
191
|
+
const dir = join(dataRoot, "decks");
|
|
192
|
+
if (!existsSync(dir)) return [];
|
|
193
|
+
return readdirSync(dir, { withFileTypes: true })
|
|
194
|
+
.filter((entry) => entry.isDirectory() && existsSync(join(dir, entry.name, "deck.json")))
|
|
195
|
+
.map((entry) => entry.name)
|
|
196
|
+
.sort();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
get dataRoot() {
|
|
201
|
+
return dataRoot;
|
|
202
|
+
},
|
|
203
|
+
recordEvent,
|
|
204
|
+
events,
|
|
205
|
+
eventPayload,
|
|
206
|
+
taskCreate,
|
|
207
|
+
taskUpdate,
|
|
208
|
+
taskGet,
|
|
209
|
+
taskList,
|
|
210
|
+
listDeckIds,
|
|
211
|
+
};
|
|
212
|
+
}
|