lightclawbot 1.2.6 → 1.2.8
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/dist/src/config.js +30 -3
- package/dist/src/gateway.js +58 -12
- package/dist/src/group/constants/index.js +20 -0
- package/dist/src/group/inbound/index.js +254 -0
- package/dist/src/group/index.js +15 -0
- package/dist/src/group/orchestrator/execution/agent-runner.js +299 -0
- package/dist/src/group/orchestrator/execution/index.js +7 -0
- package/dist/src/group/orchestrator/execution/prompt-builder.js +288 -0
- package/dist/src/group/orchestrator/execution/soul-resolver.js +38 -0
- package/dist/src/group/orchestrator/execution/types.js +7 -0
- package/dist/src/group/orchestrator/index.js +14 -0
- package/dist/src/group/orchestrator/lifecycle/conversation-state.js +162 -0
- package/dist/src/group/orchestrator/lifecycle/index.js +7 -0
- package/dist/src/group/orchestrator/lifecycle/ledger-writer.js +96 -0
- package/dist/src/group/orchestrator/lifecycle/run-registry.js +174 -0
- package/dist/src/group/orchestrator/orchestrator.js +265 -0
- package/dist/src/group/orchestrator/planning/index.js +13 -0
- package/dist/src/group/orchestrator/planning/plan-validator.js +233 -0
- package/dist/src/group/orchestrator/planning/planning-parser.js +207 -0
- package/dist/src/group/orchestrator/planning/subtask-executor.js +345 -0
- package/dist/src/group/orchestrator/planning/summarizer-runner.js +224 -0
- package/dist/src/group/orchestrator/routes/index.js +9 -0
- package/dist/src/group/orchestrator/routes/leader-dispatch.js +229 -0
- package/dist/src/group/orchestrator/routes/leader-orchestration-route.js +179 -0
- package/dist/src/group/orchestrator/routes/leader-planning.js +92 -0
- package/dist/src/group/orchestrator/routes/leader-self-answer.js +223 -0
- package/dist/src/group/orchestrator/routes/mention-concurrent-route.js +226 -0
- package/dist/src/group/orchestrator/routes/route-helpers.js +186 -0
- package/dist/src/group/orchestrator/routes/types.js +8 -0
- package/dist/src/group/services/group-cleanup-service.js +183 -0
- package/dist/src/group/services/group-creation-service.js +122 -0
- package/dist/src/group/services/group-deletion-service.js +111 -0
- package/dist/src/group/services/group-history-service.js +73 -0
- package/dist/src/group/services/group-member-service.js +169 -0
- package/dist/src/group/services/group-query-service.js +133 -0
- package/dist/src/group/services/group-update-service.js +144 -0
- package/dist/src/group/services/index.js +20 -0
- package/dist/src/group/storage/concurrency-manager.js +119 -0
- package/dist/src/group/storage/group-storage-core.js +227 -0
- package/dist/src/group/storage/index.js +12 -0
- package/dist/src/group/storage/message-reader.js +213 -0
- package/dist/src/group/storage/message-writer.js +229 -0
- package/dist/src/group/storage/slice-manager.js +165 -0
- package/dist/src/group/types/common.js +5 -0
- package/dist/src/group/types/index.js +5 -0
- package/dist/src/group/types/message.js +5 -0
- package/dist/src/group/types/orchestrator.js +5 -0
- package/dist/src/group/types/storage.js +5 -0
- package/dist/src/group/utils/id-generator.js +15 -0
- package/dist/src/group/utils/index.js +12 -0
- package/dist/src/group/utils/mime.js +36 -0
- package/dist/src/group/utils/normalize.js +32 -0
- package/dist/src/group/utils/run-helpers.js +36 -0
- package/dist/src/history/session-reader.js +8 -2
- package/dist/src/outbound.js +12 -19
- package/dist/src/shared.js +4 -3
- package/dist/src/socket/events/agents-request.js +147 -0
- package/dist/src/socket/events/chat-request.js +67 -0
- package/dist/src/socket/events/file-download.js +121 -0
- package/dist/src/socket/events/group-abort.js +59 -0
- package/dist/src/socket/events/group-history.js +59 -0
- package/dist/src/socket/events/group-member.js +83 -0
- package/dist/src/socket/events/group-request.js +91 -0
- package/dist/src/socket/events/history-request.js +95 -0
- package/dist/src/socket/events/index.js +39 -0
- package/dist/src/socket/events/message-private.js +82 -0
- package/dist/src/socket/handlers.js +53 -568
- package/dist/src/socket/native-socket.js +21 -20
- package/dist/src/socket/registry.js +6 -3
- package/dist/src/socket/reliable-emitter.js +16 -13
- package/dist/src/socket/service/chat-common.js +36 -0
- package/dist/src/socket/service/chat-create.js +75 -0
- package/dist/src/socket/service/chat-delete.js +94 -0
- package/dist/src/socket/service/chat-list.js +82 -0
- package/dist/src/socket/service/chat-update.js +83 -0
- package/dist/src/socket/service/group-abort.js +104 -0
- package/dist/src/socket/service/group-history.js +140 -0
- package/dist/src/socket/service/group-member.js +209 -0
- package/dist/src/socket/service/group.js +233 -0
- package/dist/src/socket/service/history.js +102 -0
- package/dist/src/socket/service/index.js +14 -0
- package/dist/src/socket/types/index.js +7 -0
- package/dist/src/socket/types/request.js +8 -0
- package/dist/src/socket/types/service.js +8 -0
- package/dist/src/socket/utils/agent-soul.js +95 -0
- package/dist/src/socket/utils/index.js +8 -0
- package/dist/src/socket/utils/message.js +83 -0
- package/dist/src/socket/utils/validate.js +42 -0
- package/dist/src/streaming/index.js +1 -0
- package/dist/src/streaming/stream-reply-sink.js +367 -20
- package/dist/src/streaming/thinking-formatter.js +325 -0
- package/dist/src/streaming/types.js +20 -1
- package/dist/src/{download-tool.js → tools/download-tool.js} +41 -35
- package/dist/src/tools/group-history-tool.js +172 -0
- package/dist/src/{upload-tool.js → tools/upload-tool.js} +2 -2
- package/dist/src/tools.js +4 -3
- package/dist/src/utils/index.js +1 -0
- package/dist/src/utils/logger.js +38 -0
- package/openclaw.plugin.json +2 -1
- package/package.json +1 -1
- package/dist/src/socket/agent-soul.js +0 -41
- package/dist/src/socket/chat.js +0 -257
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview 路由层公共辅助工具集
|
|
3
|
+
*
|
|
4
|
+
* @description
|
|
5
|
+
* 提取自各路由(Leader / Mention)的通用辅助方法,
|
|
6
|
+
* 供 Planning / SelfAnswer / Dispatch / MentionConcurrent 各路径复用。
|
|
7
|
+
*
|
|
8
|
+
* 包含以下能力:
|
|
9
|
+
* - 日志标签构建(buildTag)
|
|
10
|
+
* - 群成员能力卡解析(resolveMemberCards)
|
|
11
|
+
* - Leader 系统提示词构造(buildLeaderPrompt)
|
|
12
|
+
* - 统一流式输出上下文工厂封装(createSinkForAgent)
|
|
13
|
+
* - 安全账本写入(safeAppendAssistant / safeAppendSystem)
|
|
14
|
+
*
|
|
15
|
+
* @module route-helpers
|
|
16
|
+
*/
|
|
17
|
+
// ─── 工具函数 ──────────────────────────────────────────────────────────────────────
|
|
18
|
+
import { createStreamReplyConfig } from '../../../streaming/stream-reply-sink.js';
|
|
19
|
+
import { getModuleLogger } from '../../../utils/logger.js';
|
|
20
|
+
import { generateMsgId } from '../../../dedup.js';
|
|
21
|
+
import { CHANNEL_KEY } from '../../../config.js';
|
|
22
|
+
/** 模块级日志记录器 */
|
|
23
|
+
const log = getModuleLogger('group.leader-route');
|
|
24
|
+
/**
|
|
25
|
+
* 禁用的内置工具列表(所有阶段通用)
|
|
26
|
+
*
|
|
27
|
+
* @remarks
|
|
28
|
+
* 这些工具在 Leader 编排场景下不允许被 Agent 调用,
|
|
29
|
+
* 因为会话管理和子 Agent 调度由编排层统一控制。
|
|
30
|
+
*/
|
|
31
|
+
export const DENY_TOOLS = [
|
|
32
|
+
'sessions_spawn',
|
|
33
|
+
'sessions_send',
|
|
34
|
+
'sessions_list',
|
|
35
|
+
'sessions_history',
|
|
36
|
+
'sessions_yield',
|
|
37
|
+
'session_status',
|
|
38
|
+
'sessions.spawn',
|
|
39
|
+
'subagents',
|
|
40
|
+
'agents_list',
|
|
41
|
+
];
|
|
42
|
+
/**
|
|
43
|
+
* 构建日志标签
|
|
44
|
+
*
|
|
45
|
+
* @param ctx - 调度上下文
|
|
46
|
+
* @returns 格式为 `[channel conv=xxx group=xxx]` 的标签字符串
|
|
47
|
+
*/
|
|
48
|
+
export function buildTag(ctx) {
|
|
49
|
+
return `[${CHANNEL_KEY} conv=${ctx.conversationId} group=${ctx.groupId}]`;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 安全解析群成员能力卡
|
|
53
|
+
*
|
|
54
|
+
* @remarks
|
|
55
|
+
* 通过 SoulResolver 将群成员元信息解析为结构化的能力卡。
|
|
56
|
+
* 解析失败时不抛出异常,降级返回空数组(Planning 阶段将视为无可派活成员)。
|
|
57
|
+
*
|
|
58
|
+
* @param deps - 路由依赖(使用 soulResolver)
|
|
59
|
+
* @param ctx - 调度上下文(使用 meta.members)
|
|
60
|
+
* @param tag - 日志标签
|
|
61
|
+
* @returns 解析后的能力卡数组,失败时为空数组
|
|
62
|
+
*/
|
|
63
|
+
export function resolveMemberCards(deps, ctx, tag) {
|
|
64
|
+
try {
|
|
65
|
+
return deps.soulResolver.resolveMany(ctx.meta.members);
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
log.warn(`${tag} 解析群成员能力卡失败: ${err},降级为空数组`);
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* 构造 Leader 系统提示词(不含 Planning 指令)
|
|
74
|
+
*
|
|
75
|
+
* @remarks
|
|
76
|
+
* 用于自答路径,构建 Leader 的角色定义和群成员信息提示词。
|
|
77
|
+
* 与 Planning 阶段的 `buildPlanner` 不同,此处不包含 JSON 输出格式约束。
|
|
78
|
+
* 构建失败时返回空字符串,Agent 将以默认行为运行。
|
|
79
|
+
*
|
|
80
|
+
* @param deps - 路由依赖(使用 promptBuilder)
|
|
81
|
+
* @param agentId - Leader Agent ID
|
|
82
|
+
* @param memberCards - 群成员能力卡
|
|
83
|
+
* @param tag - 日志标签
|
|
84
|
+
* @returns 系统提示词字符串,失败时为空
|
|
85
|
+
*/
|
|
86
|
+
export function buildLeaderPrompt(deps, agentId, memberCards, tag) {
|
|
87
|
+
try {
|
|
88
|
+
return deps.promptBuilder.buildLeader({ leaderAgentId: agentId, memberCards });
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
log.warn(`${tag} 构建 Leader 提示词失败: ${err}`);
|
|
92
|
+
return '';
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* 构造统一流式输出上下文(公共逻辑提取)
|
|
97
|
+
*
|
|
98
|
+
* @remarks
|
|
99
|
+
* 封装 `createStreamReplyConfig` 的参数组装逻辑,
|
|
100
|
+
* 自动从 ctx 和 deps 中提取所需字段,调用方只需关注业务参数。
|
|
101
|
+
* 返回值包含完整的 StreamReplySinkContext,可直接传给 AgentRunner。
|
|
102
|
+
*
|
|
103
|
+
* @param deps - 路由依赖(使用 emitter)
|
|
104
|
+
* @param ctx - 调度上下文(使用 userId、groupId、conversationId)
|
|
105
|
+
* @param agentId - 当前发言 Agent ID
|
|
106
|
+
* @param runId - 预分配的 runId
|
|
107
|
+
* @param originalMsgId - 触发本回合的客户端原始 msgId
|
|
108
|
+
* @param options - 可选配置(parentRunId)
|
|
109
|
+
* @returns StreamReplySinkContext 实例
|
|
110
|
+
*/
|
|
111
|
+
export function createSinkForAgent(deps, ctx, agentId, runId, originalMsgId, options) {
|
|
112
|
+
const replyMsgId = generateMsgId();
|
|
113
|
+
// 构造 SignalContext(与私聊一致的信号上下文)
|
|
114
|
+
const signalCtx = {
|
|
115
|
+
emitter: deps.emitter,
|
|
116
|
+
targetId: ctx.userId,
|
|
117
|
+
replyMsgId,
|
|
118
|
+
originalMsgId,
|
|
119
|
+
agentId,
|
|
120
|
+
};
|
|
121
|
+
return createStreamReplyConfig({
|
|
122
|
+
emitter: deps.emitter,
|
|
123
|
+
targetId: ctx.userId,
|
|
124
|
+
replyMsgId,
|
|
125
|
+
originalMsgId,
|
|
126
|
+
effectiveApiKey: ctx.effectiveApiKey ?? '', // 群聊场景:从 DispatchContext 获取 apiKey,用于 COS 上传
|
|
127
|
+
agentId,
|
|
128
|
+
// 群聊扩展字段
|
|
129
|
+
groupId: ctx.groupId,
|
|
130
|
+
runId,
|
|
131
|
+
parentRunId: options?.parentRunId ?? null,
|
|
132
|
+
conversationId: ctx.conversationId,
|
|
133
|
+
}, {
|
|
134
|
+
// 群聊不让 SDK 自动 deliver 给 channel
|
|
135
|
+
suppressTyping: true,
|
|
136
|
+
// 预指定 runId
|
|
137
|
+
runId,
|
|
138
|
+
// abort 信号由 AgentRunner 层注入,此处不传
|
|
139
|
+
}, signalCtx);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* 安全写 assistant 账本
|
|
143
|
+
*
|
|
144
|
+
* @remarks
|
|
145
|
+
* 将 Agent 的回复内容持久化到群账本。
|
|
146
|
+
* 写入失败时仅记录 warn 日志,不影响主流程执行。
|
|
147
|
+
*
|
|
148
|
+
* @param deps - 路由依赖(使用 ledger)
|
|
149
|
+
* @param groupId - 群 ID
|
|
150
|
+
* @param agentId - 发言 Agent ID
|
|
151
|
+
* @param content - 回复内容
|
|
152
|
+
* @param runId - 关联的 runId
|
|
153
|
+
* @param tag - 日志标签
|
|
154
|
+
*/
|
|
155
|
+
export async function safeAppendAssistant(deps, groupId, agentId, content, runId, tag) {
|
|
156
|
+
try {
|
|
157
|
+
await deps.ledger.appendAssistant({ groupId, agentId, content, runId, parentRunId: null });
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
log.warn(`${tag} 写入 assistant 账本失败: ${err}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* 安全写 system 账本
|
|
165
|
+
*
|
|
166
|
+
* @remarks
|
|
167
|
+
* 将系统事件(如 user_aborted、run_failed)持久化到群账本。
|
|
168
|
+
* 写入失败时仅记录 warn 日志,不影响主流程执行。
|
|
169
|
+
*
|
|
170
|
+
* @param deps - 路由依赖(使用 ledger)
|
|
171
|
+
* @param groupId - 群 ID
|
|
172
|
+
* @param event - 系统事件类型
|
|
173
|
+
* @param agentId - 关联 Agent ID
|
|
174
|
+
* @param userId - 触发用户 ID
|
|
175
|
+
* @param runId - 关联的 runId
|
|
176
|
+
* @param tag - 日志标签
|
|
177
|
+
* @param content - 可选的事件附加内容(如错误信息)
|
|
178
|
+
*/
|
|
179
|
+
export async function safeAppendSystem(deps, groupId, event, agentId, userId, runId, tag, content) {
|
|
180
|
+
try {
|
|
181
|
+
await deps.ledger.appendSystem({ groupId, event, agentId, userId, runId, content });
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
log.warn(`${tag} 写入 system 账本失败: ${err}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 群组清理服务
|
|
3
|
+
* 专门负责群组数据的异步清理操作,包括物理文件删除和Agent session清理
|
|
4
|
+
*
|
|
5
|
+
* 主要功能:
|
|
6
|
+
* - 群组目录清理:递归删除群组物理存储数据
|
|
7
|
+
* - Agent session清理:删除相关Agent的session文件
|
|
8
|
+
* - 错误处理:完善的异常捕获和日志记录
|
|
9
|
+
*
|
|
10
|
+
* 设计特点:
|
|
11
|
+
* - 异步操作避免阻塞主线程
|
|
12
|
+
* - 安全的文件删除操作
|
|
13
|
+
* - 详细的日志记录
|
|
14
|
+
*/
|
|
15
|
+
import { promises as fs } from 'fs';
|
|
16
|
+
import path from 'path';
|
|
17
|
+
import os from 'os';
|
|
18
|
+
import { GroupStorageCore } from '../storage/index.js';
|
|
19
|
+
import { CHANNEL_KEY } from '../../config.js';
|
|
20
|
+
export class GroupCleanupService {
|
|
21
|
+
/**
|
|
22
|
+
* 群组存储服务实例
|
|
23
|
+
*/
|
|
24
|
+
storage;
|
|
25
|
+
/**
|
|
26
|
+
* 构造函数
|
|
27
|
+
* 初始化清理服务的依赖组件
|
|
28
|
+
*/
|
|
29
|
+
constructor() {
|
|
30
|
+
this.storage = new GroupStorageCore();
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 异步清理群数据
|
|
34
|
+
* 在后台删除群组的物理存储数据
|
|
35
|
+
*
|
|
36
|
+
* @param groupId - 要清理的群组ID
|
|
37
|
+
*
|
|
38
|
+
* 清理内容:
|
|
39
|
+
* - 删除群组目录(递归删除所有文件)
|
|
40
|
+
* - 清理各Agent的session文件
|
|
41
|
+
* - 错误处理和日志记录
|
|
42
|
+
*/
|
|
43
|
+
async cleanupGroup(groupId) {
|
|
44
|
+
try {
|
|
45
|
+
const groupDir = path.join(this.storage.getBasePath(), groupId);
|
|
46
|
+
await fs.rm(groupDir, { recursive: true, force: true });
|
|
47
|
+
// 清理各Agent的session文件
|
|
48
|
+
await this.cleanupAgentSessions(groupId);
|
|
49
|
+
console.log(`群组 ${groupId} 清理完成`);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
console.error(`清理群组 ${groupId} 失败:`, error);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* 异步清空群组聊天账本
|
|
57
|
+
* 仅清理消息账本相关文件,保留 meta.json 等群组元数据,
|
|
58
|
+
* 同时清理各 Agent 与该群组关联的 session,保证下一轮对话从干净状态开始。
|
|
59
|
+
*
|
|
60
|
+
* @param groupId - 要清空账本的群组ID
|
|
61
|
+
*
|
|
62
|
+
* 清理内容:
|
|
63
|
+
* - 删除分片索引文件 index.json
|
|
64
|
+
* - 删除所有 chat-*.jsonl 账本分片文件
|
|
65
|
+
* - 清理各 Agent 的群组 session 文件
|
|
66
|
+
* - 保留 meta.json 等群组元数据,不影响群成员、置顶等配置
|
|
67
|
+
*/
|
|
68
|
+
async clearGroupTranscript(groupId) {
|
|
69
|
+
try {
|
|
70
|
+
const groupDir = path.join(this.storage.getBasePath(), groupId);
|
|
71
|
+
// 群组目录不存在时无需清理
|
|
72
|
+
try {
|
|
73
|
+
await fs.access(groupDir);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
console.warn(`群组 ${groupId} 目录不存在,跳过账本清理`);
|
|
77
|
+
// 仍然尝试清理 Agent session,避免有残留
|
|
78
|
+
await this.cleanupAgentSessions(groupId);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
// 仅删除账本相关文件(保留 meta.json)
|
|
82
|
+
const entries = await fs.readdir(groupDir);
|
|
83
|
+
await Promise.all(entries
|
|
84
|
+
.filter((name) => name === 'index.json' || /^chat-.*\.jsonl$/.test(name))
|
|
85
|
+
.map((name) => fs.rm(path.join(groupDir, name), { force: true })));
|
|
86
|
+
// 清理各 Agent 的 session 文件,保证对话上下文同步重置
|
|
87
|
+
await this.cleanupAgentSessions(groupId);
|
|
88
|
+
console.log(`群组 ${groupId} 账本清理完成(已保留元数据)`);
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
console.error(`清空群组 ${groupId} 账本失败:`, error);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* 清理群组相关Agent的session文件
|
|
96
|
+
* 删除群组中所有Agent的session数据,避免残留
|
|
97
|
+
*
|
|
98
|
+
* @param groupId - 要清理的群组ID
|
|
99
|
+
*/
|
|
100
|
+
async cleanupAgentSessions(groupId) {
|
|
101
|
+
try {
|
|
102
|
+
// 读取群组元数据获取成员信息
|
|
103
|
+
const groupMeta = await this.storage.readGroupMeta(groupId);
|
|
104
|
+
if (!groupMeta) {
|
|
105
|
+
console.warn(`群组 ${groupId} 元数据不存在,跳过Agent session清理`);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
// 获取所有需要清理的Agent ID(包括默认代理)
|
|
109
|
+
const memberIds = groupMeta.members.map((m) => m.agentId);
|
|
110
|
+
const agentIds = [...new Set([...memberIds, groupMeta.defaultAgentId])];
|
|
111
|
+
for (const agentId of agentIds) {
|
|
112
|
+
await this.cleanupAgentSessionFiles(agentId, groupId);
|
|
113
|
+
}
|
|
114
|
+
console.log(`群组 ${groupId} 相关Agent session文件清理完成`);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
console.error(`清理群组 ${groupId} 的Agent session文件失败:`, error);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* 清理单个Agent的群组相关session文件
|
|
122
|
+
* 删除Agent session目录中与该群组相关的文件,并更新sessions.json配置
|
|
123
|
+
*
|
|
124
|
+
* @param agentId - Agent ID
|
|
125
|
+
* @param groupId - 群组ID
|
|
126
|
+
*/
|
|
127
|
+
async cleanupAgentSessionFiles(agentId, groupId) {
|
|
128
|
+
try {
|
|
129
|
+
const sessionsDir = path.join(os.homedir(), '.openclaw', 'agents', agentId, 'sessions');
|
|
130
|
+
// 检查session目录是否存在
|
|
131
|
+
try {
|
|
132
|
+
await fs.access(sessionsDir);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// 目录不存在,无需清理
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
// 读取sessions.json文件
|
|
139
|
+
const sessionsJsonPath = path.join(sessionsDir, 'sessions.json');
|
|
140
|
+
let sessionsData = {};
|
|
141
|
+
try {
|
|
142
|
+
const sessionsContent = await fs.readFile(sessionsJsonPath, 'utf-8');
|
|
143
|
+
sessionsData = JSON.parse(sessionsContent);
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
// sessions.json文件不存在或解析失败,无需清理
|
|
147
|
+
console.warn(`无法读取Agent ${agentId} 的sessions.json文件:`, error);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
// 构建sessionKey格式:agent:<agentId>:<channel>:group:<groupId>
|
|
151
|
+
const sessionKey = `agent:${agentId}:${CHANNEL_KEY}:group:${groupId}`;
|
|
152
|
+
// 在sessions.json中查找对应的session配置
|
|
153
|
+
const sessionEntry = sessionsData[sessionKey];
|
|
154
|
+
if (!sessionEntry || !sessionEntry.sessionId) {
|
|
155
|
+
// 没有找到对应的session配置,无需清理
|
|
156
|
+
console.log(`Agent ${agentId} 的session.json中未找到群组 ${groupId} 的session配置`);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const sessionId = sessionEntry.sessionId;
|
|
160
|
+
const jsonlFileName = `${sessionId}.jsonl`;
|
|
161
|
+
const jsonlFilePath = path.join(sessionsDir, jsonlFileName);
|
|
162
|
+
// 删除对应的jsonl文件
|
|
163
|
+
try {
|
|
164
|
+
await fs.access(jsonlFilePath);
|
|
165
|
+
await fs.rm(jsonlFilePath, { force: true });
|
|
166
|
+
console.log(`删除Agent ${agentId} 的群组session文件: ${jsonlFileName}`);
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
console.warn(`无法删除Agent ${agentId} 的session文件 ${jsonlFileName}:`, error);
|
|
170
|
+
}
|
|
171
|
+
// 从sessions.json中删除对应的session配置(重要:避免文件膨胀)
|
|
172
|
+
if (sessionsData[sessionKey]) {
|
|
173
|
+
delete sessionsData[sessionKey];
|
|
174
|
+
// 保存更新后的sessions.json文件
|
|
175
|
+
await fs.writeFile(sessionsJsonPath, JSON.stringify(sessionsData, null, 2), 'utf-8');
|
|
176
|
+
console.log(`从Agent ${agentId} 的sessions.json中删除群组 ${groupId} 的session配置`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
console.error(`清理Agent ${agentId} 的session文件失败:`, error);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 群组创建服务
|
|
3
|
+
* 专门负责群组的创建操作,包括参数校验、UUID生成和并发控制
|
|
4
|
+
*
|
|
5
|
+
* 主要功能:
|
|
6
|
+
* - 参数校验:验证群组名称、成员列表、默认代理等参数
|
|
7
|
+
* - UUID生成:为新建群组生成唯一标识符
|
|
8
|
+
* - 并发控制:确保创建操作的原子性
|
|
9
|
+
* - 元数据构建:创建完整的群组元数据
|
|
10
|
+
*
|
|
11
|
+
* 设计特点:
|
|
12
|
+
* - 完整的参数验证链
|
|
13
|
+
* - 线程安全的创建操作
|
|
14
|
+
* - 与存储服务解耦
|
|
15
|
+
*/
|
|
16
|
+
import { randomUUID } from 'node:crypto';
|
|
17
|
+
import { GroupStorageCore } from '../storage/index.js';
|
|
18
|
+
import { ConcurrencyManager } from '../storage/concurrency-manager.js';
|
|
19
|
+
export class GroupCreationService {
|
|
20
|
+
/**
|
|
21
|
+
* 群组存储服务实例
|
|
22
|
+
*/
|
|
23
|
+
storage;
|
|
24
|
+
/**
|
|
25
|
+
* 并发控制管理器实例
|
|
26
|
+
*/
|
|
27
|
+
concurrency;
|
|
28
|
+
/**
|
|
29
|
+
* 构造函数
|
|
30
|
+
* 初始化创建服务的依赖组件
|
|
31
|
+
*/
|
|
32
|
+
constructor() {
|
|
33
|
+
this.storage = new GroupStorageCore();
|
|
34
|
+
this.concurrency = new ConcurrencyManager();
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 创建新群组
|
|
38
|
+
* 执行完整的群组创建流程
|
|
39
|
+
*
|
|
40
|
+
* @param creatorId - 创建者用户ID
|
|
41
|
+
* @param name - 群组名称
|
|
42
|
+
* @param desc - 群组描述
|
|
43
|
+
* @param members - 成员列表(每个元素包含 agentId / agentName / agentDesc)
|
|
44
|
+
* @param defaultAgentId - 默认代理ID
|
|
45
|
+
* @returns 新建群组的元数据
|
|
46
|
+
*
|
|
47
|
+
* 创建流程:
|
|
48
|
+
* 1. 参数校验
|
|
49
|
+
* 2. 生成群组ID
|
|
50
|
+
* 3. 构建元数据
|
|
51
|
+
* 4. 使用锁确保原子性
|
|
52
|
+
* 5. 写入元数据和创建事件
|
|
53
|
+
*/
|
|
54
|
+
async createGroup(creatorId, name, desc = '', members, defaultAgentId) {
|
|
55
|
+
// 参数校验
|
|
56
|
+
this.validateCreateParams(name, members, defaultAgentId);
|
|
57
|
+
const groupId = randomUUID();
|
|
58
|
+
const now = Date.now();
|
|
59
|
+
const groupMeta = {
|
|
60
|
+
groupId,
|
|
61
|
+
ownerIds: [creatorId],
|
|
62
|
+
name,
|
|
63
|
+
desc,
|
|
64
|
+
members,
|
|
65
|
+
defaultAgentId,
|
|
66
|
+
pinned: false,
|
|
67
|
+
createdAt: now,
|
|
68
|
+
updatedAt: now,
|
|
69
|
+
};
|
|
70
|
+
// 使用锁确保原子性
|
|
71
|
+
await this.concurrency.withLock(`group_create_${groupId}`, async () => {
|
|
72
|
+
await this.storage.writeGroupMeta(groupId, groupMeta);
|
|
73
|
+
// 写入群创建事件(账本 extra.members 仅记录 agentId 列表,
|
|
74
|
+
// 因为账本 system 事件用于追踪「事件涉及的参与者 ID」,不承载完整业务对象)
|
|
75
|
+
await this.storage.appendTranscriptEntry(groupId, {
|
|
76
|
+
role: 'system',
|
|
77
|
+
event: 'group_created',
|
|
78
|
+
msgId: `sys:group_created:${groupId}`,
|
|
79
|
+
extra: {
|
|
80
|
+
members: members,
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
}, 30000); // 30秒超时
|
|
84
|
+
return groupMeta;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* 验证创建参数
|
|
88
|
+
* 检查群组创建参数的有效性
|
|
89
|
+
*
|
|
90
|
+
* @param name - 群组名称
|
|
91
|
+
* @param members - 成员列表
|
|
92
|
+
* @param defaultAgentId - 默认代理ID
|
|
93
|
+
* @throws 如果参数无效则抛出错误
|
|
94
|
+
*/
|
|
95
|
+
validateCreateParams(name, members, defaultAgentId) {
|
|
96
|
+
if (!name || name.length > 50) {
|
|
97
|
+
throw new Error('无效的群组名称:不能为空且长度不超过50个字符');
|
|
98
|
+
}
|
|
99
|
+
if (!members || members.length === 0 || members.length > 10) {
|
|
100
|
+
throw new Error('无效的成员列表:必须有1-10个成员');
|
|
101
|
+
}
|
|
102
|
+
// 单成员对象字段必填校验:避免 agentId 缺失导致后续逻辑越界
|
|
103
|
+
for (let i = 0; i < members.length; i++) {
|
|
104
|
+
const m = members[i];
|
|
105
|
+
if (!m || typeof m !== 'object') {
|
|
106
|
+
throw new Error(`无效的成员条目[${i}]:必须是对象`);
|
|
107
|
+
}
|
|
108
|
+
if (!m.agentId || typeof m.agentId !== 'string') {
|
|
109
|
+
throw new Error(`无效的成员条目[${i}]:缺少有效的 agentId`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const memberIds = members.map((m) => m.agentId);
|
|
113
|
+
if (!defaultAgentId || !memberIds.includes(defaultAgentId)) {
|
|
114
|
+
throw new Error('无效的主 Agent:必须在成员列表中');
|
|
115
|
+
}
|
|
116
|
+
// 去重校验
|
|
117
|
+
const uniqueMembers = [...new Set(memberIds)];
|
|
118
|
+
if (uniqueMembers.length !== memberIds.length) {
|
|
119
|
+
throw new Error('成员列表中存在重复的代理ID');
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 群组删除服务
|
|
3
|
+
* 专门负责群组的解散操作,包括权限验证和软删除标记
|
|
4
|
+
*
|
|
5
|
+
* 主要功能:
|
|
6
|
+
* - 权限验证:检查用户是否有权限解散群组
|
|
7
|
+
* - 软删除标记:标记群组为解散状态
|
|
8
|
+
* - 并发控制:确保删除操作的原子性
|
|
9
|
+
* - 异步清理触发:启动后台清理任务
|
|
10
|
+
*
|
|
11
|
+
* 设计特点:
|
|
12
|
+
* - 安全的软删除机制
|
|
13
|
+
* - 异步清理避免阻塞
|
|
14
|
+
* - 与清理服务解耦
|
|
15
|
+
*/
|
|
16
|
+
import { GroupStorageCore } from '../storage/index.js';
|
|
17
|
+
import { ConcurrencyManager } from '../storage/concurrency-manager.js';
|
|
18
|
+
import { GroupCleanupService } from './group-cleanup-service.js';
|
|
19
|
+
export class GroupDeletionService {
|
|
20
|
+
/**
|
|
21
|
+
* 群组存储服务实例
|
|
22
|
+
*/
|
|
23
|
+
storage;
|
|
24
|
+
/**
|
|
25
|
+
* 并发控制管理器实例
|
|
26
|
+
*/
|
|
27
|
+
concurrency;
|
|
28
|
+
/**
|
|
29
|
+
* 群组清理服务实例
|
|
30
|
+
*/
|
|
31
|
+
cleanupService;
|
|
32
|
+
/**
|
|
33
|
+
* 构造函数
|
|
34
|
+
* 初始化删除服务的依赖组件
|
|
35
|
+
*/
|
|
36
|
+
constructor() {
|
|
37
|
+
this.storage = new GroupStorageCore();
|
|
38
|
+
this.concurrency = new ConcurrencyManager();
|
|
39
|
+
this.cleanupService = new GroupCleanupService();
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* 解散群组
|
|
43
|
+
* 执行群组的解散操作
|
|
44
|
+
*
|
|
45
|
+
* @param groupId - 群组ID
|
|
46
|
+
* @param deleterId - 解散者用户ID
|
|
47
|
+
* @returns 解散后的群组元数据
|
|
48
|
+
*
|
|
49
|
+
* 解散流程:
|
|
50
|
+
* 1. 权限验证(群组存在且用户是拥有者)
|
|
51
|
+
* 2. 标记群组为解散状态
|
|
52
|
+
* 3. 异步启动清理任务
|
|
53
|
+
*/
|
|
54
|
+
async deleteGroup(groupId, deleterId) {
|
|
55
|
+
return await this.concurrency.withLock(`group_delete_${groupId}`, async () => {
|
|
56
|
+
const existingMeta = await this.storage.readGroupMeta(groupId);
|
|
57
|
+
if (!existingMeta) {
|
|
58
|
+
throw new Error('群组不存在');
|
|
59
|
+
}
|
|
60
|
+
if (!existingMeta.ownerIds.includes(deleterId)) {
|
|
61
|
+
throw new Error('权限不足');
|
|
62
|
+
}
|
|
63
|
+
// 标记解散
|
|
64
|
+
const dismissedMeta = {
|
|
65
|
+
...existingMeta,
|
|
66
|
+
dismissed: true,
|
|
67
|
+
updatedAt: Date.now(),
|
|
68
|
+
};
|
|
69
|
+
await this.storage.writeGroupMeta(groupId, dismissedMeta);
|
|
70
|
+
// 异步清理(不阻塞响应)
|
|
71
|
+
setTimeout(() => this.cleanupService.cleanupGroup(groupId), 0);
|
|
72
|
+
return dismissedMeta;
|
|
73
|
+
}, 30000); // 30秒超时
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* 清空群组聊天账本
|
|
77
|
+
* 仅清理消息记录与相关 Agent session,保留群组元数据(成员、名称、置顶等不变)
|
|
78
|
+
*
|
|
79
|
+
* @param groupId - 群组ID
|
|
80
|
+
* @param operatorId - 操作者用户ID(要求是群拥有者)
|
|
81
|
+
* @returns 操作完成后的群组元数据(仅刷新 updatedAt)
|
|
82
|
+
*
|
|
83
|
+
* 清空流程:
|
|
84
|
+
* 1. 权限验证(群组存在、未解散、用户是拥有者)
|
|
85
|
+
* 2. 异步触发账本清理任务(保留 meta.json)
|
|
86
|
+
* 3. 刷新 updatedAt 并返回最新元数据
|
|
87
|
+
*/
|
|
88
|
+
async clearGroup(groupId, operatorId) {
|
|
89
|
+
return await this.concurrency.withLock(`group_clear_${groupId}`, async () => {
|
|
90
|
+
const existingMeta = await this.storage.readGroupMeta(groupId);
|
|
91
|
+
if (!existingMeta) {
|
|
92
|
+
throw new Error('群组不存在');
|
|
93
|
+
}
|
|
94
|
+
if (existingMeta.dismissed) {
|
|
95
|
+
throw new Error('群组已解散');
|
|
96
|
+
}
|
|
97
|
+
if (!existingMeta.ownerIds.includes(operatorId)) {
|
|
98
|
+
throw new Error('权限不足');
|
|
99
|
+
}
|
|
100
|
+
// 仅刷新更新时间,元数据其它字段保持不变
|
|
101
|
+
const updatedMeta = {
|
|
102
|
+
...existingMeta,
|
|
103
|
+
updatedAt: Date.now(),
|
|
104
|
+
};
|
|
105
|
+
await this.storage.writeGroupMeta(groupId, updatedMeta);
|
|
106
|
+
// 异步清空账本(不阻塞响应)
|
|
107
|
+
setTimeout(() => this.cleanupService.clearGroupTranscript(groupId), 0);
|
|
108
|
+
return updatedMeta;
|
|
109
|
+
}, 30000); // 30秒超时
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file group-history-service.ts
|
|
3
|
+
* @description
|
|
4
|
+
* 群历史消息查询服务(业务层)。
|
|
5
|
+
*
|
|
6
|
+
* 职责:
|
|
7
|
+
* 1. 校验目标群是否存在 + 当前用户是否为 ownerIds 成员(越权拦截);
|
|
8
|
+
* 2. 调用 GroupStorageCore.readTranscriptEntries 进行底层游标分页读取;
|
|
9
|
+
* 3. 统一返回 { messages, hasMore, nextCursor } 给 socket 层,
|
|
10
|
+
* 失败兜底由 socket 层捕获异常处理(与现有 list/create/update/delete
|
|
11
|
+
* 同款 try/catch 模式)。
|
|
12
|
+
*
|
|
13
|
+
* 与 storage 层的分工:
|
|
14
|
+
* - storage 层只关心"目录里有什么 jsonl",不感知群的权限语义;
|
|
15
|
+
* - service 层负责"这个群能不能给这个用户看",不直接接触文件系统。
|
|
16
|
+
*
|
|
17
|
+
* 设计参考:群聊方案 §6.5 群历史消息模块
|
|
18
|
+
*/
|
|
19
|
+
import { GroupStorageCore } from '../storage/index.js';
|
|
20
|
+
/**
|
|
21
|
+
* 群历史消息服务。
|
|
22
|
+
*
|
|
23
|
+
* 单例(在 socket 层 lazy new),无内部状态,复用同一个 GroupStorageCore
|
|
24
|
+
* 实例以共享 MessageReader 内的文件缓存。
|
|
25
|
+
*/
|
|
26
|
+
export class GroupHistoryService {
|
|
27
|
+
/** 群存储核心:负责读 meta.json 鉴权 + 读 chat.jsonl 取历史 */
|
|
28
|
+
storage;
|
|
29
|
+
constructor(storage = new GroupStorageCore()) {
|
|
30
|
+
this.storage = storage;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 清除内部消息读取器的文件缓存。
|
|
34
|
+
* 在成员变更等写入操作后调用,确保下次 getHistory 读取到最新数据。
|
|
35
|
+
*/
|
|
36
|
+
clearReaderCache() {
|
|
37
|
+
this.storage.clearReaderCache();
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* 查询群历史消息。
|
|
41
|
+
*
|
|
42
|
+
* 流程:
|
|
43
|
+
* 1. 读 meta.json:群不存在 → 抛 'Group not found';
|
|
44
|
+
* 2. 校验 ownerIds.includes(userId):不通过 → 抛 'Permission denied';
|
|
45
|
+
* 3. 调 storage.readTranscriptEntries 走分片读取 + 时间游标分页;
|
|
46
|
+
* 4. storage 返回 { entries, hasMore, nextCursor },按协议字段名映射。
|
|
47
|
+
*
|
|
48
|
+
* @throws 'Group not found' | 'Permission denied'
|
|
49
|
+
* | 文件读取异常(由 storage 层抛出,socket 层兜底回 error)
|
|
50
|
+
*/
|
|
51
|
+
async getHistory(query) {
|
|
52
|
+
const { groupId, userId, limit, before, chatOnly } = query;
|
|
53
|
+
// ① 群存在性 + 越权校验
|
|
54
|
+
const meta = await this.storage.readGroupMeta(groupId);
|
|
55
|
+
if (!meta) {
|
|
56
|
+
throw new Error('Group not found');
|
|
57
|
+
}
|
|
58
|
+
if (!meta.ownerIds.includes(userId)) {
|
|
59
|
+
throw new Error('Permission denied');
|
|
60
|
+
}
|
|
61
|
+
// ② 读取群账本(storage 内部已做 ENOENT 兜底 → 空结果)
|
|
62
|
+
const result = await this.storage.readTranscriptEntries(groupId, {
|
|
63
|
+
limit,
|
|
64
|
+
before,
|
|
65
|
+
chatOnly,
|
|
66
|
+
});
|
|
67
|
+
return {
|
|
68
|
+
messages: result.entries,
|
|
69
|
+
hasMore: result.hasMore,
|
|
70
|
+
nextCursor: result.nextCursor,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|