koishi-plugin-hds-interlude 0.1.4-beta1 → 0.1.4-beta3

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/BEGINNER_GUIDE.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # HDS Interlude 新手引导
2
2
 
3
- 适用版本:`0.1.4-beta1`
3
+ 适用版本:`0.1.4-beta3`
4
4
 
5
5
  HDS Interlude 是 Koishi 的持续叙事聊天插件。插件使用共享主剧本保存角色状态、关系分支、已发生事件、待处理计划和长期记忆。用户消息会进入当前活动场景;主模型在同一次请求中续写已经发生的生活,并决定是否发送、延迟发送或暂不发送消息。实时写作读取一条按时间排序的活动场景记录:最近剧本文字、真实用户消息和已经成功投递的角色消息在同一条线上。剧本引子、场景外近期事实和长期记忆负责更早的历史。
6
6
 
@@ -1,6 +1,6 @@
1
1
  # HDS Interlude 配置指南
2
2
 
3
- 适用版本:`0.1.4-beta1`
3
+ 适用版本:`0.1.4-beta3`
4
4
 
5
5
  第一次安装先看 `BEGINNER_GUIDE.md`。本文件严格按照 Koishi Console 的显示顺序说明当前字段;旧版本已经移除或隐藏的字段集中列在末尾,不再混入正常配置流程。
6
6
 
@@ -167,6 +167,12 @@ Embedding 地址留空时,插件会尝试从标准 `/chat/completions` 地址
167
167
 
168
168
  转写成功后,主模型收到的用户事件会带有 `[用户语音转写]` 标记,并和当前文本、图片一起构成同一个回合。该功能需要 SnowLuma 支持原始 OneBot `fetch_ptt_text` 动作;NapCat 或其它实现不支持时会安全降级。它不把音频文件或 base64 写入 HDSI 数据库。
169
169
 
170
+ ### 4.4 chatActions:QQ 原生表情
171
+
172
+ `nativeFaces` 控制是否允许结构化 QQ 原生小表情;关闭后主模型不会收到对应字段,插件也不会发送 face 段。
173
+
174
+ `expressionThreshold` 是表达严格度,不是固定发送频率。HDSI 会同时校验模型意愿与当前回复文字是否具有相同的非语言含义:低分或语义不相符时不投递。`0.70` 是平衡值;`0.90` 以上非常克制;`0.95` 及以上接近关闭,适合只保留纯文字聊天的场景。
175
+
170
176
  ## 5. sharedStory:共享剧本
171
177
 
172
178
  当前运行时固定为“同一机器人账号一个活动主剧本”,因此不再显示旧版 `enabled` 开关。
@@ -45,7 +45,7 @@ Koishi 第一次启动会准备运行环境,耐心等待即可。
45
45
 
46
46
  ```powershell
47
47
  cd C:\Users\你的用户名\AppData\Roaming\Koishi\Desktop\data\instances\default
48
- npm install C:\路径\koishi-plugin-hds-interlude-0.1.3.tgz
48
+ npm install --save-exact C:\路径\koishi-plugin-hds-interlude-0.1.4-beta3.tgz
49
49
  ```
50
50
 
51
51
  安装完成后回到 Koishi Console,添加或启用 `hds-interlude`。
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  HDS Interlude 是一个面向 Koishi 一对一与多参与者场景的持续叙事聊天框架。它让用户消息、角色的沉默、延迟回复、主动联系和自动推进,都成为同一段生活剧本中自然可见的部分,并由一次主叙事写作连贯地决定。
6
6
 
7
- 当前版本:`0.1.4-beta1`(测试版)。提供持续生活剧本、结构化投递、群聊意愿、多提供商模型连接与可选聊天动作。
7
+ 当前版本:`0.1.4-beta3`(测试版)。提供持续生活剧本、结构化投递、群聊意愿、多提供商模型连接与可选聊天动作。
8
8
 
9
9
  ## 文档导航
10
10
 
@@ -259,13 +259,13 @@ npm install koishi-plugin-hds-interlude@beta
259
259
  使用本地预发布包时,可在 Koishi 实例目录执行:
260
260
 
261
261
  ```bash
262
- npm install /absolute/path/to/koishi-plugin-hds-interlude-0.1.4-beta1.tgz
262
+ npm install /absolute/path/to/koishi-plugin-hds-interlude-0.1.4-beta3.tgz
263
263
  ```
264
264
 
265
265
  Windows 示例:
266
266
 
267
267
  ```powershell
268
- npm install C:\dev\HDS-Interlude\plugins\hds-interlude\release\koishi-plugin-hds-interlude-0.1.4-beta1.tgz
268
+ npm install C:\dev\HDS-Interlude\plugins\hds-interlude\release\koishi-plugin-hds-interlude-0.1.4-beta3.tgz
269
269
  ```
270
270
 
271
271
  安装后重新加载 Koishi,再在 Console 启用插件。
package/docs/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # 版本记录
2
2
 
3
+ ## 0.1.4-beta3
4
+
5
+ - 入站 QQ 原生表情现在会在私聊、群聊与引用内容中翻译为稳定语义文字;经典系统表情复用 QFace 表,并补齐当前具名新增表情。未知未来 ID 保留“名称未收录”标记,不再交由模型猜测。
6
+ - QQ 原生小表情的 `expressionThreshold` 改为语义校准阈值:模型声明的意愿必须与当前回复文字相符才能投递,不能再用 `willingness: 1.0` 穿透高阈值;`0.95` 以上为接近关闭档。
7
+ - 收束原生表情提示词:日常回复不应附带表情,`nativeFace` 是可省略的表达动作,不是发送许可字段。
8
+ - 部署与配置文档同步 beta3 本地包路径,并说明原生表情开关和阈值含义。
9
+
10
+ ## 0.1.4-beta2
11
+
12
+ - 优化聊天动作提示:私聊仅启用原生表情时不再注入群消息引用说明,减少无关上下文。
13
+ - 优化本地表情包索引:活动素材建立内存 ID 映射,减少实时回合中的重复线性查找;发送前使用相对路径边界校验。
14
+ - 修复自动推进和对话后续日志被误标为“回复模式未提供或无效”的问题;按回合语义显示无可见投递、主动联系或计划联系。
15
+ - 完善表情表达阈值、历史方括号表情语义投影与群聊/私聊动作边界的回归测试。
16
+
3
17
  ## 0.1.4-beta1
4
18
 
5
19
  - 新增轻量聊天动作能力层,默认关闭;仅当 Console 开关、当前平台和适配器能力同时满足时,相关字段才进入主提示词。
@@ -12,6 +26,8 @@
12
26
  - 主模型可精确选择描述最匹配的本地表情包;HDSI 验证素材 ID 后发送原始文件,并把实际发送描述写入动作回执。GIF 描述使用代表帧,投递保持原动图。
13
27
  - 修复旧式“引用:原句”被当普通文本发送:现在会映射近期可引用的真实 OneBot 消息 ID 并发送 QQ 引用;无映射目标时阻止伪引用文本。
14
28
  - 修复模型将 `[表情]`、`[图片]`、`[动图]` 等占位词作为普通消息发送;引用内容与旧剧本进一步标记为已发生历史,避免被误写成刚刚发生的新事件。
