@fengye_nobug/dsh-qq-bot 0.2.10 → 0.2.12
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 +8 -0
- package/DESIGN.md +3 -1
- package/README.md +3 -1
- package/agent-presets/dsh-qq-bridge/skills/qq-session-reply-style/references/reply-style.md +8 -0
- package/index.js +71 -11
- package/package.json +3 -1
- package/reply-style.js +60 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# 更新日志
|
|
2
2
|
|
|
3
|
+
## v0.2.12 — 2026-11-02
|
|
4
|
+
|
|
5
|
+
- **QQ 回复风格不再鼓励使用 emoji**:`$DSH_HOME/.agent-presets/dsh-qq-bridge/skills/qq-session-reply-style/references/reply-style.md` 若还写着 “Emoji are welcome when they make the answer friendlier.”,插件启动时删掉这一句。「Avoid Markdown formatting. Use plain text.」以及前后其它行保持原样。不另加「默认不要带表情」或「少用某个表情」的规矩。这份文件不在插件目录里,DSH 升级时也不会改写 `.agent-presets`;它是人设安装时复制进去的。面板更新并重启后才会改到磁盘上的那一份。文件本来就不存在时不会新建,也不用删掉整个人设目录。用法说明和 `qq_bridge_send` 的说明不动。
|
|
6
|
+
|
|
7
|
+
## v0.2.11 — 2026-11-02
|
|
8
|
+
|
|
9
|
+
- **轮数上限会用到人设里的压缩**:`maxRecentTurns`(默认 40,填 0 不限制)在 QQ 叫醒的一轮结束并空闲后,压缩更早的内容。标准人设把压缩放在隔离域里,插件所在的宿主上下文看不见,所以 0.2.9 这次调用直接跳过,会话会一直变长,日志里也没有压缩记录。现在先向这条会话的人设要压缩服务,没有再看会话作用域和宿主。已经超限却找不到,或这次压缩没有改动会话时,记一条警告。仍然等这一轮空闲,并带上未取消的取消信号。不必在宿主配置里再打开压缩。哪一层都没有压缩时不影响收发。
|
|
10
|
+
|
|
3
11
|
## v0.2.10 — 2026-11-02
|
|
4
12
|
|
|
5
13
|
- **用法说明和发消息工具不再用具体表情名当例子**:注入的用法说明,以及 `qq_bridge_send` 的工具说明和参数说明,不再把笑哭、挥手写成推荐写法。模型之前会抓住这两个例子反复使用。现在只说明半角方括号里写 `[表情名]` 或 `[表情数字]`。对得上的表情名仍会变成 QQ 系统表情,名字表还在 `faces.js`。认不出的方括号仍留在文字里。
|
package/DESIGN.md
CHANGED
|
@@ -194,6 +194,8 @@ qq_bridge_napcat
|
|
|
194
194
|
|
|
195
195
|
四条路。装完都要让 dsh-web 重新加载插件:
|
|
196
196
|
|
|
197
|
+
0.2.12 起,插件启动时会看 `$DSH_HOME/.agent-presets/dsh-qq-bridge/skills/qq-session-reply-style/references/reply-style.md`。里面若还有 “Emoji are welcome when they make the answer friendlier.”,只删这一句。这文件不在 npm 包的安装目录里,`dsh plugin add` 不会顺手改它;人设安装时整份复制到 `$DSH_HOME/.agent-presets`,所以要等新版本的插件进程起来才写回。文件不存在就不创建。不要为了这句话删掉整个人设目录,本插件不会把人设的其它文件补回去。
|
|
198
|
+
|
|
197
199
|
1. 设置卡片里先点「检查更新」。有新正式版时出现「更新到 vX.Y.Z」。点下去优先执行 `dsh plugin --profile web add @fengye_nobug/dsh-qq-bot@X.Y.Z`,走这台机器上 pnpm 已经配置的 registry,然后重启面板。装完会把 profile、`$DSH_HOME/cordis.patch.yml`(写了 `qq-bridge` 才改)和插件补丁里 `qq-bridge` 的 `name` 改成磁盘上的包名,`config` 不动。设置还在,不改 NapCat。重启后刷新页面;登录口令可能会变。
|
|
198
200
|
2. 自己执行同一条命令。npm 上已经发出的是 `@fengye_nobug/dsh-qq-bot@0.2.5`。自己装不会自动改 profile 里的旧 `name`。
|
|
199
201
|
3. 指定 GitHub tag:`dsh plugin --profile web add github:yunfengnobug/dsh-qq-bot#vX.Y.Z`
|
|
@@ -603,7 +605,7 @@ QQ 的“拍一拍”在 OneBot 里走的是 **notice 事件**,不是消息段
|
|
|
603
605
|
|---|---|---|
|
|
604
606
|
| `targetSessionId` | `''` | 接到哪个会话。`single` 时**空 = 不接到任何会话**。`perGroup` 时可以留空(新建会话用宿主当前默认模型);填了则新建会话沿用该会话的模型 |
|
|
605
607
|
| `sessionScope` | `single` | 会话怎么分。`single` = 所有群共用一个会话;`perGroup` = 每个群、每个私聊各记各的,映射写在状态文件的 `sessionByDest`(键为 `group:<群号>` / `private:<QQ号>`)。新建会话带工作目录和「QQ群 / QQ私聊」标题,见 [README 的功能](./README.md#功能) |
|
|
606
|
-
| `maxRecentTurns` | `40` |
|
|
608
|
+
| `maxRecentTurns` | `40` | 每个会话最多留几轮。超出后,等这一轮回到空闲,再调用 `compaction.compactNow(agent, signal)` 尽量压缩。压缩服务先从这条会话的人设读取(`agentPresets.serviceFor(agent, 'compaction')`,标准人设把它放在隔离域里),没有再看会话作用域,最后看宿主。`signal` 是未取消的 AbortSignal,不传命令 id。有投影时看投影轮数。压完没变短就停。返回空也停,并记一条警告。`0` = 不限制。compaction 不在 inject 里。已经超限、三层都没有这项服务时记一条警告并跳过,收发不受影响 |
|
|
607
609
|
| `nicknames` | `['244','小助手']` | 听到这些名字就回应 |
|
|
608
610
|
| `wakeProbability` | `0.05` | 没点名时的回应概率 |
|
|
609
611
|
| `whitelist` / `groupWhitelist` | `[]` | 哪些私聊可以叫醒 / 哪些群可以叫醒(**留空 = 谁都不能叫醒**)。群白名单在卡片里按群号填;连上且查到群名时,群号旁边显示当前群名和头像,查不到或没连上只显示群号,不影响保存 |
|
package/README.md
CHANGED
|
@@ -62,7 +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
|
|
65
|
+
| `maxRecentTurns` | `40` | 每个会话最多留几轮。QQ 一轮结束并空闲后,更早的内容会尽量压缩掉。用的是这条会话自己的压缩(标准人设里就有)。人设没有时再看会话作用域和宿主。已经超限却找不到压缩,或这次压缩没有改动会话时,记一条警告。`0` = 不限制 |
|
|
66
66
|
| `nicknames` | `244`、`小助手` | 听到这些名字就叫醒 |
|
|
67
67
|
| `wakeProbability` | `0.05` | 没点名时的叫醒概率 |
|
|
68
68
|
| `whitelist` | `[]` | 哪些私聊可以叫醒。留空 = 都不叫醒 |
|
|
@@ -96,6 +96,8 @@ DSH 0.2 的卡片把值写进 `$DSH_HOME/qq-bridge-settings.json`。这份文件
|
|
|
96
96
|
|
|
97
97
|
设置卡片里先点「检查更新」。有新正式版时点「更新到 vX.Y.Z」。优先从 npm 安装;这一版还没发到 npm 时,改从 GitHub 下载。装完会自己重启 dsh-web。设置还在,不改 NapCat。刷新页面后,登录口令可能会变。
|
|
98
98
|
|
|
99
|
+
0.2.12 起,重启后如果 `$DSH_HOME/.agent-presets/dsh-qq-bridge/skills/qq-session-reply-style/references/reply-style.md` 里还有 “Emoji are welcome when they make the answer friendlier.”,会删掉这一句。「Avoid Markdown formatting. Use plain text.」还在,其它行不动,也不另写禁用表情的规矩。这份文件本来没有时不会新建。不用删掉整个人设目录。已经读进当前会话的旧规则,要等下次再读这份说明,或另开一条会话。
|
|
100
|
+
|
|
99
101
|
「检查更新」和「启动时检查一次」只询问,不会安装。
|
|
100
102
|
|
|
101
103
|
维护者发版:
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Reply Style
|
|
2
|
+
|
|
3
|
+
Use these rules when replying through QQ:
|
|
4
|
+
|
|
5
|
+
1. Put the conclusion first.
|
|
6
|
+
2. Keep the reply concise.
|
|
7
|
+
3. Avoid Markdown formatting. Use plain text.
|
|
8
|
+
4. If you changed files during this conversation, list only the modified file names with a brief note for each.
|
package/index.js
CHANGED
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
upsertSticker,
|
|
43
43
|
} from './stickers.js'
|
|
44
44
|
import { FACE_NAME_TO_ID, textToOneBotSegments } from './faces.js'
|
|
45
|
+
import { syncInstalledReplyStyle } from './reply-style.js'
|
|
45
46
|
import { applyPluginUpdate, checkPluginUpdate } from './updates.js'
|
|
46
47
|
|
|
47
48
|
const require = createRequire(import.meta.url)
|
|
@@ -726,6 +727,14 @@ export function apply(ctx, config) {
|
|
|
726
727
|
const legacySettings = hasLegacySettings(settingsService)
|
|
727
728
|
|
|
728
729
|
const home = process.env.DSH_HOME || join(homedir(), '.dsh')
|
|
730
|
+
// 人设文件在 $DSH_HOME/.agent-presets,不随 node_modules 一起换。
|
|
731
|
+
// 面板升级并重启后,这里只删掉已经复制进去的那句鼓励使用 emoji 的话。
|
|
732
|
+
try {
|
|
733
|
+
const refreshed = syncInstalledReplyStyle(home)
|
|
734
|
+
if (refreshed.changed) logger.info('已去掉 QQ 回复风格里鼓励使用 emoji 的那一句。')
|
|
735
|
+
} catch (error) {
|
|
736
|
+
logger.warn(`更新 QQ 回复风格失败:${error?.message ?? error}`)
|
|
737
|
+
}
|
|
729
738
|
/**
|
|
730
739
|
* 用户设置的落盘位置(0.2.0 专用)。
|
|
731
740
|
* 0.2.0 的宿主设置桥不给第三方插件可写域(见 docs/dsh-0.2.0-migration.md),
|
|
@@ -1899,16 +1908,48 @@ export function apply(ctx, config) {
|
|
|
1899
1908
|
}
|
|
1900
1909
|
}
|
|
1901
1910
|
|
|
1911
|
+
/**
|
|
1912
|
+
* 读一份能调用的 compaction。
|
|
1913
|
+
*
|
|
1914
|
+
* 标准人设把 `dsh-compaction-basic` 放在 `isolate: { compaction: true }` 里。
|
|
1915
|
+
* 这份服务只在人设那一组的 realm 里,宿主 `ctx.get('compaction')` 和 `agent.ctx.get`
|
|
1916
|
+
* 都按自己的 realm 查找,读到的是空。`/compact` 能用,是因为 `dsh-command-compact`
|
|
1917
|
+
* 和压缩后端在同一组里,调用的是这一份 `compactNow`。
|
|
1918
|
+
* 宿主侧对应的读法是 `agentPresets.serviceFor(agent, 'compaction')`。
|
|
1919
|
+
*
|
|
1920
|
+
* 人设注册表没有这个方法,或这份人设没挂压缩时,再看会话作用域,最后看宿主。
|
|
1921
|
+
* 宿主自己提供 compaction 的布局因此仍然可用。某一层抛错就换下一层。
|
|
1922
|
+
* @returns {{ service: any, detail: string }}
|
|
1923
|
+
*/
|
|
1924
|
+
function compactionFor(agent) {
|
|
1925
|
+
let detail = ''
|
|
1926
|
+
const take = (read) => {
|
|
1927
|
+
try {
|
|
1928
|
+
const value = read()
|
|
1929
|
+
if (value && typeof value.compactNow === 'function') return value
|
|
1930
|
+
} catch (error) {
|
|
1931
|
+
if (!detail) detail = error?.message ?? String(error)
|
|
1932
|
+
}
|
|
1933
|
+
return undefined
|
|
1934
|
+
}
|
|
1935
|
+
const service =
|
|
1936
|
+
take(() => ctx.get?.('agentPresets')?.serviceFor?.(agent, 'compaction')) ??
|
|
1937
|
+
take(() => agent?.ctx?.get?.('compaction')) ??
|
|
1938
|
+
take(() => (typeof ctx.get === 'function' ? ctx.get('compaction') : undefined))
|
|
1939
|
+
return { service, detail }
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1902
1942
|
/**
|
|
1903
1943
|
* QQ 驱动的一轮结束后,把该会话压到 maxRecentTurns。
|
|
1904
|
-
* 0
|
|
1944
|
+
* 0 时什么都不做。压缩失败只记日志,不挡出站。
|
|
1905
1945
|
*
|
|
1906
|
-
* compaction 不放进 inject
|
|
1907
|
-
*
|
|
1946
|
+
* compaction 不放进 inject。它是可选服务:哪一层都没有时,桥仍要收发 QQ。
|
|
1947
|
+
* 会话已经超过上限、却找不到 compactNow 时记一条 warn,避免静默不压缩。
|
|
1908
1948
|
*
|
|
1909
|
-
*
|
|
1910
|
-
*
|
|
1911
|
-
*
|
|
1949
|
+
* `compactNow(agent, signal, sourceCommandId?)` 会立刻 `signal.throwIfAborted()`。
|
|
1950
|
+
* 这里没有命令,传一个未中止的 AbortSignal,不传 sourceCommandId。
|
|
1951
|
+
* 少传 signal 会在写入 compaction 事件之前抛错。
|
|
1952
|
+
* 返回 null 表示没有可压的区间:记 warn 并停,不再空转。
|
|
1912
1953
|
*
|
|
1913
1954
|
* `turn/end` 写入日志时,驱动还占着 running。这时调用会在 `runMaintenance` 上失败,
|
|
1914
1955
|
* 同样写不出 compaction 事件。先等 `whenIdle`。旧宿主没有这个方法时让出一个宏任务
|
|
@@ -1917,8 +1958,6 @@ export function apply(ctx, config) {
|
|
|
1917
1958
|
async function maybeTrimSession(agent) {
|
|
1918
1959
|
const max = readSettings().maxRecentTurns
|
|
1919
1960
|
if (!max || max <= 0 || !agent) return
|
|
1920
|
-
const compaction = ctx.get('compaction')
|
|
1921
|
-
if (!compaction || typeof compaction.compactNow !== 'function') return
|
|
1922
1961
|
try {
|
|
1923
1962
|
if (typeof agent.whenIdle === 'function') await agent.whenIdle()
|
|
1924
1963
|
else await new Promise((resolve) => setTimeout(resolve, 0))
|
|
@@ -1946,19 +1985,40 @@ export function apply(ctx, config) {
|
|
|
1946
1985
|
return 0
|
|
1947
1986
|
}
|
|
1948
1987
|
}
|
|
1988
|
+
const sessionId = () => agent.session?.id ?? agent.id
|
|
1989
|
+
let compaction
|
|
1990
|
+
let lookupDetail = ''
|
|
1991
|
+
let resolved = false
|
|
1992
|
+
const engine = () => {
|
|
1993
|
+
if (!resolved) {
|
|
1994
|
+
const found = compactionFor(agent)
|
|
1995
|
+
compaction = found.service
|
|
1996
|
+
lookupDetail = found.detail
|
|
1997
|
+
resolved = true
|
|
1998
|
+
}
|
|
1999
|
+
return compaction
|
|
2000
|
+
}
|
|
1949
2001
|
let previous = Infinity
|
|
1950
2002
|
for (let i = 0; i < 6; i++) {
|
|
1951
2003
|
const turns = countTurns()
|
|
1952
2004
|
if (!turns || turns <= max) return
|
|
1953
2005
|
if (turns >= previous) {
|
|
1954
|
-
logger.info(`session ${
|
|
2006
|
+
logger.info(`session ${sessionId()} compact 后轮次没有减少(${turns}),停止`)
|
|
2007
|
+
return
|
|
2008
|
+
}
|
|
2009
|
+
if (!engine()) {
|
|
2010
|
+
const why = lookupDetail ? `(${lookupDetail})` : ''
|
|
2011
|
+
logger.warn(`compact trim: 会话 ${sessionId()} 约 ${turns} 轮,超过上限 ${max},但没有可用的压缩服务${why}`)
|
|
1955
2012
|
return
|
|
1956
2013
|
}
|
|
1957
2014
|
previous = turns
|
|
1958
2015
|
try {
|
|
1959
2016
|
const result = await compaction.compactNow(agent, new AbortController().signal)
|
|
1960
|
-
if (
|
|
1961
|
-
|
|
2017
|
+
if (result == null) {
|
|
2018
|
+
logger.warn(`compact trim: 会话 ${sessionId()} 的 compactNow 返回空(当时约 ${turns} 轮,上限 ${max})`)
|
|
2019
|
+
return
|
|
2020
|
+
}
|
|
2021
|
+
logger.info(`session ${sessionId()} trimmed via compact (turns~${turns} > ${max})`)
|
|
1962
2022
|
} catch (error) {
|
|
1963
2023
|
logger.warn(`compact trim failed: ${error?.message ?? error}`)
|
|
1964
2024
|
return
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fengye_nobug/dsh-qq-bot",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.12",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"description": "把 DSH 会话接到 QQ(OneBot/NapCat)的插件。白名单默认全关;被 @、叫到昵称或被戳一定叫醒,其余消息按可调概率抽样。Fork 自 sj244/dsh-qq-bridge(MIT)。",
|
|
@@ -57,6 +57,8 @@
|
|
|
57
57
|
"napcat.js",
|
|
58
58
|
"stickers.js",
|
|
59
59
|
"faces.js",
|
|
60
|
+
"reply-style.js",
|
|
61
|
+
"agent-presets",
|
|
60
62
|
"client",
|
|
61
63
|
"assets",
|
|
62
64
|
"screenshots.json",
|
package/reply-style.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// 已装上的 QQ 人设不在插件目录里。
|
|
2
|
+
// DSH 升级插件时只换 node_modules,不会改写 $DSH_HOME/.agent-presets。
|
|
3
|
+
// reply-style.md 是人设安装时复制进去的。插件每次启动时,如果那份文件还在鼓励使用 emoji,
|
|
4
|
+
// 就只删这一句。其它行保持原样。人设目录或这份文件不存在时,不新建。
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
7
|
+
import { isAbsolute, join } from 'node:path'
|
|
8
|
+
import { fileURLToPath } from 'node:url'
|
|
9
|
+
|
|
10
|
+
/** 磁盘上的相对路径,挂在 $DSH_HOME 下面。 */
|
|
11
|
+
export const INSTALLED_REPLY_STYLE_SEGMENTS = [
|
|
12
|
+
'.agent-presets',
|
|
13
|
+
'dsh-qq-bridge',
|
|
14
|
+
'skills',
|
|
15
|
+
'qq-session-reply-style',
|
|
16
|
+
'references',
|
|
17
|
+
'reply-style.md',
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
/** 要删掉的那一句。前后其它文字不动。 */
|
|
21
|
+
export const EMOJI_WELCOME_SENTENCE = 'Emoji are welcome when they make the answer friendlier.'
|
|
22
|
+
|
|
23
|
+
export function packagedReplyStylePath() {
|
|
24
|
+
return fileURLToPath(
|
|
25
|
+
new URL('./agent-presets/dsh-qq-bridge/skills/qq-session-reply-style/references/reply-style.md', import.meta.url),
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function installedReplyStylePath(home) {
|
|
30
|
+
return join(home, ...INSTALLED_REPLY_STYLE_SEGMENTS)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 去掉鼓励使用 emoji 的那一句,以及它前面紧挨着的空格或制表符。
|
|
35
|
+
* 不改其它字符,也不另加规矩。
|
|
36
|
+
*/
|
|
37
|
+
export function stripEmojiEncouragement(text) {
|
|
38
|
+
return String(text ?? '').replaceAll(` ${EMOJI_WELCOME_SENTENCE}`, '').replaceAll(EMOJI_WELCOME_SENTENCE, '')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 启动时刷新已经复制到 $DSH_HOME 的回复风格。
|
|
43
|
+
* 文件不在就不创建;没有那一句就原样留下。
|
|
44
|
+
* @returns {{ changed: boolean, path: string, reason: 'bad-home' | 'missing' | 'unchanged' | 'stripped' }}
|
|
45
|
+
*/
|
|
46
|
+
export function syncInstalledReplyStyle(home) {
|
|
47
|
+
if (typeof home !== 'string' || !isAbsolute(home)) {
|
|
48
|
+
return { changed: false, path: '', reason: 'bad-home' }
|
|
49
|
+
}
|
|
50
|
+
const target = installedReplyStylePath(home)
|
|
51
|
+
if (!existsSync(target)) return { changed: false, path: target, reason: 'missing' }
|
|
52
|
+
const current = readFileSync(target, 'utf8')
|
|
53
|
+
if (!current.includes(EMOJI_WELCOME_SENTENCE)) {
|
|
54
|
+
return { changed: false, path: target, reason: 'unchanged' }
|
|
55
|
+
}
|
|
56
|
+
const next = stripEmojiEncouragement(current)
|
|
57
|
+
if (next === current) return { changed: false, path: target, reason: 'unchanged' }
|
|
58
|
+
writeFileSync(target, next)
|
|
59
|
+
return { changed: true, path: target, reason: 'stripped' }
|
|
60
|
+
}
|