@ynhcj/xiaoyi 2.5.6 → 2.5.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.
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Download a file from URL to local path.
3
+ */
4
+ export declare function downloadFile(url: string, destPath: string): Promise<void>;
5
+ /**
6
+ * Download files from A2A file parts.
7
+ * Returns array of local file paths.
8
+ */
9
+ export declare function downloadFilesFromParts(fileParts: Array<{
10
+ name: string;
11
+ mimeType: string;
12
+ uri: string;
13
+ }>, tempDir?: string): Promise<Array<{
14
+ path: string;
15
+ name: string;
16
+ mimeType: string;
17
+ }>>;
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.downloadFile = downloadFile;
7
+ exports.downloadFilesFromParts = downloadFilesFromParts;
8
+ // File download utilities
9
+ const node_fetch_1 = __importDefault(require("node-fetch"));
10
+ const promises_1 = __importDefault(require("fs/promises"));
11
+ const path_1 = __importDefault(require("path"));
12
+ const logger_js_1 = require("./xy-utils/logger.js");
13
+ /**
14
+ * Download a file from URL to local path.
15
+ */
16
+ async function downloadFile(url, destPath) {
17
+ logger_js_1.logger.debug(`Downloading file from ${url} to ${destPath}`);
18
+ const controller = new AbortController();
19
+ const timeout = setTimeout(() => controller.abort(), 30000); // 30 seconds timeout
20
+ try {
21
+ const response = await (0, node_fetch_1.default)(url, { signal: controller.signal });
22
+ if (!response.ok) {
23
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
24
+ }
25
+ const arrayBuffer = await response.arrayBuffer();
26
+ const buffer = Buffer.from(arrayBuffer);
27
+ await promises_1.default.writeFile(destPath, buffer);
28
+ logger_js_1.logger.debug(`File downloaded successfully: ${destPath}`);
29
+ }
30
+ catch (error) {
31
+ if (error.name === 'AbortError') {
32
+ logger_js_1.logger.error(`Download timeout (30s) for ${url}`);
33
+ throw new Error(`Download timeout after 30 seconds`);
34
+ }
35
+ logger_js_1.logger.error(`Failed to download file from ${url}:`, error);
36
+ throw error;
37
+ }
38
+ finally {
39
+ clearTimeout(timeout);
40
+ }
41
+ }
42
+ /**
43
+ * Download files from A2A file parts.
44
+ * Returns array of local file paths.
45
+ */
46
+ async function downloadFilesFromParts(fileParts, tempDir = "/tmp/xy_channel") {
47
+ // Create temp directory if it doesn't exist
48
+ await promises_1.default.mkdir(tempDir, { recursive: true });
49
+ const downloadedFiles = [];
50
+ for (const filePart of fileParts) {
51
+ const { name, mimeType, uri } = filePart;
52
+ // Generate safe file name
53
+ const safeName = name.replace(/[^a-zA-Z0-9._-]/g, "_");
54
+ const destPath = path_1.default.join(tempDir, `${Date.now()}_${safeName}`);
55
+ try {
56
+ await downloadFile(uri, destPath);
57
+ downloadedFiles.push({
58
+ path: destPath,
59
+ name,
60
+ mimeType,
61
+ });
62
+ }
63
+ catch (error) {
64
+ logger_js_1.logger.error(`Failed to download file ${name}:`, error);
65
+ // Continue with other files
66
+ }
67
+ }
68
+ return downloadedFiles;
69
+ }
@@ -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 and protocol-level 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
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.HeartbeatManager = void 0;
7
+ // Heartbeat management for WebSocket connections
8
+ const ws_1 = __importDefault(require("ws"));
9
+ /**
10
+ * Manages heartbeat for a WebSocket connection.
11
+ * Supports both application-level and protocol-level heartbeats.
12
+ */
13
+ class HeartbeatManager {
14
+ constructor(ws, config, onTimeout, serverName = "unknown", logFn, errorFn, onHeartbeatSuccess // ✅ 心跳成功回调,向 OpenClaw 报告健康状态
15
+ ) {
16
+ this.ws = ws;
17
+ this.config = config;
18
+ this.onTimeout = onTimeout;
19
+ this.serverName = serverName;
20
+ this.onHeartbeatSuccess = onHeartbeatSuccess;
21
+ this.intervalTimer = null;
22
+ this.timeoutTimer = null;
23
+ this.lastPongTime = 0;
24
+ this.log = logFn ?? console.log;
25
+ this.error = errorFn ?? console.error;
26
+ }
27
+ /**
28
+ * Start heartbeat monitoring.
29
+ */
30
+ start() {
31
+ this.stop(); // Clear any existing timers
32
+ this.lastPongTime = Date.now();
33
+ // Setup ping/pong for protocol-level heartbeat
34
+ this.ws.on("pong", () => {
35
+ this.lastPongTime = Date.now();
36
+ if (this.timeoutTimer) {
37
+ clearTimeout(this.timeoutTimer);
38
+ this.timeoutTimer = null;
39
+ }
40
+ // ✅ Report health: heartbeat successful - notify OpenClaw framework
41
+ this.onHeartbeatSuccess?.();
42
+ this.log(`[${this.serverName}] Heartbeat pong received, health reported to OpenClaw`);
43
+ });
44
+ // Start interval timer
45
+ this.intervalTimer = setInterval(() => {
46
+ this.sendHeartbeat();
47
+ }, this.config.interval);
48
+ this.log(`[${this.serverName}] Heartbeat started: interval=${this.config.interval}ms, timeout=${this.config.timeout}ms`);
49
+ }
50
+ /**
51
+ * Stop heartbeat monitoring.
52
+ */
53
+ stop() {
54
+ if (this.intervalTimer) {
55
+ clearInterval(this.intervalTimer);
56
+ this.intervalTimer = null;
57
+ }
58
+ if (this.timeoutTimer) {
59
+ clearTimeout(this.timeoutTimer);
60
+ this.timeoutTimer = null;
61
+ }
62
+ // Remove pong listener
63
+ this.ws.off("pong", () => { });
64
+ this.log(`[${this.serverName}] Heartbeat stopped`);
65
+ }
66
+ /**
67
+ * Send a heartbeat ping.
68
+ */
69
+ sendHeartbeat() {
70
+ if (this.ws.readyState !== ws_1.default.OPEN) {
71
+ console.warn(`[${this.serverName}] Cannot send heartbeat: WebSocket not open`);
72
+ return;
73
+ }
74
+ try {
75
+ // Send application-level heartbeat message
76
+ this.log(`[${this.serverName}] Sending heartbeat frame:`, this.config.message.substring(0, 100));
77
+ this.ws.send(this.config.message);
78
+ // Send protocol-level ping
79
+ this.ws.ping();
80
+ this.log(`[${this.serverName}] Protocol-level ping sent`);
81
+ // Setup timeout timer
82
+ this.timeoutTimer = setTimeout(() => {
83
+ this.error(`[${this.serverName}] Heartbeat timeout - no pong received`);
84
+ this.onTimeout();
85
+ }, this.config.timeout);
86
+ }
87
+ catch (error) {
88
+ this.error(`[${this.serverName}] Failed to send heartbeat:`, error);
89
+ }
90
+ }
91
+ /**
92
+ * Check if connection is healthy based on last pong time.
93
+ */
94
+ isHealthy() {
95
+ if (this.lastPongTime === 0) {
96
+ return true; // Not started yet
97
+ }
98
+ const timeSinceLastPong = Date.now() - this.lastPongTime;
99
+ return timeSinceLastPong < this.config.interval + this.config.timeout;
100
+ }
101
+ }
102
+ exports.HeartbeatManager = HeartbeatManager;
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- const channel_1 = require("./channel");
4
- const runtime_1 = require("./runtime");
3
+ const channel_js_1 = require("./channel.js");
4
+ const runtime_js_1 = require("./runtime.js");
5
5
  /**
6
6
  * XiaoYi Channel Plugin for OpenClaw
7
7
  *
@@ -30,10 +30,10 @@ const plugin = {
30
30
  register(api) {
31
31
  console.log("XiaoYi: register() called - START");
32
32
  // Set runtime for managing WebSocket connections
33
- (0, runtime_1.setXiaoYiRuntime)(api.runtime);
33
+ (0, runtime_js_1.setXiaoYiRuntime)(api.runtime);
34
34
  console.log("XiaoYi: setXiaoYiRuntime() completed");
35
35
  // Clean up any existing connections from previous plugin loads
36
- const runtime = require("./runtime").getXiaoYiRuntime();
36
+ const runtime = require("./runtime.js").getXiaoYiRuntime();
37
37
  console.log(`XiaoYi: Got runtime instance: ${runtime.getInstanceId()}, isConnected: ${runtime.isConnected()}`);
38
38
  if (runtime.isConnected()) {
39
39
  console.log("XiaoYi: Cleaning up existing connection from previous load");
@@ -41,7 +41,7 @@ const plugin = {
41
41
  }
42
42
  // Register the channel plugin
43
43
  console.log("XiaoYi: About to call registerChannel()");
44
- api.registerChannel({ plugin: channel_1.xiaoyiPlugin });
44
+ api.registerChannel({ plugin: channel_js_1.xiaoyiPlugin });
45
45
  console.log("XiaoYi: registerChannel() completed");
46
46
  console.log("XiaoYi channel plugin registered - END");
47
47
  },
package/dist/push.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { XiaoYiChannelConfig } from "./types";
1
+ import { XiaoYiChannelConfig } from "./types.js";
2
2
  /**
3
3
  * Push message sending service
4
4
  * Sends notifications to XiaoYi clients via webhook API
package/dist/runtime.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { XiaoYiWebSocketManager } from "./websocket";
2
- import { XiaoYiChannelConfig } from "./types";
1
+ import { XiaoYiWebSocketManager } from "./websocket.js";
2
+ import { XiaoYiChannelConfig } from "./types.js";
3
3
  /**
4
4
  * Timeout configuration
5
5
  */
