dsh-plugin-tlrp 0.3.0 → 0.4.0

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
@@ -2,7 +2,7 @@
2
2
 
3
3
  DSH 核心角色扮演与**多形态矩阵**插件:把「当前用哪种形态说话、能动哪些工具、要不要写状态面板」从人设文本里的死规则,变成可在设置面板里自由增删改的运行时形态(Mode)。
4
4
 
5
- - 包名 `dsh-plugin-tlrp`,版本 `0.3.0`,零外部重型依赖
5
+ - 包名 `dsh-plugin-tlrp`,版本 `0.4.0`,零外部重型依赖
6
6
  - 双入口:ESM `index.js` + CommonJS `index.cjs`,同一份实现源(`lib/core.cjs`)
7
7
  - 六块能力:设置命名空间 / 形态矩阵(自定义形态 + 动态指令)/ 动态提示变量 / 每轮尾部深度注入 / 按形态的工具拦截 / 多角色与触发映射表
8
8
 
@@ -39,6 +39,11 @@ tlrp 把这三件事都挪到宿主层:
39
39
  | `customSceneOverride` | string | `""` | 临时情景覆盖 |
40
40
  | `characters` | array | 内置 `firefly` + `sam_tactical` | 多角色预设库 |
41
41
  | `triggers` | array | 内置 5 组流萤触发映射 | 提示词触发映射表(动态世界书) |
42
+ | `styleLayerDefault` | `on` \| `dialog` \| `off` | `off` | 全局风格层档位(形态与 `/style` 可各自覆盖):`on` 只加语气与表情/贴纸,`dialog` 再进一步砍掉动作、环境、外貌与旁白 |
43
+ | `styleEmojiHint` | string | 适度使用 emoji 的说明 | 风格层里的表情规则;留空=本轮不用 emoji |
44
+ | `stickerDir` | string | `""` | 表情包目录;留空落 `~/.dsh/stickers/<角色id>/` |
45
+ | `galleryDir` | string | `""` | 图库目录;留空落 `~/.dsh/gallery/` |
46
+ | `maxStickersPerTurn` | number | `2` | 每轮最多贴几张图(`0`=本轮禁止贴图) |
42
47
 
43
48
  非面板字段(通过 `cordis.patch.yml` 调):`timeZone`、`statusFooterLabel`、`registerNowVariable`、`coreSectionEnabled`、`coreSectionOrder`、`coreSectionText`、`contextOrder`、`maxTriggerInjections`、`blockedToolPatterns`、`allowedToolPatterns`、`activeProfileEntryId`。
44
49
 
@@ -65,12 +70,30 @@ Web / CLI 走 `ctx.commands.register()`(拦截层,不产生模型消息)
65
70
  | `/char <id>` | 切换激活角色(`/char` 不带参数会列出可用 id) |
66
71
  | `/scene <描述>` | 覆盖当前情景;留空回到角色默认情景 |
67
72
  | `/tlrp status`(或 `/status`) | 打印路由锁定、本轮形态、以及**全部已注册形态**的优先级 / 工具策略 / 别名 / 触发链 |
73
+ | `/style on` \| `dialog` \| `off` \| `default` \| `status` | 当轮覆盖风格层档位:`on` 只带语气+表情/贴纸、`dialog` 对话体(只说出口的话,不写动作/环境/外貌/旁白)、`off` 关掉、`default` 交回「形态 → 全局」判定;不带参数等同 `status` |
68
74
  | 形态别名(动态注册) | 每个形态 `commandAliases` 里以 `/` 开头的别名都会被注册成真指令:默认有 `/rp` `/work` `/sam` `/sleep`,你新建 `debug` 形态并给它 `/debug`,就立刻多一条 `/debug` |
69
75
 
70
76
  首行标记等价于指令,但**只认已注册别名**:`【流萤】`/`【陪伴】` → `rp`、`【战术】` → `work`、`【萨姆】`/`【机甲】` → `sam`、`【医疗舱】` → `sleep`;你自己给 `debug` 形态写 `【排障】`,那 `【排障】` 就同样能切形态。
71
77
 
72
78
  旧写法(`/rp` `/work`)在默认形态表下照旧可用,因为它们就是 `rp`/`work` 形态自带的别名。
