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 +8 -7
- package/README.md +6 -6
- package/client-src/plugin.cts +2 -2
- package/client-src/settings.cts +6 -19
- package/client-src/shared.cts +4 -4
- package/cordis.patch.yml +4 -0
- package/docs/ARCHITECTURE.en.md +1 -1
- package/docs/ARCHITECTURE.md +2 -1
- package/docs/DEVELOPMENT.md +4 -2
- package/docs/STREAMING.md +8 -18
- package/lib/client.js +7 -14
- package/lib/config.js +2 -5
- package/lib/gateway/api.d.ts +2 -3
- package/lib/gateway/api.js +3 -10
- package/lib/index.d.ts +2 -2
- package/lib/index.js +2 -2
- package/lib/session/agent-bridge.d.ts +3 -1
- package/lib/session/agent-bridge.js +43 -70
- package/lib/session/runtime.d.ts +2 -0
- package/lib/session/runtime.js +13 -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 +25 -24
- package/src/config.ts +2 -5
- package/src/gateway/api.ts +4 -13
- package/src/index.ts +2 -2
- package/src/session/agent-bridge.ts +41 -69
- package/src/session/runtime.ts +13 -12
- package/src/storage/db.ts +15 -3
- package/src/transport/rpc.ts +2 -3
- package/src/types.ts +1 -3
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
3
|
import { installModelSelection } from '@deepseek-ai/dsh-agent';
|
|
4
|
-
import {
|
|
4
|
+
import { agentPresetProjectionDefinition } from '@deepseek-ai/dsh-agent-presets';
|
|
5
5
|
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
6
6
|
import { SessionId } from '@deepseek-ai/dsh-session';
|
|
7
7
|
import '../shared/augmentations.js';
|
|
@@ -12,7 +12,10 @@ import { dispatchQQCommand, qqCommandText, slashCommandName } from '../commands/
|
|
|
12
12
|
import { MEMORY_SNAPSHOT_TTL_MS, memorySnapshotHash, restoreMemorySnapshotState, shouldRefreshMemorySnapshot } from './memory-snapshot.js';
|
|
13
13
|
const DSH_RUNTIME_CONTEXT_SOURCE = '@deepseek-ai/dsh-system-prompt';
|
|
14
14
|
export function resolveQQSessionPreset(header, events) {
|
|
15
|
-
|
|
15
|
+
let current = agentPresetProjectionDefinition.init(header);
|
|
16
|
+
for (const event of events)
|
|
17
|
+
current = agentPresetProjectionDefinition.apply(current, event);
|
|
18
|
+
return current || undefined;
|
|
16
19
|
}
|
|
17
20
|
const MEDIA_TOOL_NAMES = new Set(['qqchat_describe_image', 'qqchat_read_file', 'qqchat_media_info']);
|
|
18
21
|
const packageMetadata = createRequire(import.meta.url)('../../package.json');
|
|
@@ -34,6 +37,7 @@ export class DshQQBridge {
|
|
|
34
37
|
activeMediaReadable = new Map();
|
|
35
38
|
memorySnapshots = new Map();
|
|
36
39
|
disposeEvent;
|
|
40
|
+
disposeAssistantStream;
|
|
37
41
|
disposeToolGate;
|
|
38
42
|
disposeCommands;
|
|
39
43
|
disposeImageTool;
|
|
@@ -45,6 +49,11 @@ export class DshQQBridge {
|
|
|
45
49
|
this.config = config;
|
|
46
50
|
this.logger = logger;
|
|
47
51
|
this.disposeEvent = ctx.on('session/event', (session, event) => this.onSessionEvent(session, event));
|
|
52
|
+
this.disposeAssistantStream = ctx.on('agent/assistant-stream', ({ agent, frame }) => {
|
|
53
|
+
if (frame.type !== 'chunk' || frame.chunk.type !== 'text-delta' || !frame.chunk.text)
|
|
54
|
+
return;
|
|
55
|
+
this.pending.get(String(agent.id))?.onTextDelta?.(frame.chunk.text);
|
|
56
|
+
});
|
|
48
57
|
this.disposeToolGate = ctx.on('tools/pre-execute', async (exec, next) => {
|
|
49
58
|
const agent = exec.agent;
|
|
50
59
|
if (!agent)
|
|
@@ -71,8 +80,19 @@ export class DshQQBridge {
|
|
|
71
80
|
this.disposeMediaTools = registerQQMediaTools(ctx, db, canReadMedia);
|
|
72
81
|
this.disposeCommands = this.registerCommands();
|
|
73
82
|
}
|
|
83
|
+
async attachMappedSessionsToWorkspace() {
|
|
84
|
+
const sessionIds = [
|
|
85
|
+
...this.db.listGroups().map(row => row.dsh_session_id),
|
|
86
|
+
...this.db.listDirectChats().map(row => row.dsh_session_id),
|
|
87
|
+
];
|
|
88
|
+
for (const sessionId of sessionIds) {
|
|
89
|
+
if (sessionId)
|
|
90
|
+
await this.attachToWorkspace(sessionId);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
74
93
|
async dispose() {
|
|
75
94
|
this.disposeEvent();
|
|
95
|
+
this.disposeAssistantStream();
|
|
76
96
|
this.disposeToolGate();
|
|
77
97
|
this.disposeImageTool();
|
|
78
98
|
this.disposeMediaTools();
|
|
@@ -103,15 +123,6 @@ export class DshQQBridge {
|
|
|
103
123
|
return undefined;
|
|
104
124
|
const { agent, sessionId } = await this.ensureAgent(event.chatType, row);
|
|
105
125
|
event.sessionId = sessionId;
|
|
106
|
-
if (event.isOwner) {
|
|
107
|
-
// Keep owner messages as native user/message events even when they carry
|
|
108
|
-
// images. DSH now owns the image projection: visual routes receive the
|
|
109
|
-
// durable image block, while text-only routes receive its stable text
|
|
110
|
-
// placeholder. Falling back to a display-only event here would discard
|
|
111
|
-
// the owner's message semantics before DSH can apply that policy.
|
|
112
|
-
this.appendOwnerMessageIfMissing(agent.session, event);
|
|
113
|
-
return sessionId;
|
|
114
|
-
}
|
|
115
126
|
this.appendDisplayIfMissing(agent.session, event);
|
|
116
127
|
return sessionId;
|
|
117
128
|
}
|
|
@@ -212,13 +223,6 @@ export class DshQQBridge {
|
|
|
212
223
|
pending.text = text;
|
|
213
224
|
return;
|
|
214
225
|
}
|
|
215
|
-
if (event.type === 'assistant/chunk') {
|
|
216
|
-
const pending = this.pending.get(id);
|
|
217
|
-
const chunk = event.data.chunk;
|
|
218
|
-
if (pending?.onTextDelta && chunk.type === 'text-delta' && chunk.text)
|
|
219
|
-
pending.onTextDelta(chunk.text);
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
226
|
if (event.type === 'request/header') {
|
|
223
227
|
const { config } = event.data.header;
|
|
224
228
|
if (config.provider && config.model)
|
|
@@ -250,7 +254,7 @@ export class DshQQBridge {
|
|
|
250
254
|
this.memorySnapshots.set(sessionId, previous);
|
|
251
255
|
}
|
|
252
256
|
restoreMemorySnapshot(session) {
|
|
253
|
-
return restoreMemorySnapshotState(session.
|
|
257
|
+
return restoreMemorySnapshotState(session.snapshotEvents(), DSH_RUNTIME_CONTEXT_SOURCE);
|
|
254
258
|
}
|
|
255
259
|
async ensureAgent(chatType, row) {
|
|
256
260
|
let sessionId = this.db.getChatSession(chatType, Number(row.id));
|
|
@@ -273,6 +277,7 @@ export class DshQQBridge {
|
|
|
273
277
|
if (live) {
|
|
274
278
|
this.ensureSelection(live);
|
|
275
279
|
this.ensureTitle(live.session, chatType, row);
|
|
280
|
+
await this.attachToWorkspace(sessionId);
|
|
276
281
|
this.rememberRoute(chatType, row, live, sessionId);
|
|
277
282
|
return { agent: live, sessionId };
|
|
278
283
|
}
|
|
@@ -280,6 +285,7 @@ export class DshQQBridge {
|
|
|
280
285
|
if (resumed) {
|
|
281
286
|
this.ensureSelection(resumed.agent);
|
|
282
287
|
this.ensureTitle(resumed.agent.session, chatType, row);
|
|
288
|
+
await this.attachToWorkspace(sessionId);
|
|
283
289
|
this.rememberRoute(chatType, row, resumed.agent, sessionId);
|
|
284
290
|
return { agent: resumed.agent, sessionId };
|
|
285
291
|
}
|
|
@@ -297,6 +303,7 @@ export class DshQQBridge {
|
|
|
297
303
|
this.handles.set(sessionId, handle);
|
|
298
304
|
this.ensureSelection(handle.agent);
|
|
299
305
|
this.db.setChatSession(chatType, Number(row.id), sessionId);
|
|
306
|
+
await this.attachToWorkspace(sessionId);
|
|
300
307
|
this.ensureTitle(handle.agent.session, chatType, row);
|
|
301
308
|
this.rememberRoute(chatType, row, handle.agent, sessionId);
|
|
302
309
|
return { agent: handle.agent, sessionId };
|
|
@@ -305,6 +312,18 @@ export class DshQQBridge {
|
|
|
305
312
|
const registry = this.ctx.workspaceRegistry;
|
|
306
313
|
return registry?.archivedSessionIds.includes(sessionId) ?? false;
|
|
307
314
|
}
|
|
315
|
+
async attachToWorkspace(sessionId) {
|
|
316
|
+
const registry = this.ctx.workspaceRegistry;
|
|
317
|
+
if (!registry)
|
|
318
|
+
return;
|
|
319
|
+
try {
|
|
320
|
+
const workspace = await registry.resolveByPath(process.cwd());
|
|
321
|
+
await workspace?.attachSession(SessionId(sessionId));
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
this.logger.warn?.(`[dsh-qqchat] QQ session ${sessionId} 未能挂载到当前工作区: ${error instanceof Error ? error.message : String(error)}`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
308
327
|
rememberRoute(chatType, row, agent, sessionId) {
|
|
309
328
|
const provider = agent.options.provider || this.config.provider;
|
|
310
329
|
const model = agent.options.model || this.config.model;
|
|
@@ -316,7 +335,7 @@ export class DshQQBridge {
|
|
|
316
335
|
this.memory.setMemberRoute(Number(row.id), provider, model, sessionId);
|
|
317
336
|
}
|
|
318
337
|
appendDisplayIfMissing(session, event) {
|
|
319
|
-
if (session.
|
|
338
|
+
if (session.snapshotEvents().some(item => item.type === 'qqchat/message' && item.data.messageId === event.messageId))
|
|
320
339
|
return;
|
|
321
340
|
// Keep this on the public Session.append signature. Official DSH releases
|
|
322
341
|
// do not yet expose the optional ignorable envelope marker; if an older
|
|
@@ -324,24 +343,6 @@ export class DshQQBridge {
|
|
|
324
343
|
// fresh Session while SQLite remains the QQ history source of truth.
|
|
325
344
|
session.append('qqchat/message', displayEventPayload(event));
|
|
326
345
|
}
|
|
327
|
-
appendOwnerMessageIfMissing(session, event) {
|
|
328
|
-
if (session.events.some(item => {
|
|
329
|
-
if (item.type !== 'user/message')
|
|
330
|
-
return false;
|
|
331
|
-
const source = item.data.source;
|
|
332
|
-
return source.messageId === event.messageId;
|
|
333
|
-
}))
|
|
334
|
-
return;
|
|
335
|
-
session.append('user/message', createUserMessage({
|
|
336
|
-
source: {
|
|
337
|
-
kind: 'user', channel: 'qq', botId: 'qqchat', chatType: event.chatType,
|
|
338
|
-
chatId: event.chatId, senderId: event.senderId,
|
|
339
|
-
...(event.senderName ? { senderName: event.senderName } : {}),
|
|
340
|
-
messageId: event.messageId, mentioned: event.mentioned,
|
|
341
|
-
},
|
|
342
|
-
content: transcriptContent(event, this.db),
|
|
343
|
-
}), { surfaceOp: 'append' });
|
|
344
|
-
}
|
|
345
346
|
ensureTitle(session, chatType, row) {
|
|
346
347
|
try {
|
|
347
348
|
const getService = this.ctx.get;
|
|
@@ -506,7 +507,7 @@ export class DshQQBridge {
|
|
|
506
507
|
const available = await presets.list();
|
|
507
508
|
return { kind: 'success', text: available.length ? `可用 Agent Preset:\n${available.map(preset => `/qqpreset ${preset.id}`).join('\n')}` : '当前没有可用 Agent Preset。' };
|
|
508
509
|
}
|
|
509
|
-
if (agent.session.
|
|
510
|
+
if (agent.session.snapshotEvents().some(event => event.type === 'turn/start')) {
|
|
510
511
|
return { kind: 'error', text: '当前 Session 已经开始对话,DSH 不允许更换 Agent Preset。请先使用 /qqnew 开启新 Session。' };
|
|
511
512
|
}
|
|
512
513
|
try {
|
|
@@ -524,8 +525,9 @@ export class DshQQBridge {
|
|
|
524
525
|
}
|
|
525
526
|
statusCommand(agent) {
|
|
526
527
|
const current = this.selection(agent)?.current;
|
|
527
|
-
const
|
|
528
|
-
const
|
|
528
|
+
const events = agent.session.snapshotEvents();
|
|
529
|
+
const messages = events.filter(event => event.type === 'user/message' || event.type === 'assistant/message').length;
|
|
530
|
+
const last = events.at(-1);
|
|
529
531
|
return {
|
|
530
532
|
kind: 'success',
|
|
531
533
|
text: ['📊 Session 状态', `Session:${String(agent.id)}`, `状态:${agent.status === 'running' ? '生成中' : '空闲'}`, `模型:${current ? `${current.provider}/${current.model}` : '未知'}`, `消息数:${messages}`, `最后事件:${last?.type || '无'}`].join('\n'),
|
|
@@ -590,35 +592,6 @@ export function qqMessageContent(text, attachments) {
|
|
|
590
592
|
}
|
|
591
593
|
return content;
|
|
592
594
|
}
|
|
593
|
-
function transcriptContent(event, db) {
|
|
594
|
-
const attachments = event.attachments || [];
|
|
595
|
-
const renderableImages = attachments.filter(attachment => attachment.kind === 'image' && db.attachmentById(attachment.id)?.imageRef);
|
|
596
|
-
const imagePlaceholderLines = new Set(renderableImages.map(attachment => `[图片] ${attachment.filename}`));
|
|
597
|
-
const quote = event.quote ? formatDisplayQuote(event) : '';
|
|
598
|
-
const body = event.content.split('\n').filter(line => !imagePlaceholderLines.has(line)).join('\n').trim();
|
|
599
|
-
const text = [quote, body].filter(Boolean).join('\n\n');
|
|
600
|
-
const content = [{ type: 'text', text }];
|
|
601
|
-
for (const attachment of attachments) {
|
|
602
|
-
const imageRef = attachment.kind === 'image' ? db.attachmentById(attachment.id)?.imageRef : undefined;
|
|
603
|
-
if (imageRef) {
|
|
604
|
-
content.push({ type: 'image', attachment: imageRef });
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
return content;
|
|
608
|
-
}
|
|
609
|
-
function formatDisplayQuote(event) {
|
|
610
|
-
const quote = event.quote;
|
|
611
|
-
if (!quote)
|
|
612
|
-
return '';
|
|
613
|
-
return [
|
|
614
|
-
'[引用消息]',
|
|
615
|
-
quote.senderId ? `发送人ID=${quote.senderId}` : '',
|
|
616
|
-
quote.senderName ? `显示名=${quote.senderName}` : '',
|
|
617
|
-
quote.messageId ? `消息ID=${quote.messageId}` : '',
|
|
618
|
-
`正文=${quote.text || '(空消息)'}`,
|
|
619
|
-
quote.attachments.length ? `附件=${quote.attachments.map(attachment => `${attachment.kind || 'file'}:${attachment.filename}${attachment.attachmentId ? ` (attachment_id=${attachment.attachmentId})` : ''}`).join('、')}` : '',
|
|
620
|
-
].filter(Boolean).join('\n');
|
|
621
|
-
}
|
|
622
595
|
function displayEventPayload(event) {
|
|
623
596
|
const attachments = (event.attachments || []).map(attachment => ({
|
|
624
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;
|
|
@@ -28,6 +29,7 @@ export class QQChatRuntime {
|
|
|
28
29
|
this.media = media;
|
|
29
30
|
}
|
|
30
31
|
async start() {
|
|
32
|
+
await this.bridge.attachMappedSessionsToWorkspace();
|
|
31
33
|
for (const account of this.db.enabledAccounts())
|
|
32
34
|
this.startGateway(account);
|
|
33
35
|
this.outboxTimer = setInterval(() => void this.flushOutbox(), 3000);
|
|
@@ -51,10 +53,7 @@ export class QQChatRuntime {
|
|
|
51
53
|
this.db.close();
|
|
52
54
|
}
|
|
53
55
|
settings() {
|
|
54
|
-
|
|
55
|
-
if (settings.directReplyFormat === 'compat')
|
|
56
|
-
settings.directStreamingEnabled = false;
|
|
57
|
-
return settings;
|
|
56
|
+
return this.db.runtimeSettings(defaultRuntimeSettings(this.config));
|
|
58
57
|
}
|
|
59
58
|
updateSettings(patch) {
|
|
60
59
|
if (patch.memoryEnabled !== undefined)
|
|
@@ -75,11 +74,7 @@ export class QQChatRuntime {
|
|
|
75
74
|
if (!isReplyFormat(patch.directReplyFormat))
|
|
76
75
|
throw new Error('无效的私聊消息兼容格式');
|
|
77
76
|
this.db.setSetting('directReplyFormat', patch.directReplyFormat);
|
|
78
|
-
if (patch.directReplyFormat === 'compat')
|
|
79
|
-
this.db.setSetting('directStreamingEnabled', false);
|
|
80
77
|
}
|
|
81
|
-
if (patch.directStreamingEnabled !== undefined)
|
|
82
|
-
this.db.setSetting('directStreamingEnabled', Boolean(patch.directStreamingEnabled));
|
|
83
78
|
if (patch.groupMembersCanUseTools !== undefined)
|
|
84
79
|
this.db.setSetting('groupMembersCanUseTools', Boolean(patch.groupMembersCanUseTools));
|
|
85
80
|
if (patch.groupMembersCanReceiveMedia !== undefined)
|
|
@@ -272,21 +267,20 @@ export class QQChatRuntime {
|
|
|
272
267
|
this.memory.scheduleGroupMembers(Number(group.id));
|
|
273
268
|
}
|
|
274
269
|
}
|
|
275
|
-
if (
|
|
270
|
+
if (shouldRecordQQTranscript(shouldReply, message.text, message.mentioned)) {
|
|
276
271
|
await this.bridge.recordTranscript(displayEvent, row, true);
|
|
272
|
+
}
|
|
273
|
+
if (!shouldReply) {
|
|
277
274
|
return;
|
|
278
275
|
}
|
|
279
276
|
try {
|
|
280
|
-
// Keep the incoming QQ message visible in Web independently from the
|
|
281
|
-
// notice-shaped message used to drive the Agent turn.
|
|
282
|
-
await this.bridge.recordTranscript(displayEvent, row, true);
|
|
283
277
|
const targetId = message.chatType === 'group' ? message.groupOpenid : message.senderId;
|
|
284
278
|
const sendOptions = {
|
|
285
279
|
group: message.chatType === 'group', messageId: message.messageId || null,
|
|
286
280
|
format: message.chatType === 'group' ? settings.groupReplyFormat : settings.directReplyFormat,
|
|
287
281
|
};
|
|
288
|
-
const stream = message.chatType === 'c2c' && settings.directReplyFormat
|
|
289
|
-
? this.api.createPrivateTextStream(account, targetId, sendOptions)
|
|
282
|
+
const stream = message.chatType === 'c2c' && settings.directReplyFormat === 'markdown'
|
|
283
|
+
? this.api.createPrivateTextStream(account, targetId, { messageId: sendOptions.messageId })
|
|
290
284
|
: undefined;
|
|
291
285
|
const reply = await this.bridge.reply(message, group, member, storedAttachments, stream ? delta => stream.push(delta) : undefined, isOwner);
|
|
292
286
|
if (stream) {
|
|
@@ -344,12 +338,16 @@ export class QQChatRuntime {
|
|
|
344
338
|
}
|
|
345
339
|
}
|
|
346
340
|
function isGroupReceiveMode(value) { return value === 'auto' || value === 'mention' || value === 'silent'; }
|
|
347
|
-
function isReplyFormat(value) { return value === '
|
|
341
|
+
function isReplyFormat(value) { return value === 'markdown' || value === 'compat'; }
|
|
348
342
|
export function shouldReplyToGroup(mode, group, mentioned) {
|
|
349
343
|
if (group.enabled !== 1 || group.read_enabled !== 1)
|
|
350
344
|
return false;
|
|
351
345
|
return mode === 'auto' || (mode === 'mention' && mentioned);
|
|
352
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
|
+
}
|
|
353
351
|
function shortId(value) { return value.length > 10 ? `…${value.slice(-10)}` : value; }
|
|
354
352
|
function publicAttachments(attachments) {
|
|
355
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",
|
|
@@ -39,26 +40,25 @@
|
|
|
39
40
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
40
41
|
"test": "tsx --test tests/*.test.ts",
|
|
41
42
|
"check": "npm run typecheck && npm test && npm run build",
|
|
42
|
-
"prepublishOnly": "npm run check"
|
|
43
|
-
"prepare": "npm run build"
|
|
43
|
+
"prepublishOnly": "npm run check"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"qrcode": "^1.5.4"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
49
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
50
|
-
"@deepseek-ai/dsh-attachment": "^0.1.
|
|
51
|
-
"@deepseek-ai/dsh-agent": "^0.1.
|
|
52
|
-
"@deepseek-ai/dsh-agent-presets": "^0.1.
|
|
53
|
-
"@deepseek-ai/dsh-commands": "^0.1.
|
|
54
|
-
"@deepseek-ai/dsh-client-connection": "^0.1.
|
|
55
|
-
"@deepseek-ai/dsh-client-
|
|
56
|
-
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.
|
|
57
|
-
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.
|
|
58
|
-
"@deepseek-ai/dsh-client-ui-settings": "^0.1.
|
|
59
|
-
"@deepseek-ai/dsh-llm": "^0.1.
|
|
60
|
-
"@deepseek-ai/dsh-session": "^0.1.
|
|
61
|
-
"@deepseek-ai/dsh-tools": "^0.1.
|
|
50
|
+
"@deepseek-ai/dsh-attachment": "^0.1.6-alpha.1",
|
|
51
|
+
"@deepseek-ai/dsh-agent": "^0.1.6-alpha.1",
|
|
52
|
+
"@deepseek-ai/dsh-agent-presets": "^0.1.6-alpha.1",
|
|
53
|
+
"@deepseek-ai/dsh-commands": "^0.1.6-alpha.1",
|
|
54
|
+
"@deepseek-ai/dsh-client-connection": "^0.1.6-alpha.1",
|
|
55
|
+
"@deepseek-ai/dsh-client-modules": "^0.1.6-alpha.1",
|
|
56
|
+
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.6-alpha.1",
|
|
57
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.6-alpha.1",
|
|
58
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.1.6-alpha.1",
|
|
59
|
+
"@deepseek-ai/dsh-llm": "^0.1.6-alpha.1",
|
|
60
|
+
"@deepseek-ai/dsh-session": "^0.1.6-alpha.1",
|
|
61
|
+
"@deepseek-ai/dsh-tools": "^0.1.6-alpha.1"
|
|
62
62
|
},
|
|
63
63
|
"peerDependenciesMeta": {
|
|
64
64
|
"@deepseek-ai/dsh-client-connection": {
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"@deepseek-ai/dsh-client-ui-settings": {
|
|
68
68
|
"optional": true
|
|
69
69
|
},
|
|
70
|
-
"@deepseek-ai/dsh-client-
|
|
70
|
+
"@deepseek-ai/dsh-client-modules": {
|
|
71
71
|
"optional": true
|
|
72
72
|
},
|
|
73
73
|
"@deepseek-ai/dsh-client-ui-conversation": {
|
|
@@ -75,11 +75,12 @@
|
|
|
75
75
|
}
|
|
76
76
|
},
|
|
77
77
|
"devDependencies": {
|
|
78
|
-
"@deepseek-ai/dsh-attachment": "^0.1.
|
|
79
|
-
"@deepseek-ai/dsh-agent-default-model": "^0.1.
|
|
80
|
-
"@deepseek-ai/dsh-agent-presets": "^0.1.
|
|
81
|
-
"@deepseek-ai/dsh-commands": "^0.1.
|
|
82
|
-
"@deepseek-ai/dsh-client-connection": "^0.1.
|
|
78
|
+
"@deepseek-ai/dsh-attachment": "^0.1.6-alpha.1",
|
|
79
|
+
"@deepseek-ai/dsh-agent-default-model": "^0.1.6-alpha.1",
|
|
80
|
+
"@deepseek-ai/dsh-agent-presets": "^0.1.6-alpha.1",
|
|
81
|
+
"@deepseek-ai/dsh-commands": "^0.1.6-alpha.1",
|
|
82
|
+
"@deepseek-ai/dsh-client-connection": "^0.1.6-alpha.1",
|
|
83
|
+
"@deepseek-ai/dsh-client-modules": "^0.1.6-alpha.1",
|
|
83
84
|
"@types/node": "^22.18.0",
|
|
84
85
|
"@types/qrcode": "^1.5.5",
|
|
85
86
|
"@types/react": "^19.1.0",
|
|
@@ -94,7 +95,7 @@
|
|
|
94
95
|
"platform": "web",
|
|
95
96
|
"inject": [
|
|
96
97
|
"@deepseek-ai/dsh-client-connection",
|
|
97
|
-
"@deepseek-ai/dsh-client-
|
|
98
|
+
"@deepseek-ai/dsh-client-modules",
|
|
98
99
|
"@deepseek-ai/dsh-client-ui-conversation",
|
|
99
100
|
"@deepseek-ai/dsh-client-ui-settings"
|
|
100
101
|
]
|
|
@@ -102,7 +103,7 @@
|
|
|
102
103
|
},
|
|
103
104
|
"repository": {
|
|
104
105
|
"type": "git",
|
|
105
|
-
"url": "
|
|
106
|
+
"url": "https://github.com/Coffeiz/dsh-qqchat"
|
|
106
107
|
},
|
|
107
108
|
"keywords": [
|
|
108
109
|
"deepseek-harness",
|
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
|
-
}
|
package/src/index.ts
CHANGED
|
@@ -20,7 +20,7 @@ import type { LoggerLike, QQChatConfigInput } from './types.js'
|
|
|
20
20
|
const qqChatSessionEventRegistrations = await registerQQChatSessionEventType()
|
|
21
21
|
|
|
22
22
|
export const name = 'dsh-qqchat'
|
|
23
|
-
export const inject = ['connection', 'agents', 'agentDefaultModel', 'commands', 'llm', 'tools', 'workspaceRegistry'] as const
|
|
23
|
+
export const inject = ['connection', 'agents', 'agentDefaultModel', 'commands', 'llm', 'tools', 'webServer', 'workspaceRegistry'] as const
|
|
24
24
|
|
|
25
25
|
export function apply(ctx: Context, inputConfig: QQChatConfigInput = {}): void {
|
|
26
26
|
const config = resolveConfig(inputConfig)
|
|
@@ -38,7 +38,7 @@ export function apply(ctx: Context, inputConfig: QQChatConfigInput = {}): void {
|
|
|
38
38
|
const bridge = new DshQQBridge(ctx, db, memory, config, logger)
|
|
39
39
|
const runtime = new QQChatRuntime(ctx, db, api, auth, memory, bridge, config, logger, media)
|
|
40
40
|
|
|
41
|
-
ctx.connection.rpc.handle('/qqchat', createQQChatRpc(runtime)
|
|
41
|
+
ctx.connection.rpc.handle('/qqchat', createQQChatRpc(runtime))
|
|
42
42
|
ctx.effect(() => {
|
|
43
43
|
void (async () => {
|
|
44
44
|
await runtime.start()
|