@workclaw/openclaw-workclaw 1.0.16 → 1.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -1
- package/index.ts +210 -210
- package/openclaw.plugin.json +1 -0
- package/package.json +12 -5
- package/setup-entry.ts +6 -0
- package/skills/openclaw-workclaw-cron/SKILL.md +45 -28
- package/src/accounts.ts +62 -37
- package/src/api/accounts-api.ts +88 -89
- package/src/api/prompts-api.ts +70 -77
- package/src/api/session-api.ts +99 -108
- package/src/api/skills-api.ts +35 -37
- package/src/api/workspace.ts +27 -29
- package/src/channel.ts +200 -202
- package/src/config-schema.ts +9 -9
- package/src/connection/workclaw-client.ts +554 -567
- package/src/gateway/agent-handlers.ts +392 -426
- package/src/gateway/config-writer.ts +228 -243
- package/src/gateway/message-context.ts +534 -362
- package/src/gateway/message-dispatcher.ts +529 -489
- package/src/gateway/reconnect.ts +217 -113
- package/src/gateway/skills-handler.ts +408 -472
- package/src/gateway/skills-list-handler.ts +9 -9
- package/src/gateway/tools-list-handler.ts +70 -72
- package/src/gateway/workclaw-gateway.ts +328 -486
- package/src/media/upload.ts +83 -94
- package/src/outbound/index.ts +57 -55
- package/src/outbound/workclaw-sender.ts +134 -133
- package/src/runtime.ts +291 -194
- package/src/send.ts +1 -1
- package/src/tools/openclaw-workclaw-cron/api/index.ts +6 -6
- package/src/tools/openclaw-workclaw-cron/src/add/params.ts +20 -19
- package/src/tools/openclaw-workclaw-cron/src/add/sync.ts +2 -2
- package/src/tools/openclaw-workclaw-cron/src/disable/params.ts +1 -1
- package/src/tools/openclaw-workclaw-cron/src/disable/sync.ts +3 -3
- package/src/tools/openclaw-workclaw-cron/src/enable/params.ts +1 -1
- package/src/tools/openclaw-workclaw-cron/src/enable/sync.ts +3 -3
- package/src/tools/openclaw-workclaw-cron/src/notify/sync.ts +2 -2
- package/src/tools/openclaw-workclaw-cron/src/remove/params.ts +1 -1
- package/src/tools/openclaw-workclaw-cron/src/remove/sync.ts +3 -3
- package/src/tools/openclaw-workclaw-cron/src/update/params.ts +195 -197
- package/src/tools/openclaw-workclaw-cron/src/update/sync.ts +4 -4
- package/src/tools/openclaw-workclaw-system/src/get/index.ts +2 -2
- package/src/tools/openclaw-workclaw-system/src/token/index.ts +4 -4
- package/src/types.ts +38 -40
- package/src/utils/content.ts +16 -21
- package/tests/accounts.test.ts +285 -0
- package/tests/message-context.test.ts +313 -0
- package/tests/reconnect.test.ts +257 -0
- package/tests/workclaw-client.test.ts +112 -0
- package/tsconfig.json +8 -5
- package/vitest.config.ts +8 -0
|
@@ -1,525 +1,367 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { MessageDispatcherContext } from './message-dispatcher.js'
|
|
3
|
-
import { buildAccountMap, resolveAccountByUserIdAndAgentId } from '../accounts.js'
|
|
4
|
-
import { clearOpenclawWorkclawTokenCache, openOpenclawWorkclawConnection } from '../connection/workclaw-client.js'
|
|
1
|
+
import type { ResolvedWorkclawAccount, WorkclawConfig, WorkclawAccountConfig, WorkClawBaseConfig } from "../types.js";
|
|
5
2
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
import {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
3
|
+
getWorkclawWsConnection,
|
|
4
|
+
clearWorkclawWsConnection,
|
|
5
|
+
createLogger,
|
|
6
|
+
setWorkclawLoggerFromContext,
|
|
7
|
+
registerAccountContext,
|
|
8
|
+
unregisterAccountContext,
|
|
9
|
+
getAppKeyByAccountId,
|
|
10
|
+
getAllDispatchersByAppKey,
|
|
11
|
+
getDispatcherByAppKeyAndAccountId,
|
|
12
|
+
getReconnectScheduler,
|
|
13
|
+
setReconnectScheduler,
|
|
14
|
+
clearReconnectScheduler,
|
|
15
|
+
tryStartConnecting,
|
|
16
|
+
finishConnecting,
|
|
17
|
+
setWorkclawConnectionConfig,
|
|
18
|
+
clearWorkclawConnectionConfig,
|
|
19
|
+
type TryStartConnectingResult,
|
|
20
|
+
} from "../runtime.js";
|
|
21
|
+
import { sendMessageWorkclaw } from "../outbound/index.js";
|
|
22
|
+
import { clearWorkclawTokenCache, getWorkclawAccessToken, getWorkclawModelConfigByAppKey } from "../connection/workclaw-client.js";
|
|
23
|
+
import { buildAccountMap, resolveAccountByUserIdAndAgentId } from "../accounts.js";
|
|
24
|
+
import { dispatchWorkclawMessage, type MessageDispatcherContext } from "./message-dispatcher.js";
|
|
25
|
+
import { parseWorkClawMessage } from "./message-context.js";
|
|
26
|
+
import { createReconnectScheduler } from "./reconnect.js";
|
|
27
|
+
|
|
28
|
+
export interface WorkclawGatewayOptions {
|
|
29
|
+
accountId: string;
|
|
30
|
+
account: ResolvedWorkclawAccount;
|
|
31
|
+
cfg: any;
|
|
32
|
+
log?: {
|
|
33
|
+
info?: (msg: string) => void;
|
|
34
|
+
warn?: (msg: string) => void;
|
|
35
|
+
error?: (msg: string) => void;
|
|
36
|
+
debug?: (msg: string) => void;
|
|
37
|
+
};
|
|
31
38
|
}
|
|
32
39
|
|
|
33
|
-
export async function
|
|
34
|
-
|
|
40
|
+
export async function startWorkclawGateway(options: WorkclawGatewayOptions): Promise<void> {
|
|
41
|
+
const { accountId, account, cfg, log } = options;
|
|
35
42
|
|
|
36
|
-
|
|
37
|
-
|
|
43
|
+
const logger = createLogger("", log);
|
|
44
|
+
setWorkclawLoggerFromContext(logger);
|
|
38
45
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
46
|
+
const rawConfig = account.config as unknown as WorkclawConfig & WorkclawAccountConfig;
|
|
47
|
+
const connectionMode = rawConfig.connectionMode || "websocket";
|
|
48
|
+
const wsStrategy = rawConfig.wsConnectionStrategy || "per-account";
|
|
42
49
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
50
|
+
if (connectionMode !== "websocket") {
|
|
51
|
+
throw new Error(`Unsupported connectionMode: ${connectionMode}`);
|
|
52
|
+
}
|
|
46
53
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
54
|
+
if (wsStrategy === "per-appKey") {
|
|
55
|
+
await startSharedWebSocket(options);
|
|
56
|
+
} else {
|
|
57
|
+
await startPerAccountWebSocket(options);
|
|
58
|
+
}
|
|
53
59
|
}
|
|
54
60
|
|
|
55
61
|
/**
|
|
56
62
|
* per-account 模式: 每个账号独立 WebSocket 连接
|
|
57
63
|
*/
|
|
58
|
-
async function startPerAccountWebSocket(options:
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const { endpoint, ticket } = await openOpenclawWorkclawConnection(tokenCacheKey, connConfig)
|
|
128
|
-
logger.info(`Gateway Connection opened endpoint=${endpoint} ticketLen=${ticket?.length ?? 0}`)
|
|
129
|
-
const url = `${endpoint}?ticket=${encodeURIComponent(ticket)}`
|
|
130
|
-
|
|
131
|
-
const { Agent } = await import('undici')
|
|
132
|
-
const dispatcher = connConfig.allowInsecureTls
|
|
133
|
-
? new Agent({ connect: { rejectUnauthorized: false } })
|
|
134
|
-
: undefined
|
|
135
|
-
const wsOptions = dispatcher ? { dispatcher } : undefined
|
|
136
|
-
|
|
137
|
-
const ws = new (await import('undici')).WebSocket(url, wsOptions)
|
|
138
|
-
setOpenclawWorkclawWsConnection(accountId, ws)
|
|
139
|
-
|
|
140
|
-
await new Promise<void>((resolve, reject) => {
|
|
141
|
-
const timeout = setTimeout(() => reject(new Error('websocket connection timeout')), 30000)
|
|
142
|
-
|
|
143
|
-
ws.onopen = () => {
|
|
144
|
-
clearTimeout(timeout)
|
|
145
|
-
logger.info(`Gateway WebSocket connected`)
|
|
146
|
-
resolve()
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
ws.onerror = (event: any) => {
|
|
150
|
-
clearTimeout(timeout)
|
|
151
|
-
const errorMsg = String(event?.message ?? event)
|
|
152
|
-
logger.error(`Gateway WebSocket error: ${errorMsg}`)
|
|
153
|
-
reject(new Error(errorMsg))
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
ws.onclose = (event: any) => {
|
|
157
|
-
clearTimeout(timeout)
|
|
158
|
-
reject(new Error(`websocket closed before ready code=${event?.code ?? ''} reason=${event?.reason ?? ''}`))
|
|
159
|
-
}
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
ws.onmessage = (event: any) => {
|
|
163
|
-
const data = typeof event.data === 'string' ? event.data : String(event.data ?? '')
|
|
164
|
-
logger.info(`Gateway WebSocket message received bytes=${data.length}`)
|
|
165
|
-
handleMessage(data)
|
|
166
|
-
}
|
|
64
|
+
async function startPerAccountWebSocket(options: WorkclawGatewayOptions): Promise<void> {
|
|
65
|
+
const { accountId, account, cfg, log } = options;
|
|
66
|
+
|
|
67
|
+
const logger = createLogger("", log);
|
|
68
|
+
const rawConfig = account.config as unknown as WorkclawConfig & WorkclawAccountConfig;
|
|
69
|
+
|
|
70
|
+
const baseConfig: WorkClawBaseConfig = {
|
|
71
|
+
baseUrl: rawConfig.baseUrl,
|
|
72
|
+
websocketUrl: rawConfig.websocketUrl,
|
|
73
|
+
appKey: rawConfig.appKey,
|
|
74
|
+
appSecret: rawConfig.appSecret,
|
|
75
|
+
localIp: rawConfig.localIp,
|
|
76
|
+
allowInsecureTls: rawConfig.allowInsecureTls,
|
|
77
|
+
requestTimeout: rawConfig.requestTimeout,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const accountConfig: WorkclawAccountConfig = {
|
|
81
|
+
agentId: rawConfig.agentId,
|
|
82
|
+
userId: rawConfig.userId,
|
|
83
|
+
openConversationId: rawConfig.openConversationId,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const appKeyRaw = baseConfig.appKey ?? "";
|
|
87
|
+
const appSecretRaw = baseConfig.appSecret ?? "";
|
|
88
|
+
|
|
89
|
+
buildAccountMap(cfg);
|
|
90
|
+
|
|
91
|
+
logger.info(
|
|
92
|
+
`Gateway start (per-account) accountId=${accountId} baseUrl=${baseConfig.baseUrl ?? ""} websocketUrl=${baseConfig.websocketUrl ?? ""} localIp=${baseConfig.localIp ?? ""} allowInsecureTls=${baseConfig.allowInsecureTls} requestTimeout=${baseConfig.requestTimeout ?? ""} appKeyPrefix=${appKeyRaw ? `${appKeyRaw.slice(0, 8)}...` : "missing"} appSecretLen=${appSecretRaw.length}`,
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const ctx: MessageDispatcherContext = {
|
|
96
|
+
accountId,
|
|
97
|
+
account,
|
|
98
|
+
cfg,
|
|
99
|
+
baseConfig,
|
|
100
|
+
accountConfig,
|
|
101
|
+
log: logger,
|
|
102
|
+
scheduleReconnect: () => scheduler.scheduleReconnect(),
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const handleMessage = (data: string) => {
|
|
106
|
+
dispatchWorkclawMessage(data, ctx).catch((err) => {
|
|
107
|
+
logger.error(`Dispatch error: ${String(err)}`);
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const scheduler = createReconnectScheduler({
|
|
112
|
+
key: accountId,
|
|
113
|
+
config: baseConfig,
|
|
114
|
+
log: logger,
|
|
115
|
+
onMessage: handleMessage,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
await scheduler.connect();
|
|
119
|
+
|
|
120
|
+
// Cache the connection config to prevent cfg mutation issues during message sends
|
|
121
|
+
setWorkclawConnectionConfig(accountId, {
|
|
122
|
+
appKey: baseConfig.appKey ?? "",
|
|
123
|
+
appSecret: baseConfig.appSecret ?? "",
|
|
124
|
+
baseUrl: baseConfig.baseUrl,
|
|
125
|
+
websocketUrl: baseConfig.websocketUrl,
|
|
126
|
+
localIp: baseConfig.localIp,
|
|
127
|
+
allowInsecureTls: baseConfig.allowInsecureTls,
|
|
128
|
+
requestTimeout: baseConfig.requestTimeout,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
167
131
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
132
|
+
/**
|
|
133
|
+
* per-appKey 模式: 多个账号共享一个 WebSocket 连接
|
|
134
|
+
*/
|
|
135
|
+
async function startSharedWebSocket(options: WorkclawGatewayOptions): Promise<void> {
|
|
136
|
+
const { accountId, account, cfg, log } = options;
|
|
137
|
+
|
|
138
|
+
const logger = createLogger("", log);
|
|
139
|
+
const rawConfig = account.config as unknown as WorkclawConfig & WorkclawAccountConfig;
|
|
140
|
+
|
|
141
|
+
const baseConfig: WorkClawBaseConfig = {
|
|
142
|
+
baseUrl: rawConfig.baseUrl,
|
|
143
|
+
websocketUrl: rawConfig.websocketUrl,
|
|
144
|
+
appKey: rawConfig.appKey,
|
|
145
|
+
appSecret: rawConfig.appSecret,
|
|
146
|
+
localIp: rawConfig.localIp,
|
|
147
|
+
allowInsecureTls: rawConfig.allowInsecureTls,
|
|
148
|
+
requestTimeout: rawConfig.requestTimeout,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const accountConfig: WorkclawAccountConfig = {
|
|
152
|
+
agentId: rawConfig.agentId,
|
|
153
|
+
userId: rawConfig.userId,
|
|
154
|
+
openConversationId: rawConfig.openConversationId,
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const appKey = baseConfig.appKey || accountId;
|
|
158
|
+
const appKeyRaw = baseConfig.appKey ?? "";
|
|
159
|
+
|
|
160
|
+
buildAccountMap(cfg);
|
|
161
|
+
|
|
162
|
+
logger.info(
|
|
163
|
+
`Gateway start (per-appKey) accountId=${accountId} appKey=${appKeyRaw ? `${appKeyRaw.slice(0, 8)}...` : "missing"} baseUrl=${baseConfig.baseUrl ?? ""} websocketUrl=${baseConfig.websocketUrl ?? ""}`,
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
// 注册账号上下文
|
|
167
|
+
const ctx: MessageDispatcherContext = {
|
|
168
|
+
accountId,
|
|
169
|
+
account,
|
|
170
|
+
cfg,
|
|
171
|
+
baseConfig,
|
|
172
|
+
accountConfig,
|
|
173
|
+
log: logger,
|
|
174
|
+
scheduleReconnect: () => {
|
|
175
|
+
const scheduler = getReconnectScheduler(appKey);
|
|
176
|
+
scheduler?.scheduleReconnect();
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
registerAccountContext(appKey, accountId, ctx);
|
|
181
|
+
|
|
182
|
+
// 检查是否已存在该 appKey 的 WebSocket
|
|
183
|
+
const existingWs = getWorkclawWsConnection(appKey);
|
|
184
|
+
if (existingWs) {
|
|
185
|
+
logger.info(`Gateway (per-appKey) using existing WebSocket for appKey=${appKey}`);
|
|
186
|
+
return;
|
|
172
187
|
}
|
|
173
188
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
189
|
+
// 尝试获取连接锁,防止竞态条件
|
|
190
|
+
const startResult = tryStartConnecting(appKey);
|
|
191
|
+
|
|
192
|
+
if (!startResult.isConnector) {
|
|
193
|
+
// 另一个账号正在连接(成功或失败中),等待共享的 connectingPromise
|
|
194
|
+
logger.info(`Gateway (per-appKey) another account is connecting, waiting... appKey=${appKey}`);
|
|
195
|
+
const resultWs = await startResult.connectingPromise;
|
|
196
|
+
if (resultWs) {
|
|
197
|
+
logger.info(`Gateway (per-appKey) connection established by another account, using it appKey=${appKey}`);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
// 连接失败(resultWs === null),当前账号作为新 connector 重试
|
|
201
|
+
logger.warn(`Gateway (per-appKey) connection attempt failed, will create own connection appKey=${appKey}`);
|
|
202
|
+
} else {
|
|
203
|
+
// 是 connector,创建连接
|
|
204
|
+
logger.info(`Gateway (per-appKey) acquired connector lock, creating connection appKey=${appKey}`);
|
|
186
205
|
}
|
|
187
206
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
207
|
+
// 需要创建新的 WebSocket(只有获取到锁的账号才执行到这里)
|
|
208
|
+
const scheduler = createReconnectScheduler({
|
|
209
|
+
key: appKey,
|
|
210
|
+
config: baseConfig,
|
|
211
|
+
log: logger,
|
|
212
|
+
onMessage: (data: string) => {
|
|
213
|
+
handleSharedMessage(appKey, data, cfg, logger);
|
|
214
|
+
},
|
|
215
|
+
onClose: () => {
|
|
216
|
+
scheduler.scheduleReconnect();
|
|
217
|
+
},
|
|
218
|
+
// 旧代码在 setWorkclawWsConnection 后、ws.onopen 前就调用 finishConnecting,
|
|
219
|
+
// 这个回调模拟同样的时机:ws 已存入 runtime,通知所有等待者
|
|
220
|
+
onConnectingStarted: (ws: any) => {
|
|
221
|
+
finishConnecting(appKey, ws);
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
setReconnectScheduler(appKey, scheduler);
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
await scheduler.connect();
|
|
228
|
+
} catch (err) {
|
|
229
|
+
// 连接失败,通知所有等待者重试(不传 ws 表示失败)
|
|
230
|
+
finishConnecting(appKey);
|
|
231
|
+
throw err;
|
|
199
232
|
}
|
|
200
233
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
234
|
+
// Cache the connection config to prevent cfg mutation issues during message sends
|
|
235
|
+
// In per-appKey mode, each account still has its own config cached by accountId
|
|
236
|
+
setWorkclawConnectionConfig(accountId, {
|
|
237
|
+
appKey: baseConfig.appKey ?? "",
|
|
238
|
+
appSecret: baseConfig.appSecret ?? "",
|
|
239
|
+
baseUrl: baseConfig.baseUrl,
|
|
240
|
+
websocketUrl: baseConfig.websocketUrl,
|
|
241
|
+
localIp: baseConfig.localIp,
|
|
242
|
+
allowInsecureTls: baseConfig.allowInsecureTls,
|
|
243
|
+
requestTimeout: baseConfig.requestTimeout,
|
|
244
|
+
});
|
|
204
245
|
}
|
|
205
246
|
|
|
206
247
|
/**
|
|
207
|
-
* per-appKey
|
|
248
|
+
* per-appKey 模式下处理共享 WebSocket 消息
|
|
208
249
|
*/
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
const logger = createLogger('', log)
|
|
213
|
-
const rawConfig = account.config as unknown as OpenclawWorkclawConfig & OpenclawWorkclawAccountConfig
|
|
214
|
-
|
|
215
|
-
const baseConfig: WorkClawBaseConfig = {
|
|
216
|
-
baseUrl: rawConfig.baseUrl,
|
|
217
|
-
websocketUrl: rawConfig.websocketUrl,
|
|
218
|
-
appKey: rawConfig.appKey,
|
|
219
|
-
appSecret: rawConfig.appSecret,
|
|
220
|
-
localIp: rawConfig.localIp,
|
|
221
|
-
allowInsecureTls: rawConfig.allowInsecureTls,
|
|
222
|
-
requestTimeout: rawConfig.requestTimeout,
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
const accountConfig: OpenclawWorkclawAccountConfig = {
|
|
226
|
-
agentId: rawConfig.agentId,
|
|
227
|
-
userId: rawConfig.userId,
|
|
228
|
-
openConversationId: rawConfig.openConversationId,
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const appKey = baseConfig.appKey || accountId
|
|
232
|
-
const appKeyRaw = baseConfig.appKey ?? ''
|
|
233
|
-
|
|
234
|
-
buildAccountMap(cfg)
|
|
235
|
-
|
|
236
|
-
logger.info(
|
|
237
|
-
`Gateway start (per-appKey) accountId=${accountId} appKey=${appKeyRaw ? `${appKeyRaw.slice(0, 8)}...` : 'missing'} baseUrl=${baseConfig.baseUrl ?? ''} websocketUrl=${baseConfig.websocketUrl ?? ''}`,
|
|
238
|
-
)
|
|
239
|
-
|
|
240
|
-
// 注册账号上下文
|
|
241
|
-
const ctx: MessageDispatcherContext = {
|
|
242
|
-
accountId,
|
|
243
|
-
account,
|
|
244
|
-
cfg,
|
|
245
|
-
baseConfig,
|
|
246
|
-
accountConfig,
|
|
247
|
-
log: logger,
|
|
248
|
-
scheduleReconnect: () => {
|
|
249
|
-
const scheduler = getReconnectScheduler(appKey)
|
|
250
|
-
scheduler?.scheduleReconnect()
|
|
251
|
-
},
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
registerAccountContext(appKey, accountId, ctx)
|
|
255
|
-
|
|
256
|
-
// 检查是否已存在该 appKey 的 WebSocket
|
|
257
|
-
const existingWs = getOpenclawWorkclawWsConnection(appKey)
|
|
258
|
-
if (existingWs) {
|
|
259
|
-
logger.info(`Gateway (per-appKey) using existing WebSocket for appKey=${appKey}`)
|
|
260
|
-
return
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// 尝试获取连接锁,防止竞态条件
|
|
264
|
-
const isConnector = tryStartConnecting(appKey)
|
|
265
|
-
if (!isConnector) {
|
|
266
|
-
// 另一个账号正在连接,等待完成
|
|
267
|
-
logger.info(`Gateway (per-appKey) another account is connecting, waiting... appKey=${appKey}`)
|
|
268
|
-
// 等待直到连接建立完成(通过轮询)
|
|
269
|
-
const maxWait = 30000
|
|
270
|
-
const interval = 500
|
|
271
|
-
const startTime = Date.now()
|
|
272
|
-
while (Date.now() - startTime < maxWait) {
|
|
273
|
-
const ws = getOpenclawWorkclawWsConnection(appKey)
|
|
274
|
-
if (ws) {
|
|
275
|
-
logger.info(`Gateway (per-appKey) connection established by another account, using it appKey=${appKey}`)
|
|
276
|
-
return
|
|
277
|
-
}
|
|
278
|
-
await new Promise(resolve => setTimeout(resolve, interval))
|
|
279
|
-
}
|
|
280
|
-
logger.warn(`Gateway (per-appKey) wait timeout, will create own connection appKey=${appKey}`)
|
|
281
|
-
// 如果等待超时,由当前账号创建连接
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// 需要创建新的 WebSocket(只有获取到锁的账号才执行到这里)
|
|
285
|
-
const scheduler = createReconnectScheduler({
|
|
286
|
-
key: appKey,
|
|
287
|
-
config: baseConfig,
|
|
288
|
-
log: logger,
|
|
289
|
-
onMessage: (data: string) => {
|
|
290
|
-
handleSharedMessage(appKey, data, cfg, logger)
|
|
291
|
-
},
|
|
292
|
-
onReconnectSuccess: () => {
|
|
293
|
-
// 重连成功后重置退避时间
|
|
294
|
-
},
|
|
295
|
-
})
|
|
296
|
-
setReconnectScheduler(appKey, scheduler)
|
|
297
|
-
|
|
298
|
-
try {
|
|
299
|
-
const tokenCacheKey = baseConfig.appKey || accountId
|
|
300
|
-
const connConfig = {
|
|
301
|
-
baseUrl: baseConfig.baseUrl || '',
|
|
302
|
-
websocketUrl: baseConfig.websocketUrl,
|
|
303
|
-
appKey: baseConfig.appKey,
|
|
304
|
-
appSecret: baseConfig.appSecret,
|
|
305
|
-
localIp: baseConfig.localIp,
|
|
306
|
-
allowInsecureTls: baseConfig.allowInsecureTls,
|
|
307
|
-
requestTimeout: baseConfig.requestTimeout,
|
|
308
|
-
}
|
|
250
|
+
function handleSharedMessage(appKey: string, data: string, cfg: any, logger: any): void {
|
|
251
|
+
const parsed = parseWorkClawMessage(data, logger);
|
|
309
252
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
const { Agent } = await import('undici')
|
|
315
|
-
const dispatcher = connConfig.allowInsecureTls
|
|
316
|
-
? new Agent({ connect: { rejectUnauthorized: false } })
|
|
317
|
-
: undefined
|
|
318
|
-
const wsOptions = dispatcher ? { dispatcher } : undefined
|
|
319
|
-
|
|
320
|
-
const ws = new (await import('undici')).WebSocket(url, wsOptions)
|
|
321
|
-
setOpenclawWorkclawWsConnection(appKey, ws)
|
|
322
|
-
finishConnecting(appKey) // 连接已建立,释放锁
|
|
323
|
-
|
|
324
|
-
await new Promise<void>((resolve, reject) => {
|
|
325
|
-
const timeout = setTimeout(() => reject(new Error('websocket connection timeout')), 30000)
|
|
326
|
-
|
|
327
|
-
ws.onopen = () => {
|
|
328
|
-
clearTimeout(timeout)
|
|
329
|
-
logger.info(`Gateway (per-appKey) WebSocket connected`)
|
|
330
|
-
resolve()
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
ws.onerror = (event: any) => {
|
|
334
|
-
clearTimeout(timeout)
|
|
335
|
-
const errorMsg = String(event?.message ?? event)
|
|
336
|
-
logger.error(`Gateway (per-appKey) WebSocket error: ${errorMsg}`)
|
|
337
|
-
reject(new Error(errorMsg))
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
ws.onclose = (event: any) => {
|
|
341
|
-
clearTimeout(timeout)
|
|
342
|
-
reject(new Error(`websocket closed before ready code=${event?.code ?? ''} reason=${event?.reason ?? ''}`))
|
|
343
|
-
}
|
|
344
|
-
})
|
|
345
|
-
|
|
346
|
-
ws.onmessage = (event: any) => {
|
|
347
|
-
const data = typeof event.data === 'string' ? event.data : String(event.data ?? '')
|
|
348
|
-
logger.info(`Gateway (per-appKey) WebSocket message received bytes=${data.length}`)
|
|
349
|
-
logger.info?.(`Gateway (per-appKey) received message: ${data}`)
|
|
350
|
-
handleSharedMessage(appKey, data, cfg, logger)
|
|
253
|
+
if (!parsed) {
|
|
254
|
+
logger.warn?.(`Gateway (per-appKey) failed to parse message`);
|
|
255
|
+
return;
|
|
351
256
|
}
|
|
352
257
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
258
|
+
// ping/pong/disconnect 无需路由
|
|
259
|
+
if (parsed.type === "ping") {
|
|
260
|
+
const ws = getWorkclawWsConnection(appKey);
|
|
261
|
+
if (ws && ws.readyState === 1) { // OPEN
|
|
262
|
+
const pongResponse = {
|
|
263
|
+
code: 200,
|
|
264
|
+
message: "pong",
|
|
265
|
+
metadata: { contentType: "application/json" },
|
|
266
|
+
data: JSON.stringify(parsed.pongData),
|
|
267
|
+
};
|
|
268
|
+
ws.send(JSON.stringify(pongResponse));
|
|
269
|
+
}
|
|
270
|
+
return;
|
|
357
271
|
}
|
|
358
272
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
catch (err) {
|
|
364
|
-
finishConnecting(appKey) // 连接失败,释放锁
|
|
365
|
-
const errorStr = String(err)
|
|
366
|
-
logger.error(`Gateway (per-appKey) connect failed: ${errorStr}`)
|
|
367
|
-
|
|
368
|
-
if (errorStr.includes('访问过于频繁')) {
|
|
369
|
-
logger.warn(`Rate limited, scheduling reconnect...`)
|
|
370
|
-
scheduler.scheduleReconnect()
|
|
371
|
-
throw err
|
|
273
|
+
if (parsed.type === "disconnect") {
|
|
274
|
+
const ws = getWorkclawWsConnection(appKey);
|
|
275
|
+
ws?.close();
|
|
276
|
+
return;
|
|
372
277
|
}
|
|
373
278
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
if (
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
279
|
+
// logger.info?.(`Gateway (per-appKey) received message: ${data}`);
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
// 根据消息类型确定 userId 和 agentId
|
|
283
|
+
let userId: string = "";
|
|
284
|
+
let agentId: string = "";
|
|
285
|
+
|
|
286
|
+
if (parsed.type === "agent_message" && parsed.message) {
|
|
287
|
+
userId = String(parsed.message.userId || "");
|
|
288
|
+
agentId = String(parsed.message.agentId || "");
|
|
289
|
+
} else if (parsed.type === "agent_created") {
|
|
290
|
+
// agent_created 触发新账号创建
|
|
291
|
+
const allDispatchers = getAllDispatchersByAppKey(appKey);
|
|
292
|
+
if (allDispatchers && allDispatchers.size > 0) {
|
|
293
|
+
const firstDispatcher = allDispatchers.values().next().value;
|
|
294
|
+
dispatchWorkclawMessage(data, firstDispatcher.ctx).catch((err) => {
|
|
295
|
+
logger.error?.(`Dispatch error: ${String(err)}`);
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
return;
|
|
299
|
+
} else if (parsed.type === "agent_updated" || parsed.type === "agent_deleted") {
|
|
300
|
+
userId = String(parsed.eventData?.futureId || parsed.eventData?.userId || "");
|
|
301
|
+
agentId = String(parsed.eventData?.id || parsed.eventData?.agentId || "");
|
|
302
|
+
} else if (parsed.type === "tools_list" || parsed.type === "skills_list" || parsed.type === "skills_event") {
|
|
303
|
+
userId = String(parsed.eventData?.userId || "");
|
|
304
|
+
agentId = String(parsed.eventData?.agentId || "");
|
|
305
|
+
} else if (parsed.type === "init_agent") {
|
|
306
|
+
userId = String(parsed.eventData?.futureId || parsed.eventData?.userId || "");
|
|
307
|
+
agentId = String(parsed.eventData?.id || parsed.eventData?.agentId || "");
|
|
308
|
+
/**
|
|
309
|
+
* 在 init_agent 中,配置文件的agentId 为空,所有无法通过 resolveAccountByUserIdAndAgentId 查找账户
|
|
310
|
+
*/
|
|
311
|
+
|
|
312
|
+
const accountId = "default";
|
|
313
|
+
const dispatcher = getDispatcherByAppKeyAndAccountId(appKey, accountId);
|
|
314
|
+
if (dispatcher) {
|
|
315
|
+
dispatchWorkclawMessage(data, dispatcher.ctx).catch((err) => {
|
|
316
|
+
logger.error?.(`Dispatch error: ${String(err)}`);
|
|
317
|
+
});
|
|
318
|
+
} else {
|
|
319
|
+
logger.warn?.(`Gateway (per-appKey) no dispatcher for accountId=${accountId}`);
|
|
320
|
+
}
|
|
321
|
+
return;
|
|
322
|
+
} else {
|
|
323
|
+
// 未知消息类型,忽略
|
|
324
|
+
return;
|
|
385
325
|
}
|
|
386
326
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
/**
|
|
393
|
-
* per-appKey 模式下处理共享 WebSocket 消息
|
|
394
|
-
*/
|
|
395
|
-
function handleSharedMessage(appKey: string, data: string, cfg: any, logger: any): void {
|
|
396
|
-
const parsed = parseWorkClawMessage(data, logger)
|
|
397
|
-
|
|
398
|
-
if (!parsed) {
|
|
399
|
-
logger.warn?.(`Gateway (per-appKey) failed to parse message`)
|
|
400
|
-
return
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
// ping/pong/disconnect 无需路由
|
|
404
|
-
if (parsed.type === 'ping') {
|
|
405
|
-
const ws = getOpenclawWorkclawWsConnection(appKey)
|
|
406
|
-
if (ws && ws.readyState === 1) { // OPEN
|
|
407
|
-
const pongResponse = {
|
|
408
|
-
code: 200,
|
|
409
|
-
message: 'pong',
|
|
410
|
-
metadata: { contentType: 'application/json' },
|
|
411
|
-
data: JSON.stringify(parsed.pongData),
|
|
412
|
-
}
|
|
413
|
-
ws.send(JSON.stringify(pongResponse))
|
|
414
|
-
}
|
|
415
|
-
return
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
if (parsed.type === 'disconnect') {
|
|
419
|
-
const ws = getOpenclawWorkclawWsConnection(appKey)
|
|
420
|
-
ws?.close()
|
|
421
|
-
return
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
logger.info?.(`Gateway (per-appKey) received message: ${data}`)
|
|
425
|
-
|
|
426
|
-
// 根据消息类型确定 userId 和 agentId
|
|
427
|
-
let userId: string = ''
|
|
428
|
-
let agentId: string = ''
|
|
429
|
-
|
|
430
|
-
if (parsed.type === 'agent_message' && parsed.message) {
|
|
431
|
-
userId = String(parsed.message.userId || '')
|
|
432
|
-
agentId = String(parsed.message.agentId || '')
|
|
433
|
-
}
|
|
434
|
-
else if (parsed.type === 'agent_created') {
|
|
435
|
-
// agent_created 触发新账号创建
|
|
436
|
-
const allDispatchers = getAllDispatchersByAppKey(appKey)
|
|
437
|
-
if (allDispatchers && allDispatchers.size > 0) {
|
|
438
|
-
const firstDispatcher = allDispatchers.values().next().value
|
|
439
|
-
if (firstDispatcher) {
|
|
440
|
-
dispatchOpenclawWorkclawMessage(data, firstDispatcher.ctx).catch((err) => {
|
|
441
|
-
logger.error?.(`Dispatch error: ${String(err)}`)
|
|
442
|
-
})
|
|
443
|
-
}
|
|
327
|
+
const accountId = resolveAccountByUserIdAndAgentId(cfg, userId, agentId);
|
|
328
|
+
if (!accountId) {
|
|
329
|
+
logger.warn?.(`Gateway (per-appKey) no account found for userId=${userId} agentId=${agentId}`);
|
|
330
|
+
return;
|
|
444
331
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
else if (parsed.type === 'agent_updated' || parsed.type === 'agent_deleted') {
|
|
448
|
-
userId = String(parsed.eventData?.futureId || parsed.eventData?.userId || '')
|
|
449
|
-
agentId = String(parsed.eventData?.id || parsed.eventData?.agentId || '')
|
|
450
|
-
}
|
|
451
|
-
else if (parsed.type === 'tools_list' || parsed.type === 'skills_list') {
|
|
452
|
-
userId = String(parsed.eventData?.userId || '')
|
|
453
|
-
agentId = String(parsed.eventData?.agentId || '')
|
|
454
|
-
}
|
|
455
|
-
else if(parsed.type === "skills_event") {
|
|
456
|
-
userId = String(parsed.eventData?.data?.userId || "");
|
|
457
|
-
agentId = String(parsed.eventData?.data?.mainId || "");
|
|
458
|
-
}
|
|
459
|
-
else if (parsed.type === 'init_agent') {
|
|
460
|
-
userId = String(parsed.eventData?.futureId || parsed.eventData?.userId || '')
|
|
461
|
-
agentId = String(parsed.eventData?.id || parsed.eventData?.agentId || '')
|
|
462
|
-
/**
|
|
463
|
-
* 在 init_agent 中,配置文件的agentId 为空,所有无法通过 resolveAccountByUserIdAndAgentId 查找账户
|
|
464
|
-
*/
|
|
465
|
-
|
|
466
|
-
const accountId = 'default'
|
|
467
|
-
const dispatcher = getDispatcherByAppKeyAndAccountId(appKey, accountId)
|
|
332
|
+
|
|
333
|
+
const dispatcher = getDispatcherByAppKeyAndAccountId(appKey, accountId);
|
|
468
334
|
if (dispatcher) {
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
logger.warn?.(`Gateway (per-appKey) no dispatcher for accountId=${accountId}`)
|
|
335
|
+
dispatchWorkclawMessage(data, dispatcher.ctx).catch((err) => {
|
|
336
|
+
logger.error?.(`Dispatch error: ${String(err)}`);
|
|
337
|
+
});
|
|
338
|
+
} else {
|
|
339
|
+
logger.warn?.(`Gateway (per-appKey) no dispatcher for accountId=${accountId}`);
|
|
475
340
|
}
|
|
476
|
-
return
|
|
477
|
-
}
|
|
478
|
-
else {
|
|
479
|
-
// 未知消息类型,忽略
|
|
480
|
-
return
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
const accountId = resolveAccountByUserIdAndAgentId(cfg, userId, agentId)
|
|
484
|
-
if (!accountId) {
|
|
485
|
-
logger.warn?.(`Gateway (per-appKey) no account found for userId=${userId} agentId=${agentId}`)
|
|
486
|
-
return
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
const dispatcher = getDispatcherByAppKeyAndAccountId(appKey, accountId)
|
|
490
|
-
if (dispatcher) {
|
|
491
|
-
dispatchOpenclawWorkclawMessage(data, dispatcher.ctx).catch((err) => {
|
|
492
|
-
logger.error?.(`Dispatch error: ${String(err)}`)
|
|
493
|
-
})
|
|
494
|
-
}
|
|
495
|
-
else {
|
|
496
|
-
logger.warn?.(`Gateway (per-appKey) no dispatcher for accountId=${accountId}`)
|
|
497
|
-
}
|
|
498
341
|
}
|
|
499
342
|
|
|
500
|
-
export function
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
343
|
+
export function stopWorkclawGateway(accountId: string, wsStrategy?: string): void {
|
|
344
|
+
if (wsStrategy === "per-appKey") {
|
|
345
|
+
const appKey = getAppKeyByAccountId(accountId);
|
|
346
|
+
if (!appKey) return;
|
|
347
|
+
|
|
348
|
+
unregisterAccountContext(appKey, accountId);
|
|
349
|
+
|
|
350
|
+
if (getAllDispatchersByAppKey(appKey)?.size === 0) {
|
|
351
|
+
const ws = getWorkclawWsConnection(appKey);
|
|
352
|
+
ws?.close();
|
|
353
|
+
clearWorkclawWsConnection(appKey);
|
|
354
|
+
|
|
355
|
+
const scheduler = getReconnectScheduler(appKey);
|
|
356
|
+
scheduler?.stopReconnect();
|
|
357
|
+
clearReconnectScheduler(appKey);
|
|
358
|
+
}
|
|
359
|
+
clearWorkclawConnectionConfig(accountId);
|
|
360
|
+
} else {
|
|
361
|
+
const ws = getWorkclawWsConnection(accountId);
|
|
362
|
+
if (ws) ws.close();
|
|
363
|
+
clearWorkclawWsConnection(accountId);
|
|
364
|
+
clearWorkclawTokenCache(accountId);
|
|
365
|
+
clearWorkclawConnectionConfig(accountId);
|
|
516
366
|
}
|
|
517
|
-
}
|
|
518
|
-
else {
|
|
519
|
-
const ws = getOpenclawWorkclawWsConnection(accountId)
|
|
520
|
-
if (ws)
|
|
521
|
-
ws.close()
|
|
522
|
-
clearOpenclawWorkclawWsConnection(accountId)
|
|
523
|
-
clearOpenclawWorkclawTokenCache(accountId)
|
|
524
|
-
}
|
|
525
367
|
}
|