dsh-plugin-deck-studio 0.2.4 → 0.2.6
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/client.js +85 -10
- package/cordis.example.yml +6 -23
- package/cordis.patch.yml +7 -23
- package/index.js +27 -8
- package/lib/config.js +16 -9
- package/lib/review-agent.js +168 -0
- package/lib/review-personas.js +10 -1
- package/lib/tools.js +19 -15
- package/package.json +3 -3
- package/seeds/docs/assets/screenshots/07-new-deck.png +0 -0
- package/seeds/docs/assets/screenshots/08-presentation.png +0 -0
- package/seeds/docs/assets/screenshots/09-style-library.png +0 -0
- package/seeds/docs/assets/screenshots/10-style-management.png +0 -0
- package/seeds/docs/assets/screenshots/manual/07-new-deck.png +0 -0
- package/seeds/docs/compat.md +4 -4
- package/seeds/docs/install.en.md +17 -13
- package/seeds/docs/install.md +17 -12
- package/seeds/docs/styles.en.md +3 -3
- package/seeds/docs/styles.md +3 -3
- package/ui/workbench.html +77 -18
- package/seeds/docs/productization.en.md +0 -212
- package/seeds/docs/productization.md +0 -193
package/lib/tools.js
CHANGED
|
@@ -19,6 +19,7 @@ import { readDeckRules, renderDeckRules } from "./deck-rules.js";
|
|
|
19
19
|
import { readDeckBrief } from "./deck-brief.js";
|
|
20
20
|
import { scriptStyleHints } from "./script-style.js";
|
|
21
21
|
import { DEFAULT_MAX_REVIEWERS, HARD_MAX_REVIEWERS, deriveReviewers, mergeIssues, parseReviewerOutput, renderReviewMarkdown, reviewerPrompt } from "./review-personas.js";
|
|
22
|
+
import { REVIEW_AGENT_PROVIDER } from "./review-agent.js";
|
|
22
23
|
import { densityLine } from "./content-density.js";
|
|
23
24
|
import { faqIndex, searchFaq } from "./faq.js";
|
|
24
25
|
import { WATERMARK_POSITIONS, WATERMARK_POSITION_LABELS, findAsset, listAssets, positionLine, readPngFile, watermarkPng } from "./watermark.js";
|
|
@@ -138,7 +139,7 @@ const strEnum = (values) => ({ type: "string", enum: values });
|
|
|
138
139
|
* @param {import('./config.js').mergeConfig} services.config
|
|
139
140
|
* @param {NodeJS.ProcessEnv} [services.env]
|
|
140
141
|
*/
|
|
141
|
-
export function defineDeckTools({ store, workspace, adapters, config, env = process.env, resolveRender = null, isLocalDeck = null, repoRoot = null, subagents = null
|
|
142
|
+
export function defineDeckTools({ store, workspace, adapters, config, env = process.env, resolveRender = null, isLocalDeck = null, repoRoot = null, subagents = null }) {
|
|
142
143
|
const resolveSubagents = () => (typeof subagents === "function" ? subagents() : subagents);
|
|
143
144
|
const designs = createDesignRegistry({ dataRoot: config.dataRoot });
|
|
144
145
|
const decksRegistryFile = join(config.dataRoot, "decks-registry.json");
|
|
@@ -2085,8 +2086,8 @@ export function defineDeckTools({ store, workspace, adapters, config, env = proc
|
|
|
2085
2086
|
reviewers: derived.reviewers,
|
|
2086
2087
|
issues: [],
|
|
2087
2088
|
text: textOnly(
|
|
2088
|
-
"当前 profile 没有挂 subagent 服务(@deepseek-ai/dsh-subagent
|
|
2089
|
-
"
|
|
2089
|
+
"当前 profile 没有挂 subagent 服务(@deepseek-ai/dsh-subagent),无法启动插件内置审稿人。\n" +
|
|
2090
|
+
"请启用 DSH 子 Agent 服务后重试,不需要选择或安装审稿模式。",
|
|
2090
2091
|
),
|
|
2091
2092
|
};
|
|
2092
2093
|
}
|
|
@@ -2110,6 +2111,7 @@ export function defineDeckTools({ store, workspace, adapters, config, env = proc
|
|
|
2110
2111
|
else exec.signal?.addEventListener?.("abort", onAbort, { once: true });
|
|
2111
2112
|
let timer;
|
|
2112
2113
|
let activation;
|
|
2114
|
+
const outcome = { reviewer, label: reviewer.label };
|
|
2113
2115
|
const timeout = new Promise((_resolve, reject) => {
|
|
2114
2116
|
timer = setTimeout(() => {
|
|
2115
2117
|
controller.abort();
|
|
@@ -2118,7 +2120,7 @@ export function defineDeckTools({ store, workspace, adapters, config, env = proc
|
|
|
2118
2120
|
});
|
|
2119
2121
|
try {
|
|
2120
2122
|
activation = await runtime.startActivation({
|
|
2121
|
-
provider:
|
|
2123
|
+
provider: REVIEW_AGENT_PROVIDER,
|
|
2122
2124
|
label: `deck-review:${deck.id}:${reviewer.id}`,
|
|
2123
2125
|
request: {
|
|
2124
2126
|
prompt: [
|
|
@@ -2134,26 +2136,32 @@ export function defineDeckTools({ store, workspace, adapters, config, env = proc
|
|
|
2134
2136
|
},
|
|
2135
2137
|
],
|
|
2136
2138
|
parent: exec.agent,
|
|
2137
|
-
...(reviewPreset || config.review?.presetId ? { agentPreset: reviewPreset || config.review?.presetId } : {}),
|
|
2138
2139
|
},
|
|
2139
2140
|
signal: controller.signal,
|
|
2140
2141
|
delivery: "caller",
|
|
2141
2142
|
});
|
|
2142
2143
|
const result = await Promise.race([activation.result, timeout]);
|
|
2144
|
+
if (controller.signal.aborted) throw new Error("听众评审已取消或超时");
|
|
2145
|
+
if (result?.stopReason !== "completed") throw new Error(`审稿人未正常完成(${result?.stopReason ?? "unknown"})`);
|
|
2143
2146
|
const text = activationResultText(result);
|
|
2144
2147
|
const parsed = parseReviewerOutput(text);
|
|
2145
|
-
|
|
2148
|
+
if (parsed.unparsed) throw new Error("审稿人未返回符合契约的 JSON 意见");
|
|
2149
|
+
if (parsed.issues.some((issue) => !script.includes(issue.evidence))) {
|
|
2150
|
+
throw new Error("审稿意见的证据不是稿件原文,已拒绝采纳");
|
|
2151
|
+
}
|
|
2152
|
+
Object.assign(outcome, { parsed, text, stopReason: result.stopReason });
|
|
2146
2153
|
} catch (error) {
|
|
2147
|
-
|
|
2154
|
+
outcome.error = error instanceof Error ? error.message : String(error);
|
|
2148
2155
|
} finally {
|
|
2149
2156
|
clearTimeout(timer);
|
|
2150
2157
|
exec.signal?.removeEventListener?.("abort", onAbort);
|
|
2151
2158
|
try {
|
|
2152
2159
|
await activation?.dispose?.();
|
|
2153
2160
|
} catch {
|
|
2154
|
-
|
|
2161
|
+
outcome.error = "审稿执行清理失败,请检查 DSH 子任务状态";
|
|
2155
2162
|
}
|
|
2156
2163
|
}
|
|
2164
|
+
return outcome;
|
|
2157
2165
|
};
|
|
2158
2166
|
|
|
2159
2167
|
// 审稿人并发跑:每个都是一次独立子 agent,互不影响;失败一个不拖垮整份评审。
|
|
@@ -2171,12 +2179,8 @@ export function defineDeckTools({ store, workspace, adapters, config, env = proc
|
|
|
2171
2179
|
const generatedAt = new Date().toISOString();
|
|
2172
2180
|
const target = join(deckDir, "docs", "review.experts.md");
|
|
2173
2181
|
let written = false;
|
|
2174
|
-
if (args.write !== false) {
|
|
2182
|
+
if (args.write !== false && !exec.signal?.aborted) {
|
|
2175
2183
|
mkdirSync(dirname(target), { recursive: true });
|
|
2176
|
-
const unparsed = ok
|
|
2177
|
-
.filter((outcome) => outcome.parsed.unparsed)
|
|
2178
|
-
.map((outcome) => `### ${outcome.label}\n\n${outcome.text.slice(0, 4000)}`)
|
|
2179
|
-
.join("\n\n");
|
|
2180
2184
|
const body = [
|
|
2181
2185
|
renderReviewMarkdown({
|
|
2182
2186
|
deckTitle: deck.title,
|
|
@@ -2186,7 +2190,6 @@ export function defineDeckTools({ store, workspace, adapters, config, env = proc
|
|
|
2186
2190
|
generatedAt,
|
|
2187
2191
|
failed,
|
|
2188
2192
|
}),
|
|
2189
|
-
...(unparsed ? ["", "## 未按 JSON 输出、原文保留", "", unparsed] : []),
|
|
2190
2193
|
].join("\n");
|
|
2191
2194
|
writeFileSync(target, body.endsWith("\n") ? body : `${body}\n`, "utf8");
|
|
2192
2195
|
written = true;
|
|
@@ -2213,7 +2216,7 @@ export function defineDeckTools({ store, workspace, adapters, config, env = proc
|
|
|
2213
2216
|
});
|
|
2214
2217
|
const lines = issues.length
|
|
2215
2218
|
? issues.map((issue) => `${issue.severity} · ${issue.page || "—"} · ${issue.problem} → ${issue.suggestion}${issue.reviewer ? `(${issue.reviewer})` : ""}`)
|
|
2216
|
-
: ["
|
|
2219
|
+
: [ok.length ? "已完成评审,未发现有原文证据支持的可操作问题。" : "本次没有有效评审结果;不能据此判断稿件没有问题。"];
|
|
2217
2220
|
const failureLine = failed.length
|
|
2218
2221
|
? `\n${failed.length} 位审稿人失败:${failed.map((item) => `${item.label}(${item.error})`).join(" / ")}`
|
|
2219
2222
|
: "";
|
|
@@ -2222,6 +2225,7 @@ export function defineDeckTools({ store, workspace, adapters, config, env = proc
|
|
|
2222
2225
|
reviewers: derived.reviewers.map((reviewer) => ({ id: reviewer.id, label: reviewer.label, audience: reviewer.audience })),
|
|
2223
2226
|
issues,
|
|
2224
2227
|
bySeverity,
|
|
2228
|
+
failed,
|
|
2225
2229
|
written,
|
|
2226
2230
|
text: `听众评审(${derived.reviewers.map((reviewer) => reviewer.label).join(" / ")}):P0 ${bySeverity.P0} / P1 ${bySeverity.P1} / P2 ${bySeverity.P2}${failureLine}\n${lines.join("\n")}${written ? `\n已写入:${target}` : "\n(dry run,未写文件)"}\n这些是评审意见,不是已改稿:按 P0/P1 用 deck_script_propose_edits 提案,等用户确认后再落版。`,
|
|
2227
2231
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-deck-studio",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Deckline:DSH 的 image deck / PPT 工作台插件——原生 Chat + 整页图片 deck 工作区 + 设计风格库;用对话做 PPT、按页出图、版本可追踪、导出 PDF/PPTX。",
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
],
|
|
31
31
|
"engines": {
|
|
32
32
|
"node": "^22.19.0 || >=24.0.0",
|
|
33
|
-
"dsh": ">=0.1.
|
|
33
|
+
"dsh": ">=0.2.1-rc.2 <0.3.0"
|
|
34
34
|
},
|
|
35
35
|
"scripts": {
|
|
36
36
|
"verify": "node verify/smoke.mjs",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"url": "https://github.com/yuna78/dsh-deck-studio/issues"
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|
|
64
|
-
"@deepseek-ai/dsh": ">=0.
|
|
64
|
+
"@deepseek-ai/dsh": ">=0.2.1-rc.2 <0.3.0"
|
|
65
65
|
},
|
|
66
66
|
"author": "Yuna",
|
|
67
67
|
"files": [
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/seeds/docs/compat.md
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
# DSH 兼容矩阵(定期回归记录)
|
|
2
2
|
|
|
3
3
|
> 口径:DSH 处于 prerelease 阶段,没有 stable 线;本项目**定期批次**回归,不逐 rc 跟。
|
|
4
|
-
>
|
|
4
|
+
> 每个批次记录:证实的版本、验证范围、断点、复现方式。已知限制见 [install.md](install.md) 的故障排查部分。
|
|
5
5
|
|
|
6
6
|
## 当前支持声明
|
|
7
7
|
|
|
8
8
|
| 项 | 值 | 说明 |
|
|
9
9
|
|---|---|---|
|
|
10
|
-
| `peerDependencies["@deepseek-ai/dsh"]` | `>=0.
|
|
11
|
-
| `engines.dsh` | `>=0.1.
|
|
10
|
+
| `peerDependencies["@deepseek-ai/dsh"]` | `>=0.2.1-rc.2 <0.3.0` | 装到 0.3+ 会被 DSH 的 peer 检查拦下 |
|
|
11
|
+
| `engines.dsh` | `>=0.2.1-rc.2 <0.3.0` | 插件0.2.6起:内置审稿使用0.2.1 API,旧DSH请升级或保留插件0.2.5 |
|
|
12
12
|
| `engines.node` | `^22.19.0 \|\| >=24.0.0` | 与 DSH 运行时要求一致;**Node 23 不在范围内** |
|
|
13
|
-
| 已验证版本 | `0.1
|
|
13
|
+
| 已验证版本 | `0.2.1-rc.2`(0.2.6:内置审稿真实服务19项、产品模型评审;此前版本还有0.1.7旧基线) | 历史批次不代表本版继续支持0.1.7 |
|
|
14
14
|
|
|
15
15
|
## 回归记录
|
|
16
16
|
|
package/seeds/docs/install.en.md
CHANGED
|
@@ -10,11 +10,13 @@ assets / conversations on the right. It is not a standalone app — it runs on a
|
|
|
10
10
|
|
|
11
11
|
## 0. Prerequisites
|
|
12
12
|
|
|
13
|
+
Plugin 0.2.6+ requires DSH 0.2.1-rc.2 or a newer 0.2.x release. Restart DSH after upgrading. Remove any manually declared `preset-deck-critic` entry from your profile; legacy `review.presetId` is ignored. Reviews now run internally without selecting another mode.
|
|
14
|
+
|
|
13
15
|
| Dependency | Version | Notes |
|
|
14
16
|
|---|---|---|
|
|
15
17
|
| Node.js | `^22.19` or `≥ 24` | Same as the DSH runtime; Node 23 is not supported |
|
|
16
18
|
| pnpm | 10+ | DSH uses pnpm to manage profile dependencies |
|
|
17
|
-
| DSH | `≥ 0.1.
|
|
19
|
+
| DSH | `≥ 0.2.1-rc.2 < 0.3` (plugin 0.2.6+) | `npm i -g @deepseek-ai/dsh`; verified on 0.2.1-rc.2, see [compat.md](compat.md) |
|
|
18
20
|
| An LLM provider key | — | e.g. `DEEPSEEK_API_KEY` / `OPENAI_API_KEY` (configured in the profile, see DSH docs) |
|
|
19
21
|
| An image node key | — | For real renders; the fake mode lets you try the flow first (section 4) |
|
|
20
22
|
|
|
@@ -77,16 +79,18 @@ The minimum (append to the profile's `cordis.patch.yml`):
|
|
|
77
79
|
config:
|
|
78
80
|
displayName: 'Deckline'
|
|
79
81
|
userName: 'Yuna'
|
|
80
|
-
#
|
|
81
|
-
projectsRoot: '/absolute/path/to/your/
|
|
82
|
+
# root for new deck source files; empty = <dataRoot>/projects/<deck-id>/source
|
|
83
|
+
projectsRoot: '/absolute/path/to/your/decks'
|
|
84
|
+
# workspacePath: '/absolute/path/to/your/workspace' # DSH chat workspace, not storage
|
|
82
85
|
decks:
|
|
83
86
|
- id: my-deck
|
|
84
87
|
label: 'My deck'
|
|
85
88
|
sourceRoot: '/absolute/path/to/my-deck'
|
|
86
89
|
```
|
|
87
90
|
|
|
88
|
-
> `decks[].sourceRoot` is read-only: Deckline never writes to
|
|
89
|
-
> revisions live
|
|
91
|
+
> `decks[].sourceRoot` is a managed source and always read-only: Deckline never writes to it; working copies,
|
|
92
|
+
> revisions and renders live under `dataRoot`. A locally created deck is different: after user confirmation,
|
|
93
|
+
> script / intent source files may be written back to that local project directory. You can also start with **+ New Deck** if you have no deck yet.
|
|
90
94
|
|
|
91
95
|
### Per-deck project rules (optional)
|
|
92
96
|
|
|
@@ -105,8 +109,7 @@ Drop a `deck.rules.md` at the root of a deck project to describe the creative co
|
|
|
105
109
|
- It **cannot override** the product's hard rules (read-only source decks / QA gates / no fabrication /
|
|
106
110
|
user confirmation). In a conflict the hard rules win and the agent says so.
|
|
107
111
|
- Max 8 KB; it is never written back and never triggers any automatic action.
|
|
108
|
-
- Do not name it `AGENTS.md`: that belongs to DSH workspace instructions and Deckline does not depend on it
|
|
109
|
-
(see `docs/adr/0020` in the development repo).
|
|
112
|
+
- Do not name it `AGENTS.md`: that belongs to DSH workspace instructions and Deckline does not depend on it.
|
|
110
113
|
|
|
111
114
|
## 2.5 Where the data lives: source deck / documents / images / exports
|
|
112
115
|
|
|
@@ -126,14 +129,15 @@ creating a deck, check this table first.
|
|
|
126
129
|
- **`dataRoot` defaults to `$DSH_HOME/deckline`** (usually `~/.dsh/deckline`) — never inside the plugin
|
|
127
130
|
package, so plugin upgrades do not touch it. Override with `dataRoot: '/your/path'` in the profile patch,
|
|
128
131
|
or the `DSH_DECK_DATA_ROOT` env var.
|
|
129
|
-
- **`projectsRoot`** decides where
|
|
130
|
-
-
|
|
132
|
+
- **`projectsRoot`** decides where locally created deck source files live; if unset they go to `<dataRoot>/projects/<deckId>/source`. It does not select the chat workspace.
|
|
133
|
+
- **`workspacePath`** selects the DSH workspace used for conversation binding; it is independent of `projectsRoot` / `dataRoot`. If unset, Deckline resolves it from the startup and source directories.
|
|
134
|
+
- **Managed source decks are read-only**: Deckline never writes to a deck registered via `decks[].sourceRoot`; a confirmed local deck may write back to its own source files.
|
|
131
135
|
- **Backup**: archive `dataRoot` and `projectsRoot` together (documents + images + events):
|
|
132
136
|
```sh
|
|
133
137
|
tar czf deckline-backup-$(date +%Y%m%d).tgz "$DSH_HOME/deckline" "<your projectsRoot>"
|
|
134
138
|
```
|
|
135
|
-
- The workbench **Settings** panel shows the current data directory;
|
|
136
|
-
is
|
|
139
|
+
- The workbench **Settings** panel shows the current data directory; keep `dataRoot` outside the source tree
|
|
140
|
+
in normal installs so runtime data is portable and plugin upgrades cannot overwrite it.
|
|
137
141
|
|
|
138
142
|
## 3. Start
|
|
139
143
|
|
|
@@ -217,7 +221,7 @@ bottom of the preview (paste / pick files work the same way).
|
|
|
217
221
|
|
|
218
222
|
Samples live under `designs/<ABBR>/` (ABBR is your style abbreviation):
|
|
219
223
|
|
|
220
|
-
- canvas directory (recommended):
|
|
224
|
+
- canvas directory (recommended): `<dataRoot>/projects/<canvasDeckId>/source/designs/<ABBR>/`;
|
|
221
225
|
- or `designs/<ABBR>/` inside any managed deck source (Deckline scans and merges them).
|
|
222
226
|
|
|
223
227
|
Refresh the page afterwards. png / jpg / webp are supported; names are free-form
|
|
@@ -236,7 +240,7 @@ Swiss minimal / aurora gradient), each with samples. See [styles.md](styles.md).
|
|
|
236
240
|
| Port already in use | `dsh --profile deckline --port 13100`; or kill the old process with `lsof -tiTCP:13090 -sTCP:LISTEN` |
|
|
237
241
|
| Page 404 / blank | Make sure the plugin bundle is enabled: `dsh --profile deckline --dump-config \| grep deck-studio` |
|
|
238
242
|
| No "render" button / render fails | Check the key state in Settings; confirm `render.mode: real`; `curl` the node endpoint |
|
|
239
|
-
| New decks land in the wrong place | Set `projectsRoot`; otherwise they go to
|
|
243
|
+
| New decks land in the wrong place | Set `projectsRoot`; otherwise they go to `<dataRoot>/projects/` (by default `<DSH_HOME>/deckline/projects/`) |
|
|
240
244
|
| Where are data / samples | See section 2.5: source decks in `projectsRoot`, working copies / renders in `dataRoot` (default `~/.dsh/deckline`) |
|
|
241
245
|
| Upgrading the plugin | `dsh plugin --profile deckline install <spec>`, or `add` the new version again |
|
|
242
246
|
|
package/seeds/docs/install.md
CHANGED
|
@@ -11,11 +11,13 @@ Deckline(`dsh-plugin-deck-studio`)是 DeepSeek Harness(DSH)的 **image d
|
|
|
11
11
|
|
|
12
12
|
## 0. 前置条件
|
|
13
13
|
|
|
14
|
+
插件0.2.6起需要 DSH 0.2.1-rc.2 或更新的0.2.x;升级后重启 DSH。若手写 profile 仍声明 `preset-deck-critic`,请移除;旧 `review.presetId` 已忽略,评审改由插件内部调度,不需选审稿模式。
|
|
15
|
+
|
|
14
16
|
| 依赖 | 版本 | 说明 |
|
|
15
17
|
|---|---|---|
|
|
16
18
|
| Node.js | `^22.19` 或 `≥ 24` | 与 DSH 运行时一致;Node 23 不在支持范围 |
|
|
17
19
|
| pnpm | 10+ | DSH 用 pnpm 管理 profile 依赖 |
|
|
18
|
-
| DSH | `≥ 0.1.
|
|
20
|
+
| DSH | `≥ 0.2.1-rc.2 < 0.3`(插件0.2.6起) | **用 npm 全局安装的发行版**:`npm i -g @deepseek-ai/dsh@0.2.1-rc.2`;已验证 0.2.1-rc.2,见 [compat.md](compat.md)。从源码自编译的 dsh 若缺内置包(如 `@deepseek-ai/dsh-agent-preset`)会启动失败,见 4.3 |
|
|
19
21
|
| 一个 LLM provider key | — | 例:`DEEPSEEK_API_KEY` / `OPENAI_API_KEY`(profile 里配置,见 DSH 文档) |
|
|
20
22
|
| 一个 image 节点 key | — | 生成整页图用;可先用 fake 模式体验(见第 4 节) |
|
|
21
23
|
|
|
@@ -111,15 +113,17 @@ Deckline 的配置都在 **profile patch 层**:`~/.dsh/profiles/deckline/cordi
|
|
|
111
113
|
config:
|
|
112
114
|
displayName: 'Deckline'
|
|
113
115
|
userName: '' # 留空 = 首页只显示「欢迎你」;填了就显示「XX,欢迎你」
|
|
114
|
-
# 新建 deck
|
|
115
|
-
projectsRoot: '/absolute/path/to/your/
|
|
116
|
+
# 新建 deck 源文件根:留空 = <dataRoot>/projects/<deck-id>/source
|
|
117
|
+
projectsRoot: '/absolute/path/to/your/decks'
|
|
118
|
+
# workspacePath: '/absolute/path/to/your/workspace' # DSH 会话工作区,不是保存位置
|
|
116
119
|
decks:
|
|
117
120
|
- id: my-deck
|
|
118
121
|
label: '我的 deck'
|
|
119
122
|
sourceRoot: '/absolute/path/to/my-deck'
|
|
120
123
|
```
|
|
121
124
|
|
|
122
|
-
> `decks[].sourceRoot`
|
|
125
|
+
> `decks[].sourceRoot` 是受控源,始终只读:Deckline 不会写入它;编辑工作副本、版本和出图落在 `dataRoot`。
|
|
126
|
+
> 通过「+ 新建 Deck」创建的本机 deck 是另一种情况:用户确认后,script / intent 等源文件可以回写到该本机项目目录。
|
|
123
127
|
> 没有现成 deck 也可以先用「+ 新建 Deck」建一个。
|
|
124
128
|
|
|
125
129
|
### 每个 deck 的项目规则(可选)
|
|
@@ -137,7 +141,7 @@ Deck Agent 每次 `deck_source_overview` 时会读到并遵守:
|
|
|
137
141
|
- 只约束**这一份 deck**,不影响其他 deck;不写就等于没有规则(可选能力)。
|
|
138
142
|
- 它**不能覆盖**产品硬纪律(客户源只读 / QA 门禁 / 不编造 / 需用户确认)——冲突时以硬纪律为准,Agent 会说明。
|
|
139
143
|
- 上限 8 KB;读取时不会回写,也不会因为文件内容自动执行任何操作。
|
|
140
|
-
- 不叫 `AGENTS.md`:`AGENTS.md` 属于 DSH 的工作区指令,Deckline
|
|
144
|
+
- 不叫 `AGENTS.md`:`AGENTS.md` 属于 DSH 的工作区指令,Deckline 不依赖它。
|
|
141
145
|
|
|
142
146
|
## 2.5 数据在哪里:源 deck / 文档 / 图片 / 导出
|
|
143
147
|
|
|
@@ -155,13 +159,14 @@ Deckline 的数据分两处:**你的 deck 源**(`projectsRoot`,纯 md +
|
|
|
155
159
|
|
|
156
160
|
- **`dataRoot` 默认 `$DSH_HOME/deckline`**(通常是 `~/.dsh/deckline`)——不在插件包里,升级插件不会覆盖;
|
|
157
161
|
可在 profile patch 配 `dataRoot: '/your/path'`,或环境变量 `DSH_DECK_DATA_ROOT` 覆盖。
|
|
158
|
-
- **`projectsRoot`**
|
|
159
|
-
-
|
|
162
|
+
- **`projectsRoot`** 决定本机新建 deck 的源文件落在哪;不配置时落在 `<dataRoot>/projects/<deckId>/source`。它不决定聊天会话绑定的工作区。
|
|
163
|
+
- **`workspacePath`** 只决定会话绑定哪个 DSH workspace,与 `projectsRoot` / `dataRoot` 相互独立;未配置时插件按启动目录和源目录自动选择工作区。
|
|
164
|
+
- **受控源 deck 只读**:`decks[].sourceRoot` 指向的已有 deck 目录,插件永远不写;本机新建 deck 在用户确认后可回写自己的源文件。
|
|
160
165
|
- **备份**:把 `dataRoot` 和 `projectsRoot` 一起打包即可(文档 + 图 + 事件都在里面):
|
|
161
166
|
```sh
|
|
162
167
|
tar czf deckline-backup-$(date +%Y%m%d).tgz "$DSH_HOME/deckline" "<你的 projectsRoot>"
|
|
163
168
|
```
|
|
164
|
-
-
|
|
169
|
+
- 工作台「设置」面板会显示当前数据目录;开发环境也建议使用独立的 `dataRoot`,不要把运行数据写进仓库。
|
|
165
170
|
- **preset 工具实例要和根挂载用同一个数据根**(F-98):DSH 面板路由与 Agent 工具是两个实例,根挂载里覆盖了
|
|
166
171
|
`dataRoot` / `projectsRoot` 时,preset 的 `deck-studio-tools` 也要写同一组值(推荐 YAML 锚点),
|
|
167
172
|
否则新建 deck 只进一边的注册表,Agent 会报「未知 deck:…」。
|
|
@@ -194,7 +199,7 @@ Deckline 支持:
|
|
|
194
199
|
3. 填:**模式**(fake / real)、**生图节点 endpoint**、**模型**、**key 环境变量名**(或 **key 文件路径**)、**出图尺寸**;
|
|
195
200
|
4. 点 **保存并生效**。
|
|
196
201
|
|
|
197
|
-
保存后写入本机数据目录
|
|
202
|
+
保存后写入本机数据目录 `<dataRoot>/render-settings.json`(默认 `~/.dsh/deckline/render-settings.json`;
|
|
198
203
|
已 gitignore):节点可直存 API key(0600,界面只显示「已保存」,payload 不回显),也可改用变量名 / key 文件;
|
|
199
204
|
下次启动自动生效,并覆盖 profile 里的 render 配置。
|
|
200
205
|
|
|
@@ -252,7 +257,7 @@ export DSH_DECK_IMAGE_API_KEY="sk-…" # 只放在你的 shell / 密钥
|
|
|
252
257
|
- 没配 key / 仍是 fake 模式时,设置面板会直接给出提示。
|
|
253
258
|
- 模型名填你的中转实际开放的模型;常见 OpenAI 兼容名 `gpt-image-2` 系列的模型 id 以节点 `/v1/models` 为准。
|
|
254
259
|
- 工作台「设置 → 出图节点」可以直接切换线路,也能**新增 / 编辑 / 删除节点**:填 名称 + 接口地址 + API Key +
|
|
255
|
-
模型名即可(key 存本机
|
|
260
|
+
模型名即可(key 存本机 `<dataRoot>/render-settings.json`,0600,不进 git;也可改用 env / key 文件);
|
|
256
261
|
切换写当前 deck 的 `render-selection.json`,下一次出图生效,不用重启。
|
|
257
262
|
- **修图**(`deck_image_edit`)需要节点支持 `POST /v1/images/edits`(multipart:`image` + `prompt` + `size` + `model`)。
|
|
258
263
|
回车节点通常和出图同源;不填 `editEndpoint` 就按上面的规则派生,派生不出来(自定义路径)时工具会明确报错、不猜地址。
|
|
@@ -273,7 +278,7 @@ export DSH_DECK_IMAGE_API_KEY="sk-…" # 只放在你的 shell / 密钥
|
|
|
273
278
|
|
|
274
279
|
样张存在 `designs/<ABBR>/` 下(ABBR 是你的风格缩写):
|
|
275
280
|
|
|
276
|
-
-
|
|
281
|
+
- 画布目录(推荐):`<dataRoot>/projects/<canvasDeckId>/source/designs/<ABBR>/`;
|
|
277
282
|
- 或任何受控 deck 源里的 `designs/<ABBR>/`(Deckline 会自动扫描并合并展示)。
|
|
278
283
|
|
|
279
284
|
放完刷新页面即可。支持 png / jpg / webp;命名随意(`sample-01.jpg` 之类便于排序)。
|
|
@@ -290,7 +295,7 @@ Designs 页 **+ 导入预设风格** 会一次导入 8 个自带风格(商
|
|
|
290
295
|
| 端口被占 | `dsh --profile deckline --port 13100`;或 `lsof -tiTCP:13090 -sTCP:LISTEN` 杀掉旧进程 |
|
|
291
296
|
| 页面 404 / 白屏 | 确认插件组合包已启用:`dsh --profile deckline --dump-config \| grep deck-studio` |
|
|
292
297
|
| 没有「生图」按钮 / 出图失败 | 设置面板看 key 是否配置;确认 `render.mode: real`;节点 endpoint 可 `curl` 通 |
|
|
293
|
-
| 新 deck 目录落错位置 | 配 `projectsRoot
|
|
298
|
+
| 新 deck 目录落错位置 | 配 `projectsRoot`;否则落在 `<dataRoot>/projects/`(默认 `<DSH_HOME>/deckline/projects/`) |
|
|
294
299
|
| 数据 / 样张在哪 | 见 §2.5:源 deck 在 `projectsRoot`,工作副本 / 出图在 `dataRoot`(默认 `~/.dsh/deckline`) |
|
|
295
300
|
| 升级插件 | `dsh plugin --profile deckline install <spec>`,或重新 `add` 新版本 |
|
|
296
301
|
|
package/seeds/docs/styles.en.md
CHANGED
|
@@ -42,8 +42,8 @@ bottom of the preview (paste / pick files work the same way).
|
|
|
42
42
|
### Way 2: drop files directly
|
|
43
43
|
|
|
44
44
|
Put images under `designs/<ABBR>/` (canvas directory
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
`<dataRoot>/projects/<canvasDeckId>/source/designs/<ABBR>/`). Existing `designs/<ABBR>/` directories in managed sources are also scanned read-only.
|
|
46
|
+
Prefer uploading new samples through the canvas rather than modifying managed sources. png / jpg / webp are supported; refresh the page afterwards.
|
|
47
47
|
|
|
48
48
|
Fields of `style.json` (the plugin also writes one automatically):
|
|
49
49
|
|
|
@@ -104,7 +104,7 @@ node scripts/f74-regenerate-presets.mjs --only=BIZ --limit=1
|
|
|
104
104
|
```
|
|
105
105
|
|
|
106
106
|
What the script does: sync `styleBrief` from `seeds/styles/<ABBR>.json` into the online design's
|
|
107
|
-
`style.json` → back up old samples to
|
|
107
|
+
`style.json` → back up old samples to `<dataRoot>/tmp/f74-old-samples/<ABBR>/` → render one image per
|
|
108
108
|
`baselineBriefs` entry (prompt recorded in `samples.jsonl`) → downscale and write back into `seeds/samples/`.
|
|
109
109
|
Refresh the Designs page afterwards; no service restart needed.
|
|
110
110
|
|
package/seeds/docs/styles.md
CHANGED
|
@@ -37,8 +37,8 @@ Designs 页 → **+ 导入预设风格**:一次导入下面 8 个风格(
|
|
|
37
37
|
|
|
38
38
|
### 方式 2:直接放文件
|
|
39
39
|
|
|
40
|
-
把图片放进 `designs/<ABBR>/` 目录(画布目录
|
|
41
|
-
|
|
40
|
+
把图片放进 `designs/<ABBR>/` 目录(画布目录 `<dataRoot>/projects/<canvasDeckId>/source/designs/<ABBR>/`,
|
|
41
|
+
插件也会只读扫描已有源目录中的 `designs/<ABBR>/`)。新增样张优先通过画布上传,避免修改受控源。支持 png / jpg / webp,刷新页面即可。
|
|
42
42
|
|
|
43
43
|
`style.json` 的字段(插件也会自动写一份):
|
|
44
44
|
|
|
@@ -94,7 +94,7 @@ node scripts/f74-regenerate-presets.mjs --only=BIZ --limit=1
|
|
|
94
94
|
```
|
|
95
95
|
|
|
96
96
|
脚本做的事:把 `seeds/styles/<ABBR>.json` 的 `styleBrief` 同步进线上 design 的 `style.json` →
|
|
97
|
-
旧样张备份到
|
|
97
|
+
旧样张备份到 `<dataRoot>/tmp/f74-old-samples/<ABBR>/` → 按 `baselineBriefs` 逐张出图(prompt 落 `samples.jsonl`)→
|
|
98
98
|
预览回写 `seeds/samples/`。重制后刷新 Designs 页即可看到新样张;不用重启服务。
|
|
99
99
|
|
|
100
100
|
> 样张里的文字是示意(图像模型会糊字),不承诺逐字复刻;也不要把真实客户资料、品牌 logo 放进样例图。
|
package/ui/workbench.html
CHANGED
|
@@ -130,7 +130,7 @@
|
|
|
130
130
|
<button class="modal-close" id="ndClose" title="关闭">✕</button>
|
|
131
131
|
</div>
|
|
132
132
|
<form class="modal-body newdeck-body" id="newDeckForm">
|
|
133
|
-
<p class="nd-tip">先定「讲给谁、讲什么、想带走什么」+ 视觉风格。创建后会开出源目录与 9
|
|
133
|
+
<p class="nd-tip">先定「讲给谁、讲什么、想带走什么」+ 视觉风格。创建后会开出源目录与 9 份文档骨架,然后在对话里继续补资料、改 script,直到可以出图。<br>新项目保存位置:<code id="ndStoragePath">正在读取配置…</code>。<span id="ndStorageNote"></span></p>
|
|
134
134
|
<label class="field"><span>主题 / 标题</span><input id="ndTitle" required placeholder="例:XX 业务场景的 AI 落地分享"></label>
|
|
135
135
|
<label class="field"><span>项目目录名(可选,英文小写中划线;留空自动生成)</span><input id="ndDirName" placeholder="例:talk-ai-transformation-6-stages"></label>
|
|
136
136
|
<label class="field"><span>页数(占位页 1–40;创建后还能在对话里追加)</span><input id="ndPages" type="number" min="1" max="40" inputmode="numeric" value="6"></label>
|
|
@@ -1411,14 +1411,30 @@ async function fetchDoc(docId) {
|
|
|
1411
1411
|
if (STATE.docCache[docId]) return STATE.docCache[docId];
|
|
1412
1412
|
if (STATE.docLoading === docId) return null;
|
|
1413
1413
|
STATE.docLoading = docId;
|
|
1414
|
+
const deckId = STATE.data?.active?.deckId ?? "";
|
|
1415
|
+
// F-163:加载失败必须可见。以前只 throw,中间的 spinner 永远不停,用户不知道是 404、500 还是超时。
|
|
1416
|
+
const controller = typeof AbortController === "function" ? new AbortController() : null;
|
|
1417
|
+
const timer = controller ? setTimeout(() => controller.abort(), 15000) : null;
|
|
1414
1418
|
try {
|
|
1415
|
-
const
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1419
|
+
const res = await fetch(
|
|
1420
|
+
`/api/deck-studio.doc?` + new URLSearchParams({ deckId, docId }),
|
|
1421
|
+
controller ? { signal: controller.signal } : undefined,
|
|
1422
|
+
);
|
|
1423
|
+
const text = await res.text();
|
|
1424
|
+
let doc = null;
|
|
1425
|
+
try { doc = JSON.parse(text); } catch { doc = null; }
|
|
1426
|
+
if (!res.ok || !doc || doc.ok === false) {
|
|
1427
|
+
const detail = doc?.error?.message || text.replace(/\s+/g, " ").trim().slice(0, 200) || res.statusText;
|
|
1428
|
+
throw new Error(`${res.status} · ${detail}`);
|
|
1429
|
+
}
|
|
1430
|
+
STATE.docCache[docId] = doc;
|
|
1420
1431
|
return doc;
|
|
1432
|
+
} catch (error) {
|
|
1433
|
+
const message = error?.name === "AbortError" ? "请求超时(15 秒没有响应)" : String(error?.message ?? error);
|
|
1434
|
+
console.warn(`[deck-studio] 读取文档失败 docId=${docId} deckId=${deckId}:${message}`);
|
|
1435
|
+
return { ok: false, docId, error: { message } };
|
|
1421
1436
|
} finally {
|
|
1437
|
+
if (timer) clearTimeout(timer);
|
|
1422
1438
|
STATE.docLoading = null;
|
|
1423
1439
|
}
|
|
1424
1440
|
}
|
|
@@ -1430,6 +1446,18 @@ async function renderDocTab(docId) {
|
|
|
1430
1446
|
if (!cached) view.innerHTML = `<div class="observe-body"><div class="doc-loading">读取 ${esc(index?.label ?? docId)} …</div></div>`;
|
|
1431
1447
|
const doc = cached ?? (await fetchDoc(docId));
|
|
1432
1448
|
if (!doc || STATE.tab !== docId) return; // 加载期间用户已切走
|
|
1449
|
+
if (doc.ok === false) {
|
|
1450
|
+
view.innerHTML = `<div class="observe-body">
|
|
1451
|
+
<div class="doc-loading">读取「${esc(index?.label ?? docId)}」失败:${esc(doc.error?.message || "未知错误")}</div>
|
|
1452
|
+
<p style="text-align:center;margin-top:10px"><button type="button" class="btn" data-doc-retry="${esc(docId)}">重试</button></p>
|
|
1453
|
+
<p class="small" style="text-align:center">先在终端看服务端日志;若一直失败,把上面这行错误发我。</p>
|
|
1454
|
+
</div>`;
|
|
1455
|
+
view.querySelectorAll("[data-doc-retry]").forEach((btn) => btn.addEventListener("click", () => {
|
|
1456
|
+
STATE.docCache[docId] = null;
|
|
1457
|
+
renderDocTab(docId);
|
|
1458
|
+
}));
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1433
1461
|
view.innerHTML = `<div class="observe-body">${renderDoc(doc, index)}</div>`;
|
|
1434
1462
|
bindDocActions(view);
|
|
1435
1463
|
}
|
|
@@ -2780,15 +2808,19 @@ async function ensurePromptConversation() {
|
|
|
2780
2808
|
if (!target) return;
|
|
2781
2809
|
const bound = chatSessionsForTarget(target);
|
|
2782
2810
|
if (bound.length) {
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2811
|
+
// 发送操作也要核对 preset,不能只因绑定存在就向标准模式发制作指令。
|
|
2812
|
+
const result = target.kind === "deck"
|
|
2813
|
+
? await ensureDeckConversation(target.id)
|
|
2814
|
+
: await openDesignProject(target.id);
|
|
2815
|
+
if (!result?.ok) throw new Error(result?.reason || "无法打开 Deckline 制作会话");
|
|
2786
2816
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
2787
2817
|
return;
|
|
2788
2818
|
}
|
|
2789
2819
|
const active = STATE.data?.active;
|
|
2790
|
-
|
|
2791
|
-
|
|
2820
|
+
const result = active?.kind === "design-canvas"
|
|
2821
|
+
? await openDesignProject(active.designId)
|
|
2822
|
+
: active?.deckId ? await ensureDeckConversation(active.deckId) : null;
|
|
2823
|
+
if (!result?.ok) throw new Error(result?.reason || "无法打开 Deckline 制作会话");
|
|
2792
2824
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
2793
2825
|
}
|
|
2794
2826
|
|
|
@@ -2809,6 +2841,8 @@ async function sendAgentPrompt() {
|
|
|
2809
2841
|
}
|
|
2810
2842
|
state.textContent = "已发送到左侧对话 ✓";
|
|
2811
2843
|
setTimeout(closeAgentPrompt, 900);
|
|
2844
|
+
} catch (error) {
|
|
2845
|
+
state.textContent = `未能发送:${error?.message || error}`;
|
|
2812
2846
|
} finally {
|
|
2813
2847
|
button.disabled = false;
|
|
2814
2848
|
}
|
|
@@ -3434,11 +3468,23 @@ function renderDeckStyleChoices() {
|
|
|
3434
3468
|
}
|
|
3435
3469
|
}
|
|
3436
3470
|
|
|
3471
|
+
function newDeckStorageInfo(config = {}) {
|
|
3472
|
+
const root = String(config?.projectsRoot || "").trim();
|
|
3473
|
+
const dataRoot = String(config?.dataRoot || "").trim();
|
|
3474
|
+
const append = (base, suffix) => `${base.replace(/[\\/]+$/, "")}/${suffix}`;
|
|
3475
|
+
if (root) return { path: append(root, "<目录名或自动生成的 deck-id>/"), note: "由 profile 的 projectsRoot 配置;与会话工作区独立。" };
|
|
3476
|
+
if (dataRoot) return { path: append(dataRoot, "projects/<deck-id>/source/"), note: "未配置 projectsRoot,使用插件数据目录;不会自动保存在当前工作区。" };
|
|
3477
|
+
return { path: "暂时无法确定", note: "请等待工作台配置加载后重试;未配置 projectsRoot 时使用 <dataRoot>/projects/<deck-id>/source/。" };
|
|
3478
|
+
}
|
|
3479
|
+
|
|
3437
3480
|
function openNewDeckModal() {
|
|
3438
3481
|
// F-112:profile 里配了 defaultStyleAbbr 就默认预选这个风格(用户仍可改/取消)。
|
|
3439
3482
|
STATE.ndStyleAbbr = String(STATE.data?.config?.defaultStyleAbbr || "");
|
|
3440
3483
|
getEl("newDeckForm").reset();
|
|
3441
3484
|
getEl("ndState").textContent = "";
|
|
3485
|
+
const storage = newDeckStorageInfo(STATE.data?.config);
|
|
3486
|
+
getEl("ndStoragePath").textContent = storage.path;
|
|
3487
|
+
getEl("ndStorageNote").textContent = storage.note;
|
|
3442
3488
|
getEl("newDeckModal").hidden = false;
|
|
3443
3489
|
renderDeckStyleChoices();
|
|
3444
3490
|
setTimeout(() => getEl("ndTitle").focus(), 30);
|
|
@@ -3788,7 +3834,14 @@ function requestConversation(payload) {
|
|
|
3788
3834
|
resolve({ ok: Boolean(event.data.ok), sessionId: event.data.sessionId || "", reason: event.data.reason || "" });
|
|
3789
3835
|
};
|
|
3790
3836
|
window.addEventListener("message", onAck);
|
|
3791
|
-
target.postMessage({
|
|
3837
|
+
target.postMessage({
|
|
3838
|
+
type: "dsh-deck-studio:conversation",
|
|
3839
|
+
requestId,
|
|
3840
|
+
cwd: STATE.data?.cwd || "",
|
|
3841
|
+
workspacePath: STATE.data?.workspacePath || "",
|
|
3842
|
+
sourceRoot: STATE.data?.active?.sourceRoot || "",
|
|
3843
|
+
...payload,
|
|
3844
|
+
}, "*");
|
|
3792
3845
|
setTimeout(() => {
|
|
3793
3846
|
if (settled) return;
|
|
3794
3847
|
cleanup();
|
|
@@ -3850,14 +3903,17 @@ async function ensureDeckConversation(deckId) {
|
|
|
3850
3903
|
const chosen = bound[0] || null;
|
|
3851
3904
|
const binding = chosen ? { sessionId: chosen.sessionId } : null;
|
|
3852
3905
|
const result = await requestConversation({
|
|
3853
|
-
action:
|
|
3906
|
+
action: "ensure",
|
|
3854
3907
|
sessionId: chosen?.sessionId || "",
|
|
3855
3908
|
kind: "deck",
|
|
3856
3909
|
id: deckId,
|
|
3857
3910
|
title: label,
|
|
3858
|
-
prompt:
|
|
3911
|
+
prompt: `本对话绑定 deck ${deckId}(${label})。后续所有修改都在这个 deck 上进行;先读 intent 与 script 概要,等我的指令,不要直接出图。`,
|
|
3859
3912
|
});
|
|
3860
|
-
if (!result.ok)
|
|
3913
|
+
if (!result.ok) {
|
|
3914
|
+
getEl("chatBindChip").textContent = `会话打开失败:${result.reason || "未知原因"}`;
|
|
3915
|
+
return result;
|
|
3916
|
+
}
|
|
3861
3917
|
if (!binding || binding.sessionId !== result.sessionId) {
|
|
3862
3918
|
await bindConversation({ sessionId: result.sessionId, kind: "deck", id: deckId, title: label });
|
|
3863
3919
|
}
|
|
@@ -3872,25 +3928,28 @@ async function openDesignProject(designId) {
|
|
|
3872
3928
|
const project = (STATE.data?.designProjects || []).find((item) => item.designId === designId);
|
|
3873
3929
|
if (!project) return;
|
|
3874
3930
|
showDeck(project.canvasDeckId);
|
|
3875
|
-
const
|
|
3931
|
+
const target = chatTargetForBinding({ kind: "design", id: designId });
|
|
3932
|
+
const binding = chatSessionsForTarget(target)[0] || null;
|
|
3876
3933
|
const title = `风格 · ${project.name}`;
|
|
3877
3934
|
const result = await requestConversation({
|
|
3878
|
-
action:
|
|
3935
|
+
action: "ensure",
|
|
3879
3936
|
sessionId: binding?.sessionId || "",
|
|
3880
3937
|
kind: "design",
|
|
3881
3938
|
id: designId,
|
|
3882
3939
|
title,
|
|
3883
|
-
prompt:
|
|
3940
|
+
prompt: buildDesignPrompt(project, project.canvasDeckId),
|
|
3884
3941
|
});
|
|
3885
3942
|
if (result.ok && (!binding || binding.sessionId !== result.sessionId)) {
|
|
3886
3943
|
await bindConversation({ sessionId: result.sessionId, kind: "design", id: designId, title });
|
|
3887
3944
|
} else if (!result.ok) {
|
|
3888
3945
|
STATE.lastConvError = result.reason || "未知原因";
|
|
3946
|
+
getEl("chatBindChip").textContent = `会话打开失败:${STATE.lastConvError}`;
|
|
3889
3947
|
}
|
|
3890
3948
|
if (result.ok) {
|
|
3891
3949
|
STATE.chatSessionId = result.sessionId;
|
|
3892
3950
|
renderChatHeader();
|
|
3893
3951
|
}
|
|
3952
|
+
return result;
|
|
3894
3953
|
}
|
|
3895
3954
|
|
|
3896
3955
|
/** Conversations 页点会话:切过去,并把工作台切到它绑定的 deck / design 画布。 */
|