73
79
 
80
+ ## 三点五、风格层(只带语气,不演全套)
81
+
82
+ 风格层是叠加在形态之上的**表演强度旋钮**:它不换人设、不改工具策略,只决定「这一轮要不要写旁白」。
83
+
84
+ 四档,取值 `inherit` / `on` / `dialog` / `off`:
85
+
86
+ | 档位 | 注入内容 |
87
+ | --- | --- |
88
+ | `inherit` | 跟随下一级(形态跟全局、全局 `inherit` 视作 `off`) |
89
+ | `on` | 补语言风格(`speechStyle` + `dialogueExamples`)+ emoji 规则 + 本地表情包/图库清单;不复述人设,也不改形态原有的动作写法 |
90
+ | `dialog` | 在 `on` 之上追加「本轮的表演强度」:只输出角色说出口的话,禁止 `*斜体动作*`、环境天气、陈设外貌、心理独白串,除非人设明确要求否则不写状态面板 |
91
+ | `off` | 完全不注入风格块 |
92
+
93
+ 判定优先级:**会话 `/style` 覆盖 > 形态自带 `styleLayer` > 全局 `styleLayerDefault`**。形态自带值在设置面板的形态表单里改,全局值在「风格层(轻量注入)」分区里改,两者都不用重启。
94
+
95
+ 配合本地图库:把图片丢进 `~/.dsh/stickers/<角色id>/`(或自建目录并在 `stickerDir` 指定)与 `~/.dsh/gallery/`,标签=文件名主干,插件只扫**顶层**文件(png/jpg/jpeg/webp/gif)。模型按 `![标签](绝对路径)` 的语法贴图,路径用正斜杠;客户端会把绝对路径改写成带鉴权的 `api/file` 地址,所以本地图能直接渲染。
96
+
74
97
  ## 四、提示变量
75
98
 
76
99
  | 变量 | 取值 |
@@ -168,14 +191,14 @@ dsh --profile web --dump-config | grep -A 8 "id: tlrp"
168
191
 
169
192
  | 脚本 | 证明什么 |
170
193
  | --- | --- |
171
- | `node --test test/core.test.mjs` | 24 条纯逻辑单测:指令解析、形态归一化与迁移、触发匹配、三种准则渲染、模板中和、时区格式化、多角色回落 |
172
- | `node scripts/apply-check.mjs` | 48 条端到端检查:用**真的** `SystemPrompt` 跑 `assemble()+renderPrompt()`,证明未知变量数为 0、核心 section/角色块/时间变量都在、auto 分流、形态准则切换、按 `toolPolicy` 拦截与放行、`/mode <id>` 与自定义别名(`/debug`、`【排障】`)切形态、**自定义第 5 个形态** 的专属准则/精简人设/`block_all` 全拦、`/char` `/scene` `/tlrp` 与 `settings.mutate` 落到 `include:tlrp`、命中「橡木蛋糕卷」注入、`sam_tactical` 独立人格、关闭态不拦截 |
194
+ | `node --test test/core.test.mjs test/memory-bridge.test.mjs test/style.test.mjs` | 47 条纯逻辑单测:指令解析、形态归一化与迁移、触发匹配、三种准则渲染、模板中和、时区格式化、多角色回落、记忆桥、风格层四档解析与归一化、贴纸库扫描、风格块渲染 |
195
+ | `node scripts/apply-check.mjs` | 66 条端到端检查:用**真的** `SystemPrompt` 跑 `assemble()+renderPrompt()`,证明未知变量数为 0、核心 section/角色块/时间变量都在、auto 分流、形态准则切换、按 `toolPolicy` 拦截与放行、`/mode <id>` 与自定义别名(`/debug`、`【排障】`)切形态、**自定义第 5 个形态** 的专属准则/精简人设/`block_all` 全拦、`/char` `/scene` `/tlrp` 与 `settings.mutate` 落到 `include:tlrp`、`/style` 四档在 work 与 rp 两侧切换(`dialog` 会同时改掉陪伴族格式红线与斜体动作要求)、命中「橡木蛋糕卷」注入、`sam_tactical` 独立人格、关闭态不拦截 |
173
196
  | `node scripts/mutate-check.mjs` | 21 条设置写入检查:用**真的** `dsh-settings` 代码跑 `describe()+mutate()`,九个 volatile 字段逐个往返、`modes` 数组往返(含自定义形态)、`mode` 可锁定到任意形态 id、非 volatile 字段被拒绝、未被服务的 ns(`tlrp`)被拒绝、批量写入 |
