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,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 分片管理器
|
|
3
|
+
* 专门负责群组聊天记录的分片存储管理
|
|
4
|
+
* 包括分片索引的读写、活跃分片管理、分片创建等核心功能
|
|
5
|
+
*/
|
|
6
|
+
import { promises as fs } from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
export class SliceManager {
|
|
9
|
+
/**
|
|
10
|
+
* 读取分片索引
|
|
11
|
+
* 从文件系统读取群组的分片索引信息
|
|
12
|
+
*
|
|
13
|
+
* @param groupDir - 群组目录路径
|
|
14
|
+
* @returns 分片索引对象,如果索引不存在则创建默认索引
|
|
15
|
+
*/
|
|
16
|
+
async readSliceIndex(groupDir) {
|
|
17
|
+
const indexPath = path.join(groupDir, 'index.json');
|
|
18
|
+
try {
|
|
19
|
+
const content = await fs.readFile(indexPath, 'utf-8');
|
|
20
|
+
return JSON.parse(content);
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
// 如果索引文件不存在,创建默认索引
|
|
24
|
+
if (error.code === 'ENOENT') {
|
|
25
|
+
return {
|
|
26
|
+
version: '1.0',
|
|
27
|
+
groupId: path.basename(groupDir),
|
|
28
|
+
totalMessages: 0,
|
|
29
|
+
lastUpdated: Date.now(),
|
|
30
|
+
slices: [],
|
|
31
|
+
nextMessageId: 1,
|
|
32
|
+
nextSliceSequence: 1
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* 写入分片索引(原子操作)
|
|
40
|
+
* 使用原子操作方式写入分片索引,避免数据损坏
|
|
41
|
+
*
|
|
42
|
+
* @param groupDir - 群组目录路径
|
|
43
|
+
* @param index - 分片索引对象
|
|
44
|
+
*/
|
|
45
|
+
async writeSliceIndex(groupDir, index) {
|
|
46
|
+
const indexPath = path.join(groupDir, 'index.json');
|
|
47
|
+
const tempPath = `${indexPath}.tmp`;
|
|
48
|
+
// 更新最后更新时间
|
|
49
|
+
index.lastUpdated = Date.now();
|
|
50
|
+
// 先写入临时文件
|
|
51
|
+
await fs.writeFile(tempPath, JSON.stringify(index, null, 2));
|
|
52
|
+
// 原子重命名
|
|
53
|
+
await fs.rename(tempPath, indexPath);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* 获取当前活跃分片
|
|
57
|
+
* 返回当前正在写入的分片信息,如果不存在则创建新分片
|
|
58
|
+
*
|
|
59
|
+
* @param groupDir - 群组目录路径
|
|
60
|
+
* @param maxSliceSize - 单个分片最大消息数
|
|
61
|
+
* @returns 活跃分片信息
|
|
62
|
+
*/
|
|
63
|
+
async getActiveSlice(groupDir, maxSliceSize) {
|
|
64
|
+
const index = await this.readSliceIndex(groupDir);
|
|
65
|
+
// 如果没有分片,创建第一个分片
|
|
66
|
+
if (index.slices.length === 0) {
|
|
67
|
+
return await this.createNewSlice(groupDir, index);
|
|
68
|
+
}
|
|
69
|
+
// 获取最后一个分片
|
|
70
|
+
const lastSlice = index.slices[index.slices.length - 1];
|
|
71
|
+
// 如果最后一个分片未满,返回该分片
|
|
72
|
+
if (lastSlice.messageCount < maxSliceSize) {
|
|
73
|
+
return lastSlice;
|
|
74
|
+
}
|
|
75
|
+
// 创建新分片
|
|
76
|
+
return await this.createNewSlice(groupDir, index);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* 创建新分片
|
|
80
|
+
* 创建新的分片文件并更新索引
|
|
81
|
+
*
|
|
82
|
+
* @param groupDir - 群组目录路径
|
|
83
|
+
* @param index - 当前索引
|
|
84
|
+
* @returns 新创建的分片信息
|
|
85
|
+
*/
|
|
86
|
+
async createNewSlice(groupDir, index) {
|
|
87
|
+
const timestamp = Date.now();
|
|
88
|
+
const dateStr = new Date(timestamp).toISOString().split('T')[0].replace(/-/g, '');
|
|
89
|
+
const sequence = index.nextSliceSequence || 1;
|
|
90
|
+
const filename = `chat-${dateStr}-${String(sequence).padStart(3, '0')}.jsonl`;
|
|
91
|
+
const sliceInfo = {
|
|
92
|
+
filename,
|
|
93
|
+
startTime: timestamp,
|
|
94
|
+
endTime: timestamp,
|
|
95
|
+
messageCount: 0,
|
|
96
|
+
fileSize: 0,
|
|
97
|
+
minId: index.nextMessageId || 1,
|
|
98
|
+
maxId: 0
|
|
99
|
+
};
|
|
100
|
+
// 添加到索引
|
|
101
|
+
index.slices.push(sliceInfo);
|
|
102
|
+
index.activeSlice = filename;
|
|
103
|
+
index.nextSliceSequence = sequence + 1;
|
|
104
|
+
// 写入索引
|
|
105
|
+
await this.writeSliceIndex(groupDir, index);
|
|
106
|
+
return sliceInfo;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* 更新分片统计信息
|
|
110
|
+
* 在写入消息后更新分片的统计信息
|
|
111
|
+
*
|
|
112
|
+
* @param groupDir - 群组目录路径
|
|
113
|
+
* @param sliceFilename - 分片文件名
|
|
114
|
+
* @param messageId - 消息ID
|
|
115
|
+
* @param timestamp - 消息时间戳
|
|
116
|
+
* @param lineSize - 写入行的字节大小
|
|
117
|
+
*/
|
|
118
|
+
async updateSliceStats(groupDir, sliceFilename, messageId, timestamp, lineSize) {
|
|
119
|
+
const index = await this.readSliceIndex(groupDir);
|
|
120
|
+
const sliceIndex = index.slices.findIndex((slice) => slice.filename === sliceFilename);
|
|
121
|
+
if (sliceIndex !== -1) {
|
|
122
|
+
index.slices[sliceIndex].messageCount += 1;
|
|
123
|
+
index.slices[sliceIndex].endTime = timestamp;
|
|
124
|
+
index.slices[sliceIndex].maxId = messageId;
|
|
125
|
+
index.slices[sliceIndex].fileSize += lineSize;
|
|
126
|
+
index.totalMessages += 1;
|
|
127
|
+
index.nextMessageId = messageId + 1;
|
|
128
|
+
await this.writeSliceIndex(groupDir, index);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* 获取相关分片列表
|
|
133
|
+
* 根据时间范围获取需要查询的分片列表
|
|
134
|
+
*
|
|
135
|
+
* @param groupDir - 群组目录路径
|
|
136
|
+
* @param before - 时间戳上限
|
|
137
|
+
* @returns 按时间倒序排列的相关分片列表
|
|
138
|
+
*/
|
|
139
|
+
async getRelevantSlices(groupDir, before) {
|
|
140
|
+
const index = await this.readSliceIndex(groupDir);
|
|
141
|
+
return index.slices
|
|
142
|
+
.filter((slice) => slice.startTime < before)
|
|
143
|
+
.sort((a, b) => b.startTime - a.startTime);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* 计算是否还有更多数据
|
|
147
|
+
* 根据索引和当前查询结果判断是否还有更早的历史记录
|
|
148
|
+
*
|
|
149
|
+
* @param groupDir - 群组目录路径
|
|
150
|
+
* @param before - 查询时间上限
|
|
151
|
+
* @param currentEntries - 当前查询结果
|
|
152
|
+
* @returns 是否还有更多数据
|
|
153
|
+
*/
|
|
154
|
+
async calculateHasMore(groupDir, before, currentEntries) {
|
|
155
|
+
if (currentEntries.length === 0) {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
const index = await this.readSliceIndex(groupDir);
|
|
159
|
+
// 找到最早的分片
|
|
160
|
+
const earliestSlice = index.slices.reduce((earliest, slice) => slice.startTime < earliest.startTime ? slice : earliest);
|
|
161
|
+
// 如果最早的分片开始时间早于当前查询的最早时间戳,说明还有更早的数据
|
|
162
|
+
const earliestEntryTs = currentEntries[0]?.ts || before;
|
|
163
|
+
return earliestSlice.startTime < earliestEntryTs;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file ID 生成工具
|
|
3
|
+
* @description 提供群聊模块中各类 ID 的生成方法
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* 生成临时 runId(在 SDK 拉起 run 之前给 RunRegistry 占位用)
|
|
7
|
+
* 形如:`pre_<conversationId>_<role>_<random>`
|
|
8
|
+
*
|
|
9
|
+
* @param conversationId - 回合 ID
|
|
10
|
+
* @param role - run 角色
|
|
11
|
+
*/
|
|
12
|
+
export function generatePreRunId(conversationId, role) {
|
|
13
|
+
const rand = Math.random().toString(36).slice(2, 10);
|
|
14
|
+
return `pre_${conversationId}_${role}_${rand}`;
|
|
15
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file 群聊公共工具函数统一出口
|
|
3
|
+
* @description 统一导出所有跨子模块共享的纯函数,供 orchestrator、inbound 等模块按需引入
|
|
4
|
+
*/
|
|
5
|
+
/** ID 生成:预生成 runId,用于在 run 启动前即可关联上下文 */
|
|
6
|
+
export { generatePreRunId } from './id-generator.js';
|
|
7
|
+
/** Run 状态处理:状态规范化、错误序列化 */
|
|
8
|
+
export { normalizeRunStatus, errorToString } from './run-helpers.js';
|
|
9
|
+
/** 数据规范化:mention 列表清洗与校验 */
|
|
10
|
+
export { normalizeMentions } from './normalize.js';
|
|
11
|
+
/** MIME 类型推断:根据文件扩展名猜测 MIME 类型 */
|
|
12
|
+
export { guessMimeType } from './mime.js';
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file MIME 类型推断工具
|
|
3
|
+
* @description 根据文件扩展名猜测对应的 MIME 类型,供群聊文件处理等模块复用
|
|
4
|
+
*/
|
|
5
|
+
/** 扩展名到 MIME 类型的映射表 */
|
|
6
|
+
const EXTENSION_MIME_MAP = {
|
|
7
|
+
png: 'image/png',
|
|
8
|
+
jpg: 'image/jpeg',
|
|
9
|
+
jpeg: 'image/jpeg',
|
|
10
|
+
gif: 'image/gif',
|
|
11
|
+
webp: 'image/webp',
|
|
12
|
+
svg: 'image/svg+xml',
|
|
13
|
+
pdf: 'application/pdf',
|
|
14
|
+
doc: 'application/msword',
|
|
15
|
+
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
16
|
+
xls: 'application/vnd.ms-excel',
|
|
17
|
+
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
18
|
+
ppt: 'application/vnd.ms-powerpoint',
|
|
19
|
+
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
20
|
+
txt: 'text/plain',
|
|
21
|
+
csv: 'text/csv',
|
|
22
|
+
json: 'application/json',
|
|
23
|
+
zip: 'application/zip',
|
|
24
|
+
mp3: 'audio/mpeg',
|
|
25
|
+
mp4: 'video/mp4',
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* 根据文件扩展名猜测 MIME 类型
|
|
29
|
+
*
|
|
30
|
+
* @param fileName 文件名(含扩展名)
|
|
31
|
+
* @returns 对应的 MIME 类型字符串,未匹配时返回 'application/octet-stream'
|
|
32
|
+
*/
|
|
33
|
+
export function guessMimeType(fileName) {
|
|
34
|
+
const ext = fileName.split('.').pop()?.toLowerCase() ?? '';
|
|
35
|
+
return EXTENSION_MIME_MAP[ext] || 'application/octet-stream';
|
|
36
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file 群聊数据规范化工具
|
|
3
|
+
* @description 提供群聊场景下的数据清洗与规范化函数
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* 规范化 mention 列表
|
|
7
|
+
*
|
|
8
|
+
* - 仅保留字符串类型且非空的 ID
|
|
9
|
+
* - 去重(保持原始顺序)
|
|
10
|
+
* - 仅保留属于当前群成员的 ID
|
|
11
|
+
*
|
|
12
|
+
* @param raw - 原始 mention 数据(来自客户端 extra,类型不可信)
|
|
13
|
+
* @param members - 当前群的合法成员 ID 列表
|
|
14
|
+
* @returns 规范化后的 mention 列表
|
|
15
|
+
*/
|
|
16
|
+
export function normalizeMentions(raw, members) {
|
|
17
|
+
if (!Array.isArray(raw) || raw.length === 0)
|
|
18
|
+
return [];
|
|
19
|
+
const memberSet = new Set(members);
|
|
20
|
+
const seen = new Set();
|
|
21
|
+
const result = [];
|
|
22
|
+
for (const item of raw) {
|
|
23
|
+
if (typeof item !== 'string')
|
|
24
|
+
continue;
|
|
25
|
+
const id = item.trim();
|
|
26
|
+
if (!id || seen.has(id) || !memberSet.has(id))
|
|
27
|
+
continue;
|
|
28
|
+
seen.add(id);
|
|
29
|
+
result.push(id);
|
|
30
|
+
}
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Run 状态/错误处理工具
|
|
3
|
+
* @description 提供 Run 状态转换和错误字符串化的纯函数
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* 把 AgentRunOutput.status 转成 RunStatus
|
|
7
|
+
*/
|
|
8
|
+
export function normalizeRunStatus(status) {
|
|
9
|
+
switch (status) {
|
|
10
|
+
case 'ok':
|
|
11
|
+
return 'done';
|
|
12
|
+
case 'aborted':
|
|
13
|
+
return 'aborted';
|
|
14
|
+
case 'timeout':
|
|
15
|
+
case 'error':
|
|
16
|
+
default:
|
|
17
|
+
return 'failed';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* 把任意错误转成可见字符串(去掉栈信息)
|
|
22
|
+
*/
|
|
23
|
+
export function errorToString(err, fallback = '未知错误') {
|
|
24
|
+
if (!err)
|
|
25
|
+
return fallback;
|
|
26
|
+
if (err instanceof Error)
|
|
27
|
+
return err.message || fallback;
|
|
28
|
+
if (typeof err === 'string')
|
|
29
|
+
return err;
|
|
30
|
+
try {
|
|
31
|
+
return JSON.stringify(err);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return fallback;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -338,8 +338,14 @@ function parseLines(lines, opts) {
|
|
|
338
338
|
if (!msg || typeof msg !== "object")
|
|
339
339
|
continue;
|
|
340
340
|
// 过滤 openclaw delivery-mirror 消息(镜像投递,非真实消息)
|
|
341
|
-
|
|
342
|
-
|
|
341
|
+
// 但对 cron 直接投递的消息放行(用户需要看到定时提醒内容)
|
|
342
|
+
if (msg.model === "delivery-mirror") {
|
|
343
|
+
const idempotencyKey = msg.idempotencyKey
|
|
344
|
+
?? parsed.idempotencyKey;
|
|
345
|
+
if (!idempotencyKey || !idempotencyKey.startsWith("cron-direct-delivery:")) {
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
343
349
|
// 过滤传输层伪装为 user 的系统注入消息
|
|
344
350
|
if (isSystemInjectedUserMessage(msg))
|
|
345
351
|
continue;
|
package/dist/src/outbound.js
CHANGED
|
@@ -11,16 +11,11 @@
|
|
|
11
11
|
* 2. WS 不可用时 fallback 到 REST API(需配置 apiBaseUrl)
|
|
12
12
|
*/
|
|
13
13
|
import { CHANNEL_KEY, DEFAULT_ACCOUNT_ID, EVENT_MESSAGE_PRIVATE } from './config.js';
|
|
14
|
-
import { getLightclawRuntime } from './runtime.js';
|
|
15
14
|
import { getSocket, bufferMessage, hasEntry, getBotClientId, getReliableEmitter } from './socket/index.js';
|
|
16
15
|
import { resolveAccount } from './utils/index.js';
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
*/
|
|
21
|
-
function getLogger() {
|
|
22
|
-
return getLightclawRuntime().logging.getChildLogger({ module: 'outbound' });
|
|
23
|
-
}
|
|
16
|
+
import { getModuleLogger } from './utils/logger.js';
|
|
17
|
+
/** 出站模块专属日志器(惰性代理,模块顶层安全调用) */
|
|
18
|
+
const logger = getModuleLogger('outbound');
|
|
24
19
|
/**
|
|
25
20
|
* 出站消息 ID 计数器,用于生成全局唯一的消息序号。
|
|
26
21
|
* 模块级变量,进程生命周期内单调递增,不会重置。
|
|
@@ -107,21 +102,21 @@ function sendViaSocket(accountId, target, text, replyToId) {
|
|
|
107
102
|
emitter.emitWithAck(EVENT_MESSAGE_PRIVATE, message, msgId).then((ok) => {
|
|
108
103
|
// ok=false 表示重试耗尽仍未收到 ACK,记录错误供告警系统捕获
|
|
109
104
|
if (!ok) {
|
|
110
|
-
|
|
105
|
+
logger.error(`[${CHANNEL_KEY}] outbound delivery failed after retries: msgId=${msgId}`);
|
|
111
106
|
}
|
|
112
107
|
});
|
|
113
|
-
|
|
108
|
+
logger.info(`[${CHANNEL_KEY}] outbound sent via reliable WS: to=${target} msgId=${msgId}`);
|
|
114
109
|
return msgId;
|
|
115
110
|
}
|
|
116
111
|
// 策略 2:可靠发送器不可用,直接 emit(兜底,正常情况不应走到此分支)
|
|
117
112
|
try {
|
|
118
113
|
entry.socket.emit(EVENT_MESSAGE_PRIVATE, message);
|
|
119
|
-
|
|
114
|
+
logger.info(`[${CHANNEL_KEY}] outbound sent via WS (fallback): to=${target} msgId=${msgId}`);
|
|
120
115
|
return msgId;
|
|
121
116
|
}
|
|
122
117
|
catch (err) {
|
|
123
118
|
// emit 本身抛异常(如 socket 已关闭),降级到 REST API
|
|
124
|
-
|
|
119
|
+
logger.warn(`[${CHANNEL_KEY}] outbound WS emit failed: ${err}`);
|
|
125
120
|
return null;
|
|
126
121
|
}
|
|
127
122
|
}
|
|
@@ -130,7 +125,7 @@ function sendViaSocket(accountId, target, text, replyToId) {
|
|
|
130
125
|
if (hasEntry(resolvedAccountId)) {
|
|
131
126
|
const buffered = bufferMessage(resolvedAccountId, message);
|
|
132
127
|
if (buffered) {
|
|
133
|
-
|
|
128
|
+
logger.info(`[${CHANNEL_KEY}] outbound buffered (WS reconnecting): to=${target} msgId=${msgId}`);
|
|
134
129
|
return msgId;
|
|
135
130
|
}
|
|
136
131
|
}
|
|
@@ -153,7 +148,6 @@ function sendViaSocket(accountId, target, text, replyToId) {
|
|
|
153
148
|
* @throws 当 WS 和 REST API 均不可用时抛出 Error
|
|
154
149
|
*/
|
|
155
150
|
export async function sendText(ctx) {
|
|
156
|
-
const log = getLogger();
|
|
157
151
|
try {
|
|
158
152
|
// 解析账户配置(含 apiKey、apiBaseUrl、accountId 等)
|
|
159
153
|
const account = resolveAccount(ctx.cfg, ctx.accountId);
|
|
@@ -169,7 +163,7 @@ export async function sendText(ctx) {
|
|
|
169
163
|
if (!account.apiBaseUrl) {
|
|
170
164
|
throw new Error('WS not connected and apiBaseUrl not configured, cannot send outbound message');
|
|
171
165
|
}
|
|
172
|
-
|
|
166
|
+
logger.info(`[${CHANNEL_KEY}] outbound fallback to REST API: to=${target}`);
|
|
173
167
|
const resp = await fetch(`${account.apiBaseUrl}/send`, {
|
|
174
168
|
method: 'POST',
|
|
175
169
|
headers: {
|
|
@@ -193,7 +187,7 @@ export async function sendText(ctx) {
|
|
|
193
187
|
return { channel: CHANNEL_KEY, messageId: data.messageId ?? `msg_${Date.now()}` };
|
|
194
188
|
}
|
|
195
189
|
catch (err) {
|
|
196
|
-
|
|
190
|
+
logger.error(`[${CHANNEL_KEY}] outbound.sendText error: ${err}`);
|
|
197
191
|
throw err;
|
|
198
192
|
}
|
|
199
193
|
}
|
|
@@ -216,7 +210,6 @@ export async function sendText(ctx) {
|
|
|
216
210
|
* @throws 当 WS 和 REST API 均不可用时抛出 Error
|
|
217
211
|
*/
|
|
218
212
|
export async function sendMedia(ctx) {
|
|
219
|
-
const log = getLogger();
|
|
220
213
|
try {
|
|
221
214
|
// 解析账户配置
|
|
222
215
|
const account = resolveAccount(ctx.cfg, ctx.accountId);
|
|
@@ -240,7 +233,7 @@ export async function sendMedia(ctx) {
|
|
|
240
233
|
throw new Error('WS not connected and apiBaseUrl not configured, cannot send outbound media');
|
|
241
234
|
}
|
|
242
235
|
// 通过 REST API 发送完整媒体消息(含 mediaUrl)
|
|
243
|
-
|
|
236
|
+
logger.info(`[${CHANNEL_KEY}] outbound media via REST API: to=${target}`);
|
|
244
237
|
const resp = await fetch(`${account.apiBaseUrl}/send-media`, {
|
|
245
238
|
method: 'POST',
|
|
246
239
|
headers: {
|
|
@@ -264,7 +257,7 @@ export async function sendMedia(ctx) {
|
|
|
264
257
|
return { channel: CHANNEL_KEY, messageId: data.messageId ?? `msg_${Date.now()}` };
|
|
265
258
|
}
|
|
266
259
|
catch (err) {
|
|
267
|
-
|
|
260
|
+
logger.error(`[${CHANNEL_KEY}] outbound.sendMedia error: ${err}`);
|
|
268
261
|
throw err;
|
|
269
262
|
}
|
|
270
263
|
}
|
package/dist/src/shared.js
CHANGED
|
@@ -124,9 +124,10 @@ export function createLightclawPluginBase(params) {
|
|
|
124
124
|
// true = deliver 回调会收到 kind="block" 的中间块,实现流式推送
|
|
125
125
|
blockStreaming: true,
|
|
126
126
|
},
|
|
127
|
-
streaming
|
|
128
|
-
|
|
129
|
-
|
|
127
|
+
// 注意:原 streaming.blockStreamingCoalesceDefaults 已移除——
|
|
128
|
+
// 当前请求侧固定走 disableBlockStreaming=true(partial 通道),框架不会调用
|
|
129
|
+
// BlockReplyCoalescer,配置不生效;二次聚合改在 sink 内部由
|
|
130
|
+
// partialCoalesce 完成(详见 ADR-017 / streaming/stream-reply-sink.ts)。
|
|
130
131
|
reload: { configPrefixes: [`channels.${CHANNEL_KEY}`] },
|
|
131
132
|
configSchema: {
|
|
132
133
|
schema: {
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { CHANNEL_KEY, DEFAULT_AGENT_ID, EVENT_AGENTS_REQUEST, EVENT_AGENTS_RESPONSE } from '../../config.js';
|
|
2
|
+
import { generateMsgId } from '../../dedup.js';
|
|
3
|
+
import { getLightclawRuntime } from '../../runtime.js';
|
|
4
|
+
import { attachSoulIdsToAgents, readSoulIdMap, LIGHTSOUL_CONFIG_PATH } from '../utils/agent-soul.js';
|
|
5
|
+
import { getModuleLogger } from '../../utils/logger.js';
|
|
6
|
+
/** 模块级日志器 */
|
|
7
|
+
const log = getModuleLogger('socket.agents-request');
|
|
8
|
+
/**
|
|
9
|
+
* 提取非空字符串,空白字符串视为无效值
|
|
10
|
+
* @param value - 待检测的未知类型值
|
|
11
|
+
* @returns 去除首尾空白后的字符串;若非字符串或为空白则返回 undefined
|
|
12
|
+
*/
|
|
13
|
+
const getNonEmptyString = (value) => {
|
|
14
|
+
if (typeof value !== 'string')
|
|
15
|
+
return undefined;
|
|
16
|
+
const trimmed = value.trim();
|
|
17
|
+
return trimmed || undefined;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* 判断给定 agent 是否为默认的 main agent
|
|
21
|
+
* @param agent - agent 配置对象
|
|
22
|
+
* @returns 当 agent 的 id 或 name 等于 DEFAULT_AGENT_ID 时返回 true
|
|
23
|
+
*/
|
|
24
|
+
const isMainAgent = (agent) => {
|
|
25
|
+
const id = getNonEmptyString(agent.id);
|
|
26
|
+
const name = getNonEmptyString(agent.name);
|
|
27
|
+
return id === DEFAULT_AGENT_ID || name === DEFAULT_AGENT_ID;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* 为缺少 workspace 的 agent 补齐默认 workspace
|
|
31
|
+
* @param agent - agent 配置对象
|
|
32
|
+
* @param defaultWorkspace - 全局默认 workspace 路径
|
|
33
|
+
* @returns 补齐 workspace 后的 agent 对象(若已有则原样返回)
|
|
34
|
+
*/
|
|
35
|
+
const withDefaultWorkspace = (agent, defaultWorkspace) => {
|
|
36
|
+
if (!defaultWorkspace || getNonEmptyString(agent.workspace))
|
|
37
|
+
return agent;
|
|
38
|
+
return { ...agent, workspace: defaultWorkspace };
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* 统一发送 agents 响应消息
|
|
42
|
+
* @param reliableEmitter - 可靠消息发射器
|
|
43
|
+
* @param payload - 响应载荷,包含目标、来源、agents 列表及可选错误信息
|
|
44
|
+
*/
|
|
45
|
+
const emitAgentsResponse = (reliableEmitter, payload) => {
|
|
46
|
+
const msgId = generateMsgId();
|
|
47
|
+
reliableEmitter.emitWithAck(EVENT_AGENTS_RESPONSE, { msgId, ...payload, timestamp: Date.now() }, msgId);
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* 标准化 agents 列表,确保始终包含一个 main agent
|
|
51
|
+
*
|
|
52
|
+
* 处理逻辑:
|
|
53
|
+
* 1. 从配置中读取 agents.list
|
|
54
|
+
* 2. 遍历列表,为已存在的 main agent 补齐默认 workspace
|
|
55
|
+
* 3. 若列表中不存在 main agent,则自动创建一个并置于列表首位
|
|
56
|
+
*
|
|
57
|
+
* @param currentCfg - 当前 openclaw.json 完整配置对象
|
|
58
|
+
* @returns 标准化后的 agents 列表(保证包含 main agent)
|
|
59
|
+
*/
|
|
60
|
+
const normalizeAgentsWithMainAgent = (currentCfg) => {
|
|
61
|
+
const agentsConfig = currentCfg.agents;
|
|
62
|
+
const agentsList = Array.isArray(agentsConfig?.list) ? agentsConfig.list : [];
|
|
63
|
+
// 从 defaults 中提取全局默认 workspace(如果存在且有效)
|
|
64
|
+
const defaultWorkspace = getNonEmptyString(agentsConfig?.defaults?.workspace);
|
|
65
|
+
let hasMainAgent = false;
|
|
66
|
+
const normalizedAgents = agentsList.map((agent) => {
|
|
67
|
+
if (!isMainAgent(agent))
|
|
68
|
+
return agent;
|
|
69
|
+
hasMainAgent = true;
|
|
70
|
+
return withDefaultWorkspace(agent, defaultWorkspace);
|
|
71
|
+
});
|
|
72
|
+
// 列表中已包含 main agent,直接返回
|
|
73
|
+
if (hasMainAgent)
|
|
74
|
+
return normalizedAgents;
|
|
75
|
+
// 自动构造默认 main agent 并插入列表首位
|
|
76
|
+
const mainAgent = {
|
|
77
|
+
id: DEFAULT_AGENT_ID,
|
|
78
|
+
name: DEFAULT_AGENT_ID,
|
|
79
|
+
displayName: DEFAULT_AGENT_ID,
|
|
80
|
+
isDefault: true,
|
|
81
|
+
...(defaultWorkspace && { workspace: defaultWorkspace }),
|
|
82
|
+
};
|
|
83
|
+
return [mainAgent, ...normalizedAgents];
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* 处理 agents:request 事件的核心逻辑
|
|
87
|
+
*
|
|
88
|
+
* 流程:加载配置 → 标准化 agents → 附加 soulId → 发送响应
|
|
89
|
+
*
|
|
90
|
+
* @param from - 请求来源客户端 ID
|
|
91
|
+
* @param botClientId - 当前 bot 客户端 ID
|
|
92
|
+
* @param reliableEmitter - 可靠消息发射器
|
|
93
|
+
*/
|
|
94
|
+
const handleAgentsRequest = (from, botClientId, reliableEmitter) => {
|
|
95
|
+
const pluginRuntime = getLightclawRuntime();
|
|
96
|
+
const currentCfg = pluginRuntime.config.loadConfig();
|
|
97
|
+
const agentsList = normalizeAgentsWithMainAgent(currentCfg);
|
|
98
|
+
// 尝试为 agents 附加 soulId,失败时降级使用无 soulId 的列表
|
|
99
|
+
let agentsWithSoulIds = agentsList;
|
|
100
|
+
try {
|
|
101
|
+
agentsWithSoulIds = attachSoulIdsToAgents(agentsList, readSoulIdMap());
|
|
102
|
+
}
|
|
103
|
+
catch (soulErr) {
|
|
104
|
+
const errMsg = soulErr instanceof Error ? soulErr.message : String(soulErr);
|
|
105
|
+
log.error(`[${CHANNEL_KEY}] 读取 soul 配置 ${LIGHTSOUL_CONFIG_PATH} 失败: ${errMsg}`);
|
|
106
|
+
}
|
|
107
|
+
log.info(`[${CHANNEL_KEY}] 收到 agents 请求,返回数量: ${agentsWithSoulIds.length}`);
|
|
108
|
+
emitAgentsResponse(reliableEmitter, {
|
|
109
|
+
to: from,
|
|
110
|
+
from: botClientId,
|
|
111
|
+
agents: agentsWithSoulIds,
|
|
112
|
+
});
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* 注册 agents:request 事件监听器
|
|
116
|
+
*
|
|
117
|
+
* 当收到其他客户端发来的 agents 请求时,读取最新配置并返回 agents 列表。
|
|
118
|
+
* 忽略自身发出的请求,避免消息回环。
|
|
119
|
+
*
|
|
120
|
+
* @param socket - 原生 Socket 实例
|
|
121
|
+
* @param deps - 依赖项(botClientId、reliableEmitter、onEvent 回调)
|
|
122
|
+
*/
|
|
123
|
+
export function bindAgentsRequestHandler(socket, deps) {
|
|
124
|
+
const { botClientId, reliableEmitter, onEvent } = deps;
|
|
125
|
+
socket.on(EVENT_AGENTS_REQUEST, (data, ack) => {
|
|
126
|
+
// 立即确认收到消息
|
|
127
|
+
ack?.();
|
|
128
|
+
// 忽略自身发出的请求,防止回环
|
|
129
|
+
if (data.from === botClientId)
|
|
130
|
+
return;
|
|
131
|
+
// 触发外部事件回调(如心跳续期)
|
|
132
|
+
onEvent?.();
|
|
133
|
+
try {
|
|
134
|
+
handleAgentsRequest(data.from, botClientId, reliableEmitter);
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
log.error(`[${CHANNEL_KEY}] 处理 agents 请求异常: ${err}`);
|
|
138
|
+
// 发生异常时返回空列表及错误信息
|
|
139
|
+
emitAgentsResponse(reliableEmitter, {
|
|
140
|
+
to: data.from,
|
|
141
|
+
from: botClientId,
|
|
142
|
+
agents: [],
|
|
143
|
+
error: err instanceof Error ? err.message : String(err),
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}
|