dsh-qqchat 0.2.6 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +4 -4
- package/README.md +4 -4
- package/client-src/settings.cts +6 -19
- package/client-src/shared.cts +2 -2
- package/cordis.patch.yml +2 -3
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/STREAMING.md +6 -16
- package/lib/client.js +5 -12
- package/lib/config.js +2 -5
- package/lib/gateway/api.d.ts +2 -3
- package/lib/gateway/api.js +3 -10
- package/lib/session/agent-bridge.d.ts +0 -1
- package/lib/session/agent-bridge.js +0 -56
- package/lib/session/runtime.d.ts +2 -0
- package/lib/session/runtime.js +12 -15
- package/lib/storage/db.js +12 -3
- package/lib/transport/rpc.js +2 -4
- package/lib/types.d.ts +1 -3
- package/package.json +3 -2
- package/src/config.ts +2 -5
- package/src/gateway/api.ts +4 -13
- package/src/session/agent-bridge.ts +0 -56
- package/src/session/runtime.ts +12 -12
- package/src/storage/db.ts +15 -3
- package/src/transport/rpc.ts +2 -3
- package/src/types.ts +1 -3
package/README.en.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<p>
|
|
8
8
|
<a href="./LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="MIT License"></a>
|
|
9
9
|
<img src="https://img.shields.io/badge/DSH-0.1.6--alpha.1-6f42c1.svg" alt="DSH 0.1.6-alpha.1">
|
|
10
|
-
<img src="https://img.shields.io/badge/QQChat-0.2.
|
|
10
|
+
<img src="https://img.shields.io/badge/QQChat-0.2.7-2ea44f.svg" alt="QQChat 0.2.7">
|
|
11
11
|
<img src="https://img.shields.io/badge/TypeScript-5.x-3178c6.svg" alt="TypeScript">
|
|
12
12
|
</p>
|
|
13
13
|
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
| QR connection | Binds an official QQ Bot from **Settings -> QQ Chat** | Cancel the binding and scan again when needed |
|
|
37
37
|
| Manual connection | Connects with AppID and AppSecret from the settings page | Useful for headless servers or QR failures; the secret is sent only to the Host |
|
|
38
38
|
| Group receive modes | Auto reply, Mention only or Silent record | Silent record stores messages without waking the Agent |
|
|
39
|
-
| Message formats |
|
|
40
|
-
| Direct-chat streaming |
|
|
39
|
+
| Message formats | Markdown or Plain compatibility | Group and direct-chat formats are configured separately; groups default to Plain compatibility |
|
|
40
|
+
| Direct-chat streaming | Markdown format automatically uses QQ's official streaming message API | Plain compatibility uses a regular send; some clients may not display streaming |
|
|
41
41
|
| Quotes and mentions | Preserves quoted messages and resolves QQ mentions to display names | Stable IDs remain in metadata |
|
|
42
42
|
| Memory system | Stores group, member and direct-user memory; user memory can be reused across groups under the same Bot | Group relationships and group events stay scoped; can be disabled |
|
|
43
43
|
| Memory reflection | Converts recent daily notes into long-term memory | Runs asynchronously after idle or batch thresholds |
|
|
@@ -146,7 +146,7 @@ Configure **Settings -> QQ Chat -> Message reception**:
|
|
|
146
146
|
| Mention only | All messages are stored, but only messages mentioning the Bot trigger a reply |
|
|
147
147
|
| Silent record | Stores messages and memory without actively replying |
|
|
148
148
|
|
|
149
|
-
Groups default to Plain compatibility format. Group and direct-chat formats can be selected independently
|
|
149
|
+
Groups default to Plain compatibility format. Group and direct-chat formats can be selected independently between Markdown and Plain compatibility. Markdown direct chats stream by default; Plain compatibility uses a regular send. There is no separate streaming toggle.
|
|
150
150
|
|
|
151
151
|
## Commands
|
|
152
152
|
|
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<p>
|
|
8
8
|
<a href="./LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="MIT License"></a>
|
|
9
9
|
<img src="https://img.shields.io/badge/DSH-0.1.6--alpha.1-6f42c1.svg" alt="DSH 0.1.6-alpha.1">
|
|
10
|
-
<img src="https://img.shields.io/badge/QQChat-0.2.
|
|
10
|
+
<img src="https://img.shields.io/badge/QQChat-0.2.7-2ea44f.svg" alt="QQChat 0.2.7">
|
|
11
11
|
<img src="https://img.shields.io/badge/TypeScript-5.x-3178c6.svg" alt="TypeScript">
|
|
12
12
|
</p>
|
|
13
13
|
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
| 手动连接 | 在设置页填写 AppID 和 AppSecret 连接 QQ Bot | 适合无手机、服务器或扫码失败场景;密钥只提交到 Host |
|
|
35
35
|
| 断线重连 | QQ Gateway 断线后自动恢复连接 | 无需重复扫码 |
|
|
36
36
|
| 群聊回复模式 | 自动回应、@回复、静默记录 | 可在设置页随时切换 |
|
|
37
|
-
|
|
|
38
|
-
| 私聊流式回复 |
|
|
37
|
+
| 消息格式 | Markdown、纯文本兼容 | 群聊与私聊可分别设置;群聊默认纯文本兼容 |
|
|
38
|
+
| 私聊流式回复 | Markdown 格式自动使用 QQ 官方流式接口 | 纯文本兼容格式使用普通发送;部分客户端可能不可见 |
|
|
39
39
|
| 消息引用与 @ | 支持引用消息和 QQ @消息 | @用户优先显示用户名 |
|
|
40
40
|
| 会话隔离 | QQ 群聊和私聊分别使用独立会话 | 不同群、不同用户的上下文互相隔离 |
|
|
41
41
|
| 记忆系统 | 保存群聊、群成员和私聊用户的独立记忆;同一 Bot 下的用户记忆支持跨群复用 | 群关系和群级事件不会跨群共享;设置中可关闭 |
|
|
@@ -121,7 +121,7 @@ npx @deepseek-ai/dsh --profile qqchat
|
|
|
121
121
|
| @回复 | 所有消息都会记录,但只有 @Bot 的消息会触发回复 |
|
|
122
122
|
| 静默记录 | 只保存消息和记忆,不主动回复 |
|
|
123
123
|
|
|
124
|
-
群聊默认使用纯文本兼容格式,适合不同 QQ
|
|
124
|
+
群聊默认使用纯文本兼容格式,适合不同 QQ 客户端。私聊和群聊可以分别选择 Markdown 或纯文本兼容。私聊选择 Markdown 时默认使用 QQ 官方流式传输;选择纯文本兼容时使用普通发送,不再提供单独的流式开关。
|
|
125
125
|
|
|
126
126
|
## 命令
|
|
127
127
|
|
package/client-src/settings.cts
CHANGED
|
@@ -116,8 +116,8 @@ export function QQSettings({ rpc }: { rpc: Rpc }) {
|
|
|
116
116
|
align: 'end',
|
|
117
117
|
items: [{ id: '', label: '不设置 Owner' }, ...members.map(member => ({ id: member.platformUserId, label: member.displayName || 'QQ 群友' }))],
|
|
118
118
|
})
|
|
119
|
-
const formatLabel = (value: ReplyFormat | undefined) => value === '
|
|
120
|
-
const formatItems = [{ id: '
|
|
119
|
+
const formatLabel = (value: ReplyFormat | undefined) => value === 'compat' ? '纯文本兼容' : 'Markdown'
|
|
120
|
+
const formatItems = [{ id: 'markdown', label: 'Markdown' }, { id: 'compat', label: '纯文本兼容' }]
|
|
121
121
|
const groupReplyPicker = h(Menu, {
|
|
122
122
|
open: groupReplyMenuOpen,
|
|
123
123
|
onClose: () => setGroupReplyMenuOpen(false),
|
|
@@ -195,23 +195,10 @@ export function QQSettings({ rpc }: { rpc: Rpc }) {
|
|
|
195
195
|
h('div', null, h('div', { className: 'qqSettingTitle' }, '群聊消息兼容格式'), h('div', { className: 'qqSettingHelp' }, '仅控制发送到 QQ 群聊时使用的消息格式。')),
|
|
196
196
|
h('div', { className: 'qqControl' }, groupReplyPicker)),
|
|
197
197
|
h('div', { className: 'qqSetting' },
|
|
198
|
-
h('div', null, h('div', { className: 'qqSettingTitle' }, '
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
h('
|
|
202
|
-
h('input', {
|
|
203
|
-
type: 'checkbox',
|
|
204
|
-
checked: settings.directStreamingEnabled && settings.directReplyFormat !== 'compat',
|
|
205
|
-
disabled: settings.directReplyFormat === 'compat',
|
|
206
|
-
onChange: (event: import('react').ChangeEvent<HTMLInputElement>) => void patch({ directStreamingEnabled: event.target.checked }),
|
|
207
|
-
}),
|
|
208
|
-
h('span', null,
|
|
209
|
-
h('div', { className: 'qqSettingTitle' }, '私聊流式回复'),
|
|
210
|
-
h('div', { className: 'qqSettingHelp' }, settings.directReplyFormat === 'compat'
|
|
211
|
-
? '纯文本兼容模式不支持流式传输,已自动关闭。'
|
|
212
|
-
: settings.directStreamingEnabled
|
|
213
|
-
? '使用 QQ 官方流式接口逐步更新私聊回复;部分 QQ 客户端可能看不到流式效果。'
|
|
214
|
-
: '使用普通一次性消息发送,兼容性更好。'))))
|
|
198
|
+
h('div', null, h('div', { className: 'qqSettingTitle' }, '私聊消息格式'), h('div', { className: 'qqSettingHelp' }, settings.directReplyFormat === 'markdown'
|
|
199
|
+
? 'Markdown 格式默认使用 QQ 官方流式接口逐步更新回复;部分客户端可能看不到流式效果。'
|
|
200
|
+
: '纯文本兼容格式使用普通发送,不启用流式传输。')),
|
|
201
|
+
h('div', { className: 'qqControl' }, directReplyPicker))
|
|
215
202
|
)
|
|
216
203
|
const memoryGroup = h('div', { className: 'qqSettingsGroup' },
|
|
217
204
|
h('h3', null, '记忆系统'),
|
package/client-src/shared.cts
CHANGED
|
@@ -18,7 +18,7 @@ export function getQQRpc(): Rpc | undefined { return qqRpc }
|
|
|
18
18
|
|
|
19
19
|
export type ChatType = 'c2c' | 'group'
|
|
20
20
|
export type GroupReceiveMode = 'auto' | 'mention' | 'silent'
|
|
21
|
-
export type ReplyFormat = '
|
|
21
|
+
export type ReplyFormat = 'markdown' | 'compat'
|
|
22
22
|
export type LogLevel = 'debug' | 'info' | 'warn' | 'error'
|
|
23
23
|
export type RpcResult<T> = { ok: true; value: T } | { ok: false; error?: { message?: string } }
|
|
24
24
|
export interface Rpc { call(channel: string, endpoint: string, payload: unknown): Promise<RpcResult<unknown>> }
|
|
@@ -38,7 +38,7 @@ export interface Ctx {
|
|
|
38
38
|
slots: SlotRegistry
|
|
39
39
|
}
|
|
40
40
|
export interface Account { id: number; appId: string; enabled: boolean; gatewayStatus: string; gatewayLastError?: string | null }
|
|
41
|
-
export interface Settings { memoryEnabled: boolean; memoryMemberBatchEnabled: boolean; groupReceiveMode: GroupReceiveMode; groupReplyFormat: ReplyFormat; directReplyFormat: ReplyFormat;
|
|
41
|
+
export interface Settings { memoryEnabled: boolean; memoryMemberBatchEnabled: boolean; groupReceiveMode: GroupReceiveMode; groupReplyFormat: ReplyFormat; directReplyFormat: ReplyFormat; groupMembersCanUseTools: boolean; groupMembersCanReceiveMedia: boolean; groupMembersCanReadMedia: boolean; ownerUserId: string }
|
|
42
42
|
export interface KnownMember { id: number; platformUserId: string; displayName: string; lastSeenAt: number }
|
|
43
43
|
export interface ChatItem { chatType: ChatType; rowId: number; platformId: string; displayName: string; dshSessionId: string | null; lastMessageAt: number | null; messageCount: number }
|
|
44
44
|
export interface Memory { profile: string; summary: string; daily: string; memory: string; pattern: string }
|
package/cordis.patch.yml
CHANGED
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
config:
|
|
7
7
|
dataDir: !!js dshHomePath('plugins/dsh-qqchat')
|
|
8
8
|
|
|
9
|
-
#
|
|
10
|
-
# connection fiber, so that fiber must also receive the Web server service.
|
|
9
|
+
# Connection RPC 需要 Web 启动配置和 Web Server 来配置 trusted hosts、注册 Host routes。
|
|
11
10
|
- id: connection
|
|
12
|
-
inject: [
|
|
11
|
+
inject: [webStartup, webServer]
|
package/docs/DEVELOPMENT.md
CHANGED
|
@@ -33,7 +33,7 @@ QQChat 是 TypeScript 插件,Host 和 Client 一起构建。日常开发不需
|
|
|
33
33
|
| 群友静默记录、未 @ 消息展示 | 无 QQ 自定义展示事件 | 使用插件 Client 渲染 `qqchat/message`;不送入模型 |
|
|
34
34
|
| 图片输入 | 取决于当前 DSH 模型路由和附件服务 | 使用 DSH 原生图片块;不支持视觉时使用稳定文本降级和读取工具 |
|
|
35
35
|
| 音频、视频和普通文件 | DSH 不提供统一的 QQ 媒体理解能力 | 保存附件并提供类型提示、文件工具或媒体信息工具 |
|
|
36
|
-
| QQ 私聊流式回复 | DSH 不负责 QQ Gateway 流式协议 |
|
|
36
|
+
| QQ 私聊流式回复 | DSH 不负责 QQ Gateway 流式协议 | Markdown 格式自动使用 QQ 官方流式接口;纯文本兼容格式使用普通发送;失败时按已发送帧安全回退 |
|
|
37
37
|
| QQ 引用、@和气泡 | DSH 不认识 QQ 协议字段 | 插件把引用和 @ 转为可读文本/元数据,并由 Client 做展示 |
|
|
38
38
|
| DSH 原生命令 | DSH 只处理 DSH Web 自己收到的命令 | 插件将 `/qq...` 命令解析后转交或执行,避免与 DSH 原命令冲突 |
|
|
39
39
|
|
package/docs/STREAMING.md
CHANGED
|
@@ -207,25 +207,15 @@ abort()
|
|
|
207
207
|
| 已成功发送部分流式内容 | 不发送完整普通消息,避免用户收到重复回复 |
|
|
208
208
|
| DONE 帧失败但已有内容 | 记录错误,不重复发送全文 |
|
|
209
209
|
| 模型没有文本输出 | 不创建流,沿用插件已有的空回复处理 |
|
|
210
|
-
|
|
|
210
|
+
| 纯文本兼容格式 | 完全跳过流式接口,直接使用普通发送 |
|
|
211
211
|
|
|
212
212
|
普通发送也要继续使用已有的 Markdown/纯文本兼容格式和主动发送回退逻辑。
|
|
213
213
|
|
|
214
|
-
##
|
|
214
|
+
## 设置与格式联动
|
|
215
215
|
|
|
216
|
-
|
|
216
|
+
设置页只提供两种发送格式:Markdown 与纯文本兼容,不再提供“智能格式”或独立流式开关。私聊选择 Markdown 时自动使用 QQ 官方流式接口;选择纯文本兼容时跳过流式接口并普通发送。私聊默认 Markdown,群聊默认纯文本兼容;群聊始终普通发送,因为官方 `stream_messages` 只覆盖 C2C 私聊。
|
|
217
217
|
|
|
218
|
-
|
|
219
|
-
interface QQChatRuntimeSettings {
|
|
220
|
-
directStreamingEnabled: boolean
|
|
221
|
-
}
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
推荐默认值:`true`,保持官方流式能力;如果目标客户端普遍看不到流式效果,也可以在产品层默认关闭。
|
|
225
|
-
|
|
226
|
-
设置说明应明确:
|
|
227
|
-
|
|
228
|
-
> 私聊流式回复使用 QQ 官方流式接口逐步更新消息;部分 QQ 客户端可能看不到流式效果。关闭后使用普通一次性消息发送。
|
|
218
|
+
历史设置中的 `smart` 会迁移为 Markdown;旧的 `directStreamingEnabled` 设置项会被清理。Markdown 权限不可用时,普通发送仍保留纯文本降级。
|
|
229
219
|
|
|
230
220
|
群聊不应被这个开关影响,因为 QQ 官方 `stream_messages` 方案只覆盖 C2C 私聊。
|
|
231
221
|
|
|
@@ -310,7 +300,7 @@ if (writer.sentFrameCount === 0) {
|
|
|
310
300
|
- [x] 所有请求通过串行队列;
|
|
311
301
|
- [x] 使用节流减少更新频率;
|
|
312
302
|
- [x] 对发送失败区分“尚未发送”和“已经发送部分”;
|
|
313
|
-
- [x]
|
|
303
|
+
- [x] 私聊 Markdown 自动流式,纯文本兼容自动普通发送;
|
|
314
304
|
- [x] 使用模拟 HTTP 测试覆盖首帧、后续帧、结束帧和队列竞态;
|
|
315
305
|
- [ ] 真实 QQ 手机端和桌面端分别验证所有客户端的可见性;
|
|
316
306
|
- [x] 日志只记录 `msg_seq`、`index`、是否有 `stream_msg_id` 等定位信息,不记录完整敏感正文。
|
|
@@ -333,7 +323,7 @@ QQ 会在 `msg_seq` 重复或流式上下文不完整时返回 `40054005`。当
|
|
|
333
323
|
| 中途失败 | 不发送完整重复消息 |
|
|
334
324
|
| 429/50002 | 按退避策略重试,超过次数后降级或报错 |
|
|
335
325
|
| 空回复 | 不发送空流式消息 |
|
|
336
|
-
|
|
|
326
|
+
| 私聊纯文本兼容 | 不访问 `stream_messages` |
|
|
337
327
|
| 群聊 | 不访问 `stream_messages` |
|
|
338
328
|
|
|
339
329
|
## 当前实现位置
|
package/lib/client.js
CHANGED
|
@@ -182,8 +182,8 @@ function QQSettings({ rpc }) {
|
|
|
182
182
|
align: 'end',
|
|
183
183
|
items: [{ id: '', label: '不设置 Owner' }, ...members.map(member => ({ id: member.platformUserId, label: member.displayName || 'QQ 群友' }))],
|
|
184
184
|
});
|
|
185
|
-
const formatLabel = (value) => value === '
|
|
186
|
-
const formatItems = [{ id: '
|
|
185
|
+
const formatLabel = (value) => value === 'compat' ? '纯文本兼容' : 'Markdown';
|
|
186
|
+
const formatItems = [{ id: 'markdown', label: 'Markdown' }, { id: 'compat', label: '纯文本兼容' }];
|
|
187
187
|
const groupReplyPicker = (0, shared_cjs_1.h)(shared_cjs_1.Menu, {
|
|
188
188
|
open: groupReplyMenuOpen,
|
|
189
189
|
onClose: () => setGroupReplyMenuOpen(false),
|
|
@@ -235,16 +235,9 @@ function QQSettings({ rpc }) {
|
|
|
235
235
|
footer: [{ type: 'label', id: 'receive-help-note', text: '未开启时只能收到 @ 消息,非 @ 消息不会进入会话记录' }],
|
|
236
236
|
anchor: (0, shared_cjs_1.h)(shared_cjs_1.Button, { size: 'sm', variant: 'outline', onClick: () => setReceiveHelpOpen(!receiveHelpOpen) }, '设置方法'),
|
|
237
237
|
});
|
|
238
|
-
const receiveGroup = (0, shared_cjs_1.h)('div', { className: 'qqSettingsGroup' }, (0, shared_cjs_1.h)('h3', null, '消息接收'), (0, shared_cjs_1.h)('div', { className: 'qqSetting' }, (0, shared_cjs_1.h)('div', null, (0, shared_cjs_1.h)('div', { className: 'qqSettingTitle' }, '群聊接收方式'), (0, shared_cjs_1.h)('div', { className: 'qqSettingHelp qqHelpRow' }, '自动回应:每条群消息都会触发 Agent;@回复:所有消息都记录,但只有 @Bot 才唤醒 Agent;静默记录:只记录群聊与记忆,不主动回应。', receiveHelp)), (0, shared_cjs_1.h)('div', { className: 'qqControl' }, receiveButtons)), (0, shared_cjs_1.h)('div', { className: 'qqSetting' }, (0, shared_cjs_1.h)('div', null, (0, shared_cjs_1.h)('div', { className: 'qqSettingTitle' }, '群聊消息兼容格式'), (0, shared_cjs_1.h)('div', { className: 'qqSettingHelp' }, '仅控制发送到 QQ 群聊时使用的消息格式。')), (0, shared_cjs_1.h)('div', { className: 'qqControl' }, groupReplyPicker)), (0, shared_cjs_1.h)('div', { className: 'qqSetting' }, (0, shared_cjs_1.h)('div', null, (0, shared_cjs_1.h)('div', { className: 'qqSettingTitle' }, '
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
disabled: settings.directReplyFormat === 'compat',
|
|
242
|
-
onChange: (event) => void patch({ directStreamingEnabled: event.target.checked }),
|
|
243
|
-
}), (0, shared_cjs_1.h)('span', null, (0, shared_cjs_1.h)('div', { className: 'qqSettingTitle' }, '私聊流式回复'), (0, shared_cjs_1.h)('div', { className: 'qqSettingHelp' }, settings.directReplyFormat === 'compat'
|
|
244
|
-
? '纯文本兼容模式不支持流式传输,已自动关闭。'
|
|
245
|
-
: settings.directStreamingEnabled
|
|
246
|
-
? '使用 QQ 官方流式接口逐步更新私聊回复;部分 QQ 客户端可能看不到流式效果。'
|
|
247
|
-
: '使用普通一次性消息发送,兼容性更好。')))));
|
|
238
|
+
const receiveGroup = (0, shared_cjs_1.h)('div', { className: 'qqSettingsGroup' }, (0, shared_cjs_1.h)('h3', null, '消息接收'), (0, shared_cjs_1.h)('div', { className: 'qqSetting' }, (0, shared_cjs_1.h)('div', null, (0, shared_cjs_1.h)('div', { className: 'qqSettingTitle' }, '群聊接收方式'), (0, shared_cjs_1.h)('div', { className: 'qqSettingHelp qqHelpRow' }, '自动回应:每条群消息都会触发 Agent;@回复:所有消息都记录,但只有 @Bot 才唤醒 Agent;静默记录:只记录群聊与记忆,不主动回应。', receiveHelp)), (0, shared_cjs_1.h)('div', { className: 'qqControl' }, receiveButtons)), (0, shared_cjs_1.h)('div', { className: 'qqSetting' }, (0, shared_cjs_1.h)('div', null, (0, shared_cjs_1.h)('div', { className: 'qqSettingTitle' }, '群聊消息兼容格式'), (0, shared_cjs_1.h)('div', { className: 'qqSettingHelp' }, '仅控制发送到 QQ 群聊时使用的消息格式。')), (0, shared_cjs_1.h)('div', { className: 'qqControl' }, groupReplyPicker)), (0, shared_cjs_1.h)('div', { className: 'qqSetting' }, (0, shared_cjs_1.h)('div', null, (0, shared_cjs_1.h)('div', { className: 'qqSettingTitle' }, '私聊消息格式'), (0, shared_cjs_1.h)('div', { className: 'qqSettingHelp' }, settings.directReplyFormat === 'markdown'
|
|
239
|
+
? 'Markdown 格式默认使用 QQ 官方流式接口逐步更新回复;部分客户端可能看不到流式效果。'
|
|
240
|
+
: '纯文本兼容格式使用普通发送,不启用流式传输。')), (0, shared_cjs_1.h)('div', { className: 'qqControl' }, directReplyPicker)));
|
|
248
241
|
const memoryGroup = (0, shared_cjs_1.h)('div', { className: 'qqSettingsGroup' }, (0, shared_cjs_1.h)('h3', null, '记忆系统'), (0, shared_cjs_1.h)('div', { className: 'qqSetting' }, (0, shared_cjs_1.h)('div', null, (0, shared_cjs_1.h)('div', { className: 'qqSettingTitle' }, '启用记忆系统'), (0, shared_cjs_1.h)('div', { className: 'qqSettingHelp' }, '记忆会在新 Session、上下文 TTL 过期、记忆变化或 Session 压缩后刷新,不会每轮重复注入完整上下文;后台会在空闲时整理近期消息。启用记忆可能降低上下文缓存命中率并增加输入 token。关闭后停止记忆注入和后台整理,但不会删除已有记忆。')), (0, shared_cjs_1.h)('div', { className: 'qqControl' }, (0, shared_cjs_1.h)(shared_cjs_1.Button, { size: 'sm', variant: settings.memoryEnabled ? 'primary' : 'outline', onClick: () => void patch({ memoryEnabled: !settings.memoryEnabled }) }, settings.memoryEnabled ? '已启用' : '已关闭'))));
|
|
249
242
|
const ownerSetting = !settings.groupMembersCanUseTools
|
|
250
243
|
? (0, shared_cjs_1.h)('div', { className: 'qqSetting' }, (0, shared_cjs_1.h)('div', null, (0, shared_cjs_1.h)('div', { className: 'qqSettingTitle' }, 'Owner stable ID'), (0, shared_cjs_1.h)('div', { className: 'qqSettingHelp' }, '使用 stable sender ID,不根据昵称判断。')), (0, shared_cjs_1.h)('div', { className: 'qqControl' }, (0, shared_cjs_1.h)(shared_cjs_1.Input, {
|
package/lib/config.js
CHANGED
|
@@ -6,7 +6,7 @@ const DEFAULT_CONFIG = Object.freeze({
|
|
|
6
6
|
groupChatEnabled: true,
|
|
7
7
|
groupRequiresAt: true,
|
|
8
8
|
groupReadEnabled: true,
|
|
9
|
-
replyFormat: '
|
|
9
|
+
replyFormat: 'markdown',
|
|
10
10
|
recentGroupMessages: 40,
|
|
11
11
|
reflectionIdleMs: 900_000,
|
|
12
12
|
reflectionBatchSize: 20,
|
|
@@ -16,9 +16,7 @@ const DEFAULT_CONFIG = Object.freeze({
|
|
|
16
16
|
});
|
|
17
17
|
export function resolveConfig(input = {}) {
|
|
18
18
|
const home = process.env.DSH_HOME || join(homedir(), '.dsh');
|
|
19
|
-
const replyFormat =
|
|
20
|
-
? input.replyFormat
|
|
21
|
-
: DEFAULT_CONFIG.replyFormat;
|
|
19
|
+
const replyFormat = input.replyFormat === 'compat' ? 'compat' : 'markdown';
|
|
22
20
|
return Object.freeze({
|
|
23
21
|
dataDir: resolve(input.dataDir || join(home, 'plugins', 'dsh-qqchat')),
|
|
24
22
|
source: input.source || DEFAULT_CONFIG.source,
|
|
@@ -49,7 +47,6 @@ export function defaultRuntimeSettings(config) {
|
|
|
49
47
|
groupReceiveMode,
|
|
50
48
|
groupReplyFormat: 'compat',
|
|
51
49
|
directReplyFormat: config.replyFormat,
|
|
52
|
-
directStreamingEnabled: config.replyFormat !== 'compat',
|
|
53
50
|
groupMembersCanUseTools: false,
|
|
54
51
|
groupMembersCanReceiveMedia: true,
|
|
55
52
|
groupMembersCanReadMedia: false,
|
package/lib/gateway/api.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { QQChatDatabase } from '../storage/db.js';
|
|
2
|
-
import type { AccountRow, QQChatConfig, QQSendOptions
|
|
2
|
+
import type { AccountRow, QQChatConfig, QQSendOptions } from '../types.js';
|
|
3
3
|
export interface QQPrivateTextStream {
|
|
4
4
|
push(delta: string): void;
|
|
5
5
|
finish(text: string): Promise<void>;
|
|
@@ -25,6 +25,5 @@ export declare class QQApiClient {
|
|
|
25
25
|
private request;
|
|
26
26
|
sendText(account: AccountRow, targetId: string, text: string, { group, messageId, format }?: QQSendOptions): Promise<Record<string, unknown>>;
|
|
27
27
|
sendReplyWithActiveFallback(account: AccountRow, targetId: string, text: string, options: QQSendOptions): Promise<Record<string, unknown>>;
|
|
28
|
-
createPrivateTextStream(account: AccountRow, targetId: string, { messageId
|
|
28
|
+
createPrivateTextStream(account: AccountRow, targetId: string, { messageId }?: Pick<QQSendOptions, 'messageId'>): QQPrivateTextStream;
|
|
29
29
|
}
|
|
30
|
-
export declare function isReplyFormat(value: string): value is ReplyFormat;
|
package/lib/gateway/api.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
const API_BASE = 'https://api.sgroup.qq.com';
|
|
2
2
|
const SANDBOX_API_BASE = 'https://sandbox.api.sgroup.qq.com';
|
|
3
3
|
const TOKEN_URL = 'https://bots.qq.com/app/getAppAccessToken';
|
|
4
|
-
function looksLikeMarkdown(text) {
|
|
5
|
-
return /(^|\n)\s{0,3}(#{1,6}\s|[-*+]\s|\d+\.\s|>\s)|\*\*|__|`|\[[^\]]+\]\([^\)]+\)/m.test(text);
|
|
6
|
-
}
|
|
7
4
|
export class QQApiClient {
|
|
8
5
|
db;
|
|
9
6
|
config;
|
|
@@ -86,7 +83,7 @@ export class QQApiClient {
|
|
|
86
83
|
async sendText(account, targetId, text, { group = false, messageId = null, format = this.config.replyFormat } = {}) {
|
|
87
84
|
const path = group ? `/v2/groups/${encodeURIComponent(targetId)}/messages` : `/v2/users/${encodeURIComponent(targetId)}/messages`;
|
|
88
85
|
const seq = this.nextSeq(messageId);
|
|
89
|
-
const useMarkdown = format === 'markdown'
|
|
86
|
+
const useMarkdown = format === 'markdown';
|
|
90
87
|
const makeBody = (markdown) => ({
|
|
91
88
|
msg_type: markdown ? 2 : 0,
|
|
92
89
|
...(markdown ? { markdown: { content: text } } : { content: text }),
|
|
@@ -113,10 +110,9 @@ export class QQApiClient {
|
|
|
113
110
|
return this.sendText(account, targetId, text, { ...options, messageId: null });
|
|
114
111
|
}
|
|
115
112
|
}
|
|
116
|
-
createPrivateTextStream(account, targetId, { messageId = null
|
|
113
|
+
createPrivateTextStream(account, targetId, { messageId = null } = {}) {
|
|
117
114
|
const path = `/v2/users/${encodeURIComponent(targetId)}/stream_messages`;
|
|
118
115
|
const msgSeq = this.nextStreamSeq();
|
|
119
|
-
const contentType = format === 'markdown' ? 'markdown' : 'text';
|
|
120
116
|
let fullText = '';
|
|
121
117
|
let streamMessageId;
|
|
122
118
|
let index = 0;
|
|
@@ -130,7 +126,7 @@ export class QQApiClient {
|
|
|
130
126
|
// Build the request only when the queued task runs. The first
|
|
131
127
|
// response supplies stream_msg_id for all following frames.
|
|
132
128
|
const body = {
|
|
133
|
-
input_mode: 'replace', input_state: inputState, index: index++, content_type:
|
|
129
|
+
input_mode: 'replace', input_state: inputState, index: index++, content_type: 'markdown',
|
|
134
130
|
content_raw: content, msg_seq: msgSeq,
|
|
135
131
|
...(messageId ? { msg_id: messageId, event_id: messageId } : {}),
|
|
136
132
|
...(streamMessageId ? { stream_msg_id: streamMessageId } : {}),
|
|
@@ -201,6 +197,3 @@ async function safeJson(response) {
|
|
|
201
197
|
return undefined;
|
|
202
198
|
}
|
|
203
199
|
}
|
|
204
|
-
export function isReplyFormat(value) {
|
|
205
|
-
return value === 'smart' || value === 'markdown' || value === 'compat';
|
|
206
|
-
}
|
|
@@ -123,15 +123,6 @@ export class DshQQBridge {
|
|
|
123
123
|
return undefined;
|
|
124
124
|
const { agent, sessionId } = await this.ensureAgent(event.chatType, row);
|
|
125
125
|
event.sessionId = sessionId;
|
|
126
|
-
if (event.isOwner) {
|
|
127
|
-
// Keep owner messages as native user/message events even when they carry
|
|
128
|
-
// images. DSH now owns the image projection: visual routes receive the
|
|
129
|
-
// durable image block, while text-only routes receive its stable text
|
|
130
|
-
// placeholder. Falling back to a display-only event here would discard
|
|
131
|
-
// the owner's message semantics before DSH can apply that policy.
|
|
132
|
-
this.appendOwnerMessageIfMissing(agent.session, event);
|
|
133
|
-
return sessionId;
|
|
134
|
-
}
|
|
135
126
|
this.appendDisplayIfMissing(agent.session, event);
|
|
136
127
|
return sessionId;
|
|
137
128
|
}
|
|
@@ -352,24 +343,6 @@ export class DshQQBridge {
|
|
|
352
343
|
// fresh Session while SQLite remains the QQ history source of truth.
|
|
353
344
|
session.append('qqchat/message', displayEventPayload(event));
|
|
354
345
|
}
|
|
355
|
-
appendOwnerMessageIfMissing(session, event) {
|
|
356
|
-
if (session.snapshotEvents().some(item => {
|
|
357
|
-
if (item.type !== 'user/message')
|
|
358
|
-
return false;
|
|
359
|
-
const source = item.data.source;
|
|
360
|
-
return source.messageId === event.messageId;
|
|
361
|
-
}))
|
|
362
|
-
return;
|
|
363
|
-
session.append('user/message', createUserMessage({
|
|
364
|
-
source: {
|
|
365
|
-
kind: 'user', channel: 'qq', botId: 'qqchat', chatType: event.chatType,
|
|
366
|
-
chatId: event.chatId, senderId: event.senderId,
|
|
367
|
-
...(event.senderName ? { senderName: event.senderName } : {}),
|
|
368
|
-
messageId: event.messageId, mentioned: event.mentioned,
|
|
369
|
-
},
|
|
370
|
-
content: transcriptContent(event, this.db),
|
|
371
|
-
}), { surfaceOp: 'append' });
|
|
372
|
-
}
|
|
373
346
|
ensureTitle(session, chatType, row) {
|
|
374
347
|
try {
|
|
375
348
|
const getService = this.ctx.get;
|
|
@@ -619,35 +592,6 @@ export function qqMessageContent(text, attachments) {
|
|
|
619
592
|
}
|
|
620
593
|
return content;
|
|
621
594
|
}
|
|
622
|
-
function transcriptContent(event, db) {
|
|
623
|
-
const attachments = event.attachments || [];
|
|
624
|
-
const renderableImages = attachments.filter(attachment => attachment.kind === 'image' && db.attachmentById(attachment.id)?.imageRef);
|
|
625
|
-
const imagePlaceholderLines = new Set(renderableImages.map(attachment => `[图片] ${attachment.filename}`));
|
|
626
|
-
const quote = event.quote ? formatDisplayQuote(event) : '';
|
|
627
|
-
const body = event.content.split('\n').filter(line => !imagePlaceholderLines.has(line)).join('\n').trim();
|
|
628
|
-
const text = [quote, body].filter(Boolean).join('\n\n');
|
|
629
|
-
const content = [{ type: 'text', text }];
|
|
630
|
-
for (const attachment of attachments) {
|
|
631
|
-
const imageRef = attachment.kind === 'image' ? db.attachmentById(attachment.id)?.imageRef : undefined;
|
|
632
|
-
if (imageRef) {
|
|
633
|
-
content.push({ type: 'image', attachment: imageRef });
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
return content;
|
|
637
|
-
}
|
|
638
|
-
function formatDisplayQuote(event) {
|
|
639
|
-
const quote = event.quote;
|
|
640
|
-
if (!quote)
|
|
641
|
-
return '';
|
|
642
|
-
return [
|
|
643
|
-
'[引用消息]',
|
|
644
|
-
quote.senderId ? `发送人ID=${quote.senderId}` : '',
|
|
645
|
-
quote.senderName ? `显示名=${quote.senderName}` : '',
|
|
646
|
-
quote.messageId ? `消息ID=${quote.messageId}` : '',
|
|
647
|
-
`正文=${quote.text || '(空消息)'}`,
|
|
648
|
-
quote.attachments.length ? `附件=${quote.attachments.map(attachment => `${attachment.kind || 'file'}:${attachment.filename}${attachment.attachmentId ? ` (attachment_id=${attachment.attachmentId})` : ''}`).join('、')}` : '',
|
|
649
|
-
].filter(Boolean).join('\n');
|
|
650
|
-
}
|
|
651
595
|
function displayEventPayload(event) {
|
|
652
596
|
const attachments = (event.attachments || []).map(attachment => ({
|
|
653
597
|
id: String(attachment.id), kind: attachment.kind, filename: String(attachment.filename),
|
package/lib/session/runtime.d.ts
CHANGED
|
@@ -38,5 +38,7 @@ export declare class QQChatRuntime {
|
|
|
38
38
|
private flushOutbox;
|
|
39
39
|
}
|
|
40
40
|
export declare function shouldReplyToGroup(mode: GroupReceiveMode, group: Pick<GroupRow, 'enabled' | 'read_enabled'>, mentioned: boolean): boolean;
|
|
41
|
+
/** Record messages that do not enter the Agent turn, plus QQ commands handled outside Agent.followup. */
|
|
42
|
+
export declare function shouldRecordQQTranscript(shouldReply: boolean, text: string, mentioned: boolean): boolean;
|
|
41
43
|
export declare function mergeIndexedQuote(message: QQNormalizedMessage, indexed: import('../types.js').QQQuoteIndexRow | undefined): QQNormalizedMessage;
|
|
42
44
|
export declare function indexAttachments(inputs: QQAttachmentInput[], stored: StoredAttachmentSummary[]): QQAttachmentInput[];
|
package/lib/session/runtime.js
CHANGED
|
@@ -2,6 +2,7 @@ import { readFile } from 'node:fs/promises';
|
|
|
2
2
|
import { defaultRuntimeSettings } from '../config.js';
|
|
3
3
|
import { QQGateway } from '../gateway/gateway.js';
|
|
4
4
|
import { renderQQMentionNames } from '../gateway/normalize.js';
|
|
5
|
+
import { qqCommandText } from '../commands/dispatch.js';
|
|
5
6
|
const QUOTE_INDEX_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
6
7
|
export class QQChatRuntime {
|
|
7
8
|
ctx;
|
|
@@ -52,10 +53,7 @@ export class QQChatRuntime {
|
|
|
52
53
|
this.db.close();
|
|
53
54
|
}
|
|
54
55
|
settings() {
|
|
55
|
-
|
|
56
|
-
if (settings.directReplyFormat === 'compat')
|
|
57
|
-
settings.directStreamingEnabled = false;
|
|
58
|
-
return settings;
|
|
56
|
+
return this.db.runtimeSettings(defaultRuntimeSettings(this.config));
|
|
59
57
|
}
|
|
60
58
|
updateSettings(patch) {
|
|
61
59
|
if (patch.memoryEnabled !== undefined)
|
|
@@ -76,11 +74,7 @@ export class QQChatRuntime {
|
|
|
76
74
|
if (!isReplyFormat(patch.directReplyFormat))
|
|
77
75
|
throw new Error('无效的私聊消息兼容格式');
|
|
78
76
|
this.db.setSetting('directReplyFormat', patch.directReplyFormat);
|
|
79
|
-
if (patch.directReplyFormat === 'compat')
|
|
80
|
-
this.db.setSetting('directStreamingEnabled', false);
|
|
81
77
|
}
|
|
82
|
-
if (patch.directStreamingEnabled !== undefined)
|
|
83
|
-
this.db.setSetting('directStreamingEnabled', Boolean(patch.directStreamingEnabled));
|
|
84
78
|
if (patch.groupMembersCanUseTools !== undefined)
|
|
85
79
|
this.db.setSetting('groupMembersCanUseTools', Boolean(patch.groupMembersCanUseTools));
|
|
86
80
|
if (patch.groupMembersCanReceiveMedia !== undefined)
|
|
@@ -273,21 +267,20 @@ export class QQChatRuntime {
|
|
|
273
267
|
this.memory.scheduleGroupMembers(Number(group.id));
|
|
274
268
|
}
|
|
275
269
|
}
|
|
276
|
-
if (
|
|
270
|
+
if (shouldRecordQQTranscript(shouldReply, message.text, message.mentioned)) {
|
|
277
271
|
await this.bridge.recordTranscript(displayEvent, row, true);
|
|
272
|
+
}
|
|
273
|
+
if (!shouldReply) {
|
|
278
274
|
return;
|
|
279
275
|
}
|
|
280
276
|
try {
|
|
281
|
-
// Keep the incoming QQ message visible in Web independently from the
|
|
282
|
-
// notice-shaped message used to drive the Agent turn.
|
|
283
|
-
await this.bridge.recordTranscript(displayEvent, row, true);
|
|
284
277
|
const targetId = message.chatType === 'group' ? message.groupOpenid : message.senderId;
|
|
285
278
|
const sendOptions = {
|
|
286
279
|
group: message.chatType === 'group', messageId: message.messageId || null,
|
|
287
280
|
format: message.chatType === 'group' ? settings.groupReplyFormat : settings.directReplyFormat,
|
|
288
281
|
};
|
|
289
|
-
const stream = message.chatType === 'c2c' && settings.directReplyFormat
|
|
290
|
-
? this.api.createPrivateTextStream(account, targetId, sendOptions)
|
|
282
|
+
const stream = message.chatType === 'c2c' && settings.directReplyFormat === 'markdown'
|
|
283
|
+
? this.api.createPrivateTextStream(account, targetId, { messageId: sendOptions.messageId })
|
|
291
284
|
: undefined;
|
|
292
285
|
const reply = await this.bridge.reply(message, group, member, storedAttachments, stream ? delta => stream.push(delta) : undefined, isOwner);
|
|
293
286
|
if (stream) {
|
|
@@ -345,12 +338,16 @@ export class QQChatRuntime {
|
|
|
345
338
|
}
|
|
346
339
|
}
|
|
347
340
|
function isGroupReceiveMode(value) { return value === 'auto' || value === 'mention' || value === 'silent'; }
|
|
348
|
-
function isReplyFormat(value) { return value === '
|
|
341
|
+
function isReplyFormat(value) { return value === 'markdown' || value === 'compat'; }
|
|
349
342
|
export function shouldReplyToGroup(mode, group, mentioned) {
|
|
350
343
|
if (group.enabled !== 1 || group.read_enabled !== 1)
|
|
351
344
|
return false;
|
|
352
345
|
return mode === 'auto' || (mode === 'mention' && mentioned);
|
|
353
346
|
}
|
|
347
|
+
/** Record messages that do not enter the Agent turn, plus QQ commands handled outside Agent.followup. */
|
|
348
|
+
export function shouldRecordQQTranscript(shouldReply, text, mentioned) {
|
|
349
|
+
return !shouldReply || qqCommandText(text, mentioned) !== undefined;
|
|
350
|
+
}
|
|
354
351
|
function shortId(value) { return value.length > 10 ? `…${value.slice(-10)}` : value; }
|
|
355
352
|
function publicAttachments(attachments) {
|
|
356
353
|
return attachments.map(attachment => ({
|
package/lib/storage/db.js
CHANGED
|
@@ -239,6 +239,9 @@ export class QQChatDatabase {
|
|
|
239
239
|
error TEXT
|
|
240
240
|
);
|
|
241
241
|
`);
|
|
242
|
+
this.db.prepare("UPDATE plugin_settings SET value='\"markdown\"',updated_at=? WHERE key IN ('replyFormat','groupReplyFormat','directReplyFormat') AND value='\"smart\"'")
|
|
243
|
+
.run(now());
|
|
244
|
+
this.db.prepare("DELETE FROM plugin_settings WHERE key='directStreamingEnabled'").run();
|
|
242
245
|
this.db.prepare("UPDATE reflection_tasks SET status='failed',updated_at=? WHERE status='running' AND updated_at<?")
|
|
243
246
|
.run(now(), now() - REFLECTION_TASK_STALE_MS);
|
|
244
247
|
const columns = new Set(many(this.db.prepare('PRAGMA table_info(group_members)').all()).map(column => column.name));
|
|
@@ -362,13 +365,13 @@ export class QQChatDatabase {
|
|
|
362
365
|
.run(key, JSON.stringify(value), now());
|
|
363
366
|
}
|
|
364
367
|
runtimeSettings(defaults) {
|
|
368
|
+
const legacyReplyFormat = normalizeReplyFormat(this.getSetting('replyFormat', defaults.directReplyFormat), defaults.directReplyFormat);
|
|
365
369
|
return {
|
|
366
370
|
memoryEnabled: this.getSetting('memoryEnabled', defaults.memoryEnabled),
|
|
367
371
|
memoryMemberBatchEnabled: this.getSetting('memoryMemberBatchEnabled', defaults.memoryMemberBatchEnabled),
|
|
368
372
|
groupReceiveMode: this.getSetting('groupReceiveMode', defaults.groupReceiveMode),
|
|
369
|
-
groupReplyFormat: this.getSetting('groupReplyFormat', defaults.groupReplyFormat),
|
|
370
|
-
directReplyFormat: this.getSetting('directReplyFormat',
|
|
371
|
-
directStreamingEnabled: this.getSetting('directStreamingEnabled', defaults.directStreamingEnabled),
|
|
373
|
+
groupReplyFormat: normalizeReplyFormat(this.getSetting('groupReplyFormat', defaults.groupReplyFormat), defaults.groupReplyFormat),
|
|
374
|
+
directReplyFormat: normalizeReplyFormat(this.getSetting('directReplyFormat', legacyReplyFormat), legacyReplyFormat),
|
|
372
375
|
groupMembersCanUseTools: this.getSetting('groupMembersCanUseTools', defaults.groupMembersCanUseTools),
|
|
373
376
|
groupMembersCanReceiveMedia: this.getSetting('groupMembersCanReceiveMedia', defaults.groupMembersCanReceiveMedia),
|
|
374
377
|
groupMembersCanReadMedia: this.getSetting('groupMembersCanReadMedia', defaults.groupMembersCanReadMedia),
|
|
@@ -724,3 +727,9 @@ export class QQChatDatabase {
|
|
|
724
727
|
this.db.prepare('UPDATE outbox SET status=?,sent_at=?,error=? WHERE id=?').run(error ? 'failed' : 'sent', now(), error, id);
|
|
725
728
|
}
|
|
726
729
|
}
|
|
730
|
+
function normalizeReplyFormat(value, fallback) {
|
|
731
|
+
if (value === 'markdown' || value === 'compat')
|
|
732
|
+
return value;
|
|
733
|
+
// Older releases persisted the automatic “smart” mode. Its explicit replacement is Markdown.
|
|
734
|
+
return value === 'smart' ? 'markdown' : fallback;
|
|
735
|
+
}
|
package/lib/transport/rpc.js
CHANGED
|
@@ -304,12 +304,10 @@ function asSettingsPatch(value) {
|
|
|
304
304
|
if (record.groupReceiveMode === 'auto' || record.groupReceiveMode === 'mention' || record.groupReceiveMode === 'silent') {
|
|
305
305
|
patch.groupReceiveMode = record.groupReceiveMode;
|
|
306
306
|
}
|
|
307
|
-
if (record.groupReplyFormat === '
|
|
307
|
+
if (record.groupReplyFormat === 'markdown' || record.groupReplyFormat === 'compat')
|
|
308
308
|
patch.groupReplyFormat = record.groupReplyFormat;
|
|
309
|
-
if (record.directReplyFormat === '
|
|
309
|
+
if (record.directReplyFormat === 'markdown' || record.directReplyFormat === 'compat')
|
|
310
310
|
patch.directReplyFormat = record.directReplyFormat;
|
|
311
|
-
if (typeof record.directStreamingEnabled === 'boolean')
|
|
312
|
-
patch.directStreamingEnabled = record.directStreamingEnabled;
|
|
313
311
|
if (typeof record.groupMembersCanUseTools === 'boolean')
|
|
314
312
|
patch.groupMembersCanUseTools = record.groupMembersCanUseTools;
|
|
315
313
|
if (typeof record.groupMembersCanReceiveMedia === 'boolean')
|
package/lib/types.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export interface DailyEntry {
|
|
|
13
13
|
date: string;
|
|
14
14
|
note: string;
|
|
15
15
|
}
|
|
16
|
-
export type ReplyFormat = '
|
|
16
|
+
export type ReplyFormat = 'markdown' | 'compat';
|
|
17
17
|
export type GroupReceiveMode = 'auto' | 'mention' | 'silent';
|
|
18
18
|
export type GatewayStatus = 'offline' | 'connecting' | 'online' | 'error';
|
|
19
19
|
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
@@ -58,7 +58,6 @@ export interface QQChatRuntimeSettings {
|
|
|
58
58
|
groupReceiveMode: GroupReceiveMode;
|
|
59
59
|
groupReplyFormat: ReplyFormat;
|
|
60
60
|
directReplyFormat: ReplyFormat;
|
|
61
|
-
directStreamingEnabled: boolean;
|
|
62
61
|
groupMembersCanUseTools: boolean;
|
|
63
62
|
groupMembersCanReceiveMedia: boolean;
|
|
64
63
|
groupMembersCanReadMedia: boolean;
|
|
@@ -70,7 +69,6 @@ export interface QQChatRuntimeSettingsPatch {
|
|
|
70
69
|
groupReceiveMode?: GroupReceiveMode;
|
|
71
70
|
groupReplyFormat?: ReplyFormat;
|
|
72
71
|
directReplyFormat?: ReplyFormat;
|
|
73
|
-
directStreamingEnabled?: boolean;
|
|
74
72
|
groupMembersCanUseTools?: boolean;
|
|
75
73
|
groupMembersCanReceiveMedia?: boolean;
|
|
76
74
|
groupMembersCanReadMedia?: boolean;
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-qqchat",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
4
4
|
"description": "DeepSeek Harness 的 QQ 私聊/群聊插件,支持扫码授权、原生 Session、群记忆与工具权限。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"engines": {
|
|
8
|
-
"node": "^22.19.0 || >=24.0.0"
|
|
8
|
+
"node": "^22.19.0 || >=24.0.0",
|
|
9
|
+
"dsh": ">=0.1.6-alpha.1"
|
|
9
10
|
},
|
|
10
11
|
"main": "./lib/index.js",
|
|
11
12
|
"types": "./lib/index.d.ts",
|
package/src/config.ts
CHANGED
|
@@ -8,7 +8,7 @@ const DEFAULT_CONFIG = Object.freeze({
|
|
|
8
8
|
groupChatEnabled: true,
|
|
9
9
|
groupRequiresAt: true,
|
|
10
10
|
groupReadEnabled: true,
|
|
11
|
-
replyFormat: '
|
|
11
|
+
replyFormat: 'markdown' as ReplyFormat,
|
|
12
12
|
recentGroupMessages: 40,
|
|
13
13
|
reflectionIdleMs: 900_000,
|
|
14
14
|
reflectionBatchSize: 20,
|
|
@@ -19,9 +19,7 @@ const DEFAULT_CONFIG = Object.freeze({
|
|
|
19
19
|
|
|
20
20
|
export function resolveConfig(input: QQChatConfigInput = {}): QQChatConfig {
|
|
21
21
|
const home = process.env.DSH_HOME || join(homedir(), '.dsh')
|
|
22
|
-
const replyFormat: ReplyFormat =
|
|
23
|
-
? input.replyFormat as ReplyFormat
|
|
24
|
-
: DEFAULT_CONFIG.replyFormat
|
|
22
|
+
const replyFormat: ReplyFormat = input.replyFormat === 'compat' ? 'compat' : 'markdown'
|
|
25
23
|
|
|
26
24
|
return Object.freeze({
|
|
27
25
|
dataDir: resolve(input.dataDir || join(home, 'plugins', 'dsh-qqchat')),
|
|
@@ -54,7 +52,6 @@ export function defaultRuntimeSettings(config: QQChatConfig): QQChatRuntimeSetti
|
|
|
54
52
|
groupReceiveMode,
|
|
55
53
|
groupReplyFormat: 'compat',
|
|
56
54
|
directReplyFormat: config.replyFormat,
|
|
57
|
-
directStreamingEnabled: config.replyFormat !== 'compat',
|
|
58
55
|
groupMembersCanUseTools: false,
|
|
59
56
|
groupMembersCanReceiveMedia: true,
|
|
60
57
|
groupMembersCanReadMedia: false,
|
package/src/gateway/api.ts
CHANGED
|
@@ -12,10 +12,6 @@ const API_BASE = 'https://api.sgroup.qq.com'
|
|
|
12
12
|
const SANDBOX_API_BASE = 'https://sandbox.api.sgroup.qq.com'
|
|
13
13
|
const TOKEN_URL = 'https://bots.qq.com/app/getAppAccessToken'
|
|
14
14
|
|
|
15
|
-
function looksLikeMarkdown(text: string): boolean {
|
|
16
|
-
return /(^|\n)\s{0,3}(#{1,6}\s|[-*+]\s|\d+\.\s|>\s)|\*\*|__|`|\[[^\]]+\]\([^\)]+\)/m.test(text)
|
|
17
|
-
}
|
|
18
|
-
|
|
19
15
|
interface CachedToken {
|
|
20
16
|
token: string
|
|
21
17
|
expiresAt: number
|
|
@@ -33,7 +29,7 @@ interface QQStreamBody {
|
|
|
33
29
|
input_mode: 'replace'
|
|
34
30
|
input_state: 1 | 10
|
|
35
31
|
index: number
|
|
36
|
-
content_type: '
|
|
32
|
+
content_type: 'markdown'
|
|
37
33
|
content_raw: string
|
|
38
34
|
msg_seq: number
|
|
39
35
|
event_id?: string
|
|
@@ -143,7 +139,7 @@ export class QQApiClient {
|
|
|
143
139
|
): Promise<Record<string, unknown>> {
|
|
144
140
|
const path = group ? `/v2/groups/${encodeURIComponent(targetId)}/messages` : `/v2/users/${encodeURIComponent(targetId)}/messages`
|
|
145
141
|
const seq = this.nextSeq(messageId)
|
|
146
|
-
const useMarkdown = format === 'markdown'
|
|
142
|
+
const useMarkdown = format === 'markdown'
|
|
147
143
|
const makeBody = (markdown: boolean): QQSendBody => ({
|
|
148
144
|
msg_type: markdown ? 2 : 0,
|
|
149
145
|
...(markdown ? { markdown: { content: text } } : { content: text }),
|
|
@@ -177,11 +173,10 @@ export class QQApiClient {
|
|
|
177
173
|
createPrivateTextStream(
|
|
178
174
|
account: AccountRow,
|
|
179
175
|
targetId: string,
|
|
180
|
-
{ messageId = null
|
|
176
|
+
{ messageId = null }: Pick<QQSendOptions, 'messageId'> = {},
|
|
181
177
|
): QQPrivateTextStream {
|
|
182
178
|
const path = `/v2/users/${encodeURIComponent(targetId)}/stream_messages`
|
|
183
179
|
const msgSeq = this.nextStreamSeq()
|
|
184
|
-
const contentType = format === 'markdown' ? 'markdown' : 'text'
|
|
185
180
|
let fullText = ''
|
|
186
181
|
let streamMessageId: string | undefined
|
|
187
182
|
let index = 0
|
|
@@ -196,7 +191,7 @@ export class QQApiClient {
|
|
|
196
191
|
// Build the request only when the queued task runs. The first
|
|
197
192
|
// response supplies stream_msg_id for all following frames.
|
|
198
193
|
const body: QQStreamBody = {
|
|
199
|
-
input_mode: 'replace', input_state: inputState, index: index++, content_type:
|
|
194
|
+
input_mode: 'replace', input_state: inputState, index: index++, content_type: 'markdown',
|
|
200
195
|
content_raw: content, msg_seq: msgSeq,
|
|
201
196
|
...(messageId ? { msg_id: messageId, event_id: messageId } : {}),
|
|
202
197
|
...(streamMessageId ? { stream_msg_id: streamMessageId } : {}),
|
|
@@ -258,7 +253,3 @@ export class QQApiClient {
|
|
|
258
253
|
async function safeJson<T>(response: Response): Promise<T | undefined> {
|
|
259
254
|
try { return await response.json() as T } catch { return undefined }
|
|
260
255
|
}
|
|
261
|
-
|
|
262
|
-
export function isReplyFormat(value: string): value is ReplyFormat {
|
|
263
|
-
return value === 'smart' || value === 'markdown' || value === 'compat'
|
|
264
|
-
}
|
|
@@ -148,15 +148,6 @@ export class DshQQBridge {
|
|
|
148
148
|
if (!existing && !createSession) return undefined
|
|
149
149
|
const { agent, sessionId } = await this.ensureAgent(event.chatType, row)
|
|
150
150
|
event.sessionId = sessionId
|
|
151
|
-
if (event.isOwner) {
|
|
152
|
-
// Keep owner messages as native user/message events even when they carry
|
|
153
|
-
// images. DSH now owns the image projection: visual routes receive the
|
|
154
|
-
// durable image block, while text-only routes receive its stable text
|
|
155
|
-
// placeholder. Falling back to a display-only event here would discard
|
|
156
|
-
// the owner's message semantics before DSH can apply that policy.
|
|
157
|
-
this.appendOwnerMessageIfMissing(agent.session, event)
|
|
158
|
-
return sessionId
|
|
159
|
-
}
|
|
160
151
|
this.appendDisplayIfMissing(agent.session, event)
|
|
161
152
|
return sessionId
|
|
162
153
|
}
|
|
@@ -371,23 +362,6 @@ export class DshQQBridge {
|
|
|
371
362
|
session.append('qqchat/message', displayEventPayload(event))
|
|
372
363
|
}
|
|
373
364
|
|
|
374
|
-
private appendOwnerMessageIfMissing(session: Session, event: QQChatDisplayEvent): void {
|
|
375
|
-
if (session.snapshotEvents().some(item => {
|
|
376
|
-
if (item.type !== 'user/message') return false
|
|
377
|
-
const source = item.data.source as unknown as { messageId?: string }
|
|
378
|
-
return source.messageId === event.messageId
|
|
379
|
-
})) return
|
|
380
|
-
session.append('user/message', createUserMessage({
|
|
381
|
-
source: {
|
|
382
|
-
kind: 'user', channel: 'qq', botId: 'qqchat', chatType: event.chatType,
|
|
383
|
-
chatId: event.chatId, senderId: event.senderId,
|
|
384
|
-
...(event.senderName ? { senderName: event.senderName } : {}),
|
|
385
|
-
messageId: event.messageId, mentioned: event.mentioned,
|
|
386
|
-
},
|
|
387
|
-
content: transcriptContent(event, this.db),
|
|
388
|
-
}), { surfaceOp: 'append' })
|
|
389
|
-
}
|
|
390
|
-
|
|
391
365
|
private ensureTitle(session: Session, chatType: ChatType, row: GroupRow | MemberRow): void {
|
|
392
366
|
try {
|
|
393
367
|
const getService = (this.ctx as unknown as { get?: (name: string) => unknown }).get
|
|
@@ -632,36 +606,6 @@ export function qqMessageContent(text: string, attachments: readonly StoredAttac
|
|
|
632
606
|
return content
|
|
633
607
|
}
|
|
634
608
|
|
|
635
|
-
function transcriptContent(event: QQChatDisplayEvent, db: QQChatDatabase): ContentBlock[] {
|
|
636
|
-
const attachments = event.attachments || []
|
|
637
|
-
const renderableImages = attachments.filter(attachment => attachment.kind === 'image' && db.attachmentById(attachment.id)?.imageRef)
|
|
638
|
-
const imagePlaceholderLines = new Set(renderableImages.map(attachment => `[图片] ${attachment.filename}`))
|
|
639
|
-
const quote = event.quote ? formatDisplayQuote(event) : ''
|
|
640
|
-
const body = event.content.split('\n').filter(line => !imagePlaceholderLines.has(line)).join('\n').trim()
|
|
641
|
-
const text = [quote, body].filter(Boolean).join('\n\n')
|
|
642
|
-
const content: ContentBlock[] = [{ type: 'text', text }]
|
|
643
|
-
for (const attachment of attachments) {
|
|
644
|
-
const imageRef = attachment.kind === 'image' ? db.attachmentById(attachment.id)?.imageRef : undefined
|
|
645
|
-
if (imageRef) {
|
|
646
|
-
content.push({ type: 'image', attachment: imageRef })
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
return content
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
function formatDisplayQuote(event: QQChatDisplayEvent): string {
|
|
653
|
-
const quote = event.quote
|
|
654
|
-
if (!quote) return ''
|
|
655
|
-
return [
|
|
656
|
-
'[引用消息]',
|
|
657
|
-
quote.senderId ? `发送人ID=${quote.senderId}` : '',
|
|
658
|
-
quote.senderName ? `显示名=${quote.senderName}` : '',
|
|
659
|
-
quote.messageId ? `消息ID=${quote.messageId}` : '',
|
|
660
|
-
`正文=${quote.text || '(空消息)'}`,
|
|
661
|
-
quote.attachments.length ? `附件=${quote.attachments.map(attachment => `${attachment.kind || 'file'}:${attachment.filename}${attachment.attachmentId ? ` (attachment_id=${attachment.attachmentId})` : ''}`).join('、')}` : '',
|
|
662
|
-
].filter(Boolean).join('\n')
|
|
663
|
-
}
|
|
664
|
-
|
|
665
609
|
function displayEventPayload(event: QQChatDisplayEvent): QQChatDisplayEvent {
|
|
666
610
|
const attachments = (event.attachments || []).map(attachment => ({
|
|
667
611
|
id: String(attachment.id), kind: attachment.kind, filename: String(attachment.filename),
|
package/src/session/runtime.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type { QQApiClient } from '../gateway/api.js'
|
|
|
9
9
|
import type { QQBindService } from '../gateway/auth.js'
|
|
10
10
|
import { QQGateway } from '../gateway/gateway.js'
|
|
11
11
|
import { renderQQMentionNames } from '../gateway/normalize.js'
|
|
12
|
+
import { qqCommandText } from '../commands/dispatch.js'
|
|
12
13
|
import { QQMediaStore } from '../media/store.js'
|
|
13
14
|
import type {
|
|
14
15
|
AccountRow, ChatTargetRow, ChatType, GroupReceiveMode, GroupRow, MemberRow, QQAttachmentInput, QQChatConfig,
|
|
@@ -52,9 +53,7 @@ export class QQChatRuntime {
|
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
settings(): QQChatRuntimeSettings {
|
|
55
|
-
|
|
56
|
-
if (settings.directReplyFormat === 'compat') settings.directStreamingEnabled = false
|
|
57
|
-
return settings
|
|
56
|
+
return this.db.runtimeSettings(defaultRuntimeSettings(this.config))
|
|
58
57
|
}
|
|
59
58
|
|
|
60
59
|
updateSettings(patch: QQChatRuntimeSettingsPatch): QQChatRuntimeSettings {
|
|
@@ -71,9 +70,7 @@ export class QQChatRuntime {
|
|
|
71
70
|
if (patch.directReplyFormat !== undefined) {
|
|
72
71
|
if (!isReplyFormat(patch.directReplyFormat)) throw new Error('无效的私聊消息兼容格式')
|
|
73
72
|
this.db.setSetting('directReplyFormat', patch.directReplyFormat)
|
|
74
|
-
if (patch.directReplyFormat === 'compat') this.db.setSetting('directStreamingEnabled', false)
|
|
75
73
|
}
|
|
76
|
-
if (patch.directStreamingEnabled !== undefined) this.db.setSetting('directStreamingEnabled', Boolean(patch.directStreamingEnabled))
|
|
77
74
|
if (patch.groupMembersCanUseTools !== undefined) this.db.setSetting('groupMembersCanUseTools', Boolean(patch.groupMembersCanUseTools))
|
|
78
75
|
if (patch.groupMembersCanReceiveMedia !== undefined) this.db.setSetting('groupMembersCanReceiveMedia', Boolean(patch.groupMembersCanReceiveMedia))
|
|
79
76
|
if (patch.groupMembersCanReadMedia !== undefined) this.db.setSetting('groupMembersCanReadMedia', Boolean(patch.groupMembersCanReadMedia))
|
|
@@ -252,22 +249,21 @@ export class QQChatRuntime {
|
|
|
252
249
|
}
|
|
253
250
|
}
|
|
254
251
|
|
|
255
|
-
if (
|
|
252
|
+
if (shouldRecordQQTranscript(shouldReply, message.text, message.mentioned)) {
|
|
256
253
|
await this.bridge.recordTranscript(displayEvent, row, true)
|
|
254
|
+
}
|
|
255
|
+
if (!shouldReply) {
|
|
257
256
|
return
|
|
258
257
|
}
|
|
259
258
|
|
|
260
259
|
try {
|
|
261
|
-
// Keep the incoming QQ message visible in Web independently from the
|
|
262
|
-
// notice-shaped message used to drive the Agent turn.
|
|
263
|
-
await this.bridge.recordTranscript(displayEvent, row, true)
|
|
264
260
|
const targetId = message.chatType === 'group' ? message.groupOpenid! : message.senderId
|
|
265
261
|
const sendOptions = {
|
|
266
262
|
group: message.chatType === 'group', messageId: message.messageId || null,
|
|
267
263
|
format: message.chatType === 'group' ? settings.groupReplyFormat : settings.directReplyFormat,
|
|
268
264
|
} as const
|
|
269
|
-
const stream = message.chatType === 'c2c' && settings.directReplyFormat
|
|
270
|
-
? this.api.createPrivateTextStream(account, targetId, sendOptions)
|
|
265
|
+
const stream = message.chatType === 'c2c' && settings.directReplyFormat === 'markdown'
|
|
266
|
+
? this.api.createPrivateTextStream(account, targetId, { messageId: sendOptions.messageId })
|
|
271
267
|
: undefined
|
|
272
268
|
const reply = await this.bridge.reply(message, group, member, storedAttachments, stream ? delta => stream.push(delta) : undefined, isOwner)
|
|
273
269
|
if (stream) {
|
|
@@ -320,11 +316,15 @@ export class QQChatRuntime {
|
|
|
320
316
|
}
|
|
321
317
|
|
|
322
318
|
function isGroupReceiveMode(value: unknown): value is GroupReceiveMode { return value === 'auto' || value === 'mention' || value === 'silent' }
|
|
323
|
-
function isReplyFormat(value: unknown): value is ReplyFormat { return value === '
|
|
319
|
+
function isReplyFormat(value: unknown): value is ReplyFormat { return value === 'markdown' || value === 'compat' }
|
|
324
320
|
export function shouldReplyToGroup(mode: GroupReceiveMode, group: Pick<GroupRow, 'enabled' | 'read_enabled'>, mentioned: boolean): boolean {
|
|
325
321
|
if (group.enabled !== 1 || group.read_enabled !== 1) return false
|
|
326
322
|
return mode === 'auto' || (mode === 'mention' && mentioned)
|
|
327
323
|
}
|
|
324
|
+
/** Record messages that do not enter the Agent turn, plus QQ commands handled outside Agent.followup. */
|
|
325
|
+
export function shouldRecordQQTranscript(shouldReply: boolean, text: string, mentioned: boolean): boolean {
|
|
326
|
+
return !shouldReply || qqCommandText(text, mentioned) !== undefined
|
|
327
|
+
}
|
|
328
328
|
function shortId(value: string): string { return value.length > 10 ? `…${value.slice(-10)}` : value }
|
|
329
329
|
function publicAttachments(attachments: StoredAttachmentSummary[]): StoredAttachmentSummary[] {
|
|
330
330
|
return attachments.map(attachment => ({
|
package/src/storage/db.ts
CHANGED
|
@@ -264,6 +264,9 @@ export class QQChatDatabase {
|
|
|
264
264
|
error TEXT
|
|
265
265
|
);
|
|
266
266
|
`)
|
|
267
|
+
this.db.prepare("UPDATE plugin_settings SET value='\"markdown\"',updated_at=? WHERE key IN ('replyFormat','groupReplyFormat','directReplyFormat') AND value='\"smart\"'")
|
|
268
|
+
.run(now())
|
|
269
|
+
this.db.prepare("DELETE FROM plugin_settings WHERE key='directStreamingEnabled'").run()
|
|
267
270
|
this.db.prepare("UPDATE reflection_tasks SET status='failed',updated_at=? WHERE status='running' AND updated_at<?")
|
|
268
271
|
.run(now(), now() - REFLECTION_TASK_STALE_MS)
|
|
269
272
|
const columns = new Set(many<{ name: string }>(this.db.prepare('PRAGMA table_info(group_members)').all()).map(column => column.name))
|
|
@@ -396,13 +399,16 @@ export class QQChatDatabase {
|
|
|
396
399
|
}
|
|
397
400
|
|
|
398
401
|
runtimeSettings(defaults: QQChatRuntimeSettings): QQChatRuntimeSettings {
|
|
402
|
+
const legacyReplyFormat = normalizeReplyFormat(
|
|
403
|
+
this.getSetting<unknown>('replyFormat', defaults.directReplyFormat),
|
|
404
|
+
defaults.directReplyFormat,
|
|
405
|
+
)
|
|
399
406
|
return {
|
|
400
407
|
memoryEnabled: this.getSetting('memoryEnabled', defaults.memoryEnabled),
|
|
401
408
|
memoryMemberBatchEnabled: this.getSetting('memoryMemberBatchEnabled', defaults.memoryMemberBatchEnabled),
|
|
402
409
|
groupReceiveMode: this.getSetting('groupReceiveMode', defaults.groupReceiveMode),
|
|
403
|
-
groupReplyFormat: this.getSetting('groupReplyFormat', defaults.groupReplyFormat),
|
|
404
|
-
directReplyFormat: this.getSetting('directReplyFormat',
|
|
405
|
-
directStreamingEnabled: this.getSetting('directStreamingEnabled', defaults.directStreamingEnabled),
|
|
410
|
+
groupReplyFormat: normalizeReplyFormat(this.getSetting<unknown>('groupReplyFormat', defaults.groupReplyFormat), defaults.groupReplyFormat),
|
|
411
|
+
directReplyFormat: normalizeReplyFormat(this.getSetting<unknown>('directReplyFormat', legacyReplyFormat), legacyReplyFormat),
|
|
406
412
|
groupMembersCanUseTools: this.getSetting('groupMembersCanUseTools', defaults.groupMembersCanUseTools),
|
|
407
413
|
groupMembersCanReceiveMedia: this.getSetting('groupMembersCanReceiveMedia', defaults.groupMembersCanReceiveMedia),
|
|
408
414
|
groupMembersCanReadMedia: this.getSetting('groupMembersCanReadMedia', defaults.groupMembersCanReadMedia),
|
|
@@ -828,3 +834,9 @@ export class QQChatDatabase {
|
|
|
828
834
|
this.db.prepare('UPDATE outbox SET status=?,sent_at=?,error=? WHERE id=?').run(error ? 'failed' : 'sent', now(), error, id)
|
|
829
835
|
}
|
|
830
836
|
}
|
|
837
|
+
|
|
838
|
+
function normalizeReplyFormat(value: unknown, fallback: QQChatRuntimeSettings['directReplyFormat']): QQChatRuntimeSettings['directReplyFormat'] {
|
|
839
|
+
if (value === 'markdown' || value === 'compat') return value
|
|
840
|
+
// Older releases persisted the automatic “smart” mode. Its explicit replacement is Markdown.
|
|
841
|
+
return value === 'smart' ? 'markdown' : fallback
|
|
842
|
+
}
|
package/src/transport/rpc.ts
CHANGED
|
@@ -311,9 +311,8 @@ function asSettingsPatch(value: unknown): QQChatRuntimeSettingsPatch {
|
|
|
311
311
|
if (record.groupReceiveMode === 'auto' || record.groupReceiveMode === 'mention' || record.groupReceiveMode === 'silent') {
|
|
312
312
|
patch.groupReceiveMode = record.groupReceiveMode
|
|
313
313
|
}
|
|
314
|
-
if (record.groupReplyFormat === '
|
|
315
|
-
if (record.directReplyFormat === '
|
|
316
|
-
if (typeof record.directStreamingEnabled === 'boolean') patch.directStreamingEnabled = record.directStreamingEnabled
|
|
314
|
+
if (record.groupReplyFormat === 'markdown' || record.groupReplyFormat === 'compat') patch.groupReplyFormat = record.groupReplyFormat
|
|
315
|
+
if (record.directReplyFormat === 'markdown' || record.directReplyFormat === 'compat') patch.directReplyFormat = record.directReplyFormat
|
|
317
316
|
if (typeof record.groupMembersCanUseTools === 'boolean') patch.groupMembersCanUseTools = record.groupMembersCanUseTools
|
|
318
317
|
if (typeof record.groupMembersCanReceiveMedia === 'boolean') patch.groupMembersCanReceiveMedia = record.groupMembersCanReceiveMedia
|
|
319
318
|
if (typeof record.groupMembersCanReadMedia === 'boolean') patch.groupMembersCanReadMedia = record.groupMembersCanReadMedia
|
package/src/types.ts
CHANGED
|
@@ -16,7 +16,7 @@ export interface DailyEntry {
|
|
|
16
16
|
date: string
|
|
17
17
|
note: string
|
|
18
18
|
}
|
|
19
|
-
export type ReplyFormat = '
|
|
19
|
+
export type ReplyFormat = 'markdown' | 'compat'
|
|
20
20
|
export type GroupReceiveMode = 'auto' | 'mention' | 'silent'
|
|
21
21
|
export type GatewayStatus = 'offline' | 'connecting' | 'online' | 'error'
|
|
22
22
|
export type LogLevel = 'debug' | 'info' | 'warn' | 'error'
|
|
@@ -65,7 +65,6 @@ export interface QQChatRuntimeSettings {
|
|
|
65
65
|
groupReceiveMode: GroupReceiveMode
|
|
66
66
|
groupReplyFormat: ReplyFormat
|
|
67
67
|
directReplyFormat: ReplyFormat
|
|
68
|
-
directStreamingEnabled: boolean
|
|
69
68
|
groupMembersCanUseTools: boolean
|
|
70
69
|
groupMembersCanReceiveMedia: boolean
|
|
71
70
|
groupMembersCanReadMedia: boolean
|
|
@@ -78,7 +77,6 @@ export interface QQChatRuntimeSettingsPatch {
|
|
|
78
77
|
groupReceiveMode?: GroupReceiveMode
|
|
79
78
|
groupReplyFormat?: ReplyFormat
|
|
80
79
|
directReplyFormat?: ReplyFormat
|
|
81
|
-
directStreamingEnabled?: boolean
|
|
82
80
|
groupMembersCanUseTools?: boolean
|
|
83
81
|
groupMembersCanReceiveMedia?: boolean
|
|
84
82
|
groupMembersCanReadMedia?: boolean
|