@xia-sc/dsh-cc-studio 0.10.0 → 0.10.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -14,7 +14,7 @@ DSH(DeepSeek Harness)插件:输入框上方的胶囊 → 全屏融合工
14
14
 
15
15
  | 项目 | 值 |
16
16
  | --- | --- |
17
- | 包名 / 版本 | `@xia-sc/dsh-cc-studio` `0.10.0` |
17
+ | 包名 / 版本 | `@xia-sc/dsh-cc-studio` `0.10.3` |
18
18
  | 兼容 | DSH `>=0.4.5-rc.4`(含**桌面版**)、Node `>=22`、**零运行依赖**;逐版本实测见 [兼容与权限](#兼容与权限) |
19
19
  | 落盘位置 | `<DSH_HOME>/cc-library/`(角色卡)、`<DSH_HOME>/cc-drafts/`(会话草稿);`DSH_HOME` 默认 `~/.dsh`,旧位置仍读得到 |
20
20
  | 文档 | 用户文档=本文件;改代码前必读 [AGENTS.md](./AGENTS.md);完整历史 [CHANGELOG.md](./CHANGELOG.md) |
@@ -42,7 +42,7 @@ DSH(DeepSeek Harness)插件:输入框上方的胶囊 → 全屏融合工
42
42
  dsh --profile <profile> --dump-config
43
43
  期望在 `# == @xia-sc/dsh-cc-studio` 那一层下看到两行:`id: dsh-cc-studio`(宿主行)与 `id: preset-cc`(CC 预设行)。
44
44
  4. 告诉用户重启:宿主行只在启动时组合 —— CLI 重启 `dsh web`,桌面版退出并重新打开应用。
45
- 5. 重启后可选自检:在「CC 模式」会话里调任意一个 cc_* 工具,返回里带 "build": "0.10.0" 即说明新代码已加载
45
+ 5. 重启后可选自检:在「CC 模式」会话里调任意一个 cc_* 工具,返回里带 "build": "0.10.3" 即说明新代码已加载
46
46
  (这也是判断「改了但没重启」的唯一可靠依据)。
47
47
  6. 不要做的事:
48
48
  - 不要手工把 presets/ 拷进 ~/.dsh/.agent-presets/(dsh >= 0.4.7-alpha.4 已不读该目录,拷了也不生效);
@@ -58,9 +58,9 @@ DSH(DeepSeek Harness)插件:输入框上方的胶囊 → 全屏融合工
58
58
  ```bash
59
59
  # 从 npm 安装(推荐)
60
60
  dsh plugin --profile web add @xia-sc/dsh-cc-studio
61
- dsh plugin --profile web add @xia-sc/dsh-cc-studio@0.10.0 # 锁定版本
61
+ dsh plugin --profile web add @xia-sc/dsh-cc-studio@0.10.3 # 锁定版本
62
62
 
63
- # 从 GitHub 源安装(可加 #v0.10.0 / #master 指定 tag / 分支)
63
+ # 从 GitHub 源安装(可加 #v0.10.3 / #master 指定 tag / 分支)
64
64
  dsh plugin --profile web add github:xia-sc/dsh-cc-studio
65
65
 
66
66
  # 从本地仓库安装(开发 / 调试):装出来是 link:,改 lib/*.js 重启即生效
@@ -92,6 +92,8 @@ dsh plugin --profile web remove @xia-sc/dsh-cc-studio # 卸载(同时移
92
92
  | 重启后浏览器报 `Failed to load plugins` / `web boot: 4 entry did not activate` | 包名迁移后没按新名重装(profile 里的安装身份还是旧名)。看一眼 `~/.dsh/profiles/web/node_modules/` 下的目录名是否等于 `package.json` 的 `name`;修法是按新名重装 → 重启 → **硬刷新**(`Ctrl+Shift+R`) |
93
93
  | 工坊顶部橙条「草稿已新建」 | 该会话没有历史草稿(首次使用或草稿目录被清)。已存档会话恢复时是蓝条并显示 `creation_date`,下次写入后提示消失 |
94
94
  | 改完代码没变化 | 分三层看:改 `lib/client.js` → 刷新页面;改 `lib/index.js`(宿主)或预设 → **重启 dsh(桌面版重启应用)**;先看一眼工具返回里的 `build` 是不是你以为的版本 |
95
+ | 偶发「本轮运行失败 `tool input is invalid JSON`」 | 模型生成的工具参数 JSON 有语法错误(最常见是尾逗号),DSH provider 严格 `JSON.parse` 后判 `MALFORMED_RESPONSE`,且默认不重试。0.10.2 已在 persona 与五个写类工具描述里要求「严格 JSON + 分小步写(数组字段单独一批)」,**重启 dsh 让新文本生效**;仍偶发就重发同一句话(模型会重新生成),或在 profile 的 `cordis.patch.yml` 给 `llm-deepseek-account` 加 `retryPolicy`(`retryableCodes` 要**写全默认 5 码 + `MALFORMED_RESPONSE`** —— 补丁层对 config 是整条替换、不做深合并),完整片段见 CHANGELOG 0.10.2 |
96
+ | 卡里的换行显示成字面 `\n`(如「说完这句话。\n\n下一段」) | 模型在 tool call 里把换行写成了字面 `\n`(JSON 多转义一层),旧版工具原样落盘。0.10.3 起写类工具统一解码字面 `\n` / `\r` / `\t`(含多层转义),**重启 dsh 生效**。⚠️ 已存卡里已经被写坏的字段不会自动修复 —— 重新调一次对应工具写入那几个字段,或在工坊里手动改回真换行 |
95
97
 
96
98
  ---
97
99
 
@@ -174,8 +176,8 @@ dsh plugin --profile web remove @xia-sc/dsh-cc-studio # 卸载(同时移
174
176
  ## 开发与测试
175
177
 
176
178
  ```bash
177
- npm test # 22 个文件、852 项断言,全绿才算过
178
- node tests/tools-contract.test.mjs # 只跑 CC 工具链契约(97 项)
179
+ npm test # 22 个文件、864 项断言,全绿才算过
180
+ node tests/tools-contract.test.mjs # 只跑 CC 工具链契约(104 项)
179
181
  node tests/group-layer-guard.test.mjs # 只跑卡组层的耦合守卫(16 项)
180
182
  ```
181
183
 
@@ -186,6 +188,12 @@ node tests/group-layer-guard.test.mjs # 只跑卡组层的耦合守卫(1
186
188
 
187
189
  完整历史见 [CHANGELOG.md](./CHANGELOG.md)。最近几版大多是「另一个 LLM 以工具使用者身份跑回归」挖出来的问题:
188
190
 
191
+ - `0.10.3` **多行文本被写成字面 `\n`**:用户实机报告(六步全链路 `valid: true`)发现经 `cc_patch_character` / `cc_patch_greetings` / `cc_patch_world` 写入的字符串,换行全部落盘成字面 `\` + `n`,导进 SillyTavern 显示成「说完这句话。\n\n下一段」。取证推翻了「工具多转义」的猜测:工具层往返测试证明真换行原样保留;会话日志里模型 18 次调用有 6 次在 JSON 里把换行写成 `\\n`(多转义一层),而 `alternate_greetings` / lorebook 却写对了 —— 是**模型输出不一致**。修法:写类工具统一 `textArg()` 解码字面 `\n` / `\r` / `\t`(多层转义最多 4 轮收敛),覆盖角色 / 问候语 / 世界书条目 / 书级元信息 / 共享书;工坊保存与导入路径不动。顺带修报告 P2(卡内 `token_budget` 默认 1200 → **2400**)与 P4(autoLorebook 的「力量体系」条目裸 `@@activate` → `@@activate 100`)。测试 858 → **864 项**。**重启 dsh 生效**;已存卡里已写坏的字段需重写。
192
+
193
+ - `0.10.2` **模型偶发一个 JSON 尾逗号,整轮就没了**:报错是 `本轮运行失败 DeepSeek Messages stream: tool input is invalid JSON`。会话日志解帧取证:模型调 `cc_patch_character` 时生成的 arguments 末尾是 `..."tags":[...],}`(尾逗号)。根因在 provider:`@deepseek-ai/dsh-llm-deepseek` 在流结束时对 tool input 严格 `JSON.parse`,失败即 `MALFORMED_RESPONSE`,而默认 `retryableCodes` 不含它 → 不自动重试;工具从未执行、卡片数据没坏。插件能施加影响的只有模型可见文本,所以给 persona 加「工具参数必须是严格合法 JSON(禁尾逗号/注释/单引号/未转义换行),一次只传 1-3 个字段、长字段分多次写」,五个写类工具的 description 同步这句规则,并用测试钉住(preset-install + tools-contract 各 +1 项)。用户侧可选兜底:在 profile `cordis.patch.yml` 给 `llm-deepseek-account` 配 `retryPolicy.retryableCodes` 含 `MALFORMED_RESPONSE`(详见 CHANGELOG 0.10.2)。测试 856 → **858 项**。**重启 dsh(桌面版重启应用)生效**。
194
+
195
+ - `0.10.1` **dsh 0.2.1-rc.2 上 CC 模式每轮「本轮运行失败」**:报错 `unknown prompt variable "{{cwd}}" in section "deployment:persona-prefix"; registered variables: provider, model`。0.2.1 的 `dsh-system-prompt` 只注册 `provider` / `model` 两个 prompt 变量(工作目录改由 working-directory 服务以 runtime context 提供),而段文本插值对未知引用**严格抛错**、发生在任何模型请求之前 —— 预设本身挂载正常、roster 里也正常可选,唯独每一轮对话失败。修法:两份预设清单(`presets/cc.patch.yml` + `presets/cc/agent.cordis.yml`)的 persona prefix 删掉「,工作目录 {{cwd}}」、保留 `{{model}}`;把 prompt 变量白名单(只允许 `provider` / `model`、禁止 `{{cwd}}`、两份集合一致、`{{model}}` 仍在)钉进 `tests/preset-install.test.mjs`。测试 852 → **856 项**。**重启 dsh(桌面版重启应用)生效**,无需重装或刷新页面。
196
+
189
197
  - `0.10.0` **卡组不再是一堆工具的组合,而是一层**:0.9.x 的两轮补丁都打在同一个地方 —— 组级共享世界上线后,`simulate` / `lint` / `group_check` / `preview_export` / `validate` 五个「读数」工具**各自**补了一遍「把共享书算进来」(「改了四个、漏了第五个」)。根因不是手滑,而是同一个语义存在多份实现:卡组数据读取两半各一份、取作用域每个工具各写一遍、卡组级统计与世界书统计平行两套、界面再自己算一遍。修法是立三层并**让两半共用同一个文件** `lib/group-layer.js`(纯函数、零 IO):**数据层** `readLibrarySnapshot()` + `groupScope()`(来源卡 → 组 → 成员 → 书,唯一入口,支持 `include` / `maxCards` / 传快照避免重复读盘)、**口径层** `groupMetrics()`(所有「多少条 / 多少 token」只在这里产生,五段 + 派生量;`lorebookMetrics` 变薄包装、旧输出逐字不变)、**检查层** `runGroupChecks()` + `GROUP_CHECKS` 注册表(一份清单、一套 issue 形状、一个 `byCheck` 计数)。`checkGroup` 只剩 6 行薄包装(以前 48 行 if 序列),宿主端点、工具与既有测试一行未改。**第五个功能作为验收用例落地**:新增 **cross-card** 类别 —— 跨卡触发词冲突(只报涉及 ≥2 个来源的)、卡内条目与共享书逐字重复、组内五维漂移、成员卡全 auto,外加 **lore-quality**(共享书自身条目质量);这五项只改了注册表,五个既有工具的口径代码**一行未动**(`tests/group-layer-guard.test.mjs` 钉住)。入口仍是 `cc_group_check` / `cc_groupCheck`(新增 `checks` 参数与 `byCheck`),界面把体检结果从「一行拼接」改成按 error / warn 分组的清单。顺带:组体检读盘 `1+2N` → `1+N`;记录一个已知不可达检查(`type-unknown` —— `normalizeGroup` 早把未知 type 归一到 `other`)。测试 735 → **852 项(22 个文件)**。**需重启 dsh(桌面版重启应用)→ 刷新页面**。
190
198
 
191
199
  - `0.9.1` **共享世界书导不进 SillyTavern**:把 CCv3 独立 lorebook(`{spec:"lorebook_v3", data}`)拖进「世界信息 → 导入」只会得到 `Error importing world info: … Bad Request`。根因有两层(对着本地 ST 1.19.0 源码 + 实测复现):服务端 `/api/worldinfo/import` 拿到文件第一句就是 `if (!('entries' in worldContent)) throw` —— 顶层没有 `entries` 一律 **400**;而 ST 客户端 `importWorldInfo()` 只转 Novel / Agnai / Risu,整份 `world-info.js` 里搜不到 `lorebook_v3`(**它不是「换个后缀」能解决的**)。另外 ST 的 `parseDecorators()` 只认 `@@activate` / `@@dont_activate`,其余 `@@` 行会**连同正文一起被丢掉**。修法是多一种形态而非改掉原来那种:共享书页与导出页各多一枚「⬇ SillyTavern 世界书」,导出 `{entries:{"<uid>":{…}}}`,字段照抄 ST 自己的 `convertCharacterBook()`,`@@position` / `@@depth` 翻译成 ST 的 `position` / `depth` 并从正文删掉(`@@activate` 原样留给 ST),文件名 = 书名(ST 用上传文件名当世界名);**CCv3 那份一个字节没动**。实测:同一本真书导出的 ST 文件导入返回 **200 + `{"name":…}`**、ST 落盘、条目 uid 与源 `id` 一致、「年表」落在 `position:1`。测试 647 → 683 项(16 个文件)。**同一版还补齐了 SillyTavern 兼容层**:PNG 现在同时写 `ccv3`(纯 CCv3)与 `chara`(诚实的 V2 投影,按规范摘掉装饰器)两个块,读取器改成**按载荷里的 spec 判并优先 `ccv3`**(此前是「取第一个命中的块」,而 ST 导出的 PNG 是 `chara` 在前,于是把 ST 导出的卡拖进工坊会报「spec 需为 chara_card_v3」);导出 PNG / CHARX / JSON 时若该卡所属分组有共享书,会**只在导出副本上**写 ST 的世界书绑定(`data.extensions.world = 书名`)—— ST 里的顺序是先导世界书、再导卡,绑定就自动接上(卡内私有条目还要点一次 Import Card Lore)。规范同样明文禁止往卡片里加应用私有字段,所以卡片顶层一个 ST 私有键都不加。实测:我们导出的双块 PNG 进 ST 返回 **200**,ST 回读 `data.extensions.world`、卡内 3 条书条目与 V3 独有字段俱在且世界名可解析;ST 落盘的「V2 在前」PNG 我们也能读了。测试 683 → **718 项(17 个文件)**。**同一版还有第三步:V2 卡现在能进工坊了** —— SillyTavern 里绝大多数存量卡是 V2(ST 界面新建的就是 V2 形状,只是 `spec` 被改写成 v3 标签),此前拖 PNG/JSON 进来只会得到「spec 需为 chara_card_v3」。现在 `upgradeCardToV3()` 一份实现管 PNG / CHARX / JSON 三条导入路径(规则照 ST 的 `readFromV2()`:顶层 V1 平铺字段只在 `data` 缺值时补、`creatorcomment`→`creator_notes`、`talkativeness`/`fav`→`data.extensions`、纯 V1 整份包成 `data`),导入返回带 `upgradedFrom`、界面明说「V2 卡已升级为 CCv3」(不静默变换);JSON 导入也收口到宿主的新端点 `cc_importFromJson`(客户端不再自己 parse 卡 JSON)。`cc_setDraft` 故意不升级(它是工坊自己草稿的镜像)。测试 718 → **735 项(17 个文件)**。**需重启 dsh(桌面版重启应用)→ 刷新页面**。
package/lib/agent.js CHANGED
@@ -19,10 +19,37 @@ const name = "dsh-cc-studio-agent";
19
19
  // 由来:第五轮实测里测试者第一遍跑出「四项全无变化」,其实只是**没重启 dsh** —— LLM 侧完全无法
20
20
  // 分辨「代码没改」和「改了但没加载」。有了这个字段,任何一次调用都能先确认自己测的是哪一版。
21
21
  // 发版时与 package.json 的 version 同步。
22
- const BUILD = "0.10.0";
22
+ const BUILD = "0.10.3";
23
23
  const inject = ["tools", "agents"];
24
24
 
25
25
  function nowSec(){ return Math.floor(Date.now()/1000); }
26
+
27
+ // ── 字面转义解码(0.10.3,用户实机报告 P1)──────────────────────────────────────────
28
+ // 模型在 tool call 的 JSON 里经常把换行写成**字面** `\n`(JSON 层多转义一层:`"A\\nB"`),
29
+ // JSON.parse 之后字符串里就是「反斜杠 + n」两个字符。以前这些工具原样落盘,导出的卡在
30
+ // SillyTavern 里就显示「说完这句话。\n\n下一段」——同一张卡里有的字段正常、有的不正常
31
+ // (模型这次写对了),肉眼极难发现(用户实测 18 次调用里 6 次中招)。
32
+ // 卡内文本没有「字面反斜杠 n」的正当用途,所以写类工具统一把字面 `\n` / `\r` / `\t` 解成
33
+ // 真字符;多层转义(`A\\\\nB`)逐层收敛(最多 4 轮)。只作用于**模型输入的工具参数**,
34
+ // 不动工坊保存与导入路径(那两条链路本来就是真换行)。
35
+ function decodeEscapedText(v){
36
+ if(typeof v !== "string" || v.indexOf("\\") < 0) return v;
37
+ var s = v;
38
+ for(var round = 0; round < 4; round++){
39
+ var next = s
40
+ .replace(/\\\\/g, "\u0000") // 成对反斜杠先占位,避免被下面的 \n 规则重复匹配
41
+ .replace(/\\n/g, "\n")
42
+ .replace(/\\r/g, "\r")
43
+ .replace(/\\t/g, "\t")
44
+ .replace(/\u0000/g, "\\");
45
+ if(next === s) break;
46
+ s = next;
47
+ }
48
+ return s;
49
+ }
50
+ /** 工具参数里的文本字段:先解码字面转义,再按旧规则 String()。 */
51
+ function textArg(v){ return decodeEscapedText(String(v == null ? "" : v)); }
52
+
26
53
  function draftsMap(){ if(!globalThis.__CC_DRAFTS__) globalThis.__CC_DRAFTS__ = new Map(); return globalThis.__CC_DRAFTS__; }
27
54
  // —— 落盘根(0.3.6 起统一,与 lib/index.js 是同一套):DSH_HOME 优先,homedir()/.dsh 兜底 ——
28
55
  // 草稿与角色库都必须与宿主半算出**同一个路径**,否则又会退化成 issue #5 那种「两边互相看不见」。
@@ -219,7 +246,7 @@ function makeEmptyCard(){
219
246
  first_mes:"", alternate_greetings:[], group_only_greetings:[], mes_example:"",
220
247
  creator_notes:"", creator_notes_multilingual:{}, source:[], assets:[{type:"icon",uri:"ccdefault:",name:"main",ext:"png"}],
221
248
  creation_date: nowSec(), modification_date: nowSec(),
222
- character_book:{ name:"", description:"", scan_depth:4, token_budget:1200, recursive_scanning:false, extensions:{}, entries:[] }
249
+ character_book:{ name:"", description:"", scan_depth:4, token_budget:2400, recursive_scanning:false, extensions:{}, entries:[] }
223
250
  }
224
251
  };
225
252
  }
@@ -601,7 +628,7 @@ async function libraryCardsFor(ids){
601
628
  name: String(d.name || ""),
602
629
  description: String(d.description || ""),
603
630
  constantTokens: entries.filter(e => e && e.constant).reduce((n, e) => n + estimateTokensLite(e && e.content), 0),
604
- tokenBudget: Number(d.character_book && d.character_book.token_budget) || 1200,
631
+ tokenBudget: Number(d.character_book && d.character_book.token_budget) || 2400,
605
632
  });
606
633
  }
607
634
  return out;
@@ -1104,7 +1131,7 @@ function apply(ctx){
1104
1131
 
1105
1132
  tool({
1106
1133
  name: "cc_patch_character",
1107
- description: "【工作流第2步·共创】写入角色基础(name/nickname/description/personality/scenario/system_prompt/tags)。调用前必须已与用户讨论:至少问 1 个开放问题(如想要的气质/性别/年龄/与你的关系张力/禁忌),拿到用户偏好后再填;禁止仅凭世界观自行推断一次性填满。必须在 cc_get_card 之后、五维之前调用,之后下一步是 cc_patch_world。",
1134
+ description: "【工作流第2步·共创】写入角色基础(name/nickname/description/personality/scenario/system_prompt/tags)。调用前必须已与用户讨论:至少问 1 个开放问题(如想要的气质/性别/年龄/与你的关系张力/禁忌),拿到用户偏好后再填;禁止仅凭世界观自行推断一次性填满。必须在 cc_get_card 之后、五维之前调用,之后下一步是 cc_patch_world。一次只传 1-3 个字段(长字段分多次调用写);tags 等数组字段单独一次调用。参数必须是严格合法的 JSON(最后一个字段/数组元素后不写逗号,不要 ```json 代码块包裹,不要注释与单引号;字符串内换行要转义)。",
1108
1135
  parameters: {
1109
1136
  type:"object",
1110
1137
  properties:{
@@ -1135,16 +1162,16 @@ function apply(ctx){
1135
1162
  const changed = [];
1136
1163
  // B2(0.7.1):`changed` 与 `rev` 用同一套判据 —— 只有值真的变了才记改动,否则会出现
1137
1164
  // 「返回说改了 creator、rev 却纹丝不动」这种自相矛盾(第五轮实测 B2)。
1138
- if(args.name!==undefined) assignIfChanged(cur.data, "name", String(args.name).slice(0,120), changed);
1139
- if(args.nickname!==undefined) assignIfChanged(cur.data, "nickname", String(args.nickname).slice(0,120), changed);
1140
- if(args.description!==undefined) assignIfChanged(cur.data, "description", String(args.description).slice(0,5000), changed);
1141
- if(args.personality!==undefined) assignIfChanged(cur.data, "personality", String(args.personality).slice(0,5000), changed);
1142
- if(args.scenario!==undefined) assignIfChanged(cur.data, "scenario", String(args.scenario).slice(0,5000), changed);
1143
- if(args.system_prompt!==undefined) assignIfChanged(cur.data, "system_prompt", String(args.system_prompt).slice(0,3000), changed);
1144
- if(args.post_history_instructions!==undefined) assignIfChanged(cur.data, "post_history_instructions", String(args.post_history_instructions).slice(0,3000), changed);
1145
- if(Array.isArray(args.tags)) assignIfChanged(cur.data, "tags", args.tags.map(String).slice(0,12), changed);
1146
- if(args.creator_notes!==undefined) assignIfChanged(cur.data, "creator_notes", String(args.creator_notes).slice(0,3000), changed);
1147
- if(args.creator!==undefined) assignIfChanged(cur.data, "creator", String(args.creator).slice(0,120), changed);
1165
+ if(args.name!==undefined) assignIfChanged(cur.data, "name", textArg(args.name).slice(0,120), changed);
1166
+ if(args.nickname!==undefined) assignIfChanged(cur.data, "nickname", textArg(args.nickname).slice(0,120), changed);
1167
+ if(args.description!==undefined) assignIfChanged(cur.data, "description", textArg(args.description).slice(0,5000), changed);
1168
+ if(args.personality!==undefined) assignIfChanged(cur.data, "personality", textArg(args.personality).slice(0,5000), changed);
1169
+ if(args.scenario!==undefined) assignIfChanged(cur.data, "scenario", textArg(args.scenario).slice(0,5000), changed);
1170
+ if(args.system_prompt!==undefined) assignIfChanged(cur.data, "system_prompt", textArg(args.system_prompt).slice(0,3000), changed);
1171
+ if(args.post_history_instructions!==undefined) assignIfChanged(cur.data, "post_history_instructions", textArg(args.post_history_instructions).slice(0,3000), changed);
1172
+ if(Array.isArray(args.tags)) assignIfChanged(cur.data, "tags", args.tags.map(textArg).slice(0,12), changed);
1173
+ if(args.creator_notes!==undefined) assignIfChanged(cur.data, "creator_notes", textArg(args.creator_notes).slice(0,3000), changed);
1174
+ if(args.creator!==undefined) assignIfChanged(cur.data, "creator", textArg(args.creator).slice(0,120), changed);
1148
1175
  setDraft(ctx, cur, exec);
1149
1176
  const wf = await workflowFor(ctx, exec, cur);
1150
1177
  const warnings = [];
@@ -1161,7 +1188,7 @@ function apply(ctx){
1161
1188
 
1162
1189
  tool({
1163
1190
  name: "cc_patch_world",
1164
- description: "【工作流第3步·共创·必调】写入 5 维世界观(timeline/factions/geo/power/daily 至少 3 项)。若用户已提供世界观文档,先用 2-3 句归纳你理解的 5 维,再问用户“哪几维最想展开?有无私设要加?”;若世界观薄弱,则与用户讨论 1-2 个薄弱点后再填。严禁未与用户讨论就直接照搬文档填满 5 维。⚠️ 若本卡已在**有共享世界书**的分组里,本工具默认**不再**往卡内抄五维骨架(那些共同设定属于共享书,卡内副本会与它双重插入)—— 要卡内副本才显式传 autoLorebook:true。调后下一步是 cc_add_lorebook_entries。",
1191
+ description: "【工作流第3步·共创·必调】写入 5 维世界观(timeline/factions/geo/power/daily 至少 3 项)。若用户已提供世界观文档,先用 2-3 句归纳你理解的 5 维,再问用户“哪几维最想展开?有无私设要加?”;若世界观薄弱,则与用户讨论 1-2 个薄弱点后再填。严禁未与用户讨论就直接照搬文档填满 5 维。⚠️ 若本卡已在**有共享世界书**的分组里,本工具默认**不再**往卡内抄五维骨架(那些共同设定属于共享书,卡内副本会与它双重插入)—— 要卡内副本才显式传 autoLorebook:true。调后下一步是 cc_add_lorebook_entries。loreKeys 这类对象字段建议单独一次调用;参数必须是严格合法的 JSON(最后一个字段/数组元素后不写逗号,不要 ```json 代码块包裹,不要注释与单引号)。",
1165
1192
  parameters: {
1166
1193
  type:"object",
1167
1194
  properties:{
@@ -1188,11 +1215,11 @@ function apply(ctx){
1188
1215
  const recNote = recoveredNotice(k, cur);
1189
1216
  cur.data.extensions = cur.data.extensions || {};
1190
1217
  const w = cur.data.extensions.cc_world || {};
1191
- if(args.timeline!==undefined) w.timeline = String(args.timeline).slice(0,1000);
1192
- if(args.factions!==undefined) w.factions = String(args.factions).slice(0,1000);
1193
- if(args.geo!==undefined) w.geo = String(args.geo).slice(0,1000);
1194
- if(args.power!==undefined) w.power = String(args.power).slice(0,1000);
1195
- if(args.daily!==undefined) w.daily = String(args.daily).slice(0,1000);
1218
+ if(args.timeline!==undefined) w.timeline = textArg(args.timeline).slice(0,1000);
1219
+ if(args.factions!==undefined) w.factions = textArg(args.factions).slice(0,1000);
1220
+ if(args.geo!==undefined) w.geo = textArg(args.geo).slice(0,1000);
1221
+ if(args.power!==undefined) w.power = textArg(args.power).slice(0,1000);
1222
+ if(args.daily!==undefined) w.daily = textArg(args.daily).slice(0,1000);
1196
1223
  cur.data.extensions.cc_world = w;
1197
1224
  // 0.10.0:默认值必须看得见「组里已经有什么」——
1198
1225
  // 没显式传 autoLorebook 时,若本卡所属分组的共享书已覆盖这些维度,就**不在卡内生成副本**:
@@ -1247,7 +1274,7 @@ function apply(ctx){
1247
1274
  if(w.timeline) push("timeline", "@@position after_desc\n"+w.timeline, 10);
1248
1275
  if(w.factions) push("factions", "@@depth 4\n"+w.factions, 9);
1249
1276
  if(w.geo) push("geo", "@@position scenario\n"+w.geo, 8);
1250
- if(w.power) push("power", "@@activate\n"+w.power, 9, true);
1277
+ if(w.power) push("power", "@@activate 100\n"+w.power, 9, true);
1251
1278
  if(w.daily) push("daily", "@@depth 5\n"+w.daily, 7);
1252
1279
  }
1253
1280
  setDraft(ctx, cur, exec);
@@ -1309,7 +1336,7 @@ function apply(ctx){
1309
1336
 
1310
1337
  tool({
1311
1338
  name: "cc_add_lorebook_entries",
1312
- description: "【工作流第4步·共创·必调】添加 1-5 条 Lorebook,补到至少 5 条(年表/势力/地理/力量/日常各 1,至少 1 条 constant,@@decorator)。调用前应与用户讨论:问用户想要哪些触发词/常驻条目/要藏什么彩蛋,拿到偏好后再生成。必须在五维之后、问候语之前调用,禁止一次性无讨论批量生成。",
1339
+ description: "【工作流第4步·共创·必调】添加 1-5 条 Lorebook,补到至少 5 条(年表/势力/地理/力量/日常各 1,至少 1 条 constant,@@decorator)。调用前应与用户讨论:问用户想要哪些触发词/常驻条目/要藏什么彩蛋,拿到偏好后再生成。必须在五维之后、问候语之前调用,禁止一次性无讨论批量生成。条目较多时分成多次调用(一次 1-2 条);参数必须是严格合法的 JSON(数组元素之间不写尾逗号,最后一个元素后不写逗号,不要 ```json 代码块包裹,不要注释与单引号)。",
1313
1340
  parameters: {
1314
1341
  type:"object",
1315
1342
  properties:{
@@ -1341,14 +1368,14 @@ function apply(ctx){
1341
1368
  const recNote = recoveredNotice(k, cur);
1342
1369
  args.entries.forEach(function(e){
1343
1370
  cur.data.character_book.entries.push({
1344
- keys: Array.isArray(e.keys)? e.keys.map(String): [],
1345
- content: String(e.content).slice(0,5000),
1371
+ keys: Array.isArray(e.keys)? e.keys.map(textArg): [],
1372
+ content: textArg(e.content).slice(0,5000),
1346
1373
  enabled:true,
1347
1374
  insertion_order: cur.data.character_book.entries.length,
1348
1375
  case_sensitive:false,
1349
1376
  use_regex: !!e.use_regex,
1350
1377
  constant: !!e.constant,
1351
- name: String(e.name||"未命名").slice(0,60),
1378
+ name: textArg(e.name||"未命名").slice(0,60),
1352
1379
  priority: typeof e.priority==="number"? e.priority: 5,
1353
1380
  id: Date.now()+Math.floor(Math.random()*10000)
1354
1381
  });
@@ -1430,9 +1457,9 @@ function apply(ctx){
1430
1457
  const e = cur.data.character_book.entries.find(x=> x.id===args.id);
1431
1458
  if(!e) return { ok:false, error:"未找到 id="+args.id };
1432
1459
  const changed = [];
1433
- if(args.name!==undefined) assignIfChanged(e, "name", String(args.name).slice(0,60), changed);
1434
- if(Array.isArray(args.keys)) assignIfChanged(e, "keys", args.keys.map(String), changed);
1435
- if(args.content!==undefined) assignIfChanged(e, "content", String(args.content).slice(0,5000), changed);
1460
+ if(args.name!==undefined) assignIfChanged(e, "name", textArg(args.name).slice(0,60), changed);
1461
+ if(Array.isArray(args.keys)) assignIfChanged(e, "keys", args.keys.map(textArg), changed);
1462
+ if(args.content!==undefined) assignIfChanged(e, "content", textArg(args.content).slice(0,5000), changed);
1436
1463
  if(args.enabled!==undefined) assignIfChanged(e, "enabled", !!args.enabled, changed);
1437
1464
  if(args.constant!==undefined) assignIfChanged(e, "constant", !!args.constant, changed);
1438
1465
  if(args.use_regex!==undefined) assignIfChanged(e, "use_regex", !!args.use_regex, changed);
@@ -1451,7 +1478,7 @@ function apply(ctx){
1451
1478
 
1452
1479
  tool({
1453
1480
  name: "cc_patch_greetings",
1454
- description: "【工作流第5步·共创】写入开场与示例(first_mes / alternate_greetings≥2 / group_only_greetings≥1 / mes_example)。调用前必须问用户偏好的开场场景/关系阶段/群聊与私聊区分,拿到偏好后再写;禁止未与用户讨论就自行编造全部问候语。必须在角色+五维+世界书(≥5)后调用。",
1481
+ description: "【工作流第5步·共创】写入开场与示例(first_mes / alternate_greetings≥2 / group_only_greetings≥1 / mes_example)。调用前必须问用户偏好的开场场景/关系阶段/群聊与私聊区分,拿到偏好后再写;禁止未与用户讨论就自行编造全部问候语。必须在角色+五维+世界书(≥5)后调用。一次只传 1-2 个字段(长问候语分多次调用写);alternate_greetings / group_only_greetings 这类数组字段单独一次调用。参数必须是严格合法的 JSON(最后一个字段/数组元素后不写逗号,不要 ```json 代码块包裹,不要注释与单引号)。",
1455
1482
  parameters: {
1456
1483
  type:"object",
1457
1484
  properties:{
@@ -1469,10 +1496,10 @@ function apply(ctx){
1469
1496
  const k = keyFor(ctx, exec);
1470
1497
  const cur = JSON.parse(JSON.stringify(getDraft(ctx, exec)));
1471
1498
  const recNote = recoveredNotice(k, cur);
1472
- if(args.first_mes!==undefined) cur.data.first_mes = String(args.first_mes).slice(0,5000);
1473
- if(Array.isArray(args.alternate_greetings)) cur.data.alternate_greetings = args.alternate_greetings.map(String).slice(0,10);
1474
- if(Array.isArray(args.group_only_greetings)) cur.data.group_only_greetings = args.group_only_greetings.map(String).slice(0,10);
1475
- if(args.mes_example!==undefined) cur.data.mes_example = String(args.mes_example).slice(0,5000);
1499
+ if(args.first_mes!==undefined) cur.data.first_mes = textArg(args.first_mes).slice(0,5000);
1500
+ if(Array.isArray(args.alternate_greetings)) cur.data.alternate_greetings = args.alternate_greetings.map(textArg).slice(0,10);
1501
+ if(Array.isArray(args.group_only_greetings)) cur.data.group_only_greetings = args.group_only_greetings.map(textArg).slice(0,10);
1502
+ if(args.mes_example!==undefined) cur.data.mes_example = textArg(args.mes_example).slice(0,5000);
1476
1503
  setDraft(ctx, cur, exec);
1477
1504
  const wf = await workflowFor(ctx, exec, cur);
1478
1505
  // 若世界书/五维未齐,警告但仍写入,让 validate 卡住
@@ -1625,7 +1652,7 @@ function apply(ctx){
1625
1652
  // 【世界书元信息】此前 name/description/scan_depth/token_budget/recursive_scanning 一个都改不了
1626
1653
  tool({
1627
1654
  name: "cc_patch_book_meta",
1628
- description: "【世界书·元信息】改 character_book 的顶层参数:name / description / scan_depth / token_budget / recursive_scanning。这些字段此前没有任何工具能改,导致导出的世界书永远是无名书、预算也调不了。合并式更新,只改传入的字段。",
1655
+ description: "【世界书·元信息】改 character_book 的顶层参数:name / description / scan_depth / token_budget / recursive_scanning。这些字段此前没有任何工具能改,导致导出的世界书永远是无名书、预算也调不了。合并式更新,只改传入的字段。参数必须是严格合法的 JSON(不要尾逗号、不要注释、不要单引号)。",
1629
1656
  parameters: {
1630
1657
  type:"object",
1631
1658
  properties:{
@@ -1645,8 +1672,8 @@ function apply(ctx){
1645
1672
  cur.data.character_book = cur.data.character_book || {};
1646
1673
  const book = cur.data.character_book;
1647
1674
  const changed = [];
1648
- if(args.name!==undefined) assignIfChanged(book, "name", String(args.name).slice(0,120), changed);
1649
- if(args.description!==undefined) assignIfChanged(book, "description", String(args.description).slice(0,500), changed);
1675
+ if(args.name!==undefined) assignIfChanged(book, "name", textArg(args.name).slice(0,120), changed);
1676
+ if(args.description!==undefined) assignIfChanged(book, "description", textArg(args.description).slice(0,500), changed);
1650
1677
  if(typeof args.scan_depth==="number") assignIfChanged(book, "scan_depth", Math.max(1, Math.min(50, Math.round(args.scan_depth))), changed);
1651
1678
  if(typeof args.token_budget==="number") assignIfChanged(book, "token_budget", Math.max(0, Math.round(args.token_budget)), changed);
1652
1679
  if(args.recursive_scanning!==undefined) assignIfChanged(book, "recursive_scanning", !!args.recursive_scanning, changed);
@@ -2704,8 +2731,8 @@ async function patchSharedBook(groupId, args){
2704
2731
  error: "共享世界书已被外部修改(期望 rev=" + args.expectedBookRev + ",实际 rev=" + bookRevOf(cur) + ")—— 先 cc_get_shared_book 重新读一遍再改。" };
2705
2732
  }
2706
2733
  const applied = [], rejected = [];
2707
- if(args.name !== undefined) cur.data.name = String(args.name).slice(0, 80);
2708
- if(args.description !== undefined) cur.data.description = String(args.description).slice(0, 300);
2734
+ if(args.name !== undefined) cur.data.name = textArg(args.name).slice(0, 80);
2735
+ if(args.description !== undefined) cur.data.description = textArg(args.description).slice(0, 300);
2709
2736
  if(args.scan_depth !== undefined) cur.data.scan_depth = Math.max(1, Math.round(Number(args.scan_depth) || 4));
2710
2737
  if(args.token_budget !== undefined) cur.data.token_budget = Math.max(100, Math.round(Number(args.token_budget) || 2000));
2711
2738
  if(args.recursive_scanning !== undefined) cur.data.recursive_scanning = args.recursive_scanning === true;
@@ -2713,7 +2740,12 @@ async function patchSharedBook(groupId, args){
2713
2740
  (Array.isArray(args.ops) ? args.ops : []).forEach(op => {
2714
2741
  const kind = String((op && op.op) || "");
2715
2742
  if(kind === "addEntry"){
2716
- const e = makeBookEntry(op.entry);
2743
+ // 0.10.3:模型可能把换行写成字面 \n —— 共享书条目也是卡内文本,先解码再归一化。
2744
+ const rawEntry = Object.assign({}, op.entry);
2745
+ if(rawEntry.content !== undefined) rawEntry.content = textArg(rawEntry.content);
2746
+ if(rawEntry.name !== undefined) rawEntry.name = textArg(rawEntry.name);
2747
+ if(Array.isArray(rawEntry.keys)) rawEntry.keys = rawEntry.keys.map(textArg);
2748
+ const e = makeBookEntry(rawEntry);
2717
2749
  if(!e.content.trim()){ rejected.push({ op, error: "content 不能为空" }); return; }
2718
2750
  if(cur.data.entries.some(x => contentKeyOf(x) === contentKeyOf(e))){ rejected.push({ op, error: "同内容的条目已存在" }); return; }
2719
2751
  if(cur.data.entries.length >= BOOK_MAX_ENTRIES){ rejected.push({ op, error: "条目数已达上限 " + BOOK_MAX_ENTRIES }); return; }
@@ -2722,7 +2754,11 @@ async function patchSharedBook(groupId, args){
2722
2754
  } else if(kind === "updateEntry"){
2723
2755
  const idx = cur.data.entries.findIndex(x => contentKeyOf(x) === String(op.contentKey || ""));
2724
2756
  if(idx < 0){ rejected.push({ op, error: "找不到条目(contentKey 不匹配)" }); return; }
2725
- cur.data.entries[idx] = makeBookEntry(Object.assign({}, cur.data.entries[idx], (op.patch && typeof op.patch === "object") ? op.patch : {}));
2757
+ const rawPatch = Object.assign({}, (op.patch && typeof op.patch === "object") ? op.patch : {});
2758
+ if(rawPatch.content !== undefined) rawPatch.content = textArg(rawPatch.content);
2759
+ if(rawPatch.name !== undefined) rawPatch.name = textArg(rawPatch.name);
2760
+ if(Array.isArray(rawPatch.keys)) rawPatch.keys = rawPatch.keys.map(textArg);
2761
+ cur.data.entries[idx] = makeBookEntry(Object.assign({}, cur.data.entries[idx], rawPatch));
2726
2762
  applied.push({ op: kind, key: contentKeyOf(cur.data.entries[idx]) });
2727
2763
  } else if(kind === "removeEntry"){
2728
2764
  // 支持两种定位:`index`(工坊列表直接用它)或 `contentKey`(模型读到的签名)
@@ -90,7 +90,7 @@ function assembleScope(input){
90
90
  name: String(d.name || (row.meta && row.meta.name) || ""),
91
91
  groupId: String((row.meta && row.meta.groupId) || ""),
92
92
  entries: entries,
93
- tokenBudget: Number(d.character_book && d.character_book.token_budget) || 1200,
93
+ tokenBudget: Number(d.character_book && d.character_book.token_budget) || 2400,
94
94
  // 五维(extensions.cc_world):组内世界观漂移检查要它 —— 是在这里顺手带出,而不是让每个
95
95
  // 检查自己去翻 draft(summary 模式本来不带 draft,翻不到就会「静默不检查」)。
96
96
  world: (d.extensions && d.extensions.cc_world && typeof d.extensions.cc_world === "object") ? d.extensions.cc_world : {},
@@ -169,7 +169,7 @@ function groupMetrics(input, opts){
169
169
  id: String((r && r.id) || ""),
170
170
  name: String((r && r.name) || ""),
171
171
  entries: (r && Array.isArray(r.entries)) ? r.entries : [],
172
- tokenBudget: Number(r && r.tokenBudget) || 1200,
172
+ tokenBudget: Number(r && r.tokenBudget) || 2400,
173
173
  world: (r && r.world && typeof r.world === "object") ? r.world : {},
174
174
  }));
175
175
  const book = Array.isArray(opt.book) ? opt.book : [];
@@ -192,7 +192,7 @@ function groupMetrics(input, opts){
192
192
  // 判据与 0.9.x 的 checkGroup 完全一致(那里就是这两项相加),数字现在是同一份。
193
193
  const cardConst = rows.reduce((n, r) => n + byId[r.id].constantTokens, 0);
194
194
  const sharedConst = bookSeg.constantTokens;
195
- const minBudget = rows.length ? rows.reduce((n, r) => Math.min(n, r.tokenBudget), Infinity) : 1200;
195
+ const minBudget = rows.length ? rows.reduce((n, r) => Math.min(n, r.tokenBudget), Infinity) : 2400;
196
196
  out.derived = {
197
197
  groupConstant: {
198
198
  card: cardConst, shared: sharedConst, total: cardConst + sharedConst,
@@ -522,7 +522,7 @@ function runGroupChecks(input){
522
522
  .reduce((n, e) => n + estimateTokens(e && e.content), 0);
523
523
  const constTokens = cardConst + sharedConst;
524
524
  const minBudget = gc ? gc.minBudget
525
- : (memberCards.length ? memberCards.reduce((n, c) => Math.min(n, Number(c.tokenBudget) || 1200), Infinity) : 1200);
525
+ : (memberCards.length ? memberCards.reduce((n, c) => Math.min(n, Number(c.tokenBudget) || 2400), Infinity) : 2400);
526
526
  if(on("budget") && (memberCards.length > 1 || sharedConst > 0) && constTokens > minBudget * 0.8){
527
527
  add("budget", "warn", "constant-budget", "群聊每轮固定开销偏高:成员卡常驻 " + cardConst
528
528
  + (sharedConst ? " + 共享书常驻 " + sharedConst : "") + " = 合计约 " + constTokens
package/lib/index.js CHANGED
@@ -98,7 +98,7 @@ function makeDefaultDraft(){
98
98
  first_mes:"", alternate_greetings:[], group_only_greetings:[],
99
99
  mes_example:"", creator_notes:"", creator_notes_multilingual:{}, source:[], assets:[{type:"icon", uri:"ccdefault:", name:"main", ext:"png"}],
100
100
  creation_date: nowSec(), modification_date: nowSec(),
101
- character_book:{ name:"", description:"", scan_depth:4, token_budget:1200, recursive_scanning:false, extensions:{}, entries:[] }
101
+ character_book:{ name:"", description:"", scan_depth:4, token_budget:2400, recursive_scanning:false, extensions:{}, entries:[] }
102
102
  }
103
103
  };
104
104
  }
@@ -761,7 +761,7 @@ function normalizeDraft(d){
761
761
  // V2/V1 一律**升级**而不是拒绝(B2);本来就是 V3 时 up.card 就是原对象
762
762
  d=upgradeCardToV3(d).card;
763
763
  d.spec_version=d.spec_version||"3.0";
764
- d.data.character_book=d.data.character_book||{name:"",description:"",scan_depth:4,token_budget:1200,recursive_scanning:false,extensions:{},entries:[]};
764
+ d.data.character_book=d.data.character_book||{name:"",description:"",scan_depth:4,token_budget:2400,recursive_scanning:false,extensions:{},entries:[]};
765
765
  if(!Array.isArray(d.data.character_book.entries)) d.data.character_book.entries=[];
766
766
  if(!Array.isArray(d.data.group_only_greetings)) d.data.group_only_greetings=[];
767
767
  if(!Array.isArray(d.data.alternate_greetings)) d.data.alternate_greetings=[];
@@ -1218,7 +1218,7 @@ async function libraryCardsFor(ids){
1218
1218
  name: String(d.name || ""),
1219
1219
  description: String(d.description || ""),
1220
1220
  constantTokens: entries.filter(e => e && e.constant).reduce((n, e) => n + estimateTokensLite(e && e.content), 0),
1221
- tokenBudget: Number(d.character_book && d.character_book.token_budget) || 1200,
1221
+ tokenBudget: Number(d.character_book && d.character_book.token_budget) || 2400,
1222
1222
  });
1223
1223
  }
1224
1224
  return out;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xia-sc/dsh-cc-studio",
3
- "version": "0.10.0",
3
+ "version": "0.10.3",
4
4
  "description": "CCv3 角色卡工坊:融合工坊 + CC 模式 + JSON/PNG/CHARX + 中英双语(跟随全局)",
5
5
  "keywords": [
6
6
  "dsh",
@@ -22,12 +22,18 @@
22
22
  # The preset's own persona, shadowing the deployment default for this agent.
23
23
  # dsh >= 0.1.5-rc.1: the persona row takes `prefix` (required) + optional `suffix`
24
24
  # — the old `text` key was replaced and now fails the config schema.
25
- # `{{model}}` and `{{cwd}}` resolve from the agent's own route and workspace.
25
+ #
26
+ # dsh >= 0.2.1-rc.2: only `provider` / `model` are registered prompt
27
+ # variables. The old `cwd` variable is NOT one of them — `dsh-system-prompt`
28
+ # interpolates strictly and throws `unknown prompt variable` before the first
29
+ # model request, which makes every turn in CC mode fail. The working directory
30
+ # reaches the model as required runtime context instead, so never add that
31
+ # variable back here.
26
32
  - id: persona
27
33
  name: '@deepseek-ai/dsh-persona'
28
34
  config:
29
35
  prefix: |
30
- 你是 CC 角色卡工坊的专属引导员(CC 模式),模型 {{model}},工作目录 {{cwd}}。
36
+ 你是 CC 角色卡工坊的专属引导员(CC 模式),模型 {{model}}。
31
37
  你的唯一任务是把用户的一句话点子变成可导入 SillyTavern / Risu 的完整 CCv3 卡(spec: chara_card_v3)。
32
38
  必须走完强制工作流,不可偷懒跳过 5 维世界观和世界书:
33
39
  1) cc_get_card — 先看草稿,告诉用户还缺什么
@@ -41,6 +47,8 @@
41
47
 
42
48
  规则:每次对话只追问 1-2 个薄弱点,问完立刻调工具写入;用户说“随便/跳过”也要你合理推断补全,不能留空;严禁跳过 3/4 步直接做问候语;每调一次工具要简要总结“已写入 X,下一步 Y”,并让胶囊实时同步;结束前必须 cc_validate。工具返回里只有一份 workflow.next(下一步该做什么)与 workflow.hint(进度摘要),照它走即可;用户还没回答就别抢跑。
43
49
 
50
+ **工具参数必须是严格合法的 JSON**:最后一个字段 / 数组元素后不要写逗号,不要用 ```json 代码块包裹,不要注释、不要单引号;字符串里的换行写成 \n、双引号写成 \"。一次只传 1-3 个字段,长字段(description/personality/scenario)分多次调用写;tags / loreKeys 这类数组或对象字段单独一次调用,不要和长文本同批。参数 JSON 非法时 provider 会在调用前直接判本轮失败(用户只看到「本轮运行失败」),所以宁可分多次小调用。
51
+
44
52
  - id: agent-instructions
45
53
  name: '@deepseek-ai/dsh-agent-instructions'
46
54
  config:
@@ -57,12 +57,18 @@
57
57
  # The preset's own persona, shadowing the deployment default for this agent.
58
58
  # dsh >= 0.1.5-rc.1: the persona row takes `prefix` (required) + optional `suffix`
59
59
  # — the old `text` key was replaced and now fails the config schema.
60
- # `{{model}}` and `{{cwd}}` resolve from the agent's own route and workspace.
60
+ #
61
+ # dsh >= 0.2.1-rc.2: only `provider` / `model` are registered prompt
62
+ # variables. The old `cwd` variable is NOT one of them — `dsh-system-prompt`
63
+ # interpolates strictly and throws `unknown prompt variable` before the first
64
+ # model request, which makes every turn in CC mode fail. The working directory
65
+ # reaches the model as required runtime context instead, so never add that
66
+ # variable back here.
61
67
  - id: persona
62
68
  name: '@deepseek-ai/dsh-persona'
63
69
  config:
64
70
  prefix: |
65
- 你是 CC 角色卡工坊的专属引导员(CC 模式),模型 {{model}},工作目录 {{cwd}}。
71
+ 你是 CC 角色卡工坊的专属引导员(CC 模式),模型 {{model}}。
66
72
  你的唯一任务是把用户的一句话点子变成可导入 SillyTavern / Risu 的完整 CCv3 卡(spec: chara_card_v3)。
67
73
  必须走完强制工作流,不可偷懒跳过 5 维世界观和世界书:
68
74
  1) cc_get_card — 先看草稿,告诉用户还缺什么
@@ -76,6 +82,8 @@
76
82
 
77
83
  规则:每次对话只追问 1-2 个薄弱点,问完立刻调工具写入;用户说“随便/跳过”也要你合理推断补全,不能留空;严禁跳过 3/4 步直接做问候语;每调一次工具要简要总结“已写入 X,下一步 Y”,并让胶囊实时同步;结束前必须 cc_validate。工具返回里只有一份 workflow.next(下一步该做什么)与 workflow.hint(进度摘要),照它走即可;用户还没回答就别抢跑。
78
84
 
85
+ **工具参数必须是严格合法的 JSON**:最后一个字段 / 数组元素后不要写逗号,不要用 ```json 代码块包裹,不要注释、不要单引号;字符串里的换行写成 \n、双引号写成 \"。一次只传 1-3 个字段,长字段(description/personality/scenario)分多次调用写;tags / loreKeys 这类数组或对象字段单独一次调用,不要和长文本同批。参数 JSON 非法时 provider 会在调用前直接判本轮失败(用户只看到「本轮运行失败」),所以宁可分多次小调用。
86
+
79
87
  - id: agent-instructions
80
88
  name: '@deepseek-ai/dsh-agent-instructions'
81
89
  config: