@ynhcj/xiaoyi 2.5.2 → 2.5.5

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.
@@ -23,24 +23,24 @@ export declare const XiaoYiConfigSchema: z.ZodObject<{
23
23
  /** Multi-account configuration */
24
24
  accounts: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
25
25
  }, "strip", z.ZodTypeAny, {
26
- enabled: boolean;
27
- debug: boolean;
28
- enableStreaming: boolean;
29
- name?: string | undefined;
30
- wsUrl?: string | undefined;
31
- ak?: string | undefined;
32
- sk?: string | undefined;
33
- agentId?: string | undefined;
34
- accounts?: Record<string, unknown> | undefined;
26
+ enabled?: boolean;
27
+ wsUrl?: string;
28
+ ak?: string;
29
+ sk?: string;
30
+ agentId?: string;
31
+ enableStreaming?: boolean;
32
+ name?: string;
33
+ debug?: boolean;
34
+ accounts?: Record<string, unknown>;
35
35
  }, {
36
- enabled?: boolean | undefined;
37
- name?: string | undefined;
38
- wsUrl?: string | undefined;
39
- ak?: string | undefined;
40
- sk?: string | undefined;
41
- agentId?: string | undefined;
42
- debug?: boolean | undefined;
43
- enableStreaming?: boolean | undefined;
44
- accounts?: Record<string, unknown> | undefined;
36
+ enabled?: boolean;
37
+ wsUrl?: string;
38
+ ak?: string;
39
+ sk?: string;
40
+ agentId?: string;
41
+ enableStreaming?: boolean;
42
+ name?: string;
43
+ debug?: boolean;
44
+ accounts?: Record<string, unknown>;
45
45
  }>;
46
46
  export type XiaoYiConfig = z.infer<typeof XiaoYiConfigSchema>;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * XiaoYi onboarding adapter for CLI setup wizard.
