dsh-qqchat 0.2.5 → 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 CHANGED
@@ -6,8 +6,8 @@
6
6
 
7
7
  <p>
8
8
  <a href="./LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="MIT License"></a>
9
- <img src="https://img.shields.io/badge/DSH-0.1.1--rc.2-6f42c1.svg" alt="DSH 0.1.1-rc.2">
10
- <img src="https://img.shields.io/badge/QQChat-0.2.3-2ea44f.svg" alt="QQChat 0.2.3">
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.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
 
@@ -18,7 +18,7 @@
18
18
 
19
19
  <p><em>Chat in QQ, then view the complete Session, memory and tool activity in DSH Web.</em></p>
20
20
 
21
- <p>This release is verified against DSH <code>0.1.1-rc.2</code>. Install the plugin as-is; no DSH source patch or rebuild is required.</p>
21
+ <p>This release is verified against DSH <code>0.1.6-alpha.1</code>. Install the plugin as-is; no DSH source patch or rebuild is required.</p>
22
22
 
23
23
  `dsh-qqchat` is an official QQ Bot plugin for [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness). It maps QQ groups and direct chats to ordinary DSH Sessions and uses DSH's native Conversation, Agent, command and tool surfaces.
24
24
 
@@ -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 | Smart, Markdown or Plain compatibility | Group and direct-chat formats are configured separately; groups default to Plain compatibility |
40
- | Direct-chat streaming | Optionally uses QQ's official streaming message API for private chats | Private chats only; Plain compatibility disables it automatically; some clients may not display 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 |
@@ -126,7 +126,8 @@ npx @deepseek-ai/dsh plugin --profile qqchat add @deepseek-ai/dsh-web-app dsh-qq
126
126
  npx @deepseek-ai/dsh --profile qqchat
127
127
  ```
128
128
 
129
- Git installs build TypeScript through `prepare`. For local development:
129
+ Git installs use the committed `lib/` runtime artifacts, so they do not need to run
130
+ an install-time build. For local development:
130
131
 
131
132
  ```bash
132
133
  git clone git@github.com:Coffeiz/dsh-qqchat.git
@@ -145,7 +146,7 @@ Configure **Settings -> QQ Chat -> Message reception**:
145
146
  | Mention only | All messages are stored, but only messages mentioning the Bot trigger a reply |
146
147
  | Silent record | Stores messages and memory without actively replying |
147
148
 
148
- Groups default to Plain compatibility format. Group and direct-chat formats can be selected independently. Plain compatibility for direct chats automatically disables streaming.
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.
149
150
 
150
151
  ## Commands
151
152
 
package/README.md CHANGED
@@ -6,8 +6,8 @@
6
6
 
7
7
  <p>
8
8
  <a href="./LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="MIT License"></a>
9
- <img src="https://img.shields.io/badge/DSH-0.1.1--rc.2-6f42c1.svg" alt="DSH 0.1.1-rc.2">
10
- <img src="https://img.shields.io/badge/QQChat-0.2.3-2ea44f.svg" alt="QQChat 0.2.3">
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.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
 
@@ -19,7 +19,7 @@
19
19
 
20
20
  <p><em>dsh-qqchat 是 DeepSeek Harness(DSH)的 QQ Bot 插件,支持 QQ 群聊、私聊、会话隔离、记忆系统和 DSH 原生命令。</em></p>
21
21
 
22
- <p>当前版本基于 DSH <code>0.1.1-rc.2</code> 验证,安装插件即可使用,无需修改或重新构建 DSH。</p>
22
+ <p>当前版本基于 DSH <code>0.1.6-alpha.1</code> 验证,安装插件即可使用,无需修改或重新构建 DSH。</p>
23
23
 
