@ynhcj/xiaoyi 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/README.md +207 -0
- package/dist/auth.d.ts +36 -0
- package/dist/auth.js +111 -0
- package/dist/channel.d.ts +189 -0
- package/dist/channel.js +354 -0
- package/dist/config-schema.d.ts +46 -0
- package/dist/config-schema.js +28 -0
- package/dist/file-download.d.ts +17 -0
- package/dist/file-download.js +69 -0
- package/dist/file-handler.d.ts +36 -0
- package/dist/file-handler.js +113 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +49 -0
- package/dist/onboarding.d.ts +6 -0
- package/dist/onboarding.js +167 -0
- package/dist/push.d.ts +28 -0
- package/dist/push.js +135 -0
- package/dist/runtime.d.ts +191 -0
- package/dist/runtime.js +438 -0
- package/dist/types.d.ts +280 -0
- package/dist/types.js +8 -0
- package/dist/websocket.d.ts +219 -0
- package/dist/websocket.js +1068 -0
- package/dist/xiaoyi-media.d.ts +81 -0
- package/dist/xiaoyi-media.js +216 -0
- package/dist/xy-bot.d.ts +19 -0
- package/dist/xy-bot.js +277 -0
- package/dist/xy-client.d.ts +26 -0
- package/dist/xy-client.js +78 -0
- package/dist/xy-config.d.ts +18 -0
- package/dist/xy-config.js +37 -0
- package/dist/xy-formatter.d.ts +94 -0
- package/dist/xy-formatter.js +303 -0
- package/dist/xy-monitor.d.ts +17 -0
- package/dist/xy-monitor.js +194 -0
- package/dist/xy-parser.d.ts +49 -0
- package/dist/xy-parser.js +109 -0
- package/dist/xy-reply-dispatcher.d.ts +17 -0
- package/dist/xy-reply-dispatcher.js +308 -0
- package/dist/xy-tools/session-manager.d.ts +29 -0
- package/dist/xy-tools/session-manager.js +80 -0
- package/dist/xy-utils/config-manager.d.ts +26 -0
- package/dist/xy-utils/config-manager.js +61 -0
- package/dist/xy-utils/crypto.d.ts +8 -0
- package/dist/xy-utils/crypto.js +21 -0
- package/dist/xy-utils/logger.d.ts +6 -0
- package/dist/xy-utils/logger.js +37 -0
- package/dist/xy-utils/session.d.ts +34 -0
- package/dist/xy-utils/session.js +55 -0
- package/openclaw.plugin.json +9 -0
- package/package.json +73 -0
- package/xiaoyi.js +1 -0
package/dist/channel.js
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.xiaoyiPlugin = void 0;
|
|
37
|
+
const runtime_js_1 = require("./runtime.js");
|
|
38
|
+
const onboarding_js_1 = require("./onboarding.js");
|
|
39
|
+
const session_manager_js_1 = require("./xy-tools/session-manager.js");
|
|
40
|
+
// Special marker for default push delivery when no target is specified (cron/announce mode)
|
|
41
|
+
const DEFAULT_PUSH_MARKER = "default";
|
|
42
|
+
/**
|
|
43
|
+
* Track if message handlers have been registered to prevent duplicate registrations
|
|
44
|
+
* when startAccount() is called multiple times due to auto-restart attempts
|
|
45
|
+
*/
|
|
46
|
+
let handlersRegistered = false;
|
|
47
|
+
/**
|
|
48
|
+
* XiaoYi Channel Plugin
|
|
49
|
+
* Implements OpenClaw ChannelPlugin interface for XiaoYi A2A protocol
|
|
50
|
+
* Single account mode only
|
|
51
|
+
*/
|
|
52
|
+
exports.xiaoyiPlugin = {
|
|
53
|
+
id: "xiaoyi",
|
|
54
|
+
meta: {
|
|
55
|
+
id: "xiaoyi",
|
|
56
|
+
label: "XiaoYi",
|
|
57
|
+
selectionLabel: "XiaoYi (小艺)",
|
|
58
|
+
docsPath: "/channels/xiaoyi",
|
|
59
|
+
blurb: "小艺 A2A 协议支持,通过 WebSocket 连接。",
|
|
60
|
+
aliases: ["xiaoyi"],
|
|
61
|
+
},
|
|
62
|
+
capabilities: {
|
|
63
|
+
chatTypes: ["direct"],
|
|
64
|
+
polls: false,
|
|
65
|
+
reactions: false,
|
|
66
|
+
threads: false,
|
|
67
|
+
media: true,
|
|
68
|
+
nativeCommands: false,
|
|
69
|
+
},
|
|
70
|
+
/**
|
|
71
|
+
* Config schema for UI form rendering
|
|
72
|
+
*/
|
|
73
|
+
configSchema: {
|
|
74
|
+
schema: {
|
|
75
|
+
type: "object",
|
|
76
|
+
properties: {
|
|
77
|
+
enabled: {
|
|
78
|
+
type: "boolean",
|
|
79
|
+
default: false,
|
|
80
|
+
description: "Enable XiaoYi channel",
|
|
81
|
+
},
|
|
82
|
+
wsUrl1: {
|
|
83
|
+
type: "string",
|
|
84
|
+
default: "wss://hag.cloud.huawei.com/openclaw/v1/ws/link",
|
|
85
|
+
description: "Primary WebSocket server URL",
|
|
86
|
+
},
|
|
87
|
+
wsUrl2: {
|
|
88
|
+
type: "string",
|
|
89
|
+
default: "wss://116.63.174.231/openclaw/v1/ws/link",
|
|
90
|
+
description: "Secondary WebSocket server URL",
|
|
91
|
+
},
|
|
92
|
+
ak: {
|
|
93
|
+
type: "string",
|
|
94
|
+
description: "Access Key",
|
|
95
|
+
},
|
|
96
|
+
sk: {
|
|
97
|
+
type: "string",
|
|
98
|
+
description: "Secret Key",
|
|
99
|
+
},
|
|
100
|
+
agentId: {
|
|
101
|
+
type: "string",
|
|
102
|
+
description: "Agent ID",
|
|
103
|
+
},
|
|
104
|
+
debug: {
|
|
105
|
+
type: "boolean",
|
|
106
|
+
default: false,
|
|
107
|
+
description: "Enable debug logging",
|
|
108
|
+
},
|
|
109
|
+
apiId: {
|
|
110
|
+
type: "string",
|
|
111
|
+
default: "",
|
|
112
|
+
description: "API ID for push notifications",
|
|
113
|
+
},
|
|
114
|
+
pushId: {
|
|
115
|
+
type: "string",
|
|
116
|
+
default: "",
|
|
117
|
+
description: "Push ID for push notifications",
|
|
118
|
+
},
|
|
119
|
+
taskTimeoutMs: {
|
|
120
|
+
type: "number",
|
|
121
|
+
default: 3600000,
|
|
122
|
+
description: "Task timeout in milliseconds (default: 1 hour)",
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
onboarding: onboarding_js_1.xiaoyiOnboardingAdapter,
|
|
128
|
+
/**
|
|
129
|
+
* Config adapter - single account mode
|
|
130
|
+
*/
|
|
131
|
+
config: {
|
|
132
|
+
listAccountIds: (cfg) => {
|
|
133
|
+
const channelConfig = cfg?.channels?.xiaoyi;
|
|
134
|
+
if (!channelConfig || !channelConfig.enabled) {
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
// Single account mode: always return "default"
|
|
138
|
+
return ["default"];
|
|
139
|
+
},
|
|
140
|
+
resolveAccount: (cfg, accountId) => {
|
|
141
|
+
// Single account mode: always use "default"
|
|
142
|
+
const resolvedAccountId = "default";
|
|
143
|
+
// Access channel config from cfg.channels.xiaoyi
|
|
144
|
+
const channelConfig = cfg?.channels?.xiaoyi;
|
|
145
|
+
// If channel is not configured yet, return empty config
|
|
146
|
+
if (!channelConfig) {
|
|
147
|
+
return {
|
|
148
|
+
accountId: resolvedAccountId,
|
|
149
|
+
config: {
|
|
150
|
+
enabled: false,
|
|
151
|
+
wsUrl: "",
|
|
152
|
+
wsUrl1: "",
|
|
153
|
+
wsUrl2: "",
|
|
154
|
+
ak: "",
|
|
155
|
+
sk: "",
|
|
156
|
+
agentId: "",
|
|
157
|
+
},
|
|
158
|
+
enabled: false,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
accountId: resolvedAccountId,
|
|
163
|
+
config: channelConfig,
|
|
164
|
+
enabled: channelConfig.enabled !== false,
|
|
165
|
+
};
|
|
166
|
+
},
|
|
167
|
+
defaultAccountId: (cfg) => {
|
|
168
|
+
const channelConfig = cfg?.channels?.xiaoyi;
|
|
169
|
+
if (!channelConfig || !channelConfig.enabled) {
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
// Single account mode: always return "default"
|
|
173
|
+
return "default";
|
|
174
|
+
},
|
|
175
|
+
isConfigured: (account, cfg) => {
|
|
176
|
+
// Safely check if all required fields are present and non-empty
|
|
177
|
+
if (!account || !account.config) {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
const config = account.config;
|
|
181
|
+
// Check each field is a string and has content after trimming
|
|
182
|
+
// Note: wsUrl1/wsUrl2 are optional (defaults will be used if not provided)
|
|
183
|
+
const hasAk = typeof config.ak === 'string' && config.ak.trim().length > 0;
|
|
184
|
+
const hasSk = typeof config.sk === 'string' && config.sk.trim().length > 0;
|
|
185
|
+
const hasAgentId = typeof config.agentId === 'string' && config.agentId.trim().length > 0;
|
|
186
|
+
return hasAk && hasSk && hasAgentId;
|
|
187
|
+
},
|
|
188
|
+
isEnabled: (account, cfg) => {
|
|
189
|
+
return account?.enabled !== false;
|
|
190
|
+
},
|
|
191
|
+
disabledReason: (account, cfg) => {
|
|
192
|
+
return "Channel is disabled in configuration";
|
|
193
|
+
},
|
|
194
|
+
unconfiguredReason: (account, cfg) => {
|
|
195
|
+
return "Missing required configuration: ak, sk, or agentId (wsUrl1/wsUrl2 are optional, defaults will be used)";
|
|
196
|
+
},
|
|
197
|
+
describeAccount: (account, cfg) => ({
|
|
198
|
+
accountId: account.accountId,
|
|
199
|
+
name: 'XiaoYi',
|
|
200
|
+
enabled: account.enabled,
|
|
201
|
+
configured: Boolean(account.config?.ak && account.config?.sk && account.config?.agentId),
|
|
202
|
+
}),
|
|
203
|
+
},
|
|
204
|
+
/**
|
|
205
|
+
* Gateway adapter - manage connections
|
|
206
|
+
* Using xy-monitor for message handling (xy_channel architecture)
|
|
207
|
+
*/
|
|
208
|
+
gateway: {
|
|
209
|
+
startAccount: async (ctx) => {
|
|
210
|
+
console.log("XiaoYi: startAccount() called - START");
|
|
211
|
+
const { monitorXYProvider } = await Promise.resolve().then(() => __importStar(require("./xy-monitor.js")));
|
|
212
|
+
const account = ctx.account;
|
|
213
|
+
const config = ctx.cfg;
|
|
214
|
+
console.log(`[xiaoyi] Starting xiaoyi channel with xy_monitor architecture`);
|
|
215
|
+
console.log(`[xiaoyi] Account ID: ${account.accountId}`);
|
|
216
|
+
console.log(`[xiaoyi] Agent ID: ${account.config.agentId}`);
|
|
217
|
+
return monitorXYProvider({
|
|
218
|
+
config: config,
|
|
219
|
+
runtime: ctx.runtime,
|
|
220
|
+
abortSignal: ctx.abortSignal,
|
|
221
|
+
accountId: account.accountId,
|
|
222
|
+
setStatus: ctx.setStatus,
|
|
223
|
+
});
|
|
224
|
+
},
|
|
225
|
+
stopAccount: async (ctx) => {
|
|
226
|
+
const runtime = (0, runtime_js_1.getXiaoYiRuntime)();
|
|
227
|
+
runtime.stop();
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
/**
|
|
231
|
+
* Outbound adapter - send messages via push
|
|
232
|
+
*/
|
|
233
|
+
outbound: {
|
|
234
|
+
deliveryMode: "direct",
|
|
235
|
+
textChunkLimit: 4000,
|
|
236
|
+
resolveTarget: ({ cfg, to, accountId, mode }) => {
|
|
237
|
+
if (!to || to.trim() === "") {
|
|
238
|
+
console.log(`[xiaoyi.resolveTarget] No target specified, using default push marker`);
|
|
239
|
+
return { ok: true, to: DEFAULT_PUSH_MARKER };
|
|
240
|
+
}
|
|
241
|
+
const trimmedTo = to.trim();
|
|
242
|
+
if (!trimmedTo.includes("::")) {
|
|
243
|
+
console.log(`[xiaoyi.resolveTarget] Target "${trimmedTo}" missing taskId, looking up session context`);
|
|
244
|
+
const sessionContext = (0, session_manager_js_1.getLatestSessionContext)();
|
|
245
|
+
if (sessionContext && sessionContext.sessionId === trimmedTo) {
|
|
246
|
+
const enhancedTarget = `${trimmedTo}::${sessionContext.taskId}`;
|
|
247
|
+
console.log(`[xiaoyi.resolveTarget] Enhanced target: ${enhancedTarget}`);
|
|
248
|
+
return { ok: true, to: enhancedTarget };
|
|
249
|
+
}
|
|
250
|
+
console.log(`[xiaoyi.resolveTarget] Could not find matching session context for "${trimmedTo}"`);
|
|
251
|
+
}
|
|
252
|
+
return { ok: true, to: trimmedTo };
|
|
253
|
+
},
|
|
254
|
+
sendText: async (ctx) => {
|
|
255
|
+
const { cfg, to, text, accountId } = ctx;
|
|
256
|
+
console.log(`[xiaoyi.sendText] Called with: to=${to}, textLength=${text?.length || 0}`);
|
|
257
|
+
const { resolveXYConfig } = await Promise.resolve().then(() => __importStar(require("./xy-config.js")));
|
|
258
|
+
const { XiaoYiPushService } = await Promise.resolve().then(() => __importStar(require("./push.js")));
|
|
259
|
+
const { configManager } = await Promise.resolve().then(() => __importStar(require("./xy-utils/config-manager.js")));
|
|
260
|
+
const config = { ...resolveXYConfig(cfg) };
|
|
261
|
+
// Resolve actual target (strip taskId portion if present)
|
|
262
|
+
let actualTo = to;
|
|
263
|
+
if (to === DEFAULT_PUSH_MARKER) {
|
|
264
|
+
actualTo = config.defaultSessionId || "";
|
|
265
|
+
}
|
|
266
|
+
else if (to.includes("::")) {
|
|
267
|
+
actualTo = to.split("::")[0];
|
|
268
|
+
}
|
|
269
|
+
// Override pushId with dynamic per-session pushId if available
|
|
270
|
+
const dynamicPushId = configManager.getPushId(actualTo);
|
|
271
|
+
if (dynamicPushId) {
|
|
272
|
+
config.pushId = dynamicPushId;
|
|
273
|
+
}
|
|
274
|
+
const pushService = new XiaoYiPushService(config);
|
|
275
|
+
// Extract title (first line, up to 57 chars)
|
|
276
|
+
const title = text.split("\n")[0].slice(0, 57);
|
|
277
|
+
// Truncate content to 1000 chars
|
|
278
|
+
const pushText = text.length > 1000 ? text.slice(0, 1000) : text;
|
|
279
|
+
await pushService.sendPush(pushText, title);
|
|
280
|
+
console.log(`[xiaoyi.sendText] Push sent successfully`);
|
|
281
|
+
return {
|
|
282
|
+
channel: "xiaoyi",
|
|
283
|
+
messageId: Date.now().toString(),
|
|
284
|
+
chatId: actualTo,
|
|
285
|
+
};
|
|
286
|
+
},
|
|
287
|
+
sendMedia: async (ctx) => {
|
|
288
|
+
throw new Error("暂不支持文件回传");
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
/**
|
|
292
|
+
* Messaging adapter - normalize targets
|
|
293
|
+
* In new openclaw version, normalizeTarget receives a string and returns a normalized string
|
|
294
|
+
*/
|
|
295
|
+
messaging: {
|
|
296
|
+
normalizeTarget: (raw) => {
|
|
297
|
+
// For XiaoYi, we use sessionId as the target
|
|
298
|
+
// The raw input is already the normalized target (sessionId)
|
|
299
|
+
return raw;
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
/**
|
|
303
|
+
* Status adapter - health checks
|
|
304
|
+
* Using buildAccountSnapshot for compatibility with new openclaw version
|
|
305
|
+
*/
|
|
306
|
+
status: {
|
|
307
|
+
buildAccountSnapshot: async (params) => {
|
|
308
|
+
const runtime = (0, runtime_js_1.getXiaoYiRuntime)();
|
|
309
|
+
const connection = runtime.getConnection();
|
|
310
|
+
if (!connection) {
|
|
311
|
+
return {
|
|
312
|
+
accountId: params.account.accountId,
|
|
313
|
+
state: "offline",
|
|
314
|
+
lastEventAt: Date.now(),
|
|
315
|
+
issues: [{
|
|
316
|
+
severity: "error",
|
|
317
|
+
message: "Not connected",
|
|
318
|
+
}],
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
const state = connection.getState();
|
|
322
|
+
if (state.connected && state.authenticated) {
|
|
323
|
+
return {
|
|
324
|
+
accountId: params.account.accountId,
|
|
325
|
+
state: "ready",
|
|
326
|
+
lastEventAt: Date.now(),
|
|
327
|
+
lastInboundAt: Date.now(),
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
else if (state.connected) {
|
|
331
|
+
return {
|
|
332
|
+
accountId: params.account.accountId,
|
|
333
|
+
state: "authenticating",
|
|
334
|
+
lastEventAt: Date.now(),
|
|
335
|
+
issues: [{
|
|
336
|
+
severity: "warning",
|
|
337
|
+
message: "Connected but not authenticated",
|
|
338
|
+
}],
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
return {
|
|
343
|
+
accountId: params.account.accountId,
|
|
344
|
+
state: "offline",
|
|
345
|
+
lastEventAt: Date.now(),
|
|
346
|
+
issues: [{
|
|
347
|
+
severity: "error",
|
|
348
|
+
message: `Reconnect attempts: ${state.reconnectAttempts}/${state.maxReconnectAttempts}`,
|
|
349
|
+
}],
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* XiaoYi configuration schema using Zod
|
|
4
|
+
* Defines the structure for XiaoYi A2A protocol configuration
|
|
5
|
+
*/
|
|
6
|
+
export declare const XiaoYiConfigSchema: z.ZodObject<{
|
|
7
|
+
/** Account name (optional display name) */
|
|
8
|
+
name: z.ZodOptional<z.ZodString>;
|
|
9
|
+
/** Whether this channel is enabled */
|
|
10
|
+
enabled: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
11
|
+
/** First WebSocket server URL */
|
|
12
|
+
wsUrl1: z.ZodDefault<z.ZodOptional<z.ZodString>>;
|
|
13
|
+
/** Second WebSocket server URL */
|
|
14
|
+
wsUrl2: z.ZodDefault<z.ZodOptional<z.ZodString>>;
|
|
15
|
+
/** Access Key for authentication */
|
|
16
|
+
ak: z.ZodOptional<z.ZodString>;
|
|
17
|
+
/** Secret Key for authentication */
|
|
18
|
+
sk: z.ZodOptional<z.ZodString>;
|
|
19
|
+
/** Agent ID for this XiaoYi agent */
|
|
20
|
+
agentId: z.ZodOptional<z.ZodString>;
|
|
21
|
+
/** Enable debug logging */
|
|
22
|
+
debug: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
23
|
+
/** Multi-account configuration */
|
|
24
|
+
accounts: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
25
|
+
}, "strip", z.ZodTypeAny, {
|
|
26
|
+
enabled?: boolean;
|
|
27
|
+
wsUrl1?: string;
|
|
28
|
+
wsUrl2?: string;
|
|
29
|
+
ak?: string;
|
|
30
|
+
sk?: string;
|
|
31
|
+
agentId?: string;
|
|
32
|
+
name?: string;
|
|
33
|
+
debug?: boolean;
|
|
34
|
+
accounts?: Record<string, unknown>;
|
|
35
|
+
}, {
|
|
36
|
+
enabled?: boolean;
|
|
37
|
+
wsUrl1?: string;
|
|
38
|
+
wsUrl2?: string;
|
|
39
|
+
ak?: string;
|
|
40
|
+
sk?: string;
|
|
41
|
+
agentId?: string;
|
|
42
|
+
name?: string;
|
|
43
|
+
debug?: boolean;
|
|
44
|
+
accounts?: Record<string, unknown>;
|
|
45
|
+
}>;
|
|
46
|
+
export type XiaoYiConfig = z.infer<typeof XiaoYiConfigSchema>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.XiaoYiConfigSchema = void 0;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
/**
|
|
6
|
+
* XiaoYi configuration schema using Zod
|
|
7
|
+
* Defines the structure for XiaoYi A2A protocol configuration
|
|
8
|
+
*/
|
|
9
|
+
exports.XiaoYiConfigSchema = zod_1.z.object({
|
|
10
|
+
/** Account name (optional display name) */
|
|
11
|
+
name: zod_1.z.string().optional(),
|
|
12
|
+
/** Whether this channel is enabled */
|
|
13
|
+
enabled: zod_1.z.boolean().optional().default(false),
|
|
14
|
+
/** First WebSocket server URL */
|
|
15
|
+
wsUrl1: zod_1.z.string().optional().default("wss://hag.cloud.huawei.com/openclaw/v1/ws/link"),
|
|
16
|
+
/** Second WebSocket server URL */
|
|
17
|
+
wsUrl2: zod_1.z.string().optional().default("wss://116.63.174.231/openclaw/v1/ws/link"),
|
|
18
|
+
/** Access Key for authentication */
|
|
19
|
+
ak: zod_1.z.string().optional(),
|
|
20
|
+
/** Secret Key for authentication */
|
|
21
|
+
sk: zod_1.z.string().optional(),
|
|
22
|
+
/** Agent ID for this XiaoYi agent */
|
|
23
|
+
agentId: zod_1.z.string().optional(),
|
|
24
|
+
/** Enable debug logging */
|
|
25
|
+
debug: zod_1.z.boolean().optional().default(false),
|
|
26
|
+
/** Multi-account configuration */
|
|
27
|
+
accounts: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).optional(),
|
|
28
|
+
});
|
|
@@ -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,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple file and image handler for XiaoYi Channel
|
|
3
|
+
* Handles downloading and extracting content from URIs
|
|
4
|
+
*/
|
|
5
|
+
export interface InputImageContent {
|
|
6
|
+
type: "image";
|
|
7
|
+
data: string;
|
|
8
|
+
mimeType: string;
|
|
9
|
+
}
|
|
10
|
+
export interface ImageLimits {
|
|
11
|
+
allowUrl: boolean;
|
|
12
|
+
allowedMimes: Set<string>;
|
|
13
|
+
maxBytes: number;
|
|
14
|
+
maxRedirects: number;
|
|
15
|
+
timeoutMs: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Extract image content from URL
|
|
19
|
+
*/
|
|
20
|
+
export declare function extractImageFromUrl(url: string, limits?: Partial<ImageLimits>): Promise<InputImageContent>;
|
|
21
|
+
/**
|
|
22
|
+
* Extract text content from URL (for text-based files)
|
|
23
|
+
*/
|
|
24
|
+
export declare function extractTextFromUrl(url: string, maxBytes?: number, timeoutMs?: number): Promise<string>;
|
|
25
|
+
/**
|
|
26
|
+
* Check if a MIME type is an image
|
|
27
|
+
*/
|
|
28
|
+
export declare function isImageMimeType(mimeType: string | undefined): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Check if a MIME type is a PDF
|
|
31
|
+
*/
|
|
32
|
+
export declare function isPdfMimeType(mimeType: string | undefined): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Check if a MIME type is text-based
|
|
35
|
+
*/
|
|
36
|
+
export declare function isTextMimeType(mimeType: string | undefined): boolean;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Simple file and image handler for XiaoYi Channel
|
|
4
|
+
* Handles downloading and extracting content from URIs
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.extractImageFromUrl = extractImageFromUrl;
|
|
8
|
+
exports.extractTextFromUrl = extractTextFromUrl;
|
|
9
|
+
exports.isImageMimeType = isImageMimeType;
|
|
10
|
+
exports.isPdfMimeType = isPdfMimeType;
|
|
11
|
+
exports.isTextMimeType = isTextMimeType;
|
|
12
|
+
// Default limits
|
|
13
|
+
const DEFAULT_IMAGE_MIMES = new Set(["image/jpeg", "image/png", "image/gif", "image/webp"]);
|
|
14
|
+
const DEFAULT_MAX_BYTES = 10000000; // 10MB
|
|
15
|
+
const DEFAULT_TIMEOUT = 30000; // 30 seconds
|
|
16
|
+
const DEFAULT_MAX_REDIRECTS = 3;
|
|
17
|
+
/**
|
|
18
|
+
* Fetch content from URL with basic validation
|
|
19
|
+
*/
|
|
20
|
+
async function fetchFromUrl(url, maxBytes, timeoutMs) {
|
|
21
|
+
const controller = new AbortController();
|
|
22
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
23
|
+
try {
|
|
24
|
+
const response = await fetch(url, {
|
|
25
|
+
signal: controller.signal,
|
|
26
|
+
headers: { "User-Agent": "XiaoYi-Channel/1.0" },
|
|
27
|
+
});
|
|
28
|
+
if (!response.ok) {
|
|
29
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
30
|
+
}
|
|
31
|
+
// Check content-length header if available
|
|
32
|
+
const contentLength = response.headers.get("content-length");
|
|
33
|
+
if (contentLength) {
|
|
34
|
+
const size = parseInt(contentLength, 10);
|
|
35
|
+
if (size > maxBytes) {
|
|
36
|
+
throw new Error(`File too large: ${size} bytes (limit: ${maxBytes})`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
40
|
+
if (buffer.byteLength > maxBytes) {
|
|
41
|
+
throw new Error(`File too large: ${buffer.byteLength} bytes (limit: ${maxBytes})`);
|
|
42
|
+
}
|
|
43
|
+
// Detect MIME type
|
|
44
|
+
const contentType = response.headers.get("content-type");
|
|
45
|
+
const mimeType = contentType?.split(";")[0]?.trim() || "application/octet-stream";
|
|
46
|
+
return { buffer, mimeType };
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
clearTimeout(timeout);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Extract image content from URL
|
|
54
|
+
*/
|
|
55
|
+
async function extractImageFromUrl(url, limits) {
|
|
56
|
+
const finalLimits = {
|
|
57
|
+
allowUrl: limits?.allowUrl ?? true,
|
|
58
|
+
allowedMimes: limits?.allowedMimes ?? DEFAULT_IMAGE_MIMES,
|
|
59
|
+
maxBytes: limits?.maxBytes ?? DEFAULT_MAX_BYTES,
|
|
60
|
+
maxRedirects: limits?.maxRedirects ?? DEFAULT_MAX_REDIRECTS,
|
|
61
|
+
timeoutMs: limits?.timeoutMs ?? DEFAULT_TIMEOUT,
|
|
62
|
+
};
|
|
63
|
+
if (!finalLimits.allowUrl) {
|
|
64
|
+
throw new Error("URL sources are disabled");
|
|
65
|
+
}
|
|
66
|
+
const { buffer, mimeType } = await fetchFromUrl(url, finalLimits.maxBytes, finalLimits.timeoutMs);
|
|
67
|
+
if (!finalLimits.allowedMimes.has(mimeType)) {
|
|
68
|
+
throw new Error(`Unsupported image type: ${mimeType}`);
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
type: "image",
|
|
72
|
+
data: buffer.toString("base64"),
|
|
73
|
+
mimeType,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Extract text content from URL (for text-based files)
|
|
78
|
+
*/
|
|
79
|
+
async function extractTextFromUrl(url, maxBytes = 5000000, timeoutMs = 30000) {
|
|
80
|
+
const { buffer, mimeType } = await fetchFromUrl(url, maxBytes, timeoutMs);
|
|
81
|
+
// Only process text-based MIME types
|
|
82
|
+
const textMimes = ["text/plain", "text/markdown", "text/html", "text/csv", "application/json", "application/xml"];
|
|
83
|
+
if (!textMimes.some((tm) => mimeType.startsWith(tm) || mimeType === tm)) {
|
|
84
|
+
throw new Error(`Unsupported text type: ${mimeType}`);
|
|
85
|
+
}
|
|
86
|
+
// Try to decode as UTF-8
|
|
87
|
+
return buffer.toString("utf-8");
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Check if a MIME type is an image
|
|
91
|
+
*/
|
|
92
|
+
function isImageMimeType(mimeType) {
|
|
93
|
+
if (!mimeType)
|
|
94
|
+
return false;
|
|
95
|
+
return DEFAULT_IMAGE_MIMES.has(mimeType.toLowerCase());
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Check if a MIME type is a PDF
|
|
99
|
+
*/
|
|
100
|
+
function isPdfMimeType(mimeType) {
|
|
101
|
+
return mimeType?.toLowerCase() === "application/pdf" || false;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Check if a MIME type is text-based
|
|
105
|
+
*/
|
|
106
|
+
function isTextMimeType(mimeType) {
|
|
107
|
+
if (!mimeType)
|
|
108
|
+
return false;
|
|
109
|
+
const lower = mimeType.toLowerCase();
|
|
110
|
+
return (lower.startsWith("text/") ||
|
|
111
|
+
lower === "application/json" ||
|
|
112
|
+
lower === "application/xml");
|
|
113
|
+
}
|