@xgjktech/xg_cwork_im 1.0.5 → 1.0.7
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 +25 -19
- package/index.ts +1 -1
- package/package.json +3 -3
- package/src/channel.ts +48 -19
- package/src/group-history-tool.ts +13 -10
- package/src/send-group-message-tool.ts +2 -1
- package/src/types.ts +3 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# XG CWork IM Channel for OpenClaw
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
工作说说 IM 机器人 Channel 插件,通过 WebSocket 长连接接收 `@` 消息,由 OpenClaw AI 处理后自动回复。
|
|
4
4
|
|
|
5
5
|
## 工作原理
|
|
6
6
|
|
|
@@ -126,7 +126,7 @@ openclaw plugins enable xg_cwork_im
|
|
|
126
126
|
|
|
127
127
|
### 步骤 2:配置 Channel 账户信息
|
|
128
128
|
|
|
129
|
-
编辑 `~/.openclaw/openclaw.json
|
|
129
|
+
编辑 `~/.openclaw/openclaw.json`,添加以下配置(推荐 `accounts` 使用对象 map,key 即 `accountId`):
|
|
130
130
|
|
|
131
131
|
```json
|
|
132
132
|
{
|
|
@@ -134,15 +134,17 @@ openclaw plugins enable xg_cwork_im
|
|
|
134
134
|
"xg_cwork_im": {
|
|
135
135
|
"baseUrl": "https://cwork-web-test.xgjktech.com.cn",
|
|
136
136
|
"wsBaseUrl": "wss://cwork-web-test.xgjktech.com.cn",
|
|
137
|
-
"accounts":
|
|
138
|
-
{ "appKey": "你的机器人 appKey", "agentId": "main", "name": "个人助手" }
|
|
139
|
-
|
|
137
|
+
"accounts": {
|
|
138
|
+
"main": { "appKey": "你的机器人 appKey", "agentId": "main", "name": "个人助手" }
|
|
139
|
+
}
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
143
|
```
|
|
144
144
|
|
|
145
|
-
> **多账户**:如有多个机器人,在 `accounts`
|
|
145
|
+
> **多账户**:如有多个机器人,在 `accounts` 中继续追加即可(对象 map:key 为 `accountId`)。每个账户可指定不同的 `agentId` 对应不同的 OpenClaw Agent。
|
|
146
|
+
|
|
147
|
+
> **accounts.default(可选)**:OpenClaw 的 `doctor --fix` 可能会自动生成 `accounts.default`,用于存放默认项(例如 `groupPolicy` 等)。本插件会把 `accounts.default` 作为“默认覆盖”合并到每个真实账号上,但 **不会**把它当作一个需要启动连接的账号。
|
|
146
148
|
|
|
147
149
|
### 步骤 3:重启 Gateway
|
|
148
150
|
|
|
@@ -170,15 +172,19 @@ openclaw gateway restart
|
|
|
170
172
|
"wsBaseUrl": "wss://cwork-web-test.xgjktech.com.cn",
|
|
171
173
|
"groupPolicy": "mention",
|
|
172
174
|
"debug": false,
|
|
173
|
-
// 可选:首条 AI 回复超时时间(毫秒),默认
|
|
174
|
-
// 未配置时:
|
|
175
|
-
"firstReplyTimeoutMs":
|
|
176
|
-
"accounts":
|
|
177
|
-
{ "appKey": "appKey_机器人A", "agentId": "main", "name": "个人助手" },
|
|
178
|
-
{ "appKey": "appKey_机器人B", "agentId": "sales", "name": "销售助手" }
|
|
179
|
-
|
|
175
|
+
// 可选:首条 AI 回复超时时间(毫秒),默认 30 分钟
|
|
176
|
+
// 未配置时:30 * 60_000 = 1800000
|
|
177
|
+
"firstReplyTimeoutMs": 1800000,
|
|
178
|
+
"accounts": {
|
|
179
|
+
"main": { "appKey": "appKey_机器人A", "agentId": "main", "name": "个人助手" },
|
|
180
|
+
"sales": { "appKey": "appKey_机器人B", "agentId": "sales", "name": "销售助手" }
|
|
181
|
+
}
|
|
180
182
|
}
|
|
181
|
-
}
|
|
183
|
+
},
|
|
184
|
+
"bindings": [
|
|
185
|
+
{ "type": "route", "agentId": "main", "match": { "channel": "xg_cwork_im", "accountId": "main" } },
|
|
186
|
+
{ "type": "route", "agentId": "sales", "match": { "channel": "xg_cwork_im", "accountId": "sales" } }
|
|
187
|
+
]
|
|
182
188
|
}
|
|
183
189
|
```
|
|
184
190
|
|
|
@@ -192,13 +198,13 @@ openclaw gateway restart
|
|
|
192
198
|
"wsBaseUrl": "wss://sg-al-cwork-web.mediportal.com.cn",
|
|
193
199
|
"groupPolicy": "mention",
|
|
194
200
|
"debug": false,
|
|
195
|
-
"maxConnectionAttempts":
|
|
201
|
+
"maxConnectionAttempts": 200,
|
|
196
202
|
"maxReconnectDelay": 120000,
|
|
197
203
|
// 生产环境可根据需要调整首回复超时时间(毫秒)
|
|
198
204
|
"firstReplyTimeoutMs": 300000,
|
|
199
|
-
"accounts":
|
|
200
|
-
{ "appKey": "你的生产 appKey", "agentId": "main", "name": "个人助手" }
|
|
201
|
-
|
|
205
|
+
"accounts": {
|
|
206
|
+
"main": { "appKey": "你的生产 appKey", "agentId": "main", "name": "个人助手" }
|
|
207
|
+
}
|
|
202
208
|
}
|
|
203
209
|
}
|
|
204
210
|
}
|
|
@@ -223,7 +229,7 @@ openclaw gateway restart
|
|
|
223
229
|
| `reconnectJitter` | number | `0.3` | 重连抖动因子(0-1) |
|
|
224
230
|
| `firstReplyTimeoutMs` | number | `300000` | **首条 AI 回复超时时间(毫秒)**。用于保护「思考中」占位消息:若在该时间窗口内 AI 没有任何回复,则自动将占位消息更新为「当前请求处理超时,请稍后重试」。|
|
|
225
231
|
|
|
226
|
-
### 账户配置(`accounts[n]`)
|
|
232
|
+
### 账户配置(`accounts.<accountId>` / `accounts[n]`)
|
|
227
233
|
|
|
228
234
|
| 选项 | 类型 | 默认值 | 说明 |
|
|
229
235
|
|---|---|---|---|
|
package/index.ts
CHANGED
|
@@ -14,7 +14,7 @@ import type { XgCworkImPluginModule } from "./src/types.js";
|
|
|
14
14
|
const plugin: XgCworkImPluginModule = {
|
|
15
15
|
id: "xg_cwork_im",
|
|
16
16
|
name: "XG CWork IM Channel",
|
|
17
|
-
description: "
|
|
17
|
+
description: "工作说说 IM 机器人 Channel 插件,通过 WebSocket 长连接接入",
|
|
18
18
|
configSchema: emptyPluginConfigSchema(),
|
|
19
19
|
register(api: OpenClawPluginApi): void {
|
|
20
20
|
setXgImRuntime(api.runtime);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xgjktech/xg_cwork_im",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"description": "XG CWork IM channel plugin for OpenClaw",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bot",
|
|
@@ -48,8 +48,8 @@
|
|
|
48
48
|
"channel": {
|
|
49
49
|
"id": "xg_cwork_im",
|
|
50
50
|
"label": "XG CWork IM",
|
|
51
|
-
"selectionLabel": "XG CWork IM (
|
|
52
|
-
"blurb": "
|
|
51
|
+
"selectionLabel": "XG CWork IM (工作说说)",
|
|
52
|
+
"blurb": "工作说说 IM 机器人 Channel 插件,通过 WebSocket 长连接接收消息。",
|
|
53
53
|
"aliases": [
|
|
54
54
|
"xg_cwork_im",
|
|
55
55
|
"im"
|
package/src/channel.ts
CHANGED
|
@@ -41,7 +41,9 @@ function getXgImRuntime(): PluginRuntime {
|
|
|
41
41
|
// ─── 配置 Schema ──────────────────────────────────────────────────────────────
|
|
42
42
|
|
|
43
43
|
const XgImAccountConfigSchema = z.object({
|
|
44
|
-
|
|
44
|
+
// 注意:为了兼容 OpenClaw doctor 生成的 accounts.default(仅包含默认项,不含 appKey),这里先放宽为 optional,
|
|
45
|
+
// 然后在 XgImConfigSchema.superRefine 中强制校验:除 default 之外的账号必须提供 appKey。
|
|
46
|
+
appKey: z.string().min(1, "appKey is required").optional(),
|
|
45
47
|
agentId: z.string().optional().default("main"),
|
|
46
48
|
name: z.string().optional(),
|
|
47
49
|
groupPolicy: z.enum(["open", "mention"]).optional().default("mention"),
|
|
@@ -61,7 +63,22 @@ const XgImConfigSchema = z.object({
|
|
|
61
63
|
initialReconnectDelay: z.number().int().positive().optional().default(1_000),
|
|
62
64
|
maxReconnectDelay: z.number().int().positive().optional().default(60_000),
|
|
63
65
|
reconnectJitter: z.number().min(0).max(1).optional().default(0.3),
|
|
64
|
-
|
|
66
|
+
// 多账户:对象 map(key 为 accountId)
|
|
67
|
+
accounts: z.record(z.string(), XgImAccountConfigSchema).optional(),
|
|
68
|
+
}).superRefine((val, ctx) => {
|
|
69
|
+
const accounts = val.accounts;
|
|
70
|
+
if (!accounts) return;
|
|
71
|
+
for (const [key, acc] of Object.entries(accounts)) {
|
|
72
|
+
if (key === "default") continue;
|
|
73
|
+
if (!acc || typeof acc !== "object") continue;
|
|
74
|
+
if (!("appKey" in acc) || !acc.appKey) {
|
|
75
|
+
ctx.addIssue({
|
|
76
|
+
code: z.ZodIssueCode.custom,
|
|
77
|
+
path: ["accounts", key, "appKey"],
|
|
78
|
+
message: "appKey is required for account entries (except accounts.default)",
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
65
82
|
});
|
|
66
83
|
|
|
67
84
|
// ─── 辅助函数 ─────────────────────────────────────────────────────────────────
|
|
@@ -71,19 +88,25 @@ function getXgImConfig(cfg: OpenClawConfig, accountId?: string | null): XgImConf
|
|
|
71
88
|
const raw = (cfg as Record<string, Record<string, unknown>>)?.channels?.xg_cwork_im as XgImConfig | undefined;
|
|
72
89
|
if (!raw) throw new Error("[cwork_im] channels.xg_cwork_im config not found");
|
|
73
90
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
91
|
+
const accounts = raw.accounts as unknown;
|
|
92
|
+
const accountMap =
|
|
93
|
+
accounts && typeof accounts === "object" && !Array.isArray(accounts)
|
|
94
|
+
? (accounts as Record<string, Partial<XgImConfig> | undefined>)
|
|
95
|
+
: undefined;
|
|
96
|
+
const defaults = accountMap?.default ? { ...accountMap.default } : undefined;
|
|
97
|
+
|
|
98
|
+
// 指定了具体账户 ID 时,优先合并对应账户配置
|
|
99
|
+
if (accountId && accountId !== "default" && accountMap) {
|
|
100
|
+
const sub = accountMap[accountId];
|
|
101
|
+
if (sub) return { ...raw, ...(defaults ?? {}), ...sub };
|
|
81
102
|
}
|
|
82
103
|
|
|
83
|
-
// 没有指定 accountId(如 Cron outbound 场景),自动 fallback
|
|
104
|
+
// 没有指定 accountId(如 Cron outbound 场景),自动 fallback 到「第一个账户」
|
|
84
105
|
// 避免顶层 raw 没有 appKey 时 getToken 失败
|
|
85
|
-
if (
|
|
86
|
-
|
|
106
|
+
if (accountMap && !raw.appKey) {
|
|
107
|
+
const firstKey = Object.keys(accountMap).find((k) => k !== "default");
|
|
108
|
+
const first = firstKey ? accountMap[firstKey] : undefined;
|
|
109
|
+
if (first) return { ...raw, ...(defaults ?? {}), ...first };
|
|
87
110
|
}
|
|
88
111
|
|
|
89
112
|
return raw;
|
|
@@ -124,9 +147,9 @@ export const xgCworkImChannelPlugin: XgImChannelPlugin = {
|
|
|
124
147
|
meta: {
|
|
125
148
|
id: "xg_cwork_im",
|
|
126
149
|
label: "XG CWork IM",
|
|
127
|
-
selectionLabel: "XG CWork IM (
|
|
150
|
+
selectionLabel: "XG CWork IM (工作说说)",
|
|
128
151
|
docsPath: "/channels/xg_cwork_im",
|
|
129
|
-
blurb: "
|
|
152
|
+
blurb: "工作说说 IM 机器人,通过 WebSocket 长连接接收 @ 消息。",
|
|
130
153
|
aliases: ["xg_cwork_im", "im"],
|
|
131
154
|
},
|
|
132
155
|
// 双重类型转换绕过 zod v3/v4 的 TS 类型不兼容
|
|
@@ -147,9 +170,13 @@ export const xgCworkImChannelPlugin: XgImChannelPlugin = {
|
|
|
147
170
|
listAccountIds: (cfg: OpenClawConfig): string[] => {
|
|
148
171
|
try {
|
|
149
172
|
const config = getXgImConfig(cfg);
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
173
|
+
const accounts = config.accounts as unknown;
|
|
174
|
+
if (accounts) {
|
|
175
|
+
if (!Array.isArray(accounts) && typeof accounts === "object") {
|
|
176
|
+
// 推荐写法:对象 key 作为 accountId
|
|
177
|
+
const keys = Object.keys(accounts as Record<string, unknown>).filter((k) => k !== "default");
|
|
178
|
+
return keys;
|
|
179
|
+
}
|
|
153
180
|
}
|
|
154
181
|
return isConfigured(cfg) ? ["default"] : [];
|
|
155
182
|
} catch {
|
|
@@ -179,6 +206,8 @@ export const xgCworkImChannelPlugin: XgImChannelPlugin = {
|
|
|
179
206
|
}
|
|
180
207
|
},
|
|
181
208
|
|
|
209
|
+
// 注意:OpenClaw 可能在缺省路由时使用 defaultAccountId。
|
|
210
|
+
// 我们使用 "default" 作为别名:getXgImConfig 会自动 fallback 到第一个非 default 的真实账号。
|
|
182
211
|
defaultAccountId: (): string => "default",
|
|
183
212
|
|
|
184
213
|
isConfigured: (account: ResolvedAccount): boolean => account.configured,
|
|
@@ -432,10 +461,10 @@ export const xgCworkImChannelPlugin: XgImChannelPlugin = {
|
|
|
432
461
|
});
|
|
433
462
|
log.info(`${logPrefix} [stream] START acknowledged: msgId=${msgId}`);
|
|
434
463
|
|
|
435
|
-
// 1.1 为“思考中”占位增加首回复超时保护(默认
|
|
464
|
+
// 1.1 为“思考中”占位增加首回复超时保护(默认 30分钟,可通过 firstReplyTimeoutMs 配置)
|
|
436
465
|
let hasFirstReply = false;
|
|
437
466
|
let firstReplyTimedOut = false;
|
|
438
|
-
const firstReplyTimeoutMs = config.firstReplyTimeoutMs ??
|
|
467
|
+
const firstReplyTimeoutMs = config.firstReplyTimeoutMs ?? 30 * 60_000;
|
|
439
468
|
const timeoutLabel = `${logPrefix} [stream] First reply timeout after ${firstReplyTimeoutMs}ms, updating thinking message as error`;
|
|
440
469
|
const firstReplyTimeout = setTimeout(async () => {
|
|
441
470
|
if (hasFirstReply) {
|
|
@@ -25,7 +25,8 @@ const GroupHistoryParams = Type.Object({
|
|
|
25
25
|
),
|
|
26
26
|
accountId: Type.Optional(
|
|
27
27
|
Type.String({
|
|
28
|
-
description:
|
|
28
|
+
description:
|
|
29
|
+
"(可选)要使用的 xg_cwork_im 账户 ID(即 accounts 的 key,例如 main/orchestrator)。",
|
|
29
30
|
}),
|
|
30
31
|
),
|
|
31
32
|
limit: Type.Optional(
|
|
@@ -54,19 +55,21 @@ export function extractXgCworkImConfig(cfg: unknown): XgImConfig | null {
|
|
|
54
55
|
* - 否则:有 accounts 时默认使用 accounts[0];无 accounts 时使用顶层配置。
|
|
55
56
|
*/
|
|
56
57
|
export function resolveAccountConfig(base: XgImConfig, accountId?: string | null): XgImConfig {
|
|
57
|
-
const accounts = base.accounts
|
|
58
|
-
if (accounts
|
|
58
|
+
const accounts = base.accounts;
|
|
59
|
+
if (!accounts) {
|
|
59
60
|
return base;
|
|
60
61
|
}
|
|
61
|
-
|
|
62
|
+
|
|
63
|
+
const map = accounts as Record<string, XgImConfig | undefined>;
|
|
64
|
+
const defaults = map.default ? { ...map.default } : undefined;
|
|
65
|
+
|
|
62
66
|
if (accountId && accountId !== "default") {
|
|
63
|
-
const
|
|
64
|
-
if (
|
|
65
|
-
idx = parsed;
|
|
66
|
-
}
|
|
67
|
+
const hit = map[accountId];
|
|
68
|
+
if (hit) return { ...base, ...(defaults ?? {}), ...hit };
|
|
67
69
|
}
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
+
const firstKey = Object.keys(map).find((k) => k !== "default");
|
|
71
|
+
const first = firstKey ? map[firstKey] : undefined;
|
|
72
|
+
return first ? { ...base, ...(defaults ?? {}), ...first } : base;
|
|
70
73
|
}
|
|
71
74
|
|
|
72
75
|
/** 调用 IM 接口拉取群聊最近 N 条消息 */
|
|
@@ -24,7 +24,8 @@ const SendGroupMessageParams = Type.Object({
|
|
|
24
24
|
}),
|
|
25
25
|
accountId: Type.Optional(
|
|
26
26
|
Type.String({
|
|
27
|
-
description:
|
|
27
|
+
description:
|
|
28
|
+
"(可选)要使用的 xg_cwork_im 账户 ID(即 accounts 的 key,例如 main/orchestrator)。",
|
|
28
29
|
}),
|
|
29
30
|
),
|
|
30
31
|
text: Type.String({
|
package/src/types.ts
CHANGED
|
@@ -30,7 +30,7 @@ export type XgCworkImPluginModule = XgImPluginModule;
|
|
|
30
30
|
/** 单个机器人账户配置 */
|
|
31
31
|
export interface XgImAccountConfig {
|
|
32
32
|
/** 机器人 appKey,从 IM 后台注册获取 */
|
|
33
|
-
appKey
|
|
33
|
+
appKey?: string;
|
|
34
34
|
/** 对应 OpenClaw 的 Agent ID,默认为 'main' */
|
|
35
35
|
agentId?: string;
|
|
36
36
|
/** 账户显示名称 */
|
|
@@ -41,7 +41,7 @@ export interface XgImAccountConfig {
|
|
|
41
41
|
|
|
42
42
|
export interface XgImConfig extends OpenClawConfig {
|
|
43
43
|
/** 多账户列表 */
|
|
44
|
-
accounts?: XgImAccountConfig
|
|
44
|
+
accounts?: Record<string, XgImAccountConfig>;
|
|
45
45
|
|
|
46
46
|
/** 机器人 appKey(单账户模式) */
|
|
47
47
|
appKey?: string;
|
|
@@ -72,7 +72,7 @@ export interface XgImConfig extends OpenClawConfig {
|
|
|
72
72
|
/**
|
|
73
73
|
* 首次 AI 回复超时时间(毫秒)。
|
|
74
74
|
*
|
|
75
|
-
* - 未配置时默认
|
|
75
|
+
* - 未配置时默认 30 分钟(1800_000ms)。
|
|
76
76
|
* - 仅用于保护「思考中」占位消息,避免长时间不被更新。
|
|
77
77
|
*/
|
|
78
78
|
firstReplyTimeoutMs?: number;
|