openclaw-xiaoyou 1.1.0 → 1.2.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/docs/install-xiaoyou.sh +100 -24
- package/index.ts +1 -5
- package/package.json +1 -1
- package/setup-entry.ts +1 -1
- package/src/channel.ts +44 -48
package/docs/install-xiaoyou.sh
CHANGED
|
@@ -323,24 +323,57 @@ install_or_upgrade() {
|
|
|
323
323
|
patch_config() {
|
|
324
324
|
[[ -n "$WS_URL" || -n "$TOKEN" ]] || return 0
|
|
325
325
|
|
|
326
|
-
log "写入 xiaoyou channel
|
|
327
|
-
run openclaw config set "channels.${CHANNEL_KEY}.enabled" true --strict-json
|
|
326
|
+
log "写入 xiaoyou channel 配置(直接修改配置文件)"
|
|
328
327
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
fi
|
|
332
|
-
if [[ -n "$TOKEN" ]]; then
|
|
333
|
-
run openclaw config set "channels.${CHANNEL_KEY}.authToken" "$TOKEN"
|
|
334
|
-
fi
|
|
328
|
+
local config_file
|
|
329
|
+
config_file="$(openclaw_config_file)"
|
|
335
330
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
run openclaw config set "channels.${CHANNEL_KEY}.heartbeatTimeoutMs" 10000 --strict-json
|
|
331
|
+
if [[ ! -f "$config_file" ]]; then
|
|
332
|
+
die "配置文件不存在: $config_file"
|
|
333
|
+
fi
|
|
340
334
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
335
|
+
# 使用 python3 直接写入配置,绕过 openclaw config set 的 channel id 校验
|
|
336
|
+
python3 -c "
|
|
337
|
+
import json, sys
|
|
338
|
+
|
|
339
|
+
config_file = '$config_file'
|
|
340
|
+
ws_url = '$WS_URL'
|
|
341
|
+
token = '$TOKEN'
|
|
342
|
+
|
|
343
|
+
try:
|
|
344
|
+
with open(config_file, 'r') as f:
|
|
345
|
+
cfg = json.load(f)
|
|
346
|
+
except Exception as e:
|
|
347
|
+
print(f'[xiaoyou-install][error] 无法读取配置文件: {e}', file=sys.stderr)
|
|
348
|
+
sys.exit(1)
|
|
349
|
+
|
|
350
|
+
# 确保 channels 对象存在
|
|
351
|
+
cfg.setdefault('channels', {})
|
|
352
|
+
|
|
353
|
+
# 写入 xiaoyou channel 配置
|
|
354
|
+
xiaoyou = cfg['channels'].get('xiaoyou', {})
|
|
355
|
+
xiaoyou['enabled'] = True
|
|
356
|
+
if ws_url:
|
|
357
|
+
xiaoyou['wsUrl'] = ws_url
|
|
358
|
+
if token:
|
|
359
|
+
xiaoyou['authToken'] = token
|
|
360
|
+
xiaoyou.setdefault('dmSecurity', 'open')
|
|
361
|
+
xiaoyou.setdefault('allowFrom', ['*'])
|
|
362
|
+
xiaoyou.setdefault('reconnectIntervalMs', 3000)
|
|
363
|
+
xiaoyou.setdefault('heartbeatIntervalMs', 30000)
|
|
364
|
+
xiaoyou.setdefault('heartbeatTimeoutMs', 10000)
|
|
365
|
+
cfg['channels']['xiaoyou'] = xiaoyou
|
|
366
|
+
|
|
367
|
+
try:
|
|
368
|
+
with open(config_file, 'w') as f:
|
|
369
|
+
json.dump(cfg, f, indent=2, ensure_ascii=False)
|
|
370
|
+
print('[xiaoyou-install] 配置写入成功')
|
|
371
|
+
except Exception as e:
|
|
372
|
+
print(f'[xiaoyou-install][error] 写入配置失败: {e}', file=sys.stderr)
|
|
373
|
+
sys.exit(1)
|
|
374
|
+
" || die "写入配置失败"
|
|
375
|
+
|
|
376
|
+
log "已写入配置: $config_file"
|
|
344
377
|
}
|
|
345
378
|
|
|
346
379
|
# -----------------------------------------------------------------------------
|
|
@@ -375,11 +408,31 @@ xiayou_status() {
|
|
|
375
408
|
|
|
376
409
|
xiayou_uninstall_plugin_only() {
|
|
377
410
|
# 仅删除插件记录和文件,不删除配置(升级时用)
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
411
|
+
local config_file
|
|
412
|
+
config_file="$(openclaw_config_file)"
|
|
413
|
+
|
|
414
|
+
if [[ -f "$config_file" ]] && command_exists python3; then
|
|
415
|
+
python3 -c "
|
|
416
|
+
import json
|
|
417
|
+
config_file = '$config_file'
|
|
418
|
+
with open(config_file, 'r') as f:
|
|
419
|
+
cfg = json.load(f)
|
|
420
|
+
changed = False
|
|
421
|
+
for key in ['xiaoyou', 'openclaw-xiaoyou']:
|
|
422
|
+
if cfg.get('plugins', {}).get('entries', {}).pop(key, None): changed = True
|
|
423
|
+
if cfg.get('plugins', {}).get('installs', {}).pop(key, None): changed = True
|
|
424
|
+
allow = cfg.get('plugins', {}).get('allow', [])
|
|
425
|
+
if key in allow:
|
|
426
|
+
allow.remove(key)
|
|
427
|
+
changed = True
|
|
428
|
+
if changed:
|
|
429
|
+
with open(config_file, 'w') as f:
|
|
430
|
+
json.dump(cfg, f, indent=2, ensure_ascii=False)
|
|
431
|
+
print('[xiaoyou-install] 已清理插件注册记录')
|
|
432
|
+
" || true
|
|
381
433
|
fi
|
|
382
|
-
|
|
434
|
+
|
|
435
|
+
run_or_dry_run rm -rf "$HOME/.openclaw/extensions/xiaoyou" "$HOME/.openclaw/extensions/openclaw-xiaoyou" "$HOME"/.openclaw/extensions/.openclaw-* || true
|
|
383
436
|
}
|
|
384
437
|
|
|
385
438
|
xiayou_uninstall() {
|
|
@@ -392,13 +445,29 @@ xiayou_uninstall() {
|
|
|
392
445
|
fi
|
|
393
446
|
|
|
394
447
|
log "开始卸载 xiaoyou channel"
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
448
|
+
|
|
449
|
+
local config_file
|
|
450
|
+
config_file="$(openclaw_config_file)"
|
|
451
|
+
|
|
452
|
+
if [[ -f "$config_file" ]] && command_exists python3; then
|
|
453
|
+
run_or_dry_run python3 -c "
|
|
454
|
+
import json
|
|
455
|
+
config_file = '$config_file'
|
|
456
|
+
with open(config_file, 'r') as f:
|
|
457
|
+
cfg = json.load(f)
|
|
458
|
+
cfg.get('channels', {}).pop('xiaoyou', None)
|
|
459
|
+
for key in ['xiaoyou', 'openclaw-xiaoyou']:
|
|
460
|
+
cfg.get('plugins', {}).get('entries', {}).pop(key, None)
|
|
461
|
+
cfg.get('plugins', {}).get('installs', {}).pop(key, None)
|
|
462
|
+
allow = cfg.get('plugins', {}).get('allow', [])
|
|
463
|
+
if key in allow: allow.remove(key)
|
|
464
|
+
with open(config_file, 'w') as f:
|
|
465
|
+
json.dump(cfg, f, indent=2, ensure_ascii=False)
|
|
466
|
+
print('[xiaoyou-install] 已清理所有 xiaoyou 配置')
|
|
467
|
+
" || warn "清理配置失败"
|
|
399
468
|
fi
|
|
400
469
|
|
|
401
|
-
run_or_dry_run rm -rf "$HOME/.openclaw/extensions
|
|
470
|
+
run_or_dry_run rm -rf "$HOME/.openclaw/extensions/xiaoyou" "$HOME/.openclaw/extensions/openclaw-xiaoyou" "$HOME"/.openclaw/extensions/.openclaw-* || warn "删除插件文件失败"
|
|
402
471
|
|
|
403
472
|
if [[ "$CHECK_ONLY" -eq 1 ]]; then
|
|
404
473
|
phase_end "xiaoyou.uninstall" "planned" "check-only:已输出计划,未执行"
|
|
@@ -424,6 +493,13 @@ main() {
|
|
|
424
493
|
|
|
425
494
|
install_or_upgrade
|
|
426
495
|
|
|
496
|
+
# 重启 gateway 让插件和配置生效
|
|
497
|
+
if [[ "$CHECK_ONLY" -eq 0 ]] && openclaw_gateway_running; then
|
|
498
|
+
log "重启 gateway 以加载插件和配置..."
|
|
499
|
+
run openclaw gateway restart || warn "gateway 重启失败,请手动执行: openclaw gateway restart"
|
|
500
|
+
sleep 5
|
|
501
|
+
fi
|
|
502
|
+
|
|
427
503
|
log "完成。企业服务端应能收到来自 xiaoyou 插件的 WebSocket 连接。"
|
|
428
504
|
phase_end "main.entry" "ok" "安装流程完成"
|
|
429
505
|
}
|
package/index.ts
CHANGED
|
@@ -3,10 +3,6 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 安装方式:
|
|
5
5
|
* openclaw plugins install openclaw-xiaoyou
|
|
6
|
-
* openclaw plugins enable openclaw-xiaoyou
|
|
7
|
-
*
|
|
8
|
-
* 配置方式:
|
|
9
|
-
* openclaw channels add --channel xiaoyou
|
|
10
6
|
*/
|
|
11
7
|
|
|
12
8
|
import { xiayouPlugin, setRuntime } from "./src/channel.js";
|
|
@@ -19,7 +15,7 @@ const plugin = {
|
|
|
19
15
|
|
|
20
16
|
register(api: any) {
|
|
21
17
|
// 注入 runtime,供 gateway/outbound 内部使用
|
|
22
|
-
setRuntime(api.runtime);
|
|
18
|
+
if (api.runtime) setRuntime(api.runtime);
|
|
23
19
|
|
|
24
20
|
// 注册 channel
|
|
25
21
|
api.registerChannel({ plugin: xiayouPlugin });
|
package/package.json
CHANGED
package/setup-entry.ts
CHANGED
|
@@ -13,6 +13,6 @@ export default {
|
|
|
13
13
|
description: "Bridge OpenClaw to enterprise services via WebSocket",
|
|
14
14
|
setup: {
|
|
15
15
|
resolveAccount: xiayouPlugin.config.resolveAccount,
|
|
16
|
-
|
|
16
|
+
isConfigured: xiayouPlugin.config.isConfigured,
|
|
17
17
|
},
|
|
18
18
|
};
|
package/src/channel.ts
CHANGED
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 运行在 OpenClaw Gateway 进程内,通过 gateway adapter 管理
|
|
5
5
|
* 与企业服务的 WebSocket 连接生命周期。
|
|
6
|
-
*
|
|
7
|
-
* 用户通过 OpenClaw 标准 channel 配置来设置企业服务地址等参数。
|
|
8
6
|
*/
|
|
9
7
|
|
|
10
8
|
import type { ResolvedAccount } from "./types.js";
|
|
@@ -14,7 +12,6 @@ import { createEnterpriseClient, type EnterpriseClient } from "./enterprise-clie
|
|
|
14
12
|
|
|
15
13
|
let _client: EnterpriseClient | null = null;
|
|
16
14
|
|
|
17
|
-
/** OpenClaw runtime API 引用,由 index.ts register() 注入 */
|
|
18
15
|
let _runtime: any = null;
|
|
19
16
|
export function setRuntime(rt: any) { _runtime = rt; }
|
|
20
17
|
export function getRuntime() { return _runtime; }
|
|
@@ -25,6 +22,12 @@ function getChannelConfig(cfg: any): any {
|
|
|
25
22
|
return cfg?.channels?.xiaoyou ?? {};
|
|
26
23
|
}
|
|
27
24
|
|
|
25
|
+
function listAccountIds(cfg: any): string[] {
|
|
26
|
+
const section = getChannelConfig(cfg);
|
|
27
|
+
if (section?.wsUrl) return ["default"];
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
|
|
28
31
|
function resolveAccount(cfg: any, accountId?: string | null): any {
|
|
29
32
|
const section = getChannelConfig(cfg);
|
|
30
33
|
const id = accountId || "default";
|
|
@@ -35,32 +38,10 @@ function resolveAccount(cfg: any, accountId?: string | null): any {
|
|
|
35
38
|
config: section,
|
|
36
39
|
enabled: section.enabled !== false,
|
|
37
40
|
configured,
|
|
38
|
-
|
|
39
|
-
authToken: section.authToken ?? "",
|
|
40
|
-
allowFrom: section.allowFrom ?? [],
|
|
41
|
-
dmPolicy: section.dmSecurity,
|
|
42
|
-
reconnectIntervalMs: section.reconnectIntervalMs ?? 3000,
|
|
43
|
-
maxReconnectAttempts: section.maxReconnectAttempts ?? 0,
|
|
44
|
-
heartbeatIntervalMs: section.heartbeatIntervalMs ?? 30000,
|
|
45
|
-
heartbeatTimeoutMs: section.heartbeatTimeoutMs ?? 10000,
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function inspectAccount(cfg: any) {
|
|
50
|
-
const section = getChannelConfig(cfg);
|
|
51
|
-
return {
|
|
52
|
-
enabled: Boolean(section?.wsUrl),
|
|
53
|
-
configured: Boolean(section?.wsUrl),
|
|
54
|
-
tokenStatus: section?.authToken ? "available" : "missing",
|
|
41
|
+
name: "Xiaoyou",
|
|
55
42
|
};
|
|
56
43
|
}
|
|
57
44
|
|
|
58
|
-
function listAccountIds(cfg: any): string[] {
|
|
59
|
-
const section = getChannelConfig(cfg);
|
|
60
|
-
if (section?.wsUrl) return ["default"];
|
|
61
|
-
return [];
|
|
62
|
-
}
|
|
63
|
-
|
|
64
45
|
// ─── Channel Plugin ──────────────────────────────────
|
|
65
46
|
|
|
66
47
|
export const xiayouPlugin = {
|
|
@@ -89,11 +70,11 @@ export const xiayouPlugin = {
|
|
|
89
70
|
blockStreaming: false,
|
|
90
71
|
},
|
|
91
72
|
|
|
92
|
-
|
|
73
|
+
reload: { configPrefixes: ["channels.xiaoyou"] },
|
|
74
|
+
|
|
93
75
|
config: {
|
|
94
|
-
resolveAccount,
|
|
95
|
-
inspectAccount,
|
|
96
76
|
listAccountIds,
|
|
77
|
+
resolveAccount,
|
|
97
78
|
isConfigured: (account: any): boolean => Boolean(account?.config?.wsUrl),
|
|
98
79
|
describeAccount: (account: any) => ({
|
|
99
80
|
accountId: account?.accountId || "default",
|
|
@@ -103,37 +84,41 @@ export const xiayouPlugin = {
|
|
|
103
84
|
defaultAccountId: (): string => "default",
|
|
104
85
|
},
|
|
105
86
|
|
|
106
|
-
// ── DM 安全策略 ────────────────────────────────────
|
|
107
87
|
security: {
|
|
108
88
|
dm: {
|
|
109
89
|
channelKey: "xiaoyou",
|
|
110
|
-
resolvePolicy: (account:
|
|
111
|
-
resolveAllowFrom: (account:
|
|
112
|
-
defaultPolicy: "
|
|
90
|
+
resolvePolicy: (account: any) => account?.config?.dmSecurity ?? "open",
|
|
91
|
+
resolveAllowFrom: (account: any) => account?.config?.allowFrom ?? ["*"],
|
|
92
|
+
defaultPolicy: "open",
|
|
113
93
|
},
|
|
114
94
|
},
|
|
115
95
|
|
|
116
|
-
// ── 回复线程模式 ───────────────────────────────────
|
|
117
96
|
threading: { topLevelReplyToMode: "reply" as const },
|
|
118
97
|
|
|
119
98
|
// ── Gateway 生命周期 ───────────────────────────────
|
|
120
|
-
reload: { configPrefixes: ["channels.xiaoyou"] },
|
|
121
|
-
|
|
122
99
|
gateway: {
|
|
123
100
|
startAccount: async (ctx: any) => {
|
|
124
101
|
const { account, cfg, log } = ctx;
|
|
102
|
+
|
|
103
|
+
// 注入 runtime
|
|
104
|
+
if (ctx.runtime) setRuntime(ctx.runtime);
|
|
105
|
+
|
|
125
106
|
const section = account?.config || getChannelConfig(cfg);
|
|
126
107
|
const logger = log || console;
|
|
127
108
|
|
|
109
|
+
if (!section?.wsUrl) {
|
|
110
|
+
throw new Error("xiaoyou: wsUrl is required");
|
|
111
|
+
}
|
|
112
|
+
|
|
128
113
|
const resolved: ResolvedAccount = {
|
|
129
|
-
wsUrl: section
|
|
130
|
-
authToken: section
|
|
131
|
-
allowFrom: section
|
|
132
|
-
dmPolicy: section
|
|
133
|
-
reconnectIntervalMs: section
|
|
134
|
-
maxReconnectAttempts: section
|
|
135
|
-
heartbeatIntervalMs: section
|
|
136
|
-
heartbeatTimeoutMs: section
|
|
114
|
+
wsUrl: section.wsUrl,
|
|
115
|
+
authToken: section.authToken ?? "",
|
|
116
|
+
allowFrom: section.allowFrom ?? [],
|
|
117
|
+
dmPolicy: section.dmSecurity ?? "open",
|
|
118
|
+
reconnectIntervalMs: section.reconnectIntervalMs ?? 3000,
|
|
119
|
+
maxReconnectAttempts: section.maxReconnectAttempts ?? 0,
|
|
120
|
+
heartbeatIntervalMs: section.heartbeatIntervalMs ?? 30000,
|
|
121
|
+
heartbeatTimeoutMs: section.heartbeatTimeoutMs ?? 10000,
|
|
137
122
|
};
|
|
138
123
|
|
|
139
124
|
// 断开已有连接
|
|
@@ -145,7 +130,7 @@ export const xiayouPlugin = {
|
|
|
145
130
|
onMessage: async (msg) => {
|
|
146
131
|
const runtime = getRuntime();
|
|
147
132
|
if (!runtime) {
|
|
148
|
-
logger.error("[xiaoyou] runtime not available");
|
|
133
|
+
logger.error("[xiaoyou] runtime not available, cannot dispatch");
|
|
149
134
|
return;
|
|
150
135
|
}
|
|
151
136
|
await runtime.inbound.dispatch({
|
|
@@ -168,7 +153,18 @@ export const xiayouPlugin = {
|
|
|
168
153
|
client.connect();
|
|
169
154
|
_client = client;
|
|
170
155
|
logger.info("[xiaoyou] gateway started");
|
|
171
|
-
|
|
156
|
+
|
|
157
|
+
// 保持 startAccount 挂起,直到 abortSignal 触发
|
|
158
|
+
return new Promise<void>((resolve) => {
|
|
159
|
+
if (ctx.abortSignal) {
|
|
160
|
+
ctx.abortSignal.addEventListener("abort", () => {
|
|
161
|
+
logger.info("[xiaoyou] abortSignal received, disconnecting");
|
|
162
|
+
client.disconnect();
|
|
163
|
+
if (_client === client) _client = null;
|
|
164
|
+
resolve();
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
});
|
|
172
168
|
},
|
|
173
169
|
|
|
174
170
|
stopAccount: async () => {
|
|
@@ -211,8 +207,8 @@ export const xiayouPlugin = {
|
|
|
211
207
|
status: {
|
|
212
208
|
describe: async ({ account }: any) => {
|
|
213
209
|
const issues: Array<{ severity: string; message: string }> = [];
|
|
214
|
-
const wsUrl = account?.
|
|
215
|
-
const authToken = account?.
|
|
210
|
+
const wsUrl = account?.config?.wsUrl;
|
|
211
|
+
const authToken = account?.config?.authToken;
|
|
216
212
|
|
|
217
213
|
if (!wsUrl) issues.push({ severity: "error", message: "wsUrl not configured" });
|
|
218
214
|
if (!authToken) issues.push({ severity: "warning", message: "authToken not set" });
|