dsh-qqchat 0.2.1 → 0.2.3
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 +24 -14
- package/README.md +18 -7
- package/client-src/settings.cts +1 -1
- package/docs/ARCHITECTURE.en.md +5 -2
- package/docs/ARCHITECTURE.md +7 -7
- package/docs/DEVELOPMENT.md +26 -1
- package/docs/MEDIA_AND_QUOTES.md +20 -20
- package/docs/STREAMING.md +22 -19
- package/docs/assets//350/256/276/347/275/256/347/225/214/351/235/242.jpg +0 -0
- package/lib/client.js +1 -1
- package/lib/gateway/api.d.ts +1 -1
- package/lib/gateway/api.js +5 -1
- package/lib/gateway/gateway.js +5 -1
- package/lib/index.js +5 -0
- package/lib/media/image-tool.d.ts +1 -1
- package/lib/media/image-tool.js +2 -2
- package/lib/session/agent-bridge.d.ts +7 -3
- package/lib/session/agent-bridge.js +95 -48
- package/lib/session/registration.d.ts +14 -0
- package/lib/session/registration.js +94 -0
- package/lib/session/runtime.js +3 -0
- package/lib/shared/augmentations.d.ts +0 -4
- package/package.json +18 -14
- package/src/gateway/api.ts +5 -1
- package/src/gateway/gateway.ts +5 -1
- package/src/index.ts +6 -0
- package/src/media/image-tool.ts +2 -2
- package/src/session/agent-bridge.ts +95 -44
- package/src/session/registration.ts +102 -0
- package/src/session/runtime.ts +3 -0
- package/src/shared/augmentations.ts +0 -4
|
@@ -2,7 +2,8 @@ import { randomUUID } from 'node:crypto'
|
|
|
2
2
|
import { createRequire } from 'node:module'
|
|
3
3
|
import type { Context } from '@deepseek-ai/cordis'
|
|
4
4
|
import { installModelSelection } from '@deepseek-ai/dsh-agent'
|
|
5
|
-
import type { AgentHandle, AgentOptions, AgentSetup, ModelSelectionRef
|
|
5
|
+
import type { AgentHandle, AgentOptions, AgentSetup, ModelSelectionRef } from '@deepseek-ai/dsh-agent'
|
|
6
|
+
import { resolveSessionPreset } from '@deepseek-ai/dsh-agent-presets'
|
|
6
7
|
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
|
7
8
|
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
|
|
8
9
|
import type { CommandInvocation, CommandRuntime, CommandResult } from '@deepseek-ai/dsh-commands'
|
|
@@ -15,7 +16,7 @@ import '../shared/augmentations.js'
|
|
|
15
16
|
import { defaultRuntimeSettings } from '../config.js'
|
|
16
17
|
import { registerQQImageTool } from '../media/image-tool.js'
|
|
17
18
|
import { registerQQMediaTools } from '../media/media-tools.js'
|
|
18
|
-
import { dispatchQQCommand, qqCommandText } from '../commands/dispatch.js'
|
|
19
|
+
import { dispatchQQCommand, qqCommandText, slashCommandName } from '../commands/dispatch.js'
|
|
19
20
|
import type { QQChatDatabase } from '../storage/db.js'
|
|
20
21
|
import type { MemoryEngine } from '../storage/memory.js'
|
|
21
22
|
import type {
|
|
@@ -35,12 +36,17 @@ import type { MemorySnapshotState } from './memory-snapshot.js'
|
|
|
35
36
|
const DSH_RUNTIME_CONTEXT_SOURCE = '@deepseek-ai/dsh-system-prompt'
|
|
36
37
|
|
|
37
38
|
interface AgentPreset { id: string }
|
|
38
|
-
interface AgentPresetService { resolve(id?: string): Promise<AgentPreset>; mount(ctx: Context, id: string): Promise<unknown> }
|
|
39
|
+
interface AgentPresetService { resolve(id?: string): Promise<AgentPreset>; list(): Promise<AgentPreset[]>; mount(ctx: Context, id: string): Promise<unknown>; recompose(ctx: Context, id: string): Promise<AgentPreset> }
|
|
40
|
+
interface SessionPersistenceService { inspect(id: SessionId): Promise<{ meta: Session['header']; events: readonly SessionEvent[] }> }
|
|
39
41
|
interface SessionTitleService { get(session: Session): unknown; rename(session: Session, title: string): unknown }
|
|
40
42
|
interface WorkspaceRegistryService { archivedSessionIds: readonly string[] }
|
|
41
43
|
interface Composition { presetId?: string; setup?: AgentSetup }
|
|
42
44
|
interface ActiveActor { chatType: ChatType; senderId: string }
|
|
43
45
|
|
|
46
|
+
export function resolveQQSessionPreset(header: Session['header'], events: readonly SessionEvent[]): string | undefined {
|
|
47
|
+
return resolveSessionPreset({ header, events })
|
|
48
|
+
}
|
|
49
|
+
|
|
44
50
|
const MEDIA_TOOL_NAMES = new Set(['qqchat_describe_image', 'qqchat_read_file', 'qqchat_media_info'])
|
|
45
51
|
const packageMetadata = createRequire(import.meta.url)('../../package.json') as { version?: string }
|
|
46
52
|
const PLUGIN_VERSION = packageMetadata.version || 'unknown'
|
|
@@ -58,7 +64,6 @@ export class DshQQBridge {
|
|
|
58
64
|
private readonly memorySnapshots = new Map<string, MemorySnapshotState>()
|
|
59
65
|
private readonly disposeEvent: () => void
|
|
60
66
|
private readonly disposeToolGate: () => void
|
|
61
|
-
private readonly disposeBootstrapGate: () => void
|
|
62
67
|
private readonly disposeCommands: () => void
|
|
63
68
|
private readonly disposeImageTool: () => void
|
|
64
69
|
private readonly disposeMediaTools: () => void
|
|
@@ -71,10 +76,6 @@ export class DshQQBridge {
|
|
|
71
76
|
private readonly logger: LoggerLike = console,
|
|
72
77
|
) {
|
|
73
78
|
this.disposeEvent = ctx.on('session/event', (session, event) => this.onSessionEvent(session, event))
|
|
74
|
-
this.disposeBootstrapGate = ctx.on('agent/pre-step', async ({ messages }, next): Promise<AgentPreStepDecision> => {
|
|
75
|
-
if (messages.some(message => message.source.kind === 'qq-chat-bootstrap')) return { kind: 'reject' }
|
|
76
|
-
return next()
|
|
77
|
-
})
|
|
78
79
|
this.disposeToolGate = ctx.on('tools/pre-execute', async (exec, next): Promise<PreToolDecision> => {
|
|
79
80
|
const agent = exec.agent
|
|
80
81
|
if (!agent) return next()
|
|
@@ -99,7 +100,6 @@ export class DshQQBridge {
|
|
|
99
100
|
|
|
100
101
|
async dispose(): Promise<void> {
|
|
101
102
|
this.disposeEvent()
|
|
102
|
-
this.disposeBootstrapGate()
|
|
103
103
|
this.disposeToolGate()
|
|
104
104
|
this.disposeImageTool()
|
|
105
105
|
this.disposeMediaTools()
|
|
@@ -128,9 +128,12 @@ export class DshQQBridge {
|
|
|
128
128
|
const { agent, sessionId } = await this.ensureAgent(event.chatType, row)
|
|
129
129
|
event.sessionId = sessionId
|
|
130
130
|
if (event.isOwner) {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
// Keep owner messages as native user/message events even when they carry
|
|
132
|
+
// images. DSH now owns the image projection: visual routes receive the
|
|
133
|
+
// durable image block, while text-only routes receive its stable text
|
|
134
|
+
// placeholder. Falling back to a display-only event here would discard
|
|
135
|
+
// the owner's message semantics before DSH can apply that policy.
|
|
136
|
+
this.appendOwnerMessageIfMissing(agent.session, event)
|
|
134
137
|
return sessionId
|
|
135
138
|
}
|
|
136
139
|
this.appendDisplayIfMissing(agent.session, event)
|
|
@@ -149,6 +152,16 @@ export class DshQQBridge {
|
|
|
149
152
|
const commandText = qqCommandText(message.text, message.mentioned)
|
|
150
153
|
if (commandText !== undefined) {
|
|
151
154
|
if (!commands) return '当前 DSH profile 未加载命令系统,请重启并确认使用了最新插件。'
|
|
155
|
+
// Preset discovery/selection is a pre-session operation. Running it
|
|
156
|
+
// through DSH's native command executor would append command/run and
|
|
157
|
+
// make the blank Session ineligible for the very preset it lists.
|
|
158
|
+
// Keep the native registration for Web, but handle QQ directly.
|
|
159
|
+
if (slashCommandName(commandText) === 'qqpreset' && (message.chatType !== 'group' || isOwner)) {
|
|
160
|
+
const prefix = '/qqpreset'
|
|
161
|
+
const result = await this.presetCommand(agent, commandText.slice(prefix.length).trim())
|
|
162
|
+
if (this.pendingResets.delete(String(sessionId))) await this.resetSession(message.chatType, row)
|
|
163
|
+
return result.text || ''
|
|
164
|
+
}
|
|
152
165
|
const command = await dispatchQQCommand(commands, agent, commandText, { chatType: message.chatType, isOwner })
|
|
153
166
|
if (command.handled) {
|
|
154
167
|
if (this.pendingResets.delete(String(sessionId))) await this.resetSession(message.chatType, row)
|
|
@@ -168,15 +181,7 @@ export class DshQQBridge {
|
|
|
168
181
|
this.injectMemorySnapshot(agent, sessionId, contextText)
|
|
169
182
|
}
|
|
170
183
|
|
|
171
|
-
const
|
|
172
|
-
const currentContent: ContentBlock[] = [{ type: 'text', text: currentText }]
|
|
173
|
-
if (attachments.length && await this.imageRouteAvailable(agent)) {
|
|
174
|
-
for (const attachment of attachments) {
|
|
175
|
-
if (attachment.kind === 'image' && attachment.imageRef) {
|
|
176
|
-
currentContent.push({ type: 'image', attachment: attachment.imageRef })
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
}
|
|
184
|
+
const currentContent = qqMessageContent(this.memory.currentMessageText(message), attachments)
|
|
180
185
|
const current = createUserMessage({
|
|
181
186
|
source: {
|
|
182
187
|
kind: 'qq-chat', botId: String(message.accountId), chatType: message.chatType,
|
|
@@ -286,7 +291,6 @@ export class DshQQBridge {
|
|
|
286
291
|
this.ensureSelection(live)
|
|
287
292
|
this.ensureTitle(live.session, chatType, row)
|
|
288
293
|
this.rememberRoute(chatType, row, live, sessionId)
|
|
289
|
-
await this.ensureVisible(live)
|
|
290
294
|
return { agent: live, sessionId }
|
|
291
295
|
}
|
|
292
296
|
const resumed = await this.tryResume(sessionId)
|
|
@@ -294,7 +298,6 @@ export class DshQQBridge {
|
|
|
294
298
|
this.ensureSelection(resumed.agent)
|
|
295
299
|
this.ensureTitle(resumed.agent.session, chatType, row)
|
|
296
300
|
this.rememberRoute(chatType, row, resumed.agent, sessionId)
|
|
297
|
-
await this.ensureVisible(resumed.agent)
|
|
298
301
|
return { agent: resumed.agent, sessionId }
|
|
299
302
|
}
|
|
300
303
|
}
|
|
@@ -314,7 +317,6 @@ export class DshQQBridge {
|
|
|
314
317
|
this.db.setChatSession(chatType, Number(row.id), sessionId)
|
|
315
318
|
this.ensureTitle(handle.agent.session, chatType, row)
|
|
316
319
|
this.rememberRoute(chatType, row, handle.agent, sessionId)
|
|
317
|
-
await this.ensureVisible(handle.agent)
|
|
318
320
|
return { agent: handle.agent, sessionId }
|
|
319
321
|
}
|
|
320
322
|
|
|
@@ -331,14 +333,12 @@ export class DshQQBridge {
|
|
|
331
333
|
else this.memory.setMemberRoute(Number((row as MemberRow).id), provider, model, sessionId)
|
|
332
334
|
}
|
|
333
335
|
|
|
334
|
-
private async ensureVisible(agent: AgentHandle['agent']): Promise<void> {
|
|
335
|
-
if (agent.session.events.some(event => event.type === 'turn/start')) return
|
|
336
|
-
agent.followup(createUserMessage({ source: { kind: 'qq-chat-bootstrap', plugin: 'dsh-qqchat' }, content: [{ type: 'text', text: 'Activate QQ Chat workspace session.' }] }))
|
|
337
|
-
await agent.whenIdle()
|
|
338
|
-
}
|
|
339
|
-
|
|
340
336
|
private appendDisplayIfMissing(session: Session, event: QQChatDisplayEvent): void {
|
|
341
337
|
if (session.events.some(item => item.type === 'qqchat/message' && item.data.messageId === event.messageId)) return
|
|
338
|
+
// Keep this on the public Session.append signature. Official DSH releases
|
|
339
|
+
// do not yet expose the optional ignorable envelope marker; if an older
|
|
340
|
+
// DSH cannot restore this plugin-only event, ensureAgent falls back to a
|
|
341
|
+
// fresh Session while SQLite remains the QQ history source of truth.
|
|
342
342
|
session.append('qqchat/message', displayEventPayload(event))
|
|
343
343
|
}
|
|
344
344
|
|
|
@@ -375,7 +375,7 @@ export class DshQQBridge {
|
|
|
375
375
|
|
|
376
376
|
private async tryResume(sessionId: string): Promise<AgentHandle | undefined> {
|
|
377
377
|
try {
|
|
378
|
-
const composition = await this.composition()
|
|
378
|
+
const composition = await this.composition(sessionId)
|
|
379
379
|
const handle = await this.ctx.agents.resume({ resumeSessionId: SessionId(sessionId), agentOptions: this.agentOptions(), setup: composition.setup })
|
|
380
380
|
if (!handle.agent.session.header.cwd) {
|
|
381
381
|
await handle.dispose()
|
|
@@ -400,15 +400,6 @@ export class DshQQBridge {
|
|
|
400
400
|
return options
|
|
401
401
|
}
|
|
402
402
|
|
|
403
|
-
private async imageRouteAvailable(agent: AgentHandle['agent']): Promise<boolean> {
|
|
404
|
-
const route = this.selection(agent)?.current || { provider: agent.options.provider, model: agent.options.model }
|
|
405
|
-
if (!route.provider || !route.model) return false
|
|
406
|
-
try {
|
|
407
|
-
const info = await this.ctx.llm.resolveModelInfo(route.provider, route.model)
|
|
408
|
-
return info.inputModalities?.includes('image') === true
|
|
409
|
-
} catch { return false }
|
|
410
|
-
}
|
|
411
|
-
|
|
412
403
|
private ensureSelection(agent: AgentHandle['agent']): ModelSelectionRef | undefined {
|
|
413
404
|
const id = String(agent.id)
|
|
414
405
|
const existing = this.selections.get(id)
|
|
@@ -458,7 +449,7 @@ export class DshQQBridge {
|
|
|
458
449
|
.map(command => `/${command.name}${command.input?.hint ? ` ${command.input.hint}` : ''} — ${command.description}`),
|
|
459
450
|
'',
|
|
460
451
|
'QQChat 会话',
|
|
461
|
-
...commands.filter(command => ['qqnew', 'qqreset', 'qqclear', 'qqmodel', 'qqstop', 'qqstatus'].includes(command.name))
|
|
452
|
+
...commands.filter(command => ['qqnew', 'qqreset', 'qqclear', 'qqmodel', 'qqpreset', 'qqstop', 'qqstatus'].includes(command.name))
|
|
462
453
|
.map(command => `/${command.name}${command.input?.hint ? ` ${command.input.hint}` : ''} — ${command.description}`),
|
|
463
454
|
'',
|
|
464
455
|
'QQChat 工具',
|
|
@@ -477,6 +468,7 @@ export class DshQQBridge {
|
|
|
477
468
|
register('qqreset', '开始新会话(/qqnew 别名)', invocation => { this.requestReset(String(invocation.agent.id)); return { kind: 'success', text: '已开启新会话,下一条消息将进入新的 Session。' } }),
|
|
478
469
|
register('qqclear', '开始新会话(/qqnew 别名)', invocation => { this.requestReset(String(invocation.agent.id)); return { kind: 'success', text: '已开启新会话,下一条消息将进入新的 Session。' } }),
|
|
479
470
|
register('qqmodel', '查看或切换当前会话模型', invocation => this.modelCommand(invocation), '[provider/]model'),
|
|
471
|
+
register('qqpreset', '查看或切换当前 Session 的 Agent Preset', invocation => this.presetCommand(invocation.agent as AgentHandle['agent'], invocation.rawInput.trim()), 'preset'),
|
|
480
472
|
register('qqstop', '中止当前生成', invocation => {
|
|
481
473
|
if (invocation.agent.status !== 'running') return { kind: 'success', text: '当前没有进行中的生成。' }
|
|
482
474
|
invocation.agent.cancel({ kind: 'user' })
|
|
@@ -519,6 +511,30 @@ export class DshQQBridge {
|
|
|
519
511
|
return { kind: 'success', text: `模型已切换:${provider}/${model}\n对话上下文保留,下一轮生效。` }
|
|
520
512
|
}
|
|
521
513
|
|
|
514
|
+
private async presetCommand(agent: AgentHandle['agent'], requested: string): Promise<CommandResult> {
|
|
515
|
+
const getService = (this.ctx as unknown as { get?: (name: string) => unknown }).get
|
|
516
|
+
const presets = getService?.call(this.ctx, 'agentPresets') as AgentPresetService | undefined
|
|
517
|
+
if (!presets) return { kind: 'error', text: '当前 DSH profile 未加载 Agent Preset 系统。' }
|
|
518
|
+
if (!requested) {
|
|
519
|
+
const available = await presets.list()
|
|
520
|
+
return { kind: 'success', text: available.length ? `可用 Agent Preset:\n${available.map(preset => `/qqpreset ${preset.id}`).join('\n')}` : '当前没有可用 Agent Preset。' }
|
|
521
|
+
}
|
|
522
|
+
if (agent.session.events.some(event => event.type === 'turn/start')) {
|
|
523
|
+
return { kind: 'error', text: '当前 Session 已经开始对话,DSH 不允许更换 Agent Preset。请先使用 /qqnew 开启新 Session。' }
|
|
524
|
+
}
|
|
525
|
+
try {
|
|
526
|
+
const preset = await presets.recompose(agent.ctx, requested)
|
|
527
|
+
agent.session.append('agent-preset/selected', { agentPreset: preset.id })
|
|
528
|
+
return { kind: 'success', text: `当前 Session 已切换到 Agent Preset:${preset.id}` }
|
|
529
|
+
} catch {
|
|
530
|
+
const available = await presets.list().catch(() => [])
|
|
531
|
+
const availableText = available.length
|
|
532
|
+
? `当前可用:${available.map(preset => preset.id).join('、')}`
|
|
533
|
+
: '当前没有可用 Agent Preset。'
|
|
534
|
+
return { kind: 'error', text: `无法切换 Agent Preset「${requested}」:该 preset 不存在、已删除或格式无效。\n${availableText}` }
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
522
538
|
private statusCommand(agent: AgentHandle['agent']): CommandResult {
|
|
523
539
|
const current = this.selection(agent)?.current
|
|
524
540
|
const messages = agent.session.events.filter(event => event.type === 'user/message' || event.type === 'assistant/message').length
|
|
@@ -529,14 +545,38 @@ export class DshQQBridge {
|
|
|
529
545
|
}
|
|
530
546
|
}
|
|
531
547
|
|
|
532
|
-
private async composition(): Promise<Composition> {
|
|
548
|
+
private async composition(sessionId?: string): Promise<Composition> {
|
|
533
549
|
const getService = (this.ctx as unknown as { get?: (name: string) => unknown }).get
|
|
534
550
|
const presets = getService?.call(this.ctx, 'agentPresets') as AgentPresetService | undefined
|
|
535
551
|
if (!presets) return {}
|
|
536
|
-
const
|
|
552
|
+
const persisted = sessionId ? await this.persistedPreset(sessionId) : undefined
|
|
553
|
+
const requested = persisted || this.config.agentPreset
|
|
554
|
+
let preset: AgentPreset
|
|
555
|
+
try {
|
|
556
|
+
preset = await presets.resolve(requested)
|
|
557
|
+
} catch (error) {
|
|
558
|
+
const available = await presets.list()
|
|
559
|
+
const fallback = available[0]
|
|
560
|
+
if (!fallback) throw error
|
|
561
|
+
this.logger.warn?.(`[dsh-qqchat] Agent Preset「${requested || '默认'}」不可用,已降级到「${fallback.id}」`)
|
|
562
|
+
preset = await presets.resolve(fallback.id)
|
|
563
|
+
}
|
|
537
564
|
return { presetId: preset.id, setup: async agentCtx => { await presets.mount(agentCtx, preset.id) } }
|
|
538
565
|
}
|
|
539
566
|
|
|
567
|
+
private async persistedPreset(sessionId: string): Promise<string | undefined> {
|
|
568
|
+
const getService = (this.ctx as unknown as { get?: (name: string) => unknown }).get
|
|
569
|
+
const persistence = getService?.call(this.ctx, 'sessionPersistence') as SessionPersistenceService | undefined
|
|
570
|
+
if (!persistence) return undefined
|
|
571
|
+
try {
|
|
572
|
+
const inspection = await persistence.inspect(SessionId(sessionId))
|
|
573
|
+
return resolveQQSessionPreset(inspection.meta, inspection.events)
|
|
574
|
+
} catch (error) {
|
|
575
|
+
this.logger.debug?.(`[dsh-qqchat] persisted preset lookup skipped: ${error instanceof Error ? error.message : String(error)}`)
|
|
576
|
+
return undefined
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
540
580
|
private serial<T>(key: string, task: () => Promise<T>): Promise<T> {
|
|
541
581
|
const previous = this.locks.get(key) || Promise.resolve()
|
|
542
582
|
const current = previous.catch(() => undefined).then(task)
|
|
@@ -548,7 +588,18 @@ export class DshQQBridge {
|
|
|
548
588
|
function mediaPrompt(attachments: StoredAttachmentSummary[]): string {
|
|
549
589
|
if (!attachments.length) return ''
|
|
550
590
|
const lines = attachments.map(item => `- ${item.kind}: ${item.filename} (attachment_id=${item.id}${item.quoted ? ', 引用消息附件' : ''})`)
|
|
551
|
-
return `\n\n[QQ 媒体附件]\n${lines.join('\n')}\n
|
|
591
|
+
return `\n\n[QQ 媒体附件]\n${lines.join('\n')}\n图片由 DSH 根据当前模型能力处理;视觉模型可直接理解图片。`
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/** Build the durable DSH content for one QQ turn without pre-flighting the model. */
|
|
595
|
+
export function qqMessageContent(text: string, attachments: readonly StoredAttachmentSummary[]): ContentBlock[] {
|
|
596
|
+
const content: ContentBlock[] = [{ type: 'text', text: text + mediaPrompt([...attachments]) }]
|
|
597
|
+
for (const attachment of attachments) {
|
|
598
|
+
if (attachment.kind === 'image' && attachment.imageRef) {
|
|
599
|
+
content.push({ type: 'image', attachment: attachment.imageRef })
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return content
|
|
552
603
|
}
|
|
553
604
|
|
|
554
605
|
function transcriptContent(event: QQChatDisplayEvent, db: QQChatDatabase): ContentBlock[] {
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { createRequire } from 'node:module'
|
|
2
|
+
import { realpathSync } from 'node:fs'
|
|
3
|
+
import { homedir } from 'node:os'
|
|
4
|
+
import { dirname, join } from 'node:path'
|
|
5
|
+
import { pathToFileURL } from 'node:url'
|
|
6
|
+
|
|
7
|
+
const QQCHAT_SESSION_EVENT_TYPE = 'qqchat/message'
|
|
8
|
+
|
|
9
|
+
interface KnownEventTypesModule {
|
|
10
|
+
KNOWN_SESSION_EVENT_TYPES?: { add(type: string): unknown }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface SessionEventRegistrationOptions {
|
|
14
|
+
readonly argv?: readonly string[]
|
|
15
|
+
readonly dshHome?: string
|
|
16
|
+
readonly anchors?: readonly string[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function profileNameFromArgv(argv: readonly string[]): string | undefined {
|
|
20
|
+
const profileIndex = argv.indexOf('--profile')
|
|
21
|
+
if (profileIndex >= 0) {
|
|
22
|
+
const value = argv[profileIndex + 1]
|
|
23
|
+
if (value && !value.startsWith('-')) return value
|
|
24
|
+
}
|
|
25
|
+
const inline = argv.find(value => value.startsWith('--profile='))
|
|
26
|
+
const value = inline?.slice('--profile='.length)
|
|
27
|
+
return value && !value.startsWith('-') ? value : undefined
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Register the plugin's log-only event in every reachable dsh-session copy.
|
|
32
|
+
*
|
|
33
|
+
* DSH does not expose a public downstream event registration API yet. This
|
|
34
|
+
* small compatibility shim follows the ecosystem convention used by other
|
|
35
|
+
* DSH plugins and is intentionally idempotent and best-effort.
|
|
36
|
+
*/
|
|
37
|
+
export async function registerQQChatSessionEventType(options: SessionEventRegistrationOptions = {}): Promise<number> {
|
|
38
|
+
const argv = options.argv || process.argv
|
|
39
|
+
const profileName = profileNameFromArgv(argv)
|
|
40
|
+
const dshHome = options.dshHome || process.env.DSH_HOME || join(homedir(), '.dsh')
|
|
41
|
+
const profileAnchors = profileName
|
|
42
|
+
? [
|
|
43
|
+
join(dshHome, 'profiles', profileName, 'package.json'),
|
|
44
|
+
join(dshHome, 'profiles', 'package.json'),
|
|
45
|
+
]
|
|
46
|
+
: []
|
|
47
|
+
const anchors = [...(options.anchors || [import.meta.url, argv[1]]), ...profileAnchors].filter(
|
|
48
|
+
(anchor): anchor is string => typeof anchor === 'string' && anchor.length > 0,
|
|
49
|
+
)
|
|
50
|
+
const registeredPaths = new Set<string>()
|
|
51
|
+
let registered = 0
|
|
52
|
+
|
|
53
|
+
for (const anchor of anchors) {
|
|
54
|
+
try {
|
|
55
|
+
const require = createRequire(anchor)
|
|
56
|
+
registered += await registerSessionCopy(require, registeredPaths)
|
|
57
|
+
// Persistence may resolve its own peer dependency from a different
|
|
58
|
+
// package tree. Register the copy selected by that validator too.
|
|
59
|
+
try {
|
|
60
|
+
const persistence = require.resolve('@deepseek-ai/dsh-session-persistence')
|
|
61
|
+
registered += await registerSessionCopy(createRequire(persistence), registeredPaths)
|
|
62
|
+
} catch {
|
|
63
|
+
// Some minimal profiles do not ship the persistence package.
|
|
64
|
+
}
|
|
65
|
+
} catch {
|
|
66
|
+
// A profile may not expose this package from one of the anchors. The
|
|
67
|
+
// other anchor can still reach the validator used by the running host.
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return registered
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function registerSessionCopy(require: NodeRequire, registeredPaths: Set<string>): Promise<number> {
|
|
75
|
+
const resolved = require.resolve('@deepseek-ai/dsh-session')
|
|
76
|
+
const candidates = [resolved]
|
|
77
|
+
// The DSH development CLI can resolve workspace source through tsx while
|
|
78
|
+
// the published package resolves lib/. Register both when source exists.
|
|
79
|
+
const packageRoot = dirname(dirname(resolved))
|
|
80
|
+
candidates.push(join(packageRoot, 'src', 'index.ts'))
|
|
81
|
+
candidates.push(join(packageRoot, 'src', 'index.js'))
|
|
82
|
+
let registered = 0
|
|
83
|
+
for (const candidate of candidates) {
|
|
84
|
+
let physical: string
|
|
85
|
+
try { physical = realpathSync(candidate) } catch { continue }
|
|
86
|
+
if (registeredPaths.has(physical)) continue
|
|
87
|
+
registeredPaths.add(physical)
|
|
88
|
+
try {
|
|
89
|
+
// DSH publishes ESM builds. Dynamic import also handles a CJS build and
|
|
90
|
+
// preserves the actual module instance selected by the host anchor.
|
|
91
|
+
const module = await import(pathToFileURL(physical).href) as KnownEventTypesModule
|
|
92
|
+
if (module.KNOWN_SESSION_EVENT_TYPES?.add) {
|
|
93
|
+
module.KNOWN_SESSION_EVENT_TYPES.add(QQCHAT_SESSION_EVENT_TYPE)
|
|
94
|
+
registered += 1
|
|
95
|
+
}
|
|
96
|
+
} catch {
|
|
97
|
+
// A published package may not ship its source entry; the lib entry above
|
|
98
|
+
// remains the normal registration target.
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return registered
|
|
102
|
+
}
|
package/src/session/runtime.ts
CHANGED
|
@@ -147,6 +147,9 @@ export class QQChatRuntime {
|
|
|
147
147
|
if (!account) return
|
|
148
148
|
const id = Number(account.id)
|
|
149
149
|
if (this.gateways.has(id)) return
|
|
150
|
+
// QR binding can replace the secret for an existing account id. Never
|
|
151
|
+
// carry a token obtained with the previous secret into the new Gateway.
|
|
152
|
+
this.api.clearToken(id)
|
|
150
153
|
const gateway = new QQGateway(account, this.db, this.api, message => this.onIncoming(message), this.logger)
|
|
151
154
|
this.gateways.set(id, gateway)
|
|
152
155
|
gateway.start()
|