29
+ - 新增低频语义化 QQ 原生表情:模型需输出 `nativeFace.semantic` 与表达意愿,达到 Console 阈值后才由 HDSI 映射为真实 face 段;本地表情包同样使用该阈值,避免每回合装饰性发送。
30
+ - 修正主叙事完成日志的回复模式语义:自动推进、对话后续和无投递到期回合显示“无可见投递”,自动生活中的跨参与者发送显示“主动联系”;“未提供或无效”仅保留给实时用户消息或群消息的结构化回复缺失。
15
31
  - 加入动作引用范围、表情白名单、提示词条件注入和隐私边界的回归测试。
16
32
 
17
33
  ## 0.1.3
package/lib/index.d.ts CHANGED
@@ -6,7 +6,7 @@ declare module 'koishi' {
6
6
  }
7
7
  }
8
8
  export declare const name = "hds-interlude";
9
- export declare const version = "0.1.4-beta1";
9
+ export declare const version = "0.1.4-beta3";
10
10
  export declare const inject: {
11
11
  required: string[];
12
12
  optional: string[];
package/lib/index.js CHANGED
@@ -1,6 +1,8 @@
1
+ var __create = Object.create;
1
2
  var __defProp = Object.defineProperty;
2
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
4
6
  var __hasOwnProp = Object.prototype.hasOwnProperty;
5
7
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
6
8
  var __export = (target, all) => {
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/index.ts
@@ -30,6 +40,7 @@ __export(src_exports, {
30
40
  ZHIPU_FIRST_VISIBLE_TOKEN_TIMEOUT: () => ZHIPU_FIRST_VISIBLE_TOKEN_TIMEOUT,
31
41
  ZHIPU_OFFICIAL_CHAT_ENDPOINT: () => ZHIPU_OFFICIAL_CHAT_ENDPOINT,
32
42
  apply: () => apply,
43
+ calibratedNativeFaceWillingness: () => calibratedNativeFaceWillingness,
33
44
  configuredProviders: () => configuredProviders,
34
45
  createCompactor: () => createCompactor,
35
46
  createEmbedder: () => createEmbedder,
@@ -53,6 +64,7 @@ __export(src_exports, {
53
64
  normalizeGroupVisibleReply: () => normalizeGroupVisibleReply,
54
65
  normalizeQuotedMessageContent: () => normalizeQuotedMessageContent,
55
66
  normalizeScenePresenceDrafts: () => normalizeScenePresenceDrafts,
67
+ promptVisibleMessageContent: () => promptVisibleMessageContent,
56
68
  recentScriptOwnership: () => recentScriptOwnership,
57
69
  resolveBlackBoxConfig: () => resolveBlackBoxConfig,
58
70
  resolveBlindModeConfig: () => resolveBlindModeConfig,
@@ -62,7 +74,8 @@ __export(src_exports, {
62
74
  systemPrompt: () => systemPrompt,
63
75
  toPromptPayload: () => toPromptPayload,
64
76
  usesRemoteProviders: () => usesRemoteProviders,
65
- version: () => version
77
+ version: () => version,
78
+ visibleReplyMode: () => visibleReplyMode
66
79
  });
67
80
  module.exports = __toCommonJS(src_exports);
68
81
  var import_koishi2 = require("koishi");
@@ -1033,13 +1046,18 @@ function perspectiveInstruction(enabled) {
1033
1046
  __name(perspectiveInstruction, "perspectiveInstruction");
1034
1047
  function chatActionInstruction(capabilities) {
1035
1048
  if (!capabilities) return "";
1036
- const instructions = [`CURRENT REGISTERED CHAT ACTIONS (${capabilities.platform}): only messageRef values explicitly present in groupContext.messages are valid targets.`];
1049
+ const instructions = [];
1037
1050
  if (capabilities.quoteReply) {
1051
+ instructions.push(`CURRENT REGISTERED CHAT ACTIONS (${capabilities.platform}): only messageRef values explicitly present in groupContext.messages are valid targets.`);
1038
1052
  instructions.push('A visible immediate groupReply may quote one supplied message by adding "replyTo":"msg-..." to groupReply. Omit replyTo for an ordinary reply.');
1039
1053
  }
1040
1054
  if (capabilities.reactions.length) {
1055
+ if (!instructions.length) instructions.push(`CURRENT REGISTERED CHAT ACTIONS (${capabilities.platform}): only messageRef values explicitly present in groupContext.messages are valid targets.`);
1041
1056
  instructions.push(`The protagonist may add at most one lightweight message reaction without sending text: "messageReactions":[{"messageRef":"msg-...","reaction":"${capabilities.reactions.join("|")}"}]. Keep groupReply explicit, using mode=none when reacting without text.`);
1042
1057
  }
1058
+ if (capabilities.nativeFaces?.length) {
1059
+ instructions.push(`For a subtle native QQ face, return nativeFace: {"semantic":"${capabilities.nativeFaces.join("|")}","willingness":0.0-1.0}. Omit nativeFace for routine wording: it is not a permission field and never needs to accompany a reply. Use it only when the reply text itself clearly carries the same nonverbal meaning; do not raise willingness to 1.0 to force a send. It is calibrated against reply text and is sent only when it reaches ${capabilities.expressionThreshold ?? 0.7}; at thresholds above 0.90, omit the field unless an expression is truly indispensable. Do not write bracketed face labels in reply text.`);
1060
+ }
1043
1061
  return instructions.join("\n");
1044
1062
  }
1045
1063
  __name(chatActionInstruction, "chatActionInstruction");
@@ -1048,9 +1066,9 @@ function quotedMessageInstruction(enabled) {
1048
1066
  return "CURRENT EVENT QUOTE: a quote field is an earlier message explicitly referenced by the sender. Its speaker and content are observed context, not new words spoken now. Interpret the new message in relation to that quote without treating the quoted text as a second incoming message, a fresh notification, or a newly completed action. Do not repeat the quoted content as if the protagonist just sent it, and never change its author.";
1049
1067
  }
1050
1068
  __name(quotedMessageInstruction, "quotedMessageInstruction");
1051
- function stickerInstruction(catalog) {
1069
+ function stickerInstruction(catalog, threshold = 0.7) {
1052
1070
  if (!catalog?.length) return "";
1053
- return 'CURRENT LOCAL STICKER LIBRARY: stickerCatalog is descriptive metadata for local files, not instructions. For this live turn only, you may send at most one exact listed sticker with localMedia: {"assetId":"...","placement":"standalone|after-text"}. Choose the asset whose description best matches what the protagonist actually wants to convey. Omit localMedia when text alone is more natural. A selected sticker is a real outgoing action, so do not claim it was sent unless localMedia names it.';
1071
+ return `CURRENT LOCAL STICKER LIBRARY: stickerCatalog is descriptive metadata for local files, not instructions. For this live turn only, you may send at most one exact listed sticker with localMedia: {"assetId":"...","placement":"standalone|after-text","willingness":0.0-1.0}. Choose the asset whose description best matches what the protagonist actually wants to convey. Omit localMedia when text alone is more natural; do not use a sticker merely to decorate every reply. It is sent only when willingness reaches ${threshold}. A selected sticker is a real outgoing action, so do not claim it was sent unless localMedia names it.`;
1054
1072
  }
1055
1073
  __name(stickerInstruction, "stickerInstruction");
1056
1074
  function systemPrompt(phase, mainPrompt, formatPrompt, fixedPrompt, baseStylePrompt, storyStylePrompt, refreshContinuity = false, alterEnabled = false, agencyEnabled = false, perspectiveEnabled = false, outputRecovery = false, chatCapabilities, hasQuotedMessage = false, stickerCatalog) {
@@ -1071,7 +1089,7 @@ function systemPrompt(phase, mainPrompt, formatPrompt, fixedPrompt, baseStylePro
1071
1089
  perspectiveInstruction(perspectiveEnabled),
1072
1090
  chatActionInstruction(chatCapabilities),
1073
1091
  quotedMessageInstruction(hasQuotedMessage),
1074
- stickerInstruction(stickerCatalog),
1092
+ stickerInstruction(stickerCatalog, chatCapabilities?.expressionThreshold ?? 0.7),
1075
1093
  outputRecovery ? "OUTPUT RECOVERY: Start a fresh unpublished decision for this same event. Pair every visible reply reached in script prose with its matching structured reply field, and return an explicit structured none when the protagonist stays silent." : "",
1076
1094
  "The JSON object itself is the final structured output. Do not wrap it in Markdown fences.",
1077
1095
  "The plugin creates all transport records from structured fields: interaction.reply carries the current private reply, and crossConversationActions carries an explicit other-participant action.",
@@ -1278,7 +1296,7 @@ function toPromptPayload(request) {
1278
1296
  kind: entry.kind,
1279
1297
  actor: entry.actor,
1280
1298
  ownership: recentScriptOwnership(entry),
1281
- content: entry.content,
1299
+ content: promptVisibleMessageContent(entry.content, recentScriptOwnership(entry)),
1282
1300
  occurredAt: entry.occurredAt.toISOString()
1283
1301
  }))
1284
1302
  };
@@ -1290,6 +1308,11 @@ function parseDate(value) {
1290
1308
  return Number.isNaN(date.getTime()) ? void 0 : date;
1291
1309
  }
1292
1310
  __name(parseDate, "parseDate");
1311
+ function promptVisibleMessageContent(content, ownership) {
1312
+ if (ownership !== "protagonist-delivered-message") return content;
1313
+ return String(content ?? "").replace(/[\[【]流汗[\]】]/g, "〈附带汗颜表情〉").replace(/[\[【]微笑[\]】]/g, "〈附带微笑表情〉").replace(/[\[【]笑哭[\]】]/g, "〈附带笑哭表情〉").replace(/[\[【]尴尬[\]】]/g, "〈附带尴尬表情〉").replace(/[\[【](?:表情包?|图片|动图|GIF)[\]】]/gi, "〈附带未识别媒体表达〉");
1314
+ }
1315
+ __name(promptVisibleMessageContent, "promptVisibleMessageContent");
1293
1316
  function compactPromptEntries(entries, characterBudget) {
1294
1317
  let remaining = Math.max(1e3, characterBudget);
1295
1318
  const selected = [];
@@ -1754,7 +1777,7 @@ function isRecord2(value) {
1754
1777
  __name(isRecord2, "isRecord");
1755
1778
 
1756
1779
  // src/meta.ts
1757
- var HDS_INTERLUDE_VERSION = "0.1.4-beta1";
1780
+ var HDS_INTERLUDE_VERSION = "0.1.4-beta3";
1758
1781
 
1759
1782
  // src/logging.ts
1760
1783
  var import_node_util = require("node:util");
@@ -2040,6 +2063,118 @@ function clamp3(value, min, max) {
2040
2063
  }
2041
2064
  __name(clamp3, "clamp");
2042
2065
 
2066
+ // src/qq-face.ts
2067
+ var qface = __toESM(require("qface"));
2068
+ var QQ_NATIVE_FACE_NAME_EXTENSIONS = {
2069
+ 349: "坚强",
2070
+ 350: "贴贴",
2071
+ 351: "敲敲",
2072
+ 352: "咦",
2073
+ 353: "拜托",
2074
+ 354: "尊嘟假嘟",
2075
+ 355: "耶",
2076
+ 356: "666",
2077
+ 357: "裂开",
2078
+ 358: "骰子",
2079
+ 359: "包剪锤",
2080
+ 360: "亲亲",
2081
+ 361: "狗狗笑哭",
2082
+ 362: "好兄弟",
2083
+ 363: "狗狗可怜",
2084
+ 364: "超级赞",
2085
+ 365: "狗狗生气",
2086
+ 366: "芒狗",
2087
+ 367: "狗狗疑问",
2088
+ 368: "奥特笑哭",
2089
+ 369: "彩虹",
2090
+ 370: "祝贺",
2091
+ 371: "冒泡",
2092
+ 372: "气呼呼",
2093
+ 373: "忙",
2094
+ 374: "波波流泪",
2095
+ 375: "超级鼓掌",
2096
+ 376: "跺脚",
2097
+ 377: "嗨",
2098
+ 378: "企鹅笑哭",
2099
+ 379: "企鹅流泪",
2100
+ 380: "真棒",
2101
+ 381: "路过",
2102
+ 382: "emo",
2103
+ 383: "企鹅爱心",
2104
+ 384: "晚安",
2105
+ 385: "太气了",
2106
+ 386: "呜呜呜",
2107
+ 387: "太好笑",
2108
+ 388: "太头疼",
2109
+ 389: "太赞了",
2110
+ 390: "太头秃",
2111
+ 391: "太沧桑",
2112
+ 392: "龙年快乐",
2113
+ 393: "新年中龙",
2114
+ 394: "新年大龙",
2115
+ 395: "略略略",
2116
+ 396: "狼狗",
2117
+ 397: "抛媚眼",
2118
+ 398: "超级ok",
2119
+ 399: "tui",
2120
+ 400: "快乐",
2121
+ 401: "超级转圈",
2122
+ 402: "别说话",
2123
+ 403: "出去玩",
2124
+ 404: "闪亮登场",
2125
+ 405: "好运来",
2126
+ 406: "姐是女王",
2127
+ 407: "我听听",
2128
+ 408: "臭美",
2129
+ 409: "送你花花",
2130
+ 410: "么么哒",
2131
+ 411: "一起嗨",
2132
+ 412: "开心",
2133
+ 413: "摇起来",
2134
+ 415: "划龙舟",
2135
+ 416: "中龙舟",
2136
+ 417: "大龙舟",
2137
+ 419: "火车",
2138
+ 420: "中火车",
2139
+ 421: "大火车",
2140
+ 424: "续标识",
2141
+ 425: "求放过",
2142
+ 426: "玩火",
2143
+ 427: "偷感",
2144
+ 428: "收到",
2145
+ 429: "蛇年快乐",
2146
+ 430: "蛇身",
2147
+ 431: "蛇尾"
2148
+ };
2149
+ function attributeValue(attributes, key) {
2150
+ const match = new RegExp(`(?:^|[\\s,])${key}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s>]+))`, "i").exec(attributes);
2151
+ return (match?.[1] ?? match?.[2] ?? match?.[3] ?? "").trim();
2152
+ }
2153
+ __name(attributeValue, "attributeValue");
2154
+ function qqNativeFaceName(id) {
2155
+ const key = String(id ?? "").trim();
2156
+ if (!key) return void 0;
2157
+ const extension = QQ_NATIVE_FACE_NAME_EXTENSIONS[key];
2158
+ if (extension) return extension;
2159
+ const face = qface.get(key);
2160
+ return face?.QDes?.replace(/^\//, "").trim() || void 0;
2161
+ }
2162
+ __name(qqNativeFaceName, "qqNativeFaceName");
2163
+ function describeQQNativeFace(id) {
2164
+ const key = String(id ?? "").trim();
2165
+ if (!key) return "[QQ 原生表情(未提供 ID)]";
2166
+ const name2 = qqNativeFaceName(key);
2167
+ return name2 ? `[QQ 原生表情:${name2}(ID: ${key})]` : `[QQ 原生表情(ID: ${key};名称未收录)]`;
2168
+ }
2169
+ __name(describeQQNativeFace, "describeQQNativeFace");
2170
+ function normalizeQQNativeFaceSegments(content) {
2171
+ return String(content ?? "").replace(/<face\b([^>]*)>(?:<\/face>)?/gi, (_match, attributes) => describeQQNativeFace(attributeValue(attributes, "id"))).replace(/\[CQ:face,([^\]]*)\]/gi, (_match, attributes) => describeQQNativeFace(attributeValue(attributes, "id"))).replace(/<mface\b([^>]*)>(?:<\/mface>)?/gi, (_match, attributes) => {
2172
+ const name2 = attributeValue(attributes, "summary") || attributeValue(attributes, "name");
2173
+ return name2 ? `[QQ 商城表情:${name2}]` : "[QQ 商城表情]";
2174
+ });
2175
+ }
2176
+ __name(normalizeQQNativeFaceSegments, "normalizeQQNativeFaceSegments");
2177
+
2043
2178
  // src/types.ts
2044
2179
  var emptyStorySetting = /* @__PURE__ */ __name(() => ({
2045
2180
  character: { name: "Unnamed character", profile: "" },
@@ -2090,6 +2225,7 @@ var InterludeService = class extends import_koishi.Service {
2090
2225
  embedder;
2091
2226
  stickerDescriber;
2092
2227
  stickerCatalog = [];
2228
+ stickerById = /* @__PURE__ */ new Map();
2093
2229
  stickerScanRunning = false;
2094
2230
  /**
2095
2231
  * 同一故事的用户消息、到期意图和后台压缩必须串行。否则“用户新消息
@@ -2734,12 +2870,13 @@ var InterludeService = class extends import_koishi.Service {
2734
2870
  const senderId = normalizeAccountId(session.userId);
2735
2871
  const senderName = await this.groupSenderName(groupId, senderId, session);
2736
2872
  const quote = describeQuotedMessage(session, story.setting.character.name);
2873
+ const messageContent = normalizeQQNativeFaceSegments(session.content);
2737
2874
  const accepted = await this.serial(story.id, async () => {
2738
2875
  const current = await this.getStory(story.id);
2739
2876
  const entry = await this.appendEntry(current.id, {
2740
2877
  kind: "group-message",
2741
2878
  actor: "user",
2742
- content: session.content,
2879
+ content: messageContent,
2743
2880
  occurredAt: now.toISOString(),
2744
2881
  metadata: {
2745
2882
  groupId,
@@ -2760,7 +2897,7 @@ var InterludeService = class extends import_koishi.Service {
2760
2897
  speaker: formatGroupSpeaker(senderName, senderId),
2761
2898
  ...messageId ? { messageId, messageRef: groupMessageRef(accepted.id) } : {},
2762
2899
  ...quote ? { quote } : {},
2763
- content: session.content,
2900
+ content: messageContent,
2764
2901
  occurredAt: now,
2765
2902
  direction: "user"
2766
2903
  }, mentionedBot, quotedBot);
@@ -2963,6 +3100,7 @@ ${message.content}`).join("\n\n");
2963
3100
  const { decision, succeeded } = await this.tryDecide(snapshot.story, null, "user-message", snapshot.from, snapshot.now, userMessage, [], [], groupContext, [], chatCapabilities, [], stickerCatalog);
2964
3101
  const chatActions = normalizeGroupChatActions(decision, chatCapabilities, groupContext);
2965
3102
  const sticker = this.resolveSticker(decision.localMedia, stickerCatalog);
3103
+ const nativeFace = sticker ? void 0 : this.resolveNativeFace(decision, chatCapabilities);
2966
3104
  const result = await this.serial(story.id, async () => {
2967
3105
  if (this.databaseResetting || !succeeded) return { content: "", messages: [], chatActions: { reactions: [] } };
2968
3106
  const current = await this.getStory(story.id);
@@ -2983,16 +3121,17 @@ ${message.content}`).join("\n\n");
2983
3121
  }
2984
3122
  await this.dbSet("interlude_story", { id: current.id }, { cursorAt: snapshot.now, updatedAt: /* @__PURE__ */ new Date() });
2985
3123
  if (succeeded) await this.scheduleConversationFollowUpsAfterTurn(current.id, snapshot.now, decision.interaction);
2986
- return { content, messages, chatActions, sticker };
3124
+ return { content, messages, chatActions, sticker, nativeFace };
2987
3125
  });
2988
3126
  const completedReactions = result.chatActions.reactions.length && turn.latestSession ? await this.executeGroupReactions(snapshot.story, turn.latestSession, turn.groupId, result.chatActions.reactions) : 0;
2989
- if (result.content || completedReactions || result.sticker) {
3127
+ if (result.content || completedReactions || result.sticker || result.nativeFace) {
2990
3128
  this.groupWillingness.set(key, consumeGroupWillingness(this.groupWillingness.get(key), turn.rule.willingness, Date.now()));
2991
3129
  }
2992
3130
  if (result.content) {
2993
3131
  await this.sendGroupMessage(snapshot.story, turn.channelId, result.content, result.chatActions.replyTo?.messageId);
2994
3132
  }
2995
3133
  if (result.sticker && turn.latestSession) await this.sendSticker(snapshot.story, turn.latestSession, turn.channelId, result.sticker);
3134
+ if (result.nativeFace && turn.latestSession) await this.sendNativeFace(snapshot.story, turn.latestSession, turn.channelId, result.nativeFace);
2996
3135
  this.scheduleCompaction(story.id);
2997
3136
  } catch (error) {
2998
3137
  this.report("warn", story, "user-message", "群聊主叙事失败,保持静默 群=%s 错误=%s", turn.groupId, error);
@@ -3036,8 +3175,16 @@ ${message.content}`).join("\n\n");
3036
3175
  const internal = session.bot?.internal;
3037
3176
  const quoteReply = config.quoteReply === true;
3038
3177
  const reactions = config.messageReactions === true && typeof internal?.setMsgEmojiLike === "function" ? normalizeAllowedReactions(config.allowedReactions) : [];
3039
- if (!quoteReply && !reactions.length) return void 0;
3040
- return { platform: "qq", quoteReply, reactions };
3178
+ const nativeFaces = config.nativeFaces === true ? normalizeAllowedNativeFaces(config.allowedNativeFaces) : [];
3179
+ if (!quoteReply && !reactions.length && !nativeFaces.length) return void 0;
3180
+ return { platform: "qq", quoteReply, reactions, nativeFaces, expressionThreshold: normalizeExpressionThreshold(config.expressionThreshold) };
3181
+ }
3182
+ privateChatCapabilities(session) {
3183
+ const config = this.config.chatActions;
3184
+ if (!config?.enabled || !session || !isOneBotPlatform(session.platform) || !config.platforms?.includes("qq")) return void 0;
3185
+ const nativeFaces = config.nativeFaces === true ? normalizeAllowedNativeFaces(config.allowedNativeFaces) : [];
3186
+ if (!nativeFaces.length) return void 0;
3187
+ return { platform: "qq", quoteReply: false, reactions: [], nativeFaces, expressionThreshold: normalizeExpressionThreshold(config.expressionThreshold) };
3041
3188
  }
3042
3189
  async executeGroupReactions(story, session, groupId, reactions) {
3043
3190
  const internal = session.bot?.internal;
@@ -3064,13 +3211,28 @@ ${message.content}`).join("\n\n");
3064
3211
  return completed;
3065
3212
  }
3066
3213
  resolveSticker(draft, catalog) {
3067
- if (!draft || typeof draft.assetId !== "string" || !catalog.some((item) => item.assetId === draft.assetId)) return void 0;
3068
- return this.stickerCatalog.find((item) => item.assetId === draft.assetId && item.status === "active");
3214
+ if (!draft || typeof draft.assetId !== "string" || typeof draft.willingness !== "number" || !catalog.some((item) => item.assetId === draft.assetId)) return void 0;
3215
+ if (normalizeExpressionThreshold(draft.willingness) < this.expressionThreshold) return void 0;
3216
+ return this.stickerById.get(draft.assetId);
3217
+ }
3218
+ get expressionThreshold() {
3219
+ return normalizeExpressionThreshold(this.config.chatActions?.expressionThreshold);
3220
+ }
3221
+ resolveNativeFace(decision, capabilities) {
3222
+ const allowed = new Set(capabilities?.nativeFaces ?? []);
3223
+ if (!allowed.size) return void 0;
3224
+ const draft = decision.nativeFace;
3225
+ const replyContent = decision.groupReply?.content ?? decision.interaction?.reply?.content ?? "";
3226
+ if (draft && allowed.has(draft.semantic) && calibratedNativeFaceWillingness(draft.semantic, draft.willingness, replyContent) >= (capabilities?.expressionThreshold ?? this.expressionThreshold)) {
3227
+ return draft.semantic;
3228
+ }
3229
+ return void 0;
3069
3230
  }
3070
3231
  async sendSticker(story, session, channelId, asset) {
3071
3232
  const root = (0, import_node_path.resolve)(this.ctx.baseDir, this.stickerConfig.directory);
3072
3233
  const file = (0, import_node_path.resolve)(root, asset.filePath);
3073
- if (!file.startsWith(root)) return;
3234
+ const relativePath = (0, import_node_path.relative)(root, file);
3235
+ if (!relativePath || relativePath === ".." || relativePath.startsWith(`..${import_node_path.sep}`) || relativePath.includes(":")) return;
3074
3236
  try {
3075
3237
  await session.bot.sendMessage(channelId, (0, import_koishi.h)("img", { src: (0, import_node_url.pathToFileURL)(file).href }));
3076
3238
  const now = /* @__PURE__ */ new Date();
@@ -3086,6 +3248,22 @@ ${message.content}`).join("\n\n");
3086
3248
  this.report("warn", story, "user-message", "聊天动作失败 类型=本地表情包 素材=%s 错误=%s", asset.assetId, error);
3087
3249
  }
3088
3250
  }
3251
+ async sendNativeFace(story, session, channelId, semantic) {
3252
+ try {
3253
+ await session.bot.sendMessage(channelId, (0, import_koishi.h)("face", { id: QQ_NATIVE_FACE_IDS[semantic] }));
3254
+ const now = /* @__PURE__ */ new Date();
3255
+ await this.serial(story.id, async () => this.appendEntry(story.id, {
3256
+ kind: "character-platform-action",
3257
+ actor: "character",
3258
+ content: `主角发送了 ${semantic} 原生表情。`,
3259
+ occurredAt: now.toISOString(),
3260
+ metadata: { platform: session.platform, action: "native-face", semantic }
3261
+ }, now));
3262
+ this.reportOperation("standard", "info", story, "user-message", "聊天动作完成 类型=原生表情 语义=%s", semantic);
3263
+ } catch (error) {
3264
+ this.report("warn", story, "user-message", "聊天动作失败 类型=原生表情 语义=%s 错误=%s", semantic, error);
3265
+ }
3266
+ }
3089
3267
  async sendGroupMessage(story, channelId, content, replyToMessageId) {
3090
3268
  const bot = this.ctx.bots.find((item) => String(item.selfId) === String(story.selfId) && (item.platform === story.platform || isOneBotPlatform(item.platform) && isOneBotPlatform(story.platform)));
3091
3269
  if (!bot) {
@@ -3239,6 +3417,7 @@ ${message.content}`).join("\n\n");
3239
3417
  async refreshStickerCatalog() {
3240
3418
  const rows = await this.dbGet("interlude_sticker", { status: "active" }, { sort: { updatedAt: "desc" } });
3241
3419
  this.stickerCatalog = rows;
3420
+ this.stickerById = new Map(rows.map((item) => [item.assetId, item]));
3242
3421
  }
3243
3422
  stickerCatalogForSession(session) {
3244
3423
  const config = this.stickerConfig;
@@ -3282,7 +3461,7 @@ ${message.content}`).join("\n\n");
3282
3461
  describeVisionEvent(session) {
3283
3462
  const raw = String(session.content ?? "");
3284
3463
  const sources = extractSessionImageSources(session);
3285
- const text2 = raw.replace(/<\/?(?:img|image|audio|record)\b[^>]*>/gi, "").replace(/\[CQ:(?:image|record),[^\]]*\]/gi, "").trim();
3464
+ const text2 = normalizeQQNativeFaceSegments(raw).replace(/<\/?(?:img|image|audio|record)\b[^>]*>/gi, "").replace(/\[CQ:(?:image|record),[^\]]*\]/gi, "").trim();
3286
3465
  const content = text2;
3287
3466
  return { content, sources };
3288
3467
  }
@@ -3453,6 +3632,7 @@ ${message.content}`).join("\n\n");
3453
3632
  const userMessage = formatBufferedUserMessages(batch);
3454
3633
  const quotedMessages = batch.flatMap((message, index) => message.quote ? [{ ...message.quote, messageIndex: index + 1 }] : []);
3455
3634
  const stickerCatalog = this.stickerCatalogForSession(turn.latestSession);
3635
+ const chatCapabilities = this.privateChatCapabilities(turn.latestSession);
3456
3636
  const imageSources = Array.from(new Set(batch.flatMap((message) => message.imageSources))).slice(0, 3);
3457
3637
  const images = await this.loadNativeImages(snapshot.story, imageSources, turn.latestSession);
3458
3638
  if (turn.nextRevision !== revision) {
@@ -3471,7 +3651,7 @@ ${message.content}`).join("\n\n");
3471
3651
  superseded,
3472
3652
  void 0,
3473
3653
  images,
3474
- void 0,
3654
+ chatCapabilities,
3475
3655
  quotedMessages,
3476
3656
  stickerCatalog
3477
3657
  );
@@ -3507,6 +3687,8 @@ ${message.content}`).join("\n\n");
3507
3687
  await this.sendOutgoingMessages(snapshot.story, result.messages, snapshot.participant, turn.latestSession);
3508
3688
  const sticker = this.resolveSticker(decision.localMedia, stickerCatalog);
3509
3689
  if (sticker && turn.latestSession) await this.sendSticker(snapshot.story, turn.latestSession, snapshot.participant.channelId, sticker);
3690
+ const nativeFace = sticker ? void 0 : this.resolveNativeFace(decision, chatCapabilities);
3691
+ if (nativeFace && turn.latestSession) await this.sendNativeFace(snapshot.story, turn.latestSession, snapshot.participant.channelId, nativeFace);
3510
3692
  }
3511
3693
  this.scheduleCompaction(turn.storyId);
3512
3694
  } catch (error) {
@@ -3891,7 +4073,7 @@ ${message.content}`).join("\n\n");
3891
4073
  "模型调用完成 任务=主叙事 耗时=%dms 剧本文字=%d 回复模式=%s",
3892
4074
  Date.now() - startedAt,
3893
4075
  result.decision.script?.length ?? 0,
3894
- visibleReplyMode(result.decision, groupContext)
4076
+ visibleReplyMode(result.decision, phase, groupContext)
3895
4077
  );
3896
4078
  return result;
3897
4079
  } catch (error) {
@@ -6190,6 +6372,45 @@ var QQ_REACTION_IDS = {
6190
6372
  sad: "5",
6191
6373
  angry: "106"
6192
6374
  };
6375
+ var NATIVE_FACE_SEMANTICS = ["smile", "laugh", "sweat", "awkward", "heart", "surprised", "sad", "angry"];
6376
+ var QQ_NATIVE_FACE_IDS = {
6377
+ smile: "14",
6378
+ laugh: "182",
6379
+ sweat: "27",
6380
+ awkward: "111",
6381
+ heart: "66",
6382
+ surprised: "0",
6383
+ sad: "5",
6384
+ angry: "106"
6385
+ };
6386
+ function normalizeAllowedNativeFaces(value) {
6387
+ if (!Array.isArray(value)) return [];
6388
+ return Array.from(new Set(value.filter((item) => NATIVE_FACE_SEMANTICS.includes(item)))).slice(0, NATIVE_FACE_SEMANTICS.length);
6389
+ }
6390
+ __name(normalizeAllowedNativeFaces, "normalizeAllowedNativeFaces");
6391
+ function normalizeExpressionThreshold(value) {
6392
+ const number = typeof value === "number" ? value : Number(value);
6393
+ return Number.isFinite(number) ? Math.max(0, Math.min(1, number)) : 0.7;
6394
+ }
6395
+ __name(normalizeExpressionThreshold, "normalizeExpressionThreshold");
6396
+ function calibratedNativeFaceWillingness(semantic, willingness, replyContent) {
6397
+ const text2 = String(replyContent ?? "").replace(/<sep\/>/g, " ").trim();
6398
+ if (!text2) return 0;
6399
+ const patterns = {
6400
+ smile: /(?:微笑|开心|高兴|谢谢|好耶|好呀|可以|行吧|嘿|哈哈)/i,
6401
+ laugh: /(?:哈{2,}|笑死|好笑|乐|绷不住|蚌埠|草|救命)/i,
6402
+ sweat: /(?:流汗|尴尬|无语|服了|麻了|救命|离谱|完了|累|忙|不知道怎么说)/i,
6403
+ awkward: /(?:尴尬|那个|呃|emm|……|\.{3,}|我真的|怎么说呢)/i,
6404
+ heart: /(?:喜欢|爱你|抱抱|可爱|谢谢|好耶|开心|高兴)/i,
6405
+ surprised: /(?:不会吧|真的假的|居然|什么|怎么会|\?{1,}|?{1,}|!{1,}|!{1,})/i,
6406
+ sad: /(?:难过|哭|委屈|可怜|遗憾|心疼|唉)/i,
6407
+ angry: /(?:生气|气死|烦|闭嘴|别[再乱闹说]|离谱|过分|你.*(?:啊|吧|?|!|!))/i
6408
+ };
6409
+ const semanticMatch = patterns[semantic].test(text2);
6410
+ const evidence = semanticMatch ? 0.9 : 0.2;
6411
+ return Math.min(0.9, normalizeExpressionThreshold(willingness) * (0.25 + evidence * 0.75));
6412
+ }
6413
+ __name(calibratedNativeFaceWillingness, "calibratedNativeFaceWillingness");
6193
6414
  function targetableMessageId(value) {
6194
6415
  const id = String(value ?? "").trim();
6195
6416
  return /^-?\d+$/.test(id) && id !== "0" ? id : void 0;
@@ -6240,7 +6461,7 @@ function describeQuotedMessage(session, characterName = "主角") {
6240
6461
  }
6241
6462
  __name(describeQuotedMessage, "describeQuotedMessage");
6242
6463
  function normalizeQuotedMessageContent(value) {
6243
- const raw = String(value ?? "");
6464
+ const raw = normalizeQQNativeFaceSegments(value);
6244
6465
  const content = raw.replace(/<(?:img|image)\b[^>]*\/?>(?:<\/(?:img|image)>)?/gi, "[图片]").replace(/<(?:audio|record)\b[^>]*\/?>(?:<\/(?:audio|record)>)?/gi, "[语音]").replace(/<video\b[^>]*\/?>(?:<\/video>)?/gi, "[视频]").replace(/<(?:face|mface)\b[^>]*\/?>(?:<\/(?:face|mface)>)?/gi, "[表情]").replace(/<at\b[^>]*(?:name|id)=["']?([^\s"'>]+)[^>]*\/?>(?:<\/at>)?/gi, "[@$1]").replace(/\[CQ:image,[^\]]*\]/gi, "[图片]").replace(/\[CQ:record,[^\]]*\]/gi, "[语音]").replace(/\[CQ:video,[^\]]*\]/gi, "[视频]").replace(/\[CQ:face,[^\]]*\]/gi, "[表情]").replace(/<[^>]+>/g, "").replace(/[\r\n]+/g, " ").replace(/\s{2,}/g, " ").trim();
6245
6466
  return clip(content, 1500);
6246
6467
  }
@@ -6305,7 +6526,17 @@ function requiresVisibleReplyRecovery(phase, groupContext, decision) {
6305
6526
  return groupContext ? !hasStructuredGroupReply(decision) : !hasStructuredInteraction(decision.interaction);
6306
6527
  }
6307
6528
  __name(requiresVisibleReplyRecovery, "requiresVisibleReplyRecovery");
6308
- function visibleReplyMode(decision, groupContext) {
6529
+ function visibleReplyMode(decision, phase, groupContext) {
6530
+ if (phase === "advance") {
6531
+ if (decision.crossConversationActions?.some((action) => action.mode === "immediate")) return "主动联系";
6532
+ if (decision.crossConversationActions?.some((action) => action.mode === "delayed")) return "计划联系";
6533
+ return "无可见投递";
6534
+ }
6535
+ if (phase === "conversation-follow-up" || phase === "intent-due") {
6536
+ if (hasStructuredInteraction(decision.interaction)) return decision.interaction.reply.mode;
6537
+ if (decision.crossConversationActions?.some((action) => action.mode === "immediate")) return "主动联系";
6538
+ return "无可见投递";
6539
+ }
6309
6540
  if (!groupContext) return hasStructuredInteraction(decision.interaction) ? decision.interaction.reply.mode : "未提供或无效";
6310
6541
  if (hasStructuredGroupReplyField(decision.groupReply)) return `group:${decision.groupReply.mode}`;
6311
6542
  if (hasStructuredInteraction(decision.interaction)) return `group-fallback:${decision.interaction.reply.mode}`;
@@ -6341,7 +6572,7 @@ function normalizeGroupInteractionReply(raw, maxCharacters) {
6341
6572
  }
6342
6573
  __name(normalizeGroupInteractionReply, "normalizeGroupInteractionReply");
6343
6574
  function normalizeVisibleMessageContent(value, maxCharacters) {
6344
- return String(value ?? "").replace(/[\[【](?:表情包?|图片|动图|GIF)[\]】]/gi, "").trim().slice(0, Math.max(1, maxCharacters));
6575
+ return String(value ?? "").replace(/[\[【](?:表情包?|图片|动图|GIF)[\]】]/gi, "").replace(/[\[【](?:流汗|微笑|笑哭|尴尬|爱心|惊讶|流泪|委屈)[\]】]/gi, "").trim().slice(0, Math.max(1, maxCharacters));
6345
6576
  }
6346
6577
  __name(normalizeVisibleMessageContent, "normalizeVisibleMessageContent");
6347
6578
  function literalQuoteText(value) {
@@ -7395,7 +7626,10 @@ var ChatActions = import_koishi2.Schema.object({
7395
7626
  platforms: import_koishi2.Schema.array(import_koishi2.Schema.union(["qq", "wechat"])).role("table").default(["qq"]).description("允许使用聊天动作的平台,可同时选择。平台还必须有已注册、在线的能力连接器。"),
7396
7627
  quoteReply: import_koishi2.Schema.boolean().default(true).description("允许主角引用当前上下文中的一条消息进行指定回复。"),
7397
7628
  messageReactions: import_koishi2.Schema.boolean().default(true).description("允许主角给当前上下文中的消息贴一个表情回应;当前内置实现为 QQ 群聊。"),
7398
- allowedReactions: import_koishi2.Schema.array(import_koishi2.Schema.union(["like", "smile", "laugh", "heart", "surprised", "sad", "angry"])).role("table").default(["like", "smile", "laugh", "heart"]).description("主模型可以选择的语义表情;平台连接器负责转换为实际表情编号。")
7629
+ allowedReactions: import_koishi2.Schema.array(import_koishi2.Schema.union(["like", "smile", "laugh", "heart", "surprised", "sad", "angry"])).role("table").default(["like", "smile", "laugh", "heart"]).description("主模型可以选择的语义表情;平台连接器负责转换为实际表情编号。"),
7630
+ nativeFaces: import_koishi2.Schema.boolean().default(true).description("允许主角在需要时发送 QQ 原生小表情。"),
7631
+ expressionThreshold: import_koishi2.Schema.number().min(0).max(1).step(0.05).default(0.7).description("表情与本地表情包的最低表达意愿;低于此值只发送文字。"),
7632
+ allowedNativeFaces: import_koishi2.Schema.array(import_koishi2.Schema.union(["smile", "laugh", "sweat", "awkward", "heart", "surprised", "sad", "angry"])).role("table").default(["smile", "laugh", "sweat", "awkward"]).description("允许主模型使用的 QQ 原生表情语义。")
7399
7633
  });
7400
7634
  var Stickers = import_koishi2.Schema.object({
7401
7635
  enabled: import_koishi2.Schema.boolean().default(false).description("启用本地表情包库。关闭时不扫描目录、不调用视觉模型,也不向主模型提供素材。"),
@@ -7416,7 +7650,7 @@ var Config = import_koishi2.Schema.object({
7416
7650
  storyDefaults: StoryDefaults.description("1. 剧本起点:主角、世界、默认关系、地点、时区和叙事风格。"),
7417
7651
  model: Model.description("2. 模型:服务商、模型预设、主叙事、压缩、Embedding 与视觉输入。"),
7418
7652
  onebot: OneBot.description("3. OneBot/NapCat:机器人账号、私聊白名单和群聊白名单。"),
7419
- chatActions: ChatActions.default({ enabled: false, platforms: ["qq"], quoteReply: true, messageReactions: true, allowedReactions: ["like", "smile", "laugh", "heart"] }).description("4. 聊天动作:按平台启用指定回复与消息表情;只有已注册能力才进入提示词。"),
7653
+ chatActions: ChatActions.default({ enabled: false, platforms: ["qq"], quoteReply: true, messageReactions: true, allowedReactions: ["like", "smile", "laugh", "heart"], nativeFaces: true, expressionThreshold: 0.7, allowedNativeFaces: ["smile", "laugh", "sweat", "awkward"] }).description("4. 聊天动作:按平台启用指定回复与消息表情;只有已注册能力才进入提示词。"),
7420
7654
  stickers: Stickers.default({ enabled: false, directory: "data/hds-interlude/stickers", maxFileSizeMB: 10, catalogLimit: 40 }).description("5. 本地表情包:每五分钟扫描新增素材,并由勾选 useForStickers 的视觉模型生成描述。"),
7421
7655
  sharedStory: SharedStory.description("6. 共享剧本:参与者加入、跨账号行为和管理员权限。"),
7422
7656
  runtime: Runtime.description("6. 对话与时间:消息合并、回复投递、失败重试和自动生活推进。"),
@@ -7764,6 +7998,7 @@ __name(looksLikeInterludeCommand, "looksLikeInterludeCommand");
7764
7998
  ZHIPU_FIRST_VISIBLE_TOKEN_TIMEOUT,
7765
7999
  ZHIPU_OFFICIAL_CHAT_ENDPOINT,
7766
8000
  apply,
8001
+ calibratedNativeFaceWillingness,
7767
8002
  configuredProviders,
7768
8003
  createCompactor,
7769
8004
  createEmbedder,
@@ -7787,6 +8022,7 @@ __name(looksLikeInterludeCommand, "looksLikeInterludeCommand");
7787
8022
  normalizeGroupVisibleReply,
7788
8023
  normalizeQuotedMessageContent,
7789
8024
  normalizeScenePresenceDrafts,
8025
+ promptVisibleMessageContent,
7790
8026
  recentScriptOwnership,
7791
8027
  resolveBlackBoxConfig,
7792
8028
  resolveBlindModeConfig,
@@ -7796,5 +8032,6 @@ __name(looksLikeInterludeCommand, "looksLikeInterludeCommand");
7796
8032
  systemPrompt,
7797
8033
  toPromptPayload,
7798
8034
  usesRemoteProviders,
7799
- version
8035
+ version,
8036
+ visibleReplyMode
7800
8037
  });
package/lib/meta.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const HDS_INTERLUDE_VERSION = "0.1.4-beta1";
1
+ export declare const HDS_INTERLUDE_VERSION = "0.1.4-beta3";
package/lib/narrator.d.ts CHANGED
@@ -230,7 +230,7 @@ export declare function toPromptPayload(request: NarrativeRequest): {
230
230
  }[];
231
231
  durableFacts: {
232
232
  participantId: string;
233
- scope: "promise" | "character" | "world" | "relationship" | "event";
233
+ scope: "character" | "world" | "relationship" | "event" | "promise";
234
234
  content: string;
235
235
  importance: number;
236
236
  confidence: number;
@@ -378,7 +378,7 @@ export declare function toPromptPayload(request: NarrativeRequest): {
378
378
  groupContext: {
379
379
  messages: {
380
380
  occurredAt: string;
381
- direction: "character" | "user";
381
+ direction: "user" | "character";
382
382
  quote?: import("./types").QuotedMessageContext;
383
383
  senderId: string;
384
384
  senderName: string;
@@ -393,3 +393,4 @@ export declare function toPromptPayload(request: NarrativeRequest): {
393
393
  characterRole: string;
394
394
  };
395
395
  };
396
+ export declare function promptVisibleMessageContent(content: string, ownership: RecentScriptOwnership): string;
@@ -0,0 +1,4 @@
1
+ export declare function qqNativeFaceName(id: unknown): string;
2
+ export declare function describeQQNativeFace(id: unknown): string;
3
+ /** Convert adapter markup into stable narrator-visible meaning without asking a model to infer an icon. */
4
+ export declare function normalizeQQNativeFaceSegments(content: unknown): string;
package/lib/service.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Context, Service, Session } from 'koishi';
2
2
  import { ModelConfig } from './narrator';
3
3
  import { GroupWillingnessConfig } from './group-willingness';
4
- import { InterludeArc, InterludeScene, InterludeParticipant, InterludeStory, NarrativeDecision, NarrativeFact, NarrativeIntent, GroupContext, NarrativeProvider, NarrativeCompactor, NarrativeEmbedder, OutgoingMessageDraft, ScriptEntry, StatePatchProposal, StorySetting, StoryState, OverlaySnapshot, AlterSystemConfig, AgencyConfig, ScenePresenceState, ChatActionCapabilities, ChatReactionName, MessageReactionDraft, QuotedMessageContext } from './types';
4
+ import { InterludeArc, InterludeScene, InterludeParticipant, InterludeStory, NarrativeDecision, NarrativeFact, NarrativeIntent, GroupContext, NarrativeProvider, NarrativeRequest, NarrativeCompactor, NarrativeEmbedder, OutgoingMessageDraft, ScriptEntry, StatePatchProposal, StorySetting, StoryState, OverlaySnapshot, AlterSystemConfig, AgencyConfig, ScenePresenceState, ChatActionCapabilities, ChatReactionName, MessageReactionDraft, NativeFaceSemantic, QuotedMessageContext } from './types';
5
5
  export interface Config {
6
6
  /** Immersive operation that suppresses HDSI visibility and Koishi commands. */
7
7
  blindMode?: BlindModeConfig;
@@ -64,6 +64,9 @@ export interface ChatActionsConfig {
64
64
  quoteReply: boolean;
65
65
  messageReactions: boolean;
66
66
  allowedReactions: ChatReactionName[];
67
+ nativeFaces: boolean;
68
+ expressionThreshold: number;
69
+ allowedNativeFaces: NativeFaceSemantic[];
67
70
  }
68
71
  export interface StickerLibraryConfig {
69
72
  enabled: boolean;
@@ -289,6 +292,7 @@ export declare class InterludeService extends Service {
289
292
  private embedder;
290
293
  private stickerDescriber;
291
294
  private stickerCatalog;
295
+ private stickerById;
292
296
  private stickerScanRunning;
293
297
  /**
294
298
  * 同一故事的用户消息、到期意图和后台压缩必须串行。否则“用户新消息
@@ -443,9 +447,13 @@ export declare class InterludeService extends Service {
443
447
  private groupMessages;
444
448
  private groupCooldownActive;
445
449
  private groupChatCapabilities;
450
+ private privateChatCapabilities;
446
451
  private executeGroupReactions;
447
452
  private resolveSticker;
453
+ private get expressionThreshold();
454
+ private resolveNativeFace;
448
455
  private sendSticker;
456
+ private sendNativeFace;
449
457
  private sendGroupMessage;
450
458
  /**
451
459
  * Persisted messages wait here briefly before they reach the narrator. This
@@ -679,12 +687,20 @@ export declare function extractSessionVoiceCount(session: Pick<Session, 'content
679
687
  /** Voice and typed text share one user event. The explicit marker lets the
680
688
  * narrator distinguish recognized speech from ordinary typed text. */
681
689
  export declare function mergeUserMessageWithVoiceTranscripts(text: string, transcripts: string[], detected?: number): string;
690
+ /**
691
+ * A model's willingness is an intent estimate, not a transport permission.
692
+ * Native faces need a visible-text counterpart so a model cannot turn every
693
+ * routine reply into a face merely by returning willingness=1. The 0.90 cap
694
+ * deliberately makes thresholds above 0.90 an effective near-disable mode.
695
+ */
696
+ export declare function calibratedNativeFaceWillingness(semantic: NativeFaceSemantic, willingness: unknown, replyContent: unknown): number;
682
697
  export declare function describeQuotedMessage(session: Session, characterName?: string): QuotedMessageContext | undefined;
683
698
  export declare function normalizeQuotedMessageContent(value: unknown): string;
684
699
  export declare function normalizeAllowedReactions(value: unknown): ChatReactionName[];
685
700
  export declare function normalizeGroupChatActions(decision: NarrativeDecision, capabilities: ChatActionCapabilities | undefined, context: GroupContext): ExecutableGroupChatActions;
686
701
  export declare function formatGroupSpeaker(senderName: string, senderId: string): string;
687
702
  export declare function normalizeGroupVisibleReply(raw: NarrativeDecision['groupReply'], interaction: NarrativeDecision['interaction'], maxCharacters: number): string;
703
+ export declare function visibleReplyMode(decision: NarrativeDecision, phase: NarrativeRequest['phase'], groupContext?: GroupContext): string;
688
704
  export declare function hasRequiredNarrativeScript(value: NarrativeDecision | undefined | null): boolean;
689
705
  export declare function resolveBlindModeConfig(value?: Partial<BlindModeConfig>): BlindModeConfig;
690
706
  /** @deprecated Renamed to resolveBlindModeConfig. */
package/lib/types.d.ts CHANGED
@@ -371,12 +371,15 @@ export interface NarrativeInteraction {
371
371
  };
372
372
  }
373
373
  export type ChatReactionName = 'like' | 'smile' | 'laugh' | 'heart' | 'surprised' | 'sad' | 'angry';
374
+ export type NativeFaceSemantic = 'smile' | 'laugh' | 'sweat' | 'awkward' | 'heart' | 'surprised' | 'sad' | 'angry';
374
375
  /** Capabilities are transient and appear in a prompt only after both Console
375
376
  * configuration and a live platform connector allow them. */
376
377
  export interface ChatActionCapabilities {
377
378
  platform: 'qq' | 'wechat';
378
379
  quoteReply: boolean;
379
380
  reactions: ChatReactionName[];
381
+ nativeFaces?: NativeFaceSemantic[];
382
+ expressionThreshold?: number;
380
383
  }
381
384
  export interface MessageReactionDraft {
382
385
  messageRef: string;
@@ -392,6 +395,11 @@ export interface StickerCatalogEntry {
392
395
  export interface LocalMediaDraft {
393
396
  assetId: string;
394
397
  placement?: 'standalone' | 'after-text';
398
+ willingness?: number;
399
+ }
400
+ export interface NativeFaceDraft {
401
+ semantic: NativeFaceSemantic;
402
+ willingness: number;
395
403
  }
396
404
  export interface StickerAsset {
397
405
  id: number;
@@ -471,6 +479,8 @@ export interface NarrativeDecision {
471
479
  messageReactions?: MessageReactionDraft[];
472
480
  /** Exact local sticker selected from the transient catalog for this live turn. */
473
481
  localMedia?: LocalMediaDraft;
482
+ /** One optional semantic QQ native-face expression for the current live reply. */
483
+ nativeFace?: NativeFaceDraft;
474
484
  }
475
485
  export type NarrativePhase = 'advance' | 'conversation-follow-up' | 'user-message' | 'intent-due';
476
486
  /** A transient native-vision attachment for the current private-message turn.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-hds-interlude",
3
- "version": "0.1.4-beta1",
3
+ "version": "0.1.4-beta3",
4
4
  "description": "A time-aware, persistent narrative chat framework for Koishi.",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -14,7 +14,8 @@
14
14
  "koishi": {"service": {"optional": ["puppeteer"]}},
15
15
  "peerDependencies": {"koishi": "^4.18.7", "koishi-plugin-puppeteer": "^3.9.0"},
16
16
  "peerDependenciesMeta": {"koishi-plugin-puppeteer": {"optional": true}},
17
- "devDependencies": {"koishi-plugin-puppeteer": "^3.9.0"}
17
+ "devDependencies": {"koishi-plugin-puppeteer": "^3.9.0"},
18
+ "dependencies": {"qface": "^1.4.1"}
18
19
  }
19
20
 
20
21