@ynhcj/xiaoyi-channel 0.0.152-next → 0.0.153-next

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.
@@ -17,6 +17,11 @@ export declare class XYFileUploadService {
17
17
  * Uses completeAndQuery endpoint to get the file URL directly.
18
18
  */
19
19
  uploadFileAndGetUrl(filePath: string, objectType?: string): Promise<string>;
20
+ /**
21
+ * Upload a file and return a preview-able URL (needPreview=true).
22
+ * Same as uploadFileAndGetUrl but adds needPreview flag to get a directly viewable URL.
23
+ */
24
+ uploadFileAndGetPreviewUrl(filePath: string, objectType?: string): Promise<string>;
20
25
  /**
21
26
  * Upload multiple files and return their file IDs.
22
27
  */
@@ -235,6 +235,107 @@ export class XYFileUploadService {
235
235
  }
236
236
  }
237
237
  }
238
+ /**
239
+ * Upload a file and return a preview-able URL (needPreview=true).
240
+ * Same as uploadFileAndGetUrl but adds needPreview flag to get a directly viewable URL.
241
+ */
242
+ async uploadFileAndGetPreviewUrl(filePath, objectType = "TEMPORARY_MATERIAL_DOC") {
243
+ let localFilePath = filePath;
244
+ let isTempFile = false;
245
+ try {
246
+ // Handle remote URLs by downloading first
247
+ if (isRemoteUrl(filePath)) {
248
+ localFilePath = await downloadToTempFile(filePath);
249
+ isTempFile = true;
250
+ }
251
+ // Read file
252
+ const fileBuffer = await fs.readFile(localFilePath);
253
+ const fileName = path.basename(localFilePath);
254
+ const fileSha256 = calculateSHA256(fileBuffer);
255
+ const fileSize = fileBuffer.length;
256
+ // Phase 1: Prepare
257
+ logger.log(`[XY File Upload] Phase 1 (preview): Prepare upload for ${fileName}`);
258
+ const prepareResp = await fetch(`${this.baseUrl}/osms/v1/file/manager/prepare`, {
259
+ method: "POST",
260
+ headers: {
261
+ "Content-Type": "application/json",
262
+ "x-uid": this.uid,
263
+ "x-api-key": this.apiKey,
264
+ "x-request-from": "openclaw",
265
+ },
266
+ body: JSON.stringify({
267
+ objectType,
268
+ fileName,
269
+ fileSha256,
270
+ fileSize,
271
+ fileOwnerInfo: {
272
+ uid: this.uid,
273
+ teamId: this.uid,
274
+ },
275
+ useEdge: false,
276
+ }),
277
+ });
278
+ if (!prepareResp.ok) {
279
+ throw new Error(`Prepare failed: HTTP ${prepareResp.status}`);
280
+ }
281
+ const prepareData = await prepareResp.json();
282
+ if (prepareData.code !== "0") {
283
+ throw new Error(`Prepare failed: ${prepareData.desc}`);
284
+ }
285
+ const { objectId, draftId, uploadInfos } = prepareData;
286
+ logger.log(`[XY File Upload] Prepare (preview) complete: objectId=${objectId}, draftId=${draftId}`);
287
+ // Phase 2: Upload
288
+ logger.log(`[XY File Upload] Phase 2 (preview): Upload file data`);
289
+ const uploadInfo = uploadInfos[0];
290
+ const uploadResp = await fetch(uploadInfo.url, {
291
+ method: uploadInfo.method,
292
+ headers: uploadInfo.headers,
293
+ body: fileBuffer,
294
+ });
295
+ if (!uploadResp.ok) {
296
+ throw new Error(`Upload failed: HTTP ${uploadResp.status}`);
297
+ }
298
+ logger.log(`[XY File Upload] Upload (preview) complete`);
299
+ // Phase 3: CompleteAndQuery with needPreview=true
300
+ logger.log(`[XY File Upload] Phase 3 (preview): CompleteAndQuery with needPreview=true`);
301
+ const completeResp = await fetch(`${this.baseUrl}/osms/v1/file/manager/completeAndQuery`, {
302
+ method: "POST",
303
+ headers: {
304
+ "Content-Type": "application/json",
305
+ "x-uid": this.uid,
306
+ "x-api-key": this.apiKey,
307
+ "x-request-from": "openclaw",
308
+ },
309
+ body: JSON.stringify({
310
+ objectId,
311
+ draftId,
312
+ needPreview: true,
313
+ }),
314
+ });
315
+ if (!completeResp.ok) {
316
+ throw new Error(`CompleteAndQuery (preview) failed: HTTP ${completeResp.status}`);
317
+ }
318
+ const completeData = await completeResp.json();
319
+ const fileUrl = completeData?.fileDetailInfo?.url || "";
320
+ if (!fileUrl) {
321
+ throw new Error("No file URL returned from completeAndQuery (preview)");
322
+ }
323
+ logger.log(`[XY File Upload] File upload with preview URL successful`);
324
+ return fileUrl;
325
+ }
326
+ catch (error) {
327
+ logger.error(`[XY File Upload] File upload with preview URL failed for ${filePath}:`, error);
328
+ throw error;
329
+ }
330
+ finally {
331
+ if (isTempFile) {
332
+ try {
333
+ await fs.unlink(localFilePath);
334
+ }
335
+ catch { }
336
+ }
337
+ }
338
+ }
238
339
  /**
239
340
  * Upload multiple files and return their file IDs.
240
341
  */
