dsh-qqchat 0.2.5 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +8 -7
- package/README.md +6 -6
- package/client-src/plugin.cts +2 -2
- package/client-src/settings.cts +6 -19
- package/client-src/shared.cts +4 -4
- package/cordis.patch.yml +4 -0
- package/docs/ARCHITECTURE.en.md +1 -1
- package/docs/ARCHITECTURE.md +2 -1
- package/docs/DEVELOPMENT.md +4 -2
- package/docs/STREAMING.md +8 -18
- package/lib/client.js +7 -14
- package/lib/config.js +2 -5
- package/lib/gateway/api.d.ts +2 -3
- package/lib/gateway/api.js +3 -10
- package/lib/index.d.ts +2 -2
- package/lib/index.js +2 -2
- package/lib/session/agent-bridge.d.ts +3 -1
- package/lib/session/agent-bridge.js +43 -70
- package/lib/session/runtime.d.ts +2 -0
- package/lib/session/runtime.js +13 -15
- package/lib/storage/db.js +12 -3
- package/lib/transport/rpc.js +2 -4
- package/lib/types.d.ts +1 -3
- package/package.json +25 -24
- package/src/config.ts +2 -5
- package/src/gateway/api.ts +4 -13
- package/src/index.ts +2 -2
- package/src/session/agent-bridge.ts +41 -69
- package/src/session/runtime.ts +13 -12
- package/src/storage/db.ts +15 -3
- package/src/transport/rpc.ts +2 -3
- package/src/types.ts +1 -3
|
@@ -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 {
|
|
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
|
-
|
|
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()
|
|
@@ -129,15 +148,6 @@ export class DshQQBridge {
|
|
|
129
148
|
if (!existing && !createSession) return undefined
|
|
130
149
|
const { agent, sessionId } = await this.ensureAgent(event.chatType, row)
|
|
131
150
|
event.sessionId = sessionId
|
|
132
|
-
if (event.isOwner) {
|
|
133
|
-
// Keep owner messages as native user/message events even when they carry
|
|
134
|
-
// images. DSH now owns the image projection: visual routes receive the
|
|
135
|
-
// durable image block, while text-only routes receive its stable text
|
|
136
|
-
// placeholder. Falling back to a display-only event here would discard
|
|
137
|
-
// the owner's message semantics before DSH can apply that policy.
|
|
138
|
-
this.appendOwnerMessageIfMissing(agent.session, event)
|
|
139
|
-
return sessionId
|
|
140
|
-
}
|
|
141
151
|
this.appendDisplayIfMissing(agent.session, event)
|
|
142
152
|
return sessionId
|
|
143
153
|
}
|
|
@@ -235,12 +245,6 @@ export class DshQQBridge {
|
|
|
235
245
|
if (text.trim()) pending.text = text
|
|
236
246
|
return
|
|
237
247
|
}
|
|
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
248
|
if (event.type === 'request/header') {
|
|
245
249
|
const { config } = event.data.header
|
|
246
250
|
if (config.provider && config.model) this.routes.set(id, { provider: config.provider, model: config.model })
|
|
@@ -271,7 +275,7 @@ export class DshQQBridge {
|
|
|
271
275
|
}
|
|
272
276
|
|
|
273
277
|
private restoreMemorySnapshot(session: Session): MemorySnapshotState | undefined {
|
|
274
|
-
return restoreMemorySnapshotState(session.
|
|
278
|
+
return restoreMemorySnapshotState(session.snapshotEvents() as unknown as Parameters<typeof restoreMemorySnapshotState>[0], DSH_RUNTIME_CONTEXT_SOURCE)
|
|
275
279
|
}
|
|
276
280
|
|
|
277
281
|
private async ensureAgent(chatType: ChatType, row: GroupRow | MemberRow): Promise<{ agent: AgentHandle['agent']; sessionId: string }> {
|
|
@@ -292,6 +296,7 @@ export class DshQQBridge {
|
|
|
292
296
|
if (live) {
|
|
293
297
|
this.ensureSelection(live)
|
|
294
298
|
this.ensureTitle(live.session, chatType, row)
|
|
299
|
+
await this.attachToWorkspace(sessionId)
|
|
295
300
|
this.rememberRoute(chatType, row, live, sessionId)
|
|
296
301
|
return { agent: live, sessionId }
|
|
297
302
|
}
|
|
@@ -299,6 +304,7 @@ export class DshQQBridge {
|
|
|
299
304
|
if (resumed) {
|
|
300
305
|
this.ensureSelection(resumed.agent)
|
|
301
306
|
this.ensureTitle(resumed.agent.session, chatType, row)
|
|
307
|
+
await this.attachToWorkspace(sessionId)
|
|
302
308
|
this.rememberRoute(chatType, row, resumed.agent, sessionId)
|
|
303
309
|
return { agent: resumed.agent, sessionId }
|
|
304
310
|
}
|
|
@@ -317,6 +323,7 @@ export class DshQQBridge {
|
|
|
317
323
|
this.handles.set(sessionId, handle)
|
|
318
324
|
this.ensureSelection(handle.agent)
|
|
319
325
|
this.db.setChatSession(chatType, Number(row.id), sessionId)
|
|
326
|
+
await this.attachToWorkspace(sessionId)
|
|
320
327
|
this.ensureTitle(handle.agent.session, chatType, row)
|
|
321
328
|
this.rememberRoute(chatType, row, handle.agent, sessionId)
|
|
322
329
|
return { agent: handle.agent, sessionId }
|
|
@@ -327,6 +334,17 @@ export class DshQQBridge {
|
|
|
327
334
|
return registry?.archivedSessionIds.includes(sessionId) ?? false
|
|
328
335
|
}
|
|
329
336
|
|
|
337
|
+
private async attachToWorkspace(sessionId: string): Promise<void> {
|
|
338
|
+
const registry = (this.ctx as unknown as { workspaceRegistry?: WorkspaceRegistryService }).workspaceRegistry
|
|
339
|
+
if (!registry) return
|
|
340
|
+
try {
|
|
341
|
+
const workspace = await registry.resolveByPath(process.cwd())
|
|
342
|
+
await workspace?.attachSession(SessionId(sessionId))
|
|
343
|
+
} catch (error) {
|
|
344
|
+
this.logger.warn?.(`[dsh-qqchat] QQ session ${sessionId} 未能挂载到当前工作区: ${error instanceof Error ? error.message : String(error)}`)
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
330
348
|
private rememberRoute(chatType: ChatType, row: GroupRow | MemberRow, agent: AgentHandle['agent'], sessionId: string): void {
|
|
331
349
|
const provider = agent.options.provider || this.config.provider
|
|
332
350
|
const model = agent.options.model || this.config.model
|
|
@@ -336,7 +354,7 @@ export class DshQQBridge {
|
|
|
336
354
|
}
|
|
337
355
|
|
|
338
356
|
private appendDisplayIfMissing(session: Session, event: QQChatDisplayEvent): void {
|
|
339
|
-
if (session.
|
|
357
|
+
if (session.snapshotEvents().some(item => item.type === 'qqchat/message' && item.data.messageId === event.messageId)) return
|
|
340
358
|
// Keep this on the public Session.append signature. Official DSH releases
|
|
341
359
|
// do not yet expose the optional ignorable envelope marker; if an older
|
|
342
360
|
// DSH cannot restore this plugin-only event, ensureAgent falls back to a
|
|
@@ -344,23 +362,6 @@ export class DshQQBridge {
|
|
|
344
362
|
session.append('qqchat/message', displayEventPayload(event))
|
|
345
363
|
}
|
|
346
364
|
|
|
347
|
-
private appendOwnerMessageIfMissing(session: Session, event: QQChatDisplayEvent): void {
|
|
348
|
-
if (session.events.some(item => {
|
|
349
|
-
if (item.type !== 'user/message') return false
|
|
350
|
-
const source = item.data.source as unknown as { messageId?: string }
|
|
351
|
-
return source.messageId === event.messageId
|
|
352
|
-
})) return
|
|
353
|
-
session.append('user/message', createUserMessage({
|
|
354
|
-
source: {
|
|
355
|
-
kind: 'user', channel: 'qq', botId: 'qqchat', chatType: event.chatType,
|
|
356
|
-
chatId: event.chatId, senderId: event.senderId,
|
|
357
|
-
...(event.senderName ? { senderName: event.senderName } : {}),
|
|
358
|
-
messageId: event.messageId, mentioned: event.mentioned,
|
|
359
|
-
},
|
|
360
|
-
content: transcriptContent(event, this.db),
|
|
361
|
-
}), { surfaceOp: 'append' })
|
|
362
|
-
}
|
|
363
|
-
|
|
364
365
|
private ensureTitle(session: Session, chatType: ChatType, row: GroupRow | MemberRow): void {
|
|
365
366
|
try {
|
|
366
367
|
const getService = (this.ctx as unknown as { get?: (name: string) => unknown }).get
|
|
@@ -521,7 +522,7 @@ export class DshQQBridge {
|
|
|
521
522
|
const available = await presets.list()
|
|
522
523
|
return { kind: 'success', text: available.length ? `可用 Agent Preset:\n${available.map(preset => `/qqpreset ${preset.id}`).join('\n')}` : '当前没有可用 Agent Preset。' }
|
|
523
524
|
}
|
|
524
|
-
if (agent.session.
|
|
525
|
+
if (agent.session.snapshotEvents().some(event => event.type === 'turn/start')) {
|
|
525
526
|
return { kind: 'error', text: '当前 Session 已经开始对话,DSH 不允许更换 Agent Preset。请先使用 /qqnew 开启新 Session。' }
|
|
526
527
|
}
|
|
527
528
|
try {
|
|
@@ -539,8 +540,9 @@ export class DshQQBridge {
|
|
|
539
540
|
|
|
540
541
|
private statusCommand(agent: AgentHandle['agent']): CommandResult {
|
|
541
542
|
const current = this.selection(agent)?.current
|
|
542
|
-
const
|
|
543
|
-
const
|
|
543
|
+
const events = agent.session.snapshotEvents()
|
|
544
|
+
const messages = events.filter(event => event.type === 'user/message' || event.type === 'assistant/message').length
|
|
545
|
+
const last = events.at(-1)
|
|
544
546
|
return {
|
|
545
547
|
kind: 'success',
|
|
546
548
|
text: ['📊 Session 状态', `Session:${String(agent.id)}`, `状态:${agent.status === 'running' ? '生成中' : '空闲'}`, `模型:${current ? `${current.provider}/${current.model}` : '未知'}`, `消息数:${messages}`, `最后事件:${last?.type || '无'}`].join('\n'),
|
|
@@ -604,36 +606,6 @@ export function qqMessageContent(text: string, attachments: readonly StoredAttac
|
|
|
604
606
|
return content
|
|
605
607
|
}
|
|
606
608
|
|
|
607
|
-
function transcriptContent(event: QQChatDisplayEvent, db: QQChatDatabase): ContentBlock[] {
|
|
608
|
-
const attachments = event.attachments || []
|
|
609
|
-
const renderableImages = attachments.filter(attachment => attachment.kind === 'image' && db.attachmentById(attachment.id)?.imageRef)
|
|
610
|
-
const imagePlaceholderLines = new Set(renderableImages.map(attachment => `[图片] ${attachment.filename}`))
|
|
611
|
-
const quote = event.quote ? formatDisplayQuote(event) : ''
|
|
612
|
-
const body = event.content.split('\n').filter(line => !imagePlaceholderLines.has(line)).join('\n').trim()
|
|
613
|
-
const text = [quote, body].filter(Boolean).join('\n\n')
|
|
614
|
-
const content: ContentBlock[] = [{ type: 'text', text }]
|
|
615
|
-
for (const attachment of attachments) {
|
|
616
|
-
const imageRef = attachment.kind === 'image' ? db.attachmentById(attachment.id)?.imageRef : undefined
|
|
617
|
-
if (imageRef) {
|
|
618
|
-
content.push({ type: 'image', attachment: imageRef })
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
return content
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
function formatDisplayQuote(event: QQChatDisplayEvent): string {
|
|
625
|
-
const quote = event.quote
|
|
626
|
-
if (!quote) return ''
|
|
627
|
-
return [
|
|
628
|
-
'[引用消息]',
|
|
629
|
-
quote.senderId ? `发送人ID=${quote.senderId}` : '',
|
|
630
|
-
quote.senderName ? `显示名=${quote.senderName}` : '',
|
|
631
|
-
quote.messageId ? `消息ID=${quote.messageId}` : '',
|
|
632
|
-
`正文=${quote.text || '(空消息)'}`,
|
|
633
|
-
quote.attachments.length ? `附件=${quote.attachments.map(attachment => `${attachment.kind || 'file'}:${attachment.filename}${attachment.attachmentId ? ` (attachment_id=${attachment.attachmentId})` : ''}`).join('、')}` : '',
|
|
634
|
-
].filter(Boolean).join('\n')
|
|
635
|
-
}
|
|
636
|
-
|
|
637
609
|
function displayEventPayload(event: QQChatDisplayEvent): QQChatDisplayEvent {
|
|
638
610
|
const attachments = (event.attachments || []).map(attachment => ({
|
|
639
611
|
id: String(attachment.id), kind: attachment.kind, filename: String(attachment.filename),
|
package/src/session/runtime.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type { QQApiClient } from '../gateway/api.js'
|
|
|
9
9
|
import type { QQBindService } from '../gateway/auth.js'
|
|
10
10
|
import { QQGateway } from '../gateway/gateway.js'
|
|
11
11
|
import { renderQQMentionNames } from '../gateway/normalize.js'
|
|
12
|
+
import { qqCommandText } from '../commands/dispatch.js'
|
|
12
13
|
import { QQMediaStore } from '../media/store.js'
|
|
13
14
|
import type {
|
|
14
15
|
AccountRow, ChatTargetRow, ChatType, GroupReceiveMode, GroupRow, MemberRow, QQAttachmentInput, QQChatConfig,
|
|
@@ -29,6 +30,7 @@ export class QQChatRuntime {
|
|
|
29
30
|
) {}
|
|
30
31
|
|
|
31
32
|
async start(): Promise<void> {
|
|
33
|
+
await this.bridge.attachMappedSessionsToWorkspace()
|
|
32
34
|
for (const account of this.db.enabledAccounts()) this.startGateway(account)
|
|
33
35
|
this.outboxTimer = setInterval(() => void this.flushOutbox(), 3000)
|
|
34
36
|
this.outboxTimer.unref?.()
|
|
@@ -51,9 +53,7 @@ export class QQChatRuntime {
|
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
settings(): QQChatRuntimeSettings {
|
|
54
|
-
|
|
55
|
-
if (settings.directReplyFormat === 'compat') settings.directStreamingEnabled = false
|
|
56
|
-
return settings
|
|
56
|
+
return this.db.runtimeSettings(defaultRuntimeSettings(this.config))
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
updateSettings(patch: QQChatRuntimeSettingsPatch): QQChatRuntimeSettings {
|
|
@@ -70,9 +70,7 @@ export class QQChatRuntime {
|
|
|
70
70
|
if (patch.directReplyFormat !== undefined) {
|
|
71
71
|
if (!isReplyFormat(patch.directReplyFormat)) throw new Error('无效的私聊消息兼容格式')
|
|
72
72
|
this.db.setSetting('directReplyFormat', patch.directReplyFormat)
|
|
73
|
-
if (patch.directReplyFormat === 'compat') this.db.setSetting('directStreamingEnabled', false)
|
|
74
73
|
}
|
|
75
|
-
if (patch.directStreamingEnabled !== undefined) this.db.setSetting('directStreamingEnabled', Boolean(patch.directStreamingEnabled))
|
|
76
74
|
if (patch.groupMembersCanUseTools !== undefined) this.db.setSetting('groupMembersCanUseTools', Boolean(patch.groupMembersCanUseTools))
|
|
77
75
|
if (patch.groupMembersCanReceiveMedia !== undefined) this.db.setSetting('groupMembersCanReceiveMedia', Boolean(patch.groupMembersCanReceiveMedia))
|
|
78
76
|
if (patch.groupMembersCanReadMedia !== undefined) this.db.setSetting('groupMembersCanReadMedia', Boolean(patch.groupMembersCanReadMedia))
|
|
@@ -251,22 +249,21 @@ export class QQChatRuntime {
|
|
|
251
249
|
}
|
|
252
250
|
}
|
|
253
251
|
|
|
254
|
-
if (
|
|
252
|
+
if (shouldRecordQQTranscript(shouldReply, message.text, message.mentioned)) {
|
|
255
253
|
await this.bridge.recordTranscript(displayEvent, row, true)
|
|
254
|
+
}
|
|
255
|
+
if (!shouldReply) {
|
|
256
256
|
return
|
|
257
257
|
}
|
|
258
258
|
|
|
259
259
|
try {
|
|
260
|
-
// Keep the incoming QQ message visible in Web independently from the
|
|
261
|
-
// notice-shaped message used to drive the Agent turn.
|
|
262
|
-
await this.bridge.recordTranscript(displayEvent, row, true)
|
|
263
260
|
const targetId = message.chatType === 'group' ? message.groupOpenid! : message.senderId
|
|
264
261
|
const sendOptions = {
|
|
265
262
|
group: message.chatType === 'group', messageId: message.messageId || null,
|
|
266
263
|
format: message.chatType === 'group' ? settings.groupReplyFormat : settings.directReplyFormat,
|
|
267
264
|
} as const
|
|
268
|
-
const stream = message.chatType === 'c2c' && settings.directReplyFormat
|
|
269
|
-
? this.api.createPrivateTextStream(account, targetId, sendOptions)
|
|
265
|
+
const stream = message.chatType === 'c2c' && settings.directReplyFormat === 'markdown'
|
|
266
|
+
? this.api.createPrivateTextStream(account, targetId, { messageId: sendOptions.messageId })
|
|
270
267
|
: undefined
|
|
271
268
|
const reply = await this.bridge.reply(message, group, member, storedAttachments, stream ? delta => stream.push(delta) : undefined, isOwner)
|
|
272
269
|
if (stream) {
|
|
@@ -319,11 +316,15 @@ export class QQChatRuntime {
|
|
|
319
316
|
}
|
|
320
317
|
|
|
321
318
|
function isGroupReceiveMode(value: unknown): value is GroupReceiveMode { return value === 'auto' || value === 'mention' || value === 'silent' }
|
|
322
|
-
function isReplyFormat(value: unknown): value is ReplyFormat { return value === '
|
|
319
|
+
function isReplyFormat(value: unknown): value is ReplyFormat { return value === 'markdown' || value === 'compat' }
|
|
323
320
|
export function shouldReplyToGroup(mode: GroupReceiveMode, group: Pick<GroupRow, 'enabled' | 'read_enabled'>, mentioned: boolean): boolean {
|
|
324
321
|
if (group.enabled !== 1 || group.read_enabled !== 1) return false
|
|
325
322
|
return mode === 'auto' || (mode === 'mention' && mentioned)
|
|
326
323
|
}
|
|
324
|
+
/** Record messages that do not enter the Agent turn, plus QQ commands handled outside Agent.followup. */
|
|
325
|
+
export function shouldRecordQQTranscript(shouldReply: boolean, text: string, mentioned: boolean): boolean {
|
|
326
|
+
return !shouldReply || qqCommandText(text, mentioned) !== undefined
|
|
327
|
+
}
|
|
327
328
|
function shortId(value: string): string { return value.length > 10 ? `…${value.slice(-10)}` : value }
|
|
328
329
|
function publicAttachments(attachments: StoredAttachmentSummary[]): StoredAttachmentSummary[] {
|
|
329
330
|
return attachments.map(attachment => ({
|
package/src/storage/db.ts
CHANGED
|
@@ -264,6 +264,9 @@ export class QQChatDatabase {
|
|
|
264
264
|
error TEXT
|
|
265
265
|
);
|
|
266
266
|
`)
|
|
267
|
+
this.db.prepare("UPDATE plugin_settings SET value='\"markdown\"',updated_at=? WHERE key IN ('replyFormat','groupReplyFormat','directReplyFormat') AND value='\"smart\"'")
|
|
268
|
+
.run(now())
|
|
269
|
+
this.db.prepare("DELETE FROM plugin_settings WHERE key='directStreamingEnabled'").run()
|
|
267
270
|
this.db.prepare("UPDATE reflection_tasks SET status='failed',updated_at=? WHERE status='running' AND updated_at<?")
|
|
268
271
|
.run(now(), now() - REFLECTION_TASK_STALE_MS)
|
|
269
272
|
const columns = new Set(many<{ name: string }>(this.db.prepare('PRAGMA table_info(group_members)').all()).map(column => column.name))
|
|
@@ -396,13 +399,16 @@ export class QQChatDatabase {
|
|
|
396
399
|
}
|
|
397
400
|
|
|
398
401
|
runtimeSettings(defaults: QQChatRuntimeSettings): QQChatRuntimeSettings {
|
|
402
|
+
const legacyReplyFormat = normalizeReplyFormat(
|
|
403
|
+
this.getSetting<unknown>('replyFormat', defaults.directReplyFormat),
|
|
404
|
+
defaults.directReplyFormat,
|
|
405
|
+
)
|
|
399
406
|
return {
|
|
400
407
|
memoryEnabled: this.getSetting('memoryEnabled', defaults.memoryEnabled),
|
|
401
408
|
memoryMemberBatchEnabled: this.getSetting('memoryMemberBatchEnabled', defaults.memoryMemberBatchEnabled),
|
|
402
409
|
groupReceiveMode: this.getSetting('groupReceiveMode', defaults.groupReceiveMode),
|
|
403
|
-
groupReplyFormat: this.getSetting('groupReplyFormat', defaults.groupReplyFormat),
|
|
404
|
-
directReplyFormat: this.getSetting('directReplyFormat',
|
|
405
|
-
directStreamingEnabled: this.getSetting('directStreamingEnabled', defaults.directStreamingEnabled),
|
|
410
|
+
groupReplyFormat: normalizeReplyFormat(this.getSetting<unknown>('groupReplyFormat', defaults.groupReplyFormat), defaults.groupReplyFormat),
|
|
411
|
+
directReplyFormat: normalizeReplyFormat(this.getSetting<unknown>('directReplyFormat', legacyReplyFormat), legacyReplyFormat),
|
|
406
412
|
groupMembersCanUseTools: this.getSetting('groupMembersCanUseTools', defaults.groupMembersCanUseTools),
|
|
407
413
|
groupMembersCanReceiveMedia: this.getSetting('groupMembersCanReceiveMedia', defaults.groupMembersCanReceiveMedia),
|
|
408
414
|
groupMembersCanReadMedia: this.getSetting('groupMembersCanReadMedia', defaults.groupMembersCanReadMedia),
|
|
@@ -828,3 +834,9 @@ export class QQChatDatabase {
|
|
|
828
834
|
this.db.prepare('UPDATE outbox SET status=?,sent_at=?,error=? WHERE id=?').run(error ? 'failed' : 'sent', now(), error, id)
|
|
829
835
|
}
|
|
830
836
|
}
|
|
837
|
+
|
|
838
|
+
function normalizeReplyFormat(value: unknown, fallback: QQChatRuntimeSettings['directReplyFormat']): QQChatRuntimeSettings['directReplyFormat'] {
|
|
839
|
+
if (value === 'markdown' || value === 'compat') return value
|
|
840
|
+
// Older releases persisted the automatic “smart” mode. Its explicit replacement is Markdown.
|
|
841
|
+
return value === 'smart' ? 'markdown' : fallback
|
|
842
|
+
}
|
package/src/transport/rpc.ts
CHANGED
|
@@ -311,9 +311,8 @@ function asSettingsPatch(value: unknown): QQChatRuntimeSettingsPatch {
|
|
|
311
311
|
if (record.groupReceiveMode === 'auto' || record.groupReceiveMode === 'mention' || record.groupReceiveMode === 'silent') {
|
|
312
312
|
patch.groupReceiveMode = record.groupReceiveMode
|
|
313
313
|
}
|
|
314
|
-
if (record.groupReplyFormat === '
|
|
315
|
-
if (record.directReplyFormat === '
|
|
316
|
-
if (typeof record.directStreamingEnabled === 'boolean') patch.directStreamingEnabled = record.directStreamingEnabled
|
|
314
|
+
if (record.groupReplyFormat === 'markdown' || record.groupReplyFormat === 'compat') patch.groupReplyFormat = record.groupReplyFormat
|
|
315
|
+
if (record.directReplyFormat === 'markdown' || record.directReplyFormat === 'compat') patch.directReplyFormat = record.directReplyFormat
|
|
317
316
|
if (typeof record.groupMembersCanUseTools === 'boolean') patch.groupMembersCanUseTools = record.groupMembersCanUseTools
|
|
318
317
|
if (typeof record.groupMembersCanReceiveMedia === 'boolean') patch.groupMembersCanReceiveMedia = record.groupMembersCanReceiveMedia
|
|
319
318
|
if (typeof record.groupMembersCanReadMedia === 'boolean') patch.groupMembersCanReadMedia = record.groupMembersCanReadMedia
|
package/src/types.ts
CHANGED
|
@@ -16,7 +16,7 @@ export interface DailyEntry {
|
|
|
16
16
|
date: string
|
|
17
17
|
note: string
|
|
18
18
|
}
|
|
19
|
-
export type ReplyFormat = '
|
|
19
|
+
export type ReplyFormat = 'markdown' | 'compat'
|
|
20
20
|
export type GroupReceiveMode = 'auto' | 'mention' | 'silent'
|
|
21
21
|
export type GatewayStatus = 'offline' | 'connecting' | 'online' | 'error'
|
|
22
22
|
export type LogLevel = 'debug' | 'info' | 'warn' | 'error'
|
|
@@ -65,7 +65,6 @@ export interface QQChatRuntimeSettings {
|
|
|
65
65
|
groupReceiveMode: GroupReceiveMode
|
|
66
66
|
groupReplyFormat: ReplyFormat
|
|
67
67
|
directReplyFormat: ReplyFormat
|
|
68
|
-
directStreamingEnabled: boolean
|
|
69
68
|
groupMembersCanUseTools: boolean
|
|
70
69
|
groupMembersCanReceiveMedia: boolean
|
|
71
70
|
groupMembersCanReadMedia: boolean
|
|
@@ -78,7 +77,6 @@ export interface QQChatRuntimeSettingsPatch {
|
|
|
78
77
|
groupReceiveMode?: GroupReceiveMode
|
|
79
78
|
groupReplyFormat?: ReplyFormat
|
|
80
79
|
directReplyFormat?: ReplyFormat
|
|
81
|
-
directStreamingEnabled?: boolean
|
|
82
80
|
groupMembersCanUseTools?: boolean
|
|
83
81
|
groupMembersCanReceiveMedia?: boolean
|
|
84
82
|
groupMembersCanReadMedia?: boolean
|