@ynhcj/xiaoyi-channel 0.0.1-beta
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/index.d.ts +16 -0
- package/dist/index.js +21 -0
- package/dist/src/bot.d.ts +17 -0
- package/dist/src/bot.js +260 -0
- package/dist/src/channel.d.ts +6 -0
- package/dist/src/channel.js +87 -0
- package/dist/src/client.d.ts +35 -0
- package/dist/src/client.js +147 -0
- package/dist/src/config-schema.d.ts +54 -0
- package/dist/src/config-schema.js +55 -0
- package/dist/src/config.d.ts +17 -0
- package/dist/src/config.js +45 -0
- package/dist/src/file-download.d.ts +17 -0
- package/dist/src/file-download.js +53 -0
- package/dist/src/file-upload.d.ts +23 -0
- package/dist/src/file-upload.js +129 -0
- package/dist/src/formatter.d.ts +77 -0
- package/dist/src/formatter.js +252 -0
- package/dist/src/heartbeat.d.ts +39 -0
- package/dist/src/heartbeat.js +102 -0
- package/dist/src/monitor.d.ts +17 -0
- package/dist/src/monitor.js +191 -0
- package/dist/src/onboarding.d.ts +6 -0
- package/dist/src/onboarding.js +173 -0
- package/dist/src/outbound.d.ts +6 -0
- package/dist/src/outbound.js +208 -0
- package/dist/src/parser.d.ts +49 -0
- package/dist/src/parser.js +99 -0
- package/dist/src/push.d.ts +23 -0
- package/dist/src/push.js +146 -0
- package/dist/src/reply-dispatcher.d.ts +15 -0
- package/dist/src/reply-dispatcher.js +160 -0
- package/dist/src/runtime.d.ts +11 -0
- package/dist/src/runtime.js +18 -0
- package/dist/src/tools/calendar-tool.d.ts +6 -0
- package/dist/src/tools/calendar-tool.js +167 -0
- package/dist/src/tools/location-tool.d.ts +5 -0
- package/dist/src/tools/location-tool.js +136 -0
- package/dist/src/tools/note-tool.d.ts +5 -0
- package/dist/src/tools/note-tool.js +130 -0
- package/dist/src/tools/search-note-tool.d.ts +5 -0
- package/dist/src/tools/search-note-tool.js +130 -0
- package/dist/src/tools/session-manager.d.ts +29 -0
- package/dist/src/tools/session-manager.js +74 -0
- package/dist/src/tools/tool-context.d.ts +16 -0
- package/dist/src/tools/tool-context.js +7 -0
- package/dist/src/types.d.ts +163 -0
- package/dist/src/types.js +2 -0
- package/dist/src/utils/config-manager.d.ts +26 -0
- package/dist/src/utils/config-manager.js +56 -0
- package/dist/src/utils/crypto.d.ts +8 -0
- package/dist/src/utils/crypto.js +14 -0
- package/dist/src/utils/logger.d.ts +6 -0
- package/dist/src/utils/logger.js +34 -0
- package/dist/src/utils/session.d.ts +34 -0
- package/dist/src/utils/session.js +50 -0
- package/dist/src/websocket.d.ts +123 -0
- package/dist/src/websocket.js +547 -0
- package/openclaw.plugin.json +10 -0
- package/package.json +71 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import WebSocket from "ws";
|
|
2
|
+
export interface HeartbeatConfig {
|
|
3
|
+
interval: number;
|
|
4
|
+
timeout: number;
|
|
5
|
+
message: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Manages heartbeat for a WebSocket connection.
|
|
9
|
+
* Supports both application-level (30s) and protocol-level (20s) heartbeats.
|
|
10
|
+
*/
|
|
11
|
+
export declare class HeartbeatManager {
|
|
12
|
+
private ws;
|
|
13
|
+
private config;
|
|
14
|
+
private onTimeout;
|
|
15
|
+
private serverName;
|
|
16
|
+
private onHeartbeatSuccess?;
|
|
17
|
+
private intervalTimer;
|
|
18
|
+
private timeoutTimer;
|
|
19
|
+
private lastPongTime;
|
|
20
|
+
private log;
|
|
21
|
+
private error;
|
|
22
|
+
constructor(ws: WebSocket, config: HeartbeatConfig, onTimeout: () => void, serverName?: string, logFn?: (msg: string, ...args: any[]) => void, errorFn?: (msg: string, ...args: any[]) => void, onHeartbeatSuccess?: () => void);
|
|
23
|
+
/**
|
|
24
|
+
* Start heartbeat monitoring.
|
|
25
|
+
*/
|
|
26
|
+
start(): void;
|
|
27
|
+
/**
|
|
28
|
+
* Stop heartbeat monitoring.
|
|
29
|
+
*/
|
|
30
|
+
stop(): void;
|
|
31
|
+
/**
|
|
32
|
+
* Send a heartbeat ping.
|
|
33
|
+
*/
|
|
34
|
+
private sendHeartbeat;
|
|
35
|
+
/**
|
|
36
|
+
* Check if connection is healthy based on last pong time.
|
|
37
|
+
*/
|
|
38
|
+
isHealthy(): boolean;
|
|
39
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Heartbeat management for WebSocket connections
|
|
2
|
+
import WebSocket from "ws";
|
|
3
|
+
/**
|
|
4
|
+
* Manages heartbeat for a WebSocket connection.
|
|
5
|
+
* Supports both application-level (30s) and protocol-level (20s) heartbeats.
|
|
6
|
+
*/
|
|
7
|
+
export class HeartbeatManager {
|
|
8
|
+
ws;
|
|
9
|
+
config;
|
|
10
|
+
onTimeout;
|
|
11
|
+
serverName;
|
|
12
|
+
onHeartbeatSuccess;
|
|
13
|
+
intervalTimer = null;
|
|
14
|
+
timeoutTimer = null;
|
|
15
|
+
lastPongTime = 0;
|
|
16
|
+
// Logging functions following feishu pattern
|
|
17
|
+
log;
|
|
18
|
+
error;
|
|
19
|
+
constructor(ws, config, onTimeout, serverName = "unknown", logFn, errorFn, onHeartbeatSuccess // ✅ 新增:心跳成功回调
|
|
20
|
+
) {
|
|
21
|
+
this.ws = ws;
|
|
22
|
+
this.config = config;
|
|
23
|
+
this.onTimeout = onTimeout;
|
|
24
|
+
this.serverName = serverName;
|
|
25
|
+
this.onHeartbeatSuccess = onHeartbeatSuccess;
|
|
26
|
+
this.log = logFn ?? console.log;
|
|
27
|
+
this.error = errorFn ?? console.error;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Start heartbeat monitoring.
|
|
31
|
+
*/
|
|
32
|
+
start() {
|
|
33
|
+
this.stop(); // Clear any existing timers
|
|
34
|
+
this.lastPongTime = Date.now();
|
|
35
|
+
// Setup ping/pong for protocol-level heartbeat
|
|
36
|
+
this.ws.on("pong", () => {
|
|
37
|
+
this.lastPongTime = Date.now();
|
|
38
|
+
if (this.timeoutTimer) {
|
|
39
|
+
clearTimeout(this.timeoutTimer);
|
|
40
|
+
this.timeoutTimer = null;
|
|
41
|
+
}
|
|
42
|
+
// ✅ Report health: heartbeat successful
|
|
43
|
+
this.onHeartbeatSuccess?.();
|
|
44
|
+
});
|
|
45
|
+
// Start interval timer
|
|
46
|
+
this.intervalTimer = setInterval(() => {
|
|
47
|
+
this.sendHeartbeat();
|
|
48
|
+
}, this.config.interval);
|
|
49
|
+
this.log(`[DEBUG] Heartbeat started for ${this.serverName}: interval=${this.config.interval}ms`);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Stop heartbeat monitoring.
|
|
53
|
+
*/
|
|
54
|
+
stop() {
|
|
55
|
+
if (this.intervalTimer) {
|
|
56
|
+
clearInterval(this.intervalTimer);
|
|
57
|
+
this.intervalTimer = null;
|
|
58
|
+
}
|
|
59
|
+
if (this.timeoutTimer) {
|
|
60
|
+
clearTimeout(this.timeoutTimer);
|
|
61
|
+
this.timeoutTimer = null;
|
|
62
|
+
}
|
|
63
|
+
this.log(`[DEBUG] Heartbeat stopped for ${this.serverName}`);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Send a heartbeat ping.
|
|
67
|
+
*/
|
|
68
|
+
sendHeartbeat() {
|
|
69
|
+
if (this.ws.readyState !== WebSocket.OPEN) {
|
|
70
|
+
console.warn(`Cannot send heartbeat for ${this.serverName}: WebSocket not open`);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
// Send application-level heartbeat message
|
|
75
|
+
console.log(`[WS-${this.serverName}-SEND] Sending heartbeat frame:`, this.config.message);
|
|
76
|
+
this.ws.send(this.config.message);
|
|
77
|
+
console.log(`[WS-${this.serverName}-SEND] Heartbeat message sent, size: ${this.config.message.length} bytes`);
|
|
78
|
+
// Send protocol-level ping
|
|
79
|
+
this.ws.ping();
|
|
80
|
+
console.log(`[WS-${this.serverName}-SEND] Protocol-level ping sent`);
|
|
81
|
+
// Setup timeout timer
|
|
82
|
+
this.timeoutTimer = setTimeout(() => {
|
|
83
|
+
this.error(`Heartbeat timeout for ${this.serverName}`);
|
|
84
|
+
this.onTimeout();
|
|
85
|
+
}, this.config.timeout);
|
|
86
|
+
this.log(`[DEBUG] Heartbeat sent for ${this.serverName}`);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
this.error(`Failed to send heartbeat for ${this.serverName}:`, error);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Check if connection is healthy based on last pong time.
|
|
94
|
+
*/
|
|
95
|
+
isHealthy() {
|
|
96
|
+
if (this.lastPongTime === 0) {
|
|
97
|
+
return true; // Not started yet
|
|
98
|
+
}
|
|
99
|
+
const timeSinceLastPong = Date.now() - this.lastPongTime;
|
|
100
|
+
return timeSinceLastPong < this.config.interval + this.config.timeout;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { RuntimeEnv } from "openclaw/plugin-sdk";
|
|
2
|
+
export type MonitorXYOpts = {
|
|
3
|
+
config?: any;
|
|
4
|
+
runtime?: RuntimeEnv;
|
|
5
|
+
abortSignal?: AbortSignal;
|
|
6
|
+
accountId?: string;
|
|
7
|
+
setStatus?: (status: {
|
|
8
|
+
lastEventAt?: number;
|
|
9
|
+
lastInboundAt?: number;
|
|
10
|
+
connected?: boolean;
|
|
11
|
+
}) => void;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Monitor XY channel WebSocket connections.
|
|
15
|
+
* Keeps the connection alive until abortSignal is triggered.
|
|
16
|
+
*/
|
|
17
|
+
export declare function monitorXYProvider(opts?: MonitorXYOpts): Promise<void>;
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { resolveXYConfig } from "./config.js";
|
|
2
|
+
import { getXYWebSocketManager, diagnoseAllManagers, cleanupOrphanConnections, removeXYWebSocketManager } from "./client.js";
|
|
3
|
+
import { handleXYMessage } from "./bot.js";
|
|
4
|
+
/**
|
|
5
|
+
* Per-session serial queue that ensures messages from the same session are processed
|
|
6
|
+
* in arrival order while allowing different sessions to run concurrently.
|
|
7
|
+
* Following feishu/monitor.account.ts pattern.
|
|
8
|
+
*/
|
|
9
|
+
function createSessionQueue() {
|
|
10
|
+
const queues = new Map();
|
|
11
|
+
return (sessionId, task) => {
|
|
12
|
+
const prev = queues.get(sessionId) ?? Promise.resolve();
|
|
13
|
+
const next = prev.then(task, task);
|
|
14
|
+
queues.set(sessionId, next);
|
|
15
|
+
void next.finally(() => {
|
|
16
|
+
if (queues.get(sessionId) === next) {
|
|
17
|
+
queues.delete(sessionId);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
return next;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Monitor XY channel WebSocket connections.
|
|
25
|
+
* Keeps the connection alive until abortSignal is triggered.
|
|
26
|
+
*/
|
|
27
|
+
export async function monitorXYProvider(opts = {}) {
|
|
28
|
+
const cfg = opts.config;
|
|
29
|
+
if (!cfg) {
|
|
30
|
+
throw new Error("Config is required for XY monitor");
|
|
31
|
+
}
|
|
32
|
+
const runtime = opts.runtime;
|
|
33
|
+
const log = runtime?.log ?? console.log;
|
|
34
|
+
const error = runtime?.error ?? console.error;
|
|
35
|
+
const account = resolveXYConfig(cfg);
|
|
36
|
+
if (!account.enabled) {
|
|
37
|
+
throw new Error(`XY account is disabled`);
|
|
38
|
+
}
|
|
39
|
+
const accountId = opts.accountId ?? "default";
|
|
40
|
+
// Create trackEvent function to report health to OpenClaw framework
|
|
41
|
+
const trackEvent = opts.setStatus
|
|
42
|
+
? () => {
|
|
43
|
+
opts.setStatus({ lastEventAt: Date.now(), lastInboundAt: Date.now() });
|
|
44
|
+
}
|
|
45
|
+
: undefined;
|
|
46
|
+
// 🔍 Diagnose WebSocket managers before gateway start
|
|
47
|
+
console.log("🔍 [DIAGNOSTICS] Checking WebSocket managers before gateway start...");
|
|
48
|
+
diagnoseAllManagers();
|
|
49
|
+
// Get WebSocket manager (cached)
|
|
50
|
+
const wsManager = getXYWebSocketManager(account);
|
|
51
|
+
// ✅ Set health event callback for heartbeat reporting
|
|
52
|
+
if (trackEvent) {
|
|
53
|
+
wsManager.setHealthEventCallback(trackEvent);
|
|
54
|
+
}
|
|
55
|
+
// Track logged servers to avoid duplicate logs
|
|
56
|
+
const loggedServers = new Set();
|
|
57
|
+
// Track active message processing to detect duplicates
|
|
58
|
+
const activeMessages = new Set();
|
|
59
|
+
// Create session queue for ordered message processing
|
|
60
|
+
const enqueue = createSessionQueue();
|
|
61
|
+
// Health check interval
|
|
62
|
+
let healthCheckInterval = null;
|
|
63
|
+
return new Promise((resolve, reject) => {
|
|
64
|
+
// Event handlers (defined early so they can be referenced in cleanup)
|
|
65
|
+
const messageHandler = (message, sessionId, serverId) => {
|
|
66
|
+
const messageKey = `${sessionId}::${message.id}`;
|
|
67
|
+
log(`[MONITOR-HANDLER] ####### messageHandler triggered: serverId=${serverId}, sessionId=${sessionId}, messageId=${message.id} #######`);
|
|
68
|
+
// ✅ Report health: received a message
|
|
69
|
+
trackEvent?.();
|
|
70
|
+
// Check for duplicate message handling
|
|
71
|
+
if (activeMessages.has(messageKey)) {
|
|
72
|
+
error(`[MONITOR-HANDLER] ⚠️ WARNING: Duplicate message detected! messageKey=${messageKey}, this may cause duplicate dispatchers!`);
|
|
73
|
+
}
|
|
74
|
+
activeMessages.add(messageKey);
|
|
75
|
+
log(`[MONITOR-HANDLER] 📝 Active messages count: ${activeMessages.size}, messageKey: ${messageKey}`);
|
|
76
|
+
const task = async () => {
|
|
77
|
+
try {
|
|
78
|
+
log(`[MONITOR-HANDLER] 🚀 Starting handleXYMessage for messageKey=${messageKey}`);
|
|
79
|
+
await handleXYMessage({
|
|
80
|
+
cfg,
|
|
81
|
+
runtime,
|
|
82
|
+
message,
|
|
83
|
+
accountId, // ✅ Pass accountId ("default")
|
|
84
|
+
});
|
|
85
|
+
log(`[MONITOR-HANDLER] ✅ Completed handleXYMessage for messageKey=${messageKey}`);
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
// ✅ Only log error, don't re-throw to prevent gateway restart
|
|
89
|
+
error(`XY gateway: error handling message from ${serverId}: ${String(err)}`);
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
// Remove from active messages when done
|
|
93
|
+
activeMessages.delete(messageKey);
|
|
94
|
+
log(`[MONITOR-HANDLER] 🧹 Cleaned up messageKey=${messageKey}, remaining active: ${activeMessages.size}`);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
void enqueue(sessionId, task).catch((err) => {
|
|
98
|
+
// Error already logged in task, this is for queue failures
|
|
99
|
+
error(`XY gateway: queue processing failed for session ${sessionId}: ${String(err)}`);
|
|
100
|
+
activeMessages.delete(messageKey);
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
const connectedHandler = (serverId) => {
|
|
104
|
+
if (!loggedServers.has(serverId)) {
|
|
105
|
+
log(`XY gateway: ${serverId} connected`);
|
|
106
|
+
loggedServers.add(serverId);
|
|
107
|
+
}
|
|
108
|
+
// ✅ Report health: connection established
|
|
109
|
+
trackEvent?.();
|
|
110
|
+
opts.setStatus?.({ connected: true });
|
|
111
|
+
};
|
|
112
|
+
const disconnectedHandler = (serverId) => {
|
|
113
|
+
console.warn(`XY gateway: ${serverId} disconnected`);
|
|
114
|
+
loggedServers.delete(serverId);
|
|
115
|
+
// ✅ Report disconnection status (only if all servers disconnected)
|
|
116
|
+
if (loggedServers.size === 0) {
|
|
117
|
+
opts.setStatus?.({ connected: false });
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
const errorHandler = (err, serverId) => {
|
|
121
|
+
error(`XY gateway: ${serverId} error: ${String(err)}`);
|
|
122
|
+
};
|
|
123
|
+
const cleanup = () => {
|
|
124
|
+
log("XY gateway: cleaning up...");
|
|
125
|
+
// 🔍 Diagnose before cleanup
|
|
126
|
+
console.log("🔍 [DIAGNOSTICS] Checking WebSocket managers before cleanup...");
|
|
127
|
+
diagnoseAllManagers();
|
|
128
|
+
// Stop health check interval
|
|
129
|
+
if (healthCheckInterval) {
|
|
130
|
+
clearInterval(healthCheckInterval);
|
|
131
|
+
healthCheckInterval = null;
|
|
132
|
+
console.log("⏸️ Stopped periodic health check");
|
|
133
|
+
}
|
|
134
|
+
// Remove event handlers to prevent duplicate calls on gateway restart
|
|
135
|
+
wsManager.off("message", messageHandler);
|
|
136
|
+
wsManager.off("connected", connectedHandler);
|
|
137
|
+
wsManager.off("disconnected", disconnectedHandler);
|
|
138
|
+
wsManager.off("error", errorHandler);
|
|
139
|
+
// ✅ Disconnect the wsManager to prevent connection leaks
|
|
140
|
+
// This is safe because each gateway lifecycle should have clean connections
|
|
141
|
+
wsManager.disconnect();
|
|
142
|
+
// ✅ Remove manager from cache to prevent reusing dirty state
|
|
143
|
+
removeXYWebSocketManager(account);
|
|
144
|
+
loggedServers.clear();
|
|
145
|
+
activeMessages.clear();
|
|
146
|
+
log(`[MONITOR-HANDLER] 🧹 Cleanup complete, cleared active messages`);
|
|
147
|
+
// 🔍 Diagnose after cleanup
|
|
148
|
+
console.log("🔍 [DIAGNOSTICS] Checking WebSocket managers after cleanup...");
|
|
149
|
+
diagnoseAllManagers();
|
|
150
|
+
};
|
|
151
|
+
const handleAbort = () => {
|
|
152
|
+
log("XY gateway: abort signal received, stopping");
|
|
153
|
+
// cleanup();
|
|
154
|
+
// log("XY gateway stopped");
|
|
155
|
+
resolve();
|
|
156
|
+
};
|
|
157
|
+
if (opts.abortSignal?.aborted) {
|
|
158
|
+
// cleanup();
|
|
159
|
+
resolve();
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
opts.abortSignal?.addEventListener("abort", handleAbort, { once: true });
|
|
163
|
+
// Register event handlers (handlers are defined above in cleanup scope)
|
|
164
|
+
wsManager.on("message", messageHandler);
|
|
165
|
+
wsManager.on("connected", connectedHandler);
|
|
166
|
+
wsManager.on("disconnected", disconnectedHandler);
|
|
167
|
+
wsManager.on("error", errorHandler);
|
|
168
|
+
// Start periodic health check (every 5 minutes)
|
|
169
|
+
console.log("🏥 Starting periodic health check (every 5 minutes)...");
|
|
170
|
+
healthCheckInterval = setInterval(() => {
|
|
171
|
+
console.log("🏥 [HEALTH CHECK] Periodic WebSocket diagnostics...");
|
|
172
|
+
diagnoseAllManagers();
|
|
173
|
+
// Auto-cleanup orphan connections
|
|
174
|
+
const cleaned = cleanupOrphanConnections();
|
|
175
|
+
if (cleaned > 0) {
|
|
176
|
+
console.log(`🧹 [HEALTH CHECK] Auto-cleaned ${cleaned} manager(s) with orphan connections`);
|
|
177
|
+
}
|
|
178
|
+
}, 5 * 60 * 1000); // 5 minutes
|
|
179
|
+
// Connect to WebSocket servers
|
|
180
|
+
wsManager.connect()
|
|
181
|
+
.then(() => {
|
|
182
|
+
log("XY gateway: started successfully");
|
|
183
|
+
})
|
|
184
|
+
.catch((err) => {
|
|
185
|
+
// Connection failed but don't reject - continue monitoring for reconnection
|
|
186
|
+
error(`XY gateway: initial connection failed: ${String(err)}`);
|
|
187
|
+
// Still resolve successfully so plugin starts
|
|
188
|
+
resolve();
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ChannelOnboardingAdapter } from "openclaw/plugin-sdk";
|
|
2
|
+
/**
|
|
3
|
+
* XY Channel Onboarding Adapter
|
|
4
|
+
* Implements the ChannelOnboardingAdapter interface for OpenClaw's onboarding system
|
|
5
|
+
*/
|
|
6
|
+
export declare const xyOnboardingAdapter: ChannelOnboardingAdapter;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
const channel = "xiaoyi-channel";
|
|
2
|
+
/**
|
|
3
|
+
* Check if XY channel is properly configured with required fields
|
|
4
|
+
*/
|
|
5
|
+
function isXYConfigured(cfg) {
|
|
6
|
+
try {
|
|
7
|
+
const xyConfig = cfg.channels?.["xiaoyi-channel"];
|
|
8
|
+
if (!xyConfig) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
// Check required fields
|
|
12
|
+
const requiredFields = ["apiKey", "agentId", "uid", "apiId", "pushId"];
|
|
13
|
+
for (const field of requiredFields) {
|
|
14
|
+
if (!xyConfig[field] || (typeof xyConfig[field] === "string" && !xyConfig[field].trim())) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Get current status of XY channel configuration
|
|
26
|
+
*/
|
|
27
|
+
async function getStatus({ cfg }) {
|
|
28
|
+
const configured = isXYConfigured(cfg);
|
|
29
|
+
const xyConfig = cfg.channels?.["xiaoyi-channel"];
|
|
30
|
+
const statusLines = [];
|
|
31
|
+
if (configured) {
|
|
32
|
+
const wsUrl1 = xyConfig?.wsUrl1 || "ws://localhost:8765/ws/link";
|
|
33
|
+
const wsUrl2 = xyConfig?.wsUrl2 || "ws://localhost:8768/ws/link";
|
|
34
|
+
statusLines.push(`XY: configured (双 WebSocket: ${wsUrl1}, ${wsUrl2})`);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
statusLines.push("XY: 需要配置 (需要 apiKey, agentId, uid, apiId, pushId)");
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
channel,
|
|
41
|
+
configured,
|
|
42
|
+
statusLines,
|
|
43
|
+
selectionHint: configured ? "configured" : "需要配置",
|
|
44
|
+
quickstartScore: configured ? 5 : 0,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Configure XY channel through interactive prompts
|
|
49
|
+
*/
|
|
50
|
+
async function configure({ cfg, prompter, }) {
|
|
51
|
+
// Note current configuration status
|
|
52
|
+
const currentConfig = cfg.channels?.["xiaoyi-channel"];
|
|
53
|
+
const isUpdate = Boolean(currentConfig);
|
|
54
|
+
await prompter.note([
|
|
55
|
+
"XY Channel - 小艺 A2A 协议配置",
|
|
56
|
+
"",
|
|
57
|
+
"XY 是小艺智能助手的 A2A (Agent-to-Agent) 协议集成,",
|
|
58
|
+
"需要配置双 WebSocket 连接和相关认证信息。",
|
|
59
|
+
"",
|
|
60
|
+
isUpdate ? "当前配置将被更新。" : "首次配置 XY channel。",
|
|
61
|
+
].join("\n"), "XY Channel 配置");
|
|
62
|
+
// Prompt for WebSocket URLs
|
|
63
|
+
const wsUrl1 = await prompter.text({
|
|
64
|
+
message: "WebSocket URL 1 (主连接)",
|
|
65
|
+
initialValue: currentConfig?.wsUrl1 || "ws://localhost:8765/ws/link",
|
|
66
|
+
placeholder: "ws://localhost:8765/ws/link",
|
|
67
|
+
});
|
|
68
|
+
const wsUrl2 = await prompter.text({
|
|
69
|
+
message: "WebSocket URL 2 (辅助连接)",
|
|
70
|
+
initialValue: currentConfig?.wsUrl2 || "ws://localhost:8768/ws/link",
|
|
71
|
+
placeholder: "ws://localhost:8768/ws/link",
|
|
72
|
+
});
|
|
73
|
+
// Prompt for required authentication fields
|
|
74
|
+
const apiKey = await prompter.text({
|
|
75
|
+
message: "API Key (必需)",
|
|
76
|
+
initialValue: currentConfig?.apiKey || "",
|
|
77
|
+
placeholder: "输入小艺 API Key",
|
|
78
|
+
validate: (value) => (value.trim() ? undefined : "API Key 不能为空"),
|
|
79
|
+
});
|
|
80
|
+
const uid = await prompter.text({
|
|
81
|
+
message: "UID - 用户ID (必需)",
|
|
82
|
+
initialValue: currentConfig?.uid || "",
|
|
83
|
+
placeholder: "输入用户 ID",
|
|
84
|
+
validate: (value) => (value.trim() ? undefined : "UID 不能为空"),
|
|
85
|
+
});
|
|
86
|
+
const agentId = await prompter.text({
|
|
87
|
+
message: "Agent ID - 智能体ID (必需)",
|
|
88
|
+
initialValue: currentConfig?.agentId || "",
|
|
89
|
+
placeholder: "agent5336cca603f941ee9b112f711805e866",
|
|
90
|
+
validate: (value) => (value.trim() ? undefined : "Agent ID 不能为空"),
|
|
91
|
+
});
|
|
92
|
+
const apiId = await prompter.text({
|
|
93
|
+
message: "API ID (必需)",
|
|
94
|
+
initialValue: currentConfig?.apiId || "",
|
|
95
|
+
placeholder: "输入 API ID",
|
|
96
|
+
validate: (value) => (value.trim() ? undefined : "API ID 不能为空"),
|
|
97
|
+
});
|
|
98
|
+
const pushId = await prompter.text({
|
|
99
|
+
message: "Push ID (必需)",
|
|
100
|
+
initialValue: currentConfig?.pushId || "",
|
|
101
|
+
placeholder: "输入 Push ID",
|
|
102
|
+
validate: (value) => (value.trim() ? undefined : "Push ID 不能为空"),
|
|
103
|
+
});
|
|
104
|
+
// Optional fields
|
|
105
|
+
const fileUploadUrl = await prompter.text({
|
|
106
|
+
message: "File Upload URL (文件上传服务)",
|
|
107
|
+
initialValue: currentConfig?.fileUploadUrl || "http://localhost:8767",
|
|
108
|
+
placeholder: "http://localhost:8767",
|
|
109
|
+
});
|
|
110
|
+
const pushUrl = await prompter.text({
|
|
111
|
+
message: "Push URL (推送服务,可选)",
|
|
112
|
+
initialValue: currentConfig?.pushUrl || "",
|
|
113
|
+
placeholder: "留空使用默认值",
|
|
114
|
+
});
|
|
115
|
+
// Update configuration
|
|
116
|
+
const updatedConfig = {
|
|
117
|
+
...cfg,
|
|
118
|
+
channels: {
|
|
119
|
+
...cfg.channels,
|
|
120
|
+
"xiaoyi-channel": {
|
|
121
|
+
enabled: true,
|
|
122
|
+
wsUrl1: wsUrl1.trim(),
|
|
123
|
+
wsUrl2: wsUrl2.trim(),
|
|
124
|
+
apiKey: apiKey.trim(),
|
|
125
|
+
uid: uid.trim(),
|
|
126
|
+
agentId: agentId.trim(),
|
|
127
|
+
apiId: apiId.trim(),
|
|
128
|
+
pushId: pushId.trim(),
|
|
129
|
+
fileUploadUrl: fileUploadUrl.trim(),
|
|
130
|
+
...(pushUrl?.trim() ? { pushUrl: pushUrl.trim() } : {}),
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
// Show confirmation
|
|
135
|
+
await prompter.note([
|
|
136
|
+
"✅ XY Channel 配置完成",
|
|
137
|
+
"",
|
|
138
|
+
`主连接: ${wsUrl1}`,
|
|
139
|
+
`辅助连接: ${wsUrl2}`,
|
|
140
|
+
`Agent ID: ${agentId}`,
|
|
141
|
+
`UID: ${uid}`,
|
|
142
|
+
"",
|
|
143
|
+
"运行以下命令启动 gateway:",
|
|
144
|
+
" openclaw gateway restart",
|
|
145
|
+
"",
|
|
146
|
+
"查看日志:",
|
|
147
|
+
" openclaw logs --follow",
|
|
148
|
+
].join("\n"), "配置成功");
|
|
149
|
+
return {
|
|
150
|
+
cfg: updatedConfig,
|
|
151
|
+
accountId: "default",
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* XY Channel Onboarding Adapter
|
|
156
|
+
* Implements the ChannelOnboardingAdapter interface for OpenClaw's onboarding system
|
|
157
|
+
*/
|
|
158
|
+
export const xyOnboardingAdapter = {
|
|
159
|
+
channel,
|
|
160
|
+
getStatus,
|
|
161
|
+
configure,
|
|
162
|
+
// Optional: disable the channel
|
|
163
|
+
disable: (cfg) => ({
|
|
164
|
+
...cfg,
|
|
165
|
+
channels: {
|
|
166
|
+
...cfg.channels,
|
|
167
|
+
"xiaoyi-channel": {
|
|
168
|
+
...(cfg.channels?.["xiaoyi-channel"] || {}),
|
|
169
|
+
enabled: false,
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
}),
|
|
173
|
+
};
|