174
- | `node scripts/client-check.mjs` | 53 条客户端渲染检查:切换栏按 `modes` 动态渲染、自定义形态出现在切换栏、形态 CRUD 按钮与编辑器字段齐全 |
197
+ | `node scripts/client-check.mjs` | 62 条客户端渲染检查:切换栏按 `modes` 动态渲染、自定义形态出现在切换栏、形态 CRUD 按钮与编辑器字段齐全、五个分区可折叠(`aria-expanded`、`data-open`、`hidden` 兜底 CSS)、风格层分区与形态级档位选择 |
175
198
 
176
199
  ```bash
177
200
  cd ~/.dsh/profiles/desktop
178
- node --test "D:/Code/my-dsh-plugins/packages/tlrp/test/core.test.mjs"
201
+ node --test "D:/Code/my-dsh-plugins/packages/tlrp/test/core.test.mjs" "D:/Code/my-dsh-plugins/packages/tlrp/test/style.test.mjs"
179
202
  node "D:/Code/my-dsh-plugins/packages/tlrp/scripts/apply-check.mjs"
180
203
  node "D:/Code/my-dsh-plugins/packages/tlrp/scripts/mutate-check.mjs"
181
204
  ```
package/client.js CHANGED
@@ -93,6 +93,17 @@ window.__ModuleLoader__.load({
93
93
  /** 工具策略下拉项:与 core.cjs 的 TOOL_POLICIES 一一对应。 */
94
94
  const TOOL_POLICY_OPTIONS = ["allow_all", "whitelist_only", "block_all"];
95
95
 
96
+ /** 风格层三态:与 lib/schema.cjs 的 modeShape.styleLayer 一一对应。 */
97
+ const STYLE_LAYER_OPTIONS = ["inherit", "on", "dialog", "off"];
98
+
99
+ /** 形态风格层三态的显示文案。 */
100
+ function styleLayerLabel(t, flag) {
101
+ if (flag === "on") return t("styleOn");
102
+ if (flag === "dialog") return t("styleDialog");
103
+ if (flag === "off") return t("styleOff");
104
+ return t("styleInherit");
105
+ }
106
+
96
107
  /** 新建形态用的空骨架:字段名与 lib/schema.cjs 的 modeShape 一一对应。 */
97
108
  const EMPTY_MODE = {
98
109
  id: "",
@@ -108,6 +119,7 @@ window.__ModuleLoader__.load({
108
119
  statusFooterOverride: "",
109
120
  bindCharacterId: "",
110
121
  isDefaultFallback: false,
122
+ styleLayer: "inherit",
111
123
  };
112
124
 
113
125
  /** 形态 id 的合法字符:与宿主注册斜杠指令时用的正则保持一致。 */
@@ -261,6 +273,23 @@ window.__ModuleLoader__.load({
261
273
 
262
274
  modeSection: "形态矩阵工作室 Mode Studio",
263
275
  modeSectionHint: "运行形态可以像角色一样自由增删改:每个形态有自己的行为准则、工具策略、状态面板模板与触发指令;auto 按优先级与关键词自动分流。",
276
+ styleSection: "风格层(轻量注入)",
277
+ styleSectionHint: "只把角色的语言风格、表情与本地贴纸追加进上下文,不演微动作;人设是否完整注入仍由形态的「注入完整人设」决定。对话里可用 /style on|off|default|status 临时开关。",
278
+ field_styleLayerDefault: "全局默认",
279
+ hint_styleLayerDefault: "形态设为「跟随全局默认」时用这里。只带语气=口吻像角色但仍可演;对话体=只说话,禁旁白、动作、环境与外貌描写。",
280
+ field_styleEmojiHint: "表情(emoji)用量",
281
+ hint_styleEmojiHint: "写进上下文的 emoji 使用规则;留空则声明本轮不用 emoji。",
282
+ field_stickerDir: "表情包目录",
283
+ hint_stickerDir: "留空则用 ~/.dsh/stickers/<角色 ID>;只读该目录顶层,文件名即标签,支持 png/jpg/jpeg/webp/gif。",
284
+ field_galleryDir: "图库目录",
285
+ hint_galleryDir: "留空则用 ~/.dsh/gallery;同样只读顶层,文件名即标签。",
286
+ field_maxStickersPerTurn: "每轮最多贴图",
287
+ hint_maxStickersPerTurn: "表情包与图库共用这个上限;0 表示整轮禁止贴图。",
288
+ styleSwatch: "当前判定",
289
+ sectionsToolbarExpand: "全部展开",
290
+ sectionsToolbarCollapse: "全部收起",
291
+ sectionsSummaryModes: "{count} 个形态",
292
+ sectionsSummaryTriggers: "{count} 条触发",
264
293
  mode: "当前模式",
265
294
  modeHint: "点顶部按钮直接锁定;选「自动路由」则按关键词与优先级每轮判定。",
266
295
  modeAuto: "自动路由 (auto)",
@@ -303,6 +332,12 @@ window.__ModuleLoader__.load({
303
332
  hint_mode_statusFooterOverride: "留空则使用角色自己的状态面板模板;可用 {label}{scene}{dress}{os}{heat}{shield}{vocoder}{entropy}。",
304
333
  field_mode_bindCharacterId: "联动角色",
305
334
  hint_mode_bindCharacterId: "切到该形态时自动切换到的角色;留空表示保持当前角色。",
335
+ field_mode_styleLayer: "风格层",
336
+ hint_mode_styleLayer: "跟随全局/强制开/强制关;对话里的 /style 优先级最高。",
337
+ styleInherit: "跟随全局默认",
338
+ styleOn: "只带语气",
339
+ styleDialog: "对话体(只说话不演)",
340
+ styleOff: "强制关",
306
341
  policyAllowAll: "放行全部工具",
307
342
  policyWhitelist: "仅白名单",
308
343
  policyBlockAll: "禁用全部工具",
@@ -424,6 +459,23 @@ window.__ModuleLoader__.load({
424
459
 
425
460
  modeSection: "Mode Studio",
426
461
  modeSectionHint: "Runtime modes are as editable as characters: each one carries its own directive, tool policy, status-footer template and trigger aliases; auto routes by priority and keywords.",
462
+ styleSection: "Style layer (lightweight injection)",
463
+ styleSectionHint: "Appends only the character's speech style, emoji rules and local stickers to the context, with no action roleplay; whether the full persona is injected stays controlled by the mode's Inject full persona switch. Toggle it in chat with /style on|off|default|status.",
464
+ field_styleLayerDefault: "Global default",
465
+ hint_styleLayerDefault: "Used by modes set to Follow global default. Tone only keeps acting; Dialogue only forbids narration, actions, environment and appearance.",
466
+ field_styleEmojiHint: "Emoji allowance",
467
+ hint_styleEmojiHint: "Emoji rule injected into the context; empty means this turn uses no emoji.",
468
+ field_stickerDir: "Sticker directory",
469
+ hint_stickerDir: "Empty uses ~/.dsh/stickers/<character id>; only its top level is read and each file name is the tag (png/jpg/jpeg/webp/gif).",
470
+ field_galleryDir: "Gallery directory",
471
+ hint_galleryDir: "Empty uses ~/.dsh/gallery; likewise top level only, file name is the tag.",
472
+ field_maxStickersPerTurn: "Max images per turn",
473
+ hint_maxStickersPerTurn: "Shared by stickers and gallery; 0 forbids any image for the turn.",
474
+ styleSwatch: "Resolved",
475
+ sectionsToolbarExpand: "Expand all",
476
+ sectionsToolbarCollapse: "Collapse all",
477
+ sectionsSummaryModes: "{count} modes",
478
+ sectionsSummaryTriggers: "{count} triggers",
427
479
  mode: "Mode",
428
480
  modeHint: "Click a button to lock a mode; Auto routes every turn from keywords and priority.",
429
481
  modeAuto: "Auto route (auto)",
@@ -466,6 +518,12 @@ window.__ModuleLoader__.load({
466
518
  hint_mode_statusFooterOverride: "Empty uses the character template; supports {label}{scene}{dress}{os}{heat}{shield}{vocoder}{entropy}.",
467
519
  field_mode_bindCharacterId: "Bound character",
468
520
  hint_mode_bindCharacterId: "Character switched to when this mode activates; empty keeps the current one.",
521
+ field_mode_styleLayer: "Style layer",
522
+ hint_mode_styleLayer: "Follow global default / force on / force off; a /style command in chat wins over both.",
523
+ styleInherit: "Follow global default",
524
+ styleOn: "Tone only",
525
+ styleDialog: "Dialogue only",
526
+ styleOff: "Force off",
469
527
  policyAllowAll: "Allow every tool",
470
528
  policyWhitelist: "Allowlist only",
471
529
  policyBlockAll: "Block every tool",
@@ -583,6 +641,16 @@ window.__ModuleLoader__.load({
583
641
  .tlrp-section-head { display: flex; flex-direction: column; gap: 3px; }
584
642
  .tlrp-section-title { font-size: 13px; font-weight: 600; color: var(--dsw-alias-text-primary, #e8eaf0); }
585
643
  .tlrp-section-hint { font-size: 11.5px; line-height: 1.55; color: var(--dsw-alias-text-secondary, #9aa3b2); }
644
+ .tlrp-section-toggle { display: flex; align-items: flex-start; gap: 8px; width: 100%; margin: 0; padding: 0; border: 0; background: none; font: inherit; color: inherit; text-align: left; cursor: pointer; }
645
+ .tlrp-section-toggle:focus-visible { outline: 2px solid var(--dsw-alias-border-accent, #66e0c2); outline-offset: 3px; border-radius: 6px; }
646
+ .tlrp-section-main { display: flex; flex-direction: column; gap: 3px; min-width: 0; flex: 1; }
647
+ .tlrp-section-chevron { flex: 0 0 12px; margin-top: 3px; color: var(--dsw-alias-text-secondary, #9aa3b2); transition: transform .16s ease; }
648
+ .tlrp-section[data-open="true"] .tlrp-section-chevron { transform: rotate(90deg); }
649
+ .tlrp-section-summary { display: flex; flex-wrap: wrap; gap: 6px; }
650
+ .tlrp-section[data-open="true"] .tlrp-section-summary { display: none; }
651
+ .tlrp-section-body { display: flex; flex-direction: column; gap: 12px; }
652
+ .tlrp-section[data-open="false"] .tlrp-section-body { display: none; }
653
+ .tlrp-sections-toolbar { display: flex; justify-content: flex-end; gap: 8px; }
586
654
  .tlrp-row { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
587
655
  .tlrp-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 12px; }
588
656
  .tlrp-field { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
@@ -638,20 +706,97 @@ window.__ModuleLoader__.load({
638
706
 
639
707
  /* ─────────────────────── 基础控件 ─────────────────────── */
640
708
 
641
- /** 一行标题 + 说明的分区容器。 */
709
+ /* ── 分区收起状态:默认只展开第一块,用户的选择记在 localStorage ── */
710
+
711
+ const SECTION_IDS = ["mode", "style", "studio", "triggers", "presets"];
712
+ const SECTION_STORAGE_KEY = "tlrp.sections.open";
713
+
714
+ function mapSections(open) {
715
+ const next = {};
716
+ for (const id of SECTION_IDS) next[id] = open === true;
717
+ return next;
718
+ }
719
+
720
+ function persistSectionState(state) {
721
+ try {
722
+ window.localStorage.setItem(SECTION_STORAGE_KEY, JSON.stringify(state));
723
+ } catch (error) {
724
+ /* 隐私模式或存储配额:只丢记忆,不影响本次收展 */
725
+ }
726
+ return state;
727
+ }
728
+
729
+ function loadSectionState() {
730
+ try {
731
+ const raw = window.localStorage.getItem(SECTION_STORAGE_KEY);
732
+ const parsed = raw ? JSON.parse(raw) : null;
733
+ if (parsed && typeof parsed === "object") {
734
+ const next = {};
735
+ let anyOpen = false;
736
+ for (const id of SECTION_IDS) {
737
+ next[id] = parsed[id] === true;
738
+ if (next[id]) anyOpen = true;
739
+ }
740
+ if (anyOpen) return next;
741
+ }
742
+ } catch (error) {
743
+ /* 读不到就用默认 */
744
+ }
745
+ return { mode: true };
746
+ }
747
+
748
+ /** 分区容器:可收起卡片(标题行是按钮,收起后留一行状态摘要)。 */
642
749
  function Section(props) {
643
- return h(
644
- "section",
645
- { className: "tlrp-section" },
750
+ const open = props.open === undefined ? true : props.open === true;
751
+ const header = h(
752
+ "button",
753
+ {
754
+ className: "tlrp-section-toggle",
755
+ type: "button",
756
+ "aria-expanded": open ? "true" : "false",
757
+ onClick: props.onToggle ? () => props.onToggle(props.id) : undefined,
758
+ },
759
+ h(
760
+ "svg",
761
+ {
762
+ className: "tlrp-section-chevron",
763
+ width: 12,
764
+ height: 12,
765
+ viewBox: "0 0 12 12",
766
+ fill: "none",
767
+ stroke: "currentColor",
768
+ strokeWidth: 1.6,
769
+ strokeLinecap: "round",
770
+ strokeLinejoin: "round",
771
+ "aria-hidden": "true",
772
+ focusable: "false",
773
+ },
774
+ h("path", { d: "M4.5 2.5 L8 6 L4.5 9.5" })
775
+ ),
646
776
  h(
647
777
  "div",
648
- { className: "tlrp-section-head" },
778
+ { className: "tlrp-section-main" },
649
779
  h("div", { className: "tlrp-section-title" }, props.title),
650
780
  props.hint ? h("div", { className: "tlrp-section-hint" }, props.hint) : null
651
- ),
652
- props.children
781
+ )
653
782
  );
654
- }
783
+ return h(
784
+ "section",
785
+ { className: "tlrp-section", "data-open": open ? "true" : "false" },
786
+ header,
787
+ props.summary && props.summary.length > 0
788
+ ? h(
789
+ "div",
790
+ { className: "tlrp-section-summary" },
791
+ props.summary.map((text, index) => h("span", { key: String(index), className: "tlrp-chip" }, text))
792
+ )
793
+ : null,
794
+ h(
795
+ "div",
796
+ { className: "tlrp-section-body", hidden: open ? undefined : true },
797
+ ...(Array.isArray(props.children) ? props.children : [props.children])
798
+ )
799
+ ); }
655
800
 
656
801
  /** 字段容器:标签 + 控件 + 提示。 */
657
802
  function Field(props) {
@@ -1110,6 +1255,16 @@ window.__ModuleLoader__.load({
1110
1255
  );
1111
1256
  const [importText, setImportText] = useState("");
1112
1257
  const [notice, setNotice] = useState("");
1258
+ const [openSections, setOpenSections] = useState(loadSectionState);
1259
+ const toggleSection = useCallback(
1260
+ (id) => {
1261
+ if (typeof id !== "string") return;
1262
+ setOpenSections((prev) => persistSectionState({ ...prev, [id]: prev?.[id] !== true }));
1263
+ },
1264
+ []
1265
+ );
1266
+ const setAllSections = useCallback((open) => setOpenSections(persistSectionState(mapSections(open))), []);
1267
+ const sectionProps = (id) => ({ id, open: openSections?.[id] === true, onToggle: toggleSection });
1113
1268
 
1114
1269
  const applyImport = () => {
1115
1270
  let parsed;
@@ -1191,6 +1346,15 @@ window.__ModuleLoader__.load({
1191
1346
  }
1192
1347
 
1193
1348
  const disabled = writable !== true;
1349
+ const modeStyleLayer =
1350
+ activeMode?.styleLayer === "on" || activeMode?.styleLayer === "dialog" || activeMode?.styleLayer === "off"
1351
+ ? activeMode.styleLayer
1352
+ : null;
1353
+ const effectiveStyleLayer =
1354
+ modeStyleLayer ??
1355
+ (STYLE_LAYER_OPTIONS.includes(value.styleLayerDefault) && value.styleLayerDefault !== "inherit"
1356
+ ? value.styleLayerDefault
1357
+ : "off");
1194
1358
 
1195
1359
  return h(
1196
1360
  "div",
@@ -1205,11 +1369,25 @@ window.__ModuleLoader__.load({
1205
1369
  ),
1206
1370
  disabled ? h("p", { className: "tlrp-warn" }, t("readOnly")) : null,
1207
1371
  notice ? h("p", { className: "tlrp-note" }, notice) : null,
1372
+ h(
1373
+ "div",
1374
+ { className: "tlrp-sections-toolbar" },
1375
+ h(Button, { onClick: () => setAllSections(true) }, t("sectionsToolbarExpand")),
1376
+ h(Button, { onClick: () => setAllSections(false) }, t("sectionsToolbarCollapse"))
1377
+ ),
1208
1378
 
1209
1379
  /* ── A:形态矩阵工作室 Mode Studio ── */
1210
1380
  h(
1211
1381
  Section,
1212
- { title: t("modeSection"), hint: t("modeSectionHint") },
1382
+ {
1383
+ ...sectionProps("mode"),
1384
+ title: t("modeSection"),
1385
+ hint: t("modeSectionHint"),
1386
+ summary: [
1387
+ `${t("mode")}:${mode === "auto" ? t("modeAuto") : activeMode?.name ?? mode}`,
1388
+ t("sectionsSummaryModes", { count: modes.length }),
1389
+ ],
1390
+ },
1213
1391
  /* 顶部全局切换栏:自动路由 + 配置里的每一个形态 */
1214
1392
  h(
1215
1393
  "div",
@@ -1356,6 +1534,16 @@ window.__ModuleLoader__.load({
1356
1534
  onChange: (next) => patchMode("enableStatusFooter", next),
1357
1535
  })
1358
1536
  ),
1537
+ h(
1538
+ Field,
1539
+ { label: t("field_mode_styleLayer"), hint: t("hint_mode_styleLayer") },
1540
+ h(Select, {
1541
+ value: modeDraft.styleLayer ?? "inherit",
1542
+ disabled,
1543
+ options: STYLE_LAYER_OPTIONS.map((flag) => ({ value: flag, label: styleLayerLabel(t, flag) })),
1544
+ onChange: (next) => patchMode("styleLayer", next),
1545
+ })
1546
+ ),
1359
1547
  h(
1360
1548
  Field,
1361
1549
  { label: t("field_mode_autoKeywords"), hint: t("hint_mode_autoKeywords") },
@@ -1415,10 +1603,111 @@ window.__ModuleLoader__.load({
1415
1603
  )
1416
1604
  ),
1417
1605
 
1606
+ /* ── A2:风格层(语言风格轻量注入) ── */
1607
+ h(
1608
+ Section,
1609
+ {
1610
+ ...sectionProps("style"),
1611
+ title: t("styleSection"),
1612
+ hint: t("styleSectionHint"),
1613
+ summary: [`${t("styleSwatch")}:${styleLayerLabel(t, effectiveStyleLayer)}`],
1614
+ },
1615
+ h(
1616
+ "div",
1617
+ { className: "tlrp-row" },
1618
+ h(
1619
+ "span",
1620
+ { className: "tlrp-chip" },
1621
+ t("styleSwatch"),
1622
+ " ",
1623
+ h(
1624
+ "b",
1625
+ null,
1626
+ activeMode?.styleLayer === "on" ||
1627
+ activeMode?.styleLayer === "dialog" ||
1628
+ activeMode?.styleLayer === "off"
1629
+ ? styleLayerLabel(t, activeMode.styleLayer)
1630
+ : styleLayerLabel(
1631
+ t,
1632
+ STYLE_LAYER_OPTIONS.includes(value.styleLayerDefault) && value.styleLayerDefault !== "inherit"
1633
+ ? value.styleLayerDefault
1634
+ : "off"
1635
+ )
1636
+ )
1637
+ ),
1638
+ h("span", { className: "tlrp-chip" }, t("mode"), " ", h("b", null, activeMode?.name ?? "—"))
1639
+ ),
1640
+ h(
1641
+ "div",
1642
+ { className: "tlrp-grid" },
1643
+ h(
1644
+ Field,
1645
+ { label: t("field_styleLayerDefault"), hint: t("hint_styleLayerDefault") },
1646
+ h(Select, {
1647
+ value: ["on", "dialog", "off"].includes(value.styleLayerDefault) ? value.styleLayerDefault : "off",
1648
+ disabled,
1649
+ options: [
1650
+ { value: "on", label: t("styleOn") },
1651
+ { value: "dialog", label: t("styleDialog") },
1652
+ { value: "off", label: t("styleOff") },
1653
+ ],
1654
+ onChange: (next) => write("styleLayerDefault", next),
1655
+ })
1656
+ ),
1657
+ h(
1658
+ Field,
1659
+ { label: t("field_maxStickersPerTurn"), hint: t("hint_maxStickersPerTurn") },
1660
+ h(TextInput, {
1661
+ value: String(value.maxStickersPerTurn ?? 2),
1662
+ disabled,
1663
+ onChange: (next) => {
1664
+ const parsed = Number(next);
1665
+ write("maxStickersPerTurn", Number.isFinite(parsed) ? Math.max(0, Math.trunc(parsed)) : 0);
1666
+ },
1667
+ })
1668
+ ),
1669
+ h(
1670
+ Field,
1671
+ { label: t("field_stickerDir"), hint: t("hint_stickerDir") },
1672
+ h(TextInput, {
1673
+ value: typeof value.stickerDir === "string" ? value.stickerDir : "",
1674
+ placeholder: "C:/Users/<you>/.dsh/stickers/<角色 ID>",
1675
+ disabled,
1676
+ onChange: (next) => write("stickerDir", next),
1677
+ })
1678
+ ),
1679
+ h(
1680
+ Field,
1681
+ { label: t("field_galleryDir"), hint: t("hint_galleryDir") },
1682
+ h(TextInput, {
1683
+ value: typeof value.galleryDir === "string" ? value.galleryDir : "",
1684
+ placeholder: "C:/Users/<you>/.dsh/gallery",
1685
+ disabled,
1686
+ onChange: (next) => write("galleryDir", next),
1687
+ })
1688
+ )
1689
+ ),
1690
+ h(
1691
+ Field,
1692
+ { label: t("field_styleEmojiHint"), hint: t("hint_styleEmojiHint") },
1693
+ h(TextArea, {
1694
+ rows: 3,
1695
+ value: typeof value.styleEmojiHint === "string" ? value.styleEmojiHint : "",
1696
+ disabled,
1697
+ onChange: (next) => write("styleEmojiHint", next),
1698
+ })
1699
+ )
1700
+ ),
1701
+
1418
1702
  /* ── B:角色工作室 ── */
1419
1703
  h(
1420
1704
  Section,
1421
- { title: t("studio"), hint: t("studioHint") },
1705
+ {
1706
+ ...sectionProps("studio"),
1707
+ title: t("studio"),
1708
+ hint: t("studioHint"),
1709
+ summary: [`${t("character")}:${activeCharacter?.name || activeCharacter?.id || t("unknownCharacter")}`],
1710
+ },
1422
1711
  h(
1423
1712
  "div",
1424
1713
  { className: "tlrp-row" },
@@ -1465,7 +1754,12 @@ window.__ModuleLoader__.load({
1465
1754
  /* ── C:触发词映射表 ── */
1466
1755
  h(
1467
1756
  Section,
1468
- { title: t("triggers"), hint: t("triggersHint") },
1757
+ {
1758
+ ...sectionProps("triggers"),
1759
+ title: t("triggers"),
1760
+ hint: t("triggersHint"),
1761
+ summary: [t("sectionsSummaryTriggers", { count: localTriggers.length })],
1762
+ },
1469
1763
  h(
1470
1764
  "div",
1471
1765
  { className: "tlrp-row" },
@@ -1555,7 +1849,7 @@ window.__ModuleLoader__.load({
1555
1849
  /* ── D:预设导入导出与调试探针 ── */
1556
1850
  h(
1557
1851
  Section,
1558
- { title: t("presets"), hint: t("presetsHint") },
1852
+ { ...sectionProps("presets"), title: t("presets"), hint: t("presetsHint") },
1559
1853
  h(
1560
1854
  "div",
1561
1855
  { className: "tlrp-grid" },