advanced-preset 1.6.0 → 1.7.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/dist/ap.js CHANGED
@@ -7377,7 +7377,11 @@ var FORMAT_VERSION = 3;
7377
7377
  var TRANSPORTS = ["prefill", "plaintext", "structured", "tool-single", "tool-multi"];
7378
7378
  var TOOL_ARG_STREAMING = ["auto", "on", "off"];
7379
7379
  var BUILD_VAR_TYPES = ["string", "list", "dict"];
7380
- var STRUCTURE_KINDS = ["reply", "memory", "recap", "impersonate", "synopsis", "options", "custom"];
7380
+ var STRUCTURE_KINDS = ["reply", "memory", "recap", "impersonate", "synopsis", "options", "custom", "annotations"];
7381
+ var INLINE_STRUCTURE_KINDS = ["annotations"];
7382
+ var isInlineStructure = (s) => INLINE_STRUCTURE_KINDS.includes(s.kind);
7383
+ var USER_INPUT_MODES = ["keep", "truncate", "remove"];
7384
+ var DEFAULT_SYNOPSIS_HISTORY_TEMPLATE = "{{content}}\n\n[{{time}} \xB7 {{location}}] {{event}}: {{summary}}";
7381
7385
 
7382
7386
  // ../protocol/src/expr.ts
7383
7387
  var ExprError = class extends Error {
@@ -7610,7 +7614,8 @@ var STRUCTURE_PROPERTIES = {
7610
7614
  memory: ["scope", "operation", "key", "content"],
7611
7615
  synopsis: ["time", "location", "event", "summary"],
7612
7616
  options: ["label", "content"],
7613
- custom: []
7617
+ custom: [],
7618
+ annotations: []
7614
7619
  };
7615
7620
  function structureFieldSchema(decl, expand = (t) => t) {
7616
7621
  const d = (t) => t ? expand(t).trim() || void 0 : void 0;
@@ -7659,6 +7664,8 @@ function structureFieldSchema(decl, expand = (t) => t) {
7659
7664
  },
7660
7665
  ...decl.count ? { minItems: decl.count, maxItems: decl.count } : {}
7661
7666
  });
7667
+ case "annotations":
7668
+ return withDesc({ type: "string" });
7662
7669
  case "custom":
7663
7670
  return decl.shape === "kv" ? withDesc({ type: "object", additionalProperties: { type: "string" } }) : withDesc({ type: "string" });
7664
7671
  }
