@workclaw/openclaw-workchat 1.0.0 → 1.0.4
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 +210 -210
- package/index.js +49568 -0
- package/index.ts +19 -19
- package/openclaw.plugin.json +25 -25
- package/package.json +64 -63
- package/src/api/api.ts +260 -260
- package/src/api/types.ts +56 -56
- package/src/auth/accounts.ts +279 -290
- package/src/auth/bind-agent.ts +52 -70
- package/src/auth/login-qr-terminal.ts +16 -16
- package/src/auth/login-qr.ts +477 -475
- package/src/auth/pairing.ts +133 -133
- package/src/channel.ts +282 -282
- package/src/compat.ts +75 -75
- package/src/config/config-schema.ts +29 -29
- package/src/messaging/debug-mode.ts +57 -57
- package/src/messaging/error-notice.ts +30 -30
- package/src/messaging/inbound.ts +174 -174
- package/src/messaging/markdown-filter.ts +361 -361
- package/src/messaging/outbound-hooks.ts +84 -84
- package/src/messaging/process-message.ts +530 -530
- package/src/messaging/send.ts +119 -119
- package/src/messaging/slash-commands.ts +105 -105
- package/src/monitor/monitor.ts +261 -261
- package/src/storage/state-dir.ts +11 -11
- package/src/util/logger.ts +147 -147
- package/src/util/random.ts +9 -9
- package/src/util/redact.ts +39 -39
- package/src/vendor.d.ts +10 -10
package/src/channel.ts
CHANGED
|
@@ -1,282 +1,282 @@
|
|
|
1
|
-
import type { ChannelGatewayContext, ChannelPlugin, OpenClawConfig } from "openclaw/plugin-sdk";
|
|
2
|
-
|
|
3
|
-
import type { ResolvedWorkChatAccount } from "./auth/accounts.js";
|
|
4
|
-
import {
|
|
5
|
-
listWorkChatAccountIds,
|
|
6
|
-
resolveWorkChatAccount,
|
|
7
|
-
} from "./auth/accounts.js";
|
|
8
|
-
import {
|
|
9
|
-
startWorkChatLoginWithQr,
|
|
10
|
-
waitForWorkChatLogin,
|
|
11
|
-
} from "./auth/login-qr.js";
|
|
12
|
-
import type { WorkChatQrStartResult, WorkChatQrWaitResult } from "./auth/login-qr.js";
|
|
13
|
-
import { pushWorkChatMessage, replyWorkChatMessage } from "./messaging/send.js";
|
|
14
|
-
import { logger } from "./util/logger.js";
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Resolve the effective account for an outbound message when the caller did
|
|
18
|
-
* not provide one. With a single bound account it is used directly; multiple
|
|
19
|
-
* accounts require the explicit id (target resolution is ambiguous).
|
|
20
|
-
*/
|
|
21
|
-
function resolveOutboundAccount(cfg: OpenClawConfig, accountId?: string | null): ResolvedWorkChatAccount {
|
|
22
|
-
const allIds = listWorkChatAccountIds(cfg);
|
|
23
|
-
if (allIds.length === 0) {
|
|
24
|
-
throw new Error(
|
|
25
|
-
"workchat: no accounts registered — scan the binding QR code first (`openclaw channels login --channel openclaw-workchat`)",
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
const targetId = accountId?.trim() ?? allIds[0];
|
|
29
|
-
return resolveWorkChatAccount(cfg, targetId);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export const workchatPlugin: ChannelPlugin<ResolvedWorkChatAccount> = {
|
|
33
|
-
id: "openclaw-workchat",
|
|
34
|
-
meta: {
|
|
35
|
-
id: "openclaw-workchat",
|
|
36
|
-
label: "openclaw-workchat",
|
|
37
|
-
selectionLabel: "openclaw-workchat (WebSocket)",
|
|
38
|
-
docsPath: "/channels/openclaw-workchat",
|
|
39
|
-
docsLabel: "openclaw-workchat",
|
|
40
|
-
blurb: "WorkChat open-platform channel: binding QR upstream, reply/push downstream; appKey auth.",
|
|
41
|
-
order: 80,
|
|
42
|
-
},
|
|
43
|
-
gatewayMethods: ["web.login.start", "web.login.wait"],
|
|
44
|
-
configSchema: {
|
|
45
|
-
schema: {
|
|
46
|
-
type: "object",
|
|
47
|
-
additionalProperties: false,
|
|
48
|
-
properties: {},
|
|
49
|
-
},
|
|
50
|
-
},
|
|
51
|
-
capabilities: {
|
|
52
|
-
chatTypes: ["direct"],
|
|
53
|
-
media: true,
|
|
54
|
-
blockStreaming: true,
|
|
55
|
-
},
|
|
56
|
-
streaming: {
|
|
57
|
-
blockStreamingCoalesceDefaults: {
|
|
58
|
-
minChars: 200,
|
|
59
|
-
idleMs: 3000,
|
|
60
|
-
},
|
|
61
|
-
},
|
|
62
|
-
messaging: {
|
|
63
|
-
targetResolver: {
|
|
64
|
-
// WorkChat cloud user ids are numeric strings; treat any non-empty
|
|
65
|
-
// digit-run as a direct ID and skip directory lookup.
|
|
66
|
-
looksLikeId: (raw) => /^\d{4,}$/.test(raw.trim()),
|
|
67
|
-
},
|
|
68
|
-
},
|
|
69
|
-
agentPrompt: {
|
|
70
|
-
messageToolHints: () => [
|
|
71
|
-
"To send a message to the current WorkChat user, use the message tool with action='send'. You do not need to specify 'to' — the current conversation recipient is used automatically.",
|
|
72
|
-
"IMPORTANT: When creating a cron job (scheduled task) for the current WorkChat user, you MUST set delivery.to to the user's WorkChat ID (the numeric ID from the current conversation) AND set delivery.accountId to the current AccountId. Without an explicit 'to', the cron delivery will fail with 'requires target'. Without an explicit 'accountId', the message may be sent from the wrong bot account. Example: delivery: { mode: 'announce', channel: 'openclaw-workchat', to: '<current_user_id>', accountId: '<current_AccountId>' }.",
|
|
73
|
-
"IMPORTANT: When sending media, set 'media' to a URL the WorkChat platform can fetch (an HTTPS URL). Local file paths are not supported by the WorkChat send API yet — include the link in the message text instead.",
|
|
74
|
-
],
|
|
75
|
-
},
|
|
76
|
-
reload: { configPrefixes: ["channels.openclaw-workchat"] },
|
|
77
|
-
config: {
|
|
78
|
-
listAccountIds: (cfg) => listWorkChatAccountIds(cfg),
|
|
79
|
-
resolveAccount: (cfg, accountId) => resolveWorkChatAccount(cfg, accountId),
|
|
80
|
-
isConfigured: (account) => account.configured,
|
|
81
|
-
describeAccount: (account) => ({
|
|
82
|
-
accountId: account.accountId,
|
|
83
|
-
name: account.name,
|
|
84
|
-
enabled: account.enabled,
|
|
85
|
-
configured: account.configured,
|
|
86
|
-
}),
|
|
87
|
-
},
|
|
88
|
-
outbound: {
|
|
89
|
-
deliveryMode: "direct",
|
|
90
|
-
textChunkLimit: 4000,
|
|
91
|
-
sendText: async (ctx) => {
|
|
92
|
-
const account = resolveOutboundAccount(ctx.cfg, ctx.accountId);
|
|
93
|
-
const replyToId = ctx.replyToId?.trim();
|
|
94
|
-
const result = replyToId
|
|
95
|
-
? await replyWorkChatMessage({
|
|
96
|
-
account,
|
|
97
|
-
to: ctx.to,
|
|
98
|
-
text: ctx.text,
|
|
99
|
-
replyToMessageId: replyToId,
|
|
100
|
-
})
|
|
101
|
-
: await pushWorkChatMessage({
|
|
102
|
-
account,
|
|
103
|
-
to: ctx.to,
|
|
104
|
-
text: ctx.text,
|
|
105
|
-
});
|
|
106
|
-
return { channel: "openclaw-workchat", messageId: result.messageId };
|
|
107
|
-
},
|
|
108
|
-
sendMedia: async (ctx) => {
|
|
109
|
-
// Media sends degrade to a text message carrying the media URL until the
|
|
110
|
-
// platform media-upload API is confirmed.
|
|
111
|
-
const account = resolveOutboundAccount(ctx.cfg, ctx.accountId);
|
|
112
|
-
const text = ctx.text?.trim() ? ctx.text : ctx.mediaUrl ?? "";
|
|
113
|
-
const result = await pushWorkChatMessage({
|
|
114
|
-
account,
|
|
115
|
-
to: ctx.to,
|
|
116
|
-
text,
|
|
117
|
-
});
|
|
118
|
-
return { channel: "openclaw-workchat", messageId: result.messageId };
|
|
119
|
-
},
|
|
120
|
-
},
|
|
121
|
-
status: {
|
|
122
|
-
defaultRuntime: {
|
|
123
|
-
accountId: "",
|
|
124
|
-
lastError: null,
|
|
125
|
-
lastInboundAt: null,
|
|
126
|
-
lastOutboundAt: null,
|
|
127
|
-
},
|
|
128
|
-
collectStatusIssues: () => [],
|
|
129
|
-
buildChannelSummary: ({ snapshot }) => ({
|
|
130
|
-
configured: snapshot.configured ?? false,
|
|
131
|
-
lastError: snapshot.lastError ?? null,
|
|
132
|
-
lastInboundAt: snapshot.lastInboundAt ?? null,
|
|
133
|
-
lastOutboundAt: snapshot.lastOutboundAt ?? null,
|
|
134
|
-
}),
|
|
135
|
-
buildAccountSnapshot: ({ account, runtime }) => ({
|
|
136
|
-
...runtime,
|
|
137
|
-
accountId: account.accountId,
|
|
138
|
-
name: account.name,
|
|
139
|
-
enabled: account.enabled,
|
|
140
|
-
configured: account.configured,
|
|
141
|
-
}),
|
|
142
|
-
},
|
|
143
|
-
gateway: {
|
|
144
|
-
startAccount: async (ctx: ChannelGatewayContext<ResolvedWorkChatAccount>) => {
|
|
145
|
-
logger.debug(`startAccount entry`);
|
|
146
|
-
if (!ctx) {
|
|
147
|
-
logger.warn(`gateway.startAccount: called with undefined ctx, skipping`);
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
const account = ctx.account;
|
|
151
|
-
const aLog = logger.withAccount(account.accountId);
|
|
152
|
-
aLog.info(`starting workchat websocket monitor`);
|
|
153
|
-
|
|
154
|
-
if (!account.configured) {
|
|
155
|
-
aLog.error(`account not configured`);
|
|
156
|
-
ctx.log?.error?.(
|
|
157
|
-
`[${account.accountId}] workchat not configured — missing WORKCHAT_API_KEY/WORKCHAT_APP_SECRET`,
|
|
158
|
-
);
|
|
159
|
-
ctx.setStatus?.({ accountId: account.accountId, running: false });
|
|
160
|
-
throw new Error("workchat not configured: missing appKey/appSecret");
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// The gateway injects the channel runtime surface per-call (task-scoped). We require it:
|
|
164
|
-
// it carries reply/routing/session helpers used by processOneFrame.
|
|
165
|
-
if (!ctx.channelRuntime) {
|
|
166
|
-
const msg = `ctx.channelRuntime missing — host too old or plugin SDK contract violated`;
|
|
167
|
-
aLog.error(msg);
|
|
168
|
-
ctx.log?.error?.(`[${account.accountId}] ${msg}`);
|
|
169
|
-
ctx.setStatus?.({ accountId: account.accountId, running: false });
|
|
170
|
-
throw new Error(msg);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const { monitorWorkChatProvider } = await import("./monitor/monitor.js");
|
|
174
|
-
return monitorWorkChatProvider({
|
|
175
|
-
apiOpts: {
|
|
176
|
-
baseUrl: account.baseUrl,
|
|
177
|
-
appKey: account.appKey,
|
|
178
|
-
appSecret: account.appSecret,
|
|
179
|
-
timeoutMs: account.requestTimeout,
|
|
180
|
-
allowInsecureTls: account.allowInsecureTls,
|
|
181
|
-
},
|
|
182
|
-
accountId: account.accountId,
|
|
183
|
-
account,
|
|
184
|
-
config: ctx.cfg,
|
|
185
|
-
runtime: ctx.runtime,
|
|
186
|
-
channelRuntime: ctx.channelRuntime as unknown as import("openclaw/plugin-sdk").PluginRuntime["channel"],
|
|
187
|
-
abortSignal: ctx.abortSignal,
|
|
188
|
-
setStatus: ctx.setStatus,
|
|
189
|
-
});
|
|
190
|
-
},
|
|
191
|
-
stopAccount: async (ctx: ChannelGatewayContext<ResolvedWorkChatAccount>) => {
|
|
192
|
-
// The WebSocket monitor exits via its abortSignal; nothing extra to do.
|
|
193
|
-
const account = ctx.account;
|
|
194
|
-
logger.withAccount(account.accountId).info(`stopAccount`);
|
|
195
|
-
ctx.setStatus?.({ accountId: account.accountId, running: false });
|
|
196
|
-
},
|
|
197
|
-
/**
|
|
198
|
-
* WorkChat 绑定二维码:由网关 RPC (web.login.start / web.login.wait) 调用。
|
|
199
|
-
* 用户在 WorkChat 平台扫码创建/绑定智能体;完成后平台推送
|
|
200
|
-
* AGENT_CREATED/INIT_AGENT,插件持久化 agentId/userId。
|
|
201
|
-
*/
|
|
202
|
-
loginWithQrStart: async ({ accountId, force, verbose }) => {
|
|
203
|
-
void force;
|
|
204
|
-
void verbose;
|
|
205
|
-
// The gateway adapter has no cfg in scope here — read the live config
|
|
206
|
-
// from disk (the gateway's own config, same file triggerWorkChatChannelReload
|
|
207
|
-
// writes to) so the ${WORKCHAT_API_KEY} placeholder is resolved by the host.
|
|
208
|
-
const { loadConfig } = await import("openclaw/plugin-sdk/config-runtime");
|
|
209
|
-
const cfg = loadConfig() as OpenClawConfig;
|
|
210
|
-
const account = resolveWorkChatAccount(cfg, accountId ?? "default");
|
|
211
|
-
const result: WorkChatQrStartResult = await startWorkChatLoginWithQr({
|
|
212
|
-
baseUrl: account.baseUrl,
|
|
213
|
-
appKey: account.appKey,
|
|
214
|
-
appSecret: account.appSecret,
|
|
215
|
-
});
|
|
216
|
-
return {
|
|
217
|
-
qrDataUrl: result.qrcodeUrl,
|
|
218
|
-
message: result.message,
|
|
219
|
-
sessionKey: result.sessionKey,
|
|
220
|
-
} as { qrDataUrl?: string; message: string };
|
|
221
|
-
},
|
|
222
|
-
loginWithQrWait: async (params) => {
|
|
223
|
-
// sessionKey is forwarded by the client after loginWithQrStart (runtime param extension).
|
|
224
|
-
const sessionKey = (params as { sessionKey?: string }).sessionKey || params.accountId || "";
|
|
225
|
-
const result: WorkChatQrWaitResult = await waitForWorkChatLogin({
|
|
226
|
-
sessionKey,
|
|
227
|
-
timeoutMs: params.timeoutMs,
|
|
228
|
-
});
|
|
229
|
-
return {
|
|
230
|
-
connected: result.connected,
|
|
231
|
-
message: result.message,
|
|
232
|
-
accountId: result.accountId,
|
|
233
|
-
agentId: result.agentId,
|
|
234
|
-
} as { connected: boolean; message: string };
|
|
235
|
-
},
|
|
236
|
-
},
|
|
237
|
-
auth: {
|
|
238
|
-
login: async ({ cfg, accountId, verbose, runtime }) => {
|
|
239
|
-
const account = resolveWorkChatAccount(cfg, accountId);
|
|
240
|
-
|
|
241
|
-
const log = (msg: string) => {
|
|
242
|
-
runtime?.log?.(msg);
|
|
243
|
-
};
|
|
244
|
-
|
|
245
|
-
log(`正在启动...`);
|
|
246
|
-
const startResult: WorkChatQrStartResult = await startWorkChatLoginWithQr({
|
|
247
|
-
baseUrl: account.baseUrl,
|
|
248
|
-
appKey: account.appKey,
|
|
249
|
-
appSecret: account.appSecret,
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
if (!startResult.qrcodeUrl) {
|
|
253
|
-
logger.warn(
|
|
254
|
-
`auth.login: failed to get QR code accountId=${account.accountId} message=${startResult.message}`,
|
|
255
|
-
);
|
|
256
|
-
log(startResult.message);
|
|
257
|
-
throw new Error(startResult.message);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
log(`\n请在 WorkChat 平台扫描以下二维码,以继续绑定:\n`);
|
|
261
|
-
const { displayQRCode } = await import("./auth/login-qr-terminal.js");
|
|
262
|
-
await displayQRCode(startResult.qrcodeUrl!);
|
|
263
|
-
|
|
264
|
-
const loginTimeoutMs = 480_000;
|
|
265
|
-
log(`\n正在等待操作...\n`);
|
|
266
|
-
|
|
267
|
-
const waitResult: WorkChatQrWaitResult = await waitForWorkChatLogin({
|
|
268
|
-
sessionKey: startResult.sessionKey,
|
|
269
|
-
timeoutMs: loginTimeoutMs,
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
if (waitResult.connected) {
|
|
273
|
-
log(`\n已将此 OpenClaw 绑定到 WorkChat。`);
|
|
274
|
-
} else {
|
|
275
|
-
logger.warn(
|
|
276
|
-
`auth.login: binding did not complete accountId=${account.accountId} message=${waitResult.message}`,
|
|
277
|
-
);
|
|
278
|
-
throw new Error(waitResult.message);
|
|
279
|
-
}
|
|
280
|
-
},
|
|
281
|
-
},
|
|
282
|
-
};
|
|
1
|
+
import type { ChannelGatewayContext, ChannelPlugin, OpenClawConfig } from "openclaw/plugin-sdk";
|
|
2
|
+
|
|
3
|
+
import type { ResolvedWorkChatAccount } from "./auth/accounts.js";
|
|
4
|
+
import {
|
|
5
|
+
listWorkChatAccountIds,
|
|
6
|
+
resolveWorkChatAccount,
|
|
7
|
+
} from "./auth/accounts.js";
|
|
8
|
+
import {
|
|
9
|
+
startWorkChatLoginWithQr,
|
|
10
|
+
waitForWorkChatLogin,
|
|
11
|
+
} from "./auth/login-qr.js";
|
|
12
|
+
import type { WorkChatQrStartResult, WorkChatQrWaitResult } from "./auth/login-qr.js";
|
|
13
|
+
import { pushWorkChatMessage, replyWorkChatMessage } from "./messaging/send.js";
|
|
14
|
+
import { logger } from "./util/logger.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Resolve the effective account for an outbound message when the caller did
|
|
18
|
+
* not provide one. With a single bound account it is used directly; multiple
|
|
19
|
+
* accounts require the explicit id (target resolution is ambiguous).
|
|
20
|
+
*/
|
|
21
|
+
function resolveOutboundAccount(cfg: OpenClawConfig, accountId?: string | null): ResolvedWorkChatAccount {
|
|
22
|
+
const allIds = listWorkChatAccountIds(cfg);
|
|
23
|
+
if (allIds.length === 0) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
"workchat: no accounts registered — scan the binding QR code first (`openclaw channels login --channel openclaw-workchat`)",
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
const targetId = accountId?.trim() ?? allIds[0];
|
|
29
|
+
return resolveWorkChatAccount(cfg, targetId);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const workchatPlugin: ChannelPlugin<ResolvedWorkChatAccount> = {
|
|
33
|
+
id: "openclaw-workchat",
|
|
34
|
+
meta: {
|
|
35
|
+
id: "openclaw-workchat",
|
|
36
|
+
label: "openclaw-workchat",
|
|
37
|
+
selectionLabel: "openclaw-workchat (WebSocket)",
|
|
38
|
+
docsPath: "/channels/openclaw-workchat",
|
|
39
|
+
docsLabel: "openclaw-workchat",
|
|
40
|
+
blurb: "WorkChat open-platform channel: binding QR upstream, reply/push downstream; appKey auth.",
|
|
41
|
+
order: 80,
|
|
42
|
+
},
|
|
43
|
+
gatewayMethods: ["web.login.start", "web.login.wait"],
|
|
44
|
+
configSchema: {
|
|
45
|
+
schema: {
|
|
46
|
+
type: "object",
|
|
47
|
+
additionalProperties: false,
|
|
48
|
+
properties: {},
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
capabilities: {
|
|
52
|
+
chatTypes: ["direct"],
|
|
53
|
+
media: true,
|
|
54
|
+
blockStreaming: true,
|
|
55
|
+
},
|
|
56
|
+
streaming: {
|
|
57
|
+
blockStreamingCoalesceDefaults: {
|
|
58
|
+
minChars: 200,
|
|
59
|
+
idleMs: 3000,
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
messaging: {
|
|
63
|
+
targetResolver: {
|
|
64
|
+
// WorkChat cloud user ids are numeric strings; treat any non-empty
|
|
65
|
+
// digit-run as a direct ID and skip directory lookup.
|
|
66
|
+
looksLikeId: (raw) => /^\d{4,}$/.test(raw.trim()),
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
agentPrompt: {
|
|
70
|
+
messageToolHints: () => [
|
|
71
|
+
"To send a message to the current WorkChat user, use the message tool with action='send'. You do not need to specify 'to' — the current conversation recipient is used automatically.",
|
|
72
|
+
"IMPORTANT: When creating a cron job (scheduled task) for the current WorkChat user, you MUST set delivery.to to the user's WorkChat ID (the numeric ID from the current conversation) AND set delivery.accountId to the current AccountId. Without an explicit 'to', the cron delivery will fail with 'requires target'. Without an explicit 'accountId', the message may be sent from the wrong bot account. Example: delivery: { mode: 'announce', channel: 'openclaw-workchat', to: '<current_user_id>', accountId: '<current_AccountId>' }.",
|
|
73
|
+
"IMPORTANT: When sending media, set 'media' to a URL the WorkChat platform can fetch (an HTTPS URL). Local file paths are not supported by the WorkChat send API yet — include the link in the message text instead.",
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
reload: { configPrefixes: ["channels.openclaw-workchat"] },
|
|
77
|
+
config: {
|
|
78
|
+
listAccountIds: (cfg) => listWorkChatAccountIds(cfg),
|
|
79
|
+
resolveAccount: (cfg, accountId) => resolveWorkChatAccount(cfg, accountId),
|
|
80
|
+
isConfigured: (account) => account.configured,
|
|
81
|
+
describeAccount: (account) => ({
|
|
82
|
+
accountId: account.accountId,
|
|
83
|
+
name: account.name,
|
|
84
|
+
enabled: account.enabled,
|
|
85
|
+
configured: account.configured,
|
|
86
|
+
}),
|
|
87
|
+
},
|
|
88
|
+
outbound: {
|
|
89
|
+
deliveryMode: "direct",
|
|
90
|
+
textChunkLimit: 4000,
|
|
91
|
+
sendText: async (ctx) => {
|
|
92
|
+
const account = resolveOutboundAccount(ctx.cfg, ctx.accountId);
|
|
93
|
+
const replyToId = ctx.replyToId?.trim();
|
|
94
|
+
const result = replyToId
|
|
95
|
+
? await replyWorkChatMessage({
|
|
96
|
+
account,
|
|
97
|
+
to: ctx.to,
|
|
98
|
+
text: ctx.text,
|
|
99
|
+
replyToMessageId: replyToId,
|
|
100
|
+
})
|
|
101
|
+
: await pushWorkChatMessage({
|
|
102
|
+
account,
|
|
103
|
+
to: ctx.to,
|
|
104
|
+
text: ctx.text,
|
|
105
|
+
});
|
|
106
|
+
return { channel: "openclaw-workchat", messageId: result.messageId };
|
|
107
|
+
},
|
|
108
|
+
sendMedia: async (ctx) => {
|
|
109
|
+
// Media sends degrade to a text message carrying the media URL until the
|
|
110
|
+
// platform media-upload API is confirmed.
|
|
111
|
+
const account = resolveOutboundAccount(ctx.cfg, ctx.accountId);
|
|
112
|
+
const text = ctx.text?.trim() ? ctx.text : ctx.mediaUrl ?? "";
|
|
113
|
+
const result = await pushWorkChatMessage({
|
|
114
|
+
account,
|
|
115
|
+
to: ctx.to,
|
|
116
|
+
text,
|
|
117
|
+
});
|
|
118
|
+
return { channel: "openclaw-workchat", messageId: result.messageId };
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
status: {
|
|
122
|
+
defaultRuntime: {
|
|
123
|
+
accountId: "",
|
|
124
|
+
lastError: null,
|
|
125
|
+
lastInboundAt: null,
|
|
126
|
+
lastOutboundAt: null,
|
|
127
|
+
},
|
|
128
|
+
collectStatusIssues: () => [],
|
|
129
|
+
buildChannelSummary: ({ snapshot }) => ({
|
|
130
|
+
configured: snapshot.configured ?? false,
|
|
131
|
+
lastError: snapshot.lastError ?? null,
|
|
132
|
+
lastInboundAt: snapshot.lastInboundAt ?? null,
|
|
133
|
+
lastOutboundAt: snapshot.lastOutboundAt ?? null,
|
|
134
|
+
}),
|
|
135
|
+
buildAccountSnapshot: ({ account, runtime }) => ({
|
|
136
|
+
...runtime,
|
|
137
|
+
accountId: account.accountId,
|
|
138
|
+
name: account.name,
|
|
139
|
+
enabled: account.enabled,
|
|
140
|
+
configured: account.configured,
|
|
141
|
+
}),
|
|
142
|
+
},
|
|
143
|
+
gateway: {
|
|
144
|
+
startAccount: async (ctx: ChannelGatewayContext<ResolvedWorkChatAccount>) => {
|
|
145
|
+
logger.debug(`startAccount entry`);
|
|
146
|
+
if (!ctx) {
|
|
147
|
+
logger.warn(`gateway.startAccount: called with undefined ctx, skipping`);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const account = ctx.account;
|
|
151
|
+
const aLog = logger.withAccount(account.accountId);
|
|
152
|
+
aLog.info(`starting workchat websocket monitor`);
|
|
153
|
+
|
|
154
|
+
if (!account.configured) {
|
|
155
|
+
aLog.error(`account not configured`);
|
|
156
|
+
ctx.log?.error?.(
|
|
157
|
+
`[${account.accountId}] workchat not configured — missing WORKCHAT_API_KEY/WORKCHAT_APP_SECRET`,
|
|
158
|
+
);
|
|
159
|
+
ctx.setStatus?.({ accountId: account.accountId, running: false });
|
|
160
|
+
throw new Error("workchat not configured: missing appKey/appSecret");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// The gateway injects the channel runtime surface per-call (task-scoped). We require it:
|
|
164
|
+
// it carries reply/routing/session helpers used by processOneFrame.
|
|
165
|
+
if (!ctx.channelRuntime) {
|
|
166
|
+
const msg = `ctx.channelRuntime missing — host too old or plugin SDK contract violated`;
|
|
167
|
+
aLog.error(msg);
|
|
168
|
+
ctx.log?.error?.(`[${account.accountId}] ${msg}`);
|
|
169
|
+
ctx.setStatus?.({ accountId: account.accountId, running: false });
|
|
170
|
+
throw new Error(msg);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const { monitorWorkChatProvider } = await import("./monitor/monitor.js");
|
|
174
|
+
return monitorWorkChatProvider({
|
|
175
|
+
apiOpts: {
|
|
176
|
+
baseUrl: account.baseUrl,
|
|
177
|
+
appKey: account.appKey,
|
|
178
|
+
appSecret: account.appSecret,
|
|
179
|
+
timeoutMs: account.requestTimeout,
|
|
180
|
+
allowInsecureTls: account.allowInsecureTls,
|
|
181
|
+
},
|
|
182
|
+
accountId: account.accountId,
|
|
183
|
+
account,
|
|
184
|
+
config: ctx.cfg,
|
|
185
|
+
runtime: ctx.runtime,
|
|
186
|
+
channelRuntime: ctx.channelRuntime as unknown as import("openclaw/plugin-sdk").PluginRuntime["channel"],
|
|
187
|
+
abortSignal: ctx.abortSignal,
|
|
188
|
+
setStatus: ctx.setStatus,
|
|
189
|
+
});
|
|
190
|
+
},
|
|
191
|
+
stopAccount: async (ctx: ChannelGatewayContext<ResolvedWorkChatAccount>) => {
|
|
192
|
+
// The WebSocket monitor exits via its abortSignal; nothing extra to do.
|
|
193
|
+
const account = ctx.account;
|
|
194
|
+
logger.withAccount(account.accountId).info(`stopAccount`);
|
|
195
|
+
ctx.setStatus?.({ accountId: account.accountId, running: false });
|
|
196
|
+
},
|
|
197
|
+
/**
|
|
198
|
+
* WorkChat 绑定二维码:由网关 RPC (web.login.start / web.login.wait) 调用。
|
|
199
|
+
* 用户在 WorkChat 平台扫码创建/绑定智能体;完成后平台推送
|
|
200
|
+
* AGENT_CREATED/INIT_AGENT,插件持久化 agentId/userId。
|
|
201
|
+
*/
|
|
202
|
+
loginWithQrStart: async ({ accountId, force, verbose }) => {
|
|
203
|
+
void force;
|
|
204
|
+
void verbose;
|
|
205
|
+
// The gateway adapter has no cfg in scope here — read the live config
|
|
206
|
+
// from disk (the gateway's own config, same file triggerWorkChatChannelReload
|
|
207
|
+
// writes to) so the ${WORKCHAT_API_KEY} placeholder is resolved by the host.
|
|
208
|
+
const { loadConfig } = await import("openclaw/plugin-sdk/config-runtime");
|
|
209
|
+
const cfg = loadConfig() as OpenClawConfig;
|
|
210
|
+
const account = resolveWorkChatAccount(cfg, accountId ?? "default");
|
|
211
|
+
const result: WorkChatQrStartResult = await startWorkChatLoginWithQr({
|
|
212
|
+
baseUrl: account.baseUrl,
|
|
213
|
+
appKey: account.appKey,
|
|
214
|
+
appSecret: account.appSecret,
|
|
215
|
+
});
|
|
216
|
+
return {
|
|
217
|
+
qrDataUrl: result.qrcodeUrl,
|
|
218
|
+
message: result.message,
|
|
219
|
+
sessionKey: result.sessionKey,
|
|
220
|
+
} as { qrDataUrl?: string; message: string };
|
|
221
|
+
},
|
|
222
|
+
loginWithQrWait: async (params) => {
|
|
223
|
+
// sessionKey is forwarded by the client after loginWithQrStart (runtime param extension).
|
|
224
|
+
const sessionKey = (params as { sessionKey?: string }).sessionKey || params.accountId || "";
|
|
225
|
+
const result: WorkChatQrWaitResult = await waitForWorkChatLogin({
|
|
226
|
+
sessionKey,
|
|
227
|
+
timeoutMs: params.timeoutMs,
|
|
228
|
+
});
|
|
229
|
+
return {
|
|
230
|
+
connected: result.connected,
|
|
231
|
+
message: result.message,
|
|
232
|
+
accountId: result.accountId,
|
|
233
|
+
agentId: result.agentId,
|
|
234
|
+
} as { connected: boolean; message: string };
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
auth: {
|
|
238
|
+
login: async ({ cfg, accountId, verbose, runtime }) => {
|
|
239
|
+
const account = resolveWorkChatAccount(cfg, accountId);
|
|
240
|
+
|
|
241
|
+
const log = (msg: string) => {
|
|
242
|
+
runtime?.log?.(msg);
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
log(`正在启动...`);
|
|
246
|
+
const startResult: WorkChatQrStartResult = await startWorkChatLoginWithQr({
|
|
247
|
+
baseUrl: account.baseUrl,
|
|
248
|
+
appKey: account.appKey,
|
|
249
|
+
appSecret: account.appSecret,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
if (!startResult.qrcodeUrl) {
|
|
253
|
+
logger.warn(
|
|
254
|
+
`auth.login: failed to get QR code accountId=${account.accountId} message=${startResult.message}`,
|
|
255
|
+
);
|
|
256
|
+
log(startResult.message);
|
|
257
|
+
throw new Error(startResult.message);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
log(`\n请在 WorkChat 平台扫描以下二维码,以继续绑定:\n`);
|
|
261
|
+
const { displayQRCode } = await import("./auth/login-qr-terminal.js");
|
|
262
|
+
await displayQRCode(startResult.qrcodeUrl!);
|
|
263
|
+
|
|
264
|
+
const loginTimeoutMs = 480_000;
|
|
265
|
+
log(`\n正在等待操作...\n`);
|
|
266
|
+
|
|
267
|
+
const waitResult: WorkChatQrWaitResult = await waitForWorkChatLogin({
|
|
268
|
+
sessionKey: startResult.sessionKey,
|
|
269
|
+
timeoutMs: loginTimeoutMs,
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
if (waitResult.connected) {
|
|
273
|
+
log(`\n已将此 OpenClaw 绑定到 WorkChat。`);
|
|
274
|
+
} else {
|
|
275
|
+
logger.warn(
|
|
276
|
+
`auth.login: binding did not complete accountId=${account.accountId} message=${waitResult.message}`,
|
|
277
|
+
);
|
|
278
|
+
throw new Error(waitResult.message);
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
};
|