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,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 群成员管理服务
|
|
3
|
+
*
|
|
4
|
+
* 负责群组成员的「加入」与「移除」两类原子操作,是群聊方案 §6.4
|
|
5
|
+
* (`group:member:request`) 在领域层的具体实现。
|
|
6
|
+
*
|
|
7
|
+
* 关键约束(来自群聊方案 §2.3 A4 / §6.4.3):
|
|
8
|
+
* - 加成员:当前 members.length < 10,且 agentId 必须合法(在 agents.list 中)
|
|
9
|
+
* 且不在 members 中;
|
|
10
|
+
* - 踢成员:agentId !== meta.defaultAgentId(不能踢主 Agent),且 agentId
|
|
11
|
+
* 必须 ∈ members;
|
|
12
|
+
* - 操作必须由 ownerIds 中的用户发起(与 update/delete 一致,避免越权);
|
|
13
|
+
* - 成员变更后写入 system 事件到群账本:member_added / member_removed,
|
|
14
|
+
* 便于前端回放历史时还原"加群/踢出"动作(§3.4 群账本设计)。
|
|
15
|
+
*
|
|
16
|
+
* 设计选择:
|
|
17
|
+
* - 与 GroupUpdateService 一样使用 ConcurrencyManager 加 `group_member_<groupId>`
|
|
18
|
+
* 锁,避免并发 add/remove 造成 members 列表错乱;
|
|
19
|
+
* - agentId 是否合法(在 agents.list 中)由 socket 层注入校验函数,
|
|
20
|
+
* service 层只关心"群内成员一致性"。这样保留域服务的纯粹性,
|
|
21
|
+
* 不直接依赖 lightclaw runtime / config,便于单元测试。
|
|
22
|
+
*/
|
|
23
|
+
import { GroupStorageCore } from '../storage/index.js';
|
|
24
|
+
import { ConcurrencyManager } from '../storage/concurrency-manager.js';
|
|
25
|
+
import { generateMsgId } from '../../dedup.js';
|
|
26
|
+
export class GroupMemberService {
|
|
27
|
+
/** 群组存储服务实例(meta 读写 + 群账本追加) */
|
|
28
|
+
storage;
|
|
29
|
+
/** 并发控制管理器实例(按 groupId 加锁) */
|
|
30
|
+
concurrency;
|
|
31
|
+
/**
|
|
32
|
+
* 群成员上限(来自 §2.3 A1:Agent 数量 ≤ 10,本期 §6.4.3 校验文案 max 10)
|
|
33
|
+
*/
|
|
34
|
+
static MAX_MEMBERS = 10;
|
|
35
|
+
constructor() {
|
|
36
|
+
this.storage = new GroupStorageCore();
|
|
37
|
+
this.concurrency = new ConcurrencyManager();
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* 添加 Agent 到群组
|
|
41
|
+
*
|
|
42
|
+
* 流程:
|
|
43
|
+
* 1. 锁内读取 meta:群必须存在 + 未解散 + 当前用户在 ownerIds 内;
|
|
44
|
+
* 2. agentId 合法性校验(可选)+ 不在 members + members 未达上限;
|
|
45
|
+
* 3. 末尾追加 agentId → 写回 meta.json(updatedAt 自动刷新);
|
|
46
|
+
* 4. 群账本追加 system: member_added 事件(agentId 落在 entry.agentId)。
|
|
47
|
+
*
|
|
48
|
+
* @param groupId 目标群 ID
|
|
49
|
+
* @param updaterId 发起操作的用户 ID(必须 ∈ ownerIds)
|
|
50
|
+
* @param agentId 要加入的 Agent ID
|
|
51
|
+
* @param options 可选:注入 agentId 合法性校验
|
|
52
|
+
* @returns 操作完成后的 meta 与 members
|
|
53
|
+
*
|
|
54
|
+
* @throws Error 群不存在 / 已解散 / 越权 / agentId 非法 / 已在群内 / 群已满
|
|
55
|
+
*/
|
|
56
|
+
async addMember(groupId, updaterId, agentId, options = {}) {
|
|
57
|
+
if (!agentId || !agentId.trim()) {
|
|
58
|
+
throw new Error('无效的 agentId:不能为空');
|
|
59
|
+
}
|
|
60
|
+
const trimmedAgentId = agentId.trim();
|
|
61
|
+
return await this.concurrency.withLock(`group_member_${groupId}`, async () => {
|
|
62
|
+
const meta = await this.requireWritableMeta(groupId, updaterId);
|
|
63
|
+
if (options.isValidAgentId && !options.isValidAgentId(trimmedAgentId)) {
|
|
64
|
+
throw new Error(`无效的 agentId:${trimmedAgentId} 不在 agents.list 中`);
|
|
65
|
+
}
|
|
66
|
+
// 基于 agentId 做去重判断,避免对象比较带来的歧义
|
|
67
|
+
if (meta.members.some((m) => m.agentId === trimmedAgentId)) {
|
|
68
|
+
throw new Error('Agent already in group');
|
|
69
|
+
}
|
|
70
|
+
if (meta.members.length >= GroupMemberService.MAX_MEMBERS) {
|
|
71
|
+
throw new Error(`Group is full (max ${GroupMemberService.MAX_MEMBERS})`);
|
|
72
|
+
}
|
|
73
|
+
// 通过注入的解析器拿到 agentName / agentDesc,缺省时以 agentId 兜底,
|
|
74
|
+
// 保证 GroupMember 三个字段始终被填写
|
|
75
|
+
const resolved = options.resolveAgentMeta?.(trimmedAgentId);
|
|
76
|
+
const newMember = {
|
|
77
|
+
agentId: trimmedAgentId,
|
|
78
|
+
agentName: resolved?.agentName ?? trimmedAgentId,
|
|
79
|
+
agentDesc: resolved?.agentDesc ?? '',
|
|
80
|
+
};
|
|
81
|
+
const updatedMeta = {
|
|
82
|
+
...meta,
|
|
83
|
+
members: [...meta.members, newMember],
|
|
84
|
+
updatedAt: Date.now(),
|
|
85
|
+
};
|
|
86
|
+
await this.storage.writeGroupMeta(groupId, updatedMeta);
|
|
87
|
+
await this.storage.appendTranscriptEntry(groupId, {
|
|
88
|
+
role: 'system',
|
|
89
|
+
event: 'member_added',
|
|
90
|
+
agentId: trimmedAgentId,
|
|
91
|
+
msgId: generateMsgId(),
|
|
92
|
+
});
|
|
93
|
+
return {
|
|
94
|
+
meta: updatedMeta,
|
|
95
|
+
members: [...updatedMeta.members],
|
|
96
|
+
};
|
|
97
|
+
}, 30000);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* 从群组踢出 Agent
|
|
101
|
+
*
|
|
102
|
+
* 流程:
|
|
103
|
+
* 1. 锁内读取 meta:群必须存在 + 未解散 + 当前用户在 ownerIds 内;
|
|
104
|
+
* 2. 不能踢主 Agent(defaultAgentId) + agentId 必须 ∈ members;
|
|
105
|
+
* 3. 从 members 中移除 → 写回 meta.json(updatedAt 自动刷新);
|
|
106
|
+
* 4. 群账本追加 system: member_removed 事件。
|
|
107
|
+
*
|
|
108
|
+
* 注意:被踢 Agent 在该群内的历史发言保留(不删 sessions/.jsonl 与账本旧条目),
|
|
109
|
+
* 仅"不再被调度"——这是群聊方案 §6.4.3 B 的硬性约束。
|
|
110
|
+
*
|
|
111
|
+
* @param groupId 目标群 ID
|
|
112
|
+
* @param updaterId 发起操作的用户 ID(必须 ∈ ownerIds)
|
|
113
|
+
* @param agentId 要踢出的 Agent ID
|
|
114
|
+
* @returns 操作完成后的 meta 与 members
|
|
115
|
+
*
|
|
116
|
+
* @throws Error 群不存在 / 已解散 / 越权 / 试图踢主 Agent / agentId 不在群内
|
|
117
|
+
*/
|
|
118
|
+
async removeMember(groupId, updaterId, agentId) {
|
|
119
|
+
if (!agentId || !agentId.trim()) {
|
|
120
|
+
throw new Error('无效的 agentId:不能为空');
|
|
121
|
+
}
|
|
122
|
+
const trimmedAgentId = agentId.trim();
|
|
123
|
+
return await this.concurrency.withLock(`group_member_${groupId}`, async () => {
|
|
124
|
+
const meta = await this.requireWritableMeta(groupId, updaterId);
|
|
125
|
+
if (trimmedAgentId === meta.defaultAgentId) {
|
|
126
|
+
throw new Error('Cannot remove default agent');
|
|
127
|
+
}
|
|
128
|
+
if (!meta.members.some((m) => m.agentId === trimmedAgentId)) {
|
|
129
|
+
throw new Error('Agent not in group');
|
|
130
|
+
}
|
|
131
|
+
const updatedMeta = {
|
|
132
|
+
...meta,
|
|
133
|
+
members: meta.members.filter((m) => m.agentId !== trimmedAgentId),
|
|
134
|
+
updatedAt: Date.now(),
|
|
135
|
+
};
|
|
136
|
+
await this.storage.writeGroupMeta(groupId, updatedMeta);
|
|
137
|
+
await this.storage.appendTranscriptEntry(groupId, {
|
|
138
|
+
role: 'system',
|
|
139
|
+
event: 'member_removed',
|
|
140
|
+
agentId: trimmedAgentId,
|
|
141
|
+
msgId: generateMsgId(),
|
|
142
|
+
});
|
|
143
|
+
return {
|
|
144
|
+
meta: updatedMeta,
|
|
145
|
+
members: [...updatedMeta.members],
|
|
146
|
+
};
|
|
147
|
+
}, 30000);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* 读取并校验"可写"状态的群元数据。
|
|
151
|
+
*
|
|
152
|
+
* 把 add / remove 共用的前置校验抽到这里,保证两条链路对"群存在性 +
|
|
153
|
+
* 解散态 + 越权"的处理完全一致:任一不通过即抛错;通过则返回原 meta,
|
|
154
|
+
* 由调用方做后续业务字段变更。
|
|
155
|
+
*/
|
|
156
|
+
async requireWritableMeta(groupId, updaterId) {
|
|
157
|
+
const meta = await this.storage.readGroupMeta(groupId);
|
|
158
|
+
if (!meta) {
|
|
159
|
+
throw new Error('Group not found');
|
|
160
|
+
}
|
|
161
|
+
if (meta.dismissed === true) {
|
|
162
|
+
throw new Error('Group has been dismissed');
|
|
163
|
+
}
|
|
164
|
+
if (!meta.ownerIds.includes(updaterId)) {
|
|
165
|
+
throw new Error('Permission denied');
|
|
166
|
+
}
|
|
167
|
+
return meta;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 群组查询服务
|
|
3
|
+
* 专门负责群组数据的查询操作,包括列表查询、过滤和排序
|
|
4
|
+
*
|
|
5
|
+
* 性能优化特性:
|
|
6
|
+
* - 用户群组列表缓存:减少重复的文件系统操作
|
|
7
|
+
* - 批量元数据读取:优化多群组查询性能
|
|
8
|
+
* - 智能缓存失效:基于文件修改时间的缓存管理
|
|
9
|
+
*
|
|
10
|
+
* 主要功能:
|
|
11
|
+
* - 群组列表查询:按拥有者过滤和排序
|
|
12
|
+
* - 群组元数据读取:获取指定群组的详细信息
|
|
13
|
+
* - 权限验证:检查用户对群组的访问权限
|
|
14
|
+
*
|
|
15
|
+
* 设计特点:
|
|
16
|
+
* - 单一职责:专注于查询操作
|
|
17
|
+
* - 可复用性:可被其他服务调用
|
|
18
|
+
* - 错误处理:完善的异常捕获和空结果处理
|
|
19
|
+
*/
|
|
20
|
+
import { promises as fs } from 'fs';
|
|
21
|
+
import { GroupStorageCore } from '../storage/index.js';
|
|
22
|
+
export class GroupQueryService {
|
|
23
|
+
/**
|
|
24
|
+
* 群组存储服务实例
|
|
25
|
+
*/
|
|
26
|
+
storage;
|
|
27
|
+
/**
|
|
28
|
+
* 用户群组列表缓存
|
|
29
|
+
* key: userId
|
|
30
|
+
* value: { groups: GroupMeta[], timestamp: number }
|
|
31
|
+
*/
|
|
32
|
+
userGroupsCache = new Map();
|
|
33
|
+
/**
|
|
34
|
+
* 缓存有效期(毫秒)
|
|
35
|
+
*/
|
|
36
|
+
CACHE_TTL = 60000; // 60秒
|
|
37
|
+
/**
|
|
38
|
+
* 构造函数
|
|
39
|
+
* 初始化查询服务的依赖组件
|
|
40
|
+
*/
|
|
41
|
+
constructor() {
|
|
42
|
+
this.storage = new GroupStorageCore();
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* 清空缓存
|
|
46
|
+
* 可用于内存管理或测试
|
|
47
|
+
*/
|
|
48
|
+
clearCache() {
|
|
49
|
+
this.userGroupsCache.clear();
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 查询用户拥有的群组列表
|
|
53
|
+
* 获取用户拥有的所有活跃群组,支持排序和过滤
|
|
54
|
+
*
|
|
55
|
+
* @param userId - 用户ID
|
|
56
|
+
* @returns 群组元数据列表,按置顶优先和更新时间倒序排序
|
|
57
|
+
*
|
|
58
|
+
* 查询逻辑:
|
|
59
|
+
* 1. 检查缓存是否存在且有效
|
|
60
|
+
* 2. 读取群组存储目录
|
|
61
|
+
* 3. 批量读取所有群组元数据(性能优化)
|
|
62
|
+
* 4. 过滤:只返回当前用户拥有且未解散的群组
|
|
63
|
+
* 5. 排序:置顶群组优先,按更新时间倒序
|
|
64
|
+
*/
|
|
65
|
+
async getUserGroups(userId) {
|
|
66
|
+
const now = Date.now();
|
|
67
|
+
// 检查缓存
|
|
68
|
+
const cached = this.userGroupsCache.get(userId);
|
|
69
|
+
if (cached && now - cached.timestamp < this.CACHE_TTL) {
|
|
70
|
+
return cached.groups;
|
|
71
|
+
}
|
|
72
|
+
const groupsDir = this.storage.getBasePath();
|
|
73
|
+
try {
|
|
74
|
+
const groupDirs = await fs.readdir(groupsDir);
|
|
75
|
+
const groups = [];
|
|
76
|
+
// 批量读取所有群组元数据(性能优化)
|
|
77
|
+
const metaPromises = groupDirs.map(async (groupId) => {
|
|
78
|
+
try {
|
|
79
|
+
const meta = await this.storage.readGroupMeta(groupId);
|
|
80
|
+
if (meta && meta.ownerIds.includes(userId) && !meta.dismissed) {
|
|
81
|
+
return meta;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
// 忽略单个群组读取失败的情况
|
|
86
|
+
console.warn(`读取群组 ${groupId} 元数据失败:`, error);
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
});
|
|
90
|
+
const metaResults = await Promise.all(metaPromises);
|
|
91
|
+
const validGroups = metaResults.filter(Boolean);
|
|
92
|
+
// 排序:置顶优先,按更新时间倒序
|
|
93
|
+
validGroups.sort((a, b) => {
|
|
94
|
+
if (a.pinned !== b.pinned) {
|
|
95
|
+
return a.pinned ? -1 : 1;
|
|
96
|
+
}
|
|
97
|
+
return b.updatedAt - a.updatedAt;
|
|
98
|
+
});
|
|
99
|
+
// 更新缓存
|
|
100
|
+
this.userGroupsCache.set(userId, { groups: validGroups, timestamp: now });
|
|
101
|
+
return validGroups;
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
// 如果存储目录不存在(ENOENT),返回空列表
|
|
105
|
+
if (error.code === 'ENOENT') {
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* 获取群组详细信息
|
|
113
|
+
* 读取指定群组的元数据信息
|
|
114
|
+
*
|
|
115
|
+
* @param groupId - 群组ID
|
|
116
|
+
* @returns 群组元数据,如果不存在则返回null
|
|
117
|
+
*/
|
|
118
|
+
async getGroupDetail(groupId) {
|
|
119
|
+
return await this.storage.readGroupMeta(groupId);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* 验证用户对群组的权限
|
|
123
|
+
* 检查用户是否有权限访问指定群组
|
|
124
|
+
*
|
|
125
|
+
* @param groupId - 群组ID
|
|
126
|
+
* @param userId - 用户ID
|
|
127
|
+
* @returns 用户是否有权限访问该群组
|
|
128
|
+
*/
|
|
129
|
+
async validateUserPermission(groupId, userId) {
|
|
130
|
+
const groupMeta = await this.getGroupDetail(groupId);
|
|
131
|
+
return !!(groupMeta && groupMeta.ownerIds.includes(userId));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 群组更新服务
|
|
3
|
+
* 专门负责群组信息的更新操作,包括权限验证和字段更新
|
|
4
|
+
*
|
|
5
|
+
* 主要功能:
|
|
6
|
+
* - 权限验证:检查用户是否有权限更新群组
|
|
7
|
+
* - 字段更新:合并更新字段并维护时间戳
|
|
8
|
+
* - 并发控制:确保更新操作的原子性
|
|
9
|
+
*
|
|
10
|
+
* 设计特点:
|
|
11
|
+
* - 严格的权限验证
|
|
12
|
+
* - 安全的字段更新机制
|
|
13
|
+
* - 与存储服务解耦
|
|
14
|
+
*/
|
|
15
|
+
import { promises as fs } from 'fs';
|
|
16
|
+
import { GroupStorageCore } from '../storage/index.js';
|
|
17
|
+
import { ConcurrencyManager } from '../storage/concurrency-manager.js';
|
|
18
|
+
export class GroupUpdateService {
|
|
19
|
+
/**
|
|
20
|
+
* 群组存储服务实例
|
|
21
|
+
*/
|
|
22
|
+
storage;
|
|
23
|
+
/**
|
|
24
|
+
* 并发控制管理器实例
|
|
25
|
+
*/
|
|
26
|
+
concurrency;
|
|
27
|
+
/**
|
|
28
|
+
* 构造函数
|
|
29
|
+
* 初始化更新服务的依赖组件
|
|
30
|
+
*/
|
|
31
|
+
constructor() {
|
|
32
|
+
this.storage = new GroupStorageCore();
|
|
33
|
+
this.concurrency = new ConcurrencyManager();
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* 更新群组信息
|
|
37
|
+
* 执行群组信息的更新操作
|
|
38
|
+
*
|
|
39
|
+
* @param groupId - 群组ID
|
|
40
|
+
* @param updaterId - 更新者用户ID
|
|
41
|
+
* @param updates - 更新字段对象
|
|
42
|
+
* @returns 更新后的群组元数据
|
|
43
|
+
*
|
|
44
|
+
* 更新流程:
|
|
45
|
+
* 1. 权限验证(群组存在且用户是拥有者)
|
|
46
|
+
* 2. 验证更新字段有效性
|
|
47
|
+
* 3. 合并更新字段
|
|
48
|
+
* 4. 更新更新时间戳
|
|
49
|
+
* 5. 写入更新后的元数据
|
|
50
|
+
*/
|
|
51
|
+
async updateGroup(groupId, updaterId, updates) {
|
|
52
|
+
return await this.concurrency.withLock(`group_update_${groupId}`, async () => {
|
|
53
|
+
const existingMeta = await this.storage.readGroupMeta(groupId);
|
|
54
|
+
if (!existingMeta) {
|
|
55
|
+
throw new Error('群组不存在');
|
|
56
|
+
}
|
|
57
|
+
if (!existingMeta.ownerIds.includes(updaterId)) {
|
|
58
|
+
throw new Error('权限不足');
|
|
59
|
+
}
|
|
60
|
+
// 验证更新字段有效性(排除members和defaultAgentId的特殊处理)
|
|
61
|
+
const hasUpdates = Object.keys(updates).some((key) => key !== 'members' && key !== 'defaultAgentId');
|
|
62
|
+
if (!hasUpdates) {
|
|
63
|
+
throw new Error('没有有效的更新字段');
|
|
64
|
+
}
|
|
65
|
+
const updatedMeta = {
|
|
66
|
+
...existingMeta,
|
|
67
|
+
...updates,
|
|
68
|
+
updatedAt: Date.now(),
|
|
69
|
+
};
|
|
70
|
+
// 写入群组元数据
|
|
71
|
+
await this.storage.writeGroupMeta(groupId, updatedMeta);
|
|
72
|
+
return updatedMeta;
|
|
73
|
+
}, 30000); // 30秒超时
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* 全量同步成员元信息
|
|
77
|
+
*
|
|
78
|
+
* 当 Agent 配置变更(agentName / agentDesc 修改)后,遍历所有群组,
|
|
79
|
+
* 将 members 中匹配的成员属性刷新为最新值。
|
|
80
|
+
*
|
|
81
|
+
* 设计要点:
|
|
82
|
+
* - 不限制 userId,无论谁改了 agent 都跟着同步
|
|
83
|
+
* - 只更新 agentName / agentDesc,不增删成员,保证成员列表一致性
|
|
84
|
+
* - 每个群组独立加锁,避免与其他更新操作冲突
|
|
85
|
+
* - 仅当群组中确实存在需要更新的成员且属性有变化时才写入,减少无效 IO
|
|
86
|
+
*
|
|
87
|
+
* @param agentMetaMap - Agent 元信息映射表,key 为 agentId
|
|
88
|
+
* @returns 同步结果,包含被更新的群组 ID 列表和扫描总数
|
|
89
|
+
*/
|
|
90
|
+
async syncMembersMeta(agentMetaMap) {
|
|
91
|
+
const agentIds = Object.keys(agentMetaMap);
|
|
92
|
+
if (agentIds.length === 0) {
|
|
93
|
+
return { updatedGroupIds: [], scannedCount: 0 };
|
|
94
|
+
}
|
|
95
|
+
// 扫描所有群组目录
|
|
96
|
+
const groupsDir = this.storage.getBasePath();
|
|
97
|
+
let groupDirs = [];
|
|
98
|
+
try {
|
|
99
|
+
groupDirs = await fs.readdir(groupsDir);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// 群组目录不存在时直接返回
|
|
103
|
+
return { updatedGroupIds: [], scannedCount: 0 };
|
|
104
|
+
}
|
|
105
|
+
const updatedGroupIds = [];
|
|
106
|
+
// 并行处理所有群组(每个群组独立加锁)
|
|
107
|
+
const tasks = groupDirs.map(async (groupId) => {
|
|
108
|
+
try {
|
|
109
|
+
await this.concurrency.withLock(`group_update_${groupId}`, async () => {
|
|
110
|
+
const meta = await this.storage.readGroupMeta(groupId);
|
|
111
|
+
if (!meta || meta.dismissed) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
// 检查是否有成员需要更新
|
|
115
|
+
let hasChange = false;
|
|
116
|
+
const updatedMembers = meta.members.map((member) => {
|
|
117
|
+
const newMeta = agentMetaMap[member.agentId];
|
|
118
|
+
if (newMeta &&
|
|
119
|
+
(member.agentName !== newMeta.agentName || member.agentDesc !== newMeta.agentDesc)) {
|
|
120
|
+
hasChange = true;
|
|
121
|
+
return { ...member, agentName: newMeta.agentName, agentDesc: newMeta.agentDesc };
|
|
122
|
+
}
|
|
123
|
+
return member;
|
|
124
|
+
});
|
|
125
|
+
if (!hasChange) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const updatedMeta = {
|
|
129
|
+
...meta,
|
|
130
|
+
members: updatedMembers,
|
|
131
|
+
updatedAt: Date.now(),
|
|
132
|
+
};
|
|
133
|
+
await this.storage.writeGroupMeta(groupId, updatedMeta);
|
|
134
|
+
updatedGroupIds.push(groupId);
|
|
135
|
+
}, 30000);
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// 单个群组同步失败不影响其他群组
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
await Promise.all(tasks);
|
|
142
|
+
return { updatedGroupIds, scannedCount: groupDirs.length };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 群组服务模块索引文件
|
|
3
|
+
* 统一导出所有群组相关的服务类,便于外部模块引用
|
|
4
|
+
*
|
|
5
|
+
* 包含服务:
|
|
6
|
+
* - GroupQueryService: 群组查询服务
|
|
7
|
+
* - GroupCreationService: 群组创建服务
|
|
8
|
+
* - GroupUpdateService: 群组更新服务(含 syncMembersMeta 全量同步成员元信息)
|
|
9
|
+
* - GroupDeletionService: 群组删除服务
|
|
10
|
+
* - GroupCleanupService: 群组清理服务
|
|
11
|
+
* - GroupMemberService: 群组成员管理服务(add / remove)
|
|
12
|
+
* - GroupHistoryService: 群历史消息查询服务(chat.jsonl 游标分页 + 权限校验)
|
|
13
|
+
*/
|
|
14
|
+
export { GroupQueryService } from './group-query-service.js';
|
|
15
|
+
export { GroupCreationService } from './group-creation-service.js';
|
|
16
|
+
export { GroupUpdateService } from './group-update-service.js';
|
|
17
|
+
export { GroupDeletionService } from './group-deletion-service.js';
|
|
18
|
+
export { GroupCleanupService } from './group-cleanup-service.js';
|
|
19
|
+
export { GroupMemberService } from './group-member-service.js';
|
|
20
|
+
export { GroupHistoryService } from './group-history-service.js';
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 并发控制管理器
|
|
3
|
+
* 用于确保群聊操作的原子性和线程安全,防止数据竞争和并发冲突
|
|
4
|
+
*
|
|
5
|
+
* 设计原理:
|
|
6
|
+
* - 基于Promise的锁机制实现,确保同一时刻对同一资源的操作串行化
|
|
7
|
+
* - 使用Map数据结构存储锁状态,键为资源标识符,值为锁的Promise对象
|
|
8
|
+
* - 支持异步操作的等待和释放,保证操作的完整性和一致性
|
|
9
|
+
* - 添加超时机制防止死锁,锁状态监控便于调试
|
|
10
|
+
*
|
|
11
|
+
* 适用场景:
|
|
12
|
+
* - 群组创建、更新、删除等写操作
|
|
13
|
+
* - 群组成员管理操作
|
|
14
|
+
* - 任何需要保证操作原子性的并发场景
|
|
15
|
+
*/
|
|
16
|
+
export class ConcurrencyManager {
|
|
17
|
+
/**
|
|
18
|
+
* 锁状态映射表
|
|
19
|
+
* - key: 资源标识符(如groupId)
|
|
20
|
+
* - value: 表示锁状态的Promise对象,resolve时表示锁已释放
|
|
21
|
+
*/
|
|
22
|
+
locks = new Map();
|
|
23
|
+
/**
|
|
24
|
+
* 锁创建时间映射表,用于监控锁状态和超时检测
|
|
25
|
+
*/
|
|
26
|
+
lockTimestamps = new Map();
|
|
27
|
+
/**
|
|
28
|
+
* 默认锁超时时间(毫秒)
|
|
29
|
+
*/
|
|
30
|
+
DEFAULT_TIMEOUT = 30000; // 30秒
|
|
31
|
+
/**
|
|
32
|
+
* 使用锁执行异步操作
|
|
33
|
+
* 确保同一时刻对同一资源的操作按顺序执行,避免并发冲突
|
|
34
|
+
*
|
|
35
|
+
* @param key - 锁的键,通常为资源标识符(如群组ID)
|
|
36
|
+
* @param operation - 要执行的异步操作函数,返回Promise<T>
|
|
37
|
+
* @param timeout - 锁超时时间(毫秒),默认30秒
|
|
38
|
+
* @returns 操作结果的Promise,保持原操作的返回类型
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```typescript
|
|
42
|
+
* const manager = new ConcurrencyManager();
|
|
43
|
+
* const result = await manager.withLock('group-123', async () => {
|
|
44
|
+
* // 执行需要并发控制的群组操作
|
|
45
|
+
* return await updateGroup(groupId, updates);
|
|
46
|
+
* }, 60000); // 60秒超时
|
|
47
|
+
* ```
|
|
48
|
+
*
|
|
49
|
+
* 执行流程:
|
|
50
|
+
* 1. 检查是否已有相同key的锁存在
|
|
51
|
+
* 2. 如果存在,等待该锁释放,但设置超时限制
|
|
52
|
+
* 3. 创建新的锁并添加到锁映射表,记录创建时间
|
|
53
|
+
* 4. 执行操作函数,设置超时保护
|
|
54
|
+
* 5. 无论操作成功或失败,最终释放锁并清理状态
|
|
55
|
+
*/
|
|
56
|
+
async withLock(key, operation, timeout = this.DEFAULT_TIMEOUT) {
|
|
57
|
+
const startTime = Date.now();
|
|
58
|
+
// 等待现有锁释放,但设置超时限制
|
|
59
|
+
while (this.locks.has(key)) {
|
|
60
|
+
const lockPromise = this.locks.get(key);
|
|
61
|
+
if (lockPromise) {
|
|
62
|
+
// 检查锁是否超时
|
|
63
|
+
const lockTimestamp = this.lockTimestamps.get(key);
|
|
64
|
+
if (lockTimestamp && Date.now() - lockTimestamp > timeout) {
|
|
65
|
+
// 强制释放超时的锁
|
|
66
|
+
console.warn(`锁 ${key} 已超时,强制释放`);
|
|
67
|
+
this.locks.delete(key);
|
|
68
|
+
this.lockTimestamps.delete(key);
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
// 等待锁释放,但设置超时
|
|
72
|
+
try {
|
|
73
|
+
await Promise.race([
|
|
74
|
+
lockPromise,
|
|
75
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`等待锁 ${key} 超时`)), timeout))
|
|
76
|
+
]);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
// 等待超时,强制释放锁
|
|
80
|
+
console.warn(`等待锁 ${key} 超时,强制释放`);
|
|
81
|
+
this.locks.delete(key);
|
|
82
|
+
this.lockTimestamps.delete(key);
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// 创建新的锁:使用Promise控制锁的状态
|
|
88
|
+
let resolveLock;
|
|
89
|
+
const lock = new Promise(resolve => {
|
|
90
|
+
resolveLock = resolve;
|
|
91
|
+
});
|
|
92
|
+
// 设置锁状态:将新锁添加到映射表中
|
|
93
|
+
this.locks.set(key, lock);
|
|
94
|
+
this.lockTimestamps.set(key, Date.now());
|
|
95
|
+
try {
|
|
96
|
+
// 执行受保护的操作:在锁的保护下执行实际业务逻辑,设置超时保护
|
|
97
|
+
const result = await Promise.race([
|
|
98
|
+
operation(),
|
|
99
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`操作 ${key} 执行超时`)), timeout))
|
|
100
|
+
]);
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
// 记录操作失败信息
|
|
105
|
+
console.error(`锁保护的操作失败: ${key}`, error);
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
// 清理锁状态:无论操作成功或失败,都要释放锁
|
|
110
|
+
this.locks.delete(key);
|
|
111
|
+
this.lockTimestamps.delete(key);
|
|
112
|
+
resolveLock(); // 通知等待的请求锁已释放
|
|
113
|
+
const duration = Date.now() - startTime;
|
|
114
|
+
if (duration > timeout / 2) {
|
|
115
|
+
console.warn(`锁 ${key} 持有时间较长: ${duration}ms`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|