package/dist/runtime.js CHANGED
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.XiaoYiRuntime = void 0;
4
4
  exports.getXiaoYiRuntime = getXiaoYiRuntime;
5
5
  exports.setXiaoYiRuntime = setXiaoYiRuntime;
6
- const websocket_1 = require("./websocket");
6
+ const websocket_js_1 = require("./websocket.js");
7
7
  /**
8
8
  * Default timeout configuration
9
9
  */
@@ -64,7 +64,7 @@ class XiaoYiRuntime {
64
64
  return;
65
65
  }
66
66
  this.config = config;
67
- const manager = new websocket_1.XiaoYiWebSocketManager(config);
67
+ const manager = new websocket_js_1.XiaoYiWebSocketManager(config);
68
68
  // Setup basic event handlers (message handling is done in channel.ts)
69
69
  manager.on("error", (error) => {
70
70
  console.error("XiaoYi channel error:", error);
package/dist/types.d.ts CHANGED
@@ -1,3 +1,69 @@
1
+ export interface A2AJsonRpcRequest {
2
+ jsonrpc: "2.0";
3
+ method: string;
4
+ params: A2ARequestParams;
5
+ id: string;
6
+ }
7
+ export interface A2ARequestParams {
8
+ id: string;
9
+ sessionId: string;
10
+ agentLoginSessionId?: string;
11
+ message: A2AMessage;
12
+ }
13
+ export interface A2AMessage {
14
+ role: "user" | "assistant" | "system";
15
+ parts: A2AMessagePart[];
16
+ }
17
+ export type A2AMessagePart = A2ATextPart | A2AFilePart | A2ADataPart;
18
+ export interface A2ATextPart {
19
+ kind: "text";
20
+ text: string;
21
+ }
22
+ export interface A2AFilePart {
23
+ kind: "file";
24
+ file: {
25
+ name: string;
26
+ mimeType: string;
27
+ uri: string;
28
+ };
29
+ }
30
+ export interface A2ADataPart {
31
+ kind: "data";
32
+ data: {
33
+ event?: A2ADataEvent;
34
+ variables?: {
35
+ systemVariables?: {
36
+ push_id?: string;
37
+ };
38
+ };
39
+ [key: string]: any;
40
+ };
41
+ }
42
+ export interface A2ADataEvent {
43
+ intentName: string;
44
+ outputs: Record<string, any>;
45
+ status: "success" | "failed";
46
+ }
47
+ export interface A2AReasoningTextPart {
48
+ kind: "reasoningText";
49
+ reasoningText: string;
50
+ }
51
+ export interface A2ACommandPart {
52
+ kind: "command";
53
+ command: A2ACommand;
54
+ }
55
+ export interface A2ACommand {
56
+ header: {
57
+ namespace: string;
58
+ name: string;
59
+ };
60
+ payload: Record<string, any>;
61
+ }
62
+ export type A2AArtifactPart = A2ATextPart | A2ADataPart | A2ACommandPart | A2AReasoningTextPart;
63
+ export interface A2AArtifact {
64
+ artifactId: string;
65
+ parts: A2AArtifactPart[];
66
+ }
1
67
  export interface A2ARequestMessage {
2
68
  agentId: string;
3
69
  jsonrpc: "2.0";
@@ -75,8 +141,9 @@ export interface A2ATaskArtifactUpdateEvent {
75
141
  artifact: {
76
142
  artifactId: string;
77
143
  parts: Array<{
78
- kind: "text" | "file" | "data";
144
+ kind: "text" | "file" | "data" | "reasoningText";
79
145
  text?: string;
146
+ reasoningText?: string;
80
147
  file?: {
81
148
  name: string;
82
149
  mimeType: string;
@@ -205,3 +272,9 @@ export interface SessionCleanupState {
205
272
  reason: 'user_cleared' | 'timeout' | 'error';
206
273
  accumulatedText?: string;
207
274
  }
275
+ export interface SessionBinding {
276
+ sessionId: string;
277
+ server: ServerIdentifier;
278
+ boundAt: number;
279
+ }
280
+ export type ServerIdentifier = 'server1' | 'server2';
@@ -1,5 +1,5 @@
1
1
  import { EventEmitter } from "events";
2
- import { A2AResponseMessage, WebSocketConnectionState, XiaoYiChannelConfig, ServerId, ServerConnectionState, SessionCleanupState } from "./types";
2
+ import { A2AResponseMessage, OutboundWebSocketMessage, WebSocketConnectionState, XiaoYiChannelConfig, ServerId, ServerConnectionState, SessionCleanupState } from "./types.js";
3
3
  export declare class XiaoYiWebSocketManager extends EventEmitter {
4
4
  private ws1;
5
5
  private ws2;
@@ -13,13 +13,21 @@ export declare class XiaoYiWebSocketManager extends EventEmitter {
13
13
  private heartbeatTimeout1?;
14
14
  private heartbeatTimeout2?;
15
15
  private appHeartbeatInterval?;
16
+ private heartbeat1?;
17
+ private heartbeat2?;
16
18
  private reconnectTimeout1?;
17
19
  private reconnectTimeout2?;
18
20
  private stableConnectionTimer1?;
19
21
  private stableConnectionTimer2?;
20
22
  private static readonly STABLE_CONNECTION_THRESHOLD;
21
23
  private activeTasks;
24
+ private onHealthEvent?;
22
25
  constructor(config: XiaoYiChannelConfig);
26
+ /**
27
+ * Set health event callback to report activity to OpenClaw framework.
28
+ * This callback is invoked on heartbeat success to update lastEventAt.
29
+ */
30
+ setHealthEventCallback(callback: () => void): void;
23
31
  /**
24
32
  * Check if URL is wss + IP format (skip certificate verification)
25
33
  */
@@ -93,6 +101,14 @@ export declare class XiaoYiWebSocketManager extends EventEmitter {
93
101
  * TODO: 实现实际的推送消息发送逻辑
94
102
  */
95
103
  sendPushMessage(sessionId: string, message: string): Promise<void>;
104
+ /**
105
+ * Send an outbound WebSocket message directly.
106
+ * This is a low-level method that sends a pre-formatted OutboundWebSocketMessage.
107
+ *
108
+ * @param sessionId - Session ID for routing
109
+ * @param message - Pre-formatted outbound message
110
+ */
111
+ sendMessage(sessionId: string, message: OutboundWebSocketMessage): Promise<void>;
96
112
  /**
97
113
  * Send tasks cancel response to specific server
98
114
  */