@@ -7739,7 +7746,8 @@ function stageSchema(stage, description, tools = DEFAULT_TOOLS, expand = same) {
7739
7746
  function tool(name, description, parameters) {
7740
7747
  return { type: "function", function: { name, ...description ? { description } : {}, parameters } };
7741
7748
  }
7742
- function turnArtifacts(tools, workflow, stages, structures, expand = same) {
7749
+ function turnArtifacts(tools, workflow, stages, allStructures, expand = same) {
7750
+ const structures = allStructures.filter((s) => !isInlineStructure(s));
7743
7751
  const desc = (template, stage) => {
7744
7752
  if (!template) return void 0;
7745
7753
  const t = expand(stage ? fillTemplate(template, stageVars(stages, stage)) : template).trim();
@@ -8827,6 +8835,19 @@ function compile(tree) {
8827
8835
  decl.shape = o.shape === "kv" ? "kv" : "text";
8828
8836
  decl.display = o.display === "hidden" ? "hidden" : "card";
8829
8837
  }
8838
+ if (kind === "annotations") {
8839
+ if (!decl.formatPrompt.tag) decl.formatPrompt.tag = decl.formatPrompt.field;
8840
+ if (!decl.formatPrompt.field) decl.formatPrompt.field = decl.formatPrompt.tag;
8841
+ }
8842
+ if (o.history !== void 0) {
8843
+ if (kind !== "annotations" && kind !== "synopsis") diag.error("only annotations and synopsis structures support `history`", { ...where, path: `${where.path}.history` });
8844
+ else if (typeof o.history === "number" && Number.isInteger(o.history) && o.history >= 0) decl.history = o.history;
8845
+ else diag.error("`history` must be a non-negative integer", { ...where, path: `${where.path}.history` });
8846
+ }
8847
+ if (kind === "synopsis" && (o.history !== void 0 || o.historyTemplate !== void 0)) {
8848
+ decl.historyTemplate = str2(o.historyTemplate, DEFAULT_SYNOPSIS_HISTORY_TEMPLATE);
8849
+ if (!decl.historyTemplate.includes("{{content}}")) diag.warn("`historyTemplate` has no `{{content}}`: kept messages would lose their text", { ...where, path: `${where.path}.historyTemplate` });
8850
+ } else if (o.historyTemplate !== void 0) diag.warn("`historyTemplate` only applies to synopsis structures and is ignored", { ...where, path: `${where.path}.historyTemplate` });
8830
8851
  if (o.title !== void 0) decl.title = text(str2(o.title), where);
8831
8852
  if (!/^[A-Za-z_][\w.-]*$/.test(decl.tag)) diag.error(`invalid tag name \`${decl.tag}\``, where);
8832
8853
  if (!/^[A-Za-z_]\w*$/.test(decl.field)) diag.error(`invalid field name \`${decl.field}\``, where);
@@ -8864,9 +8885,16 @@ function compile(tree) {
8864
8885
  if (e) meta.tasks[task] = e;
8865
8886
  }
8866
8887
  const tsSrc = isObj3(raw.tokenSaving) ? raw.tokenSaving : {};
8888
+ const userInputMode = (v, path) => {
8889
+ if (v === void 0) return void 0;
8890
+ if (USER_INPUT_MODES.includes(v)) return v;
8891
+ diag.error(`\`userInput\` must be one of ${USER_INPUT_MODES.join(", ")}`, { file: M, path });
8892
+ return void 0;
8893
+ };
8867
8894
  const tokenSaving = {
8868
8895
  keepFullTurns: typeof tsSrc.keepFullTurns === "number" ? tsSrc.keepFullTurns : 6,
8869
8896
  truncateUserAfter: typeof tsSrc.truncateUserAfter === "number" ? tsSrc.truncateUserAfter : 2e3,
8897
+ userInput: userInputMode(tsSrc.userInput, "tokenSaving.userInput") ?? "truncate",
8870
8898
  synopsisTemplate: str2(tsSrc.synopsisTemplate, "[{{time}} \xB7 {{location}}] {{event}}: {{summary}}")
8871
8899
  };
8872
8900
  const syntheticTurns = (Array.isArray(raw.syntheticTurns) ? raw.syntheticTurns : []).map((s, i) => {
@@ -8977,7 +9005,7 @@ function compile(tree) {
8977
9005
  },
8978
9006
  modules: Object.fromEntries(modules.filter((m) => m.toggle === "user").map((m) => [m.id, m.default])),
8979
9007
  params: Object.fromEntries(params.map((p) => [p.id, p.default])),
8980
- tokenSaving: { enabled: false, keepFullTurns: tokenSaving.keepFullTurns, truncateUserAfter: tokenSaving.truncateUserAfter, ...sect("tokenSaving") },
9008
+ tokenSaving: { enabled: false, keepFullTurns: tokenSaving.keepFullTurns, truncateUserAfter: tokenSaving.truncateUserAfter, userInput: tokenSaving.userInput, ...sect("tokenSaving") },
8981
9009
  meta: { recentCount: meta.recentCount, ...sect("meta") }
8982
9010
  };
8983
9011
  for (const k of ["names", "prefixes"]) if (isObj3(identitySrc[k])) Object.assign(configurationDefaults.identity[k], identitySrc[k]);
@@ -8989,12 +9017,14 @@ function compile(tree) {
8989
9017
  if (!(k in configurationDefaults.params)) diag.error(`configurationDefaults.params: unknown param \`${k}\``, { file: M, path: `configurationDefaults.params.${k}` });
8990
9018
  else configurationDefaults.params[k] = v;
8991
9019
  }
9020
+ userInputMode(sect("tokenSaving").userInput, "configurationDefaults.tokenSaving.userInput");
8992
9021
  for (const k of Object.keys(cd)) if (!(k in configurationDefaults)) diag.warn(`unknown configuration section \`${k}\``, { file: M, path: `configurationDefaults.${k}` });
8993
9022
  const configurations = (Array.isArray(raw.configurations) ? raw.configurations : []).map((c, i) => {
8994
9023
  const o = isObj3(c) ? c : {};
8995
9024
  const rc = { id: str2(o.id), name: str2(o.name, str2(o.id)), diff: isObj3(o.diff) ? o.diff : {} };
8996
9025
  if (o.description !== void 0) rc.description = str2(o.description);
8997
9026
  if (!rc.id) diag.error("configuration id is required", { file: M, path: `configurations[${i}]` });
9027
+ if (isObj3(rc.diff.tokenSaving)) userInputMode(rc.diff.tokenSaving.userInput, `configurations[${i}].diff.tokenSaving.userInput`);
8998
9028
  return rc;
8999
9029
  });
9000
9030
  if (!configurations.length) configurations.push({ id: "default", name: "Default", diff: {} });
@@ -9433,6 +9463,7 @@ function validate(p, map, diag) {
9433
9463
  for (const [k, ids] of byKind) if (ids.length > 1) diag.error(`only one structure of kind ${k} is allowed (${ids.join(", ")})`, { file: M, path: "structures" });
9434
9464
  const fields = /* @__PURE__ */ new Map();
9435
9465
  for (const s of Object.values(p.structures)) {
9466
+ if (isInlineStructure(s)) continue;
9436
9467
  if (fields.has(s.field)) diag.error(`structures ${fields.get(s.field)} and ${s.id} share field \`${s.field}\``, { file: M, path: "structures" });
9437
9468
  fields.set(s.field, s.id);
9438
9469
  }
@@ -9446,7 +9477,7 @@ function validate(p, map, diag) {
9446
9477
  if (env.thinking.name === env.result.name) diag.error("envelope thinking and result fields must differ", T);
9447
9478
  const synthetic = new Set(p.syntheticTurns.map((t) => t.tool.name));
9448
9479
  if (synthetic.has(p.tools.submit.name)) diag.error(`tools.submit.name \`${p.tools.submit.name}\` is also a synthetic turn tool`, T);
9449
- const structureFields = new Set(Object.values(p.structures).map((s) => s.field));
9480
+ const structureFields = new Set(Object.values(p.structures).filter((s) => !isInlineStructure(s)).map((s) => s.field));
9450
9481
  function checkNames(w, where) {
9451
9482
  const structs = Object.values(p.structures);
9452
9483
  const a = turnArtifacts(p.tools, w, w.stages, structs);
@@ -9665,7 +9696,7 @@ async function dev(o) {
9665
9696
  import { cp, mkdir as mkdir2, readdir as readdir2, readFile as readFile2, rename, stat, writeFile as writeFile2 } from "node:fs/promises";
9666
9697
  import { join as join2 } from "node:path";
9667
9698
  import { fileURLToPath } from "node:url";
9668
- var CLI_VERSION = true ? "1.6.0" : "0.0.0-dev";
9699
+ var CLI_VERSION = true ? "1.7.0" : "0.0.0-dev";
9669
9700
  var TEMPLATE = fileURLToPath(new URL("../template", import.meta.url));
9670
9701
  async function* files(dir) {
9671
9702
  for (const ent of await readdir2(dir, { withFileTypes: true })) {
@@ -9761,7 +9792,7 @@ async function installSkills(target, source = SKILLS_SOURCE) {
9761
9792
  }
9762
9793
 
9763
9794
  // src/cli.ts
9764
- var VERSION = true ? "1.6.0" : "0.0.0-dev";
9795
+ var VERSION = true ? "1.7.0" : "0.0.0-dev";
9765
9796
  var HELP = `ap ${VERSION} \u2014 Advanced Preset toolchain
9766
9797
 
9767
9798
  Usage
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "advanced-preset",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "Advanced Preset CLI (ap) — build, check, preview and hot-reload Advanced Preset sources for SillyTavern.",
5
5
  "keywords": [
6
6
  "advanced-preset",
@@ -68,6 +68,10 @@ AP 接管的每条助手消息在 `message.extra.advanced_preset` 中保存一
68
68
  "patched": "…", // 有补丁时:修订后的正文
69
69
  "patchesApplied": true, // 当前显示的是否为修订稿
70
70
  "structures": { "options": [ { "label": "…", "content": "…" } ] },
71
+ "annotations": [ // 行内批注(有 annotations 结构时)
72
+ { "id": "a1", "start": 12, "end": 17, "quote": "轻轻推开门", "prefix": "…", "suffix": "…", "comment": "…" }
73
+ ],
74
+ "annotationVersions": { "draft": [ ], "patched": [ ] }, // 有补丁时:两份正文各自的批注
71
75
  "events": [ { "type": "memory.replace", "key": "…", "content": "…", "source": "model" } ],
72
76
  "metaInput": ["…"], // 本轮输入中的元指令片段
73
77
  "reasoning": "…", // 模型的推理内容(如果有)
@@ -77,7 +81,23 @@ AP 接管的每条助手消息在 `message.extra.advanced_preset` 中保存一
77
81
 
78
82
  - 阶段 `status`:`done`(完成)、`missing`(必需阶段没有输出)、`interrupted`(生成被中断);
79
83
  - 消息正文 `mes` 只包含终结正文(或纯轮次的回复文字),阶段与结构不写进正文,所以切换预设或关闭 AP 后聊天记录仍然干净;
80
- - 用户在 UI 中切换原稿 / 修订稿时,`mes` 与 `patchesApplied` 随之改变。
84
+ - 用户在 UI 中切换原稿 / 修订稿时,`mes`、`patchesApplied` 与 `annotations` 随之改变。
85
+ - 用户在事件摘要卡片上编辑后,`structures` 中对应的值被直接覆盖(只改当前 swipe,`mes` 不变)。
86
+
87
+ ### 行内批注
88
+
89
+ `annotations` 中每条批注的 `start` / `end` 是干净正文 `mes` 中的 UTF-16 偏移,`quote` 是被批注的原文,`prefix` / `suffix` 是前后各约 32 字的上下文,`comment` 是批注文字。用户编辑消息后 AP 用 `quote` 与上下文重新定位并更新偏移;找不到时标记 `"orphan": true`,保留但不显示。
90
+
91
+ **显示面**:AP 在每个显示面中分别定位批注——ST 的 `.mes_text`,以及其中每个**同源** iframe(例如酒馆助手渲染的前端界面)的文档。匹配按可见文字进行(跳过隐藏元素、`script` / `style`,忽略 Markdown 强调符号、零宽字符与引号样式差异),每个显示面里一条批注至多标记一处;有多处时依次用上下文、出现序号消歧。划线使用各文档自己的 `CSS.highlights`(名为 `ap-anno`),不修改前端的 DOM;气泡统一显示在 ST 主页面。跨源 iframe 无法适配。
92
+
93
+ **前端控制**:在 iframe 的 `<html>` 或 `<body>` 上设置
94
+
95
+ | 属性 | 效果 |
96
+ | --- | --- |
97
+ | `data-ap-annotations="off"` | 不在此 iframe 中显示批注 |
98
+ | `data-ap-annotations="manual"` | 不自动标记,前端通过 `window.parent.AdvancedPreset.annotations` 自行绘制 |
99
+
100
+ **TauriTavern**:在受管 ChatSurface 模式下(`isManagedOwnershipRequired()` 为真),AP 通过清单 hook `chatSurface` 注册 participant `advanced-preset/annotations`,在 `didCommitContent` 中挂载批注并返回 disposer;其他模式使用常规 DOM 监听。
81
101
 
82
102
  ### 事件
83
103
 
@@ -106,6 +126,9 @@ AP 接管的每条助手消息在 `message.extra.advanced_preset` 中保存一
106
126
  | `listPresets()` | 可用预设:`[{ id, name, version, source }]`,`source` 为 `builtin` / `installed` / `dev` |
107
127
  | `activePresetId()` | 当前选中的 Carrier 对应的预设 id,未选中时为 `null` |
108
128
  | `update()` | 检查并应用扩展更新,返回 `'updated'` / `'current'` |
129
+ | `togglePanel(open?)` | 打开 / 关闭 AP 面板(不传参数时切换) |
130
+ | `annotations.get(mesId)` | 消息当前可见的行内批注(不含孤立批注),偏移对应当前 `mes` |
131
+ | `annotations.on('change', (mesId) => …)` | 订阅批注变化(生成、编辑、切换 swipe 或补丁),返回取消订阅函数 |
109
132
 
110
133
  脚本调用 ST 生成(如 `generateRaw` / `generateQuietPrompt`)时,如果当前选中的是 Carrier,AP 同样会组装区域并接管请求,来源为 `task`;调用方的工具与 JSON Schema 的处理见 [transports.md](transports.md#调用方的工具与-schema)。
111
134
 
@@ -38,7 +38,7 @@ configurationDefaults:
38
38
  prefixes: { narrator: "" } # 空字符串 = 使用角色的默认前缀
39
39
  modules: { options: true } # 只能写用户可开关的模组
40
40
  params: { length: 600 }
41
- tokenSaving: { enabled: false, keepFullTurns: 6, truncateUserAfter: 2000 }
41
+ tokenSaving: { enabled: false, keepFullTurns: 6, truncateUserAfter: 2000, userInput: truncate }
42
42
  meta: { recentCount: 3 }
43
43
  ```
44
44
 
@@ -57,7 +57,7 @@ configurationDefaults:
57
57
  | `identity` | `names.<actor>` / `prefixes.<actor>` | `''` | 覆盖角色名字 / 前缀;空字符串表示用清单 `actors` 的值 |
58
58
  | `modules` | `<module id>` | 模组的 `default` | 用户可开关模组的状态 |
59
59
  | `params` | `<param id>` | 参数的 `default` | 参数值 |
60
- | `tokenSaving` | `enabled` / `keepFullTurns` / `truncateUserAfter` | `false` / 清单 `tokenSaving` 的值 | 省 token([meta-and-tasks.md](meta-and-tasks.md#省-token)) |
60
+ | `tokenSaving` | `enabled` / `keepFullTurns` / `truncateUserAfter` / `userInput` | `false` / 清单 `tokenSaving` 的值 | 省 token([meta-and-tasks.md](meta-and-tasks.md#省-token)) |
61
61
  | `meta` | `recentCount` | 清单 `meta.recentCount` | `{{ap::recent_meta}}` 的条数 |
62
62
 
63
63
  ### sampling
@@ -270,16 +270,18 @@ meta:
270
270
  tokenSaving:
271
271
  keepFullTurns: 6
272
272
  truncateUserAfter: 2000
273
+ userInput: truncate
273
274
  synopsisTemplate: "[{{time}} · {{location}}] {{event}}: {{summary}}"
274
275
  ```
275
276
 
276
277
  | 字段 | 默认 | 说明 |
277
278
  | --- | --- | --- |
278
279
  | `keepFullTurns` | `6` | 保留最近几轮完整内容 |
279
- | `truncateUserAfter` | `2000` | 更早的用户消息超过这个字符数就截断并加 `…`(0 表示不截断) |
280
+ | `truncateUserAfter` | `2000` | `userInput: truncate` 时,更早的用户消息超过这个字符数就截断并加 `…`(0 表示不截断) |
281
+ | `userInput` | `truncate` | 更早的用户消息:`keep`(原样)/ `truncate`(截断)/ `remove`(不发送) |
280
282
  | `synopsisTemplate` | 如上 | 更早的助手消息替换成事件摘要时的格式 |
281
283
 
282
- 是否启用由配置 `tokenSaving.enabled` 决定(默认关闭);`keepFullTurns` / `truncateUserAfter` 也可在配置中覆盖。规则见 [meta-and-tasks.md](meta-and-tasks.md#省-token)。
284
+ 是否启用由配置 `tokenSaving.enabled` 决定(默认关闭);`keepFullTurns` / `truncateUserAfter` / `userInput` 是配置的默认值,用户可在面板中修改。最近几条消息附带事件摘要由 `synopsis` 结构的 `history` / `historyTemplate` 声明(见 [structures.md](structures.md#synopsis))。规则见 [meta-and-tasks.md](meta-and-tasks.md#省-token)。
283
285
 
284
286
  ## 主题 `theme`
285
287
 
@@ -85,7 +85,8 @@ AdvancedPreset.nextGeneration({ origin: 'story' }); // 只影响下一次生成
85
85
 
86
86
  1. **剔除纯轮次**:`turn` 为 `meta-pure` / `summary` / `impersonate` 的助手消息,连同触发它的用户消息一起不发送(当前输入除外);
87
87
  2. **剥离元指令**:较早用户消息中的元指令片段被去掉;剥离后为空的用户消息不发送;当前输入保持原样;
88
- 3. **省 token**(启用时,见下)。
88
+ 3. **省 token**(启用时,见下);
89
+ 4. **附带事件摘要**:`synopsis` 结构声明了 `history: N` 且请求它的模组已开启时,最近 N 条助手消息(含已被省 token 替换的)中仍为原文的,按 `historyTemplate` 附上自己的事件摘要(见 [structures.md](structures.md#synopsis))。
89
90
 
90
91
  ## 省 token
91
92
 
@@ -94,10 +95,10 @@ AdvancedPreset.nextGeneration({ origin: 'story' }); // 只影响下一次生成
94
95
  - 最近 `keepFullTurns` 条助手消息以及它们前面的用户消息保持完整(用户消息与其后的助手回复算同一轮);当前输入总是保留;
95
96
  - 更早的消息中:
96
97
  - 已被总结覆盖的(真实索引 ≤ 当前总结的 `coversUpTo`)**不发送**,由模组通过 `{{ap::recap}}` 提供总结;
97
- - 用户消息超过 `truncateUserAfter` 个字符时截断并加 `…`(0 表示不截断);
98
+ - 用户消息按 `userInput` 处理:`keep` 原样发送;`truncate` 超过 `truncateUserAfter` 个字符时截断并加 `…`(0 表示不截断);`remove` 不发送。扩写类写法中正文(及其事件摘要)已包含用户输入,适合 `remove`;
98
99
  - 助手消息如果有事件摘要(`synopsis` 结构),替换为 `tokenSaving.synopsisTemplate` 渲染的一行(占位符 `{{time}}` `{{location}}` `{{event}}` `{{summary}}`);没有则保持原样。
99
100
 
100
- `keepFullTurns`、`truncateUserAfter` 的默认值来自清单 `tokenSaving`,可被配置覆盖。条件变量 `features.tokenSaving` 表示已开启,`recap.present` 表示有生效的总结。
101
+ `keepFullTurns`、`truncateUserAfter`、`userInput` 的默认值来自清单 `tokenSaving`,可被配置覆盖。条件变量 `features.tokenSaving` 表示已开启,`recap.present` 表示有生效的总结。
101
102
 
102
103
  ## 脚本路径的回填
103
104
 
@@ -1,6 +1,6 @@
1
1
  # 结构
2
2
 
3
- **结构**是模型在正文之外交回的附加数据:叙述者的回复、记忆更新、剧情总结、代写、事件摘要、选项,以及预设自定义的块。结构的**机制**(数据形状、解析、保存、卡片 UI)由 AP 内置;预设只为它们**命名**(标签名 / 字段名)并提供**说明文字**。
3
+ **结构**是模型在正文之外交回的附加数据:叙述者的回复、记忆更新、剧情总结、代写、事件摘要、选项、行内批注,以及预设自定义的块。结构的**机制**(数据形状、解析、保存、卡片 UI)由 AP 内置;预设只为它们**命名**(标签名 / 字段名)并提供**说明文字**。
4
4
 
5
5
  ```yaml
6
6
  structures:
@@ -27,17 +27,20 @@ structures:
27
27
 
28
28
  | 字段 | 默认 | 说明 |
29
29
  | --- | --- | --- |
30
- | `kind` | 结构 id | 种类:`reply` `memory` `recap` `impersonate` `synopsis` `options` `custom`。其他值报错 |
30
+ | `kind` | 结构 id | 种类:`reply` `memory` `recap` `impersonate` `synopsis` `options` `annotations` `custom`。其他值报错 |
31
31
  | `tag` | 结构 id | 对话式写法中的 XML 标签名,须匹配 `^[A-Za-z_][\w.-]*$` |
32
32
  | `field` | 结构 id | 指令式写法中的 JSON 字段名 / 工具参数名,须匹配 `^[A-Za-z_]\w*$` |
33
33
  | `description` | — | 字段整体的说明,进入 JSON Schema 的 `description`(模型可见,支持宏) |
34
34
  | `properties` | — | 子字段说明,键只能是该种类的固定子字段(见下表),其他键报错 |
35
35
  | `count` | — | 仅 `options`:要求的选项数,写入 schema 的 `minItems` / `maxItems` |
36
+ | `history` | `0` | 仅 `synopsis`:最近几条原样发送的助手消息附带自己的事件摘要(0 表示不附带),见下 |
37
+ | `historyTemplate` | `{{content}}` + 空行 + `[{{time}} · {{location}}] {{event}}: {{summary}}` | 仅 `synopsis`:附带摘要的消息的格式 |
36
38
  | `title` | 结构 id | 卡片标题(自定义结构用;`reply` 等也可以设,如花瓣的回复卡片显示叙述者名字)。可在 `locales` 中翻译 |
37
39
  | `formatPrompt.tag` | `''` | 对话式下的格式说明,由 `{{ap::structure::<id>}}` 输出 |
38
40
  | `formatPrompt.field` | `''` | 指令式下的格式说明,由 `{{ap::structure::<id>}}` 输出 |
39
41
  | `shape` | `text` | 仅 `custom`:`text`(一段文字)或 `kv`(键值对) |
40
42
  | `display` | `card` | 仅 `custom`:`card`(消息下方显示可折叠卡片)或 `hidden`(只保存,不显示) |
43
+ | `history` | `0` | 仅 `annotations`:把批注以 CriticMarkup 重新注入最近 N 条助手消息的历史(见 [annotations](#annotations)) |
41
44
 
42
45
  约束(报错):
43
46
 
@@ -54,10 +57,13 @@ structures:
54
57
  | `impersonate` | `string` | — | 是 | 代写的用户下一条输入,卡片可“填入输入框” |
55
58
  | `synopsis` | `{ time, location, event, summary }` | `time` `location` `event` `summary` | 是 | 本段的事件摘要卡片;省 token 时替代旧回复 |
56
59
  | `options` | `{ label, content }[]` | `label` `content` | 是 | 选项卡片,点击填入输入框 |
60
+ | `annotations` | 正文内标记 → `Annotation[]` | — | — | **行内**结构:写在正文里,解析后从正文移除,显示为虚线下划线与气泡 |
57
61
  | `custom` | `string` 或 `Record<string, string>` | — | 是 | 预设自定义的块(状态栏等) |
58
62
 
59
63
  “必填”指生成的 JSON Schema 中该字段是否进入 `required`。`memory` 与 `recap` 是可选的:模型可以不写。
60
64
 
65
+ `annotations` 是唯一的**行内结构**:它不是 schema 字段,也没有标签,两种写法下都由模型直接写在正文里(`tag` / `field` / `description` / `properties` 不使用)。
66
+
61
67
  ## 何时请求结构
62
68
 
63
69
  结构**不会自动出现**。只有当某个**当前生效的模组**在 frontmatter 中列出它时,这一轮才会请求它:
@@ -74,6 +80,7 @@ structures: [options]
74
80
 
75
81
  - 指令式:作为字段加入回合 schema(结构化、单工具),或作为锚点阶段工具的附加参数(多工具),位置见 [workflows.md](workflows.md#结构的位置);
76
82
  - 对话式:AP 从模型输出中提取对应标签;
83
+ - 行内结构(`annotations`):两种写法相同,AP 从正文中解析标记;
77
84
 
78
85
  由脚本或其他扩展发起的任务生成(`gen.type == "script"` 且 `gen.origin == "task"`)不请求任何结构,即使模组列出了它们。
79
86
 
@@ -157,7 +164,28 @@ SillyTavern 的“代写”按钮会发送清单中的 `meta.impersonatePrompt`
157
164
 
158
165
  ### synopsis
159
166
 
160
- 显示为事件摘要卡片。启用省 token 时,较早的助手消息会被 `tokenSaving.synopsisTemplate` 渲染的摘要替代(见 [meta-and-tasks.md](meta-and-tasks.md#省-token))。
167
+ 显示为事件摘要卡片,用户可在卡片上编辑四个字段,保存后直接覆盖该楼层(当前 swipe)Sidecar 中的结构值,不改正文 `mes`,也不保留原值。事件摘要在历史中有两种用法,模板都由预设指定,都读取编辑后的值:
168
+
169
+ - **替换**:启用省 token 时,较早的助手消息被 `tokenSaving.synopsisTemplate` 渲染的摘要替代(见 [meta-and-tasks.md](meta-and-tasks.md#省-token));
170
+ - **附带**:`history: N` 时,最近 N 条助手消息保留原文,并按 `historyTemplate` 附上自己的摘要(与是否启用省 token 无关)。占位符 `{{content}}`(消息原文)与 `{{time}}` `{{location}}` `{{event}}` `{{summary}}`。
171
+
172
+ 故事中没有明确时间时,模型往往只能从先前的事件摘要推断时间;附带最近几条摘要可以避免时间线混乱。只有请求了该结构的模组开启时才附带;已被省 token 替换的消息也计入 N。
173
+
174
+ ```yaml
175
+ structures:
176
+ synopsis:
177
+ kind: synopsis
178
+ history: 3
179
+ historyTemplate: |-
180
+ {{content}}
181
+
182
+ <synopsis>
183
+ <time>{{time}}</time>
184
+ <location>{{location}}</location>
185
+ <event>{{event}}</event>
186
+ <summary>{{summary}}</summary>
187
+ </synopsis>
188
+ ```
161
189
 
162
190
  ### options
163
191
 
@@ -185,6 +213,49 @@ structures:
185
213
  Previous status: {{ap::structure::status::last}}
186
214
  ```
187
215
 
216
+ ### annotations
217
+
218
+ 行内批注:模型在正文中用 CriticMarkup 的子集标出一段文字并附上批注:
219
+
220
+ ```text
221
+ 她{==轻轻推开门==}{>>这里的“轻轻”呼应上一幕的犹豫。<<},走了进去。
222
+ ```
223
+
224
+ ```yaml
225
+ structures:
226
+ annotations:
227
+ kind: annotations
228
+ history: 0
229
+ formatPrompt:
230
+ tag: |-
231
+ Inline annotations are written inside the story text as `{==phrase==}{>>note<<}`.
232
+ Mark a short phrase or a single sentence, never across paragraphs, and never nest marks.
233
+ ```
234
+
235
+ `formatPrompt` 只需写 `tag` 或 `field` 之一,另一个自动取相同文字(行内写法与输出方式无关)。建议这里只写语法,批注的用途、口吻与数量写在请求它的模组里,这样不同模组(如作者评论、术语注释)可以复用同一份语法说明。
236
+
237
+ **语法**(严格成对):
238
+
239
+ | 写法 | 结果 |
240
+ | --- | --- |
241
+ | `{==原文==}{>>批注<<}` | 一条批注;正文保留“原文” |
242
+ | 单独的 `{==原文==}` | 去掉标记,保留“原文” |
243
+ | 单独的 `{>>批注<<}` | 整段移除 |
244
+ | 未闭合的 `{==` | 丢弃标记,保留其后文字 |
245
+ | 未闭合的 `{>>` | 按原文保留 |
246
+
247
+ 标记不能跨段落(空行),也不能嵌套。被批注的“原文”应是短语或句子。
248
+
249
+ **保存**:消息的 `mes` 始终是**去掉标记后的干净正文**;批注单独存于 sidecar 的 `annotations`(见 [artifact-and-runtime.md](artifact-and-runtime.md#行内批注))。每条批注用偏移 + 原文 + 前后各约 32 字的上下文锚定;用户编辑正文后 AP 重新定位,找不到的批注成为**孤立批注**——保留但不显示,可在调试页查看。工作流的补丁作用于带标记的文本,因此补丁前后各有一份批注,随“显示 / 撤销补丁”切换。
250
+
251
+ **流式**:生成过程中 AP 只把已确定的部分交给 ST,标记不会闪现;下划线在生成结束后出现。
252
+
253
+ **历史**:默认(`history: 0`)历史中只有干净正文。`history: N` 时,最近 N 条助手消息(未被用户改动过的)在组装时把批注以 CriticMarkup 重新注入,帮助模型保持批注风格;代价是更多 token。
254
+
255
+ **显示**:被批注的文字显示虚线下划线(CSS Custom Highlight API,不改动消息 DOM;不支持时退化为 `<ap-anno>` 包裹),悬停或轻点弹出气泡,署名为叙述者。同源 iframe 前端(如酒馆助手渲染的界面)自动适配,详见 [artifact-and-runtime.md](artifact-and-runtime.md#行内批注)。用户可在外观设置中关闭显示或更改下划线颜色。
256
+
257
+ 请求方式与其他结构相同:由生效模组的 `structures: [annotations]` 请求,并在正文里放 `{{ap::structure::annotations}}` 输出格式说明。
258
+
188
259
  ### 保存条件
189
260
 
190
- 记忆与总结只在**拦截器参与的生成**中保存,且生成类型不是 `quiet`(后台生成)。所有结构值都写在消息的 `extra.advanced_preset.structures` 中(见 [artifact-and-runtime.md](artifact-and-runtime.md))。
261
+ 记忆与总结只在**拦截器参与的生成**中保存,且生成类型不是 `quiet`(后台生成)。所有结构值都写在消息的 `extra.advanced_preset.structures` 中,行内批注除外(`extra.advanced_preset.annotations`,见 [artifact-and-runtime.md](artifact-and-runtime.md))。
@@ -4,6 +4,8 @@ on:
4
4
  branches: [main]
5
5
  tags: ['v*']
6
6
  pull_request:
7
+ permissions:
8
+ contents: write # create the GitHub Release on v* tags
7
9
  jobs:
8
10
  build:
9
11
  runs-on: ubuntu-latest