@fengye_nobug/dsh-qq-bot 0.2.8 → 0.2.10

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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # 更新日志
2
2
 
3
+ ## v0.2.10 — 2026-11-02
4
+
5
+ - **用法说明和发消息工具不再用具体表情名当例子**:注入的用法说明,以及 `qq_bridge_send` 的工具说明和参数说明,不再把笑哭、挥手写成推荐写法。模型之前会抓住这两个例子反复使用。现在只说明半角方括号里写 `[表情名]` 或 `[表情数字]`。对得上的表情名仍会变成 QQ 系统表情,名字表还在 `faces.js`。认不出的方括号仍留在文字里。
6
+
7
+ ## v0.2.9 — 2026-11-02
8
+
9
+ - **每个会话的轮数上限会真正压缩**:`maxRecentTurns`(默认 40,填 0 不限制)在 QQ 叫醒的一轮结束后,会请宿主把更早的内容压掉。以前这次调用少传了宿主要求的取消信号,压缩还没开始就失败,失败只留在日志里,会话仍会一直变长。现在会等这一轮结束并空闲下来,再按宿主的参数去压缩。宿主没有压缩能力时仍然跳过,不影响收发。
10
+
3
11
  ## 0.2.8
4
12
 
5
13
  - 设置卡片:群白名单在连上 NapCat 时可显示群名和头像;查不到时仍只显示群号
package/DESIGN.md CHANGED
@@ -449,7 +449,7 @@ QQ 路径与 `NapCatWinBootHook.dll`;此时 QQ 路径取不到会明确报错
449
449
  - **字数上限**:出站文本超过 `replyMaxChars` 时**一条都不发**,调用方拿到 `reason: 'too-long'`,
450
450
  应按提示缩短后重发;未超长时 `replyWithQuote: true` 让消息带上引用;
451
451
  `stripMarkdown: true`(默认)时先去掉 Markdown 标记(QQ 不渲染)。字数按去掉 Markdown 之后、拆表情之前的正文计算。
