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.
- package/LICENSE +21 -0
- package/README.en.md +276 -0
- package/README.md +211 -0
- package/client-src/plugin.cts +15 -0
- package/client-src/settings.cts +216 -0
- package/client-src/shared.cts +104 -0
- package/client-src/workspace.cts +164 -0
- package/cordis.patch.yml +7 -0
- package/docs/ARCHITECTURE.en.md +346 -0
- package/docs/ARCHITECTURE.md +411 -0
- package/docs/DEVELOPMENT.md +69 -0
- package/docs/MEDIA_AND_QUOTES.md +644 -0
- package/docs/MEMORY.md +99 -0
- package/docs/MEMORY_CONTEXT.md +254 -0
- package/docs/QUOTES.md +355 -0
- package/docs/README.md +22 -0
- package/docs/SECURITY.md +62 -0
- package/docs/STREAMING.md +347 -0
- package/docs/assets//346/211/253/347/240/201/351/223/276/346/216/245.jpg +0 -0
- package/docs/assets//347/276/244/350/201/212/347/225/214/351/235/242.jpg +0 -0
- package/docs/assets//350/256/260/345/277/206/347/263/273/347/273/237.jpg +0 -0
- package/docs/assets//350/256/276/347/275/256/347/225/214/351/235/242.jpg +0 -0
- package/docs/devlog.md +24 -0
- package/lib/client.js +433 -0
- package/lib/commands/dispatch.d.ts +16 -0
- package/lib/commands/dispatch.js +48 -0
- package/lib/config.d.ts +3 -0
- package/lib/config.js +63 -0
- package/lib/gateway/api.d.ts +30 -0
- package/lib/gateway/api.js +202 -0
- package/lib/gateway/auth.d.ts +35 -0
- package/lib/gateway/auth.js +80 -0
- package/lib/gateway/crypto.d.ts +2 -0
- package/lib/gateway/crypto.js +17 -0
- package/lib/gateway/gateway.d.ts +32 -0
- package/lib/gateway/gateway.js +219 -0
- package/lib/gateway/normalize.d.ts +6 -0
- package/lib/gateway/normalize.js +248 -0
- package/lib/index.d.ts +14 -0
- package/lib/index.js +35 -0
- package/lib/media/image-tool.d.ts +4 -0
- package/lib/media/image-tool.js +62 -0
- package/lib/media/media-tools.d.ts +3 -0
- package/lib/media/media-tools.js +68 -0
- package/lib/media/store.d.ts +14 -0
- package/lib/media/store.js +176 -0
- package/lib/session/agent-bridge.d.ts +56 -0
- package/lib/session/agent-bridge.js +613 -0
- package/lib/session/memory-snapshot.d.ts +23 -0
- package/lib/session/memory-snapshot.js +27 -0
- package/lib/session/runtime.d.ts +41 -0
- package/lib/session/runtime.js +395 -0
- package/lib/shared/augmentations.d.ts +40 -0
- package/lib/shared/augmentations.js +1 -0
- package/lib/shared/logging.d.ts +14 -0
- package/lib/shared/logging.js +41 -0
- package/lib/storage/db.d.ts +82 -0
- package/lib/storage/db.js +669 -0
- package/lib/storage/memory.d.ts +50 -0
- package/lib/storage/memory.js +566 -0
- package/lib/transport/rpc.d.ts +18 -0
- package/lib/transport/rpc.js +318 -0
- package/lib/types.d.ts +416 -0
- package/lib/types.js +1 -0
- package/package.json +115 -0
- package/scripts/wrap-client.mjs +20 -0
- package/src/commands/dispatch.ts +75 -0
- package/src/config.ts +70 -0
- package/src/gateway/api.ts +260 -0
- package/src/gateway/auth.ts +99 -0
- package/src/gateway/crypto.ts +16 -0
- package/src/gateway/gateway.ts +207 -0
- package/src/gateway/normalize.ts +236 -0
- package/src/index.ts +46 -0
- package/src/media/image-tool.ts +62 -0
- package/src/media/media-tools.ts +66 -0
- package/src/media/store.ts +157 -0
- package/src/session/agent-bridge.ts +624 -0
- package/src/session/memory-snapshot.ts +41 -0
- package/src/session/runtime.ts +368 -0
- package/src/shared/augmentations.ts +45 -0
- package/src/shared/logging.ts +36 -0
- package/src/storage/db.ts +760 -0
- package/src/storage/memory.ts +581 -0
- package/src/transport/rpc.ts +316 -0
- package/src/types.ts +456 -0
- package/tsconfig.build.json +12 -0
- package/tsconfig.client.json +14 -0
- 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
|
+
}
|