dsh-qqchat 0.2.0

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.
Files changed (89) hide show
  1. package/LICENSE +21 -0
  2. package/README.en.md +276 -0
  3. package/README.md +211 -0
  4. package/client-src/plugin.cts +15 -0
  5. package/client-src/settings.cts +216 -0
  6. package/client-src/shared.cts +104 -0
  7. package/client-src/workspace.cts +164 -0
  8. package/cordis.patch.yml +7 -0
  9. package/docs/ARCHITECTURE.en.md +346 -0
  10. package/docs/ARCHITECTURE.md +411 -0
  11. package/docs/DEVELOPMENT.md +69 -0
  12. package/docs/MEDIA_AND_QUOTES.md +644 -0
  13. package/docs/MEMORY.md +99 -0
  14. package/docs/MEMORY_CONTEXT.md +254 -0
  15. package/docs/QUOTES.md +355 -0
  16. package/docs/README.md +22 -0
  17. package/docs/SECURITY.md +62 -0
  18. package/docs/STREAMING.md +347 -0
  19. package/docs/assets//346/211/253/347/240/201/351/223/276/346/216/245.jpg +0 -0
  20. package/docs/assets//347/276/244/350/201/212/347/225/214/351/235/242.jpg +0 -0
  21. package/docs/assets//350/256/260/345/277/206/347/263/273/347/273/237.jpg +0 -0
  22. package/docs/assets//350/256/276/347/275/256/347/225/214/351/235/242.jpg +0 -0
  23. package/docs/devlog.md +24 -0
  24. package/lib/client.js +433 -0
  25. package/lib/commands/dispatch.d.ts +16 -0
  26. package/lib/commands/dispatch.js +48 -0
  27. package/lib/config.d.ts +3 -0
  28. package/lib/config.js +63 -0
  29. package/lib/gateway/api.d.ts +30 -0
  30. package/lib/gateway/api.js +202 -0
  31. package/lib/gateway/auth.d.ts +35 -0
  32. package/lib/gateway/auth.js +80 -0
  33. package/lib/gateway/crypto.d.ts +2 -0
  34. package/lib/gateway/crypto.js +17 -0
  35. package/lib/gateway/gateway.d.ts +32 -0
  36. package/lib/gateway/gateway.js +219 -0
  37. package/lib/gateway/normalize.d.ts +6 -0
  38. package/lib/gateway/normalize.js +248 -0
  39. package/lib/index.d.ts +14 -0
  40. package/lib/index.js +35 -0
  41. package/lib/media/image-tool.d.ts +4 -0
  42. package/lib/media/image-tool.js +62 -0
  43. package/lib/media/media-tools.d.ts +3 -0
  44. package/lib/media/media-tools.js +68 -0
  45. package/lib/media/store.d.ts +14 -0
  46. package/lib/media/store.js +176 -0
  47. package/lib/session/agent-bridge.d.ts +56 -0
  48. package/lib/session/agent-bridge.js +613 -0
  49. package/lib/session/memory-snapshot.d.ts +23 -0
  50. package/lib/session/memory-snapshot.js +27 -0
  51. package/lib/session/runtime.d.ts +41 -0
  52. package/lib/session/runtime.js +395 -0
  53. package/lib/shared/augmentations.d.ts +40 -0
  54. package/lib/shared/augmentations.js +1 -0
  55. package/lib/shared/logging.d.ts +14 -0
  56. package/lib/shared/logging.js +41 -0
  57. package/lib/storage/db.d.ts +82 -0
  58. package/lib/storage/db.js +669 -0
  59. package/lib/storage/memory.d.ts +50 -0
  60. package/lib/storage/memory.js +566 -0
  61. package/lib/transport/rpc.d.ts +18 -0
  62. package/lib/transport/rpc.js +318 -0
  63. package/lib/types.d.ts +416 -0
  64. package/lib/types.js +1 -0
  65. package/package.json +115 -0
  66. package/scripts/wrap-client.mjs +20 -0
  67. package/src/commands/dispatch.ts +75 -0
  68. package/src/config.ts +70 -0
  69. package/src/gateway/api.ts +260 -0
  70. package/src/gateway/auth.ts +99 -0
  71. package/src/gateway/crypto.ts +16 -0
  72. package/src/gateway/gateway.ts +207 -0
  73. package/src/gateway/normalize.ts +236 -0
  74. package/src/index.ts +46 -0
  75. package/src/media/image-tool.ts +62 -0
  76. package/src/media/media-tools.ts +66 -0
  77. package/src/media/store.ts +157 -0
  78. package/src/session/agent-bridge.ts +624 -0
  79. package/src/session/memory-snapshot.ts +41 -0
  80. package/src/session/runtime.ts +368 -0
  81. package/src/shared/augmentations.ts +45 -0
  82. package/src/shared/logging.ts +36 -0
  83. package/src/storage/db.ts +760 -0
  84. package/src/storage/memory.ts +581 -0
  85. package/src/transport/rpc.ts +316 -0
  86. package/src/types.ts +456 -0
  87. package/tsconfig.build.json +12 -0
  88. package/tsconfig.client.json +14 -0
  89. package/tsconfig.json +23 -0