452
- - **系统表情**:去掉 Markdown 之后,`sendToQQ` 把正文拆成 OneBot 消息段。`[笑哭]`、`[挥手]` 这类名字变成 `face` 段(笑哭是 182,挥手是 129),NapCat 显示系统表情。也认入站占位符 `[表情182]`,以及 `[CQ:face,id=182]`。对不上的方括号(例如 `[没这表情]`、`[图片]`)留在文字段里。标记块和 `qq_bridge_send` 都走这条路。名字表在 `faces.js`,常见的还有微笑、捂脸、吃瓜、比心。商城表情不走这条路,由 `qq_bridge_sticker` 按登记表发送,见[表情包](#表情包)。
452
+ - **系统表情**:去掉 Markdown 之后,`sendToQQ` 把正文拆成 OneBot 消息段。半角方括号里的 `[表情名]` 变成 `face` 段,编号取自 `faces.js` 的名字表,NapCat 显示系统表情。也认入站占位符 `[表情数字]`,以及 `[CQ:face,id=数字]`。对不上的方括号(例如 `[没这表情]`、`[图片]`)留在文字段里。标记块和 `qq_bridge_send` 都走这条路。商城表情不走这条路,由 `qq_bridge_sticker` 按登记表发送,见[表情包](#表情包)。
453
453
  - **目的地钉在唤醒那一刻**。这一轮的标记块、`qq_bridge_send`、`qq_bridge_sticker` 和 `qq_bridge_listen` 都发回把这一轮叫醒的那个群或私聊,不会被同一时间另一条只记录下来的消息改掉。显式传入 `groupId` 或 `userId` 时发到那里,并且不引用别的会话里的原消息;这个群号必须在 `groupWhitelist` 里,这个 QQ 号必须在 `whitelist` 里,对不上就一条都不发(`reason: "not-allowlisted"`)。白名单留空等于不允许。钉住的目的地不再查一遍,它在入站时已经通过了白名单。同一会话后面没有新唤醒的轮次,仍回落到最后一次非丢弃的入站。
454
454
  - **连不上或还没有目的地时丢弃并记日志**,不静默排队堆积。
455
455
  - **标记块发出时传输失败**(`reason: 'send-failed'`)同样记进丢弃台账,并把原因回注到这一轮的会话里。模型能看到没发出去,而不是只在日志里留一行。
@@ -461,7 +461,7 @@ QQ 路径与 `NapCatWinBootHook.dll`;此时 QQ 路径取不到会明确报错
461
461
 
462
462
  QQ 的表情、表情包、图片、语音、视频、文件、卡片、合并转发等都会换成可读占位符
463
463
  (如 `[表情4]`、`[表情包:[动画表情]]`、`[图片]`、`[文件:a.zip]`),**不会让整条消息丢失**。
464
- 系统表情的入站占位符带编号,例如 `[表情182]`。出站时模型写的 `[笑哭]`,或这种占位符,会再变回系统表情,见[出站](#出站三种模式--来源闸门)。
464
+ 系统表情的入站占位符带编号,形如 `[表情数字]`。出站时写的 `[表情名]`,或这种占位符,会再变回系统表情,见[出站](#出站三种模式--来源闸门)。
465
465
  商城表情若带了可下载的图,描述填回去之后写成 `[表情包#短哈希]`,和普通图片的 `[图片#短哈希]` 分开,见[表情包](#表情包)。
466
466
 
467
467
  ### 戳一戳
@@ -603,7 +603,7 @@ QQ 的“拍一拍”在 OneBot 里走的是 **notice 事件**,不是消息段
603
603
  |---|---|---|
604
604
  | `targetSessionId` | `''` | 接到哪个会话。`single` 时**空 = 不接到任何会话**。`perGroup` 时可以留空(新建会话用宿主当前默认模型);填了则新建会话沿用该会话的模型 |
605
605
  | `sessionScope` | `single` | 会话怎么分。`single` = 所有群共用一个会话;`perGroup` = 每个群、每个私聊各记各的,映射写在状态文件的 `sessionByDest`(键为 `group:<群号>` / `private:<QQ号>`)。新建会话带工作目录和「QQ群 / QQ私聊」标题,见 [README 的功能](./README.md#功能) |
606
- | `maxRecentTurns` | `40` | 每个会话最多留几轮。超出后调用宿主 `compaction.compactNow` 尽量压缩。有投影时看投影轮数。压完没变短就停。`0` = 不限制 |
606
+ | `maxRecentTurns` | `40` | 每个会话最多留几轮。超出后,等这一轮回到空闲,再调用宿主 `compaction.compactNow(agent, signal)` 尽量压缩。`signal` 是未取消的 AbortSignal,不传命令 id。有投影时看投影轮数。压完没变短就停。`0` = 不限制。compaction 不在 inject 里,宿主没有这项服务时跳过 |
607
607
  | `nicknames` | `['244','小助手']` | 听到这些名字就回应 |
608
608
  | `wakeProbability` | `0.05` | 没点名时的回应概率 |
609
609
  | `whitelist` / `groupWhitelist` | `[]` | 哪些私聊可以叫醒 / 哪些群可以叫醒(**留空 = 谁都不能叫醒**)。群白名单在卡片里按群号填;连上且查到群名时,群号旁边显示当前群名和头像,查不到或没连上只显示群号,不影响保存 |
@@ -654,7 +654,7 @@ QQ 的“拍一拍”在 OneBot 里走的是 **notice 事件**,不是消息段
654
654
  | 入站格式 | 入站消息格式、未唤醒摘要、“[图片#<短哈希>]:<描述>”(就地填回,可能不准)、被戳显示成“[戳一戳](戳的是我)” |
655
655
  | 回复方式 | 按 `replyMode` 分叉:标记块怎么写、该调用哪个工具、非 QQ 轮的 2 步确认 |
656
656
  | 表情包 | 用 `qq_bridge_sticker` 的 `list` / `send` 发表情包;用 `save` 收藏群友发的图。`[表情包#短哈希]` 是商城表情,发出去是原生表情包;`[图片#短哈希]` 发出去仍是图片 |
657
- | 群聊礼仪 | 1–2 句话;默认不超过 40 字;不刷屏;不放路径、代码、日志、提交号;不汇报过程。系统表情写 `[笑哭]`、`[挥手]` 或 `[表情182]` |
657
+ | 群聊礼仪 | 1–2 句话;默认不超过 40 字;不刷屏;不放路径、代码、日志、提交号;不汇报过程。系统表情写 `[表情名]` 或 `[表情数字]` |
658
658
  | 无人值守 | 不碰特权操作,见下一节 |
659
659
  | 会话跟随(监听模式) | 何时开窗;默认 5 分钟、上限 60 分钟;只对同一目的地生效;答完主动关 |
660
660
 
@@ -698,7 +698,7 @@ QQ 侧没有人能点击“同意”,因此:
698
698
  | `qq_bridge_status` | `debugTools` | 设置、目标会话是否已加载、连接状态、目的地、缓冲条数、最近日志,以及**最近观测到的 `user/message` 来源**(`recentInboundSources`)。排查“哑火”先看这个工具:`ours: false` = 没有认出那条是插件灌入的 QQ 消息;`ours: true` 但工具仍被拒 = 标记被别的事件清掉了 |
699
699
  | `qq_bridge_transport` | `debugTools` | 查看连接状态(含 token 是否解析到),或 `action: 'reconnect'` 强制重连 |
700
700
  | `qq_bridge_simulate` | `debugTools` | 模拟一条入站消息走完整策略;可带 `groupName`(群名,用来写 perGroup 会话标题);`dryRun: true` 只看决策,不注入 |
701
- | `qq_bridge_send` | 始终 | 出站。正文里的 `[笑哭]` 会变成系统表情。`replyMode: 'tool'` 时该工具**就是唯一的出站方式**,因此不能藏在调试开关后面 |
701
+ | `qq_bridge_send` | 始终 | 出站。正文里的 `[表情名]` 会变成系统表情。`replyMode: 'tool'` 时该工具**就是唯一的出站方式**,因此不能藏在调试开关后面 |
702
702
  | `qq_bridge_sticker` | 始终 | 表情包:`list` / `save` / `send`。登记项带齐商城表情字段时发 `mface`,否则图片以 base64 出站。与 `qq_bridge_send` 共用来源闸门,见[表情包](#表情包) |
703
703
  | `qq_bridge_listen` | 始终 | 会话跟随窗口的开启与关闭。这是产品的使用方式,不是调试工具 |
704
704
  | `qq_bridge_napcat` | 始终 | NapCat 自助托管(下载 / 配置 / 启停) |
package/README.md CHANGED
@@ -16,7 +16,7 @@ Cordis 插件 id 是 `qq-bridge`。npm 包名是 `@fengye_nobug/dsh-qq-bot`。
16
16
  - 白名单留空时,任何群和私聊都不会叫醒。
17
17
  - 被 @、命中昵称、被戳一定叫醒。其余按 `wakeProbability`(默认 `0.05`)抽样,没抽中只记录。
18
18
  - 发回 QQ 的内容由 `replyMode` 决定。在 DSH 界面里聊同一个会话时,正文不会进群。
19
- - 图片先落盘,叫醒时写成文字。表情包可以登记后再发。`[笑哭]` 这类写法会变成 QQ 系统表情。
19
+ - 图片先落盘,叫醒时写成文字。表情包可以登记后再发。半角方括号里的 `[表情名]` 会变成 QQ 系统表情。
20
20
  - 设置卡片在 **设置 → 插件 → 插件配置 → QQ 桥接**。可以检查更新;有新正式版时一键安装,并重启面板。
21
21
  - 可以按需下载、配置、启停 NapCat。加载插件时不会自己下载或执行。
22
22
 
@@ -62,6 +62,7 @@ DSH 0.2 的卡片把值写进 `$DSH_HOME/qq-bridge-settings.json`。这份文件
62
62
  |---|---|---|
63
63
  | `targetSessionId` | 空 | `single` 时接到哪个会话。留空 = 不接到任何会话 |
64
64
  | `sessionScope` | `single` | `perGroup` 时每个群、每个私聊各一条会话 |
65
+ | `maxRecentTurns` | `40` | 每个会话最多留几轮。QQ 一轮结束后,更早的内容会尽量压缩掉。`0` = 不限制 |
65
66
  | `nicknames` | `244`、`小助手` | 听到这些名字就叫醒 |
66
67
  | `wakeProbability` | `0.05` | 没点名时的叫醒概率 |
67
68
  | `whitelist` | `[]` | 哪些私聊可以叫醒。留空 = 都不叫醒 |
package/index.js CHANGED
@@ -11,7 +11,7 @@
11
11
  // M1:以上全部 + 出站文本采集 + 调试工具。
12
12
  // M2:OneBot 传输层(见 onebot.js)——WS 入站映射到 handleInbound(),
13
13
  // 出站文本超过 replyMaxChars 就拒绝发送;未超长才发给最后一次入站的目的地。
14
- // 正文里的 [笑哭] 会拆成 face 段,NapCat 显示系统表情。
14
+ // 正文里的 [表情名] 会拆成 face 段,NapCat 显示系统表情。
15
15
  // 传输细节(内置 WebSocket、token 走查询串、退避重连)全部封装在 onebot.js。
16
16
 
17
17
  import { randomUUID } from 'node:crypto'
@@ -1902,12 +1902,30 @@ export function apply(ctx, config) {
1902
1902
  /**
1903
1903
  * QQ 驱动的一轮结束后,把该会话压到 maxRecentTurns。
1904
1904
  * 0 或宿主没有 compaction 时什么都不做。压缩失败只记日志,不挡出站。
1905
+ *
1906
+ * compaction 不放进 inject。它是可选服务:宿主没装压缩时,桥仍要收发 QQ。
1907
+ * `ctx.get` 不要求 inject;没有这项服务时返回 undefined。
1908
+ *
1909
+ * 宿主 `compactNow(agent, signal, sourceCommandId?)` 会立刻 `signal.throwIfAborted()`。
1910
+ * 官方 `/compact` 传的是这次命令的 signal 和 commandId。这里没有命令,传一个未中止的
1911
+ * AbortSignal,不传 sourceCommandId。少传 signal 会在写入 compaction 事件之前抛错。
1912
+ *
1913
+ * `turn/end` 写入日志时,驱动还占着 running。这时调用会在 `runMaintenance` 上失败,
1914
+ * 同样写不出 compaction 事件。先等 `whenIdle`。旧宿主没有这个方法时让出一个宏任务
1915
+ * (微任务会排在驱动回到 idle 之前)。
1905
1916
  */
1906
1917
  async function maybeTrimSession(agent) {
1907
1918
  const max = readSettings().maxRecentTurns
1908
1919
  if (!max || max <= 0 || !agent) return
1909
1920
  const compaction = ctx.get('compaction')
1910
1921
  if (!compaction || typeof compaction.compactNow !== 'function') return
1922
+ try {
1923
+ if (typeof agent.whenIdle === 'function') await agent.whenIdle()
1924
+ else await new Promise((resolve) => setTimeout(resolve, 0))
1925
+ } catch (error) {
1926
+ logger.warn(`compact trim skipped: ${error?.message ?? error}`)
1927
+ return
1928
+ }
1911
1929
  const countTurns = () => {
1912
1930
  // 投影里的轮次是压缩之后还留着的。历史事件里的 turn/end 压不掉,先数它会一直超限。
1913
1931
  try {
@@ -1938,7 +1956,7 @@ export function apply(ctx, config) {
1938
1956
  }
1939
1957
  previous = turns
1940
1958
  try {
1941
- const result = await compaction.compactNow(agent)
1959
+ const result = await compaction.compactNow(agent, new AbortController().signal)
1942
1960
  if (!result) return
1943
1961
  logger.info(`session ${agent.session?.id ?? agent.id} trimmed via compact (turns~${turns} > ${max})`)
1944
1962
  } catch (error) {
@@ -3579,11 +3597,11 @@ export function apply(ctx, config) {
3579
3597
  '"本轮不是 QQ 发起的,确认要发吗",同一轮里再调一次才真的发出去。' +
3580
3598
  'replyMode=tool 时这是唯一的出站方式;marker 模式下它是「直接发这一条」的旁路。' +
3581
3599
  '文本超过出站字数上限时**一条都不发**,返回 `reason: "too-long"` 与 `limit`、`length` —— 请缩短后重发。' +
3582
- '正文里的 [笑哭]、[挥手] 或 [表情182] 会变成 QQ 系统表情;认不出的方括号原样发出。' +
3600
+ '正文里的 [表情名] 或 [表情数字] 会变成 QQ 系统表情;认不出的方括号原样发出。' +
3583
3601
  '显式 groupId / userId 必须在对应白名单里,否则 `reason: "not-allowlisted"`,一条都不发。' +
3584
3602
  '发出去的内容会记进 outbox(via: tool)。',
3585
3603
  parameters: {
3586
- text: { type: 'string', required: true, description: '要发送的文本;超过出站字数上限时不会发出。[笑哭] 这类表情名会变成系统表情。' },
3604
+ text: { type: 'string', required: true, description: '要发送的文本;超过出站字数上限时不会发出。[表情名] 或 [表情数字] 会变成系统表情。' },
3587
3605
  userId: { type: 'string', description: '私聊目标 QQ 号;与 groupId 二选一,都不给则用这一轮钉住的目的地。' },
3588
3606
  groupId: { type: 'string', description: '群号;与 userId 二选一。显式指定时不引用别的群的消息。' },
3589
3607
  },
@@ -4603,7 +4621,7 @@ export function buildUsagePrompt(s = {}) {
4603
4621
  ' QQ 商城表情(带表情 id 的)形如「[表情包#a1b2c3]:<表情名> <描述>」。描述可能不准,别当成绝对事实。',
4604
4622
  ' 方括号里的 6 位短哈希就是编号:交给 `qq_bridge_sticker` 的 save / send。',
4605
4623
  '- 被「戳一戳」会显示成「[戳一戳](戳的是我)」—— 那和被人 @ 一样,是有人在叫你。',
4606
- '- 系统表情显示成「[表情182]」这种带编号的占位符。',
4624
+ '- 系统表情显示成「[表情数字]」这种带编号的占位符。',
4607
4625
  '',
4608
4626
  '【怎么把话说回 QQ】',
4609
4627
  // 出站方式是**开关**(replyMode),提示词必须跟着变 —— 否则会教模型一个在本部署里
@@ -4644,7 +4662,7 @@ export function buildUsagePrompt(s = {}) {
4644
4662
  ' 标签起不到界定作用,只会跟着正文一起发到 QQ。',
4645
4663
  '- 不确定就说不知道,别假装做过某件事。',
4646
4664
  `- 出站会自动去掉 Markdown 记号;超过 ${maxChars} 字的回复不会发出,请缩短后重发。`,
4647
- '- 想发 QQ 自带表情,就写半角方括号里的表情名,例如 [笑哭]、[挥手],或入站那种 [表情182]。',
4665
+ '- 想发 QQ 自带表情,就写半角方括号里的表情名,形如 [表情名],或入站那种 [表情数字]。',
4648
4666
  ' 发出去会变成系统表情。认不出的方括号会留在文字里。',
4649
4667
  '',
4650
4668
  '【无人值守:不要碰需要提权的操作】',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fengye_nobug/dsh-qq-bot",
3
- "version": "0.2.8",
3
+ "version": "0.2.10",
4
4
  "type": "module",
5
5
  "main": "index.js",
6
6
  "description": "把 DSH 会话接到 QQ(OneBot/NapCat)的插件。白名单默认全关;被 @、叫到昵称或被戳一定叫醒,其余消息按可调概率抽样。Fork 自 sj244/dsh-qq-bridge(MIT)。",