dsh-plugin-deck-studio 0.2.8 → 0.2.10
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/README.md +67 -29
- package/index.js +192 -3
- package/lib/render.js +107 -10
- package/lib/tools.js +2 -1
- package/lib/update-check.js +166 -0
- package/package.json +11 -11
- 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/f189-00-before.jpg +0 -0
- package/seeds/docs/assets/screenshots/f189-01-oncanvas-subtitle-claim.jpg +0 -0
- package/seeds/docs/assets/screenshots/f190-01-no-legend.jpg +0 -0
- package/seeds/docs/manual.en.md +17 -0
- package/seeds/docs/manual.md +17 -0
- package/seeds/styles/TEC.json +1 -1
- package/ui/workbench.css +22 -5
- package/ui/workbench.html +189 -5
package/README.md
CHANGED
|
@@ -1,53 +1,91 @@
|
|
|
1
|
-
|
|
1
|
+

|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
# Deckline · 用对话制作整页图片幻灯片
|
|
4
|
+
|
|
5
|
+
**从「讲给谁、讲什么」到脚本、逐页出图、版本与交付,在 DSH 里完成。**
|
|
6
|
+
|
|
7
|
+
Deckline 是 DeepSeek Harness(DSH)的 **image deck 工作台插件**,不是独立应用,也不是可编辑图层的 PPT 编辑器。对话留在 DSH 原生 Chat;工作台负责脚本、图片、设计风格、版本和资料。
|
|
8
|
+
|
|
9
|
+
[安装与配置](../../docs/install.md) · [使用手册](../../docs/manual.md) · [示例 deck](../../samples/deckline-intro) · [English overview](../../README.en.md)
|
|
10
|
+
|
|
11
|
+
## 先看看它怎么工作
|
|
12
|
+
|
|
13
|
+

|
|
14
|
+
|
|
15
|
+
*上图为内置公开示例:中间浏览整页图片,右侧查看与修改这一页的正文、画面要求和提示词。截图展示对应拍摄版本,具体控件以当前版本为准。*
|
|
16
|
+
|
|
17
|
+
### 从想法到可交付的 deck
|
|
18
|
+
|
|
19
|
+
| 你要做什么 | Deckline 提供什么 |
|
|
20
|
+
|---|---|
|
|
21
|
+
| 把想法讲清楚 | 从受众、意图与参考资料开始,组织故事线和逐页脚本 |
|
|
22
|
+
| 做出整页画面 | 按页生成图片;点名多页时可连续出图,每页保留 prompt 与版本 |
|
|
23
|
+
| 调整内容和画面 | 在 Chat 里改稿,查看提案;已有图片可做局部修图,生成新版本而不覆盖旧图 |
|
|
24
|
+
| 统一视觉风格 | 内置风格与样张预览,也可创建自己的 Design、添加参考图并跨 deck 复用 |
|
|
25
|
+
| 检查再交付 | 听众评审、资料与审稿文档;直接播放,或导出 PDF / PPTX / 交接包 |
|
|
26
|
+
|
|
27
|
+
> **导出的 PPTX 每页是一张整页图片,不是可逐项编辑的文本和图表。** 真实出图需要自行配置兼容的 image API;默认 fake 模式用于体验流程。
|
|
28
|
+
|
|
29
|
+
## 写完就能讲:直接全屏播放
|
|
30
|
+
|
|
31
|
+
在工作台点击「全屏演示」,用方向键翻页、Esc 退出;不必为了预览先导出文件。
|
|
32
|
+
|
|
33
|
+

|
|
34
|
+
|
|
35
|
+
## Designs:不只选风格,还能维护自己的风格库
|
|
36
|
+
|
|
37
|
+
预览内置样张、为 deck 选择风格;也可以创建自己的风格,管理画面描述、参考图和样张。这里的 Design 是可复用的视觉定义,**不是固定布局的 PPT 母版**。
|
|
38
|
+
|
|
39
|
+

|
|
40
|
+
|
|
41
|
+