@@ -0,0 +1,316 @@
1
+ import type { ConnectionRpcHandler } from '@deepseek-ai/dsh-client-connection'
2
+ import type { QQChatRuntime } from '../session/runtime.js'
3
+ import type {
4
+ ChatType,
5
+ GroupListRow,
6
+ GroupPatch,
7
+ GroupRow,
8
+ MemoryDocuments,
9
+ MessageRow,
10
+ PublicAccountRow,
11
+ QQChatRuntimeSettingsPatch,
12
+ } from '../types.js'
13
+
14
+ type RpcSuccess<T> = { ok: true; value: T }
15
+ type RpcFailure = { ok: false; error: { code: 'internal'; message: string; details: Record<string, never> } }
16
+ export type QQChatRpcResult<T = unknown> = RpcSuccess<T> | RpcFailure
17
+ export type QQChatRpcHandler = ConnectionRpcHandler
18
+
19
+ const ok = <T>(value: T): RpcSuccess<T> => ({ ok: true, value })
20
+ const fail = (message: string): RpcFailure => ({ ok: false, error: { code: 'internal', message, details: {} } })
21
+
22
+ export function createQQChatRpc(runtime: QQChatRuntime): ConnectionRpcHandler {
23
+ return async (endpoint, payload) => {
24
+ try {
25
+ switch (endpoint) {
26
+ case 'status':
27
+ return ok({
28
+ accounts: runtime.db.publicAccounts().map(publicAccount),
29
+ databasePath: runtime.db.path,
30
+ settings: runtime.settings(),
31
+ config: {
32
+ agentPreset: runtime.config.agentPreset || null,
33
+ provider: runtime.config.provider || null,
34
+ model: runtime.config.model || null,
35
+ reflectionIdleMs: runtime.config.reflectionIdleMs,
36
+ reflectionBatchSize: runtime.config.reflectionBatchSize,
37
+ },
38
+ })
39
+ case 'auth/start':
40
+ return ok(await runtime.auth.start())
41
+ case 'auth/poll': {
42
+ const result = await runtime.auth.poll(requireString(payload, 'taskId'))
43
+ if (result.status === 'success') {
44
+ const account = runtime.db.accountById(Number(result.account.id))
45
+ runtime.startGateway(account)
46
+ }
47
+ return ok(result)
48
+ }
49
+ case 'account/reconnect': {
50
+ const id = requireNumber(payload, 'accountId')
51
+ runtime.db.setAccountEnabled(id, true)
52
+ await runtime.restartGateway(id)
53
+ return ok(true)
54
+ }
55
+ case 'account/disconnect': {
56
+ const id = requireNumber(payload, 'accountId')
57
+ runtime.db.setAccountEnabled(id, false)
58
+ await runtime.restartGateway(id)
59
+ return ok(true)
60
+ }
61
+ case 'settings/get':
62
+ return ok({
63
+ settings: runtime.settings(),
64
+ members: runtime.db.listKnownMembers().map(member => ({
65
+ id: Number(member.id),
66
+ platformUserId: member.platform_user_id,
67
+ displayName: member.display_name || '',
68
+ lastSeenAt: Number(member.last_seen_at),
69
+ })),
70
+ })
71
+ case 'settings/update':
72
+ return ok({ settings: runtime.updateSettings(asSettingsPatch(asRecord(payload)?.patch)) })
73
+ case 'logs/list': {
74
+ const limit = Math.max(1, Math.min(500, Number(asRecord(payload)?.limit || 200)))
75
+ return ok({ logs: runtime.logger.list(limit) })
76
+ }
77
+ case 'chats/list':
78
+ return ok({ chats: runtime.listChats().map(chat => ({
79
+ chatType: chat.chatType,
80
+ rowId: chat.rowId,
81
+ accountId: chat.accountId,
82
+ platformId: chat.platformId,
83
+ displayName: chat.displayName,
84
+ dshSessionId: chat.dshSessionId,
85
+ lastMessageAt: chat.lastMessageAt,
86
+ messageCount: chat.messageCount,
87
+ })) })
88
+ case 'chat/ensure': {
89
+ const chatType = requireChatType(payload)
90
+ const rowId = requireNumber(payload, 'rowId')
91
+ return ok({ sessionId: await runtime.ensureChatSession(chatType, rowId) })
92
+ }
93
+ case 'chat/send': {
94
+ const chatType = requireChatType(payload)
95
+ const rowId = requireNumber(payload, 'rowId')
96
+ const content = requireString(payload, 'content')
97
+ return ok({ sessionId: await runtime.sendActive(chatType, rowId, content) })
98
+ }
99
+ case 'attachment/read': {
100
+ const sessionId = requireString(payload, 'sessionId')
101
+ const attachmentId = requireString(payload, 'attachmentId')
102
+ return ok(await runtime.readAttachment(sessionId, attachmentId))
103
+ }
104
+ case 'chat/info': {
105
+ const sessionId = requireString(payload, 'sessionId')
106
+ const group = runtime.db.groupBySession(sessionId)
107
+ if (group) {
108
+ const view = runtime.memory.memoryView(Number(group.id))
109
+ if (!view) throw new Error('群不存在')
110
+ return ok({
111
+ chatType: 'group' as const,
112
+ rowId: Number(group.id),
113
+ title: group.name || `QQ群 ${shortId(group.platform_group_id)}`,
114
+ platformId: group.platform_group_id,
115
+ memory: normalizeMemory(view.groupMemory),
116
+ members: view.members.map(member => ({
117
+ id: Number(member.id),
118
+ platformUserId: member.platform_user_id,
119
+ displayName: member.display_name || '',
120
+ aliases: parseStringList(member.aliases_json),
121
+ nicknames: parseStringList(member.nicknames_json),
122
+ messageCount: Number(member.message_count || 0),
123
+ memory: normalizeMemory(member.memory),
124
+ })),
125
+ })
126
+ }
127
+ const member = runtime.db.memberBySession(sessionId)
128
+ if (!member) throw new Error('不是 QQ Chat session')
129
+ return ok({
130
+ chatType: 'c2c' as const,
131
+ rowId: Number(member.id),
132
+ title: member.display_name || `QQ 用户 ${shortId(member.platform_user_id)}`,
133
+ platformId: member.platform_user_id,
134
+ memory: normalizeMemory(runtime.db.memoryDocs('member', Number(member.id))),
135
+ members: [],
136
+ })
137
+ }
138
+ case 'groups/list':
139
+ return ok({ groups: runtime.db.listGroups().map(publicGroup) })
140
+ case 'group/get': {
141
+ const id = requireNumber(payload, 'groupId')
142
+ const view = runtime.memory.memoryView(id)
143
+ if (!view) throw new Error('群不存在')
144
+ return ok({
145
+ group: publicGroup(view.group),
146
+ groupMemory: normalizeMemory(view.groupMemory),
147
+ members: view.members.map(member => ({
148
+ id: Number(member.id),
149
+ platformUserId: member.platform_user_id,
150
+ displayName: member.display_name || '',
151
+ aliases: parseStringList(member.aliases_json),
152
+ nicknames: parseStringList(member.nicknames_json),
153
+ messageCount: Number(member.message_count || 0),
154
+ firstSeenAt: Number(member.first_seen_at),
155
+ lastSeenAt: Number(member.last_seen_at),
156
+ memory: normalizeMemory(member.memory),
157
+ })),
158
+ })
159
+ }
160
+ case 'group/messages': {
161
+ const id = requireNumber(payload, 'groupId')
162
+ const limit = Math.max(1, Math.min(300, Number(asRecord(payload)?.limit || 120)))
163
+ return ok({ messages: runtime.db.listMessages(id, limit).map(publicMessage) })
164
+ }
165
+ case 'group/update': {
166
+ const id = requireNumber(payload, 'groupId')
167
+ const patch = asGroupPatch(asRecord(payload)?.patch)
168
+ const row = runtime.db.updateGroup(id, patch)
169
+ if (!row) throw new Error('群不存在')
170
+ return ok(publicGroup(row))
171
+ }
172
+ case 'group/send': {
173
+ const id = requireNumber(payload, 'groupId')
174
+ const content = requireString(payload, 'content')
175
+ return ok({ sessionId: await runtime.sendActive('group', id, content) })
176
+ }
177
+ case 'group/reflect': {
178
+ if (!runtime.settings().memoryEnabled) throw new Error('记忆系统已关闭,请先在设置中重新启用')
179
+ const id = requireNumber(payload, 'groupId')
180
+ const view = await runtime.memory.reflectNow(id)
181
+ return ok({ groupMemory: normalizeMemory(view.groupMemory) })
182
+ }
183
+ default:
184
+ throw new Error(`未知 QQ Chat RPC endpoint: ${endpoint}`)
185
+ }
186
+ } catch (error) {
187
+ return fail(error instanceof Error ? error.message : String(error))
188
+ }
189
+ }
190
+ }
191
+
192
+ function publicAccount(row: PublicAccountRow) {
193
+ return {
194
+ id: Number(row.id), appId: row.app_id, botUserId: row.bot_user_id || null,
195
+ enabled: row.enabled === 1, sandbox: row.sandbox === 1,
196
+ gatewayStatus: row.gateway_status, gatewayLastError: row.gateway_last_error || null,
197
+ createdAt: Number(row.created_at), updatedAt: Number(row.updated_at),
198
+ }
199
+ }
200
+
201
+ function publicGroup(row: GroupRow | GroupListRow) {
202
+ const aggregate = row as Partial<GroupListRow>
203
+ return {
204
+ id: Number(row.id), accountId: Number(row.account_id), platformGroupId: row.platform_group_id,
205
+ name: row.name || '', enabled: row.enabled === 1,
206
+ readEnabled: row.read_enabled === 1, dshSessionId: row.dsh_session_id || null,
207
+ memberCount: Number(aggregate.member_count || 0), messageCount: Number(aggregate.message_count || 0),
208
+ lastMessageAt: aggregate.last_message_at ? Number(aggregate.last_message_at) : null,
209
+ }
210
+ }
211
+
212
+ function publicMessage(row: MessageRow) {
213
+ return {
214
+ id: Number(row.id), platformMessageId: row.platform_message_id || null,
215
+ direction: row.direction, content: row.content, quotedText: row.quoted_text || '',
216
+ mentioned: row.mentioned === 1, createdAt: Number(row.created_at),
217
+ senderId: row.platform_user_id || (row.direction === 'outbound' ? 'BOT' : ''),
218
+ senderName: row.display_name || (row.direction === 'outbound' ? 'DSH Agent' : ''),
219
+ attachments: parseAttachments(row.attachments_json),
220
+ quote: parseQuote(row.quote_json),
221
+ }
222
+ }
223
+
224
+ function parseAttachments(value: string | null): Array<Record<string, unknown>> {
225
+ if (!value) return []
226
+ try {
227
+ const parsed = JSON.parse(value)
228
+ if (!Array.isArray(parsed)) return []
229
+ return parsed.map(item => {
230
+ if (!item || typeof item !== 'object') return null
231
+ const { localPath: _localPath, ...publicRecord } = item as Record<string, unknown>
232
+ return publicRecord
233
+ }).filter((item): item is Record<string, unknown> => Boolean(item))
234
+ } catch { return [] }
235
+ }
236
+
237
+ function parseQuote(value: string | null): Record<string, unknown> | null {
238
+ if (!value) return null
239
+ try {
240
+ const parsed = JSON.parse(value)
241
+ if (!parsed || typeof parsed !== 'object') return null
242
+ const record = parsed as Record<string, unknown>
243
+ return { ...record, attachments: Array.isArray(record.attachments) ? record.attachments : [] }
244
+ } catch { return null }
245
+ }
246
+
247
+ function normalizeMemory(memory: MemoryDocuments) {
248
+ return {
249
+ profile: memory.profile || '', summary: memory.summary || '', daily: memory.daily || '',
250
+ memory: memory.memory || '', pattern: memory.pattern || '',
251
+ }
252
+ }
253
+
254
+ function parseStringList(value: string | null | undefined): string[] {
255
+ if (!value) return []
256
+ try {
257
+ const parsed = JSON.parse(value)
258
+ return Array.isArray(parsed) ? parsed.filter(item => typeof item === 'string') : []
259
+ } catch {
260
+ return []
261
+ }
262
+ }
263
+
264
+ function requireString(payload: unknown, key: string): string {
265
+ const value = asRecord(payload)?.[key]
266
+ if (typeof value !== 'string' || !value.trim()) throw new Error(`${key} 必须是非空字符串`)
267
+ return value.trim()
268
+ }
269
+
270
+ function requireNumber(payload: unknown, key: string): number {
271
+ const value = Number(asRecord(payload)?.[key])
272
+ if (!Number.isSafeInteger(value) || value <= 0) throw new Error(`${key} 必须是正整数`)
273
+ return value
274
+ }
275
+
276
+ function requireChatType(payload: unknown): ChatType {
277
+ const value = asRecord(payload)?.chatType
278
+ if (value !== 'group' && value !== 'c2c') throw new Error('chatType 必须是 group 或 c2c')
279
+ return value
280
+ }
281
+
282
+ function asRecord(value: unknown): Record<string, unknown> | undefined {
283
+ return typeof value === 'object' && value !== null ? value as Record<string, unknown> : undefined
284
+ }
285
+
286
+ function asGroupPatch(value: unknown): GroupPatch {
287
+ const record = asRecord(value)
288
+ if (!record) return {}
289
+ const patch: GroupPatch = {}
290
+ if (typeof record.name === 'string') patch.name = record.name
291
+ if (typeof record.enabled === 'boolean') patch.enabled = record.enabled
292
+ if (typeof record.readEnabled === 'boolean') patch.readEnabled = record.readEnabled
293
+ return patch
294
+ }
295
+
296
+ function asSettingsPatch(value: unknown): QQChatRuntimeSettingsPatch {
297
+ const record = asRecord(value)
298
+ if (!record) return {}
299
+ const patch: QQChatRuntimeSettingsPatch = {}
300
+ if (typeof record.memoryEnabled === 'boolean') patch.memoryEnabled = record.memoryEnabled
301
+ if (record.groupReceiveMode === 'auto' || record.groupReceiveMode === 'mention' || record.groupReceiveMode === 'silent') {
302
+ patch.groupReceiveMode = record.groupReceiveMode
303
+ }
304
+ if (record.groupReplyFormat === 'smart' || record.groupReplyFormat === 'markdown' || record.groupReplyFormat === 'compat') patch.groupReplyFormat = record.groupReplyFormat
305
+ if (record.directReplyFormat === 'smart' || record.directReplyFormat === 'markdown' || record.directReplyFormat === 'compat') patch.directReplyFormat = record.directReplyFormat
306
+ if (typeof record.directStreamingEnabled === 'boolean') patch.directStreamingEnabled = record.directStreamingEnabled
307
+ if (typeof record.groupMembersCanUseTools === 'boolean') patch.groupMembersCanUseTools = record.groupMembersCanUseTools
308
+ if (typeof record.groupMembersCanReceiveMedia === 'boolean') patch.groupMembersCanReceiveMedia = record.groupMembersCanReceiveMedia
309
+ if (typeof record.groupMembersCanReadMedia === 'boolean') patch.groupMembersCanReadMedia = record.groupMembersCanReadMedia
310
+ if (typeof record.ownerUserId === 'string') patch.ownerUserId = record.ownerUserId
311
+ return patch
312
+ }
313
+
314
+ function shortId(value: string): string {
315
+ return value.length > 10 ? `…${value.slice(-10)}` : value
316
+ }