3
+ */
4
+ type ChannelOnboardingAdapter = any;
5
+ export declare const xiaoyiOnboardingAdapter: ChannelOnboardingAdapter;
6
+ export {};
@@ -0,0 +1,167 @@
1
+ "use strict";
2
+ /**
3
+ * XiaoYi onboarding adapter for CLI setup wizard.
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.xiaoyiOnboardingAdapter = void 0;
7
+ const channel = "xiaoyi";
8
+ /**
9
+ * Get XiaoYi channel config from OpenClaw config
10
+ */
11
+ function getXiaoYiConfig(cfg) {
12
+ return cfg?.channels?.xiaoyi;
13
+ }
14
+ /**
15
+ * Check if XiaoYi is properly configured
16
+ */
17
+ function isXiaoYiConfigured(config) {
18
+ if (!config) {
19
+ return false;
20
+ }
21
+ // Check required fields: ak, sk, agentId
22
+ // wsUrl1/wsUrl2 are optional (defaults will be used if not provided)
23
+ const hasAk = typeof config.ak === "string" && config.ak.trim().length > 0;
24
+ const hasSk = typeof config.sk === "string" && config.sk.trim().length > 0;
25
+ const hasAgentId = typeof config.agentId === "string" && config.agentId.trim().length > 0;
26
+ return hasAk && hasSk && hasAgentId;
27
+ }
28
+ /**
29
+ * Set XiaoYi channel configuration
30
+ */
31
+ function setXiaoYiConfig(cfg, config) {
32
+ const existing = getXiaoYiConfig(cfg);
33
+ const merged = {
34
+ enabled: config.enabled ?? existing?.enabled ?? true,
35
+ wsUrl: config.wsUrl ?? existing?.wsUrl ?? "",
36
+ wsUrl1: config.wsUrl1 ?? existing?.wsUrl1 ?? "",
37
+ wsUrl2: config.wsUrl2 ?? existing?.wsUrl2 ?? "",
38
+ ak: config.ak ?? existing?.ak ?? "",
39
+ sk: config.sk ?? existing?.sk ?? "",
40
+ agentId: config.agentId ?? existing?.agentId ?? "",
41
+ enableStreaming: config.enableStreaming ?? existing?.enableStreaming ?? true,
42
+ };
43
+ return {
44
+ ...cfg,
45
+ channels: {
46
+ ...cfg.channels,
47
+ xiaoyi: merged,
48
+ },
49
+ };
50
+ }
51
+ /**
52
+ * Note about XiaoYi setup
53
+ */
54
+ async function noteXiaoYiSetupHelp(prompter) {
55
+ await prompter.note([
56
+ "XiaoYi (小艺) uses A2A protocol via WebSocket connection.",
57
+ "",
58
+ "Required credentials:",
59
+ " - ak: Access Key for authentication",
60
+ " - sk: Secret Key for authentication",
61
+ " - agentId: Your agent identifier",
62
+ "",
63
+ "WebSocket URLs will use default values.",
64
+ "",
65
+ "Docs: https://docs.openclaw.ai/channels/xiaoyi",
66
+ ].join("\n"), "XiaoYi setup");
67
+ }
68
+ /**
69
+ * Prompt for Access Key
70
+ */
71
+ async function promptAk(prompter, config) {
72
+ const existing = config?.ak ?? "";
73
+ return String(await prompter.text({
74
+ message: "XiaoYi Access Key (ak)",
75
+ initialValue: existing,
76
+ validate: (value) => (value?.trim() ? undefined : "Required"),
77
+ })).trim();
78
+ }
79
+ /**
80
+ * Prompt for Secret Key
81
+ */
82
+ async function promptSk(prompter, config) {
83
+ const existing = config?.sk ?? "";
84
+ return String(await prompter.text({
85
+ message: "XiaoYi Secret Key (sk)",
86
+ initialValue: existing,
87
+ validate: (value) => (value?.trim() ? undefined : "Required"),
88
+ })).trim();
89
+ }
90
+ /**
91
+ * Prompt for Agent ID
92
+ */
93
+ async function promptAgentId(prompter, config) {
94
+ const existing = config?.agentId ?? "";
95
+ return String(await prompter.text({
96
+ message: "XiaoYi Agent ID",
97
+ initialValue: existing,
98
+ validate: (value) => (value?.trim() ? undefined : "Required"),
99
+ })).trim();
100
+ }
101
+ exports.xiaoyiOnboardingAdapter = {
102
+ channel,
103
+ getStatus: async ({ cfg }) => {
104
+ const config = getXiaoYiConfig(cfg);
105
+ const configured = isXiaoYiConfigured(config);
106
+ const enabled = config?.enabled !== false;
107
+ const statusLines = [];
108
+ if (configured) {
109
+ statusLines.push(`XiaoYi: ${enabled ? "enabled" : "disabled"}`);
110
+ if (config?.wsUrl1 || config?.wsUrl) {
111
+ statusLines.push(` WebSocket: ${config.wsUrl1 || config.wsUrl}`);
112
+ }
113
+ if (config?.wsUrl2) {
114
+ statusLines.push(` Secondary: ${config.wsUrl2}`);
115
+ }
116
+ if (config?.agentId) {
117
+ statusLines.push(` Agent ID: ${config.agentId}`);
118
+ }
119
+ }
120
+ else {
121
+ statusLines.push("XiaoYi: needs ak, sk, and agentId");
122
+ }
123
+ return {
124
+ channel,
125
+ configured,
126
+ statusLines,
127
+ selectionHint: configured ? "configured" : "needs setup",
128
+ quickstartScore: 50,
129
+ };
130
+ },
131
+ configure: async ({ cfg, prompter }) => {
132
+ const config = getXiaoYiConfig(cfg);
133
+ if (!isXiaoYiConfigured(config)) {
134
+ await noteXiaoYiSetupHelp(prompter);
135
+ }
136
+ else {
137
+ const reconfigure = await prompter.confirm({
138
+ message: "XiaoYi already configured. Reconfigure?",
139
+ initialValue: false,
140
+ });
141
+ if (!reconfigure) {
142
+ return { cfg, accountId: "default" };
143
+ }
144
+ }
145
+ // Prompt for required credentials
146
+ const ak = await promptAk(prompter, config);
147
+ const sk = await promptSk(prompter, config);
148
+ const agentId = await promptAgentId(prompter, config);
149
+ const cfgWithConfig = setXiaoYiConfig(cfg, {
150
+ ak,
151
+ sk,
152
+ agentId,
153
+ enabled: true,
154
+ });
155
+ return { cfg: cfgWithConfig, accountId: "default" };
156
+ },
157
+ disable: (cfg) => {
158
+ const xiaoyi = getXiaoYiConfig(cfg);
159
+ return {
160
+ ...cfg,
161
+ channels: {
162
+ ...cfg.channels,
163
+ xiaoyi: { ...xiaoyi, enabled: false },
164
+ },
165
+ };
166
+ },
167
+ };
package/dist/runtime.d.ts CHANGED
@@ -22,6 +22,7 @@ export declare class XiaoYiRuntime {
22
22
  private sessionTimeoutSent;
23
23
  private timeoutConfig;
24
24
  private sessionAbortControllerMap;
25
+ private sessionActiveRunMap;
25
26
  constructor();
26
27
  getInstanceId(): string;
27
28
  /**
@@ -52,11 +53,16 @@ export declare class XiaoYiRuntime {
52
53
  * Set timeout for a session
53
54
  * @param sessionId - Session ID
54
55
  * @param callback - Function to call when timeout occurs
55
- * @returns The timeout ID (for cancellation)
56
+ * @returns The interval ID (for cancellation)
57
+ *
58
+ * IMPORTANT: This now uses setInterval instead of setTimeout
59
+ * - First trigger: after 60 seconds
60
+ * - Subsequent triggers: every 60 seconds after that
61
+ * - Cleared when: response received, session completed, or explicitly cleared
56
62
  */
57
63
  setTimeoutForSession(sessionId: string, callback: () => void): NodeJS.Timeout | undefined;
58
64
  /**
59
- * Clear timeout for a session
65
+ * Clear timeout interval for a session
60
66
  * @param sessionId - Session ID
61
67
  */
62
68
  clearSessionTimeout(sessionId: string): void;
@@ -71,7 +77,7 @@ export declare class XiaoYiRuntime {
71
77
  */
72
78
  markSessionCompleted(sessionId: string): void;
73
79
  /**
74
- * Clear all timeouts
80
+ * Clear all timeout intervals
75
81
  */
76
82
  clearAllTimeouts(): void;
77
83
  /**
@@ -101,12 +107,18 @@ export declare class XiaoYiRuntime {
101
107
  /**
102
108
  * Create and register an AbortController for a session
103
109
  * @param sessionId - Session ID
104
- * @returns The AbortController and its signal
110
+ * @returns The AbortController and its signal, or null if session is busy
105
111
  */
106
112
  createAbortControllerForSession(sessionId: string): {
107
113
  controller: AbortController;
108
114
  signal: AbortSignal;
109
- };
115
+ } | null;
116
+ /**
117
+ * Check if a session has an active agent run
118
+ * @param sessionId - Session ID
119
+ * @returns true if session is busy
120
+ */
121
+ isSessionActive(sessionId: string): boolean;
110
122
  /**
111
123
  * Abort a session's agent run
112
124
  * @param sessionId - Session ID
package/dist/runtime.js CHANGED
@@ -10,7 +10,7 @@ const websocket_1 = require("./websocket");
10
10
  const DEFAULT_TIMEOUT_CONFIG = {
11
11
  enabled: true,
12
12
  duration: 60000, // 60 seconds
13
- message: "任务还在处理中,请稍后回来查看",
13
+ message: "任务正在处理中,请稍后",
14
14
  };
15
15
  /**
16
16
  * Runtime state for XiaoYi channel
@@ -28,6 +28,8 @@ class XiaoYiRuntime {
28
28
  this.timeoutConfig = DEFAULT_TIMEOUT_CONFIG;
29
29
  // AbortController management for canceling agent runs
30
30
  this.sessionAbortControllerMap = new Map();
31
+ // Track if a session has an active agent run (for concurrent request detection)
32
+ this.sessionActiveRunMap = new Map();
31
33
  this.instanceId = `runtime_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
32
34
  console.log(`XiaoYi: Created new runtime instance: ${this.instanceId}`);
33
35
  }
@@ -122,7 +124,12 @@ class XiaoYiRuntime {
122
124
  * Set timeout for a session
123
125
  * @param sessionId - Session ID
124
126
  * @param callback - Function to call when timeout occurs
125
- * @returns The timeout ID (for cancellation)
127
+ * @returns The interval ID (for cancellation)
128
+ *
129
+ * IMPORTANT: This now uses setInterval instead of setTimeout
130
+ * - First trigger: after 60 seconds
131
+ * - Subsequent triggers: every 60 seconds after that
132
+ * - Cleared when: response received, session completed, or explicitly cleared
126
133
  */
127
134
  setTimeoutForSession(sessionId, callback) {
128
135
  if (!this.timeoutConfig.enabled) {
@@ -133,32 +140,33 @@ class XiaoYiRuntime {
133
140
  const hadExistingTimeout = this.sessionTimeoutMap.has(sessionId);
134
141
  const hadSentTimeout = this.sessionTimeoutSent.has(sessionId);
135
142
  this.clearSessionTimeout(sessionId);
136
- // Also clear the timeout sent flag to allow this session to timeout again
143
+ // Clear the timeout sent flag to allow this session to timeout again
137
144
  if (hadSentTimeout) {
138
145
  this.sessionTimeoutSent.delete(sessionId);
139
146
  console.log(`[TIMEOUT] Previous timeout flag cleared for session ${sessionId} (session reuse)`);
140
147
  }
141
- const timeoutId = setTimeout(() => {
142
- console.log(`[TIMEOUT] Timeout triggered for session ${sessionId}`);
143
- this.sessionTimeoutMap.delete(sessionId);
148
+ // Use setInterval for periodic timeout triggers
149
+ // First trigger after duration, then every duration after that
150
+ const intervalId = setInterval(() => {
151
+ console.log(`[TIMEOUT] Timeout triggered for session ${sessionId} (will trigger again in ${this.timeoutConfig.duration}ms if still active)`);
144
152
  this.sessionTimeoutSent.add(sessionId);
145
153
  callback();
146
154
  }, this.timeoutConfig.duration);
147
- this.sessionTimeoutMap.set(sessionId, timeoutId);
148
- const logSuffix = hadExistingTimeout ? " (replacing existing timeout)" : "";
149
- console.log(`[TIMEOUT] ${this.timeoutConfig.duration}ms timeout started for session ${sessionId}${logSuffix}`);
150
- return timeoutId;
155
+ this.sessionTimeoutMap.set(sessionId, intervalId);
156
+ const logSuffix = hadExistingTimeout ? " (replacing existing interval)" : "";
157
+ console.log(`[TIMEOUT] ${this.timeoutConfig.duration}ms periodic timeout started for session ${sessionId}${logSuffix}`);
158
+ return intervalId;
151
159
  }
152
160
  /**
153
- * Clear timeout for a session
161
+ * Clear timeout interval for a session
154
162
  * @param sessionId - Session ID
155
163
  */
156
164
  clearSessionTimeout(sessionId) {
157
- const timeoutId = this.sessionTimeoutMap.get(sessionId);
158
- if (timeoutId) {
159
- clearTimeout(timeoutId);
165
+ const intervalId = this.sessionTimeoutMap.get(sessionId);
166
+ if (intervalId) {
167
+ clearInterval(intervalId);
160
168
  this.sessionTimeoutMap.delete(sessionId);
161
- console.log(`[TIMEOUT] Timeout cleared for session ${sessionId}`);
169
+ console.log(`[TIMEOUT] Timeout interval cleared for session ${sessionId}`);
162
170
  }
163
171
  }
164
172
  /**
@@ -178,15 +186,15 @@ class XiaoYiRuntime {
178
186
  console.log(`[TIMEOUT] Session ${sessionId} marked as completed`);
179
187
  }
180
188
  /**
181
- * Clear all timeouts
189
+ * Clear all timeout intervals
182
190
  */
183
191
  clearAllTimeouts() {
184
- for (const [sessionId, timeoutId] of this.sessionTimeoutMap.entries()) {
185
- clearTimeout(timeoutId);
192
+ for (const [sessionId, intervalId] of this.sessionTimeoutMap.entries()) {
193
+ clearInterval(intervalId);
186
194
  }
187
195
  this.sessionTimeoutMap.clear();
188
196
  this.sessionTimeoutSent.clear();
189
- console.log("[TIMEOUT] All timeouts cleared");
197
+ console.log("[TIMEOUT] All timeout intervals cleared");
190
198
  }
191
199
  /**
192
200
  * Get WebSocket manager
@@ -227,16 +235,28 @@ class XiaoYiRuntime {
227
235
  /**
228
236
  * Create and register an AbortController for a session
229
237
  * @param sessionId - Session ID
230
- * @returns The AbortController and its signal
238
+ * @returns The AbortController and its signal, or null if session is busy
231
239
  */
232
240
  createAbortControllerForSession(sessionId) {
233
- // Abort any existing controller for this session
234
- this.abortSession(sessionId);
241
+ // Check if there's an active agent run for this session
242
+ if (this.sessionActiveRunMap.get(sessionId)) {
243
+ console.log(`[CONCURRENT] Session ${sessionId} has an active agent run, cannot create new AbortController`);
244
+ return null;
245
+ }
235
246
  const controller = new AbortController();
236
247
  this.sessionAbortControllerMap.set(sessionId, controller);
248
+ this.sessionActiveRunMap.set(sessionId, true);
237
249
  console.log(`[ABORT] Created AbortController for session ${sessionId}`);
238
250
  return { controller, signal: controller.signal };
239
251
  }
252
+ /**
253
+ * Check if a session has an active agent run
254
+ * @param sessionId - Session ID
255
+ * @returns true if session is busy
256
+ */
257
+ isSessionActive(sessionId) {
258
+ return this.sessionActiveRunMap.get(sessionId) || false;
259
+ }
240
260
  /**
241
261
  * Abort a session's agent run
242
262
  * @param sessionId - Session ID
@@ -272,6 +292,9 @@ class XiaoYiRuntime {
272
292
  this.sessionAbortControllerMap.delete(sessionId);
273
293
  console.log(`[ABORT] Cleared AbortController for session ${sessionId}`);
274
294
  }
295
+ // Also clear the active run flag
296
+ this.sessionActiveRunMap.delete(sessionId);
297
+ console.log(`[CONCURRENT] Session ${sessionId} marked as inactive`);
275
298
  }
276
299
  /**
277
300
  * Clear all AbortControllers
@@ -75,6 +75,22 @@ export declare class XiaoYiWebSocketManager extends EventEmitter {
75
75
  * This uses "status-update" event type which keeps the conversation active
76
76
  */
77
77
  sendStatusUpdate(taskId: string, sessionId: string, message: string, targetServer?: ServerId): Promise<void>;
78
+ /**
79
+ * Send PUSH message (主动推送) via HTTP API
80
+ *
81
+ * This is used when SubAgent completes execution and needs to push results to user
82
+ * independently of the original A2A request-response flow.
83
+ *
84
+ * Unlike sendResponse (which responds to a specific request via WebSocket), push messages are
85
+ * sent through HTTP API asynchronously.
86
+ *
87
+ * @param sessionId - User's session ID
88
+ * @param message - Message content to push
89
+ *
90
+ * Reference: 华为小艺推送消息 API
91
+ * TODO: 实现实际的推送消息发送逻辑
92
+ */
93
+ sendPushMessage(sessionId: string, message: string): Promise<void>;
78
94
  /**
79
95
  * Send tasks cancel response to specific server
80
96
  */
package/dist/websocket.js CHANGED
@@ -514,6 +514,27 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
514
514
  throw error;
515
515
  }
516
516
  }
517
+ /**
518
+ * Send PUSH message (主动推送) via HTTP API
519
+ *
520
+ * This is used when SubAgent completes execution and needs to push results to user
521
+ * independently of the original A2A request-response flow.
522
+ *
523
+ * Unlike sendResponse (which responds to a specific request via WebSocket), push messages are
524
+ * sent through HTTP API asynchronously.
525
+ *
526
+ * @param sessionId - User's session ID
527
+ * @param message - Message content to push
528
+ *
529
+ * Reference: 华为小艺推送消息 API
530
+ * TODO: 实现实际的推送消息发送逻辑
531
+ */
532
+ async sendPushMessage(sessionId, message) {
533
+ console.log(`[PUSH] Would send push message to session ${sessionId}, length: ${message.length} chars`);
534
+ console.log(`[PUSH] Content: ${message.substring(0, 50)}${message.length > 50 ? "..." : ""}`);
535
+ // TODO: Implement actual push message sending via HTTP API
536
+ // Need to confirm correct push message format with XiaoYi API documentation
537
+ }
517
538
  /**
518
539
  * Send tasks cancel response to specific server
519
540
  */
@@ -0,0 +1,81 @@
1
+ /**
2
+ * XiaoYi Media Handler - Downloads and saves media files locally
3
+ * Similar to clawdbot-feishu's media.ts approach
4
+ */
5
+ type PluginRuntime = any;
6
+ export interface DownloadedMedia {
7
+ path: string;
8
+ contentType: string;
9
+ placeholder: string;
10
+ fileName?: string;
11
+ }
12
+ export interface MediaDownloadOptions {
13
+ maxBytes?: number;
14
+ timeoutMs?: number;
15
+ }
16
+ /**
17
+ * Check if a MIME type is an image
18
+ */
19
+ export declare function isImageMimeType(mimeType: string | undefined): boolean;
20
+ /**
21
+ * Check if a MIME type is a PDF
22
+ */
23
+ export declare function isPdfMimeType(mimeType: string | undefined): boolean;
24
+ /**
25
+ * Check if a MIME type is text-based
26
+ */
27
+ export declare function isTextMimeType(mimeType: string | undefined): boolean;
28
+ /**
29
+ * Download and save media file to local disk
30
+ * This is the key function that follows clawdbot-feishu's approach
31
+ */
32
+ export declare function downloadAndSaveMedia(runtime: PluginRuntime, uri: string, mimeType: string, fileName: string, options?: MediaDownloadOptions): Promise<DownloadedMedia>;
33
+ /**
34
+ * Download and save multiple media files
35
+ */
36
+ export declare function downloadAndSaveMediaList(runtime: PluginRuntime, files: Array<{
37
+ uri: string;
38
+ mimeType: string;
39
+ name: string;
40
+ }>, options?: MediaDownloadOptions): Promise<DownloadedMedia[]>;
41
+ /**
42
+ * Build media payload for inbound context
43
+ * Similar to clawdbot-feishu's buildFeishuMediaPayload()
44
+ */
45
+ export declare function buildXiaoYiMediaPayload(mediaList: DownloadedMedia[]): {
46
+ MediaPath?: string;
47
+ MediaType?: string;
48
+ MediaUrl?: string;
49
+ MediaPaths?: string[];
50
+ MediaUrls?: string[];
51
+ MediaTypes?: string[];
52
+ };
53
+ /**
54
+ * Extract text from downloaded file for including in message body
55
+ */
56
+ export declare function extractTextFromFile(path: string, mimeType: string): Promise<string | null>;
57
+ /**
58
+ * Input image content type for AI processing
59
+ */
60
+ export interface InputImageContent {
61
+ type: "image";
62
+ data: string;
63
+ mimeType: string;
64
+ }
65
+ /**
66
+ * Image download limits
67
+ */
68
+ export interface ImageLimits {
69
+ maxBytes?: number;
70
+ timeoutMs?: number;
71
+ }
72
+ /**
73
+ * Extract image from URL and return base64 encoded data
74
+ */
75
+ export declare function extractImageFromUrl(url: string, limits?: Partial<ImageLimits>): Promise<InputImageContent>;
76
+ /**
77
+ * Extract text content from URL
78
+ * Supports text-based files (txt, md, json, xml, csv, etc.)
79
+ */
80
+ export declare function extractTextFromUrl(url: string, maxBytes?: number, timeoutMs?: number): Promise<string>;
81
+ export {};