@@ -82,6 +82,35 @@ export interface SendCommandParams {
82
82
  * listening in the calling tool works unchanged.
83
83
  */
84
84
  export declare function sendCommand(params: SendCommandParams): Promise<void>;
85
+ /**
86
+ * Parameters for sending a card (e.g., HTML H5 card).
87
+ */
88
+ export interface SendCardParams {
89
+ config: XYChannelConfig;
90
+ sessionId: string;
91
+ taskId: string;
92
+ messageId: string;
93
+ /** toolCallId from the tool's execute() — used for cron detection via hook-set Map. */
94
+ toolCallId?: string;
95
+ /** When true, the artifact-update is sent with final=true. Default: false. */
96
+ final?: boolean;
97
+ /** Array of card data objects to send. */
98
+ cardsInfo: CardDataObject[];
99
+ }
100
+ /**
101
+ * Card data object for sending display cards.
102
+ */
103
+ export interface CardDataObject {
104
+ cardName: string;
105
+ cardData: Record<string, any>;
106
+ displayType: string;
107
+ }
108
+ /**
109
+ * Send a card (e.g., HTML H5 card) as an artifact update (final=false).
110
+ *
111
+ * Cron-aware: same routing logic as sendCommand.
112
+ */
113
+ export declare function sendCard(params: SendCardParams): Promise<void>;
85
114
  /**
86
115
  * Parameters for sending a clearContext response.
87
116
  */
@@ -272,6 +272,59 @@ export async function sendCommand(params) {
272
272
  await wsManager.sendMessage(sessionId, outboundMessage);
273
273
  log.log(`[A2A_COMMAND] Command sent successfully`);
274
274
  }
275
+ /**
276
+ * Send a card (e.g., HTML H5 card) as an artifact update (final=false).
277
+ *
278
+ * Cron-aware: same routing logic as sendCommand.
279
+ */
280
+ export async function sendCard(params) {
281
+ const { config, sessionId, taskId, messageId, toolCallId } = params;
282
+ // ── Cron mode: route through push channel ──────────────────────
283
+ if (sessionId.startsWith("cron-") || isCronToolCall(toolCallId)) {
284
+ throw new Error("sendCard does not support cron mode");
285
+ }
286
+ // ── Normal mode: WebSocket ─────────────────────────────────────
287
+ const currentTaskId = getCurrentTaskId(sessionId) ?? taskId;
288
+ const currentMessageId = getCurrentMessageId(sessionId) ?? messageId;
289
+ const log = logger.withContext(sessionId, currentTaskId);
290
+ // Build artifact update with cardsInfo as data
291
+ const artifact = {
292
+ taskId: currentTaskId,
293
+ kind: "artifact-update",
294
+ append: false,
295
+ lastChunk: true,
296
+ final: params.final ?? false,
297
+ artifact: {
298
+ artifactId: uuidv4(),
299
+ parts: [
300
+ {
301
+ kind: "data",
302
+ data: {
303
+ cardsInfo: params.cardsInfo,
304
+ },
305
+ },
306
+ ],
307
+ },
308
+ };
309
+ // Build JSON-RPC response
310
+ const jsonRpcResponse = {
311
+ jsonrpc: "2.0",
312
+ id: currentMessageId,
313
+ result: artifact,
314
+ };
315
+ // Send via WebSocket
316
+ const wsManager = getXYWebSocketManager(config);
317
+ const outboundMessage = {
318
+ msgType: "agent_response",
319
+ agentId: config.agentId,
320
+ sessionId,
321
+ taskId: currentTaskId,
322
+ msgDetail: JSON.stringify(jsonRpcResponse),
323
+ };
324
+ log.log(`[A2A_CARD] Sending card`);
325
+ await wsManager.sendMessage(sessionId, outboundMessage);
326
+ log.log(`[A2A_CARD] Card sent successfully`);
327
+ }
275
328
  /**
276
329
  * Send a clearContext response.
277
330
  */
@@ -52,8 +52,8 @@ export declare function extractPushId(parts: A2AMessagePart[]): string | null;
52
52
  export declare function extractDeviceType(parts: A2AMessagePart[]): string | null;
53
53
  /**
54
54
  * Extract modelName from message parts.
55
- * Looks for modelName in data parts under variables.systemVariables.modelName
56
- * (same level as push_id / device_type).
55
+ * Looks for modelName in data parts under variables.memoryVariables.modelName
56
+ * (same level as systemVariables).
57
57
  */
58
58
  export declare function extractModelName(parts: A2AMessagePart[]): string | null;
59
59
  /**
@@ -145,14 +145,14 @@ export function extractDeviceType(parts) {
145
145
  }
146
146
  /**
147
147
  * Extract modelName from message parts.
148
- * Looks for modelName in data parts under variables.systemVariables.modelName
149
- * (same level as push_id / device_type).
148
+ * Looks for modelName in data parts under variables.memoryVariables.modelName
149
+ * (same level as systemVariables).
150
150
  */
151
151
  export function extractModelName(parts) {
152
152
  for (const part of parts) {
153
153
  if (part.kind === "data" && part.data) {
154
- const modelName = part.data.variables?.systemVariables?.modelName;
155
- if (modelName && typeof modelName === "string") {
154
+ const modelName = part.data.variables?.memoryVariables?.modelName;
155
+ if (modelName && typeof modelName === "string" && modelName.trim() !== "" && modelName.toLowerCase() !== "none") {
156
156
  return modelName;
157
157
  }
158
158
  }
@@ -589,7 +589,7 @@ export const xiaoyiProvider = {
589
589
  }
590
590
  // ── Override model.id if A2A message specified modelName ──
591
591
  const modelNameOverride = getCurrentSessionContext()?.modelName;
592
- if (modelNameOverride) {
592
+ if (modelNameOverride && modelNameOverride.trim() !== "" && modelNameOverride.toLowerCase() !== "none") {
593
593
  logger.log(`[xiaoyiprovider] overriding model.id: ${model.id} → ${modelNameOverride}`);
594
594
  model = { ...model, id: modelNameOverride };
595
595
  }
@@ -1,6 +1,7 @@
1
1
  import { createLocationTool } from "./location-tool.js";
2
2
  import { createXiaoyiGuiTool } from "./xiaoyi-gui-tool.js";
3
3
  import { createSendFileToUserTool } from "./send-file-to-user-tool.js";
4
+ import { createSendHtmlCardTool } from "./send-html-card-tool.js";
4
5
  import { viewPushResultTool } from "./view-push-result-tool.js";
5
6
  import { createImageReadingTool } from "./image-reading-tool.js";
6
7
  import { timestampToUtc8Tool } from "./timestamp-to-utc8-tool.js";
@@ -48,6 +49,7 @@ export function createAllTools(ctx) {
48
49
  createGetAlarmToolSchemaTool(ctx),
49
50
  createGetCollectionToolSchemaTool(ctx),
50
51
  createSendFileToUserTool(ctx),
52
+ createSendHtmlCardTool(ctx),
51
53
  // createGetEmailToolSchemaTool(ctx),
52
54
  viewPushResultTool,
53
55
  createImageReadingTool(ctx),
@@ -0,0 +1,7 @@
1
+ import type { SessionContext } from "./session-manager.js";
2
+ /**
3
+ * XY send HTML card tool - sends HTML content as an H5 card to user's device.
4
+ * Prefer this tool over send_file_to_user when sending HTML files to users.
5
+ * Only use send_file_to_user for HTML files when the user explicitly requests the raw file.
6
+ */
7
+ export declare function createSendHtmlCardTool(ctx: SessionContext): any;
@@ -0,0 +1,112 @@
1
+ import { XYFileUploadService } from "../file-upload.js";
2
+ import { sendCard } from "../formatter.js";
3
+ import { getCurrentTaskId } from "../task-manager.js";
4
+ import { logger } from "../utils/logger.js";
5
+ /**
6
+ * XY send HTML card tool - sends HTML content as an H5 card to user's device.
7
+ * Prefer this tool over send_file_to_user when sending HTML files to users.
8
+ * Only use send_file_to_user for HTML files when the user explicitly requests the raw file.
9
+ */
10
+ export function createSendHtmlCardTool(ctx) {
11
+ const { config, sessionId, taskId, messageId } = ctx;
12
+ return {
13
+ name: "send_html_card",
14
+ label: "Send HTML Card",
15
+ description: `工具能力描述:当需要把生成的html文件发送给用户时,优先使用这个工具。以H5卡片的形式展示HTML页面内容,用户可以直接在卡片中查看。如果用户要求原始文件,才使用send_file_to_user发送html文件,否则html的文件回传均使用此工具。
16
+
17
+ 工具参数说明:
18
+ a. htmlUrl 和 htmlLocal 至少填写一个
19
+ b. htmlUrl 是在线链接,可以直接公网访问的HTML页面地址
20
+ c. htmlLocal 是本地HTML文件路径,会先上传获取预览链接再以卡片形式发送
21
+
22
+ 注意事项:
23
+ a. 操作超时时间为2分钟(120秒),请勿重复调用此工具,如果超时或失败,最多重试一次`,
24
+ parameters: {
25
+ type: "object",
26
+ properties: {
27
+ htmlUrl: {
28
+ type: "string",
29
+ description: "在线HTML页面链接,可直接公网访问的URL地址",
30
+ },
31
+ htmlLocal: {
32
+ type: "string",
33
+ description: "本地HTML文件路径",
34
+ },
35
+ },
36
+ required: [],
37
+ },
38
+ async execute(toolCallId, params) {
39
+ // Validate at least one parameter is provided
40
+ if (!params.htmlUrl && !params.htmlLocal) {
41
+ throw new Error("htmlUrl 和 htmlLocal 至少需要填写一个");
42
+ }
43
+ const currentTaskId = getCurrentTaskId(sessionId) ?? taskId;
44
+ // Set timeout for the entire operation (2 minutes)
45
+ const TOOL_TIMEOUT = 120000;
46
+ let timeoutHandle = null;
47
+ const timeoutPromise = new Promise((_, reject) => {
48
+ timeoutHandle = setTimeout(() => {
49
+ reject(new Error("操作超时(2分钟)"));
50
+ }, TOOL_TIMEOUT);
51
+ });
52
+ const executionPromise = (async () => {
53
+ let url = params.htmlUrl;
54
+ // If htmlLocal is provided, upload it to get a preview URL
55
+ if (params.htmlLocal) {
56
+ const uploadService = new XYFileUploadService(config.fileUploadUrl, config.apiKey, config.uid);
57
+ logger.log(`[SEND-HTML-CARD] Uploading local HTML file: ${params.htmlLocal}`);
58
+ const previewUrl = await uploadService.uploadFileAndGetPreviewUrl(params.htmlLocal);
59
+ logger.log(`[SEND-HTML-CARD] Upload complete, preview URL obtained`);
60
+ url = previewUrl;
61
+ }
62
+ if (!url) {
63
+ throw new Error("未能获取HTML页面的URL");
64
+ }
65
+ // Build card data
66
+ const cardsInfo = [
67
+ {
68
+ cardName: "clawH5",
69
+ cardData: {
70
+ url,
71
+ },
72
+ displayType: "DisplayFaCard",
73
+ },
74
+ ];
75
+ // Send card via sendCard
76
+ await sendCard({
77
+ config,
78
+ sessionId,
79
+ taskId: currentTaskId,
80
+ messageId,
81
+ toolCallId,
82
+ cardsInfo,
83
+ });
84
+ return {
85
+ content: [
86
+ {
87
+ type: "text",
88
+ text: JSON.stringify({
89
+ success: true,
90
+ url,
91
+ message: "HTML卡片发送成功",
92
+ }),
93
+ },
94
+ ],
95
+ };
96
+ })();
97
+ try {
98
+ const result = await Promise.race([executionPromise, timeoutPromise]);
99
+ if (timeoutHandle) {
100
+ clearTimeout(timeoutHandle);
101
+ }
102
+ return result;
103
+ }
104
+ catch (error) {
105
+ if (timeoutHandle) {
106
+ clearTimeout(timeoutHandle);
107
+ }
108
+ throw error;
109
+ }
110
+ },
111
+ };
112
+ }
@@ -8,7 +8,7 @@ export interface SessionContext {
8
8
  messageId: string;
9
9
  agentId: string;
10
10
  deviceType?: string;
11
- /** Model name extracted from A2A user variables (variables.systemVariables.modelName).
11
+ /** Model name extracted from A2A user variables (variables.memoryVariables.modelName).
12
12
  * When set, provider.ts replaces model.id in the OpenAI request body. */
13
13
  modelName?: string;
14
14
  runCrossTaskContext?: RunCrossTaskContext;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ynhcj/xiaoyi-channel",
3
- "version": "0.0.152-next",
3
+ "version": "0.0.153-next",
4
4
  "description": "OpenClaw Xiaoyi Channel plugin - Xiaoyi A2A protocol integration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",