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.
@@ -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 { resolveSessionPreset } from '@deepseek-ai/dsh-agent-presets';
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
- return resolveSessionPreset({ header, events });
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.events, DSH_RUNTIME_CONTEXT_SOURCE);
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.events.some(item => item.type === 'qqchat/message' && item.data.messageId === event.messageId))
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.events.some(event => event.type === 'turn/start')) {
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 messages = agent.session.events.filter(event => event.type === 'user/message' || event.type === 'assistant/message').length;
528
- const last = agent.session.events.at(-1);
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),
@@ -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[];
@@ -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
- const settings = this.db.runtimeSettings(defaultRuntimeSettings(this.config));
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 (!shouldReply) {
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 !== 'compat' && settings.directStreamingEnabled
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 === 'smart' || value === 'markdown' || value === 'compat'; }
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', this.getSetting('replyFormat', defaults.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
+ }
@@ -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 === 'smart' || record.groupReplyFormat === 'markdown' || record.groupReplyFormat === 'compat')
307
+ if (record.groupReplyFormat === 'markdown' || record.groupReplyFormat === 'compat')
308
308
  patch.groupReplyFormat = record.groupReplyFormat;
309
- if (record.directReplyFormat === 'smart' || record.directReplyFormat === 'markdown' || record.directReplyFormat === 'compat')
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 = 'smart' | 'markdown' | 'compat';
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.5",
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.1-rc.2",
51
- "@deepseek-ai/dsh-agent": "^0.1.1-rc.2",
52
- "@deepseek-ai/dsh-agent-presets": "^0.1.1-rc.2",
53
- "@deepseek-ai/dsh-commands": "^0.1.1-rc.2",
54
- "@deepseek-ai/dsh-client-connection": "^0.1.1-rc.2",
55
- "@deepseek-ai/dsh-client-runtime": "^0.1.1-rc.2",
56
- "@deepseek-ai/dsh-client-ui-conversation": "^0.1.1-rc.2",
57
- "@deepseek-ai/dsh-client-ui-primitives": "^0.1.1-rc.2",
58
- "@deepseek-ai/dsh-client-ui-settings": "^0.1.1-rc.2",
59
- "@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
60
- "@deepseek-ai/dsh-session": "^0.1.1-rc.2",
61
- "@deepseek-ai/dsh-tools": "^0.1.1-rc.2"
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-runtime": {
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.1-rc.2",
79
- "@deepseek-ai/dsh-agent-default-model": "^0.1.1-rc.2",
80
- "@deepseek-ai/dsh-agent-presets": "^0.1.1-rc.2",
81
- "@deepseek-ai/dsh-commands": "^0.1.1-rc.2",
82
- "@deepseek-ai/dsh-client-connection": "^0.1.1-rc.2",
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-runtime",
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": "git+ssh://git@github.com/Coffeiz/dsh-qqchat.git"
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: 'smart' as 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 = ['smart', 'markdown', 'compat'].includes(input.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,
@@ -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: 'text' | 'markdown'
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' || (format === 'smart' && looksLikeMarkdown(text))
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, format = this.config.replyFormat }: QQSendOptions = {},
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: contentType,
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), { authority: 'loopback' })
41
+ ctx.connection.rpc.handle('/qqchat', createQQChatRpc(runtime))
42
42
  ctx.effect(() => {
43
43
  void (async () => {
44
44
  await runtime.start()