@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.
Files changed (52) hide show
  1. package/README.md +207 -0
  2. package/dist/auth.d.ts +36 -0
  3. package/dist/auth.js +111 -0
  4. package/dist/channel.d.ts +189 -0
  5. package/dist/channel.js +354 -0
  6. package/dist/config-schema.d.ts +46 -0
  7. package/dist/config-schema.js +28 -0
  8. package/dist/file-download.d.ts +17 -0
  9. package/dist/file-download.js +69 -0
  10. package/dist/file-handler.d.ts +36 -0
  11. package/dist/file-handler.js +113 -0
  12. package/dist/index.d.ts +29 -0
  13. package/dist/index.js +49 -0
  14. package/dist/onboarding.d.ts +6 -0
  15. package/dist/onboarding.js +167 -0
  16. package/dist/push.d.ts +28 -0
  17. package/dist/push.js +135 -0
  18. package/dist/runtime.d.ts +191 -0
  19. package/dist/runtime.js +438 -0
  20. package/dist/types.d.ts +280 -0
  21. package/dist/types.js +8 -0
  22. package/dist/websocket.d.ts +219 -0
  23. package/dist/websocket.js +1068 -0
  24. package/dist/xiaoyi-media.d.ts +81 -0
  25. package/dist/xiaoyi-media.js +216 -0
  26. package/dist/xy-bot.d.ts +19 -0
  27. package/dist/xy-bot.js +277 -0
  28. package/dist/xy-client.d.ts +26 -0
  29. package/dist/xy-client.js +78 -0
  30. package/dist/xy-config.d.ts +18 -0
  31. package/dist/xy-config.js +37 -0
  32. package/dist/xy-formatter.d.ts +94 -0
  33. package/dist/xy-formatter.js +303 -0
  34. package/dist/xy-monitor.d.ts +17 -0
  35. package/dist/xy-monitor.js +194 -0
  36. package/dist/xy-parser.d.ts +49 -0
  37. package/dist/xy-parser.js +109 -0
  38. package/dist/xy-reply-dispatcher.d.ts +17 -0
  39. package/dist/xy-reply-dispatcher.js +308 -0
  40. package/dist/xy-tools/session-manager.d.ts +29 -0
  41. package/dist/xy-tools/session-manager.js +80 -0
  42. package/dist/xy-utils/config-manager.d.ts +26 -0
  43. package/dist/xy-utils/config-manager.js +61 -0
  44. package/dist/xy-utils/crypto.d.ts +8 -0
  45. package/dist/xy-utils/crypto.js +21 -0
  46. package/dist/xy-utils/logger.d.ts +6 -0
  47. package/dist/xy-utils/logger.js +37 -0
  48. package/dist/xy-utils/session.d.ts +34 -0
  49. package/dist/xy-utils/session.js +55 -0
  50. package/openclaw.plugin.json +9 -0
  51. package/package.json +73 -0
  52. package/xiaoyi.js +1 -0
@@ -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
+ }