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.
- package/cordis.example.yml +85 -7
- package/cordis.patch.yml +78 -7
- package/index.js +367 -27
- package/lib/capabilities.js +234 -0
- package/lib/citation-check.js +93 -0
- package/lib/config.js +27 -0
- package/lib/deck-brief.js +35 -1
- package/lib/deck-docs.js +45 -7
- package/lib/deck-templates.js +8 -1
- package/lib/deck-workspace.js +27 -1
- package/lib/designs.js +15 -0
- package/lib/fact-labels.js +81 -0
- package/lib/first-draft.js +119 -0
- package/lib/image-review.js +523 -0
- package/lib/page-contract.js +112 -0
- package/lib/proposal-decision.js +249 -75
- package/lib/render.js +101 -10
- package/lib/review-agent.js +1 -1
- package/lib/review-personas.js +49 -2
- package/lib/review-protocol.js +411 -0
- package/lib/review-readers.js +300 -0
- package/lib/review-score.js +198 -0
- package/lib/title-chain.js +122 -0
- package/lib/tools.js +973 -78
- package/lib/update-check.js +166 -0
- package/package.json +12 -11
- package/seeds/docs/assets/screenshots/f186-01-agent-style-create.png +0 -0
- package/seeds/docs/assets/screenshots/f186-02-designs-card.png +0 -0
- package/seeds/docs/assets/screenshots/f187-01-review-readers.png +0 -0
- package/seeds/docs/assets/screenshots/f187-02-gap-report.png +0 -0
- package/seeds/docs/assets/screenshots/f188-01-header-buttons.png +0 -0
- package/seeds/docs/assets/screenshots/f188-02-update-toast.png +0 -0
- package/seeds/docs/assets/screenshots/f188-03-review-doc-tabs.png +0 -0
- package/seeds/docs/assets/screenshots/f188-04-script-doc-nochips.png +0 -0
- package/seeds/docs/assets/screenshots/i14-01-create.png +0 -0
- package/seeds/docs/assets/screenshots/i14-02-first-draft.png +0 -0
- package/seeds/docs/assets/screenshots/i14-03-rendered.png +0 -0
- package/seeds/docs/assets/screenshots/i14-04-image-review.png +0 -0
- package/seeds/docs/assets/screenshots/i14-05-p01-page.png +0 -0
- package/seeds/docs/install.en.md +13 -3
- package/seeds/docs/install.md +13 -4
- package/seeds/docs/manual.en.md +17 -0
- package/seeds/docs/manual.md +17 -0
- package/seeds/docs/sample-deck.en.md +1 -1
- package/seeds/docs/sample-deck.md +1 -1
- package/seeds/docs/styles.en.md +2 -1
- package/seeds/docs/styles.md +14 -4
- package/seeds/samples/DSA-01.jpg +0 -0
- package/seeds/samples/DSA-02.jpg +0 -0
- package/seeds/samples/DSA-03.jpg +0 -0
- package/seeds/styles/DSA.json +22 -0
- package/ui/workbench.css +25 -3
- package/ui/workbench.html +286 -10
package/index.js
CHANGED
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
|
|
12
12
|
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
13
13
|
import { createHash } from "node:crypto";
|
|
14
|
-
import {
|
|
14
|
+
import { execFile } from "node:child_process";
|
|
15
|
+
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
15
16
|
import { PLUGIN_DIR, mergeConfig, repoRootFrom } from "./lib/config.js";
|
|
16
17
|
import { createRuntimeStore } from "./lib/store.js";
|
|
17
18
|
import { createDeckWorkspace, scriptSectionRange } from "./lib/deck-workspace.js";
|
|
@@ -26,13 +27,33 @@ import { contentDensityOptions, densitySpec, normalizeContentDensity } from "./l
|
|
|
26
27
|
import { defineDeckTools } from "./lib/tools.js";
|
|
27
28
|
import { registerReviewAgentProvider } from "./lib/review-agent.js";
|
|
28
29
|
import { faqIndex, searchFaq } from "./lib/faq.js";
|
|
29
|
-
import { createScriptToSourceSync, decideProposal } from "./lib/proposal-decision.js";
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
30
|
+
import { applyConfirmedBatch, createScriptToSourceSync, decideProposal } from "./lib/proposal-decision.js";
|
|
31
|
+
import { autolandBadge, latestAutoland, rollbackFirstDraft } from "./lib/first-draft.js";
|
|
32
|
+
import { createDesignRegistry, normalizeAbbr, suggestAbbr } from "./lib/designs.js";
|
|
33
|
+
import { appendSampleRecord, readSampleRecords, readStyleDefinition, writeStyleDefinition } from "./lib/design-style.js";
|
|
32
34
|
import { resolveMediaFile, scanDesignStyles, scanResources } from "./lib/home-catalog.js";
|
|
33
35
|
import { defaultSelection, renderModelPayload, resolveSelection } from "./lib/render-nodes.js";
|
|
34
36
|
import { applyRenderSettings, readRenderSettings, renderSettingsPayload, saveRenderSettings } from "./lib/render-settings.js";
|
|
35
|
-
import { editEndpointFor, selectRenderer } from "./lib/render.js";
|
|
37
|
+
import { buildPrompt, editEndpointFor, selectRenderer } from "./lib/render.js";
|
|
38
|
+
import { readDeckBrief } from "./lib/deck-brief.js";
|
|
39
|
+
import { detectProfileDir, installModeOf, isNewer, latestFromRegistry, repoLinksOf } from "./lib/update-check.js";
|
|
40
|
+
|
|
41
|
+
/** 跑一个外部命令(异步、带超时、合并 stdout/stderr;失败时把输出带进错误信息)。 */
|
|
42
|
+
function runCommand(command, args, { cwd = "", timeoutMs = 120000 } = {}) {
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
execFile(command, args, { cwd: cwd || undefined, timeout: timeoutMs, maxBuffer: 8 * 1024 * 1024, env: process.env }, (error, stdout, stderr) => {
|
|
45
|
+
const output = `${stdout ?? ""}${stderr ?? ""}`.trim();
|
|
46
|
+
if (error) {
|
|
47
|
+
const failure = new Error(`${command} ${args.join(" ")} 失败:${error.message}${output ? `
|
|
48
|
+
${output.slice(-2000)}` : ""}`);
|
|
49
|
+
failure.output = output;
|
|
50
|
+
reject(failure);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
resolve(output);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
}
|
|
36
57
|
|
|
37
58
|
/** 稳定 Cordis 插件名。 */
|
|
38
59
|
export const name = "deck-studio";
|
|
@@ -50,6 +71,7 @@ export const ROUTES = {
|
|
|
50
71
|
docfile: "/api/deck-studio.docfile",
|
|
51
72
|
export: "/api/deck-studio.export",
|
|
52
73
|
pageEdit: "/api/deck-studio.page-edit",
|
|
74
|
+
pagePrompt: "/api/deck-studio.page-prompt",
|
|
53
75
|
pageSave: "/api/deck-studio.page-save",
|
|
54
76
|
media: "/api/deck-studio.media",
|
|
55
77
|
renderModel: "/api/deck-studio.render-model",
|
|
@@ -66,6 +88,8 @@ export const ROUTES = {
|
|
|
66
88
|
sessionBind: "/api/deck-studio.session-bind",
|
|
67
89
|
// F-108:工作台「确认落版 / 驳回」按钮——与 Agent 的 deck_revision_confirm 共用落版逻辑。
|
|
68
90
|
proposalDecide: "/api/deck-studio.proposal-decide",
|
|
91
|
+
// F-173:首稿自动落版的回滚入口(工作台徽标旁的「回滚首稿」按钮)
|
|
92
|
+
draftRollback: "/api/deck-studio.draft-rollback",
|
|
69
93
|
sessionBindings: "/api/deck-studio.session-bindings",
|
|
70
94
|
handoff: "/api/deck-studio.handoff",
|
|
71
95
|
sourcefile: "/api/deck-studio.sourcefile",
|
|
@@ -75,6 +99,9 @@ export const ROUTES = {
|
|
|
75
99
|
manual: "/api/deck-studio.manual",
|
|
76
100
|
manualDoc: "/api/deck-studio.manual-doc",
|
|
77
101
|
docsAsset: "/api/deck-studio.docs-asset",
|
|
102
|
+
// F-188:顶栏「更新」按钮——查 npm 最新版 / 一键更新到最新版(只在 npm 安装模式下执行)
|
|
103
|
+
updateCheck: "/api/deck-studio.update-check",
|
|
104
|
+
update: "/api/deck-studio.update",
|
|
78
105
|
};
|
|
79
106
|
|
|
80
107
|
/** 出图资产 MIME(image deck 只有图片;svg 仅 fake 测试用)。 */
|
|
@@ -195,6 +222,117 @@ function pluginVersion() {
|
|
|
195
222
|
return versionCache;
|
|
196
223
|
}
|
|
197
224
|
|
|
225
|
+
/** 插件自己的 package.json(版本 / 仓库链接来源;只读一次)。 */
|
|
226
|
+
let pluginMetaCache = null;
|
|
227
|
+
function pluginMeta() {
|
|
228
|
+
if (pluginMetaCache) return pluginMetaCache;
|
|
229
|
+
let pkg = {};
|
|
230
|
+
try {
|
|
231
|
+
pkg = JSON.parse(readFileSync(join(PLUGIN_DIR, "package.json"), "utf8"));
|
|
232
|
+
} catch {
|
|
233
|
+
pkg = {};
|
|
234
|
+
}
|
|
235
|
+
pluginMetaCache = {
|
|
236
|
+
name: String(pkg.name ?? "dsh-plugin-deck-studio"),
|
|
237
|
+
version: String(pkg.version ?? pluginVersion()),
|
|
238
|
+
links: repoLinksOf(pkg),
|
|
239
|
+
};
|
|
240
|
+
return pluginMetaCache;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** 本机 profile 目录:先按插件真实路径反推(npm 布局),再退回 DSH 注入的 env。 */
|
|
244
|
+
function profileDirHint() {
|
|
245
|
+
const env = typeof process !== "undefined" ? process.env : {};
|
|
246
|
+
const meta = pluginMeta();
|
|
247
|
+
const found = detectProfileDir({ pluginDir: PLUGIN_DIR, packageName: meta.name, env });
|
|
248
|
+
if (found) return found;
|
|
249
|
+
const home = env.DSH_HOME ? String(env.DSH_HOME) : "";
|
|
250
|
+
const profile = env.DSH_PROFILE ? String(env.DSH_PROFILE) : "";
|
|
251
|
+
return home && profile ? join(home, "profiles", profile) : "";
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** 更新相关的静态信息(不发网络请求;workbench 首屏用)。 */
|
|
255
|
+
function updateBrief() {
|
|
256
|
+
const meta = pluginMeta();
|
|
257
|
+
const install = installModeOf({ profileDir: profileDirHint(), packageName: meta.name });
|
|
258
|
+
const env = typeof process !== "undefined" ? process.env : {};
|
|
259
|
+
return {
|
|
260
|
+
currentVersion: meta.version,
|
|
261
|
+
installMode: install.mode,
|
|
262
|
+
installSpec: install.spec,
|
|
263
|
+
installReason: install.reason,
|
|
264
|
+
profile: (() => {
|
|
265
|
+
const dir = profileDirHint();
|
|
266
|
+
return dir ? basename(dir) : String(env.DSH_PROFILE ?? "");
|
|
267
|
+
})(),
|
|
268
|
+
repo: meta.links.repo,
|
|
269
|
+
issues: meta.links.issues,
|
|
270
|
+
suggest: meta.links.suggest,
|
|
271
|
+
registry: `https://registry.npmjs.org/${encodeURIComponent(meta.name)}`,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** 查 npm 上的最新版本(6s 超时;离线 / 超时不抛,只在 payload 里报错)。 */
|
|
276
|
+
async function updateStatus() {
|
|
277
|
+
const brief = updateBrief();
|
|
278
|
+
const payload = { ...brief, latestVersion: "", hasUpdate: false, checkedAt: new Date().toISOString(), error: "" };
|
|
279
|
+
try {
|
|
280
|
+
const res = await fetch(`${brief.registry}/latest`, {
|
|
281
|
+
headers: { accept: "application/json" },
|
|
282
|
+
signal: AbortSignal.timeout(12000),
|
|
283
|
+
});
|
|
284
|
+
if (!res.ok) throw new Error(`registry HTTP ${res.status}`);
|
|
285
|
+
const latest = latestFromRegistry(await res.json());
|
|
286
|
+
if (!latest) throw new Error("registry 返回里没有 latest 版本");
|
|
287
|
+
payload.latestVersion = latest;
|
|
288
|
+
payload.hasUpdate = isNewer(latest, brief.currentVersion);
|
|
289
|
+
payload.command = brief.installMode === "npm"
|
|
290
|
+
? `dsh plugin --profile ${brief.profile || "<profile>"} add ${pluginMeta().name}@${latest}`
|
|
291
|
+
: "";
|
|
292
|
+
if (brief.installMode === "linked") payload.note = "本机是开发链接安装,更新走仓库(git pull);界面不执行 pnpm。";
|
|
293
|
+
} catch (error) {
|
|
294
|
+
const message = String(error?.message ?? error);
|
|
295
|
+
const timedOut = error?.name === "TimeoutError" || /abort|timeout/i.test(message);
|
|
296
|
+
payload.error = timedOut
|
|
297
|
+
? "检查更新超时:连不上 npm registry(网络 / 代理问题),可以直接用命令手动更新。"
|
|
298
|
+
: `检查更新失败:${message}`;
|
|
299
|
+
}
|
|
300
|
+
return payload;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** 一键更新:npm 安装模式下在 profile 目录跑 pnpm add <name>@<latest>(超时 240s)。 */
|
|
304
|
+
async function runSelfUpdate() {
|
|
305
|
+
const brief = updateBrief();
|
|
306
|
+
if (brief.installMode !== "npm") {
|
|
307
|
+
throw new Error(
|
|
308
|
+
brief.installMode === "linked"
|
|
309
|
+
? "本机是开发链接安装(link:/file:),界面不执行更新;请在仓库里 git pull 后重启 DSH。"
|
|
310
|
+
: "没识别出 profile 的安装方式,无法自动更新;请手动执行 dsh plugin --profile <profile> add dsh-plugin-deck-studio@latest。",
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
const status = await updateStatus();
|
|
314
|
+
if (status.error) throw new Error(status.error);
|
|
315
|
+
if (!status.latestVersion) throw new Error("没查到 npm 上的最新版本");
|
|
316
|
+
const target = `${pluginMeta().name}@${status.latestVersion}`;
|
|
317
|
+
const output = await runCommand("pnpm", ["add", target], { cwd: brief.profile, timeoutMs: 240000 });
|
|
318
|
+
store.recordEvent({
|
|
319
|
+
type: "plugin.update",
|
|
320
|
+
deckId: "",
|
|
321
|
+
actor: "user",
|
|
322
|
+
status: "succeeded",
|
|
323
|
+
payload: { from: brief.currentVersion, to: status.latestVersion, target },
|
|
324
|
+
});
|
|
325
|
+
return {
|
|
326
|
+
ok: true,
|
|
327
|
+
from: brief.currentVersion,
|
|
328
|
+
to: status.latestVersion,
|
|
329
|
+
target,
|
|
330
|
+
restarted: false,
|
|
331
|
+
restartHint: "更新已写入 profile,重启 DSH 后生效(当前进程仍是旧版本)。",
|
|
332
|
+
output,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
198
336
|
function json(payload, status = 200) {
|
|
199
337
|
return Response.json(payload, { status, headers: { "cache-control": "no-store" } });
|
|
200
338
|
}
|
|
@@ -709,7 +847,7 @@ export function createService(config, logger, env = process.env) {
|
|
|
709
847
|
}
|
|
710
848
|
}
|
|
711
849
|
|
|
712
|
-
/** F-157:风格注册表为空时,自动导入插件自带的
|
|
850
|
+
/** F-157:风格注册表为空时,自动导入插件自带的 9 个预设风格(含样张)。 */
|
|
713
851
|
function bootstrapDesignStyles() {
|
|
714
852
|
try {
|
|
715
853
|
if (designRegistry.listDesigns().length > 0) return;
|
|
@@ -1003,6 +1141,111 @@ export function createService(config, logger, env = process.env) {
|
|
|
1003
1141
|
return { design: designProjectView(design), deckId: canvas.deckId };
|
|
1004
1142
|
}
|
|
1005
1143
|
|
|
1144
|
+
/**
|
|
1145
|
+
* F-186:把某个 deck 已出图页面的成品图复制成 design 样张(不重新出图、不花额度)。
|
|
1146
|
+
* 默认挑 3 张(封面 / 信息页 / 数据页各一更好)——三张最能说明一套风格在封面、信息页、
|
|
1147
|
+
* 对比/数据页上都成立,和预设风格的 3 张基线样张口径一致。
|
|
1148
|
+
*/
|
|
1149
|
+
function copyDeckImagesAsSamples(design, { deckId, pages, max = 3 }) {
|
|
1150
|
+
const adapter = adapterOf(deckId);
|
|
1151
|
+
if (!adapter) throw new Error(`未知 deck:${deckId}`);
|
|
1152
|
+
const dir = designStyleDir(design);
|
|
1153
|
+
mkdirSync(dir, { recursive: true });
|
|
1154
|
+
const deck = workspace.manifest(deckId) ?? workspace.ensureDeck(adapter, { styleBrief: config.styleBrief });
|
|
1155
|
+
const requested = Array.isArray(pages) ? pages : [];
|
|
1156
|
+
const pageIds = requested.length
|
|
1157
|
+
? requested.map((page) => `p${String(Number(String(page).replace(/^p/i, ""))).padStart(2, "0")}`)
|
|
1158
|
+
: deck.slides.slice(0, max).map((slide) => slide.id);
|
|
1159
|
+
const sourceImages = adapter.sourceImages?.() ?? {};
|
|
1160
|
+
const samples = [];
|
|
1161
|
+
for (const pageId of pageIds.slice(0, 6)) {
|
|
1162
|
+
const info = slideRenderInfo(deckId, pageId);
|
|
1163
|
+
if (!info) continue;
|
|
1164
|
+
const sourcePath = info.fromSource
|
|
1165
|
+
? sourceImages[pageId]?.path
|
|
1166
|
+
: join(workspace.slideDir(deckId, pageId), "versions", info.revision, `image${info.ext}`);
|
|
1167
|
+
if (!sourcePath || !existsSync(sourcePath)) continue;
|
|
1168
|
+
const ext = String(info.ext ?? "png").replace(/^\./, "");
|
|
1169
|
+
let fileName = `sample-${pageId}-${info.revision}.${ext}`;
|
|
1170
|
+
let n = 1;
|
|
1171
|
+
while (existsSync(join(dir, fileName))) fileName = `sample-${pageId}-${info.revision}-${++n}.${ext}`;
|
|
1172
|
+
writeFileSync(join(dir, fileName), readFileSync(sourcePath));
|
|
1173
|
+
let prompt = "";
|
|
1174
|
+
try {
|
|
1175
|
+
prompt = readFileSync(join(workspace.slideDir(deckId, pageId), "versions", info.revision, "prompt.txt"), "utf8");
|
|
1176
|
+
} catch {
|
|
1177
|
+
prompt = "";
|
|
1178
|
+
}
|
|
1179
|
+
appendSampleRecord(dir, {
|
|
1180
|
+
revision: info.revision,
|
|
1181
|
+
brief: `从 ${deckId} ${pageId} 的成品图复制(登记风格用,未重新出图)`,
|
|
1182
|
+
prompt,
|
|
1183
|
+
imagePath: `designs/${design.abbr}/${fileName}`,
|
|
1184
|
+
model: "",
|
|
1185
|
+
size: "",
|
|
1186
|
+
});
|
|
1187
|
+
samples.push({ pageId, revision: info.revision, file: fileName, sizeBytes: statSync(join(dir, fileName)).size });
|
|
1188
|
+
}
|
|
1189
|
+
if (samples.length) {
|
|
1190
|
+
store.recordEvent({
|
|
1191
|
+
type: "design.sample.add",
|
|
1192
|
+
deckId: design.canvasDeckId,
|
|
1193
|
+
actor: "agent",
|
|
1194
|
+
status: "succeeded",
|
|
1195
|
+
payload: { designId: design.designId, abbr: design.abbr, fromDeck: deckId, samples: samples.map((item) => item.file) },
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
return samples;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
/**
|
|
1202
|
+
* F-186:普通对话里登记风格——没先选风格,deck 做出来后觉得这套视觉语言值得复用。
|
|
1203
|
+
*
|
|
1204
|
+
* 两种用法:
|
|
1205
|
+
* - 不传 designId:新建 design(与 Designs 页「+添加风格」同一份实体),缩写可自动生成;
|
|
1206
|
+
* - 传 designId(或缩写):只往已有风格里补样张,不动名称与提示词。
|
|
1207
|
+
* 样张一律从 deck 已出图页面**复制成品图**,不重新出图、不花额度,登记下来和用户看到的一致。
|
|
1208
|
+
*/
|
|
1209
|
+
function createStyleFromDeck(payload = {}) {
|
|
1210
|
+
const deckId = String(payload.deckId ?? "").trim();
|
|
1211
|
+
const existingKey = String(payload.designId ?? "").trim();
|
|
1212
|
+
if (existingKey) {
|
|
1213
|
+
const design = designRegistry.findDesign(existingKey) ?? designRegistry.findDesignByAbbr(existingKey);
|
|
1214
|
+
if (!design) throw new Error(`未知 design:${existingKey}`);
|
|
1215
|
+
if (!deckId) throw new Error("补样张要指定 deckId(从哪个 deck 复制成品图)");
|
|
1216
|
+
const samples = copyDeckImagesAsSamples(design, { deckId, pages: payload.pages, max: 3 });
|
|
1217
|
+
return {
|
|
1218
|
+
designId: design.designId,
|
|
1219
|
+
abbr: design.abbr,
|
|
1220
|
+
name: design.name,
|
|
1221
|
+
canvasDeckId: design.canvasDeckId,
|
|
1222
|
+
samples,
|
|
1223
|
+
appended: true,
|
|
1224
|
+
totalSamples: readSampleRecords(designStyleDir(design)).length,
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
const name = String(payload.name ?? "").trim();
|
|
1228
|
+
if (!name) throw new Error("风格名称不能为空");
|
|
1229
|
+
const styleBrief = String(payload.styleBrief ?? "").trim();
|
|
1230
|
+
if (!styleBrief) throw new Error("风格提示词不能为空:先写清这套风格的画面语言(颜色 / 材质 / 字体 / 版式)");
|
|
1231
|
+
const description = String(payload.description ?? "").trim() || styleBrief;
|
|
1232
|
+
const taken = designRegistry.listDesigns().map((item) => item.abbr);
|
|
1233
|
+
const abbr = String(payload.abbr ?? "").trim() ? normalizeAbbr(payload.abbr) : suggestAbbr(name, taken);
|
|
1234
|
+
if (designRegistry.findDesignByAbbr(abbr)) throw new Error(`缩写 ${abbr} 已被其他风格占用,换一个 abbr`);
|
|
1235
|
+
const created = createDesign({
|
|
1236
|
+
name,
|
|
1237
|
+
abbr,
|
|
1238
|
+
description,
|
|
1239
|
+
styleBrief,
|
|
1240
|
+
constraints: payload.constraints,
|
|
1241
|
+
reference: payload.reference,
|
|
1242
|
+
});
|
|
1243
|
+
const design = designRegistry.findDesign(created.design.designId);
|
|
1244
|
+
mkdirSync(designStyleDir(design), { recursive: true });
|
|
1245
|
+
const samples = deckId ? copyDeckImagesAsSamples(design, { deckId, pages: payload.pages, max: 3 }) : [];
|
|
1246
|
+
return { designId: design.designId, abbr: design.abbr, name: design.name, canvasDeckId: design.canvasDeckId, samples, appended: false };
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1006
1249
|
/** 预设风格包里某个缩写的 styleBrief(老 design 迁移用;没有就 null)。 */
|
|
1007
1250
|
function readSeedStyleBrief(abbr) {
|
|
1008
1251
|
const file = join(SEED_STYLES_DIR, `${String(abbr ?? "").toUpperCase()}.json`);
|
|
@@ -1300,26 +1543,23 @@ export function createService(config, logger, env = process.env) {
|
|
|
1300
1543
|
|
|
1301
1544
|
/** script revision 列表(修改日志用:revisions/vNNN.md + 同名 note)。 */
|
|
1302
1545
|
function deckRevisions(deckId) {
|
|
1303
|
-
let dir;
|
|
1304
1546
|
try {
|
|
1305
|
-
|
|
1547
|
+
return workspace.listScriptRevisions(deckId).map((row) => {
|
|
1548
|
+
const autoland = autolandOf(deckId, row.revision);
|
|
1549
|
+
return { ...row, ...(autoland ? { autoland, autolandBadge: autolandBadge(autoland) } : {}) };
|
|
1550
|
+
});
|
|
1306
1551
|
} catch {
|
|
1307
1552
|
return [];
|
|
1308
1553
|
}
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
.
|
|
1314
|
-
.
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
revision,
|
|
1319
|
-
savedAt: statSync(join(dir, name)).mtime.toISOString(),
|
|
1320
|
-
note: existsSync(noteFile) ? readFileSync(noteFile, "utf8").trim() : "",
|
|
1321
|
-
};
|
|
1322
|
-
});
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
function autolandOf(deckId, revision) {
|
|
1557
|
+
try {
|
|
1558
|
+
const raw = readFileSync(join(workspace.deckDir(deckId), "script", "revisions", `${revision}.autoland.json`), "utf8");
|
|
1559
|
+
return JSON.parse(raw);
|
|
1560
|
+
} catch {
|
|
1561
|
+
return null;
|
|
1562
|
+
}
|
|
1323
1563
|
}
|
|
1324
1564
|
|
|
1325
1565
|
/** 9 份必备 deck 文档的治理状态(不含大段正文)。 */
|
|
@@ -1334,14 +1574,15 @@ export function createService(config, logger, env = process.env) {
|
|
|
1334
1574
|
}
|
|
1335
1575
|
|
|
1336
1576
|
/** 单份文档(中间区标签用):正文 + 结构化视图。 */
|
|
1337
|
-
|
|
1577
|
+
/** F-187:`file` 指定同一份文档下的某个相关文件(如审稿页的 review.gap.md),不传就用默认正文。 */
|
|
1578
|
+
function deckDoc(deckId, docId, file = "") {
|
|
1338
1579
|
let deckDir;
|
|
1339
1580
|
try {
|
|
1340
1581
|
deckDir = workspace.deckDir(deckId);
|
|
1341
1582
|
} catch {
|
|
1342
1583
|
return null;
|
|
1343
1584
|
}
|
|
1344
|
-
return readDeckDoc({ adapter: adapterOf(deckId), deckDir, docId, render: config.render, revisions: deckRevisions(deckId), events: deckEvents(deckId) });
|
|
1585
|
+
return readDeckDoc({ adapter: adapterOf(deckId), deckDir, docId, file, render: config.render, revisions: deckRevisions(deckId), events: deckEvents(deckId) });
|
|
1345
1586
|
}
|
|
1346
1587
|
|
|
1347
1588
|
/** 给缺失文档打骨架(只写 .deck 工作副本,不动源 deck)。 */
|
|
@@ -1358,6 +1599,39 @@ export function createService(config, logger, env = process.env) {
|
|
|
1358
1599
|
return pageEditView(adapter, pageId);
|
|
1359
1600
|
}
|
|
1360
1601
|
|
|
1602
|
+
/**
|
|
1603
|
+
* F-189:某页「拼装后的完整出图 prompt」(只读,脚本页折叠展示用)。
|
|
1604
|
+
* 与出图工具同一套拼装:deck 关联 design 的 styleBrief + 图上文字清单 + 讲述用正文 + 画面要求。
|
|
1605
|
+
*/
|
|
1606
|
+
function pagePrompt(deckId, pageId) {
|
|
1607
|
+
const adapter = adapterOf(deckId);
|
|
1608
|
+
if (!adapter?.available()) return null;
|
|
1609
|
+
const deck = workspace.manifest(deckId);
|
|
1610
|
+
const normalized = String(pageId ?? "").trim();
|
|
1611
|
+
const slide = (deck?.slides ?? []).find((item) => item.id === normalized || `p${String(item.order).padStart(2, "0")}` === normalized);
|
|
1612
|
+
if (!slide) return null;
|
|
1613
|
+
let styleBrief = config.styleBrief;
|
|
1614
|
+
const association = designRegistry.deckDesignFor(deckId);
|
|
1615
|
+
const design = association?.designId ? designRegistry.findDesign(association.designId) : null;
|
|
1616
|
+
if (design) {
|
|
1617
|
+
let definition = null;
|
|
1618
|
+
try {
|
|
1619
|
+
definition = readStyleDefinition(adapter.sourceRoot, design.abbr ?? "", design);
|
|
1620
|
+
} catch {
|
|
1621
|
+
definition = null;
|
|
1622
|
+
}
|
|
1623
|
+
const brief = String(design.styleBrief || definition?.styleBrief || definition?.description || design.description || "").trim();
|
|
1624
|
+
const constraints = String(design.constraints || definition?.constraints || "").trim();
|
|
1625
|
+
if (brief) styleBrief = [brief, constraints ? `不要什么:${constraints}` : ""].filter(Boolean).join("\n");
|
|
1626
|
+
}
|
|
1627
|
+
return {
|
|
1628
|
+
deckId,
|
|
1629
|
+
pageId: slide.id,
|
|
1630
|
+
design: design?.abbr ?? null,
|
|
1631
|
+
prompt: buildPrompt(slide, styleBrief, { brief: readDeckBrief(adapter.sourceRoot) }),
|
|
1632
|
+
};
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1361
1635
|
/**
|
|
1362
1636
|
* 页面微调保存:写回源 script.md / prompts/Pnn.txt,重建 manifest,并自动 git 提交。
|
|
1363
1637
|
* 这是唯一允许直接写源 deck 的动作(ADR-0006 的例外,见 ADR-0008)。
|
|
@@ -1731,6 +2005,12 @@ export function createService(config, logger, env = process.env) {
|
|
|
1731
2005
|
// 已过期:工作副本变了,这条提案的 before 行不存在,确认会「工作副本已漂移」。
|
|
1732
2006
|
stale: isStaleProposal(proposal),
|
|
1733
2007
|
createdAt: proposal.createdAt,
|
|
2008
|
+
// F-176 / I-18:评审意见清单要靠这些字段分组和判断能否勾选(C 类不可选)。
|
|
2009
|
+
status: proposal.status,
|
|
2010
|
+
reviewRef: proposal.reviewRef ?? null,
|
|
2011
|
+
severity: proposal.severity ?? null,
|
|
2012
|
+
confidence: proposal.confidence ?? null,
|
|
2013
|
+
evidence: typeof proposal.evidence === "string" ? proposal.evidence.slice(0, 300) : null,
|
|
1734
2014
|
})),
|
|
1735
2015
|
generatedAt: new Date().toISOString(),
|
|
1736
2016
|
};
|
|
@@ -1993,6 +2273,8 @@ export function createService(config, logger, env = process.env) {
|
|
|
1993
2273
|
designProjects: designProjects(),
|
|
1994
2274
|
bindings: listBindings(),
|
|
1995
2275
|
proposals: base.pendingProposals,
|
|
2276
|
+
// F-188:顶栏「更新 / GitHub」按钮首屏数据(不打网络,检查更新另调 update-check)
|
|
2277
|
+
update: updateBrief(),
|
|
1996
2278
|
};
|
|
1997
2279
|
}
|
|
1998
2280
|
|
|
@@ -2002,7 +2284,7 @@ export function createService(config, logger, env = process.env) {
|
|
|
2002
2284
|
bootstrapDesignStyles();
|
|
2003
2285
|
}
|
|
2004
2286
|
|
|
2005
|
-
return { config, store, workspace, adapters, status, state, workbench, readManual, readUserName: () => config.userName, saveUserName, readDoc, docsAsset, setDeckDensity, renderSettings, saveRenderConfig, isLocalDeck: (deckId) => localDeckIds().has(String(deckId ?? "")), assetFile, slideRenderInfo, deckCoverUrl, deckRevisions, deckDocs, deckDoc, deckDocsScaffold, deckDocFile, pageForEdit, savePage, exportDeckFile, designSamples, sourceFile, homeCatalog, mediaFile, renderState, setRenderSelection, renderRuntime, createDeck, createDesign, designProjects, designStyle, updateDesignStyle, backfillStyleBriefs, listBindings, bindSession, unbindSession, deleteDesign, deleteDeck, setDeckDesign, addDesignSamples, seedStyleDesigns, renderStatus, exportHandoff }
|
|
2287
|
+
return { config, store, workspace, adapters, status, state, workbench, readManual, readUserName: () => config.userName, saveUserName, readDoc, docsAsset, setDeckDensity, renderSettings, saveRenderConfig, isLocalDeck: (deckId) => localDeckIds().has(String(deckId ?? "")), assetFile, slideRenderInfo, deckCoverUrl, deckRevisions, deckDocs, deckDoc, deckDocsScaffold, deckDocFile, pageForEdit, pagePrompt, savePage, exportDeckFile, designSamples, sourceFile, homeCatalog, mediaFile, renderState, setRenderSelection, renderRuntime, createDeck, createDesign, createStyleFromDeck, designProjects, designStyle, updateDesignStyle, backfillStyleBriefs, listBindings, bindSession, unbindSession, deleteDesign, deleteDeck, setDeckDesign, addDesignSamples, seedStyleDesigns, renderStatus, exportHandoff, updateBrief, updateStatus, runSelfUpdate }
|
|
2006
2288
|
}
|
|
2007
2289
|
|
|
2008
2290
|
/**
|
|
@@ -2079,6 +2361,12 @@ export function apply(ctx, rawConfig) {
|
|
|
2079
2361
|
return view ? json({ ok: true, ...view }) : json({ ok: false, error: { message: "page not found" } }, 404);
|
|
2080
2362
|
});
|
|
2081
2363
|
|
|
2364
|
+
registerRoute(ROUTES.pagePrompt, (request) => {
|
|
2365
|
+
const url = new URL(request?.url ?? "/", "http://localhost");
|
|
2366
|
+
const view = service.pagePrompt(url.searchParams.get("deckId") ?? "", url.searchParams.get("pageId") ?? "");
|
|
2367
|
+
return view ? json({ ok: true, ...view }) : json({ ok: false, error: { message: "page not found" } }, 404);
|
|
2368
|
+
});
|
|
2369
|
+
|
|
2082
2370
|
registerRoute(ROUTES.pageSave, async (request) => {
|
|
2083
2371
|
const body = await request.json().catch(() => ({}));
|
|
2084
2372
|
const result = service.savePage(body.deckId ?? "", body.pageId ?? "", body.fields ?? body);
|
|
@@ -2198,6 +2486,18 @@ export function apply(ctx, rawConfig) {
|
|
|
2198
2486
|
const quote = String(body?.quote ?? "").trim()
|
|
2199
2487
|
|| (confirmed ? "(工作台:用户点了「确认落版」)" : "(工作台:用户点了「驳回」)");
|
|
2200
2488
|
const syncScriptToSource = createScriptToSourceSync({ adapters: service.adapters, isLocalDeck: (deckId) => service.isLocalDeck(deckId) });
|
|
2489
|
+
// F-176 / I-18:「接受并更新」——勾选的一批评审修改合成**一条** revision(不是一条提案一条 revision)。
|
|
2490
|
+
if (confirmed && body?.batch === true) {
|
|
2491
|
+
const proposals = ids.map((proposalId) => service.workspace.proposal(proposalId)).filter(Boolean);
|
|
2492
|
+
const missing = ids.filter((proposalId) => !proposals.some((proposal) => proposal.proposalId === proposalId));
|
|
2493
|
+
if (missing.length) return json({ ok: false, error: { message: `提案不存在:${missing.join("、")}`, missing } }, 404);
|
|
2494
|
+
try {
|
|
2495
|
+
const landed = applyConfirmedBatch({ store: service.store, workspace: service.workspace, proposals, quote, sessionId: null, syncScriptToSource });
|
|
2496
|
+
return json({ ok: true, batch: true, revision: landed.revision, reviewRefs: landed.reviewRefs, decided: proposals.length, sourceSync: landed.sourceSync ?? null });
|
|
2497
|
+
} catch (error) {
|
|
2498
|
+
return json({ ok: false, error: { message: String(error?.message ?? error) } }, 409);
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2201
2501
|
const results = [];
|
|
2202
2502
|
for (const proposalId of ids) {
|
|
2203
2503
|
const proposal = service.workspace.proposal(proposalId);
|
|
@@ -2217,6 +2517,31 @@ export function apply(ctx, rawConfig) {
|
|
|
2217
2517
|
["POST"],
|
|
2218
2518
|
);
|
|
2219
2519
|
|
|
2520
|
+
// F-173:工作台「回滚首稿」——与 Agent 的 deck_draft_rollback 同一条逻辑。
|
|
2521
|
+
registerRoute(
|
|
2522
|
+
ROUTES.draftRollback,
|
|
2523
|
+
async (request) => {
|
|
2524
|
+
if ((request?.method ?? "POST").toUpperCase() !== "POST") return json({ ok: false, error: { message: "POST only" } }, 405);
|
|
2525
|
+
const body = await request.json().catch(() => ({}));
|
|
2526
|
+
const deckId = String(body?.deckId ?? "").trim();
|
|
2527
|
+
if (!deckId) return json({ ok: false, error: { message: "缺少 deckId" } }, 400);
|
|
2528
|
+
if (!service.workspace.manifest(deckId)) return json({ ok: false, error: { message: `未知 deck:${deckId}` } }, 404);
|
|
2529
|
+
const latest = latestAutoland(service.workspace, deckId);
|
|
2530
|
+
if (!latest) return json({ ok: false, error: { message: "没有可回滚的首稿:最新 revision 不是自动落版(或已回滚过)" } }, 409);
|
|
2531
|
+
try {
|
|
2532
|
+
const result = rollbackFirstDraft({ workspace: service.workspace, deckId, note: String(body?.note ?? "(工作台:用户点了「回滚首稿」)") });
|
|
2533
|
+
const text = service.workspace.scriptCurrent(deckId) ?? "";
|
|
2534
|
+
const syncScriptToSource = createScriptToSourceSync({ adapters: service.adapters, isLocalDeck: (id) => service.isLocalDeck(id) });
|
|
2535
|
+
const sourceSync = syncScriptToSource ? syncScriptToSource(deckId, text, null) : null;
|
|
2536
|
+
service.store.recordEvent({ type: "deck.edit", sessionId: null, taskId: null, deckId, slideIds: [], actor: "user", status: "succeeded", payload: { outcome: "autoland-rollback", fromRevision: result.beforeRevision, revision: result.revision } });
|
|
2537
|
+
return json({ ok: true, ...result, ...(sourceSync ? { sourceSync } : {}) });
|
|
2538
|
+
} catch (error) {
|
|
2539
|
+
return json({ ok: false, error: { message: String(error?.message ?? error) } }, 409);
|
|
2540
|
+
}
|
|
2541
|
+
},
|
|
2542
|
+
["POST"],
|
|
2543
|
+
);
|
|
2544
|
+
|
|
2220
2545
|
// 同一路径只能注册一次(connection 按 path 去重):GET 读状态 / POST 切换选择。
|
|
2221
2546
|
registerRoute(
|
|
2222
2547
|
ROUTES.renderModel,
|
|
@@ -2266,6 +2591,19 @@ export function apply(ctx, rawConfig) {
|
|
|
2266
2591
|
});
|
|
2267
2592
|
});
|
|
2268
2593
|
|
|
2594
|
+
// F-188:顶栏「更新」按钮——GET 查 npm 最新版(6s 超时,离线只报错不抛)。
|
|
2595
|
+
registerRoute(ROUTES.updateCheck, async () => json({ ok: true, ...(await service.updateStatus()) }), ["GET"]);
|
|
2596
|
+
|
|
2597
|
+
// F-188:一键更新到最新版(只在 npm 安装模式执行;开发链接安装返回 400 说明原因)。
|
|
2598
|
+
registerRoute(ROUTES.update, async (request) => {
|
|
2599
|
+
if ((request?.method ?? "POST").toUpperCase() !== "POST") return json({ ok: false, error: { message: "POST only" } }, 405);
|
|
2600
|
+
try {
|
|
2601
|
+
return json(await service.runSelfUpdate());
|
|
2602
|
+
} catch (error) {
|
|
2603
|
+
return json({ ok: false, error: { message: String(error?.message ?? error) } }, 400);
|
|
2604
|
+
}
|
|
2605
|
+
}, ["POST"]);
|
|
2606
|
+
|
|
2269
2607
|
registerRoute(ROUTES.faq, (request) => {
|
|
2270
2608
|
const url = new URL(request?.url ?? "/", "http://localhost");
|
|
2271
2609
|
const query = String(url.searchParams.get("q") ?? "").trim();
|
|
@@ -2286,8 +2624,10 @@ export function apply(ctx, rawConfig) {
|
|
|
2286
2624
|
const deckId = url.searchParams.get("deckId") ?? "";
|
|
2287
2625
|
const docId = url.searchParams.get("docId") ?? "";
|
|
2288
2626
|
// F-163:读取失败要把真实原因吐给前端,不能 500 空手而归(前端以前会一直转圈)。
|
|
2627
|
+
// F-187:审稿页的「相关文件」(review.readers.md / review.gap.md / review.experts.md …)可逐个打开。
|
|
2628
|
+
const file = url.searchParams.get("file") ?? "";
|
|
2289
2629
|
try {
|
|
2290
|
-
const doc = service.deckDoc(deckId, docId);
|
|
2630
|
+
const doc = service.deckDoc(deckId, docId, file);
|
|
2291
2631
|
if (!doc) return json({ ok: false, error: { message: `unknown deck or doc:deckId=${deckId || "(空)"} docId=${docId || "(空)"}` } }, 404);
|
|
2292
2632
|
return json({ ok: true, deckId, ...doc });
|
|
2293
2633
|
} catch (error) {
|
|
@@ -2334,7 +2674,7 @@ export function apply(ctx, rawConfig) {
|
|
|
2334
2674
|
if (!toolRuntime?.register) {
|
|
2335
2675
|
ctx.logger?.warn?.(`${name}: config.tools=true 但 tools 服务不可用——deck 工具未注册`);
|
|
2336
2676
|
} else {
|
|
2337
|
-
const tools = defineDeckTools({ store: service.store, workspace: service.workspace, adapters: service.adapters, config, env: process.env, repoRoot: repoRootFrom(PLUGIN_DIR), isLocalDeck: (deckId) => service.isLocalDeck(deckId), resolveRender: (deckId) => service.renderRuntime(deckId), subagents: () => ctx.get("subagents") ?? null });
|
|
2677
|
+
const tools = defineDeckTools({ store: service.store, workspace: service.workspace, adapters: service.adapters, config, env: process.env, repoRoot: repoRootFrom(PLUGIN_DIR), isLocalDeck: (deckId) => service.isLocalDeck(deckId), resolveRender: (deckId) => service.renderRuntime(deckId), createStyle: (payload) => service.createStyleFromDeck(payload), subagents: () => ctx.get("subagents") ?? null });
|
|
2338
2678
|
for (const tool of tools) {
|
|
2339
2679
|
disposers.push(toolRuntime.register(tool));
|
|
2340
2680
|
}
|