lightclawbot 1.2.0-beta.0 → 1.2.0-beta.1
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/channel.js +83 -214
- package/dist/src/gateway.js +4 -6
- package/dist/src/inbound.js +3 -21
- package/dist/src/outbound.js +1 -3
- package/dist/src/runtime.js +4 -24
- package/dist/src/socket/handlers.js +1 -1
- package/dist/src/streaming/stream-reply-sink.js +4 -4
- package/dist/src/types.js +1 -2
- package/package.json +1 -1
package/dist/src/channel.js
CHANGED
|
@@ -1,313 +1,182 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* LightClaw — ChannelPlugin
|
|
2
|
+
* LightClaw — ChannelPlugin 主定义
|
|
3
3
|
*
|
|
4
|
-
* 使用
|
|
4
|
+
* 使用 SDK 标准的 createChatChannelPlugin 构建器,
|
|
5
|
+
* 参照官方 Discord/Slack channel 实现。
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
* base:
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* gateway: Gateway 生命周期 - WebSocket 连接管理和账户生命周期
|
|
11
|
-
* outbound: 出站消息适配器 - 消息发送和媒体文件处理
|
|
12
|
-
*
|
|
13
|
-
* 该插件实现了与 AI 助手服务的 WebSocket 通信、消息收发、状态管理等核心功能。
|
|
7
|
+
* 结构:
|
|
8
|
+
* base: 来自 shared.ts 的 createLightclawPluginBase + 扩展字段
|
|
9
|
+
* security: DM 安全策略
|
|
10
|
+
* outbound: 出站消息适配器
|
|
14
11
|
*/
|
|
15
12
|
import { createChatChannelPlugin } from 'openclaw/plugin-sdk/core';
|
|
16
|
-
// 导入工具函数和常量
|
|
17
13
|
import { chunkText, defaultAccountId } from './utils/index.js';
|
|
18
14
|
import { CHANNEL_KEY, DEFAULT_ACCOUNT_ID, TEXT_CHUNK_LIMIT } from './config.js';
|
|
19
|
-
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
*
|
|
28
|
-
* 使用 createChatChannelPlugin 工厂函数创建类型安全的 ChannelPlugin 实例,
|
|
29
|
-
* 泛型参数 ResolvedAssistantAccount 指定了账户数据的类型结构。
|
|
30
|
-
*
|
|
31
|
-
* @type {ChannelPlugin<ResolvedAssistantAccount>}
|
|
32
|
-
*/
|
|
15
|
+
import { sendText, sendMedia } from './outbound.js';
|
|
16
|
+
import { startGateway } from './gateway.js';
|
|
17
|
+
import { createLightclawPluginBase } from './shared.js';
|
|
18
|
+
import { lightclawSetupAdapter } from './setup-core.js';
|
|
19
|
+
import { normalizeTarget, looksLikeId } from './messaging.js';
|
|
20
|
+
// ============================================================
|
|
21
|
+
// ChannelPlugin 定义(使用 createChatChannelPlugin)
|
|
22
|
+
// ============================================================
|
|
33
23
|
export const lightclawPlugin = createChatChannelPlugin({
|
|
34
|
-
// ==================== 基础配置 ====================
|
|
35
24
|
base: {
|
|
36
25
|
// ---- 来自 shared.ts 的基础定义 ----
|
|
37
|
-
// 使用扩展运算符合并基础配置和 setup 适配器
|
|
38
26
|
...createLightclawPluginBase({
|
|
39
|
-
setup: lightclawSetupAdapter,
|
|
27
|
+
setup: lightclawSetupAdapter,
|
|
40
28
|
}),
|
|
41
|
-
// ----
|
|
29
|
+
// ---- 消息目标解析 ----
|
|
42
30
|
messaging: {
|
|
43
|
-
|
|
44
|
-
* 目标标准化函数
|
|
45
|
-
* 对原始目标字符串进行标准化处理:去除空格、统一格式、转换编码等
|
|
46
|
-
*
|
|
47
|
-
* @param {string} target - 原始目标字符串
|
|
48
|
-
* @returns {string} 标准化后的目标字符串
|
|
49
|
-
*/
|
|
31
|
+
// 对原始目标字符串进行标准化处理,如去除空格、统一格式、转换编码等,返回标准化后的字符串
|
|
50
32
|
normalizeTarget: (target) => {
|
|
51
33
|
return normalizeTarget(target);
|
|
52
34
|
},
|
|
53
|
-
/** 目标解析器配置 */
|
|
54
35
|
targetResolver: {
|
|
55
|
-
|
|
56
|
-
* ID 格式检测函数
|
|
57
|
-
* 判断一个原始字符串是否看起来像一个有效的 ID 格式
|
|
58
|
-
* 用于在完整解析前快速分流处理逻辑,提高性能
|
|
59
|
-
*
|
|
60
|
-
* @param {string} id - 待检测的字符串
|
|
61
|
-
* @param {string} [normalized] - 可选的标准化的字符串
|
|
62
|
-
* @returns {boolean} 是否看起来像 ID 格式
|
|
63
|
-
*/
|
|
36
|
+
// 判断一个原始字符串是否看起来像一个 ID,用于在解析前快速分流处理逻辑
|
|
64
37
|
looksLikeId: (id, normalized) => {
|
|
65
38
|
return looksLikeId(id, normalized);
|
|
66
39
|
},
|
|
67
|
-
/**
|
|
68
|
-
* 目标格式提示文本
|
|
69
|
-
* 显示在 CLI 帮助信息中,指导用户正确的目标格式
|
|
70
|
-
*/
|
|
40
|
+
/** 目标格式提示文本,显示在 CLI 帮助信息中 */
|
|
71
41
|
hint: `user:<userId> or channel:<groupId>`,
|
|
72
42
|
},
|
|
73
43
|
},
|
|
74
|
-
// ----
|
|
44
|
+
// ---- 状态面板 ----
|
|
75
45
|
status: {
|
|
76
|
-
/** 默认运行时状态配置 */
|
|
77
46
|
defaultRuntime: {
|
|
78
|
-
accountId: DEFAULT_ACCOUNT_ID,
|
|
79
|
-
running: false,
|
|
80
|
-
connected: false,
|
|
81
|
-
lastConnectedAt: null,
|
|
82
|
-
lastError: null,
|
|
47
|
+
accountId: DEFAULT_ACCOUNT_ID,
|
|
48
|
+
running: false,
|
|
49
|
+
connected: false,
|
|
50
|
+
lastConnectedAt: null,
|
|
51
|
+
lastError: null,
|
|
83
52
|
},
|
|
84
|
-
/**
|
|
85
|
-
* 构建频道摘要信息
|
|
86
|
-
* 根据快照数据生成频道级别的状态摘要
|
|
87
|
-
*
|
|
88
|
-
* @param {Object} param0 - 参数对象
|
|
89
|
-
* @param {Object} param0.snapshot - 状态快照数据
|
|
90
|
-
* @returns {Object} 频道摘要信息
|
|
91
|
-
*/
|
|
92
53
|
buildChannelSummary: ({ snapshot }) => ({
|
|
93
|
-
configured: snapshot.configured ?? false,
|
|
94
|
-
running: snapshot.running ?? false,
|
|
95
|
-
connected: snapshot.connected ?? false,
|
|
96
|
-
lastConnectedAt: snapshot.lastConnectedAt ?? null,
|
|
97
|
-
lastError: snapshot.lastError ?? null,
|
|
54
|
+
configured: snapshot.configured ?? false,
|
|
55
|
+
running: snapshot.running ?? false,
|
|
56
|
+
connected: snapshot.connected ?? false,
|
|
57
|
+
lastConnectedAt: snapshot.lastConnectedAt ?? null,
|
|
58
|
+
lastError: snapshot.lastError ?? null,
|
|
98
59
|
}),
|
|
99
|
-
/**
|
|
100
|
-
* 构建账户快照信息
|
|
101
|
-
* 根据账户、运行时和配置数据生成账户级别的状态快照
|
|
102
|
-
*
|
|
103
|
-
* @param {Object} param0 - 参数对象
|
|
104
|
-
* @param {ResolvedAssistantAccount} param0.account - 账户信息
|
|
105
|
-
* @param {Object} param0.runtime - 运行时状态
|
|
106
|
-
* @param {OpenClawConfig} param0.cfg - 配置信息
|
|
107
|
-
* @returns {Object} 账户快照信息
|
|
108
|
-
*/
|
|
109
60
|
buildAccountSnapshot: ({ account, runtime, cfg }) => ({
|
|
110
|
-
accountId: account?.accountId ?? defaultAccountId(cfg),
|
|
111
|
-
name: account?.name,
|
|
112
|
-
enabled: account?.enabled ?? false,
|
|
113
|
-
configured: Boolean(account?.apiKey),
|
|
114
|
-
running: runtime?.running ?? false,
|
|
115
|
-
connected: runtime?.connected ?? false,
|
|
116
|
-
lastConnectedAt: runtime?.lastConnectedAt ?? null,
|
|
117
|
-
lastError: runtime?.lastError ?? null,
|
|
61
|
+
accountId: account?.accountId ?? defaultAccountId(cfg),
|
|
62
|
+
name: account?.name,
|
|
63
|
+
enabled: account?.enabled ?? false,
|
|
64
|
+
configured: Boolean(account?.apiKey),
|
|
65
|
+
running: runtime?.running ?? false,
|
|
66
|
+
connected: runtime?.connected ?? false,
|
|
67
|
+
lastConnectedAt: runtime?.lastConnectedAt ?? null,
|
|
68
|
+
lastError: runtime?.lastError ?? null,
|
|
118
69
|
}),
|
|
119
70
|
},
|
|
120
|
-
// ---- Gateway
|
|
71
|
+
// ---- Gateway 生命周期(核心!) ----
|
|
121
72
|
gateway: {
|
|
122
73
|
/**
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
* OpenClaw 框架会在加载配置后为每个启用的账户调用此方法
|
|
126
|
-
*
|
|
127
|
-
* @async
|
|
128
|
-
* @param {Object} ctx - 上下文对象
|
|
129
|
-
* @param {ResolvedAssistantAccount} ctx.account - 账户信息
|
|
130
|
-
* @param {AbortSignal} ctx.abortSignal - 中止信号,用于取消操作
|
|
131
|
-
* @param {Object} ctx.log - 日志记录器
|
|
132
|
-
* @param {OpenClawConfig} ctx.cfg - 配置信息
|
|
133
|
-
* @param {Function} ctx.setStatus - 状态设置函数
|
|
134
|
-
* @param {Function} ctx.getStatus - 状态获取函数
|
|
74
|
+
* 启动账户:建立 WS 长连接到 AI 助手 Server
|
|
75
|
+
* OpenClaw 会在加载配置后为每个 enabled 的账户调用此方法
|
|
135
76
|
*/
|
|
136
77
|
startAccount: async (ctx) => {
|
|
137
|
-
const { account, abortSignal, log, cfg
|
|
138
|
-
|
|
139
|
-
log?.info(`[${CHANNEL_KEY}:${accountId}] Starting gateway...`);
|
|
78
|
+
const { account, abortSignal, log, cfg } = ctx;
|
|
79
|
+
log?.info(`[${CHANNEL_KEY}:${account.accountId}] Starting gateway...`);
|
|
140
80
|
// 启动并维持一个 Socket.IO Gateway 实例
|
|
141
81
|
await startGateway({
|
|
142
|
-
account,
|
|
143
|
-
abortSignal,
|
|
144
|
-
cfg,
|
|
145
|
-
log,
|
|
146
|
-
/**
|
|
147
|
-
* WebSocket 连接就绪回调
|
|
148
|
-
* 当 WebSocket 连接成功建立时调用
|
|
149
|
-
*/
|
|
82
|
+
account,
|
|
83
|
+
abortSignal,
|
|
84
|
+
cfg,
|
|
85
|
+
log,
|
|
86
|
+
/** WS 连接就绪回调 */
|
|
150
87
|
onReady: () => {
|
|
151
|
-
log?.info(`[${CHANNEL_KEY}:${accountId}] Gateway ready: WS connected`);
|
|
88
|
+
log?.info(`[${CHANNEL_KEY}:${account.accountId}] Gateway ready: WS connected`);
|
|
152
89
|
const now = Date.now();
|
|
153
|
-
// 更新运行时状态
|
|
154
90
|
ctx.setStatus({
|
|
155
|
-
...ctx.getStatus(),
|
|
156
|
-
running: true,
|
|
157
|
-
connected: true,
|
|
158
|
-
lastConnectedAt: now,
|
|
91
|
+
...ctx.getStatus(),
|
|
92
|
+
running: true,
|
|
93
|
+
connected: true,
|
|
94
|
+
lastConnectedAt: now,
|
|
159
95
|
// 连接建立时即设置 lastEventAt,作为 health-monitor stale-socket 检测的初始基准
|
|
160
|
-
lastEventAt: now,
|
|
96
|
+
lastEventAt: now,
|
|
161
97
|
});
|
|
162
98
|
},
|
|
163
|
-
/**
|
|
164
|
-
* WebSocket 连接断开回调
|
|
165
|
-
* 当 WebSocket 连接断开时调用
|
|
166
|
-
*/
|
|
99
|
+
/** WS 连接断开回调 */
|
|
167
100
|
onDisconnect: () => {
|
|
168
|
-
log?.info(`[${CHANNEL_KEY}:${accountId}] Gateway ready: WS disconnect`);
|
|
169
|
-
// 更新连接状态为断开
|
|
101
|
+
log?.info(`[${CHANNEL_KEY}:${account.accountId}] Gateway ready: WS disconnect`);
|
|
170
102
|
ctx.setStatus({
|
|
171
|
-
...ctx.getStatus(),
|
|
172
|
-
connected: false,
|
|
103
|
+
...ctx.getStatus(),
|
|
104
|
+
connected: false,
|
|
173
105
|
});
|
|
174
106
|
},
|
|
175
|
-
/**
|
|
176
|
-
* 错误回调
|
|
177
|
-
* 当发生错误时调用,记录错误信息但不中断重连逻辑
|
|
178
|
-
* 重连逻辑由 gateway 内部处理
|
|
179
|
-
*
|
|
180
|
-
* @param {Error} error - 错误对象
|
|
181
|
-
*/
|
|
107
|
+
/** 错误回调:记录错误信息,不中断重连逻辑(由 gateway 内部处理) */
|
|
182
108
|
onError: (error) => {
|
|
183
|
-
log?.error(`[${CHANNEL_KEY}:${accountId}] Gateway error: ${error.message}`);
|
|
184
|
-
// 记录错误信息
|
|
109
|
+
log?.error(`[${CHANNEL_KEY}:${account.accountId}] Gateway error: ${error.message}`);
|
|
185
110
|
ctx.setStatus({
|
|
186
|
-
...ctx.getStatus(),
|
|
187
|
-
lastError: error.message,
|
|
111
|
+
...ctx.getStatus(),
|
|
112
|
+
lastError: error.message,
|
|
188
113
|
});
|
|
189
114
|
},
|
|
190
115
|
/**
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
* -
|
|
194
|
-
* - 通知框架 health-monitor 该连接仍处于活跃状态
|
|
195
|
-
* - 防止因长时间无活动而被判定为僵死连接
|
|
116
|
+
* 入站事件回调:每次收到消息时调用。
|
|
117
|
+
* 刷新 lastEventAt 和 lastInboundAt,
|
|
118
|
+
* 通知框架 health-monitor 该连接仍处于活跃状态。
|
|
196
119
|
*/
|
|
197
120
|
onEvent: () => {
|
|
198
|
-
log?.info(`[${CHANNEL_KEY}:${accountId}] Gateway Ready: WS message enter`);
|
|
199
|
-
// 更新事件时间戳
|
|
121
|
+
log?.info(`[${CHANNEL_KEY}:${account.accountId}] Gateway Ready: WS message enter`);
|
|
200
122
|
ctx.setStatus({
|
|
201
|
-
...ctx.getStatus(),
|
|
202
|
-
lastEventAt: Date.now(),
|
|
203
|
-
lastInboundAt: Date.now(),
|
|
123
|
+
...ctx.getStatus(),
|
|
124
|
+
lastEventAt: Date.now(),
|
|
125
|
+
lastInboundAt: Date.now(),
|
|
204
126
|
});
|
|
205
127
|
},
|
|
206
128
|
});
|
|
207
129
|
},
|
|
208
|
-
/**
|
|
209
|
-
* 登出账户
|
|
210
|
-
* 清除账户的凭证信息(如 API 密钥)
|
|
211
|
-
*
|
|
212
|
-
* @async
|
|
213
|
-
* @param {Object} param0 - 参数对象
|
|
214
|
-
* @param {string} param0.accountId - 账户ID
|
|
215
|
-
* @param {OpenClawConfig} param0.cfg - 配置信息
|
|
216
|
-
* @returns {Promise<{ok: boolean, cleared: boolean}>} 操作结果
|
|
217
|
-
*/
|
|
130
|
+
/** 登出账户:清除凭证 */
|
|
218
131
|
logoutAccount: async ({ accountId, cfg }) => {
|
|
219
|
-
// 创建配置副本以避免修改原始配置
|
|
220
132
|
const nextCfg = { ...cfg };
|
|
221
|
-
// 获取当前 channel 的配置节
|
|
222
133
|
const section = nextCfg.channels?.[CHANNEL_KEY];
|
|
223
|
-
let cleared = false;
|
|
134
|
+
let cleared = false;
|
|
224
135
|
if (section) {
|
|
225
|
-
// 处理默认账户的 API 密钥
|
|
226
136
|
if (accountId === DEFAULT_ACCOUNT_ID && section.apiKeys) {
|
|
227
|
-
delete section.apiKeys;
|
|
228
|
-
cleared = true;
|
|
137
|
+
delete section.apiKeys;
|
|
138
|
+
cleared = true;
|
|
229
139
|
}
|
|
230
|
-
// 处理特定账户的 API 密钥
|
|
231
140
|
const accounts = section.accounts;
|
|
232
141
|
if (accounts?.[accountId]?.apiKeys) {
|
|
233
|
-
delete accounts[accountId].apiKeys;
|
|
234
|
-
cleared = true;
|
|
142
|
+
delete accounts[accountId].apiKeys;
|
|
143
|
+
cleared = true;
|
|
235
144
|
}
|
|
236
145
|
}
|
|
237
|
-
// 返回操作结果
|
|
238
146
|
return { ok: true, cleared };
|
|
239
147
|
},
|
|
240
148
|
},
|
|
241
149
|
},
|
|
242
|
-
//
|
|
150
|
+
// ---- 出站消息适配器 ----
|
|
243
151
|
outbound: {
|
|
244
|
-
// 基础出站配置
|
|
245
152
|
base: {
|
|
246
|
-
deliveryMode: 'direct',
|
|
247
|
-
chunker: chunkText,
|
|
248
|
-
chunkerMode: 'markdown',
|
|
249
|
-
textChunkLimit: TEXT_CHUNK_LIMIT,
|
|
250
|
-
/**
|
|
251
|
-
* 目标解析函数
|
|
252
|
-
* 解析消息发送的目标地址
|
|
253
|
-
*
|
|
254
|
-
* @param {Object} param0 - 参数对象
|
|
255
|
-
* @param {string} param0.to - 显式指定的目标
|
|
256
|
-
* @param {string[]} param0.allowFrom - 允许的来源列表
|
|
257
|
-
* @param {string} param0.mode - 模式('direct' 或 'implicit')
|
|
258
|
-
* @returns {Object} 解析结果
|
|
259
|
-
*/
|
|
153
|
+
deliveryMode: 'direct',
|
|
154
|
+
chunker: chunkText,
|
|
155
|
+
chunkerMode: 'markdown',
|
|
156
|
+
textChunkLimit: TEXT_CHUNK_LIMIT,
|
|
260
157
|
resolveTarget: ({ to, allowFrom, mode }) => {
|
|
261
|
-
//
|
|
158
|
+
// 优先使用显式 to;implicit 模式下回退到 allowFrom 第一个非通配符条目
|
|
262
159
|
const effectiveTo = to?.trim();
|
|
263
160
|
if (effectiveTo) {
|
|
264
161
|
return { ok: true, to: effectiveTo };
|
|
265
162
|
}
|
|
266
|
-
// implicit 模式下回退到 allowFrom 第一个非通配符条目
|
|
267
163
|
if (mode === 'implicit' && allowFrom && allowFrom.length > 0) {
|
|
268
164
|
const candidate = allowFrom.find((entry) => entry && entry !== '*');
|
|
269
165
|
if (candidate) {
|
|
270
166
|
return { ok: true, to: candidate };
|
|
271
167
|
}
|
|
272
168
|
}
|
|
273
|
-
// 没有找到有效目标,返回错误
|
|
274
169
|
return {
|
|
275
170
|
ok: false,
|
|
276
171
|
error: new Error(`No delivery target for ${CHANNEL_KEY}. Specify a target with --to or configure allowFrom.`),
|
|
277
172
|
};
|
|
278
173
|
},
|
|
279
174
|
},
|
|
280
|
-
// 附加结果处理
|
|
281
175
|
attachedResults: {
|
|
282
|
-
channel: CHANNEL_KEY,
|
|
283
|
-
/**
|
|
284
|
-
* 发送文本消息
|
|
285
|
-
*
|
|
286
|
-
* @async
|
|
287
|
-
* @param {Object} param0 - 参数对象
|
|
288
|
-
* @param {string} param0.to - 目标地址
|
|
289
|
-
* @param {string} param0.text - 文本内容
|
|
290
|
-
* @param {string} param0.accountId - 账户ID
|
|
291
|
-
* @param {string} param0.replyToId - 回复消息ID
|
|
292
|
-
* @param {OpenClawConfig} param0.cfg - 配置信息
|
|
293
|
-
* @returns {Promise<any>} 发送结果
|
|
294
|
-
*/
|
|
176
|
+
channel: CHANNEL_KEY,
|
|
295
177
|
sendText: async ({ to, text, accountId, replyToId, cfg }) => {
|
|
296
178
|
return sendText({ to, text, accountId, replyToId, cfg });
|
|
297
179
|
},
|
|
298
|
-
/**
|
|
299
|
-
* 发送媒体消息
|
|
300
|
-
*
|
|
301
|
-
* @async
|
|
302
|
-
* @param {Object} param0 - 参数对象
|
|
303
|
-
* @param {string} param0.to - 目标地址
|
|
304
|
-
* @param {string} param0.text - 文本内容
|
|
305
|
-
* @param {string} param0.mediaUrl - 媒体文件URL
|
|
306
|
-
* @param {string} param0.accountId - 账户ID
|
|
307
|
-
* @param {string} param0.replyToId - 回复消息ID
|
|
308
|
-
* @param {OpenClawConfig} param0.cfg - 配置信息
|
|
309
|
-
* @returns {Promise<any>} 发送结果
|
|
310
|
-
*/
|
|
311
180
|
sendMedia: async ({ to, text, mediaUrl, accountId, replyToId, cfg }) => {
|
|
312
181
|
return sendMedia({ to, text, mediaUrl, accountId, replyToId, cfg });
|
|
313
182
|
},
|
package/dist/src/gateway.js
CHANGED
|
@@ -130,7 +130,6 @@ export async function startGateway(ctx) {
|
|
|
130
130
|
* 底层 emit:将 PrivateMessageData 通过 ReliableEmitter 发送给客户端。
|
|
131
131
|
* - 统一对 content 字段执行 formatCosUrls(将内部 COS 路径转换为公网 URL)
|
|
132
132
|
* - 所有消息都走可靠发送队列,入队即视为"发送成功",实际 ACK 由 ReliableEmitter 保证
|
|
133
|
-
* - 需求约束:所有出站消息必须携带 agentId。调用方未显式指定时,回退到包装 emitter 绑定的 agentId。
|
|
134
133
|
*/
|
|
135
134
|
const emit = (data) => {
|
|
136
135
|
// 完整发送日志由 ReliableEmitter 打印(含 idempotencyKey)
|
|
@@ -147,8 +146,7 @@ export async function startGateway(ctx) {
|
|
|
147
146
|
* @param targetId - 接收方用户 ID
|
|
148
147
|
* @param text - 回复文本内容
|
|
149
148
|
* @param replyToMsgId - 可选,回复对应的原始消息 ID(用于前端展示引用关系)
|
|
150
|
-
* @param agentId -
|
|
151
|
-
* 缺省时由上层包装 emitter 注入,所有出站消息都将携带 agentId。
|
|
149
|
+
* @param agentId - 可选,目标 Agent ID
|
|
152
150
|
*/
|
|
153
151
|
const sendReply = (targetId, text, replyToMsgId, agentId) => {
|
|
154
152
|
log?.info(`[${CHANNEL_KEY}] sendReply: ${text} to ${targetId} (replyTo: ${replyToMsgId || 'none'})`);
|
|
@@ -170,7 +168,7 @@ export async function startGateway(ctx) {
|
|
|
170
168
|
* @param text - 消息文本(可为空字符串)
|
|
171
169
|
* @param files - 文件附件列表({name, mimeType, bytes} 格式)
|
|
172
170
|
* @param replyToMsgId - 可选,回复对应的原始消息 ID
|
|
173
|
-
* @param agentId -
|
|
171
|
+
* @param agentId - 可选,目标 Agent ID
|
|
174
172
|
*/
|
|
175
173
|
const sendFiles = (targetId, text, files, replyToMsgId, agentId) => {
|
|
176
174
|
return emit({
|
|
@@ -202,7 +200,7 @@ export async function startGateway(ctx) {
|
|
|
202
200
|
catch (err) {
|
|
203
201
|
log?.error(`[${CHANNEL_KEY}] Message handler error: ${err}`);
|
|
204
202
|
try {
|
|
205
|
-
emitter.sendReply(msg.senderId, "消息处理异常,请稍后重试。", msg.messageId
|
|
203
|
+
emitter.sendReply(msg.senderId, "消息处理异常,请稍后重试。", msg.messageId);
|
|
206
204
|
emitter.emit({
|
|
207
205
|
msgId: generateMsgId(),
|
|
208
206
|
from: emitter.botClientId,
|
|
@@ -256,7 +254,7 @@ export async function startGateway(ctx) {
|
|
|
256
254
|
// 使用原生 WebSocket 封装层建立连接
|
|
257
255
|
const socket = new NativeSocketClient(WS_URL, {
|
|
258
256
|
// 认证:ticket 作为 URL query 参数,替代 Authorization header
|
|
259
|
-
path: `${SOCKET_PATH}${ticketQuery}`,
|
|
257
|
+
path: `${SOCKET_PATH}${ticketQuery}&enableMultiLogin=false`,
|
|
260
258
|
// 注入日志对象,便于 NativeSocketClient 内部打印连接/重连/错误等诊断信息
|
|
261
259
|
log,
|
|
262
260
|
logPrefix: `[${CHANNEL_KEY}:${account.accountId}:NativeSocket]`,
|
package/dist/src/inbound.js
CHANGED
|
@@ -77,24 +77,7 @@ export function createInboundHandler(account, emitter, log) {
|
|
|
77
77
|
const targetId = msg.senderId;
|
|
78
78
|
// 2. 提前发 typing_start(作为"已读回执",减少 COS 下载/LLM TTFT 期间的无反馈感)
|
|
79
79
|
const replyMsgId = generateMsgId();
|
|
80
|
-
|
|
81
|
-
const effectiveAgentId = route.agentId ?? resolvedAgentId ?? DEFAULT_AGENT_ID;
|
|
82
|
-
// 基于上层 gateway 的 emitter,构造一个绑定当前 agentId 的包装 emitter
|
|
83
|
-
// 所有调用 emit / sendReply / sendFiles 未显式传 agentId 时,消息体都会自动注入 effectiveAgentId
|
|
84
|
-
const boundEmitter = {
|
|
85
|
-
botClientId: emitter.botClientId,
|
|
86
|
-
agentId: effectiveAgentId,
|
|
87
|
-
emit: (data) => emitter.emit({ ...data, agentId: data.agentId ?? effectiveAgentId }),
|
|
88
|
-
sendReply: (tid, text, replyTo, agentId) => emitter.sendReply(tid, text, replyTo, agentId ?? effectiveAgentId),
|
|
89
|
-
sendFiles: (tid, text, files, replyTo, agentId) => emitter.sendFiles(tid, text, files, replyTo, agentId ?? effectiveAgentId),
|
|
90
|
-
};
|
|
91
|
-
const signalCtx = {
|
|
92
|
-
emitter: boundEmitter,
|
|
93
|
-
targetId,
|
|
94
|
-
replyMsgId,
|
|
95
|
-
originalMsgId: msg.messageId,
|
|
96
|
-
agentId: effectiveAgentId,
|
|
97
|
-
};
|
|
80
|
+
const signalCtx = { emitter, targetId, replyMsgId, originalMsgId: msg.messageId, agentId: resolvedAgentId };
|
|
98
81
|
emitSignal(signalCtx, 'typing_start');
|
|
99
82
|
// 3. 处理文件附件(files[] → 本地存储 + 公网 URL)
|
|
100
83
|
const localMediaPaths = [];
|
|
@@ -241,14 +224,13 @@ export function createInboundHandler(account, emitter, log) {
|
|
|
241
224
|
});
|
|
242
225
|
// 7. 构建流式 dispatcher
|
|
243
226
|
const { dispatcher, replyOptions: streamReplyOptions, hasEmittedContent, } = createStreamReplyConfig({
|
|
244
|
-
emitter
|
|
227
|
+
emitter,
|
|
245
228
|
targetId,
|
|
246
229
|
replyMsgId,
|
|
247
230
|
originalMsgId: msg.messageId,
|
|
248
231
|
log,
|
|
249
232
|
effectiveApiKey,
|
|
250
233
|
typingAlreadyStarted: true,
|
|
251
|
-
agentId: effectiveAgentId,
|
|
252
234
|
}, { ...prefixOptions, onModelSelected }, signalCtx);
|
|
253
235
|
// 8. 分发给 AI 引擎(真流式)。abort 由 openclaw fast-abort 处理,sink 层
|
|
254
236
|
// 把英文回执汉化;此处仅兜底自身抛错。
|
|
@@ -268,7 +250,7 @@ export function createInboundHandler(account, emitter, log) {
|
|
|
268
250
|
log?.error(`[${CHANNEL_KEY}] Dispatch error: ${errMsg}`);
|
|
269
251
|
// 已推过可见内容时不再追加错误文案,避免打断阅读。
|
|
270
252
|
if (!hasEmittedContent()) {
|
|
271
|
-
emitter.sendReply(targetId, '抱歉,处理您的消息时出现了问题,请稍后重试', msg.messageId
|
|
253
|
+
emitter.sendReply(targetId, '抱歉,处理您的消息时出现了问题,请稍后重试', msg.messageId);
|
|
272
254
|
}
|
|
273
255
|
}
|
|
274
256
|
};
|
package/dist/src/outbound.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* - 连接断开但 entry 存在(重连中)→ 缓冲消息,重连后自动 flush
|
|
11
11
|
* 2. WS 不可用时 fallback 到 REST API(需配置 apiBaseUrl)
|
|
12
12
|
*/
|
|
13
|
-
import { CHANNEL_KEY, DEFAULT_ACCOUNT_ID,
|
|
13
|
+
import { CHANNEL_KEY, DEFAULT_ACCOUNT_ID, EVENT_MESSAGE_PRIVATE } from './config.js';
|
|
14
14
|
import { getLightclawRuntime } from './runtime.js';
|
|
15
15
|
import { getSocket, bufferMessage, hasEntry, getBotClientId, getReliableEmitter } from './socket/index.js';
|
|
16
16
|
import { resolveAccount } from './utils/index.js';
|
|
@@ -99,8 +99,6 @@ function sendViaSocket(accountId, target, text, replyToId) {
|
|
|
99
99
|
timestamp: Date.now(),
|
|
100
100
|
// undefined 表示非回复消息,避免发送多余字段
|
|
101
101
|
replyToMsgId: replyToId ?? undefined,
|
|
102
|
-
// 所有出站消息必须携带 agentId,outbound 主动发送场景使用默认 Agent
|
|
103
|
-
agentId: DEFAULT_AGENT_ID,
|
|
104
102
|
};
|
|
105
103
|
if (entry) {
|
|
106
104
|
// 策略 1:Socket 已连接,优先走可靠发送(emitWithAck + 自动重试)
|
package/dist/src/runtime.js
CHANGED
|
@@ -1,29 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* LightClaw —
|
|
2
|
+
* LightClaw — 插件运行时存储
|
|
3
3
|
*
|
|
4
|
-
* 使用
|
|
5
|
-
*
|
|
6
|
-
* 该模块的主要功能:
|
|
7
|
-
* - 提供类型安全的运行时存储管理
|
|
8
|
-
* - 避免手动单例模式带来的潜在问题
|
|
9
|
-
* - 提供统一的错误处理和初始化状态管理
|
|
10
|
-
* - 支持插件运行时的动态设置和获取
|
|
4
|
+
* 使用 SDK 标准的 createPluginRuntimeStore 替代手动单例管理,
|
|
5
|
+
* 确保与官方 channel 插件(Discord/Slack)的模式一致。
|
|
11
6
|
*/
|
|
12
|
-
// 导入运行时存储创建工具函数
|
|
13
7
|
import { createPluginRuntimeStore } from 'openclaw/plugin-sdk/runtime-store';
|
|
14
|
-
|
|
15
|
-
* 创建 LightClaw 插件运行时存储
|
|
16
|
-
*
|
|
17
|
-
* 使用 createPluginRuntimeStore 工厂函数创建运行时存储对象,
|
|
18
|
-
* 该函数返回一个包含 setRuntime 和 getRuntime 方法的对象。
|
|
19
|
-
*
|
|
20
|
-
* @template PluginRuntime - 运行时对象的类型参数
|
|
21
|
-
* @param {string} 'LightClaw runtime not initialized' - 运行时未初始化时的错误消息
|
|
22
|
-
* @returns {Object} 包含 setRuntime 和 getRuntime 方法的对象
|
|
23
|
-
* @property {Function} setRuntime - 设置运行时实例的函数
|
|
24
|
-
* @property {Function} getRuntime - 获取运行时实例的函数(如果未设置会抛出错误)
|
|
25
|
-
*/
|
|
26
|
-
const { setRuntime: setLightclawRuntime, getRuntime: getLightclawRuntime } = createPluginRuntimeStore('LightClaw runtime not initialized' // 运行时未初始化时的自定义错误消息
|
|
27
|
-
);
|
|
28
|
-
// 导出运行时管理函数,供其他模块使用
|
|
8
|
+
const { setRuntime: setLightclawRuntime, getRuntime: getLightclawRuntime } = createPluginRuntimeStore('LightClaw runtime not initialized');
|
|
29
9
|
export { getLightclawRuntime, setLightclawRuntime };
|
|
@@ -145,7 +145,7 @@ export function bindSocketHandlers(socket, deps) {
|
|
|
145
145
|
sessionKey: '',
|
|
146
146
|
messages: [],
|
|
147
147
|
error: err instanceof Error ? err.message : String(err),
|
|
148
|
-
agentId: data.agentId,
|
|
148
|
+
agentId: data.agentId || DEFAULT_AGENT_ID,
|
|
149
149
|
}, errorMsgId);
|
|
150
150
|
}
|
|
151
151
|
});
|
|
@@ -30,7 +30,7 @@ function localizeAbortReplyText(text) {
|
|
|
30
30
|
return OPENCLAW_ABORT_REPLY_RE.test(text.trim()) ? LOCALIZED_ABORT_REPLY : text;
|
|
31
31
|
}
|
|
32
32
|
export function createStreamReplyConfig(opts, prefixOptions, signalCtx) {
|
|
33
|
-
const { emitter, targetId, originalMsgId, log, effectiveApiKey, typingAlreadyStarted
|
|
33
|
+
const { emitter, targetId, originalMsgId, log, effectiveApiKey, typingAlreadyStarted } = opts;
|
|
34
34
|
// ── 增量追踪 & 已推送文本 ──
|
|
35
35
|
let partialReplyState = createDeltaTrackerState();
|
|
36
36
|
let streamedText = "";
|
|
@@ -110,11 +110,11 @@ export function createStreamReplyConfig(opts, prefixOptions, signalCtx) {
|
|
|
110
110
|
enrichedText = enrichedText ? `${enrichedText}\n\n${urlSection}` : urlSection;
|
|
111
111
|
}
|
|
112
112
|
if (files.length > 0) {
|
|
113
|
-
emitter.sendFiles(targetId, enrichedText, files, originalMsgId
|
|
113
|
+
emitter.sendFiles(targetId, enrichedText, files, originalMsgId);
|
|
114
114
|
emittedUserVisible = true;
|
|
115
115
|
}
|
|
116
116
|
else if (enrichedText.trim()) {
|
|
117
|
-
emitter.sendReply(targetId, enrichedText, originalMsgId
|
|
117
|
+
emitter.sendReply(targetId, enrichedText, originalMsgId);
|
|
118
118
|
emittedUserVisible = true;
|
|
119
119
|
}
|
|
120
120
|
};
|
|
@@ -256,7 +256,7 @@ export function createStreamReplyConfig(opts, prefixOptions, signalCtx) {
|
|
|
256
256
|
: "LLM only produced thinking, no visible text";
|
|
257
257
|
log?.warn(`[${CHANNEL_KEY}] [stream] NO_REPLY: counts=${JSON.stringify(counts)} ` +
|
|
258
258
|
`toolStart=${toolStartCount} assistantMsg=${assistantMessageCount}. Cause: ${cause}`);
|
|
259
|
-
emitter.sendReply(targetId, "NO_REPLY", originalMsgId
|
|
259
|
+
emitter.sendReply(targetId, "NO_REPLY", originalMsgId);
|
|
260
260
|
}
|
|
261
261
|
else {
|
|
262
262
|
log?.info(`[${CHANNEL_KEY}] [stream] markComplete: counts=${JSON.stringify(counts)} ` +
|
package/dist/src/types.js
CHANGED
|
@@ -24,8 +24,7 @@ export function emitSignal(ctx, kind, content = "", extra) {
|
|
|
24
24
|
timestamp: Date.now(),
|
|
25
25
|
kind,
|
|
26
26
|
...(kind !== "typing_start" ? { replyToMsgId: originalMsgId } : {}),
|
|
27
|
-
// 所有出站信号帧统一携带 agentId,优先使用 signalCtx.agentId,否则回退 emitter 绑定值
|
|
28
|
-
...(agentId || emitter.agentId ? { agentId: agentId ?? emitter.agentId } : {}),
|
|
29
27
|
...extra,
|
|
28
|
+
agentId,
|
|
30
29
|
});
|
|
31
30
|
}
|