@xia-sc/dsh-cc-studio 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +33 -10
  2. package/lib/client.js +61 -33
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -15,7 +15,7 @@ DSH(DeepSeek Harness)插件:输入框上方的胶囊 → 全屏融合工
15
15
  | 项目 | 值 |
16
16
  | --- | --- |
17
17
  | 插件包名 | `@xia-sc/dsh-cc-studio` |
18
- | 当前版本 | `0.4.0` |
18
+ | 当前版本 | `0.4.1` |
19
19
  | 兼容与权限 | DSH `>=0.1.5-rc.1`、Node `>=22`、**零运行依赖**;权限边界见 [兼容与权限](#兼容与权限) |
20
20
  | 宿主 RPC | `/dsh-cc-studio-rpc`(自带路由,适配 dsh ≥ `0.1.5-rc.1`;已在 `0.1.6-alpha.2` / `0.1.7-alpha.1` / `0.1.7-rc.1` 实测) |
21
21
  | CC 预设 | `presets/cc.patch.yml`(dsh ≥ `0.1.7-alpha.1`,组合声明行)/`<DSH_HOME>/.agent-presets/cc`(≤ `0.1.6-alpha.2`,目录形态) |
@@ -24,13 +24,15 @@ DSH(DeepSeek Harness)插件:输入框上方的胶囊 → 全屏融合工
24
24
 
25
25
  ## 安装
26
26
 
27
+ 本插件同时支持 **`dsh web`(CLI 起的 Web GUI)** 与 **DSH 桌面版(DeepSeek Harness Desktop)**:两者是同一个 dsh 宿主,插件都装在 profile 的 `dsh.profile.bundles` 里,加载方式与全部功能完全一致。下面以 `web` profile 为例;桌面版的差异只有 profile 名与「重启应用」这一步,见下文 **[7. 桌面版](#7-桌面版deepseek-harness-desktop)**。
28
+
27
29
  ### 1. 从 npm 安装(推荐)
28
30
 
29
31
  ```bash
30
32
  dsh plugin --profile web add @xia-sc/dsh-cc-studio
31
33
 
32
34
  # 锁定版本
33
- dsh plugin --profile web add @xia-sc/dsh-cc-studio@0.3.7
35
+ dsh plugin --profile web add @xia-sc/dsh-cc-studio@0.4.0
34
36
  ```
35
37
 
36
38
  **或从 GitHub 源安装**:
@@ -219,6 +221,22 @@ dsh plugin --profile web add @xia-sc/dsh-cc-studio
219
221
  | 校验报错 | 必填 `spec: chara_card_v3` / `spec_version: 3.0` / `group_only_greetings`;主图标需唯一;正则需合法 |
220
222
  | 重启后浏览器报 `Failed to load plugins` / `web boot: 1 entry did not activate` / `import failed` | 包名迁移后**没有按新名重装**:profile 里的安装身份(`dsh.profile.bundles` 那行与 `dependencies` 里的 `link:`)还是旧名 `@dsh-plugins/dsh-cc-studio`,而包内 `package.json` 与 `lib/client.js` 已改叫 `@xia-sc/dsh-cc-studio`,客户端资源就解析不上(宿主照样起,只有浏览器那一半挂)。看一眼 `~/.dsh/profiles/web/node_modules/` 下的目录名是否等于 `package.json` 的 `name` 即可确认;修法是按新名重装(见「5. 更新与卸载」的迁移命令)→ 重启 `dsh web` → **硬刷新**(`Ctrl+Shift+R`;旧页面的引导图是缓存,普通刷新会复现同一句错) |
221
223
 
224
+ ### 7. 桌面版(DeepSeek Harness Desktop)
225
+
226
+ **桌面版就是同一个 dsh 宿主**(Electron 内嵌),插件照样装在 profile 的 `dsh.profile.bundles` 里,所以本插件**不需要任何额外适配**:三个客户端挂载点(胶囊 / 工坊 / 设置页)、`CC 模式` 预设、RPC 通道、草稿与角色卡落盘,都与 `dsh web` 一模一样(0.4.0 在桌面版 profile 上实测跑通全流程:胶囊 → 工坊 → 导入导出 → 草稿落盘)。
227
+
228
+ 差异只有「怎么装」和「怎么生效」两步:
229
+
230
+ | | `dsh web`(CLI) | 桌面版 |
231
+ | --- | --- | --- |
232
+ | 安装 | `dsh plugin --profile web add @xia-sc/dsh-cc-studio` | 应用内左侧「插件」面板安装 `@xia-sc/dsh-cc-studio`;装了 CLI 的话也可以用 `dsh plugin --profile desktop add @xia-sc/dsh-cc-studio` |
233
+ | profile | 一般是 `web` | 一般是 `desktop`(`~/.dsh/profiles/desktop`),以应用内显示为准 |
234
+ | 宿主 / 插件改动 | 重启 `dsh web` | 退出并重新打开桌面应用 |
235
+ | 只改客户端(`lib/client.js`) | 刷新页面 | 在应用窗口里刷新(找不到刷新入口就重启应用) |
236
+ | 数据位置 | `<DSH_HOME>/cc-drafts/`、`<DSH_HOME>/cc-library/`(默认 `~/.dsh`) | 同上,桌面版共用同一套 `DSH_HOME` 与落盘目录 |
237
+
238
+ > 桌面版的应用窗口本身就是它的 Web GUI(端口由应用分配),所以「重启应用」≈ 重启宿主、「刷新窗口」≈ 刷新页面,排查问题时两者可以混用。
239
+
222
240
  ## 快速上手
223
241
 
224
242
  1. 新建会话,会话模式选 **CC 模式**(胶囊随即出现在输入框上方)。
@@ -231,12 +249,13 @@ dsh plugin --profile web add @xia-sc/dsh-cc-studio
231
249
 
232
250
  ### 融合工坊(客户端)
233
251
 
234
- - **布局**:输入框上方胶囊(CC 模式自动出现)→ `shell.overlay` 全屏工坊,左侧导航 4 页(**5维世界观 / 角色细化 / 世界书 / 校验导出**)/ 自适应主区 / 280px 已存角色侧栏 / 360px 实时 `card.json` 预览。深浅色自适应(DSW Token + 品牌紫 `#7c5cff` 固定)。
252
+ - **布局**:输入框上方胶囊(CC 模式自动出现)→ `shell.overlay` 全屏工坊,左侧导航 **3 页**(**5维世界观 / 角色细化 / 世界书**)+ 左下角「**校验导出**」入口 / 自适应主区 / 268px 可折叠已存角色侧栏 / 主区底部两块默认收起的阅读辅助(实时 `card.json` 预览、CBS 速查)。深浅色自适应(DSW Token + 品牌紫 `#7c5cff` 固定)。
253
+ - **导出入口只有两处**(0.4.1 归一):左下角「校验导出」与已存角色卡片上的「导出」,两者打开的是同一个**导出页**(`JSON / PNG / 写入PNG / CHARX / 校验`)——导出页导出的始终是当前工坊里的角色卡(标题后面就写着卡名,没名字时显示「未命名」),要导某张已存卡请先「载入」;顶部工具条不再有「导出 JSON」。卡片上的「导出」点下去会立刻变成「已切到 ✓」,若那张卡还没载入,导出页顶部会提示「要导它请先点它那行的『载入』」。
235
254
  - **风格标签**:在「角色细化」中以逗号分隔编辑 `tags`,实时写回 `data.tags`。
236
- - **5 维世界观**:年表 / 势力 / 地理 / 力量体系 / 日常 → `cc_patch_world(autoLorebook=true)` 自动生成带 `@@position / @@depth / @@activate` 的 Lorebook 条目(至少 1 条 `constant` 常驻;再次调用自动覆盖旧自动条目、保留手动条目)。每维为**预览卡片 + 展开大框编辑**(小卡显示 140 字预览/字数,点击卡片或「⛶ 编辑」弹出 720px 大框)。
237
- - **全量长文本大框编辑**:`description / personality / scenario / system_prompt / post_history_instructions / first_mes / alternate_greetings / group_only_greetings / mes_example / creator_notes` 全部是「标签 + 右上 `⛶`」的小框 + 720px 大框,实时同步,解决多行长文在小框里难预览/编辑。
238
- - **已存角色侧栏(ID 化 CRUD)**:280px 可折叠,高亮当前载入卡(紫框 + 顶部 `已载入 ID:xxxx`,显示 ID 前 8 位),`★ 保存当前` 在已载入 ID 时原地覆盖、`+ 另存为新` 强制新建、`✎ 重命名` / `+ 新建`;搜索/载入/导出/删除落盘 `~/.dsh/cc-library/<id>.json`。
239
- - **导入导出**:`⬆ 导入 JSON/PNG/CHARX` 自动识别容器;`⬇ JSON` / `⬇ PNG`(1×1 占位图)/ `⬆ 写入 PNG`(写入你上传的任意 PNG,自动剥离旧 `ccv3/chara` 块并 `CRC32` 重算)/ `⬇ CHARX`(打包 `card.json`)。
255
+ - **5 维世界观**:年表 / 势力 / 地理 / 力量体系 / 日常 → `cc_patch_world(autoLorebook=true)` 自动生成带 `@@position / @@depth / @@activate` 的 Lorebook 条目(至少 1 条 `constant` 常驻;再次调用自动覆盖旧自动条目、保留手动条目)。五维默认是**文档式阅读排版**(无边框 textarea 按内容自动撑高,点进去就能改),写长文可点右上「⛶」开 720px 大框(带打开时快照,「取消」真回滚)。
256
+ - **全量长文本大框编辑**:`description / personality / scenario / system_prompt / post_history_instructions / first_mes / alternate_greetings / group_only_greetings / mes_example / creator_notes` 都可点「⛶」开 720px 大框,实时同步,解决多行长文难预览/编辑的问题。
257
+ - **已存角色侧栏(ID 化 CRUD)**:268px 可折叠(收成 40px 竖条),高亮当前载入卡(紫框 + 顶部 `已载入 ID:xxxx`,显示 ID 前 8 位),`★ 保存当前` 在已载入 ID 时原地覆盖、`+ 另存为新` 强制新建、`✎ 重命名` / `+ 新建`;搜索 / 载入 / 导出 / 删除落盘 `~/.dsh/cc-library/<id>.json`。
258
+ - **导入导出**:侧栏 `⬆ 导入 JSON/PNG/CHARX` 自动识别容器;导出页 `⬇ JSON` / `⬇ PNG`(1×1 占位图)/ `⬆ 写入 PNG`(写入你上传的任意 PNG,自动剥离旧 `ccv3/chara` 块并 `CRC32` 重算)/ `⬇ CHARX`(打包 `card.json`)。
240
259
  - **中英双语**:完整 `zh / en` 词表(`locale: dshCcStudio`),跟随全局 `设置 → 通用 → 语言` 自动切换(胶囊/工坊/设置即时刷新,插件内无手动开关)。
241
260
  - **深浅色自适应**:全量使用 `var(--dsw-alias-bg-* / border-l1/l2 / label-primary/secondary)`,主按钮/选中态固定品牌紫,刷新即生效。
242
261
 
@@ -270,7 +289,7 @@ dsh-cc-studio/
270
289
  │ └── cc/ # CC 预设目录模板(dsh ≤ 0.1.6-alpha.2,插件挂载时安装到用户根)
271
290
  │ ├── preset.yml
272
291
  │ └── agent.cordis.yml
273
- ├── tests/ # 8 个测试文件、335 项断言(不随包发布)
292
+ ├── tests/ # 10 个测试文件、380 项断言(不随包发布)
274
293
  │ ├── rpc-channel.test.mjs # host 半 RPC 通道回归(假 ctx + 真实 http,45 项断言)
275
294
  │ ├── preset-install.test.mjs # 预设安装决策 + native 让位 + 两份清单防漂移 + 行契约,79 项断言
276
295
  │ ├── cc-detection.test.mjs # CC 模式探测的源码级守卫,51 项断言
@@ -278,6 +297,8 @@ dsh-cc-studio/
278
297
  │ ├── draft-slot-sync.test.mjs # 草稿槽行为级回归(假 React/slot/RPC harness),18 项断言
279
298
  │ ├── nav-highlight.test.mjs # 工坊步骤导航三态回归(选中/键盘焦点/悬停互斥),23 项断言
280
299
  │ ├── clear-card.test.mjs # 「清空当前角色卡」二次确认回归,20 项断言
300
+ │ ├── export-entry.test.mjs # 导出入口归一回归(三处导出收成两处),31 项断言
301
+ │ ├── close-button.test.mjs # 工坊关闭按钮 ✕(悬停红底)回归,14 项断言
281
302
  │ └── data-root.test.mjs # 落盘根:DSH_HOME 优先 + 读时回退 ~/.dsh,37 项断言
282
303
  ├── CHANGELOG.md # 完整版本历史
283
304
  ├── README.md # 中文(默认)
@@ -289,7 +310,7 @@ dsh-cc-studio/
289
310
  ## 开发与测试
290
311
 
291
312
  ```bash
292
- npm test # 8 个文件、335 项断言(八个 node 脚本串联)
313
+ npm test # 10 个文件、380 项断言(十个 node 脚本串联)
293
314
  node tests/rpc-channel.test.mjs # 只跑 host 半 RPC 通道回归(45 项断言)
294
315
  ```
295
316
 
@@ -309,7 +330,8 @@ node tests/rpc-channel.test.mjs # 只跑 host 半 RPC 通道回归(
309
330
  | DSH 兼容范围 | `>=0.1.5-rc.1`(`package.json` 的 `dsh.compatibility.dsh`) |
310
331
  | 逐版本证据 | `dsh.compatibility.dshReleases` 里 `0.1.5-rc.1` / `0.1.5-rc.2` / `0.1.6-alpha.1` / `0.1.6-alpha.2` / `0.1.7-alpha.1` / `0.1.7-rc.1` / **`0.2.0-rc.2`** 为 `compatible`。**没实测过的版本不写**(`0.1.7-alpha.2`、`0.1.7-rc.2`、`0.2.0-rc.1`、`0.2.1-alpha.1` 保持未声明,商城显示为未知),而不是先写 `compatible` 再补测 |
311
332
  | Node.js | `engines.node: ">=22"`(实测运行时 Node v26.9.0) |
312
- | Profile | `web`(由 `dsh.client.platform` 推出) |
333
+ | 宿主形态 | `dsh web`(CLI)与 **DSH 桌面版(DeepSeek Harness Desktop)** 都是同一套宿主 + profile bundles 机制,**均支持**(差异与安装方式见 [7. 桌面版](#7-桌面版deepseek-harness-desktop)) |
334
+ | 客户端平台 | `dsh.client.platform: "web"` —— 这是**浏览器半**的平台标识,与 dsh 的 profile 名(`web` / `desktop` …)不是一回事;桌面版与 CLI 加载的是同一份客户端 bundle |
313
335
  | 运行依赖 | **无**。`lib/*.js` 只 `import` `node:` 内建与相对路径;随包的补丁 / 预设是静态 YAML,没有安装期脚本 |
314
336
  | 生命周期脚本 | **无** `preinstall` / `install` / `postinstall` / `prepare`(只有 `test` 与 `prepublishOnly`,只在本仓库跑,不发给安装端) |
315
337
  | 权限 · 文件 | 读 `<DSH_HOME>`(含旧根 `~/.dsh`)下自己的三个目录:`cc-drafts/`、`cc-library/`、`.agent-presets/cc/`(后者只在 dsh ≤ `0.1.6-alpha.2` 上写);另读自身 `package.json`。不写宿主配置,不碰别的插件的数据 |
@@ -330,6 +352,7 @@ node tests/rpc-channel.test.mjs # 只跑 host 半 RPC 通道回归(
330
352
 
331
353
  完整历史见 [CHANGELOG.md](./CHANGELOG.md)。最近几版:
332
354
 
355
+ - `0.4.1` **导出入口归一**:同一个「导出」原本在工坊里出现三处(顶部「⬇ 导出 JSON」/ 左侧导航第 4 步「校验导出」/ 已存角色卡片里的「导出」),彼此重复又不一致。现在顶部那枚删掉;左侧导航与顶部胶囊都只留写作三步,「校验导出」与左下角那枚「校验」按钮合并成一枚入口,点开即是导出页;卡片上的「导出」保留原位置、但改成切到同一页 —— 导出动作只剩导出页那一套实现(卡片「导出」不再直接下载 JSON,`store.exportLibraryEntry` 旁路已删),导出页新增一行说明「导出的是当前工坊里的卡,要导某张已存角色请先载入」。**另一半是文档**:README / README_EN 写明**桌面版(DeepSeek Harness Desktop)支持** —— 新增「7. 桌面版」一节(安装入口、profile 名、重启应用 vs 刷新窗口、数据位置),「兼容与权限」表补「宿主形态」行,并把容易误读的 `Profile | web` 改名为「客户端平台」。另外按实机反馈补了四处:导出页标题带上当前卡名(没名字时显示「未命名」)、卡片「导出」点下去立刻变成「已切到 ✓」并在 6 秒后复原、导出页在「你点的那张卡还没载入」时给出醒目提示、左下角那枚入口按钮**不做选中态**(它一旦跟着亮,就和卡片那枚反馈凑成「两处看起来像选中」)。工作坊头部的「收起」也换成了「✕」图标按钮(悬停红底白字,点击关闭)。测试 335 → 380 项(新增 `tests/export-entry.test.mjs` 31 项、`tests/close-button.test.mjs` 14 项)。
333
356
  - `0.4.0` 修工坊左侧步骤列表的高亮:鼠标点过某一步后,那一项周围会多出一圈「莫名其妙的描边」,再用顶部胶囊切步时描边还赖在旧项上,看起来像同时选中了两项。根因是 `navItem` 只画 `s.cur` 的选中态、从没管过浏览器焦点(`<button>` 被点击后一直持有焦点,宿主/UA 的焦点描边就留在按钮外沿)。现在:导航项 `onMouseDown` 阻止默认(鼠标点击不抢焦点、键盘 Tab 照旧)、`outline` 由插件自己接管(只在 `:focus-visible` 时给 2px 品牌色环)、选中态换成品牌紫淡底 + 左竖条 + `aria-current`,并补上 hover 反馈与「鼠标一动就收起焦点环」。新增**「清空当前角色卡」按钮**(左侧底部「校验」下面,`window.confirm` 二次确认后换回空卡,并解除与已存角色的关联 —— 否则接着点「保存当前」会把库里那张覆盖成空卡)。**并重构了工坊 UI(本版主线)**:砍掉右侧占地 360px 那一栏,实时 CCv3 预览与 CBS 速查收到主区底部、**默认全部折叠**(预览那行仍显示 spec 与通过/失败徽章;原「融合说明」那块纯介绍文案已删除);字段改成「文档式」阅读排版(无边框 textarea + `field-sizing: content` 自动撑高),手动编辑退成次要入口(长文走 ⛶ 大框),阅读区从 ~700px 拓到 ~876px。功能一个没少。`tests/nav-highlight.test.mjs`(23 项)与 `tests/clear-card.test.mjs`(20 项)钉住新行为。测试 292 → 335 项。
334
357
  - `0.3.8` 补 DSH STORE 上架契约([Store #1173](https://github.com/AI-Scarlett/DSH-Store/issues/1173)):`engines.node: ">=22"`、`dsh.compatibility`(兼容范围 + **只写实测过版本**的精确矩阵)、删掉那个从 0.3.0 起就没被任何代码 import 过的 `@deepseek-ai/cordis` 运行依赖,README / README_EN 新增「兼容与权限」披露节,`tests/preset-install.test.mjs` 加 11 项契约守卫(281 → 292 项断言)。**但这个 issue 光改仓库修不完**:商城条目的包名还停在 0.3.0 的旧名 `@dsh-plugins/dsh-cc-studio`,STORE 复检第一步就因「包名与条目身份不符」判 `update-blocked`,得先由 STORE 侧迁移条目身份(重新提交也绕不过,`entryIds` 会撞旧条目);而 files / credentials 两个权限信号是本插件的核心功能(角色卡草稿落盘 + 读 `DSH_HOME`),自动低风险通道**不可能**通过,商城只应按 `user-reviewed`(用户逐次确认)收录。另已做完 **dsh `0.2.0-rc.2` 兼容性审计**(隔离宿主实测宿主行/预设行组合、RPC 线协议与围栏、会话槽 key、预设 roster 含 `cc` 且 `broken` 空、三个客户端挂载点 `active`、PNG/CHARX 往返逐字一致、真实 `~/.dsh` 零变化),矩阵补上 `0.2.0-rc.2: compatible`;`0.2.0-rc.1` / `0.2.1-alpha.1` 未测,仍不声明。
335
358
  - `0.3.7` dsh `0.1.7-rc.1` 兼容性审计:**接口面无一处需要适配**(隔离宿主实测了 RPC 路由/线协议、预设 roster、三个前端挂载点、草稿槽 key、落盘根、PNG/CHARX 往返)。真正的修复是预设里**一行自始漏抄**:`command-goal` 在内置 `standard` 里从 `0.1.6-alpha.2` 起就有,本预设初次拷贝时就漏了它 —— 不报错、不判 broken,只是 CC 模式会话少了 `/goal` 斜杠命令(与 0.3.4 补回的 `present` 同类),现已两份载体同步补齐并加了**行集快照**守卫。另更正 3 处会误导下次升级的文档口径(「异步迭代 `IncomingMessage` 必抛」实测不成立且官方 `/api` 桥自己就在用;`/api/agentPresets/list` 其实带可选 `name`/`description`/`broken`;channel/endpoint 命名正则在 `dsh-client-connection` 而非 `webServer.register`),并给「客户端 `preset.ccLabel` 必须逐字等于宿主预设显示名」这条隐性耦合补了跨文件守卫。顺带精简了 roster 下拉里「CC 模式」的介绍文案:原来把模型侧的 6 个工具名(`cc_get_card` / `cc_patch_character` / …)全列在用户界面上,既撑满下拉项又对用户没有信息量(工具名只对读 persona 的模型有意义),现在只讲「模型先问再填 / 胶囊实时同步 / 工坊全屏编辑 / 导出 JSON、PNG、CHARX」。测试 278 → 281 项。
package/lib/client.js CHANGED
@@ -30,10 +30,8 @@ window.__ModuleLoader__.load({
30
30
  "step.world.desc":"年表/势力/地理/力量/日常",
31
31
  "step.char.desc":"六件套 + 开场",
32
32
  "step.lore.desc":"Lorebook + Decorator",
33
- "step.export.desc":"校验导出",
34
33
  "action.expand":"✦ AI 扩写",
35
34
  "action.validate":"校验",
36
- "action.export":"⬇ 导出 JSON",
37
35
  "action.exportJson":"⬇ JSON",
38
36
  "action.exportPng":"⬇ PNG",
39
37
  "action.embedPng":"⬆ 写入 PNG",
@@ -109,6 +107,8 @@ window.__ModuleLoader__.load({
109
107
  "workshop.export.failed":"✗ 校验未通过",
110
108
  "workshop.export.hint":"PNG=tEXt ccv3 · CHARX=ZIP card.json",
111
109
  "workshop.export.hint2":"⬇ PNG=1x1底图 · ⬆ 写入=选本地PNG写入ccv3 · CHARX=ZIP",
110
+ "workshop.export.currentHint":"导出的是当前工坊里的角色卡;要导出某张已存角色,请先在右侧载入它",
111
+ "workshop.export.jumpedOther":"已切到导出页 · 导出的是当前工坊里的「{name}」;「{from}」还没载入,要导它请先点它那行的「载入」",
112
112
  "workshop.export.creatorNotes":"creator_notes / 多语言",
113
113
  "workshop.export.creationDate":"creation_date",
114
114
  "workshop.export.modificationDate":"modification_date(导出自动刷新)",
@@ -138,6 +138,8 @@ window.__ModuleLoader__.load({
138
138
  "workshop.library.noMatch":"无匹配",
139
139
  "workshop.library.load":"载入",
140
140
  "workshop.library.export":"导出",
141
+ "workshop.library.exportTip":"切到导出页(导出的仍是当前工坊里的卡)",
142
+ "workshop.library.exportJumped":"已切到 ✓",
141
143
  "workshop.library.delete":"删除",
142
144
  "workshop.library.unnamed":"未命名",
143
145
  "workshop.edit.large":"大框编辑",
@@ -202,8 +204,6 @@ window.__ModuleLoader__.load({
202
204
  "store.promptRename":"输入新名称",
203
205
  "store.renamed":"已重命名为:{name}",
204
206
  "store.errRenameFailed":"重命名失败",
205
- "store.exported":"已导出:{name}",
206
- "store.errExportFailed":"导出失败",
207
207
  "store.saving":"更新中…",
208
208
  "store.savingNew":"另存中…",
209
209
  "workshop.greeting.entryLabel":"{title} · 第 {n} 条",
@@ -242,10 +242,8 @@ window.__ModuleLoader__.load({
242
242
  "step.world.desc":"Timeline/Factions/Geo/Power/Daily",
243
243
  "step.char.desc":"6-piece + Openings",
244
244
  "step.lore.desc":"Lorebook + Decorator",
245
- "step.export.desc":"Validate & Export",
246
245
  "action.expand":"AI Expand",
247
246
  "action.validate":"Validate",
248
- "action.export":"Export JSON",
249
247
  "action.exportJson":"⬇ JSON",
250
248
  "action.exportPng":"⬇ PNG",
251
249
  "action.embedPng":"⬆ Embed into PNG",
@@ -321,6 +319,8 @@ window.__ModuleLoader__.load({
321
319
  "workshop.export.failed":"✗ Failed",
322
320
  "workshop.export.hint":"PNG=tEXt ccv3 · CHARX=ZIP card.json",
323
321
  "workshop.export.hint2":"⬇ PNG=1×1 placeholder · ⬆ Embed=write into your PNG · CHARX=ZIP",
322
+ "workshop.export.currentHint":"Exports the card currently in the workshop; to export a saved one, load it from the right first",
323
+ "workshop.export.jumpedOther":"Jumped to the export page — it exports “{name}” from the workshop; “{from}” is not loaded yet, hit Load on its row first",
324
324
  "workshop.export.creatorNotes":"creator_notes / i18n",
325
325
  "workshop.export.creationDate":"creation_date",
326
326
  "workshop.export.modificationDate":"modification_date (auto on export)",
@@ -350,6 +350,8 @@ window.__ModuleLoader__.load({
350
350
  "workshop.library.noMatch":"No matches",
351
351
  "workshop.library.load":"Load",
352
352
  "workshop.library.export":"Export",
353
+ "workshop.library.exportTip":"Jump to the export page (it exports the card currently in the workshop)",
354
+ "workshop.library.exportJumped":"Opened ✓",
353
355
  "workshop.library.delete":"Delete",
354
356
  "workshop.library.unnamed":"Unnamed",
355
357
  "workshop.edit.large":"Large Editor",
@@ -414,8 +416,6 @@ window.__ModuleLoader__.load({
414
416
  "store.promptRename":"Enter a new name",
415
417
  "store.renamed":"Renamed to: {name}",
416
418
  "store.errRenameFailed":"Rename failed",
417
- "store.exported":"Exported: {name}",
418
- "store.errExportFailed":"Export failed",
419
419
  "store.saving":"Updating…",
420
420
  "store.savingNew":"Saving as new…",
421
421
  "workshop.greeting.entryLabel":"{title} · #{n}",
@@ -908,21 +908,10 @@ window.__ModuleLoader__.load({
908
908
  }catch(e){ set({lastMsg: String(e && e.message || e)}); }
909
909
  set({libraryBusy:null});
910
910
  }
911
- async function exportLibraryEntry(id){
912
- if(!id) return;
913
- set({libraryBusy:"export:"+id});
914
- try{
915
- var res=await rpc("cc_getLibraryEntry", {id:id});
916
- if(res && res.ok && res.value && res.value.draft){
917
- var draft=res.value.draft;
918
- var blob=new Blob([JSON.stringify(draft,null,2)],{type:"application/json"});
919
- var a=document.createElement("a"); a.href=URL.createObjectURL(blob); a.download=(res.value.meta.name||draft.data.name||"card")+".json"; a.click(); setTimeout(function(){ URL.revokeObjectURL(a.href); },800);
920
- set({lastMsg:t("store.exported",{name:(res.value.meta.name||id)})});
921
- } else set({lastMsg: res && res.error? String(res.error.message):t("store.errExportFailed")});
922
- }catch(e){ set({lastMsg: String(e && e.message || e)}); }
923
- set({libraryBusy:null});
924
- }
925
- return { getSnapshot:getSnapshot, subscribe:subscribe, setCur:setCur, setPanel:setPanel, setSideExpanded:setSideExpanded, setIsCcMode:setIsCcMode, setDraftFromHost:setDraftFromHost, setSessionId:setSessionId, setSlotWarn:setSlotWarn, setAlternateSlots:setAlternateSlots, migrateSlot:migrateSlot, checkIsCcMode:checkIsCcMode, setPollEnabled:setPollEnabled, setPollInterval:setPollInterval, setAutoValidate:setAutoValidate, setAutoSaveOnImport:setAutoSaveOnImport, updateDraft:updateDraft, validate:validate, exportJSON:exportJSON, exportPNG:exportPNG, embedIntoPng:embedIntoPng, exportCharx:exportCharx, importJSON:importJSON, importAny:importAny, setWorldPatch:setWorldPatch, setEditingWorld:setEditingWorld, cancelEditingWorld:cancelEditingWorld, setEditingText:setEditingText, clearEditingText:clearEditingText, cancelEditingText:cancelEditingText, updateEditingText:updateEditingText, addEntry:addEntry, setLibraryQuery:setLibraryQuery, toggleLibraryCollapsed:toggleLibraryCollapsed, refreshLibrary:refreshLibrary, saveCurrentToLibrary:saveCurrentToLibrary, saveAsNewToLibrary:saveAsNewToLibrary, clearCurrentLibraryId:clearCurrentLibraryId, clearDraft:clearDraft, loadFromLibrary:loadFromLibrary, deleteFromLibrary:deleteFromLibrary, exportLibraryEntry:exportLibraryEntry, renameCurrentInLibrary:renameCurrentInLibrary };
911
+ // 0.4.1:这里原本还有一个 exportLibraryEntry —— 直接下载某张已存角色的 JSON。
912
+ // 三个导出入口归一之后,卡片上的「导出」改成切到导出页,导出动作只剩导出页那一套实现,
913
+ // 这条旁路就没有调用者了(留着它等于「同一次导出有两套行为」),故删除。
914
+ return { getSnapshot:getSnapshot, subscribe:subscribe, setCur:setCur, setPanel:setPanel, setSideExpanded:setSideExpanded, setIsCcMode:setIsCcMode, setDraftFromHost:setDraftFromHost, setSessionId:setSessionId, setSlotWarn:setSlotWarn, setAlternateSlots:setAlternateSlots, migrateSlot:migrateSlot, checkIsCcMode:checkIsCcMode, setPollEnabled:setPollEnabled, setPollInterval:setPollInterval, setAutoValidate:setAutoValidate, setAutoSaveOnImport:setAutoSaveOnImport, updateDraft:updateDraft, validate:validate, exportJSON:exportJSON, exportPNG:exportPNG, embedIntoPng:embedIntoPng, exportCharx:exportCharx, importJSON:importJSON, importAny:importAny, setWorldPatch:setWorldPatch, setEditingWorld:setEditingWorld, cancelEditingWorld:cancelEditingWorld, setEditingText:setEditingText, clearEditingText:clearEditingText, cancelEditingText:cancelEditingText, updateEditingText:updateEditingText, addEntry:addEntry, setLibraryQuery:setLibraryQuery, toggleLibraryCollapsed:toggleLibraryCollapsed, refreshLibrary:refreshLibrary, saveCurrentToLibrary:saveCurrentToLibrary, saveAsNewToLibrary:saveAsNewToLibrary, clearCurrentLibraryId:clearCurrentLibraryId, clearDraft:clearDraft, loadFromLibrary:loadFromLibrary, deleteFromLibrary:deleteFromLibrary, renameCurrentInLibrary:renameCurrentInLibrary };
926
915
  }
927
916
 
928
917
  var S={
@@ -1158,12 +1147,17 @@ window.__ModuleLoader__.load({
1158
1147
  var focusRef=React.useState(null); var focusKey=focusRef[0], setFocusKey=focusRef[1];
1159
1148
  // 主区底部三块阅读辅助(实时预览 / CBS 速查 / 融合说明)默认全收起:占地方、但不常看
1160
1149
  var foldRef=React.useState({}); var openFold=foldRef[0], setOpenFold=foldRef[1];
1150
+ // 从「已存角色」卡片跳进导出页时的来源卡(几秒后自动清掉):用来给那枚按钮一个即时反馈,
1151
+ // 并在导出页把「导出的仍是工坊里这张、你点的那张还没载入」说清楚。清空后回到常态提示。
1152
+ var jumpRef=React.useState(null); var jumpFrom=jumpRef[0], setJumpFrom=jumpRef[1];
1161
1153
  React.useEffect(function(){ return store.subscribe(function(){ setS(store.getSnapshot()); }); }, [store]);
1162
1154
  React.useEffect(function(){
1163
1155
  if(s.panelOpen) store.refreshLibrary();
1164
1156
  }, [s.panelOpen]);
1165
1157
  if(!s.panelOpen) return null;
1166
- var steps=[t("step.world"), t("step.char"), t("step.lore"), t("step.export")];
1158
+ // 顶部胶囊与左侧导航都只有写作三步:导出不再是「第 4 步」(0.4.1 归一)——
1159
+ // 「校验导出」并进左下角那枚按钮,已存角色卡片上的「导出」也指向同一页,三处入口收成两处。
1160
+ var steps=[t("step.world"), t("step.char"), t("step.lore")];
1167
1161
  // 步骤项的三个视觉状态严格互斥:选中(active) / 键盘焦点(focus-visible) / 悬停(hover)。
1168
1162
  // 鼠标点击走 onMouseDown preventDefault,不把焦点抢到按钮上 —— 否则旧项的焦点环会留在原地,
1169
1163
  // 与新的选中项同时发亮,看起来像「同时选中了两个」(这是本次修的问题)。
@@ -1183,6 +1177,13 @@ window.__ModuleLoader__.load({
1183
1177
  }
1184
1178
  // 工具按钮(⛶ / 删除…)鼠标按下就拦掉默认行为:别把焦点抢走,否则又留下一圈焦点描边
1185
1179
  function stopFocusSteal(e){ if(e && e.preventDefault) e.preventDefault(); }
1180
+ // 卡片上的「导出」= 切到导出页 + 立刻反馈(按钮文案短暂变成「已切到 ✓」)。
1181
+ // 只切页本身太安静了:用户的视线在右侧卡片上,中栏换了内容他未必看得见。
1182
+ function jumpToExport(entry){
1183
+ store.setCur(3);
1184
+ setJumpFrom({ id:String((entry&&entry.id)||""), name:String((entry&&entry.name)||"") });
1185
+ try{ setTimeout(function(){ setJumpFrom(null); }, 6000); }catch(e){}
1186
+ }
1186
1187
  // 折叠区:主区底部那几块「阅读辅助」(实时 CCv3 预览 / CBS 速查 / 融合说明)。
1187
1188
  // 默认全部收起 —— 它们不常看,收起来只占一行标题,阅读区因此能拿到整幅宽度。
1188
1189
  function foldPanel(id, title, badge, body){
@@ -1339,7 +1340,10 @@ window.__ModuleLoader__.load({
1339
1340
  var v=s.valid;
1340
1341
  main=h("div",null,
1341
1342
  h("div",{style:S.readCard},
1342
- h("h3",{style:S.readTitle},t("workshop.export.title")),
1343
+ // 标题后面挂上「现在导的是哪张卡」:从已存角色卡片跳进来时,用户最需要确认的就是这个
1344
+ h("h3",{style:S.readTitle}, t("workshop.export.title"),
1345
+ h("span",{style:{marginLeft:8, fontSize:12, fontWeight:400, color:"var(--dsw-alias-label-secondary)"}},
1346
+ "· "+(String(s.draft.data.name||"").trim() || t("workshop.library.unnamed")))),
1343
1347
  h("div",{style:{display:"flex", flexWrap:"wrap", gap:8, marginBottom:8}},
1344
1348
  h("span",{style: Object.assign({}, S.badge, v.valid? {borderColor:"var(--dsw-alias-state-success-primary)", color:"#a6e8c8", background:"color-mix(in srgb, var(--dsw-alias-state-success-primary) 12%, var(--dsw-alias-bg-base))"}:{borderColor:"var(--dsw-alias-state-error-primary)", color:"#e8a6a6", background:"color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, var(--dsw-alias-bg-base))"})}, v.valid? t("workshop.export.passed"):t("workshop.export.failed")),
1345
1349
  h("span",{style:S.badge},t("common.groupGreetings",{n:s.draft.data.group_only_greetings.length})),
@@ -1348,13 +1352,20 @@ window.__ModuleLoader__.load({
1348
1352
  v.errors.length? h("div",{style:{background:"color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, var(--dsw-alias-bg-base))", border:"1px solid var(--dsw-alias-state-error-primary)", borderRadius:8, padding:8, marginBottom:8, color:"#e8a6a6", fontSize:12}}, v.errors.map(function(e){ return h("div",{key:e}, "• "+e); })):null,
1349
1353
  v.warnings.length? h("div",{style:{background:"color-mix(in srgb, var(--dsw-alias-border-l1) 40%, var(--dsw-alias-bg-base))", border:"1px solid var(--dsw-alias-border-l1)", borderRadius:8, padding:8, marginBottom:8, color:"var(--dsw-alias-label-secondary)", fontSize:12}}, v.warnings.map(function(e){ return h("div",{key:e}, "• "+e); })):null,
1350
1354
  h("div",{style:{display:"flex", flexWrap:"wrap", gap:8, margin:"12px 0"}},
1351
- h("button",{style:S.btn, onClick:function(){ store.exportJSON(); }}, "⬇ JSON"),
1355
+ h("button",{style:S.btn, onClick:function(){ store.exportJSON(); }}, t("action.exportJson")),
1352
1356
  h("button",{style: Object.assign({}, S.btn, {background:"#e05555"}), onClick:function(){ store.exportPNG(); }}, s.busy==="exportPng"? t("action.generating"):t("action.exportPng")),
1353
1357
  h("button",{style: Object.assign({}, S.btn, {background:"#e05555", opacity:0.92}), onClick:function(){ store.embedIntoPng(); }}, s.busy==="exportPng"? t("action.writing"):t("action.embedPng")),
1354
1358
  h("button",{style: Object.assign({}, S.btn, {background:"#2e86de"}), onClick:function(){ store.exportCharx(); }}, s.busy==="exportCharx"? t("action.generating"):t("action.exportCharx")),
1355
1359
  h("button",{style: Object.assign({}, S.btn, S.btnGhost), onClick:function(){ store.validate(); }}, s.busy==="validate"? t("action.validating"): t("action.validate")),
1356
1360
  h("span",{style:{color:"var(--dsw-alias-label-secondary)", fontSize:11, alignSelf:"center"}}, t("workshop.export.hint2"))
1357
1361
  ),
1362
+ // 导出页只认「当前工坊里的卡」:右侧卡片上的「导出」也是切到这一页,所以这里把口径写清楚
1363
+ // 从卡片跳进来、且那张卡不是工坊里这张时,把口径说透(导出的是谁、要导它该怎么做);
1364
+ // 其余情况只留常态说明。
1365
+ (jumpFrom && jumpFrom.id && jumpFrom.id!==s.currentLibraryId)
1366
+ ? h("div",{style:{fontSize:11, margin:"-4px 0 10px", lineHeight:1.7, color:"var(--dsw-alias-label-primary)", background:"rgba(255,170,60,.10)", border:"1px solid rgba(255,170,60,.45)", borderRadius:8, padding:"6px 10px"}},
1367
+ t("workshop.export.jumpedOther",{name:(String(s.draft.data.name||"").trim()||t("workshop.library.unnamed")), from:(jumpFrom.name||t("workshop.library.unnamed"))}))
1368
+ : h("div",{style:{fontSize:11, color:"var(--dsw-alias-label-secondary)", margin:"-4px 0 10px", lineHeight:1.7}}, t("workshop.export.currentHint")),
1358
1369
  field(t("workshop.export.creatorNotes"),"creator_notes",{rowKey:"notes"}),
1359
1370
  h("div",{style:{display:"grid", gridTemplateColumns:"1fr 1fr", gap:10}},
1360
1371
  field("creation_date","creation_date",{rowKey:"cdate", input:true, readOnly:true, bigBox:false, value:String(s.draft.data.creation_date)}),
@@ -1443,7 +1454,10 @@ window.__ModuleLoader__.load({
1443
1454
  h("div",{style:{fontSize:10, color:"var(--dsw-alias-label-secondary)", marginTop:4}}, new Date(entry.updatedAt*1000).toLocaleString()),
1444
1455
  h("div",{style:{display:"flex", gap:6, marginTop:8}},
1445
1456
  h("button",{style:Object.assign({}, S.btn, {padding:"4px 8px", fontSize:11, flex:1}), onClick:function(){ store.loadFromLibrary(entry.id); }, disabled: !!isBusy}, isBusy?"…":t("workshop.library.load")),
1446
- h("button",{style:Object.assign({}, S.btnGhost, {border:"1px solid var(--dsw-alias-border-l1)", background:"transparent", color:"var(--dsw-alias-label-secondary)", borderRadius:8, padding:"4px 8px", fontSize:11}), onClick:function(){ store.exportLibraryEntry(entry.id); }, disabled: !!isBusy}, t("workshop.library.export")),
1457
+ // 「导出」保留在原位,但改成切到导出页(0.4.1 归一):导出页导出的始终是当前工坊里的卡,
1458
+ // 想导这张已存角色先点「载入」—— 免得这里悄悄换掉用户正在编辑的草稿。
1459
+ // 点下去按钮自己先变(「已切到 ✓」),不然切页发生在中栏、用户视线却在右侧,会以为没反应。
1460
+ h("button",{style:Object.assign({}, S.btnGhost, {border:"1px solid var(--dsw-alias-border-l1)", background:"transparent", color:(jumpFrom && jumpFrom.id===entry.id)? "#a6e8c8" : "var(--dsw-alias-label-secondary)", borderRadius:8, padding:"4px 8px", fontSize:11}), title:t("workshop.library.exportTip"), onClick:function(){ jumpToExport(entry); }}, (jumpFrom && jumpFrom.id===entry.id)? t("workshop.library.exportJumped"): t("workshop.library.export")),
1447
1461
  h("button",{style:Object.assign({}, S.btnGhost, {border:"1px solid #3a2a2a", background:"transparent", color:"#e8a6a6", borderRadius:8, padding:"4px 8px", fontSize:11}), onClick:function(){ store.deleteFromLibrary(entry.id); }, disabled: !!isBusy}, t("common.delete"))
1448
1462
  )
1449
1463
  );
@@ -1464,10 +1478,20 @@ window.__ModuleLoader__.load({
1464
1478
  h("span",{style:S.badge}, t("panel.badge")),
1465
1479
  h("div",{style:{display:"flex", gap:6, alignItems:"center"}}, stepsEls)
1466
1480
  ),
1467
- h("div",{style:{display:"flex", gap:8}},
1468
- h("button",{style: Object.assign({}, S.btn, S.btnGhost, {padding:"6px 10px", fontSize:12}), onClick:function(){ store.setPanel(false); }}, t("action.close")),
1469
- h("button",{style: Object.assign({}, S.btn, {padding:"6px 10px", fontSize:12}), onClick:function(){ store.exportJSON(); }}, t("action.export"))
1470
- )
1481
+ // 工具条右侧只留关闭:这里原本还有一枚「⬇ 导出 JSON」(与导出页里的「⬇ JSON」重复,0.4.1 归一)。
1482
+ // 关闭做成 ✕ 图标按钮:不再占「收起」两个字的宽度,悬停给红底 —— 红色是「离开 / 收起」的通用暗示,
1483
+ // 平时保持低调的描边样式。hover 用局部 state(内联样式没有伪类),鼠标按下仍不抢焦点(同步骤项)。
1484
+ h("button",{type:"button", title:t("action.close"), "aria-label":t("action.close"),
1485
+ style:{ appearance:"none", boxSizing:"border-box", width:30, height:30, display:"flex", alignItems:"center", justifyContent:"center",
1486
+ padding:0, font:"inherit", fontSize:15, lineHeight:1, cursor:"pointer", borderRadius:8,
1487
+ background: hoverKey==="close"? "#e05555" : "transparent",
1488
+ border:"1px solid "+(hoverKey==="close"? "#e05555" : "var(--dsw-alias-border-l1)"),
1489
+ color: hoverKey==="close"? "#fff" : "var(--dsw-alias-label-primary)",
1490
+ transition:"background .12s ease, border-color .12s ease, color .12s ease" },
1491
+ onMouseDown:stopFocusSteal,
1492
+ onMouseEnter:function(){ setHoverKey("close"); },
1493
+ onMouseLeave:function(){ setHoverKey(null); },
1494
+ onClick:function(){ store.setPanel(false); }}, "✕")
1471
1495
  ),
1472
1496
  (function(){
1473
1497
  var st = s.draftStatus;
@@ -1503,11 +1527,15 @@ window.__ModuleLoader__.load({
1503
1527
  navItem(0, t("step.world"), t("step.world.desc")),
1504
1528
  navItem(1, t("step.char"), t("step.char.desc")),
1505
1529
  navItem(2, t("step.lore"), t("step.lore.desc")),
1506
- navItem(3, t("step.export"), t("step.export.desc")),
1507
1530
  h("div",{style:{marginTop:"auto", paddingTop:12, borderTop:"1px solid var(--dsw-alias-border-l1)", display:"flex", flexDirection:"column", gap:8}},
1531
+ // 「校验导出」入口:这一步原本是导航里的第 4 项,与这枚按钮重复,0.4.1 合并成一枚。
1532
+ // 点它切到导出页(校验按钮在那一页里)。
1533
+ // ⚠️ 它**不做选中态**:曾经给过淡紫底 + aria-current,结果从右侧卡片点「导出」时,
1534
+ // 这枚按钮和卡片那枚「已切到 ✓」会同时发亮 —— 又是「两处看起来像选中」(0.4.0 修过的同一个坑)。
1535
+ // 「现在在导出页」靠中栏标题表达(标题里写着卡名),不靠按钮。
1508
1536
  h("button",{type:"button", style: Object.assign({}, S.btn, {padding:"6px 10px", fontSize:12}),
1509
1537
  onMouseDown:function(e){ if(e && e.preventDefault) e.preventDefault(); }, // 鼠标点击不抢焦点,免得又留下一圈焦点描边(同左侧步骤项)
1510
- onClick:function(){ store.validate(); }}, s.busy==="validate"? t("action.validating"): t("action.validate")),
1538
+ onClick:function(){ store.setCur(3); }}, t("step.export")),
1511
1539
  // 清空是破坏性操作:先过二次确认才动手;工坊里没有撤销,所以按钮用危险色并与「校验」分开放
1512
1540
  h("button",{type:"button", style: Object.assign({}, S.btn, S.btnGhost, {padding:"6px 10px", fontSize:12, borderColor:"rgba(224,85,85,.55)", color:"#e05555"}),
1513
1541
  onMouseDown:function(e){ if(e && e.preventDefault) e.preventDefault(); },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xia-sc/dsh-cc-studio",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "CCv3 角色卡工坊:融合工坊 + CC 模式 + JSON/PNG/CHARX + 中英双语(跟随全局)",
5
5
  "keywords": [
6
6
  "dsh",
@@ -38,7 +38,7 @@
38
38
  "presets"
39
39
  ],
40
40
  "scripts": {
41
- "test": "node tests/rpc-channel.test.mjs && node tests/preset-install.test.mjs && node tests/cc-detection.test.mjs && node tests/client-greetings.test.mjs && node tests/draft-slot-sync.test.mjs && node tests/nav-highlight.test.mjs && node tests/clear-card.test.mjs && node tests/data-root.test.mjs",
41
+ "test": "node tests/rpc-channel.test.mjs && node tests/preset-install.test.mjs && node tests/cc-detection.test.mjs && node tests/client-greetings.test.mjs && node tests/draft-slot-sync.test.mjs && node tests/nav-highlight.test.mjs && node tests/clear-card.test.mjs && node tests/export-entry.test.mjs && node tests/close-button.test.mjs && node tests/data-root.test.mjs",
42
42
  "prepublishOnly": "npm test"
43
43
  },
44
44
  "dsh": {