|
|
42
|
+
|
|
43
|
+
更多画面与流程见 [仓库首页](../../README.md)、[风格指南](../../docs/styles.md) 和 [完整示例](../../docs/sample-deck.md)。以上图片均来自公开示例或演示数据。
|
|
5
44
|
|
|
6
45
|
## 安装
|
|
7
46
|
|
|
47
|
+
前置:Node.js `^22.19 或 ≥24`、pnpm 10+、DSH `≥0.2.1-rc.2 <0.3`。详细兼容范围见 [兼容说明](../../docs/compat.md)。
|
|
48
|
+
|
|
8
49
|
```sh
|
|
9
50
|
# 1) 新建一个 profile(基于 DSH 自带 web 模板)
|
|
10
51
|
dsh --profile deckline --from-default-profile web
|
|
11
52
|
|
|
12
|
-
# 2)
|
|
13
|
-
dsh plugin --profile deckline add dsh-plugin-deck-studio@
|
|
53
|
+
# 2) 安装 npm 最新版;安装后自动注册 bundle
|
|
54
|
+
dsh plugin --profile deckline add dsh-plugin-deck-studio@latest
|
|
14
55
|
|
|
15
|
-
#
|
|
16
|
-
npm_config_registry=https://registry.npmmirror.com dsh plugin --profile deckline add dsh-plugin-deck-studio@
|
|
56
|
+
# 国内网络可按需使用镜像,注意镜像版本同步可能有延迟
|
|
57
|
+
# npm_config_registry=https://registry.npmmirror.com dsh plugin --profile deckline add dsh-plugin-deck-studio@latest
|
|
17
58
|
|
|
18
|
-
# 3)
|
|
59
|
+
# 3) 启动,打开终端打印的带 token 链接
|
|
19
60
|
dsh --profile deckline --port 13090
|
|
20
61
|
```
|
|
21
62
|
|
|
22
|
-
|
|
63
|
+
也可以在 DSH「插件 → 发现」中添加索引源:
|
|
23
64
|
|
|
24
|
-
```
|
|
25
|
-
dsh
|
|
65
|
+
```text
|
|
66
|
+
https://cdn.jsdelivr.net/gh/yuna78/dsh-deck-studio@main/market-index.json
|
|
26
67
|
```
|
|
27
68
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
从源码 / Git 安装、Windows、以及完整配置(数据目录 / deck 源 / 出图节点 / key)见仓库文档:
|
|
69
|
+
从源码 / Git 安装、Windows、模型与出图配置及排障见 [中文安装指南](../../docs/install.md) / [English installation](../../docs/install.en.md)。升级后重启 DSH;听众审稿由插件内部调用,不需要单独选择审稿 Mode。
|
|
31
70
|
|
|
32
|
-
|
|
33
|
-
- [docs/install.en.md](https://github.com/yuna78/dsh-deck-studio/blob/main/docs/install.en.md)(English)
|
|
34
|
-
- [示例 deck 与效果](https://github.com/yuna78/dsh-deck-studio/tree/main/samples/deckline-intro)
|
|
71
|
+
## 快速开始
|
|
35
72
|
|
|
36
|
-
|
|
73
|
+
1. 首页点「+ 新建 Deck」,填写标题、受众和一句话意图,选择视觉风格。
|
|
74
|
+
2. 在 Chat 中补充资料、起草故事线与逐页脚本,再按需修改和出图。
|
|
75
|
+
3. 默认 fake 模式生成占位图;真实图片请在「设置」配置出图节点 endpoint、模型与 key。
|
|
76
|
+
4. 在工作台查看页面和版本,直接全屏演示,或导出文件交付。
|
|
37
77
|
|
|
38
|
-
|
|
39
|
-
2. 在 Chat 里让 Agent 起草 `intent.md` / `script.md`,确认后逐页出图;
|
|
40
|
-
3. 默认 fake 模式出占位图,能走完整个流程;要看真图再进「设置」填出图节点 endpoint / 模型 / key。
|
|
78
|
+
出图节点不内置。真实渲染需要配置 OpenAI 兼容的 `/v1/images/generations` 地址。节点 key 保存在本机数据目录的 `render-settings.json`(0600,不回显);不要将 key 提交到 Git。
|
|
41
79
|
|
|
42
|
-
|
|
43
|
-
`~/.dsh/deckline/render-settings.json`,0600,不进 git、不回显)。
|
|
80
|
+
## 数据与边界
|
|
44
81
|
|
|
45
|
-
|
|
82
|
+
- 工作副本、图片、版本与事件默认写 `$DSH_HOME/deckline`,可通过 `dataRoot` 覆盖。
|
|
83
|
+
- 受控源 deck 的 `sourceRoot` **只读**;本机新建 deck 位于 `projectsRoot` 或 `<dataRoot>/projects/<deckId>/source`,有独立写入流程。
|
|
84
|
+
- 每次出图保留版本,旧图片不会因新图生成而被覆盖。
|
|
85
|
+
- 这是 DSH 插件,运行时依赖宿主与用户配置的模型服务;不承诺离线真实出图。
|
|
46
86
|
|
|
47
|
-
|
|
48
|
-
- 受控源 deck:`decks[].sourceRoot` 指向的目录,**只读**,Deckline 永不回写;
|
|
49
|
-
- 新建 deck:`projectsRoot` 或 `<dataRoot>/projects/<deckId>/source`。
|
|
87
|
+
## 许可与反馈
|
|
50
88
|
|
|
51
|
-
|
|
89
|
+
MIT © 2026 Yuna (Deckline) · [LICENSE](../../LICENSE)
|
|
52
90
|
|
|
53
|
-
|
|
91
|
+
[报告问题 / 提建议](https://github.com/yuna78/dsh-deck-studio/issues) · [发布记录](https://github.com/yuna78/dsh-deck-studio/releases)
|
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";
|
|
@@ -33,7 +34,26 @@ import { appendSampleRecord, readSampleRecords, readStyleDefinition, writeStyleD
|
|
|
33
34
|
import { resolveMediaFile, scanDesignStyles, scanResources } from "./lib/home-catalog.js";
|
|
34
35
|
import { defaultSelection, renderModelPayload, resolveSelection } from "./lib/render-nodes.js";
|
|
35
36
|
import { applyRenderSettings, readRenderSettings, renderSettingsPayload, saveRenderSettings } from "./lib/render-settings.js";
|
|
36
|
-
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
|
+
}
|
|
37
57
|
|
|
38
58
|
/** 稳定 Cordis 插件名。 */
|
|
39
59
|
export const name = "deck-studio";
|
|
@@ -51,6 +71,7 @@ export const ROUTES = {
|
|
|
51
71
|
docfile: "/api/deck-studio.docfile",
|
|
52
72
|
export: "/api/deck-studio.export",
|
|
53
73
|
pageEdit: "/api/deck-studio.page-edit",
|
|
74
|
+
pagePrompt: "/api/deck-studio.page-prompt",
|
|
54
75
|
pageSave: "/api/deck-studio.page-save",
|
|
55
76
|
media: "/api/deck-studio.media",
|
|
56
77
|
renderModel: "/api/deck-studio.render-model",
|
|
@@ -78,6 +99,9 @@ export const ROUTES = {
|
|
|
78
99
|
manual: "/api/deck-studio.manual",
|
|
79
100
|
manualDoc: "/api/deck-studio.manual-doc",
|
|
80
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",
|
|
81
105
|
};
|
|
82
106
|
|
|
83
107
|
/** 出图资产 MIME(image deck 只有图片;svg 仅 fake 测试用)。 */
|
|
@@ -198,6 +222,117 @@ function pluginVersion() {
|
|
|
198
222
|
return versionCache;
|
|
199
223
|
}
|
|
200
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
|
+
|
|
201
336
|
function json(payload, status = 200) {
|
|
202
337
|
return Response.json(payload, { status, headers: { "cache-control": "no-store" } });
|
|
203
338
|
}
|
|
@@ -1464,6 +1599,39 @@ export function createService(config, logger, env = process.env) {
|
|
|
1464
1599
|
return pageEditView(adapter, pageId);
|
|
1465
1600
|
}
|
|
1466
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
|
+
|
|
1467
1635
|
/**
|
|
1468
1636
|
* 页面微调保存:写回源 script.md / prompts/Pnn.txt,重建 manifest,并自动 git 提交。
|
|
1469
1637
|
* 这是唯一允许直接写源 deck 的动作(ADR-0006 的例外,见 ADR-0008)。
|
|
@@ -2105,6 +2273,8 @@ export function createService(config, logger, env = process.env) {
|
|
|
2105
2273
|
designProjects: designProjects(),
|
|
2106
2274
|
bindings: listBindings(),
|
|
2107
2275
|
proposals: base.pendingProposals,
|
|
2276
|
+
// F-188:顶栏「更新 / GitHub」按钮首屏数据(不打网络,检查更新另调 update-check)
|
|
2277
|
+
update: updateBrief(),
|
|
2108
2278
|
};
|
|
2109
2279
|
}
|
|
2110
2280
|
|
|
@@ -2114,7 +2284,7 @@ export function createService(config, logger, env = process.env) {
|
|
|
2114
2284
|
bootstrapDesignStyles();
|
|
2115
2285
|
}
|
|
2116
2286
|
|
|
2117
|
-
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, createStyleFromDeck, 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 }
|
|
2118
2288
|
}
|
|
2119
2289
|
|
|
2120
2290
|
/**
|
|
@@ -2191,6 +2361,12 @@ export function apply(ctx, rawConfig) {
|
|
|
2191
2361
|
return view ? json({ ok: true, ...view }) : json({ ok: false, error: { message: "page not found" } }, 404);
|
|
2192
2362
|
});
|
|
2193
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
|
+
|
|
2194
2370
|
registerRoute(ROUTES.pageSave, async (request) => {
|
|
2195
2371
|
const body = await request.json().catch(() => ({}));
|
|
2196
2372
|
const result = service.savePage(body.deckId ?? "", body.pageId ?? "", body.fields ?? body);
|
|
@@ -2415,6 +2591,19 @@ export function apply(ctx, rawConfig) {
|
|
|
2415
2591
|
});
|
|
2416
2592
|
});
|
|
2417
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
|
+
|
|
2418
2607
|
registerRoute(ROUTES.faq, (request) => {
|
|
2419
2608
|
const url = new URL(request?.url ?? "/", "http://localhost");
|
|
2420
2609
|
const query = String(url.searchParams.get("q") ?? "").trim();
|
package/lib/render.js
CHANGED
|
@@ -19,37 +19,125 @@ const SECRET_PATTERNS = [
|
|
|
19
19
|
[/password\s*[=::]\s*\S+/i, "password 赋值"],
|
|
20
20
|
];
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* 从正文里抽「要点」短句(F-189):图上只放短句,不排长段。
|
|
24
|
+
* 切块优先级:空行分段 → ①②③ 编号 → 句号;每块取冒号前的标签 + 后半句短描述,压到 ~28 字。
|
|
25
|
+
* @param {string} body
|
|
26
|
+
* @param {number} [limit]
|
|
27
|
+
* @returns {string[]}
|
|
28
|
+
*/
|
|
29
|
+
/** 把一句压到 limit 字内:优先在标点处断(不出现省略号——画面上的省略号会被当成内容印出来)。 */
|
|
30
|
+
function shortenClause(text, limit = 28) {
|
|
31
|
+
const value = String(text ?? "").trim();
|
|
32
|
+
if (value.length <= limit) return value;
|
|
33
|
+
const cut = value.slice(0, limit);
|
|
34
|
+
let best = -1;
|
|
35
|
+
for (const punct of [",", "、", ";", ":", "。"]) {
|
|
36
|
+
best = Math.max(best, cut.lastIndexOf(punct));
|
|
37
|
+
}
|
|
38
|
+
const base = best >= 10 ? cut.slice(0, best) : cut;
|
|
39
|
+
return base.replace(/[,、;:。]$/u, "").trim();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function bodyKeyPoints(body, limit = 4) {
|
|
43
|
+
const text = String(body ?? "").trim();
|
|
44
|
+
if (!text) return [];
|
|
45
|
+
const blocks = text
|
|
46
|
+
.split(/\n{2,}|[①②③④⑤⑥⑦⑧⑨⑩]/u)
|
|
47
|
+
.map((block) => block.trim())
|
|
48
|
+
.filter(Boolean);
|
|
49
|
+
const points = [];
|
|
50
|
+
for (const block of blocks) {
|
|
51
|
+
const first = block.split(/(?<=[。!?])/u)[0].trim().replace(/[。!?]$/u, "");
|
|
52
|
+
if (!first) continue;
|
|
53
|
+
// 「标签(补充):说明」→ 保留标签 + 说明的首个短句,图上才读得完
|
|
54
|
+
const match = first.match(/^([^:]{1,14}:)\s*(.+)$/u);
|
|
55
|
+
const point = match ? `${match[1]}${shortenClause(match[2], 20)}` : shortenClause(first);
|
|
56
|
+
if (point) points.push(point);
|
|
57
|
+
if (points.length >= limit) break;
|
|
58
|
+
}
|
|
59
|
+
return points;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 「这一页必须在画面上印出的文字」清单(F-189)。
|
|
64
|
+
* 出图模型只认「要印什么」,把字段名换成口语会漏印——所以这里给出逐字清单,供 prompt 与 QA 共用。
|
|
65
|
+
* @param {object} slide
|
|
66
|
+
* @returns {{title:string, subtitle:string, claim:string, bullets:string[]}}
|
|
67
|
+
*/
|
|
68
|
+
export function onImageText(slide) {
|
|
69
|
+
const text = (value) => String(value ?? "").trim();
|
|
70
|
+
const title = text(slide?.title);
|
|
71
|
+
const subtitle = text(slide?.question);
|
|
72
|
+
const claim = text(slide?.claim);
|
|
73
|
+
return {
|
|
74
|
+
title,
|
|
75
|
+
subtitle,
|
|
76
|
+
// 主张超过 40 字就别整句塞画面(读不完),让副标题 + 要点承担信息。
|
|
77
|
+
claim: claim.length > 40 ? `${claim.slice(0, 39)}…` : claim,
|
|
78
|
+
bullets: bodyKeyPoints(slide?.body),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const ON_IMAGE_HEAD = "【这一页必须在画面上印出的文字:逐字照排,不要改写、翻译、缩写或省略】";
|
|
83
|
+
/** 反装饰:图例 / 清单面板 / 英文角标这类「看起来专业但跟本页无关」的元素,模型最容易凭空造。 */
|
|
84
|
+
const CANVAS_DISCIPLINE =
|
|
85
|
+
"画面纪律:只画上面【图上文字】与「画面要求」点名的元素;不要新增图例 / 清单面板 / 装饰性英文标签 / 示意数据 / 与内容无关的图标;同一元素不要既画出来又列进清单(重复两遍);同一信息只写一次,不要用双层括号,也不要既加括注又写注释行。";
|
|
86
|
+
|
|
87
|
+
/** 把清单渲染成 prompt 行(QA 只检查 head 存在与关键行逐字命中)。 */
|
|
88
|
+
function onImageLines(list) {
|
|
89
|
+
const lines = [ON_IMAGE_HEAD];
|
|
90
|
+
if (list.title) lines.push(`主标题:${list.title}`);
|
|
91
|
+
if (list.subtitle) lines.push(`副标题(标题下方一行,字号小于主标题):${list.subtitle}`);
|
|
92
|
+
if (list.claim) lines.push(`一句话主张:${list.claim}`);
|
|
93
|
+
if (list.bullets.length) {
|
|
94
|
+
lines.push("要点(原句只作理解;画面上每条压成 ≤12 字短语贴到对应模块):");
|
|
95
|
+
list.bullets.forEach((point, index) => lines.push(`${index + 1}. ${point}`));
|
|
96
|
+
}
|
|
97
|
+
lines.push("版式与字号预算:主标题最大;副标题紧接其下、约标题的 45% 字号;一句话主张做成通栏一行(字号小于副标题、大于要点)——这三项都必须完整印出,不得省略或用别的话替代。");
|
|
98
|
+
return lines;
|
|
99
|
+
}
|
|
100
|
+
|
|
22
101
|
/**
|
|
23
102
|
* 由 slide 字段 + 全局风格 preset 组 prompt。
|
|
24
103
|
* F-158:封面页单列一套版式纪律(只放标题/副标题/作者/日期,不堆正文);
|
|
25
104
|
* 普通页带上 intent 的受众 / 目的,避免「图很漂亮但讲错人」。
|
|
105
|
+
* F-189:文字不再是「语境」,而是显式「图上文字清单」——副标题 / 主张 / 要点逐字列出,
|
|
106
|
+
* 并声明与 visualBrief 的排他指令冲突时文字优先(否则「画面上只出现 XX 标签」会把副标题吞掉)。
|
|
26
107
|
* @param {object} slide manifest slide
|
|
27
108
|
* @param {string} styleBrief
|
|
28
109
|
* @param {{brief?: object}} [context] readDeckBrief() 的结果
|
|
29
110
|
*/
|
|
30
111
|
export function buildPrompt(slide, styleBrief, context = {}) {
|
|
31
112
|
const brief = context.brief ?? {};
|
|
113
|
+
const list = onImageText(slide);
|
|
32
114
|
const isCover = slide?.type === "cover" || Number(slide?.order) === 1;
|
|
33
115
|
if (isCover) {
|
|
34
|
-
const parts = [styleBrief, "封面页(16:9):只放标题信息",
|
|
35
|
-
if (slide?.question) parts.push(`副标题:${slide.question}`);
|
|
116
|
+
const parts = [styleBrief, "封面页(16:9):只放标题信息", ...onImageLines({ ...list, bullets: [] })];
|
|
36
117
|
parts.push(...coverPromptLines(brief));
|
|
37
118
|
parts.push("版式纪律:只放主标题 / 副标题 / 作者 / 日期;正文不超过一行;不要目录、要点清单、数据图表、大段文字;留白为主。");
|
|
38
119
|
if (slide?.visualBrief) parts.push(`画面要求:${slide.visualBrief}`);
|
|
120
|
+
parts.push(CANVAS_DISCIPLINE);
|
|
121
|
+
parts.push("文字优先:上面【图上文字】的每一行都必须出现在画面里;画面要求与它冲突时,以上面为准。");
|
|
39
122
|
return parts.filter(Boolean).join("\n");
|
|
40
123
|
}
|
|
41
|
-
const parts = [styleBrief,
|
|
42
|
-
|
|
43
|
-
if (
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
124
|
+
const parts = [styleBrief, ...onImageLines(list)];
|
|
125
|
+
const body = String(slide?.body ?? "").trim();
|
|
126
|
+
if (body) parts.push(`讲述用正文(不要整段排进画面,只提炼成上面的要点;用来把握语气与例子):${body.slice(0, 300)}`);
|
|
127
|
+
if (slide?.visualBrief) parts.push(`画面要求:${slide.visualBrief}`);
|
|
128
|
+
parts.push(CANVAS_DISCIPLINE);
|
|
129
|
+
parts.push("文字优先:上面【图上文字】的主标题 / 副标题 / 主张必须完整印出(可换行、可分层,但不得省略、不得改写成别的说法);画面要求若写「只出现 XX」,XX 之外仍要保留这三项与要点短语。");
|
|
47
130
|
parts.push(...briefPromptLines(brief));
|
|
48
131
|
return parts.filter(Boolean).join("\n");
|
|
49
132
|
}
|
|
50
133
|
|
|
51
|
-
/**
|
|
52
|
-
|
|
134
|
+
/**
|
|
135
|
+
* QA 门禁。返回 {ok, checks:[{name, ok, detail}]}。
|
|
136
|
+
* @param {string} prompt 最终 prompt
|
|
137
|
+
* @param {string} size 尺寸
|
|
138
|
+
* @param {object} [slide] 传了就额外检查「图上文字」是否都在 prompt 里(F-189)
|
|
139
|
+
*/
|
|
140
|
+
export function qaCheck(prompt, size, slide) {
|
|
53
141
|
const checks = [];
|
|
54
142
|
const promptOk = typeof prompt === "string" && prompt.trim().length >= 8;
|
|
55
143
|
checks.push({ name: "prompt-exists", ok: promptOk, detail: promptOk ? `${prompt.trim().length} chars` : "prompt 为空或过短" });
|
|
@@ -57,6 +145,15 @@ export function qaCheck(prompt, size) {
|
|
|
57
145
|
checks.push({ name: "size-valid", ok: sizeOk, detail: String(size ?? "") });
|
|
58
146
|
const secretHit = SECRET_PATTERNS.find(([pattern]) => pattern.test(prompt ?? ""));
|
|
59
147
|
checks.push({ name: "no-secrets", ok: !secretHit, detail: secretHit ? `疑似敏感串:${secretHit[1]}` : "clean" });
|
|
148
|
+
if (slide) {
|
|
149
|
+
const list = onImageText(slide);
|
|
150
|
+
const missing = [];
|
|
151
|
+
if (!String(prompt ?? "").includes(ON_IMAGE_HEAD)) missing.push("图上文字清单");
|
|
152
|
+
if (list.title && !String(prompt ?? "").includes(list.title)) missing.push("主标题");
|
|
153
|
+
if (list.subtitle && !String(prompt ?? "").includes(list.subtitle)) missing.push("副标题");
|
|
154
|
+
if (list.claim && !String(prompt ?? "").includes(list.claim)) missing.push("主张");
|
|
155
|
+
checks.push({ name: "on-image-text", ok: missing.length === 0, detail: missing.length ? `prompt 缺:${missing.join(" / ")}` : `${list.title ? "标题" : ""}${list.subtitle ? "+副标题" : ""}${list.claim ? "+主张" : ""}${list.bullets.length ? `+${list.bullets.length} 要点` : ""}` || "clean" });
|
|
156
|
+
}
|
|
60
157
|
return { ok: checks.every((check) => check.ok), checks };
|
|
61
158
|
}
|
|
62
159
|
|
package/lib/tools.js
CHANGED
|
@@ -1214,7 +1214,7 @@ export function defineDeckTools({ store, workspace, adapters, config, env = proc
|
|
|
1214
1214
|
? [linked.brief, linked.constraints ? `不要什么:${linked.constraints}` : ""].filter(Boolean).join("\n")
|
|
1215
1215
|
: config.styleBrief;
|
|
1216
1216
|
const prompt = buildPrompt(slide, styleBrief, { brief: readDeckBrief(renderAdapter.sourceRoot) });
|
|
1217
|
-
const qa = qaCheck(prompt, size);
|
|
1217
|
+
const qa = qaCheck(prompt, size, slide);
|
|
1218
1218
|
if (!qa.ok) {
|
|
1219
1219
|
store.recordEvent({ type: "deck.qa", sessionId, taskId: task.taskId, deckId: deck.id, slideIds: [pageId], actor: "system", status: "failed", payload: { gate: "qa", qa } });
|
|
1220
1220
|
return tryFinish({ status: "failed", error: "qa-gate", value: { taskId: task.taskId, deckId: deck.id, slideIds: [pageId], qa, text: `QA 门禁未通过:${qa.checks.filter((check) => !check.ok).map((check) => `${check.name}(${check.detail})`).join(";")}` } });
|
|
@@ -1380,6 +1380,7 @@ export function defineDeckTools({ store, workspace, adapters, config, env = proc
|
|
|
1380
1380
|
};
|
|
1381
1381
|
try {
|
|
1382
1382
|
const prompt = `# 修图(局部修改,其余保持不变)\n${instruction}\n\n要求:只改点名的地方;不换整页构图;图里的文字保持可读,不要新增没提到的文字或 logo。`;
|
|
1383
|
+
// 修图是「以现有图为底图」的局部指令,不走「图上文字清单」检查(那是整页出图的门禁)。
|
|
1383
1384
|
const qa = qaCheck(prompt, size);
|
|
1384
1385
|
if (!qa.ok) {
|
|
1385
1386
|
store.recordEvent({ type: "deck.qa", sessionId, taskId: task.taskId, deckId: deck.id, slideIds: pageId ? [pageId] : [], actor: "system", status: "failed", payload: { gate: "qa", qa } });
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 插件自更新:版本比较 + 安装方式判断 + 更新命令(F-188)。
|
|
3
|
+
*
|
|
4
|
+
* 背景:插件装好之后没有「检查更新 / 一键更新」入口,用户只能自己去终端 `dsh plugin add`。
|
|
5
|
+
* 工作台顶栏给一个刷新按钮(有新版时带小圆点),点了能看版本差异并直接更新;旁边是仓库入口(提 issue)。
|
|
6
|
+
*
|
|
7
|
+
* 纪律:
|
|
8
|
+
* - **只在 npm 安装模式下执行更新**;本机 `link:` / `file:` 开发安装一律拒绝执行,只解释原因
|
|
9
|
+
* (开发树里跑 pnpm add 会把本地 link 换成 registry 版本,把人正在改的代码顶掉)。
|
|
10
|
+
* - 更新只改 profile 里的依赖,**不改运行中的进程**:跑完提示重启 DSH 才生效。
|
|
11
|
+
* - 版本比较按 semver 的「主.次.补 + 预发」做,不引第三方依赖。
|
|
12
|
+
*
|
|
13
|
+
* @module dsh-plugin-deck-studio/update-check
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
const clamp = (value) => String(value ?? "").trim();
|
|
18
|
+
|
|
19
|
+
/** 解析 "1.2.3-rc.4" → { main:[1,2,3], pre:["rc","4"] };非法返回 null。 */
|
|
20
|
+
export function parseVersion(raw) {
|
|
21
|
+
const text = clamp(raw).replace(/^v/i, "");
|
|
22
|
+
const match = text.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/);
|
|
23
|
+
if (!match) return null;
|
|
24
|
+
return {
|
|
25
|
+
main: [Number(match[1]), Number(match[2]), Number(match[3])],
|
|
26
|
+
pre: match[4] ? match[4].split(".") : [],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** semver 优先级比较:a>b 返回 1,a<b 返回 -1,相等 0(含预发版规则)。非法版本抛错。 */
|
|
31
|
+
export function compareVersions(a, b) {
|
|
32
|
+
const left = parseVersion(a);
|
|
33
|
+
const right = parseVersion(b);
|
|
34
|
+
if (!left) throw new Error(`版本号不合法:${a}`);
|
|
35
|
+
if (!right) throw new Error(`版本号不合法:${b}`);
|
|
36
|
+
for (let i = 0; i < 3; i += 1) {
|
|
37
|
+
if (left.main[i] !== right.main[i]) return left.main[i] > right.main[i] ? 1 : -1;
|
|
38
|
+
}
|
|
39
|
+
// 有预发标记的版本低于同号正式版(0.3.0-rc.1 < 0.3.0)
|
|
40
|
+
if (!left.pre.length && !right.pre.length) return 0;
|
|
41
|
+
if (!left.pre.length) return 1;
|
|
42
|
+
if (!right.pre.length) return -1;
|
|
43
|
+
const size = Math.max(left.pre.length, right.pre.length);
|
|
44
|
+
for (let i = 0; i < size; i += 1) {
|
|
45
|
+
const x = left.pre[i];
|
|
46
|
+
const y = right.pre[i];
|
|
47
|
+
if (x === undefined) return -1;
|
|
48
|
+
if (y === undefined) return 1;
|
|
49
|
+
if (x === y) continue;
|
|
50
|
+
const xn = /^\d+$/.test(x);
|
|
51
|
+
const yn = /^\d+$/.test(y);
|
|
52
|
+
if (xn && yn) return Number(x) > Number(y) ? 1 : -1;
|
|
53
|
+
if (xn) return -1; // 数字标识符优先级低于字母
|
|
54
|
+
if (yn) return 1;
|
|
55
|
+
return x > y ? 1 : -1;
|
|
56
|
+
}
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** latest 比 current 新才返回 true;任一版本号非法时返回 false(宁可不动)。 */
|
|
61
|
+
export function isNewer(latest, current) {
|
|
62
|
+
try {
|
|
63
|
+
return compareVersions(latest, current) > 0;
|
|
64
|
+
} catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** 从 registry 的 packument / version 文档里取 latest 版本(兼容 dist-tags 与单版本文档)。 */
|
|
70
|
+
export function latestFromRegistry(payload) {
|
|
71
|
+
if (!payload || typeof payload !== "object") return "";
|
|
72
|
+
const tagged = clamp(payload["dist-tags"]?.latest);
|
|
73
|
+
if (tagged && parseVersion(tagged)) return tagged; // 坏 tag("abc")不许当版本
|
|
74
|
+
const single = clamp(payload.version);
|
|
75
|
+
return parseVersion(single) ? single : "";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 判断插件是怎么装进 profile 的:
|
|
80
|
+
* - `linked`:`link:` / `file:` / `portal:` 等本地路径 —— 开发模式,更新走 git,不执行 pnpm
|
|
81
|
+
* - `npm`:semver 版本(或 `^` / `~` / `latest` 等 tag)—— 可以 `pnpm add <pkg>@latest`
|
|
82
|
+
* - `unknown`:找不到 profile 或依赖缺失
|
|
83
|
+
*/
|
|
84
|
+
export function installModeOf({ profileDir = "", packageName = "" } = {}) {
|
|
85
|
+
if (!profileDir || !packageName) return { mode: "unknown", spec: "", reason: "拿不到 profile 目录或包名" };
|
|
86
|
+
let pkg;
|
|
87
|
+
try {
|
|
88
|
+
pkg = JSON.parse(readFileSync(join(profileDir, "package.json"), "utf8"));
|
|
89
|
+
} catch {
|
|
90
|
+
return { mode: "unknown", spec: "", reason: `读不到 ${join(profileDir, "package.json")}` };
|
|
91
|
+
}
|
|
92
|
+
const spec = clamp(pkg?.dependencies?.[packageName]);
|
|
93
|
+
if (!spec) return { mode: "unknown", spec: "", reason: `profile 的 dependencies 里没有 ${packageName}` };
|
|
94
|
+
if (/^(link|file|portal|workspace):/i.test(spec) || spec.startsWith(".") || spec.startsWith("/")) {
|
|
95
|
+
return { mode: "linked", spec, reason: "本机是开发链接安装(link:/file:),更新走仓库,不在界面里执行" };
|
|
96
|
+
}
|
|
97
|
+
return { mode: "npm", spec, reason: "" };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 找出插件所在的 profile 目录(更新命令要在那里跑 pnpm):
|
|
102
|
+
* 1. 从插件真实路径往上走,哪一级 package.json 的 dependencies 声明了这个包,哪一级就是 profile
|
|
103
|
+
* —— npm 布局下插件在 `<profile>/node_modules/<pkg>`,这一条就够,不依赖 env;
|
|
104
|
+
* 2. 退而用 env.DSH_PROFILE_DIR(DSH 启动时注入),要求它确实声明了这个依赖;
|
|
105
|
+
* 3. 都没有返回 ""(界面只解释、不执行)。
|
|
106
|
+
*/
|
|
107
|
+
export function detectProfileDir({ pluginDir = "", packageName = "", env = {} } = {}) {
|
|
108
|
+
if (!pluginDir || !packageName) return "";
|
|
109
|
+
const declares = (dir) => {
|
|
110
|
+
try {
|
|
111
|
+
const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
|
|
112
|
+
return Boolean(pkg?.dependencies?.[packageName]);
|
|
113
|
+
} catch {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
let dir = "";
|
|
118
|
+
try {
|
|
119
|
+
dir = realpathSync(pluginDir);
|
|
120
|
+
} catch {
|
|
121
|
+
dir = pluginDir;
|
|
122
|
+
}
|
|
123
|
+
// 逐级向上(最多 6 层,避免走到磁盘根)
|
|
124
|
+
for (let depth = 0; depth < 6; depth += 1) {
|
|
125
|
+
const parent = dirname(dir);
|
|
126
|
+
if (parent === dir) break;
|
|
127
|
+
if (declares(parent)) return parent;
|
|
128
|
+
dir = parent;
|
|
129
|
+
}
|
|
130
|
+
const hinted = clamp(env?.DSH_PROFILE_DIR);
|
|
131
|
+
if (hinted && declares(hinted)) return hinted;
|
|
132
|
+
// 开发链接安装:插件不在 profile 里,扫 DSH_HOME/profiles/*,挑一个确实声明了依赖的
|
|
133
|
+
const root = clamp(env?.DSH_HOME) ? join(clamp(env?.DSH_HOME), "profiles") : "";
|
|
134
|
+
if (root) {
|
|
135
|
+
try {
|
|
136
|
+
const candidates = readdirSync(root)
|
|
137
|
+
.map((entry) => join(root, entry))
|
|
138
|
+
.filter((dir) => declares(dir));
|
|
139
|
+
if (candidates.length) {
|
|
140
|
+
const named = env?.DSH_PROFILE ? candidates.find((dir) => dir.endsWith(`/${env.DSH_PROFILE}`)) : null;
|
|
141
|
+
return named ?? candidates[0];
|
|
142
|
+
}
|
|
143
|
+
} catch {
|
|
144
|
+
/* profiles 目录不存在就算了 */
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return "";
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** 插件包里仓库相关链接(用于顶栏「GitHub」按钮);没有就留空。 */
|
|
151
|
+
export function repoLinksOf(packageJson = {}) {
|
|
152
|
+
const repo = packageJson.repository;
|
|
153
|
+
const raw = typeof repo === "string" ? repo : clamp(repo?.url);
|
|
154
|
+
const url = raw
|
|
155
|
+
.replace(/^git\+/, "")
|
|
156
|
+
.replace(/^git@github\.com:/, "https://github.com/")
|
|
157
|
+
.replace(/\.git$/, "");
|
|
158
|
+
const homepage = clamp(packageJson.homepage) || url;
|
|
159
|
+
const bugs = typeof packageJson.bugs === "string" ? packageJson.bugs : clamp(packageJson.bugs?.url);
|
|
160
|
+
const issues = bugs ? bugs.replace(/\/+$/, "") : url ? `${url}/issues` : "";
|
|
161
|
+
const suggest = issues.endsWith("/issues") ? `${issues}/new/choose` : url ? `${url}/issues/new/choose` : "";
|
|
162
|
+
return { repo: url, homepage, issues, suggest };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
import { readFileSync, readdirSync, realpathSync } from "node:fs";
|
|
166
|
+
import { dirname, join } from "node:path";
|
package/package.json
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-deck-studio",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"private": false,
|
|
3
|
+
"version": "0.2.10",
|
|
5
4
|
"description": "Deckline:DSH 的 image deck / PPT 工作台插件——原生 Chat + 整页图片 deck 工作区 + 设计风格库;用对话做 PPT、按页出图、版本可追踪、导出 PDF/PPTX。",
|
|
5
|
+
"homepage": "https://github.com/yuna78/dsh-deck-studio#readme",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/yuna78/dsh-deck-studio.git",
|
|
9
|
+
"directory": "plugin/deck-studio"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/yuna78/dsh-deck-studio/issues"
|
|
13
|
+
},
|
|
14
|
+
"private": false,
|
|
6
15
|
"license": "MIT",
|
|
7
16
|
"type": "module",
|
|
8
17
|
"main": "./index.js",
|
|
@@ -52,15 +61,6 @@
|
|
|
52
61
|
]
|
|
53
62
|
}
|
|
54
63
|
},
|
|
55
|
-
"repository": {
|
|
56
|
-
"type": "git",
|
|
57
|
-
"url": "git+https://github.com/yuna78/dsh-deck-studio.git",
|
|
58
|
-
"directory": "plugin/deck-studio"
|
|
59
|
-
},
|
|
60
|
-
"homepage": "https://github.com/yuna78/dsh-deck-studio#readme",
|
|
61
|
-
"bugs": {
|
|
62
|
-
"url": "https://github.com/yuna78/dsh-deck-studio/issues"
|
|
63
|
-
},
|
|
64
64
|
"peerDependencies": {
|
|
65
65
|
"@deepseek-ai/dsh": ">=0.2.1-rc.2 <0.3.0"
|
|
66
66
|
},
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/seeds/docs/manual.en.md
CHANGED
|
@@ -292,3 +292,20 @@ fabrication, explicit confirmation); suspicious instructions are flagged, never
|
|
|
292
292
|
| `exports/` | exported PDF / PPTX / handoff bundles. |
|
|
293
293
|
|
|
294
294
|
`.deck/` and `exports/` are gitignored; the repository only ships code, docs, samples and preset styles.
|
|
295
|
+
## 13. Updating the plugin & giving feedback
|
|
296
|
+
|
|
297
|
+
Three always-on buttons live in the top-right of the workbench header:
|
|
298
|
+
|
|
299
|
+
| Button | What it does |
|
|
300
|
+
|---|---|
|
|
301
|
+
| **⚙** | Jumps to the settings panel (render node / model / API key source / size); from the home page it returns to the current deck first |
|
|
302
|
+
| **↻ vX.Y.Z** | Checks npm for the latest release: up to date → tells you the version; newer → asks "current vX → vY", then runs `dsh plugin add` for you (restart DSH to apply). A `link:` / `file:` dev install is never auto-updated — it explains to pull the repo instead, so your local link is not replaced by a registry copy |
|
|
303
|
+
| **GitHub** | Opens the repo's new-issue page (feature requests / bug reports) |
|
|
304
|
+
|
|
305
|
+
Command-line equivalent:
|
|
306
|
+
|
|
307
|
+
```sh
|
|
308
|
+
dsh plugin --profile deckline add dsh-plugin-deck-studio@latest
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
If npm is unreachable the button says so ("check timed out") instead of hanging.
|
package/seeds/docs/manual.md
CHANGED
|
@@ -256,3 +256,20 @@ key 值只存在本机数据文件里,任何接口都不会回显。
|
|
|
256
256
|
| `exports/` | 导出产物(PDF / PPTX / 交接包)。 |
|
|
257
257
|
|
|
258
258
|
`.deck/` 与 `exports/` 都在 `.gitignore` 里;仓库只带代码、文档、示例与预设风格。
|
|
259
|
+
## 13. 更新插件与反馈
|
|
260
|
+
|
|
261
|
+
工作台顶栏右上角三个常驻按钮,装好之后不用记命令:
|
|
262
|
+
|
|
263
|
+
| 按钮 | 行为 |
|
|
264
|
+
|---|---|
|
|
265
|
+
| **⚙** | 直接切到右栏「设置」页(出图节点 / 模型 / key 来源 / 尺寸);在首页时先回到当前 deck 再切 |
|
|
266
|
+
| **↻ vX.Y.Z** | 点一下查 npm 最新版:已是最新 → 提示版本号;有新版 → 弹「当前 vX → vY」确认,点「更新」直接跑 `dsh plugin add`,完成后**重启 DSH 生效**;本机 `link:` / `file:` 开发安装只解释「更新走仓库」,不会把本地链接换成 registry 包 |
|
|
267
|
+
| **GitHub** | 打开仓库的「新建 issue」页(提建议 / 报问题) |
|
|
268
|
+
|
|
269
|
+
命令行的等价写法:
|
|
270
|
+
|
|
271
|
+
```sh
|
|
272
|
+
dsh plugin --profile deckline add dsh-plugin-deck-studio@latest
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
不上网时点 ↻ 会明确提示「检查更新超时:连不上 npm registry」,不会卡住界面。
|
package/seeds/styles/TEC.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"description": "科技蓝图风:深海军蓝网格底、青白技术线条、等距系统/架构图、等宽字体标签、发光节点;像工程蓝图和架构白皮书。",
|
|
5
5
|
"constraints": "不要照片、不要卡通、不要渐变光效泛滥、不要彩色斑块、不要写实人物、不要真实品牌 logo。",
|
|
6
6
|
"reference": "架构白皮书 / 工程蓝图",
|
|
7
|
-
"styleBrief": "科技蓝图 / 等距架构图:深海军蓝底 #0A1628,叠加极淡青色网格(#1E3A5F);主体是等距(isometric)线框模块:节点、服务器、数据库、云、客户端,用青白色 #7DD3FC 细线与发光边,节点间正交折线连接 + 小箭头;标签用等宽字体(monospace
|
|
7
|
+
"styleBrief": "科技蓝图 / 等距架构图:深海军蓝底 #0A1628,叠加极淡青色网格(#1E3A5F);主体是等距(isometric)线框模块:节点、服务器、数据库、云、客户端,用青白色 #7DD3FC 细线与发光边,节点间正交折线连接 + 小箭头;标签用等宽字体(monospace)全大写小字。图例 / 清单面板只在画面里确实有多种连线或符号需要解释时才加,且只列本页出现过的元素,不要为填充版面编造图例项。点缀极少量暖色 #F59E0B 强调关键节点。整体像工程蓝图 / 架构白皮书,信息密度高但网格秩序强;无照片、无卡通、无廉价霓虹。",
|
|
8
8
|
"baselineBriefs": [
|
|
9
9
|
{
|
|
10
10
|
"title": "架构总览",
|
package/ui/workbench.css
CHANGED
|
@@ -83,11 +83,16 @@
|
|
|
83
83
|
.tool-line.err{border-left-color:#c0533f;background:#faf0ed;color:#8c3a2c;white-space:normal}
|
|
84
84
|
|
|
85
85
|
/* 8 份必备文档(中间区标签) */
|
|
86
|
-
.doc-head{display:flex;
|
|
87
|
-
.doc-head
|
|
88
|
-
.doc-head
|
|
89
|
-
|
|
90
|
-
.doc-head .doc-
|
|
86
|
+
.doc-head{display:flex;flex-direction:column;gap:6px;margin-bottom:10px}
|
|
87
|
+
.doc-head-top{display:flex;align-items:flex-start;gap:12px;flex-wrap:wrap}
|
|
88
|
+
.doc-head-top h2{margin:6px 0 0;font-family:"Noto Serif SC",Georgia,serif;font-size:22px;color:var(--ink)}
|
|
89
|
+
.doc-head-side{margin-left:auto;display:flex;flex-direction:column;align-items:flex-end;gap:5px;text-align:right}
|
|
90
|
+
.doc-head .doc-file{color:#8a9599;font-size:10.5px}
|
|
91
|
+
/* F-187:审稿页的相关文件切换(听众读后感 / 意图差距 / 工程口径 …)
|
|
92
|
+
F-188 后续:一轮评审有 5 个 chip,跟标题挤一行会右对齐折成两截,改成独立一行、左对齐。 */
|
|
93
|
+
.doc-head-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-top:2px}
|
|
94
|
+
.doc-head .doc-files{margin:0;display:flex;gap:6px;flex-wrap:wrap;justify-content:flex-start;max-width:100%}
|
|
95
|
+
.doc-head-extra{display:flex;align-items:center;gap:8px;flex-wrap:wrap;justify-content:flex-end}
|
|
91
96
|
.doc-file-chip{border:1px solid #d7dee0;background:#fff;color:#5a6a70;border-radius:999px;padding:3px 10px;font-size:10.5px;cursor:pointer}
|
|
92
97
|
.doc-file-chip:hover{border-color:#0f6f7a;color:#0f6f7a}
|
|
93
98
|
.doc-file-chip.active{background:#0f6f7a;border-color:#0f6f7a;color:#fff}
|
|
@@ -690,3 +695,15 @@ body.presenting{overflow:hidden}
|
|
|
690
695
|
.source-title .doc-status{margin-left:8px;font-size:9px;font-weight:400;vertical-align:2px}
|
|
691
696
|
.doc-status.optional{color:#7a7f7c;background:#efece3;border:1px solid #e0dbcd;border-radius:8px;padding:0 6px;line-height:16px}
|
|
692
697
|
.source-hint{font-size:10px;color:#8b8f8b;margin-top:3px;line-height:1.5}
|
|
698
|
+
|
|
699
|
+
/* F-188:顶栏「更新 / GitHub」小按钮 + 临时提示(顶栏是深色,沿用 .top button 的浅色底) */
|
|
700
|
+
.top .dl-icon-btn{position:relative;display:inline-flex;align-items:center;gap:5px;padding:6px 9px;cursor:pointer;font:inherit;font-size:12px;line-height:1}
|
|
701
|
+
.top .dl-icon-btn:hover{background:#ffffff2e}
|
|
702
|
+
.top .dl-icon-btn:active{transform:translateY(1px)}
|
|
703
|
+
.top .dl-icon-btn.busy svg{animation:dl-spin 1s linear infinite}
|
|
704
|
+
.top .dl-icon-btn svg{flex:0 0 auto;opacity:.85}
|
|
705
|
+
.top .dl-icon-ver{opacity:.72;font-size:11px;font-variant-numeric:tabular-nums}
|
|
706
|
+
.top .dl-icon-dot{position:absolute;top:3px;right:4px;width:7px;height:7px;border-radius:50%;background:#e8b45c;box-shadow:0 0 0 2px var(--ink)}
|
|
707
|
+
.dl-toast{position:fixed;left:50%;top:66px;transform:translateX(-50%) translateY(-6px);z-index:80;max-width:min(560px,90vw);padding:9px 14px;border:1px solid var(--line);border-radius:10px;background:#fffdf7;color:#2c2a26;font-size:13px;line-height:1.5;box-shadow:0 10px 28px rgba(0,0,0,.18);opacity:0;pointer-events:none;transition:opacity .16s ease,transform .16s ease;white-space:pre-wrap}
|
|
708
|
+
.dl-toast.show{opacity:1;transform:translateX(-50%) translateY(0)}
|
|
709
|
+
@keyframes dl-spin{to{transform:rotate(360deg)}}
|
package/ui/workbench.html
CHANGED
|
@@ -92,6 +92,19 @@
|
|
|
92
92
|
<span class="project">AI Image Deck Studio</span>
|
|
93
93
|
<span class="spacer"></span>
|
|
94
94
|
<span id="save">DSH 会话 · 连接中…</span>
|
|
95
|
+
<!-- F-188:更新 / 仓库 / 设置入口(点刷新查 npm 最新版,有新版带小圆点) -->
|
|
96
|
+
<button id="settingsBtn" class="dl-icon-btn" title="设置(出图节点 / 模型 / 尺寸)" aria-label="设置">
|
|
97
|
+
<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true"><path fill="currentColor" d="M19.4 13a7.7 7.7 0 0 0 0-2l2-1.6-2-3.4-2.4 1a7.7 7.7 0 0 0-1.7-1L15 3.5h-4l-.3 2.5a7.7 7.7 0 0 0-1.7 1l-2.4-1-2 3.4 2 1.6a7.7 7.7 0 0 0 0 2l-2 1.6 2 3.4 2.4-1a7.7 7.7 0 0 0 1.7 1l.3 2.5h4l.3-2.5a7.7 7.7 0 0 0 1.7-1l2.4 1 2-3.4-2-1.6zM12 15.2A3.2 3.2 0 1 1 12 8.8a3.2 3.2 0 0 1 0 6.4z"/></svg>
|
|
98
|
+
</button>
|
|
99
|
+
<button id="updateBtn" class="dl-icon-btn" title="检查更新" aria-label="检查更新">
|
|
100
|
+
<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true"><path fill="currentColor" d="M12 4V1.5L7.5 6l4.5 4.5V8a5 5 0 1 1-5 5H5a7 7 0 1 0 7-9z"/></svg>
|
|
101
|
+
<span class="dl-icon-ver" id="updateVer">—</span>
|
|
102
|
+
<i class="dl-icon-dot" id="updateDot" hidden></i>
|
|
103
|
+
</button>
|
|
104
|
+
<button id="repoBtn" class="dl-icon-btn" title="GitHub 仓库 / 提建议" aria-label="GitHub 仓库">
|
|
105
|
+
<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true"><path fill="currentColor" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg>
|
|
106
|
+
</button>
|
|
107
|
+
<div id="dlToast" class="dl-toast" hidden></div>
|
|
95
108
|
<button id="shellSidebarBtn" title="显示 / 收起 DSH 侧栏(会话 · 插件 · 设置)" hidden>☰ DSH</button>
|
|
96
109
|
</header>
|
|
97
110
|
|
|
@@ -1044,6 +1057,137 @@ async function postJson(routeName, body) {
|
|
|
1044
1057
|
return data;
|
|
1045
1058
|
}
|
|
1046
1059
|
|
|
1060
|
+
/** GET 一个插件路由(失败抛后端 message)。 */
|
|
1061
|
+
async function getJson(routeName) {
|
|
1062
|
+
const res = await fetch("/api/deck-studio." + routeName, { headers: { accept: "application/json" } });
|
|
1063
|
+
const data = await res.json().catch(() => ({}));
|
|
1064
|
+
if (!res.ok || data?.ok === false) throw new Error(data?.error?.message || "HTTP " + res.status);
|
|
1065
|
+
return data;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
/** 顶栏临时提示(不打断操作)。 */
|
|
1069
|
+
let DL_TOAST_TIMER = null;
|
|
1070
|
+
function dlToast(text, ms = 4200) {
|
|
1071
|
+
const box = getEl("dlToast");
|
|
1072
|
+
if (!box) return;
|
|
1073
|
+
box.textContent = text;
|
|
1074
|
+
box.hidden = false;
|
|
1075
|
+
box.classList.add("show");
|
|
1076
|
+
clearTimeout(DL_TOAST_TIMER);
|
|
1077
|
+
DL_TOAST_TIMER = setTimeout(() => {
|
|
1078
|
+
box.classList.remove("show");
|
|
1079
|
+
box.hidden = true;
|
|
1080
|
+
}, ms);
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
/** F-188:更新状态(版本 / 安装方式 / 仓库链接)由 workbench payload 带下来。 */
|
|
1084
|
+
const UPDATE = { current: "", latest: "", hasUpdate: false, installMode: "", repo: "", suggest: "", note: "" };
|
|
1085
|
+
|
|
1086
|
+
function paintUpdate() {
|
|
1087
|
+
const ver = getEl("updateVer");
|
|
1088
|
+
if (ver) ver.textContent = UPDATE.current ? `v${UPDATE.current}` : "—";
|
|
1089
|
+
const dot = getEl("updateDot");
|
|
1090
|
+
if (dot) dot.hidden = !UPDATE.hasUpdate;
|
|
1091
|
+
const btn = getEl("updateBtn");
|
|
1092
|
+
if (btn) {
|
|
1093
|
+
btn.title = UPDATE.hasUpdate
|
|
1094
|
+
? `有新版 v${UPDATE.latest}(当前 v${UPDATE.current})`
|
|
1095
|
+
: UPDATE.installMode === "linked"
|
|
1096
|
+
? "本机是开发链接安装:更新走 git pull"
|
|
1097
|
+
: "检查更新";
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
function absorbUpdate(payload) {
|
|
1102
|
+
const brief = payload?.update ?? {};
|
|
1103
|
+
if (brief.currentVersion) UPDATE.current = brief.currentVersion;
|
|
1104
|
+
if (brief.installMode) UPDATE.installMode = brief.installMode;
|
|
1105
|
+
if (brief.repo) UPDATE.repo = brief.repo;
|
|
1106
|
+
if (brief.suggest) UPDATE.suggest = brief.suggest;
|
|
1107
|
+
if (brief.issues) UPDATE.issues = brief.issues;
|
|
1108
|
+
paintUpdate();
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
async function runPluginUpdate() {
|
|
1112
|
+
try {
|
|
1113
|
+
const data = await postJson("update", {});
|
|
1114
|
+
UPDATE.current = data.to || UPDATE.current;
|
|
1115
|
+
UPDATE.hasUpdate = false;
|
|
1116
|
+
paintUpdate();
|
|
1117
|
+
openConfirm({
|
|
1118
|
+
title: `已更新到 v${data.to}`,
|
|
1119
|
+
desc: `${data.restartHint || "重启 DSH 后生效。"}\n\n${data.output ? String(data.output).slice(-600) : ""}`,
|
|
1120
|
+
okLabel: "知道了",
|
|
1121
|
+
onOk: null,
|
|
1122
|
+
});
|
|
1123
|
+
} catch (error) {
|
|
1124
|
+
dlToast(String(error?.message || error), 8000);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
/** 点刷新:先查 npm 最新版,再决定是提示「已是最新」、解释开发链接、还是弹更新确认。 */
|
|
1129
|
+
async function checkPluginUpdate() {
|
|
1130
|
+
const btn = getEl("updateBtn");
|
|
1131
|
+
if (btn) btn.classList.add("busy");
|
|
1132
|
+
try {
|
|
1133
|
+
const data = await getJson("update-check");
|
|
1134
|
+
UPDATE.latest = data.latestVersion || "";
|
|
1135
|
+
UPDATE.hasUpdate = Boolean(data.hasUpdate);
|
|
1136
|
+
UPDATE.installMode = data.installMode || UPDATE.installMode;
|
|
1137
|
+
if (data.note) UPDATE.note = data.note;
|
|
1138
|
+
paintUpdate();
|
|
1139
|
+
if (data.error) {
|
|
1140
|
+
dlToast(data.error, 6000);
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
if (!UPDATE.hasUpdate) {
|
|
1144
|
+
dlToast(`已是最新版本 v${data.currentVersion}`);
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
if (data.installMode === "linked") {
|
|
1148
|
+
dlToast(data.note || "本机是开发链接安装,更新走仓库(git pull)。", 7000);
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
if (data.installMode !== "npm") {
|
|
1152
|
+
dlToast("没识别出 profile 的安装方式,无法一键更新;请手动 dsh plugin add。", 7000);
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
openConfirm({
|
|
1156
|
+
title: `更新到 v${data.latestVersion}`,
|
|
1157
|
+
desc: `当前 v${data.currentVersion} → v${data.latestVersion}。会在 profile 里执行 pnpm add,下载完成后重启 DSH 生效。`,
|
|
1158
|
+
okLabel: "更新",
|
|
1159
|
+
onOk: runPluginUpdate,
|
|
1160
|
+
});
|
|
1161
|
+
} catch (error) {
|
|
1162
|
+
dlToast(`检查更新失败:${String(error?.message || error)}`, 6000);
|
|
1163
|
+
} finally {
|
|
1164
|
+
if (btn) btn.classList.remove("busy");
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
/** 顶栏齿轮:直接切到右栏「设置」(首页时先回到当前 deck 的工作台)。 */
|
|
1169
|
+
async function openSettingsPanel() {
|
|
1170
|
+
const deckId = STATE.data?.active?.deckId;
|
|
1171
|
+
if (!deckId) {
|
|
1172
|
+
dlToast("先在 Decks 里打开一本 deck,右栏就有设置(出图节点 / 模型 / 尺寸)", 6000);
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
if (STATE.view === "home") await showDeck(deckId);
|
|
1176
|
+
activateRightTab("settings");
|
|
1177
|
+
const panel = getEl("settings");
|
|
1178
|
+
if (panel) panel.scrollIntoView({ block: "start" });
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
/** 打开仓库 / 提建议页(新标签)。 */
|
|
1182
|
+
function openRepo() {
|
|
1183
|
+
const url = UPDATE.suggest || UPDATE.issues || UPDATE.repo;
|
|
1184
|
+
if (!url) {
|
|
1185
|
+
dlToast("插件包里没配 repository 链接", 5000);
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
window.open(url, "_blank", "noopener");
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1047
1191
|
let CONFIRM_OK = null;
|
|
1048
1192
|
|
|
1049
1193
|
/** 通用确认弹层(删除等破坏性操作用)。 */
|
|
@@ -1407,12 +1551,24 @@ function emptyDoc(doc) {
|
|
|
1407
1551
|
}
|
|
1408
1552
|
|
|
1409
1553
|
function docHeader(doc, extra = "") {
|
|
1554
|
+
// 只有「多个可切换的文档」才出 chip:目录(prompts/ 之类)、不存在的相关文件都不算。
|
|
1555
|
+
// 以前脚本页右侧也挂着 script.md / prompts/ 两个 chip,看着像能切,其实没内容可切。
|
|
1556
|
+
const switchable = (doc.files || []).filter((file) => !String(file.name || "").endsWith("/") && file.exists !== false);
|
|
1557
|
+
const chips = switchable.length > 1
|
|
1558
|
+
? `<div class="doc-files">${switchable.map((file) => `<button type="button" class="doc-file-chip${file.name === doc.primary ? " active" : ""}" data-doc-file="${esc(file.name)}" title="${esc(file.name)}">${esc(file.name.replace(/^docs\//, ""))}</button>`).join("")}</div>`
|
|
1559
|
+
: "";
|
|
1560
|
+
// 两行:第一行标题 + 徽标,右上角是主文件路径与草案说明 / 动作;第二行只有文件 chips(左对齐,可换行)。
|
|
1561
|
+
// 一轮审稿会有 review.md / readers / gap / experts / images / scores 六个 chip,跟标题挤一行会被挤到右侧折成两截,很难读。
|
|
1410
1562
|
return `<div class="doc-head">
|
|
1411
|
-
<div
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1563
|
+
<div class="doc-head-top">
|
|
1564
|
+
<div><div class="eyebrow">Deck Document</div><h2>${esc(doc.label)}</h2></div>
|
|
1565
|
+
${docBadge(doc.status)}
|
|
1566
|
+
<span class="doc-head-side">
|
|
1567
|
+
<span class="small doc-file">${esc(doc.primary || "无文件")}</span>
|
|
1568
|
+
${extra ? `<span class="doc-head-extra">${extra}</span>` : ""}
|
|
1569
|
+
</span>
|
|
1570
|
+
</div>
|
|
1571
|
+
${chips ? `<div class="doc-head-row">${chips}</div>` : ""}
|
|
1416
1572
|
</div>`;
|
|
1417
1573
|
}
|
|
1418
1574
|
|
|
@@ -1650,6 +1806,7 @@ function renderDoc(doc, index) {
|
|
|
1650
1806
|
})()}
|
|
1651
1807
|
${bodyText ? `<div class="page-content md${placeholderContent ? " is-placeholder" : ""}">${placeholderContent ? esc(bodyText) : mdToHtml(bodyText)}</div><p class="density-line">${densityBadge("body", bodyText)}</p>` : `<p class="engine-note">(这页没有正文)</p>`}
|
|
1652
1808
|
<details class="prompt-details"><summary>提示词(${placeholderPrompt ? "待补" : `${String(promptText).length} 字符`}${densityTarget && !placeholderPrompt ? ` · 目标 ${densityTarget.briefMin}–${densityTarget.briefMax} 字` : ""})</summary><div class="prompt-quote">${esc(promptText || "(缺提示词)")}${placeholderPrompt ? pendingHint(["visualBrief"]) : ""}</div></details>
|
|
1809
|
+
<details class="prompt-details" data-assembled-prompt="${esc(slide.id)}"><summary>出图时拼装的完整 prompt(含图上文字清单,展开时读取)</summary><div class="prompt-quote" data-assembled-body="${esc(slide.id)}">展开读取…</div></details>
|
|
1653
1810
|
${proposalBlock}
|
|
1654
1811
|
</div></div>`;
|
|
1655
1812
|
}).join("")}</div>` + docFooter(doc, content);
|
|
@@ -1929,6 +2086,29 @@ async function rollbackFirstDraft(revision) {
|
|
|
1929
2086
|
}
|
|
1930
2087
|
|
|
1931
2088
|
function bindDocActions(root) {
|
|
2089
|
+
root.querySelectorAll("details[data-assembled-prompt]").forEach((details) => {
|
|
2090
|
+
details.addEventListener("toggle", async () => {
|
|
2091
|
+
if (!details.open) return;
|
|
2092
|
+
const pageId = details.getAttribute("data-assembled-prompt");
|
|
2093
|
+
const body = details.querySelector("[data-assembled-body]");
|
|
2094
|
+
if (!pageId || !body || body.dataset.loaded === "1") return;
|
|
2095
|
+
const deckId = STATE.data?.active?.deckId;
|
|
2096
|
+
if (!deckId) return;
|
|
2097
|
+
body.textContent = "读取中…";
|
|
2098
|
+
try {
|
|
2099
|
+
const res = await fetch(`/api/deck-studio.page-prompt?` + new URLSearchParams({ deckId, pageId }), { headers: { accept: "application/json" } });
|
|
2100
|
+
const data = await res.json().catch(() => ({}));
|
|
2101
|
+
if (res.ok && data?.ok && data.prompt) {
|
|
2102
|
+
body.textContent = data.prompt;
|
|
2103
|
+
body.dataset.loaded = "1";
|
|
2104
|
+
} else {
|
|
2105
|
+
body.textContent = `读取失败:${data?.error?.message || "未知错误"}`;
|
|
2106
|
+
}
|
|
2107
|
+
} catch (error) {
|
|
2108
|
+
body.textContent = `读取失败:${String(error?.message ?? error)}`;
|
|
2109
|
+
}
|
|
2110
|
+
});
|
|
2111
|
+
});
|
|
1932
2112
|
root.querySelectorAll("[data-doc-file]").forEach((btn) => btn.addEventListener("click", () => {
|
|
1933
2113
|
const file = btn.getAttribute("data-doc-file") || "";
|
|
1934
2114
|
if (file) openDocFile(STATE.tab, file);
|
|
@@ -4678,6 +4858,10 @@ function backToManual(box) {
|
|
|
4678
4858
|
}
|
|
4679
4859
|
getEl("embedBack").addEventListener("click", () => window.parent?.postMessage({ type: "dsh-deck-studio:close" }, "*"));
|
|
4680
4860
|
await loadWorkbench();
|
|
4861
|
+
absorbUpdate(STATE.data);
|
|
4862
|
+
getEl("settingsBtn").addEventListener("click", openSettingsPanel);
|
|
4863
|
+
getEl("updateBtn").addEventListener("click", checkPluginUpdate);
|
|
4864
|
+
getEl("repoBtn").addEventListener("click", openRepo);
|
|
4681
4865
|
await refreshSessions();
|
|
4682
4866
|
getEl("save").textContent = EMBED ? "对话在 DSH 原生 Chat" : "对话在左侧 · DSH 原生";
|
|
4683
4867
|
// F-69:只有 profile 开了 hideShellSidebar,宿主左侧栏才会默认收起;此时顶栏给一个切换入口。
|