24
24
  > 本项目是一个 Vibe Coding 项目,代码质量和工程完整性可能需要改进。如果你发现问题或有更好的实现建议,欢迎提交 [Issue](https://github.com/Coffeiz/dsh-qqchat/issues) 或 [Pull Request](https://github.com/Coffeiz/dsh-qqchat/pulls)。
25
25
 
@@ -34,8 +34,8 @@
34
34
  | 手动连接 | 在设置页填写 AppID 和 AppSecret 连接 QQ Bot | 适合无手机、服务器或扫码失败场景;密钥只提交到 Host |
35
35
  | 断线重连 | QQ Gateway 断线后自动恢复连接 | 无需重复扫码 |
36
36
  | 群聊回复模式 | 自动回应、@回复、静默记录 | 可在设置页随时切换 |
37
- | 消息格式兼容 | 智能兼容、Markdown、纯文本兼容 | 可兼容旧版本QQ的群友们! |
38
- | 私聊流式回复 | 私聊可选用 QQ 官方流式消息接口逐步更新回复 | 仅适用于私聊;纯文本兼容模式自动关闭;部分客户端可能不可见 |
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 客户端。私聊和群聊可以分别选择智能兼容、Markdown 或纯文本兼容。私聊使用纯文本兼容时会自动关闭流式传输。
124
+ 群聊默认使用纯文本兼容格式,适合不同 QQ 客户端。私聊和群聊可以分别选择 Markdown 或纯文本兼容。私聊选择 Markdown 时默认使用 QQ 官方流式传输;选择纯文本兼容时使用普通发送,不再提供单独的流式开关。
125
125
 
126
126
  ## 命令
127
127
 
@@ -3,12 +3,12 @@ import type { Ctx } from './shared.cjs'
3
3
  import { QQSettings } from './settings.cjs'
4
4
  import { QQSessionUtility, QQTranscriptNode, createQQMessageDefinition } from './workspace.cjs'
5
5
 
6
- exports.inject = ['slots', 'connection', 'conversationEvents'] as const
6
+ exports.inject = ['slots', 'connection', 'uiConversation'] as const
7
7
  exports.apply = function apply(ctx: Ctx) {
8
8
  setQQRpc(ctx.connection.rpc)
9
9
  ctx.effect(installStyles, 'dsh-qqchat: client styles')
10
10
  ctx.effect(installQQSettingsIcon, 'dsh-qqchat: QQ settings icon')
11
- ctx.conversationEvents.register(createQQMessageDefinition())
11
+ ctx.uiConversation.events.register(createQQMessageDefinition())
12
12
 
13
13
  ctx.slots.inject('settings.section', () => ctx.slots.register({ name: 'settings.section', id: 'qqchat', order: 35, label: () => 'QQ Chat', inject: () => ({ rpc: ctx.connection.rpc }) }, QQSettings))
14
14
  ctx.slots.inject('conversation.chat.node', () => ctx.slots.register({ name: 'conversation.chat.node', key: 'qqchat-message' }, QQTranscriptNode))
@@ -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 === 'markdown' ? 'Markdown' : value === 'compat' ? '纯文本兼容' : '智能兼容'
120
- const formatItems = [{ id: 'smart', label: '智能兼容' }, { id: 'markdown', label: 'Markdown' }, { id: 'compat', label: '纯文本兼容' }]
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' }, '私聊消息兼容格式'), h('div', { className: 'qqSettingHelp' }, '仅控制发送到 QQ 私聊时使用的消息格式。')),
199
- h('div', { className: 'qqControl' }, directReplyPicker)),
200
- h('div', { className: 'qqSetting' },
201
- h('label', { className: 'qqCheckRow' },
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, '记忆系统'),
@@ -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 = 'smart' | 'markdown' | 'compat'
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>> }
@@ -29,16 +29,16 @@ export interface SlotRegistry {
29
29
  inject(name: string, fn: () => unknown): unknown
30
30
  register<P>(options: Record<string, unknown>, component: import('react').ComponentType<P>): unknown
31
31
  }
32
- export interface ConversationEvents { register(definition: Record<string, unknown>): unknown }
32
+ export interface UiConversation { events: { register(definition: Record<string, unknown>): unknown } }
33
33
  export interface Ctx {
34
34
  connection: { rpc: Rpc }
35
35
  sessions: SessionsService
36
- conversationEvents: ConversationEvents
36
+ uiConversation: UiConversation
37
37
  effect(fn: () => (() => void) | void, label: string): unknown
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; directStreamingEnabled: boolean; groupMembersCanUseTools: boolean; groupMembersCanReceiveMedia: boolean; groupMembersCanReadMedia: boolean; ownerUserId: string }
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
@@ -5,3 +5,7 @@
5
5
  name: dsh-qqchat
6
6
  config:
7
7
  dataDir: !!js dshHomePath('plugins/dsh-qqchat')
8
+
9
+ # Connection RPC 需要 Web 启动配置和 Web Server 来配置 trusted hosts、注册 Host routes。
10
+ - id: connection
11
+ inject: [webStartup, webServer]
@@ -339,7 +339,7 @@ client-src/*.cts
339
339
  tests/*.test.ts
340
340
  ```
341
341
 
342
- The Host is built with `tsc`. The Client is compiled to temporary CJS and then wrapped by `scripts/wrap-client.mjs` as a DSH Client factory. `lib/` is generated output rather than source truth.
342
+ The Host is built with `tsc`. The Client is compiled to temporary CJS and then wrapped by `scripts/wrap-client.mjs` as a DSH Client factory. `lib/` is generated release output committed with fixed revisions so DSH Store can find the runtime entry without executing third-party install scripts; source remains the implementation truth.
343
343
 
344
344
  ## Development and security documentation
345
345
 
@@ -401,7 +401,8 @@ tests/*.test.ts
401
401
 
402
402
  Host 使用 `tsc`;Client 编译到临时 CJS 后由 `scripts/wrap-client.mjs` 包装成 DSH Client factory。
403
403
 
404
- `lib/` 是生成物,不作为源码真相。
404
+ `lib/` 是由源码生成的发布产物,并随固定 Commit 提交,以便 DSH Store 无需执行
405
+ 第三方安装脚本即可找到运行入口;源码仍是实现真相。
405
406
 
406
407
  ## 开发与安全文档
407
408
 
@@ -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 流式协议 | 插件调用 QQ 官方接口;失败时回退普通发送,纯文本兼容模式自动关闭 |
36
+ | QQ 私聊流式回复 | DSH 不负责 QQ Gateway 流式协议 | Markdown 格式自动使用 QQ 官方流式接口;纯文本兼容格式使用普通发送;失败时按已发送帧安全回退 |
37
37
  | QQ 引用、@和气泡 | DSH 不认识 QQ 协议字段 | 插件把引用和 @ 转为可读文本/元数据,并由 Client 做展示 |
38
38
  | DSH 原生命令 | DSH 只处理 DSH Web 自己收到的命令 | 插件将 `/qq...` 命令解析后转交或执行,避免与 DSH 原命令冲突 |
39
39
 
@@ -60,7 +60,9 @@ client-src/*.cts -> 临时 CJS Client
60
60
  临时 CJS -> lib/client.js DSH ModuleLoader factory
61
61
  ```
62
62
 
63
- `lib/` 和临时 Client 构建目录是生成物,不是源码真相。
63
+ `lib/` 是由构建生成的发布产物,同时提交到仓库以支持 DSH Store 从固定 Commit
64
+ 直接安装;修改源码后发布前必须重新构建并检查 `lib/` 的变更。临时 Client
65
+ 构建目录仍是本地生成物,不提交。
64
66
 
65
67
  ## 启动 DSH Web
66
68
 
package/docs/STREAMING.md CHANGED
@@ -11,7 +11,7 @@
11
11
  推荐采用以下结构:
12
12
 
13
13
  ```text
14
- DSH assistant/chunk
14
+ DSH `agent/assistant-stream` 的 chunk frame
15
15
  -> StreamingWriter
16
16
  -> 累计完整文本
17
17
  -> 节流定时器
@@ -118,7 +118,7 @@ interface StreamingWriterState {
118
118
 
119
119
  ### 2. 累计全文,不累计分片
120
120
 
121
- DSH 的 `assistant/chunk` 通常是增量文本:
121
+ DSH 的 `agent/assistant-stream` chunk frame 通常携带增量文本:
122
122
 
123
123
  ```text
124
124
  "你好"
@@ -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
- ```ts
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
- | 设置关闭 | 不访问 `stream_messages` |
326
+ | 私聊纯文本兼容 | 不访问 `stream_messages` |
337
327
  | 群聊 | 不访问 `stream_messages` |
338
328
 
339
329
  ## 当前实现位置
package/lib/client.js CHANGED
@@ -6,12 +6,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const shared_cjs_1 = require("./shared.cjs");
7
7
  const settings_cjs_1 = require("./settings.cjs");
8
8
  const workspace_cjs_1 = require("./workspace.cjs");
9
- exports.inject = ['slots', 'connection', 'conversationEvents'];
9
+ exports.inject = ['slots', 'connection', 'uiConversation'];
10
10
  exports.apply = function apply(ctx) {
11
11
  (0, shared_cjs_1.setQQRpc)(ctx.connection.rpc);
12
12
  ctx.effect(shared_cjs_1.installStyles, 'dsh-qqchat: client styles');
13
13
  ctx.effect(shared_cjs_1.installQQSettingsIcon, 'dsh-qqchat: QQ settings icon');
14
- ctx.conversationEvents.register((0, workspace_cjs_1.createQQMessageDefinition)());
14
+ ctx.uiConversation.events.register((0, workspace_cjs_1.createQQMessageDefinition)());
15
15
  ctx.slots.inject('settings.section', () => ctx.slots.register({ name: 'settings.section', id: 'qqchat', order: 35, label: () => 'QQ Chat', inject: () => ({ rpc: ctx.connection.rpc }) }, settings_cjs_1.QQSettings));
16
16
  ctx.slots.inject('conversation.chat.node', () => ctx.slots.register({ name: 'conversation.chat.node', key: 'qqchat-message' }, workspace_cjs_1.QQTranscriptNode));
17
17
  ctx.slots.inject('conversation.session.header.utilities', () => ctx.slots.register({ name: 'conversation.session.header.utilities', id: 'qqchat-memory', order: 30, inject: () => ({ rpc: ctx.connection.rpc }) }, workspace_cjs_1.QQSessionUtility));
@@ -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 === 'markdown' ? 'Markdown' : value === 'compat' ? '纯文本兼容' : '智能兼容';
186
- const formatItems = [{ id: 'smart', label: '智能兼容' }, { id: 'markdown', label: 'Markdown' }, { id: 'compat', label: '纯文本兼容' }];
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' }, '私聊消息兼容格式'), (0, shared_cjs_1.h)('div', { className: 'qqSettingHelp' }, '仅控制发送到 QQ 私聊时使用的消息格式。')), (0, shared_cjs_1.h)('div', { className: 'qqControl' }, directReplyPicker)), (0, shared_cjs_1.h)('div', { className: 'qqSetting' }, (0, shared_cjs_1.h)('label', { className: 'qqCheckRow' }, (0, shared_cjs_1.h)('input', {
239
- type: 'checkbox',
240
- checked: settings.directStreamingEnabled && settings.directReplyFormat !== 'compat',
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: 'smart',
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 = ['smart', 'markdown', 'compat'].includes(input.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,
@@ -1,5 +1,5 @@
1
1
  import type { QQChatDatabase } from '../storage/db.js';
2
- import type { AccountRow, QQChatConfig, QQSendOptions, ReplyFormat } from '../types.js';
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, format }?: QQSendOptions): QQPrivateTextStream;
28
+ createPrivateTextStream(account: AccountRow, targetId: string, { messageId }?: Pick<QQSendOptions, 'messageId'>): QQPrivateTextStream;
29
29
  }
30
- export declare function isReplyFormat(value: string): value is ReplyFormat;
@@ -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' || (format === 'smart' && looksLikeMarkdown(text));
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, format = this.config.replyFormat } = {}) {
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: contentType,
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
- }
package/lib/index.d.ts CHANGED
@@ -1,14 +1,14 @@
1
1
  import type { Context } from '@deepseek-ai/cordis';
2
2
  import type { QQChatConfigInput } from './types.js';
3
3
  export declare const name = "dsh-qqchat";
4
- export declare const inject: readonly ["connection", "agents", "agentDefaultModel", "commands", "llm", "tools", "workspaceRegistry"];
4
+ export declare const inject: readonly ["connection", "agents", "agentDefaultModel", "commands", "llm", "tools", "webServer", "workspaceRegistry"];
5
5
  export declare function apply(ctx: Context, inputConfig?: QQChatConfigInput): void;
6
6
  export type { QQChatConfig, QQChatConfigInput } from './types.js';
7
7
  export { QQChatDatabase } from './storage/db.js';
8
8
  export { normalizeQQDispatch } from './gateway/normalize.js';
9
9
  declare const _default: {
10
10
  name: string;
11
- inject: readonly ["connection", "agents", "agentDefaultModel", "commands", "llm", "tools", "workspaceRegistry"];
11
+ inject: readonly ["connection", "agents", "agentDefaultModel", "commands", "llm", "tools", "webServer", "workspaceRegistry"];
12
12
  apply: typeof apply;
13
13
  };
14
14
  export default _default;
package/lib/index.js CHANGED
@@ -13,7 +13,7 @@ import { QQChatRuntime } from './session/runtime.js';
13
13
  import { registerQQChatSessionEventType } from './session/registration.js';
14
14
  const qqChatSessionEventRegistrations = await registerQQChatSessionEventType();
15
15
  export const name = 'dsh-qqchat';
16
- export const inject = ['connection', 'agents', 'agentDefaultModel', 'commands', 'llm', 'tools', 'workspaceRegistry'];
16
+ export const inject = ['connection', 'agents', 'agentDefaultModel', 'commands', 'llm', 'tools', 'webServer', 'workspaceRegistry'];
17
17
  export function apply(ctx, inputConfig = {}) {
18
18
  const config = resolveConfig(inputConfig);
19
19
  mkdirSync(config.dataDir, { recursive: true, mode: 0o700 });
@@ -29,7 +29,7 @@ export function apply(ctx, inputConfig = {}) {
29
29
  const media = new QQMediaStore(db, ctx, logger);
30
30
  const bridge = new DshQQBridge(ctx, db, memory, config, logger);
31
31
  const runtime = new QQChatRuntime(ctx, db, api, auth, memory, bridge, config, logger, media);
32
- ctx.connection.rpc.handle('/qqchat', createQQChatRpc(runtime), { authority: 'loopback' });
32
+ ctx.connection.rpc.handle('/qqchat', createQQChatRpc(runtime));
33
33
  ctx.effect(() => {
34
34
  void (async () => {
35
35
  await runtime.start();
@@ -23,11 +23,13 @@ export declare class DshQQBridge {
23
23
  private readonly activeMediaReadable;
24
24
  private readonly memorySnapshots;
25
25
  private readonly disposeEvent;
26
+ private readonly disposeAssistantStream;
26
27
  private readonly disposeToolGate;
27
28
  private readonly disposeCommands;
28
29
  private readonly disposeImageTool;
29
30
  private readonly disposeMediaTools;
30
31
  constructor(ctx: Context, db: QQChatDatabase, memory: MemoryEngine, config: QQChatConfig, logger?: LoggerLike);
32
+ attachMappedSessionsToWorkspace(): Promise<void>;
31
33
  dispose(): Promise<void>;
32
34
  ensureChatSession(chatType: ChatType, row: GroupRow | MemberRow): Promise<string>;
33
35
  recordTranscript(event: QQChatDisplayEvent, row: GroupRow | MemberRow, createSession?: boolean): Promise<string | undefined>;
@@ -37,9 +39,9 @@ export declare class DshQQBridge {
37
39
  private restoreMemorySnapshot;
38
40
  private ensureAgent;
39
41
  private isArchived;
42
+ private attachToWorkspace;
40
43
  private rememberRoute;
41
44
  private appendDisplayIfMissing;
42
- private appendOwnerMessageIfMissing;
43
45
  private ensureTitle;
44
46
  private tryResume;
45
47
  private agentOptions;