dsh-qqchat 0.2.4 → 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 +5 -4
- package/README.md +3 -3
- package/client-src/plugin.cts +2 -2
- package/client-src/settings.cts +18 -1
- package/client-src/shared.cts +4 -4
- package/cordis.patch.yml +5 -0
- package/docs/ARCHITECTURE.en.md +1 -1
- package/docs/ARCHITECTURE.md +2 -1
- package/docs/DEVELOPMENT.md +3 -1
- package/docs/MEMORY.md +32 -4
- package/docs/MEMORY_CONTEXT.md +16 -0
- package/docs/README.md +2 -0
- package/docs/STREAMING.md +2 -2
- package/docs/prds/PRD-MEM-1-/347/276/244/350/256/260/345/277/206/344/270/216/347/276/244/345/217/213/350/256/260/345/277/206/347/255/226/347/225/245/350/260/203/346/225/264.md +396 -0
- package/docs/prds/README.md +62 -0
- package/lib/client.js +21 -4
- package/lib/config.js +3 -2
- package/lib/index.d.ts +2 -2
- package/lib/index.js +2 -3
- package/lib/session/agent-bridge.d.ts +3 -4
- package/lib/session/agent-bridge.js +40 -46
- package/lib/session/memory-snapshot.d.ts +4 -0
- package/lib/session/memory-snapshot.js +6 -2
- package/lib/session/runtime.js +15 -5
- package/lib/storage/db.d.ts +6 -1
- package/lib/storage/db.js +57 -0
- package/lib/storage/memory.d.ts +28 -2
- package/lib/storage/memory.js +375 -101
- package/lib/transport/rpc.js +2 -0
- package/lib/types.d.ts +26 -0
- package/package.json +24 -23
- package/prompts/group-compression.md +5 -0
- package/prompts/group-reflection.md +39 -0
- package/prompts/member-batch-reflection.md +39 -0
- package/prompts/member-compression.md +5 -0
- package/prompts/private-reflection.md +32 -0
- package/src/config.ts +3 -2
- package/src/index.ts +2 -3
- package/src/session/agent-bridge.ts +40 -47
- package/src/session/memory-snapshot.ts +6 -3
- package/src/session/runtime.ts +14 -4
- package/src/storage/db.ts +70 -0
- package/src/storage/memory.ts +343 -84
- package/src/transport/rpc.ts +1 -0
- package/src/types.ts +30 -0
package/lib/types.d.ts
CHANGED
|
@@ -54,6 +54,7 @@ export interface StoredAttachmentSummary {
|
|
|
54
54
|
}
|
|
55
55
|
export interface QQChatRuntimeSettings {
|
|
56
56
|
memoryEnabled: boolean;
|
|
57
|
+
memoryMemberBatchEnabled: boolean;
|
|
57
58
|
groupReceiveMode: GroupReceiveMode;
|
|
58
59
|
groupReplyFormat: ReplyFormat;
|
|
59
60
|
directReplyFormat: ReplyFormat;
|
|
@@ -65,6 +66,7 @@ export interface QQChatRuntimeSettings {
|
|
|
65
66
|
}
|
|
66
67
|
export interface QQChatRuntimeSettingsPatch {
|
|
67
68
|
memoryEnabled?: boolean;
|
|
69
|
+
memoryMemberBatchEnabled?: boolean;
|
|
68
70
|
groupReceiveMode?: GroupReceiveMode;
|
|
69
71
|
groupReplyFormat?: ReplyFormat;
|
|
70
72
|
directReplyFormat?: ReplyFormat;
|
|
@@ -384,6 +386,16 @@ export interface ModelRoute {
|
|
|
384
386
|
model: string;
|
|
385
387
|
sessionId?: string;
|
|
386
388
|
}
|
|
389
|
+
export type ReflectionScopeType = 'group' | 'member' | 'private';
|
|
390
|
+
export type ReflectionTaskType = 'group' | 'member-batch' | 'private-owner';
|
|
391
|
+
export interface ReflectionTaskInput {
|
|
392
|
+
scopeType: ReflectionScopeType;
|
|
393
|
+
scopeKey: number;
|
|
394
|
+
taskType: ReflectionTaskType;
|
|
395
|
+
startMessageId: number;
|
|
396
|
+
endMessageId: number;
|
|
397
|
+
idempotencyKey: string;
|
|
398
|
+
}
|
|
387
399
|
export interface PendingReply {
|
|
388
400
|
text: string;
|
|
389
401
|
onTextDelta?: (delta: string) => void;
|
|
@@ -402,6 +414,20 @@ export interface ReflectionMemberUpdate {
|
|
|
402
414
|
memory?: unknown;
|
|
403
415
|
nicknames?: unknown;
|
|
404
416
|
}
|
|
417
|
+
export interface ReflectionMemberBatchUpdate {
|
|
418
|
+
senderId?: unknown;
|
|
419
|
+
profile_add?: unknown;
|
|
420
|
+
profile_remove?: unknown;
|
|
421
|
+
pattern_add?: unknown;
|
|
422
|
+
pattern_remove?: unknown;
|
|
423
|
+
summary?: unknown;
|
|
424
|
+
daily?: unknown;
|
|
425
|
+
memory?: unknown;
|
|
426
|
+
nicknames?: unknown;
|
|
427
|
+
}
|
|
428
|
+
export interface MemberBatchReflectionPayload {
|
|
429
|
+
members?: ReflectionMemberBatchUpdate[];
|
|
430
|
+
}
|
|
405
431
|
export interface ReflectionPayload {
|
|
406
432
|
group?: ReflectionGroupUpdate;
|
|
407
433
|
members?: ReflectionMemberUpdate[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-qqchat",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"description": "DeepSeek Harness 的 QQ 私聊/群聊插件,支持扫码授权、原生 Session、群记忆与工具权限。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"files": [
|
|
21
21
|
"lib",
|
|
22
22
|
"src",
|
|
23
|
+
"prompts",
|
|
23
24
|
"client-src",
|
|
24
25
|
"cordis.patch.yml",
|
|
25
26
|
"README.md",
|
|
@@ -38,26 +39,25 @@
|
|
|
38
39
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
39
40
|
"test": "tsx --test tests/*.test.ts",
|
|
40
41
|
"check": "npm run typecheck && npm test && npm run build",
|
|
41
|
-
"prepublishOnly": "npm run check"
|
|
42
|
-
"prepare": "npm run build"
|
|
42
|
+
"prepublishOnly": "npm run check"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"qrcode": "^1.5.4"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
48
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
49
|
-
"@deepseek-ai/dsh-attachment": "^0.1.
|
|
50
|
-
"@deepseek-ai/dsh-agent": "^0.1.
|
|
51
|
-
"@deepseek-ai/dsh-agent-presets": "^0.1.
|
|
52
|
-
"@deepseek-ai/dsh-commands": "^0.1.
|
|
53
|
-
"@deepseek-ai/dsh-client-connection": "^0.1.
|
|
54
|
-
"@deepseek-ai/dsh-client-
|
|
55
|
-
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.
|
|
56
|
-
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.
|
|
57
|
-
"@deepseek-ai/dsh-client-ui-settings": "^0.1.
|
|
58
|
-
"@deepseek-ai/dsh-llm": "^0.1.
|
|
59
|
-
"@deepseek-ai/dsh-session": "^0.1.
|
|
60
|
-
"@deepseek-ai/dsh-tools": "^0.1.
|
|
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"
|
|
61
61
|
},
|
|
62
62
|
"peerDependenciesMeta": {
|
|
63
63
|
"@deepseek-ai/dsh-client-connection": {
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"@deepseek-ai/dsh-client-ui-settings": {
|
|
67
67
|
"optional": true
|
|
68
68
|
},
|
|
69
|
-
"@deepseek-ai/dsh-client-
|
|
69
|
+
"@deepseek-ai/dsh-client-modules": {
|
|
70
70
|
"optional": true
|
|
71
71
|
},
|
|
72
72
|
"@deepseek-ai/dsh-client-ui-conversation": {
|
|
@@ -74,11 +74,12 @@
|
|
|
74
74
|
}
|
|
75
75
|
},
|
|
76
76
|
"devDependencies": {
|
|
77
|
-
"@deepseek-ai/dsh-attachment": "^0.1.
|
|
78
|
-
"@deepseek-ai/dsh-agent-default-model": "^0.1.
|
|
79
|
-
"@deepseek-ai/dsh-agent-presets": "^0.1.
|
|
80
|
-
"@deepseek-ai/dsh-commands": "^0.1.
|
|
81
|
-
"@deepseek-ai/dsh-client-connection": "^0.1.
|
|
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",
|
|
82
83
|
"@types/node": "^22.18.0",
|
|
83
84
|
"@types/qrcode": "^1.5.5",
|
|
84
85
|
"@types/react": "^19.1.0",
|
|
@@ -93,7 +94,7 @@
|
|
|
93
94
|
"platform": "web",
|
|
94
95
|
"inject": [
|
|
95
96
|
"@deepseek-ai/dsh-client-connection",
|
|
96
|
-
"@deepseek-ai/dsh-client-
|
|
97
|
+
"@deepseek-ai/dsh-client-modules",
|
|
97
98
|
"@deepseek-ai/dsh-client-ui-conversation",
|
|
98
99
|
"@deepseek-ai/dsh-client-ui-settings"
|
|
99
100
|
]
|
|
@@ -101,7 +102,7 @@
|
|
|
101
102
|
},
|
|
102
103
|
"repository": {
|
|
103
104
|
"type": "git",
|
|
104
|
-
"url": "
|
|
105
|
+
"url": "https://github.com/Coffeiz/dsh-qqchat"
|
|
105
106
|
},
|
|
106
107
|
"keywords": [
|
|
107
108
|
"deepseek-harness",
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
你负责维护当前 QQ 群的公开长期记忆。你会收到当前群信息、已有群记忆和一批完整消息。这是群组反思,不是当前发言人的个人总结,也不是成员画像任务。
|
|
2
|
+
|
|
3
|
+
## 一、群组记忆边界
|
|
4
|
+
|
|
5
|
+
只记录当前群公开且明确的内容:
|
|
6
|
+
|
|
7
|
+
- 群名称、用途和性质;
|
|
8
|
+
- 群内明确确认的规则、协作约定和长期沟通偏好;
|
|
9
|
+
- 明确公开的长期项目、共同决定、分工、关系和持续讨论;
|
|
10
|
+
- 当前群正在进行的事项和近期状态。
|
|
11
|
+
|
|
12
|
+
不要记录:
|
|
13
|
+
|
|
14
|
+
- 单个成员的个人资料、兴趣、性格、经历或私聊内容;
|
|
15
|
+
- 根据昵称、语气、@、引用或他人评价推断身份;
|
|
16
|
+
- 一次性闲聊、玩笑、临时情绪和短期安排;
|
|
17
|
+
- 没有明确依据的猜测;
|
|
18
|
+
- 原始 senderId、groupId、openid 等内部 ID;
|
|
19
|
+
- 群外信息。
|
|
20
|
+
|
|
21
|
+
## 二、主体和证据判断
|
|
22
|
+
|
|
23
|
+
先判断一条消息描述的是群体、某个成员、引用中的人,还是不明确的第三方,再判断是否属于群记忆。消息发送者不等于语义主体。引用和转述中的个人事实不能因为出现在群里就变成群事实;只有明确的群体决定、公开协作或群内共同事件才可以记录。
|
|
24
|
+
|
|
25
|
+
“可能、应该、感觉、听说、好像、我觉得”不能作为确定事实。涉及时间时使用消息时间换算出的绝对日期,不使用“今天、明天、最近”等相对时间。
|
|
26
|
+
|
|
27
|
+
## 三、字段规则
|
|
28
|
+
|
|
29
|
+
- profile:稳定、公开、未来仍有参考价值的群事实;本批没有变化时保持原内容,不要清空或重写。
|
|
30
|
+
- summary:不超过 150 字的当前群状态,回答“现在群里在做什么”;没有变化时沿用已有内容。
|
|
31
|
+
- daily:本批值得留档的近期群事件、决定和讨论进展,一条一个事实;一次性内容不要写入 profile。
|
|
32
|
+
- memory:真正长期有用的群项目、关系、约定和背景;不要复制完整聊天记录,也不要写个人属性。
|
|
33
|
+
- 与 existing 冲突时,只有消息明确推翻旧内容才修正;不能因为新消息暂时没提到旧事实就删除。
|
|
34
|
+
|
|
35
|
+
输出必须是严格 JSON,不要 Markdown、解释或额外文字。只修改 group,不要输出 members,也不要为 BOT 创建成员记忆。
|
|
36
|
+
|
|
37
|
+
输出结构:
|
|
38
|
+
|
|
39
|
+
{"group":{"profile":{},"summary":"","daily":"","memory":["..."]}}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
你负责维护本批 QQ 群消息中多个成员的个人长期记忆。你会收到已有成员记忆、完整群消息和成员列表。这是成员批量反思,不是当前发言人的单人总结,也不是群组总结。
|
|
2
|
+
|
|
3
|
+
## 一、先判断“是谁的”,再判断“是什么”
|
|
4
|
+
|
|
5
|
+
对每条候选信息按以下顺序判断:
|
|
6
|
+
|
|
7
|
+
1. 语义主体是发送者本人、其他群成员、群外的人、引用中的人,还是无法明确?
|
|
8
|
+
2. 主体明确后,它是稳定个人资料、可复用行为模式、近期状态、高价值个人事件,还是只属于群组事件?
|
|
9
|
+
|
|
10
|
+
只有主体能明确落到本批真实出现的 senderId,且内容描述该成员本人时,才允许写入该成员对象。消息发送者不等于语义主体,宁可漏记,也不要错记。
|
|
11
|
+
|
|
12
|
+
## 二、允许输出的成员
|
|
13
|
+
|
|
14
|
+
members 中只能包含 messages 里真实作为发送者出现、元数据提供 senderId、且确实有可记录个人信息的成员。不得因为某人被昵称提到、被 @、出现在引用中、被代词指代或被他人描述,就猜造其 senderId。没有可记内容的成员可以省略。senderId 是唯一身份依据,displayName 只用于理解和展示。
|
|
15
|
+
|
|
16
|
+
## 三、主体归属规则
|
|
17
|
+
|
|
18
|
+
- “我喜欢爵士”可以记为发送者本人的偏好。
|
|
19
|
+
- “我觉得小明喜欢爵士”描述小明,不能记为发送者喜欢爵士。
|
|
20
|
+
- “我和小明一起排查问题”可以记录发送者自己的协作模式,不能据此记录小明的能力或偏好。
|
|
21
|
+
- 引用、转述、第三方评价和“他说/听说/好像/我觉得他”等模糊表达不能直接升级为个人记忆。
|
|
22
|
+
- 只有被描述成员随后明确确认,才可以记录该成员自己的事实。
|
|
23
|
+
- 群规、群角色、公共决定、群项目和群体关系属于 group,不写入成员记忆。
|
|
24
|
+
- 不要为 BOT 创建成员记忆,也不要把 BOT 的消息当作任何成员的事实证据。
|
|
25
|
+
|
|
26
|
+
## 四、字段和证据门槛
|
|
27
|
+
|
|
28
|
+
- profile_add/profile_remove:稳定且未来有用的个人资料。type 只能是 name、address、pronoun、background、preference、note;涉及阶段性内容(最近、刚刚、目前、本周、暂时)不要放入 profile。
|
|
29
|
+
- pattern_add/pattern_remove:重复且可复用、可观察的行为或决策模式。不能因为一次消息或一件事就推断稳定人格。
|
|
30
|
+
- summary:不超过 120 字的近期状态,可以准确描述该成员参与的互动;没有变化时沿用 existing,不要写成流水账。
|
|
31
|
+
- memory:高价值、可长期回看的个人事件,包含必要的时间、背景、行动和结果;其他人的属性和群组信息不能写入。
|
|
32
|
+
- daily:本批重要但阶段性的个人事实,使用绝对日期;没有就返回空字符串。
|
|
33
|
+
- 只返回本批新增或明确修正,不要回显已有内容。旧内容只有被明确推翻时才放入 remove 数组。
|
|
34
|
+
|
|
35
|
+
严格只输出 JSON,不要 Markdown、解释或额外文字。
|
|
36
|
+
|
|
37
|
+
输出结构:
|
|
38
|
+
|
|
39
|
+
{"members":[{"senderId":"本批消息中真实出现的可靠 ID","profile_add":[{"type":"name|address|pronoun|background|preference|note","text":"只描述该成员本人","ts":0}],"profile_remove":[{"text":"被明确推翻的旧资料"}],"pattern_add":[],"pattern_remove":[],"summary":"不超过120字","daily":"绝对日期的近期事实","memory":"高价值个人事件"}]}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
你负责维护当前 QQ 私聊对象的长期记忆。你会收到已有记忆和一批按时间排列的私聊消息,只针对这个 senderId,不针对 BOT,也不针对其他被提到的人。
|
|
2
|
+
|
|
3
|
+
## 一、事实边界和主体判断
|
|
4
|
+
|
|
5
|
+
输入中的 direction 是硬性事实边界:
|
|
6
|
+
|
|
7
|
+
- direction=inbound 的消息是用户自己的表达,才可能产生用户事实;
|
|
8
|
+
- direction=outbound 的 BOT 消息只是对话上下文,不能证明用户拥有某个偏好、身份、经历或状态;
|
|
9
|
+
- 引用、转述、第三方描述、代词、玩笑、反问和“听说/好像/我觉得他”等内容,主体不明确时宁可不记;
|
|
10
|
+
- senderId 是唯一身份依据,不能根据昵称、语气、最近发言或模型推断补全身份。
|
|
11
|
+
|
|
12
|
+
## 二、记忆字段
|
|
13
|
+
|
|
14
|
+
- profile:当前用户稳定的身份、背景、地址、称呼和长期偏好。阶段性内容(最近、刚刚、目前、本周、暂时)不要写入。
|
|
15
|
+
- pattern:当前用户重复出现且可复用的行为或决策模式;一次行为不能直接推断稳定人格。
|
|
16
|
+
- summary:不超过 120 字的近期状态或关注点;没有变化时原样沿用 existing,不要清空或改成流水账。
|
|
17
|
+
- daily:本轮重要但阶段性的事实,一句话即可,涉及时间必须使用绝对日期。
|
|
18
|
+
- memory:高价值、可长期回看的用户个人事件,保留必要时间、背景、行动和结果,不复制聊天原文。
|
|
19
|
+
|
|
20
|
+
## 三、增量和安全规则
|
|
21
|
+
|
|
22
|
+
- profile_add/profile_remove、pattern_add/pattern_remove 只包含本轮新增或明确修正,不要回显已有内容。
|
|
23
|
+
- 只有被当前用户明确推翻的旧资料或模式才进入 remove 数组。
|
|
24
|
+
- 不要把 BOT 的建议、观点或回复写成用户事实。
|
|
25
|
+
- 不要把其他人的个人信息、群组规则或群体事件写入当前用户记忆。
|
|
26
|
+
- 不确定时宁可漏记;existing 是已有事实,应克制更新而不是无条件推翻。
|
|
27
|
+
|
|
28
|
+
严格只输出 JSON,不要 Markdown、解释或额外文字。
|
|
29
|
+
|
|
30
|
+
输出结构:
|
|
31
|
+
|
|
32
|
+
{"profile_add":[{"type":"name|address|pronoun|background|preference|note","text":"只描述当前私聊对象","ts":0}],"profile_remove":[{"text":"被明确推翻的旧资料"}],"pattern_add":[],"pattern_remove":[],"summary":"不超过120字的当前状态","daily":"绝对日期的阶段性事实","memory":"高价值个人事件"}
|
package/src/config.ts
CHANGED
|
@@ -10,10 +10,10 @@ const DEFAULT_CONFIG = Object.freeze({
|
|
|
10
10
|
groupReadEnabled: true,
|
|
11
11
|
replyFormat: 'smart' as ReplyFormat,
|
|
12
12
|
recentGroupMessages: 40,
|
|
13
|
-
reflectionIdleMs:
|
|
13
|
+
reflectionIdleMs: 900_000,
|
|
14
14
|
reflectionBatchSize: 20,
|
|
15
15
|
reflectionMaxMessages: 80,
|
|
16
|
-
memoryMaxTokens:
|
|
16
|
+
memoryMaxTokens: 4096,
|
|
17
17
|
memoryCompressionMaxTokens: 15000,
|
|
18
18
|
})
|
|
19
19
|
|
|
@@ -50,6 +50,7 @@ export function defaultRuntimeSettings(config: QQChatConfig): QQChatRuntimeSetti
|
|
|
50
50
|
: config.groupRequiresAt ? 'mention' : 'auto'
|
|
51
51
|
return {
|
|
52
52
|
memoryEnabled: true,
|
|
53
|
+
memoryMemberBatchEnabled: true,
|
|
53
54
|
groupReceiveMode,
|
|
54
55
|
groupReplyFormat: 'compat',
|
|
55
56
|
directReplyFormat: config.replyFormat,
|
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,10 +38,9 @@ export function apply(ctx: Context, inputConfig: QQChatConfigInput = {}): void {
|
|
|
38
38
|
const bridge = new DshQQBridge(ctx, db, memory, config, logger)
|
|
39
39
|
const runtime = new QQChatRuntime(ctx, db, api, auth, memory, bridge, config, logger, media)
|
|
40
40
|
|
|
41
|
-
ctx.connection.rpc.handle('/qqchat', createQQChatRpc(runtime)
|
|
41
|
+
ctx.connection.rpc.handle('/qqchat', createQQChatRpc(runtime))
|
|
42
42
|
ctx.effect(() => {
|
|
43
43
|
void (async () => {
|
|
44
|
-
await bridge.ensureWorkspaceMembership()
|
|
45
44
|
await runtime.start()
|
|
46
45
|
})().catch(error => logger.error?.(error))
|
|
47
46
|
return () => runtime.stop()
|
|
@@ -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'
|
|
@@ -39,16 +39,17 @@ interface AgentPreset { id: string }
|
|
|
39
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
40
|
interface SessionPersistenceService { inspect(id: SessionId): Promise<{ meta: Session['header']; events: readonly SessionEvent[] }> }
|
|
41
41
|
interface SessionTitleService { get(session: Session): unknown; rename(session: Session, title: string): unknown }
|
|
42
|
-
interface WorkspaceEntity { attachSession(sessionId: SessionId): Promise<void> }
|
|
43
42
|
interface WorkspaceRegistryService {
|
|
44
43
|
archivedSessionIds: readonly string[]
|
|
45
|
-
|
|
44
|
+
resolveByPath(path: string): Promise<{ attachSession(sessionId: import('@deepseek-ai/dsh-session').SessionId): Promise<void> } | undefined>
|
|
46
45
|
}
|
|
47
46
|
interface Composition { presetId?: string; setup?: AgentSetup }
|
|
48
47
|
interface ActiveActor { chatType: ChatType; senderId: string }
|
|
49
48
|
|
|
50
49
|
export function resolveQQSessionPreset(header: Session['header'], events: readonly SessionEvent[]): string | undefined {
|
|
51
|
-
|
|
50
|
+
let current = agentPresetProjectionDefinition.init(header)
|
|
51
|
+
for (const event of events) current = agentPresetProjectionDefinition.apply(current, event)
|
|
52
|
+
return current || undefined
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
const MEDIA_TOOL_NAMES = new Set(['qqchat_describe_image', 'qqchat_read_file', 'qqchat_media_info'])
|
|
@@ -66,8 +67,8 @@ export class DshQQBridge {
|
|
|
66
67
|
private readonly activeAttachments = new Map<string, Set<string>>()
|
|
67
68
|
private readonly activeMediaReadable = new Map<string, boolean>()
|
|
68
69
|
private readonly memorySnapshots = new Map<string, MemorySnapshotState>()
|
|
69
|
-
private qqWorkspace?: Promise<WorkspaceEntity | undefined>
|
|
70
70
|
private readonly disposeEvent: () => void
|
|
71
|
+
private readonly disposeAssistantStream: () => void
|
|
71
72
|
private readonly disposeToolGate: () => void
|
|
72
73
|
private readonly disposeCommands: () => void
|
|
73
74
|
private readonly disposeImageTool: () => void
|
|
@@ -81,6 +82,10 @@ export class DshQQBridge {
|
|
|
81
82
|
private readonly logger: LoggerLike = console,
|
|
82
83
|
) {
|
|
83
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
|
+
})
|
|
84
89
|
this.disposeToolGate = ctx.on('tools/pre-execute', async (exec, next): Promise<PreToolDecision> => {
|
|
85
90
|
const agent = exec.agent
|
|
86
91
|
if (!agent) return next()
|
|
@@ -103,8 +108,19 @@ export class DshQQBridge {
|
|
|
103
108
|
this.disposeCommands = this.registerCommands()
|
|
104
109
|
}
|
|
105
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
|
+
|
|
106
121
|
async dispose(): Promise<void> {
|
|
107
122
|
this.disposeEvent()
|
|
123
|
+
this.disposeAssistantStream()
|
|
108
124
|
this.disposeToolGate()
|
|
109
125
|
this.disposeImageTool()
|
|
110
126
|
this.disposeMediaTools()
|
|
@@ -127,16 +143,6 @@ export class DshQQBridge {
|
|
|
127
143
|
return sessionId
|
|
128
144
|
}
|
|
129
145
|
|
|
130
|
-
async ensureWorkspaceMembership(): Promise<void> {
|
|
131
|
-
const sessionIds = [
|
|
132
|
-
...this.db.listGroups().map(row => row.dsh_session_id),
|
|
133
|
-
...this.db.listDirectChats().map(row => row.dsh_session_id),
|
|
134
|
-
]
|
|
135
|
-
for (const sessionId of sessionIds) {
|
|
136
|
-
if (sessionId) await this.attachToQQWorkspace(sessionId)
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
146
|
async recordTranscript(event: QQChatDisplayEvent, row: GroupRow | MemberRow, createSession = false): Promise<string | undefined> {
|
|
141
147
|
const existing = this.db.getChatSession(event.chatType, Number(row.id))
|
|
142
148
|
if (!existing && !createSession) return undefined
|
|
@@ -248,12 +254,6 @@ export class DshQQBridge {
|
|
|
248
254
|
if (text.trim()) pending.text = text
|
|
249
255
|
return
|
|
250
256
|
}
|
|
251
|
-
if (event.type === 'assistant/chunk') {
|
|
252
|
-
const pending = this.pending.get(id)
|
|
253
|
-
const chunk = event.data.chunk
|
|
254
|
-
if (pending?.onTextDelta && chunk.type === 'text-delta' && chunk.text) pending.onTextDelta(chunk.text)
|
|
255
|
-
return
|
|
256
|
-
}
|
|
257
257
|
if (event.type === 'request/header') {
|
|
258
258
|
const { config } = event.data.header
|
|
259
259
|
if (config.provider && config.model) this.routes.set(id, { provider: config.provider, model: config.model })
|
|
@@ -284,7 +284,7 @@ export class DshQQBridge {
|
|
|
284
284
|
}
|
|
285
285
|
|
|
286
286
|
private restoreMemorySnapshot(session: Session): MemorySnapshotState | undefined {
|
|
287
|
-
return restoreMemorySnapshotState(session.
|
|
287
|
+
return restoreMemorySnapshotState(session.snapshotEvents() as unknown as Parameters<typeof restoreMemorySnapshotState>[0], DSH_RUNTIME_CONTEXT_SOURCE)
|
|
288
288
|
}
|
|
289
289
|
|
|
290
290
|
private async ensureAgent(chatType: ChatType, row: GroupRow | MemberRow): Promise<{ agent: AgentHandle['agent']; sessionId: string }> {
|
|
@@ -305,16 +305,16 @@ export class DshQQBridge {
|
|
|
305
305
|
if (live) {
|
|
306
306
|
this.ensureSelection(live)
|
|
307
307
|
this.ensureTitle(live.session, chatType, row)
|
|
308
|
+
await this.attachToWorkspace(sessionId)
|
|
308
309
|
this.rememberRoute(chatType, row, live, sessionId)
|
|
309
|
-
await this.attachToQQWorkspace(sessionId)
|
|
310
310
|
return { agent: live, sessionId }
|
|
311
311
|
}
|
|
312
312
|
const resumed = await this.tryResume(sessionId)
|
|
313
313
|
if (resumed) {
|
|
314
314
|
this.ensureSelection(resumed.agent)
|
|
315
315
|
this.ensureTitle(resumed.agent.session, chatType, row)
|
|
316
|
+
await this.attachToWorkspace(sessionId)
|
|
316
317
|
this.rememberRoute(chatType, row, resumed.agent, sessionId)
|
|
317
|
-
await this.attachToQQWorkspace(sessionId)
|
|
318
318
|
return { agent: resumed.agent, sessionId }
|
|
319
319
|
}
|
|
320
320
|
}
|
|
@@ -332,36 +332,28 @@ export class DshQQBridge {
|
|
|
332
332
|
this.handles.set(sessionId, handle)
|
|
333
333
|
this.ensureSelection(handle.agent)
|
|
334
334
|
this.db.setChatSession(chatType, Number(row.id), sessionId)
|
|
335
|
+
await this.attachToWorkspace(sessionId)
|
|
335
336
|
this.ensureTitle(handle.agent.session, chatType, row)
|
|
336
337
|
this.rememberRoute(chatType, row, handle.agent, sessionId)
|
|
337
|
-
await this.attachToQQWorkspace(sessionId)
|
|
338
338
|
return { agent: handle.agent, sessionId }
|
|
339
339
|
}
|
|
340
340
|
|
|
341
|
-
|
|
342
|
-
|
|
341
|
+
private isArchived(sessionId: string): boolean {
|
|
342
|
+
const registry = (this.ctx as unknown as { workspaceRegistry?: WorkspaceRegistryService }).workspaceRegistry
|
|
343
|
+
return registry?.archivedSessionIds.includes(sessionId) ?? false
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
private async attachToWorkspace(sessionId: string): Promise<void> {
|
|
343
347
|
const registry = (this.ctx as unknown as { workspaceRegistry?: WorkspaceRegistryService }).workspaceRegistry
|
|
344
348
|
if (!registry) return
|
|
345
|
-
if (!this.qqWorkspace) {
|
|
346
|
-
this.qqWorkspace = registry.create(process.cwd(), 'QQ Chat').catch(error => {
|
|
347
|
-
this.logger.warn?.(`[dsh-qqchat] QQ Workspace 创建失败,将保留在未分组: ${error instanceof Error ? error.message : String(error)}`)
|
|
348
|
-
return undefined
|
|
349
|
-
})
|
|
350
|
-
}
|
|
351
|
-
const workspace = await this.qqWorkspace
|
|
352
|
-
if (!workspace) return
|
|
353
349
|
try {
|
|
354
|
-
await
|
|
350
|
+
const workspace = await registry.resolveByPath(process.cwd())
|
|
351
|
+
await workspace?.attachSession(SessionId(sessionId))
|
|
355
352
|
} catch (error) {
|
|
356
|
-
this.logger.warn?.(`[dsh-qqchat] QQ session ${sessionId}
|
|
353
|
+
this.logger.warn?.(`[dsh-qqchat] QQ session ${sessionId} 未能挂载到当前工作区: ${error instanceof Error ? error.message : String(error)}`)
|
|
357
354
|
}
|
|
358
355
|
}
|
|
359
356
|
|
|
360
|
-
private isArchived(sessionId: string): boolean {
|
|
361
|
-
const registry = (this.ctx as unknown as { workspaceRegistry?: WorkspaceRegistryService }).workspaceRegistry
|
|
362
|
-
return registry?.archivedSessionIds.includes(sessionId) ?? false
|
|
363
|
-
}
|
|
364
|
-
|
|
365
357
|
private rememberRoute(chatType: ChatType, row: GroupRow | MemberRow, agent: AgentHandle['agent'], sessionId: string): void {
|
|
366
358
|
const provider = agent.options.provider || this.config.provider
|
|
367
359
|
const model = agent.options.model || this.config.model
|
|
@@ -371,7 +363,7 @@ export class DshQQBridge {
|
|
|
371
363
|
}
|
|
372
364
|
|
|
373
365
|
private appendDisplayIfMissing(session: Session, event: QQChatDisplayEvent): void {
|
|
374
|
-
if (session.
|
|
366
|
+
if (session.snapshotEvents().some(item => item.type === 'qqchat/message' && item.data.messageId === event.messageId)) return
|
|
375
367
|
// Keep this on the public Session.append signature. Official DSH releases
|
|
376
368
|
// do not yet expose the optional ignorable envelope marker; if an older
|
|
377
369
|
// DSH cannot restore this plugin-only event, ensureAgent falls back to a
|
|
@@ -380,7 +372,7 @@ export class DshQQBridge {
|
|
|
380
372
|
}
|
|
381
373
|
|
|
382
374
|
private appendOwnerMessageIfMissing(session: Session, event: QQChatDisplayEvent): void {
|
|
383
|
-
if (session.
|
|
375
|
+
if (session.snapshotEvents().some(item => {
|
|
384
376
|
if (item.type !== 'user/message') return false
|
|
385
377
|
const source = item.data.source as unknown as { messageId?: string }
|
|
386
378
|
return source.messageId === event.messageId
|
|
@@ -556,7 +548,7 @@ export class DshQQBridge {
|
|
|
556
548
|
const available = await presets.list()
|
|
557
549
|
return { kind: 'success', text: available.length ? `可用 Agent Preset:\n${available.map(preset => `/qqpreset ${preset.id}`).join('\n')}` : '当前没有可用 Agent Preset。' }
|
|
558
550
|
}
|
|
559
|
-
if (agent.session.
|
|
551
|
+
if (agent.session.snapshotEvents().some(event => event.type === 'turn/start')) {
|
|
560
552
|
return { kind: 'error', text: '当前 Session 已经开始对话,DSH 不允许更换 Agent Preset。请先使用 /qqnew 开启新 Session。' }
|
|
561
553
|
}
|
|
562
554
|
try {
|
|
@@ -574,8 +566,9 @@ export class DshQQBridge {
|
|
|
574
566
|
|
|
575
567
|
private statusCommand(agent: AgentHandle['agent']): CommandResult {
|
|
576
568
|
const current = this.selection(agent)?.current
|
|
577
|
-
const
|
|
578
|
-
const
|
|
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)
|
|
579
572
|
return {
|
|
580
573
|
kind: 'success',
|
|
581
574
|
text: ['📊 Session 状态', `Session:${String(agent.id)}`, `状态:${agent.status === 'running' ? '生成中' : '空闲'}`, `模型:${current ? `${current.provider}/${current.model}` : '未知'}`, `消息数:${messages}`, `最后事件:${last?.type || '无'}`].join('\n'),
|
|
@@ -12,7 +12,7 @@ export interface MemorySnapshotEvent {
|
|
|
12
12
|
type?: string
|
|
13
13
|
time?: number
|
|
14
14
|
data?: {
|
|
15
|
-
source?: { kind?: string; plugin?: string }
|
|
15
|
+
source?: { kind?: string; plugin?: string; sections?: readonly { name?: string; text?: string }[] }
|
|
16
16
|
content?: readonly { type?: string; text?: string }[]
|
|
17
17
|
}
|
|
18
18
|
}
|
|
@@ -32,8 +32,11 @@ export function restoreMemorySnapshotState(events: readonly MemorySnapshotEvent[
|
|
|
32
32
|
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
33
33
|
const event = events[index]
|
|
34
34
|
if (!event) continue
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
const source = event.data?.source
|
|
36
|
+
if (event.type !== 'user/message' || source?.kind !== 'plugin' || source.plugin !== sourcePlugin) continue
|
|
37
|
+
if (!source.sections?.some(section => section.name === 'qq-chat-context')) continue
|
|
38
|
+
const content = event.data?.content
|
|
39
|
+
const block = content?.length === 1 ? content[0] : undefined
|
|
37
40
|
if (block?.type !== 'text' || typeof block.text !== 'string') continue
|
|
38
41
|
return { hash: memorySnapshotHash(block.text), lastInjectedAt: Number(event.time) || 0, stale: lastCompactionAt > (Number(event.time) || 0) }
|
|
39
42
|
}
|
package/src/session/runtime.ts
CHANGED
|
@@ -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?.()
|
|
@@ -58,6 +59,7 @@ export class QQChatRuntime {
|
|
|
58
59
|
|
|
59
60
|
updateSettings(patch: QQChatRuntimeSettingsPatch): QQChatRuntimeSettings {
|
|
60
61
|
if (patch.memoryEnabled !== undefined) this.db.setSetting('memoryEnabled', Boolean(patch.memoryEnabled))
|
|
62
|
+
if (patch.memoryMemberBatchEnabled !== undefined) this.db.setSetting('memoryMemberBatchEnabled', Boolean(patch.memoryMemberBatchEnabled))
|
|
61
63
|
if (patch.groupReceiveMode !== undefined) {
|
|
62
64
|
if (!isGroupReceiveMode(patch.groupReceiveMode)) throw new Error('无效的群聊接收模式')
|
|
63
65
|
this.db.setSetting('groupReceiveMode', patch.groupReceiveMode)
|
|
@@ -120,7 +122,10 @@ export class QQChatRuntime {
|
|
|
120
122
|
content: text, quotedText: '', mentioned: false, createdAt: Date.now(),
|
|
121
123
|
}, row, true)
|
|
122
124
|
if (this.settings().memoryEnabled) {
|
|
123
|
-
if (chatType === 'group')
|
|
125
|
+
if (chatType === 'group') {
|
|
126
|
+
this.memory.schedule(Number(row.id))
|
|
127
|
+
if (settings.memoryMemberBatchEnabled) this.memory.scheduleGroupMembers(Number(row.id))
|
|
128
|
+
}
|
|
124
129
|
else this.memory.scheduleMember(Number(row.id))
|
|
125
130
|
}
|
|
126
131
|
this.logger.info?.(`[dsh-qqchat] active ${chatType} message sent to ${shortId(targetId)}`)
|
|
@@ -241,8 +246,10 @@ export class QQChatRuntime {
|
|
|
241
246
|
quotedText: message.quotedText, mentioned: message.mentioned, createdAt: Date.now(), attachments: publicAttachments(ownAttachments), quote: safeQuote,
|
|
242
247
|
}
|
|
243
248
|
if (settings.memoryEnabled) {
|
|
244
|
-
if (group)
|
|
245
|
-
|
|
249
|
+
if (group) {
|
|
250
|
+
this.memory.schedule(Number(group.id))
|
|
251
|
+
if (settings.memoryMemberBatchEnabled) this.memory.scheduleGroupMembers(Number(group.id))
|
|
252
|
+
}
|
|
246
253
|
}
|
|
247
254
|
|
|
248
255
|
if (!shouldReply) {
|
|
@@ -279,7 +286,10 @@ export class QQChatRuntime {
|
|
|
279
286
|
memberId: message.chatType === 'c2c' ? member.id : undefined, direction: 'outbound', content: reply, mentioned: false,
|
|
280
287
|
})
|
|
281
288
|
if (settings.memoryEnabled) {
|
|
282
|
-
if (group)
|
|
289
|
+
if (group) {
|
|
290
|
+
this.memory.schedule(Number(group.id))
|
|
291
|
+
if (settings.memoryMemberBatchEnabled) this.memory.scheduleGroupMembers(Number(group.id))
|
|
292
|
+
}
|
|
283
293
|
else this.memory.scheduleMember(Number(member.id))
|
|
284
294
|
}
|
|
285
295
|
} catch (error) {
|