dsh-qqchat 0.2.5 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -6,8 +6,8 @@
6
6
 
7
7
  <p>
8
8
  <a href="./LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="MIT License"></a>
9
- <img src="https://img.shields.io/badge/DSH-0.1.1--rc.2-6f42c1.svg" alt="DSH 0.1.1-rc.2">
10
- <img src="https://img.shields.io/badge/QQChat-0.2.3-2ea44f.svg" alt="QQChat 0.2.3">
9
+ <img src="https://img.shields.io/badge/DSH-0.1.6--alpha.1-6f42c1.svg" alt="DSH 0.1.6-alpha.1">
10
+ <img src="https://img.shields.io/badge/QQChat-0.2.6-2ea44f.svg" alt="QQChat 0.2.6">
11
11
  <img src="https://img.shields.io/badge/TypeScript-5.x-3178c6.svg" alt="TypeScript">
12
12
  </p>
13
13
 
@@ -18,7 +18,7 @@
18
18
 
19
19
  <p><em>Chat in QQ, then view the complete Session, memory and tool activity in DSH Web.</em></p>
20
20
 
21
- <p>This release is verified against DSH <code>0.1.1-rc.2</code>. Install the plugin as-is; no DSH source patch or rebuild is required.</p>
21
+ <p>This release is verified against DSH <code>0.1.6-alpha.1</code>. Install the plugin as-is; no DSH source patch or rebuild is required.</p>
22
22
 
23
23
  `dsh-qqchat` is an official QQ Bot plugin for [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness). It maps QQ groups and direct chats to ordinary DSH Sessions and uses DSH's native Conversation, Agent, command and tool surfaces.
24
24
 
@@ -126,7 +126,8 @@ npx @deepseek-ai/dsh plugin --profile qqchat add @deepseek-ai/dsh-web-app dsh-qq
126
126
  npx @deepseek-ai/dsh --profile qqchat
127
127
  ```
128
128
 
129
- Git installs build TypeScript through `prepare`. For local development:
129
+ Git installs use the committed `lib/` runtime artifacts, so they do not need to run
130
+ an install-time build. For local development:
130
131
 
131
132
  ```bash
132
133
  git clone git@github.com:Coffeiz/dsh-qqchat.git
package/README.md CHANGED
@@ -6,8 +6,8 @@
6
6
 
7
7
  <p>
8
8
  <a href="./LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="MIT License"></a>
9
- <img src="https://img.shields.io/badge/DSH-0.1.1--rc.2-6f42c1.svg" alt="DSH 0.1.1-rc.2">
10
- <img src="https://img.shields.io/badge/QQChat-0.2.3-2ea44f.svg" alt="QQChat 0.2.3">
9
+ <img src="https://img.shields.io/badge/DSH-0.1.6--alpha.1-6f42c1.svg" alt="DSH 0.1.6-alpha.1">
10
+ <img src="https://img.shields.io/badge/QQChat-0.2.6-2ea44f.svg" alt="QQChat 0.2.6">
11
11
  <img src="https://img.shields.io/badge/TypeScript-5.x-3178c6.svg" alt="TypeScript">
12
12
  </p>
13
13
 
@@ -19,7 +19,7 @@
19
19
 
20
20
  <p><em>dsh-qqchat 是 DeepSeek Harness(DSH)的 QQ Bot 插件,支持 QQ 群聊、私聊、会话隔离、记忆系统和 DSH 原生命令。</em></p>
21
21
 
22
- <p>当前版本基于 DSH <code>0.1.1-rc.2</code> 验证,安装插件即可使用,无需修改或重新构建 DSH。</p>
22
+ <p>当前版本基于 DSH <code>0.1.6-alpha.1</code> 验证,安装插件即可使用,无需修改或重新构建 DSH。</p>
23
23
 
24
24
  > 本项目是一个 Vibe Coding 项目,代码质量和工程完整性可能需要改进。如果你发现问题或有更好的实现建议,欢迎提交 [Issue](https://github.com/Coffeiz/dsh-qqchat/issues) 或 [Pull Request](https://github.com/Coffeiz/dsh-qqchat/pulls)。
25
25
 
@@ -3,12 +3,12 @@ import type { Ctx } from './shared.cjs'
3
3
  import { QQSettings } from './settings.cjs'
4
4
  import { QQSessionUtility, QQTranscriptNode, createQQMessageDefinition } from './workspace.cjs'
5
5
 
6
- exports.inject = ['slots', 'connection', 'conversationEvents'] as const
6
+ exports.inject = ['slots', 'connection', 'uiConversation'] as const
7
7
  exports.apply = function apply(ctx: Ctx) {
8
8
  setQQRpc(ctx.connection.rpc)
9
9
  ctx.effect(installStyles, 'dsh-qqchat: client styles')
10
10
  ctx.effect(installQQSettingsIcon, 'dsh-qqchat: QQ settings icon')
11
- ctx.conversationEvents.register(createQQMessageDefinition())
11
+ ctx.uiConversation.events.register(createQQMessageDefinition())
12
12
 
13
13
  ctx.slots.inject('settings.section', () => ctx.slots.register({ name: 'settings.section', id: 'qqchat', order: 35, label: () => 'QQ Chat', inject: () => ({ rpc: ctx.connection.rpc }) }, QQSettings))
14
14
  ctx.slots.inject('conversation.chat.node', () => ctx.slots.register({ name: 'conversation.chat.node', key: 'qqchat-message' }, QQTranscriptNode))
@@ -29,11 +29,11 @@ export interface SlotRegistry {
29
29
  inject(name: string, fn: () => unknown): unknown
30
30
  register<P>(options: Record<string, unknown>, component: import('react').ComponentType<P>): unknown
31
31
  }
32
- export interface ConversationEvents { register(definition: Record<string, unknown>): unknown }
32
+ export interface UiConversation { events: { register(definition: Record<string, unknown>): unknown } }
33
33
  export interface Ctx {
34
34
  connection: { rpc: Rpc }
35
35
  sessions: SessionsService
36
- conversationEvents: ConversationEvents
36
+ uiConversation: UiConversation
37
37
  effect(fn: () => (() => void) | void, label: string): unknown
38
38
  slots: SlotRegistry
39
39
  }
package/cordis.patch.yml CHANGED
@@ -5,3 +5,8 @@
5
5
  name: dsh-qqchat
6
6
  config:
7
7
  dataDir: !!js dshHomePath('plugins/dsh-qqchat')
8
+
9
+ # DSH 0.1.6's Connection RPC registry owns route lifetimes on the
10
+ # connection fiber, so that fiber must also receive the Web server service.
11
+ - id: connection
12
+ inject: [webRuntime, webServer]
@@ -339,7 +339,7 @@ client-src/*.cts
339
339
  tests/*.test.ts
340
340
  ```
341
341
 
342
- The Host is built with `tsc`. The Client is compiled to temporary CJS and then wrapped by `scripts/wrap-client.mjs` as a DSH Client factory. `lib/` is generated output rather than source truth.
342
+ The Host is built with `tsc`. The Client is compiled to temporary CJS and then wrapped by `scripts/wrap-client.mjs` as a DSH Client factory. `lib/` is generated release output committed with fixed revisions so DSH Store can find the runtime entry without executing third-party install scripts; source remains the implementation truth.
343
343
 
344
344
  ## Development and security documentation
345
345
 
@@ -401,7 +401,8 @@ tests/*.test.ts
401
401
 
402
402
  Host 使用 `tsc`;Client 编译到临时 CJS 后由 `scripts/wrap-client.mjs` 包装成 DSH Client factory。
403
403
 
404
- `lib/` 是生成物,不作为源码真相。
404
+ `lib/` 是由源码生成的发布产物,并随固定 Commit 提交,以便 DSH Store 无需执行
405
+ 第三方安装脚本即可找到运行入口;源码仍是实现真相。
405
406
 
406
407
  ## 开发与安全文档
407
408
 
@@ -60,7 +60,9 @@ client-src/*.cts -> 临时 CJS Client
60
60
  临时 CJS -> lib/client.js DSH ModuleLoader factory
61
61
  ```
62
62
 
63
- `lib/` 和临时 Client 构建目录是生成物,不是源码真相。
63
+ `lib/` 是由构建生成的发布产物,同时提交到仓库以支持 DSH Store 从固定 Commit
64
+ 直接安装;修改源码后发布前必须重新构建并检查 `lib/` 的变更。临时 Client
65
+ 构建目录仍是本地生成物,不提交。
64
66
 
65
67
  ## 启动 DSH Web
66
68
 
package/docs/STREAMING.md CHANGED
@@ -11,7 +11,7 @@
11
11
  推荐采用以下结构:
12
12
 
13
13
  ```text
14
- DSH assistant/chunk
14
+ DSH `agent/assistant-stream` 的 chunk frame
15
15
  -> StreamingWriter
16
16
  -> 累计完整文本
17
17
  -> 节流定时器
@@ -118,7 +118,7 @@ interface StreamingWriterState {
118
118
 
119
119
  ### 2. 累计全文,不累计分片
120
120
 
121
- DSH 的 `assistant/chunk` 通常是增量文本:
121
+ DSH 的 `agent/assistant-stream` chunk frame 通常携带增量文本:
122
122
 
123
123
  ```text
124
124
  "你好"
package/lib/client.js CHANGED
@@ -6,12 +6,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const shared_cjs_1 = require("./shared.cjs");
7
7
  const settings_cjs_1 = require("./settings.cjs");
8
8
  const workspace_cjs_1 = require("./workspace.cjs");
9
- exports.inject = ['slots', 'connection', 'conversationEvents'];
9
+ exports.inject = ['slots', 'connection', 'uiConversation'];
10
10
  exports.apply = function apply(ctx) {
11
11
  (0, shared_cjs_1.setQQRpc)(ctx.connection.rpc);
12
12
  ctx.effect(shared_cjs_1.installStyles, 'dsh-qqchat: client styles');
13
13
  ctx.effect(shared_cjs_1.installQQSettingsIcon, 'dsh-qqchat: QQ settings icon');
14
- ctx.conversationEvents.register((0, workspace_cjs_1.createQQMessageDefinition)());
14
+ ctx.uiConversation.events.register((0, workspace_cjs_1.createQQMessageDefinition)());
15
15
  ctx.slots.inject('settings.section', () => ctx.slots.register({ name: 'settings.section', id: 'qqchat', order: 35, label: () => 'QQ Chat', inject: () => ({ rpc: ctx.connection.rpc }) }, settings_cjs_1.QQSettings));
16
16
  ctx.slots.inject('conversation.chat.node', () => ctx.slots.register({ name: 'conversation.chat.node', key: 'qqchat-message' }, workspace_cjs_1.QQTranscriptNode));
17
17
  ctx.slots.inject('conversation.session.header.utilities', () => ctx.slots.register({ name: 'conversation.session.header.utilities', id: 'qqchat-memory', order: 30, inject: () => ({ rpc: ctx.connection.rpc }) }, workspace_cjs_1.QQSessionUtility));
package/lib/index.d.ts CHANGED
@@ -1,14 +1,14 @@
1
1
  import type { Context } from '@deepseek-ai/cordis';
2
2
  import type { QQChatConfigInput } from './types.js';
3
3
  export declare const name = "dsh-qqchat";
4
- export declare const inject: readonly ["connection", "agents", "agentDefaultModel", "commands", "llm", "tools", "workspaceRegistry"];
4
+ export declare const inject: readonly ["connection", "agents", "agentDefaultModel", "commands", "llm", "tools", "webServer", "workspaceRegistry"];
5
5
  export declare function apply(ctx: Context, inputConfig?: QQChatConfigInput): void;
6
6
  export type { QQChatConfig, QQChatConfigInput } from './types.js';
7
7
  export { QQChatDatabase } from './storage/db.js';
8
8
  export { normalizeQQDispatch } from './gateway/normalize.js';
9
9
  declare const _default: {
10
10
  name: string;
11
- inject: readonly ["connection", "agents", "agentDefaultModel", "commands", "llm", "tools", "workspaceRegistry"];
11
+ inject: readonly ["connection", "agents", "agentDefaultModel", "commands", "llm", "tools", "webServer", "workspaceRegistry"];
12
12
  apply: typeof apply;
13
13
  };
14
14
  export default _default;
package/lib/index.js CHANGED
@@ -13,7 +13,7 @@ import { QQChatRuntime } from './session/runtime.js';
13
13
  import { registerQQChatSessionEventType } from './session/registration.js';
14
14
  const qqChatSessionEventRegistrations = await registerQQChatSessionEventType();
15
15
  export const name = 'dsh-qqchat';
16
- export const inject = ['connection', 'agents', 'agentDefaultModel', 'commands', 'llm', 'tools', 'workspaceRegistry'];
16
+ export const inject = ['connection', 'agents', 'agentDefaultModel', 'commands', 'llm', 'tools', 'webServer', 'workspaceRegistry'];
17
17
  export function apply(ctx, inputConfig = {}) {
18
18
  const config = resolveConfig(inputConfig);
19
19
  mkdirSync(config.dataDir, { recursive: true, mode: 0o700 });
@@ -29,7 +29,7 @@ export function apply(ctx, inputConfig = {}) {
29
29
  const media = new QQMediaStore(db, ctx, logger);
30
30
  const bridge = new DshQQBridge(ctx, db, memory, config, logger);
31
31
  const runtime = new QQChatRuntime(ctx, db, api, auth, memory, bridge, config, logger, media);
32
- ctx.connection.rpc.handle('/qqchat', createQQChatRpc(runtime), { authority: 'loopback' });
32
+ ctx.connection.rpc.handle('/qqchat', createQQChatRpc(runtime));
33
33
  ctx.effect(() => {
34
34
  void (async () => {
35
35
  await runtime.start();
@@ -23,11 +23,13 @@ export declare class DshQQBridge {
23
23
  private readonly activeMediaReadable;
24
24
  private readonly memorySnapshots;
25
25
  private readonly disposeEvent;
26
+ private readonly disposeAssistantStream;
26
27
  private readonly disposeToolGate;
27
28
  private readonly disposeCommands;
28
29
  private readonly disposeImageTool;
29
30
  private readonly disposeMediaTools;
30
31
  constructor(ctx: Context, db: QQChatDatabase, memory: MemoryEngine, config: QQChatConfig, logger?: LoggerLike);
32
+ attachMappedSessionsToWorkspace(): Promise<void>;
31
33
  dispose(): Promise<void>;
32
34
  ensureChatSession(chatType: ChatType, row: GroupRow | MemberRow): Promise<string>;
33
35
  recordTranscript(event: QQChatDisplayEvent, row: GroupRow | MemberRow, createSession?: boolean): Promise<string | undefined>;
@@ -37,6 +39,7 @@ export declare class DshQQBridge {
37
39
  private restoreMemorySnapshot;
38
40
  private ensureAgent;
39
41
  private isArchived;
42
+ private attachToWorkspace;
40
43
  private rememberRoute;
41
44
  private appendDisplayIfMissing;
42
45
  private appendOwnerMessageIfMissing;
@@ -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();
@@ -212,13 +232,6 @@ export class DshQQBridge {
212
232
  pending.text = text;
213
233
  return;
214
234
  }
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
235
  if (event.type === 'request/header') {
223
236
  const { config } = event.data.header;
224
237
  if (config.provider && config.model)
@@ -250,7 +263,7 @@ export class DshQQBridge {
250
263
  this.memorySnapshots.set(sessionId, previous);
251
264
  }
252
265
  restoreMemorySnapshot(session) {
253
- return restoreMemorySnapshotState(session.events, DSH_RUNTIME_CONTEXT_SOURCE);
266
+ return restoreMemorySnapshotState(session.snapshotEvents(), DSH_RUNTIME_CONTEXT_SOURCE);
254
267
  }
255
268
  async ensureAgent(chatType, row) {
256
269
  let sessionId = this.db.getChatSession(chatType, Number(row.id));
@@ -273,6 +286,7 @@ export class DshQQBridge {
273
286
  if (live) {
274
287
  this.ensureSelection(live);
275
288
  this.ensureTitle(live.session, chatType, row);
289
+ await this.attachToWorkspace(sessionId);
276
290
  this.rememberRoute(chatType, row, live, sessionId);
277
291
  return { agent: live, sessionId };
278
292
  }
@@ -280,6 +294,7 @@ export class DshQQBridge {
280
294
  if (resumed) {
281
295
  this.ensureSelection(resumed.agent);
282
296
  this.ensureTitle(resumed.agent.session, chatType, row);
297
+ await this.attachToWorkspace(sessionId);
283
298
  this.rememberRoute(chatType, row, resumed.agent, sessionId);
284
299
  return { agent: resumed.agent, sessionId };
285
300
  }
@@ -297,6 +312,7 @@ export class DshQQBridge {
297
312
  this.handles.set(sessionId, handle);
298
313
  this.ensureSelection(handle.agent);
299
314
  this.db.setChatSession(chatType, Number(row.id), sessionId);
315
+ await this.attachToWorkspace(sessionId);
300
316
  this.ensureTitle(handle.agent.session, chatType, row);
301
317
  this.rememberRoute(chatType, row, handle.agent, sessionId);
302
318
  return { agent: handle.agent, sessionId };
@@ -305,6 +321,18 @@ export class DshQQBridge {
305
321
  const registry = this.ctx.workspaceRegistry;
306
322
  return registry?.archivedSessionIds.includes(sessionId) ?? false;
307
323
  }
324
+ async attachToWorkspace(sessionId) {
325
+ const registry = this.ctx.workspaceRegistry;
326
+ if (!registry)
327
+ return;
328
+ try {
329
+ const workspace = await registry.resolveByPath(process.cwd());
330
+ await workspace?.attachSession(SessionId(sessionId));
331
+ }
332
+ catch (error) {
333
+ this.logger.warn?.(`[dsh-qqchat] QQ session ${sessionId} 未能挂载到当前工作区: ${error instanceof Error ? error.message : String(error)}`);
334
+ }
335
+ }
308
336
  rememberRoute(chatType, row, agent, sessionId) {
309
337
  const provider = agent.options.provider || this.config.provider;
310
338
  const model = agent.options.model || this.config.model;
@@ -316,7 +344,7 @@ export class DshQQBridge {
316
344
  this.memory.setMemberRoute(Number(row.id), provider, model, sessionId);
317
345
  }
318
346
  appendDisplayIfMissing(session, event) {
319
- if (session.events.some(item => item.type === 'qqchat/message' && item.data.messageId === event.messageId))
347
+ if (session.snapshotEvents().some(item => item.type === 'qqchat/message' && item.data.messageId === event.messageId))
320
348
  return;
321
349
  // Keep this on the public Session.append signature. Official DSH releases
322
350
  // do not yet expose the optional ignorable envelope marker; if an older
@@ -325,7 +353,7 @@ export class DshQQBridge {
325
353
  session.append('qqchat/message', displayEventPayload(event));
326
354
  }
327
355
  appendOwnerMessageIfMissing(session, event) {
328
- if (session.events.some(item => {
356
+ if (session.snapshotEvents().some(item => {
329
357
  if (item.type !== 'user/message')
330
358
  return false;
331
359
  const source = item.data.source;
@@ -506,7 +534,7 @@ export class DshQQBridge {
506
534
  const available = await presets.list();
507
535
  return { kind: 'success', text: available.length ? `可用 Agent Preset:\n${available.map(preset => `/qqpreset ${preset.id}`).join('\n')}` : '当前没有可用 Agent Preset。' };
508
536
  }
509
- if (agent.session.events.some(event => event.type === 'turn/start')) {
537
+ if (agent.session.snapshotEvents().some(event => event.type === 'turn/start')) {
510
538
  return { kind: 'error', text: '当前 Session 已经开始对话,DSH 不允许更换 Agent Preset。请先使用 /qqnew 开启新 Session。' };
511
539
  }
512
540
  try {
@@ -524,8 +552,9 @@ export class DshQQBridge {
524
552
  }
525
553
  statusCommand(agent) {
526
554
  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);
555
+ const events = agent.session.snapshotEvents();
556
+ const messages = events.filter(event => event.type === 'user/message' || event.type === 'assistant/message').length;
557
+ const last = events.at(-1);
529
558
  return {
530
559
  kind: 'success',
531
560
  text: ['📊 Session 状态', `Session:${String(agent.id)}`, `状态:${agent.status === 'running' ? '生成中' : '空闲'}`, `模型:${current ? `${current.provider}/${current.model}` : '未知'}`, `消息数:${messages}`, `最后事件:${last?.type || '无'}`].join('\n'),
@@ -28,6 +28,7 @@ export class QQChatRuntime {
28
28
  this.media = media;
29
29
  }
30
30
  async start() {
31
+ await this.bridge.attachMappedSessionsToWorkspace();
31
32
  for (const account of this.db.enabledAccounts())
32
33
  this.startGateway(account);
33
34
  this.outboxTimer = setInterval(() => void this.flushOutbox(), 3000);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-qqchat",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "DeepSeek Harness 的 QQ 私聊/群聊插件,支持扫码授权、原生 Session、群记忆与工具权限。",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -39,26 +39,25 @@
39
39
  "typecheck": "tsc -p tsconfig.json --noEmit",
40
40
  "test": "tsx --test tests/*.test.ts",
41
41
  "check": "npm run typecheck && npm test && npm run build",
42
- "prepublishOnly": "npm run check",
43
- "prepare": "npm run build"
42
+ "prepublishOnly": "npm run check"
44
43
  },
45
44
  "dependencies": {
46
45
  "qrcode": "^1.5.4"
47
46
  },
48
47
  "peerDependencies": {
49
48
  "@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"
49
+ "@deepseek-ai/dsh-attachment": "^0.1.6-alpha.1",
50
+ "@deepseek-ai/dsh-agent": "^0.1.6-alpha.1",
51
+ "@deepseek-ai/dsh-agent-presets": "^0.1.6-alpha.1",
52
+ "@deepseek-ai/dsh-commands": "^0.1.6-alpha.1",
53
+ "@deepseek-ai/dsh-client-connection": "^0.1.6-alpha.1",
54
+ "@deepseek-ai/dsh-client-modules": "^0.1.6-alpha.1",
55
+ "@deepseek-ai/dsh-client-ui-conversation": "^0.1.6-alpha.1",
56
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.1.6-alpha.1",
57
+ "@deepseek-ai/dsh-client-ui-settings": "^0.1.6-alpha.1",
58
+ "@deepseek-ai/dsh-llm": "^0.1.6-alpha.1",
59
+ "@deepseek-ai/dsh-session": "^0.1.6-alpha.1",
60
+ "@deepseek-ai/dsh-tools": "^0.1.6-alpha.1"
62
61
  },
63
62
  "peerDependenciesMeta": {
64
63
  "@deepseek-ai/dsh-client-connection": {
@@ -67,7 +66,7 @@
67
66
  "@deepseek-ai/dsh-client-ui-settings": {
68
67
  "optional": true
69
68
  },
70
- "@deepseek-ai/dsh-client-runtime": {
69
+ "@deepseek-ai/dsh-client-modules": {
71
70
  "optional": true
72
71
  },
73
72
  "@deepseek-ai/dsh-client-ui-conversation": {
@@ -75,11 +74,12 @@
75
74
  }
76
75
  },
77
76
  "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",
77
+ "@deepseek-ai/dsh-attachment": "^0.1.6-alpha.1",
78
+ "@deepseek-ai/dsh-agent-default-model": "^0.1.6-alpha.1",
79
+ "@deepseek-ai/dsh-agent-presets": "^0.1.6-alpha.1",
80
+ "@deepseek-ai/dsh-commands": "^0.1.6-alpha.1",
81
+ "@deepseek-ai/dsh-client-connection": "^0.1.6-alpha.1",
82
+ "@deepseek-ai/dsh-client-modules": "^0.1.6-alpha.1",
83
83
  "@types/node": "^22.18.0",
84
84
  "@types/qrcode": "^1.5.5",
85
85
  "@types/react": "^19.1.0",
@@ -94,7 +94,7 @@
94
94
  "platform": "web",
95
95
  "inject": [
96
96
  "@deepseek-ai/dsh-client-connection",
97
- "@deepseek-ai/dsh-client-runtime",
97
+ "@deepseek-ai/dsh-client-modules",
98
98
  "@deepseek-ai/dsh-client-ui-conversation",
99
99
  "@deepseek-ai/dsh-client-ui-settings"
100
100
  ]
@@ -102,7 +102,7 @@
102
102
  },
103
103
  "repository": {
104
104
  "type": "git",
105
- "url": "git+ssh://git@github.com/Coffeiz/dsh-qqchat.git"
105
+ "url": "https://github.com/Coffeiz/dsh-qqchat"
106
106
  },
107
107
  "keywords": [
108
108
  "deepseek-harness",
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()
@@ -3,7 +3,7 @@ import { createRequire } from 'node:module'
3
3
  import type { Context } from '@deepseek-ai/cordis'
4
4
  import { installModelSelection } from '@deepseek-ai/dsh-agent'
5
5
  import type { AgentHandle, AgentOptions, AgentSetup, ModelSelectionRef } from '@deepseek-ai/dsh-agent'
6
- import { resolveSessionPreset } from '@deepseek-ai/dsh-agent-presets'
6
+ import { agentPresetProjectionDefinition } from '@deepseek-ai/dsh-agent-presets'
7
7
  import { createUserMessage } from '@deepseek-ai/dsh-llm'
8
8
  import type { ContentBlock } from '@deepseek-ai/dsh-llm'
9
9
  import type { CommandInvocation, CommandRuntime, CommandResult } from '@deepseek-ai/dsh-commands'
@@ -41,12 +41,15 @@ interface SessionPersistenceService { inspect(id: SessionId): Promise<{ meta: Se
41
41
  interface SessionTitleService { get(session: Session): unknown; rename(session: Session, title: string): unknown }
42
42
  interface WorkspaceRegistryService {
43
43
  archivedSessionIds: readonly string[]
44
+ resolveByPath(path: string): Promise<{ attachSession(sessionId: import('@deepseek-ai/dsh-session').SessionId): Promise<void> } | undefined>
44
45
  }
45
46
  interface Composition { presetId?: string; setup?: AgentSetup }
46
47
  interface ActiveActor { chatType: ChatType; senderId: string }
47
48
 
48
49
  export function resolveQQSessionPreset(header: Session['header'], events: readonly SessionEvent[]): string | undefined {
49
- return resolveSessionPreset({ header, events })
50
+ let current = agentPresetProjectionDefinition.init(header)
51
+ for (const event of events) current = agentPresetProjectionDefinition.apply(current, event)
52
+ return current || undefined
50
53
  }
51
54
 
52
55
  const MEDIA_TOOL_NAMES = new Set(['qqchat_describe_image', 'qqchat_read_file', 'qqchat_media_info'])
@@ -65,6 +68,7 @@ export class DshQQBridge {
65
68
  private readonly activeMediaReadable = new Map<string, boolean>()
66
69
  private readonly memorySnapshots = new Map<string, MemorySnapshotState>()
67
70
  private readonly disposeEvent: () => void
71
+ private readonly disposeAssistantStream: () => void
68
72
  private readonly disposeToolGate: () => void
69
73
  private readonly disposeCommands: () => void
70
74
  private readonly disposeImageTool: () => void
@@ -78,6 +82,10 @@ export class DshQQBridge {
78
82
  private readonly logger: LoggerLike = console,
79
83
  ) {
80
84
  this.disposeEvent = ctx.on('session/event', (session, event) => this.onSessionEvent(session, event))
85
+ this.disposeAssistantStream = ctx.on('agent/assistant-stream', ({ agent, frame }) => {
86
+ if (frame.type !== 'chunk' || frame.chunk.type !== 'text-delta' || !frame.chunk.text) return
87
+ this.pending.get(String(agent.id))?.onTextDelta?.(frame.chunk.text)
88
+ })
81
89
  this.disposeToolGate = ctx.on('tools/pre-execute', async (exec, next): Promise<PreToolDecision> => {
82
90
  const agent = exec.agent
83
91
  if (!agent) return next()
@@ -100,8 +108,19 @@ export class DshQQBridge {
100
108
  this.disposeCommands = this.registerCommands()
101
109
  }
102
110
 
111
+ async attachMappedSessionsToWorkspace(): Promise<void> {
112
+ const sessionIds = [
113
+ ...this.db.listGroups().map(row => row.dsh_session_id),
114
+ ...this.db.listDirectChats().map(row => row.dsh_session_id),
115
+ ]
116
+ for (const sessionId of sessionIds) {
117
+ if (sessionId) await this.attachToWorkspace(sessionId)
118
+ }
119
+ }
120
+
103
121
  async dispose(): Promise<void> {
104
122
  this.disposeEvent()
123
+ this.disposeAssistantStream()
105
124
  this.disposeToolGate()
106
125
  this.disposeImageTool()
107
126
  this.disposeMediaTools()
@@ -235,12 +254,6 @@ export class DshQQBridge {
235
254
  if (text.trim()) pending.text = text
236
255
  return
237
256
  }
238
- if (event.type === 'assistant/chunk') {
239
- const pending = this.pending.get(id)
240
- const chunk = event.data.chunk
241
- if (pending?.onTextDelta && chunk.type === 'text-delta' && chunk.text) pending.onTextDelta(chunk.text)
242
- return
243
- }
244
257
  if (event.type === 'request/header') {
245
258
  const { config } = event.data.header
246
259
  if (config.provider && config.model) this.routes.set(id, { provider: config.provider, model: config.model })
@@ -271,7 +284,7 @@ export class DshQQBridge {
271
284
  }
272
285
 
273
286
  private restoreMemorySnapshot(session: Session): MemorySnapshotState | undefined {
274
- return restoreMemorySnapshotState(session.events as unknown as Parameters<typeof restoreMemorySnapshotState>[0], DSH_RUNTIME_CONTEXT_SOURCE)
287
+ return restoreMemorySnapshotState(session.snapshotEvents() as unknown as Parameters<typeof restoreMemorySnapshotState>[0], DSH_RUNTIME_CONTEXT_SOURCE)
275
288
  }
276
289
 
277
290
  private async ensureAgent(chatType: ChatType, row: GroupRow | MemberRow): Promise<{ agent: AgentHandle['agent']; sessionId: string }> {
@@ -292,6 +305,7 @@ export class DshQQBridge {
292
305
  if (live) {
293
306
  this.ensureSelection(live)
294
307
  this.ensureTitle(live.session, chatType, row)
308
+ await this.attachToWorkspace(sessionId)
295
309
  this.rememberRoute(chatType, row, live, sessionId)
296
310
  return { agent: live, sessionId }
297
311
  }
@@ -299,6 +313,7 @@ export class DshQQBridge {
299
313
  if (resumed) {
300
314
  this.ensureSelection(resumed.agent)
301
315
  this.ensureTitle(resumed.agent.session, chatType, row)
316
+ await this.attachToWorkspace(sessionId)
302
317
  this.rememberRoute(chatType, row, resumed.agent, sessionId)
303
318
  return { agent: resumed.agent, sessionId }
304
319
  }
@@ -317,6 +332,7 @@ export class DshQQBridge {
317
332
  this.handles.set(sessionId, handle)
318
333
  this.ensureSelection(handle.agent)
319
334
  this.db.setChatSession(chatType, Number(row.id), sessionId)
335
+ await this.attachToWorkspace(sessionId)
320
336
  this.ensureTitle(handle.agent.session, chatType, row)
321
337
  this.rememberRoute(chatType, row, handle.agent, sessionId)
322
338
  return { agent: handle.agent, sessionId }
@@ -327,6 +343,17 @@ export class DshQQBridge {
327
343
  return registry?.archivedSessionIds.includes(sessionId) ?? false
328
344
  }
329
345
 
346
+ private async attachToWorkspace(sessionId: string): Promise<void> {
347
+ const registry = (this.ctx as unknown as { workspaceRegistry?: WorkspaceRegistryService }).workspaceRegistry
348
+ if (!registry) return
349
+ try {
350
+ const workspace = await registry.resolveByPath(process.cwd())
351
+ await workspace?.attachSession(SessionId(sessionId))
352
+ } catch (error) {
353
+ this.logger.warn?.(`[dsh-qqchat] QQ session ${sessionId} 未能挂载到当前工作区: ${error instanceof Error ? error.message : String(error)}`)
354
+ }
355
+ }
356
+
330
357
  private rememberRoute(chatType: ChatType, row: GroupRow | MemberRow, agent: AgentHandle['agent'], sessionId: string): void {
331
358
  const provider = agent.options.provider || this.config.provider
332
359
  const model = agent.options.model || this.config.model
@@ -336,7 +363,7 @@ export class DshQQBridge {
336
363
  }
337
364
 
338
365
  private appendDisplayIfMissing(session: Session, event: QQChatDisplayEvent): void {
339
- if (session.events.some(item => item.type === 'qqchat/message' && item.data.messageId === event.messageId)) return
366
+ if (session.snapshotEvents().some(item => item.type === 'qqchat/message' && item.data.messageId === event.messageId)) return
340
367
  // Keep this on the public Session.append signature. Official DSH releases
341
368
  // do not yet expose the optional ignorable envelope marker; if an older
342
369
  // DSH cannot restore this plugin-only event, ensureAgent falls back to a
@@ -345,7 +372,7 @@ export class DshQQBridge {
345
372
  }
346
373
 
347
374
  private appendOwnerMessageIfMissing(session: Session, event: QQChatDisplayEvent): void {
348
- if (session.events.some(item => {
375
+ if (session.snapshotEvents().some(item => {
349
376
  if (item.type !== 'user/message') return false
350
377
  const source = item.data.source as unknown as { messageId?: string }
351
378
  return source.messageId === event.messageId
@@ -521,7 +548,7 @@ export class DshQQBridge {
521
548
  const available = await presets.list()
522
549
  return { kind: 'success', text: available.length ? `可用 Agent Preset:\n${available.map(preset => `/qqpreset ${preset.id}`).join('\n')}` : '当前没有可用 Agent Preset。' }
523
550
  }
524
- if (agent.session.events.some(event => event.type === 'turn/start')) {
551
+ if (agent.session.snapshotEvents().some(event => event.type === 'turn/start')) {
525
552
  return { kind: 'error', text: '当前 Session 已经开始对话,DSH 不允许更换 Agent Preset。请先使用 /qqnew 开启新 Session。' }
526
553
  }
527
554
  try {
@@ -539,8 +566,9 @@ export class DshQQBridge {
539
566
 
540
567
  private statusCommand(agent: AgentHandle['agent']): CommandResult {
541
568
  const current = this.selection(agent)?.current
542
- const messages = agent.session.events.filter(event => event.type === 'user/message' || event.type === 'assistant/message').length
543
- const last = agent.session.events.at(-1)
569
+ const events = agent.session.snapshotEvents()
570
+ const messages = events.filter(event => event.type === 'user/message' || event.type === 'assistant/message').length
571
+ const last = events.at(-1)
544
572
  return {
545
573
  kind: 'success',
546
574
  text: ['📊 Session 状态', `Session:${String(agent.id)}`, `状态:${agent.status === 'running' ? '生成中' : '空闲'}`, `模型:${current ? `${current.provider}/${current.model}` : '未知'}`, `消息数:${messages}`, `最后事件:${last?.type || '无'}`].join('\n'),
@@ -29,6 +29,7 @@ export class QQChatRuntime {
29
29
  ) {}
30
30
 
31
31
  async start(): Promise<void> {
32
+ await this.bridge.attachMappedSessionsToWorkspace()
32
33
  for (const account of this.db.enabledAccounts()) this.startGateway(account)
33
34
  this.outboxTimer = setInterval(() => void this.flushOutbox(), 3000)
34
35
  this.outboxTimer.unref?.()