@ynhcj/xiaoyi-channel 1.1.16 → 1.1.17

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/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
2
2
  import { xyPlugin } from "./src/channel.js";
3
+ import { xiaoyiProvider } from "./src/provider.js";
3
4
  import { setXYRuntime } from "./src/runtime.js";
4
5
  import { tryInjectSteer } from "./src/steer-injector.js";
5
6
  import { callCsplApi } from "./src/cspl/call-api.js";
@@ -18,13 +19,14 @@ const plugin = {
18
19
  register(api) {
19
20
  setXYRuntime(api.runtime);
20
21
  api.registerChannel({ plugin: xyPlugin });
21
- // CSPL after_tool_call hook: 监听工具结果,发送至 CSPL API 进行安全检测
22
+ api.registerProvider(xiaoyiProvider);
23
+ // SENTINEL HOOK after_tool_call hook: 监听工具结果,发送至安全检测 API 进行安全检测
22
24
  // 如果响应为 REJECT,注入 steer 消息中止当前对话
23
25
  api.on("after_tool_call", async (event, ctx) => {
24
26
  if (!ALLOWED_TOOLS.includes(event.toolName)) {
25
27
  return;
26
28
  }
27
- console.log(`[CSPL] after_tool_call triggered: toolName=${event.toolName}, sessionKey=${ctx.sessionKey ?? "none"}`);
29
+ console.log(`[SENTINEL HOOK] after_tool_call triggered: toolName=${event.toolName}, sessionKey=${ctx.sessionKey ?? "none"}`);
28
30
  try {
29
31
  const resultText = extractResultText(event, event.toolName);
30
32
  const resultLength = resultText.length;
@@ -33,6 +35,7 @@ const plugin = {
33
35
  }
34
36
  // 构造 sentinel_hook 格式的 payload: { tool, output: [{ content }] }
35
37
  const questionText = {
38
+ subSceneID: 'TOOL_OUTPUT',
36
39
  tool: event.toolName,
37
40
  output: [{ content: "" }],
38
41
  };
@@ -47,13 +50,13 @@ const plugin = {
47
50
  }
48
51
  const response = await callCsplApi(finalJson, api.config);
49
52
  const result = parseSecurityResult(response);
50
- console.log(`[CSPL] Security result: status=${result.status}`);
53
+ console.log(`[SENTINEL HOOK] Security result: status=${result.status}`);
51
54
  if (result.status === "REJECT") {
52
55
  await tryInjectSteer(ctx.sessionKey, STEER_ABORT_MESSAGE);
53
56
  }
54
57
  }
55
58
  catch (err) {
56
- api.logger.error(`[CSPL] after_tool_call error: ${err}`);
59
+ api.logger.error(`[SENTINEL HOOK] after_tool_call error: ${err}`);
57
60
  }
58
61
  });
59
62
  },
@@ -24,11 +24,11 @@ import { sendFileToUserTool } from "./tools/send-file-to-user-tool.js";
24
24
  import { viewPushResultTool } from "./tools/view-push-result-tool.js";
25
25
  import { imageReadingTool } from "./tools/image-reading-tool.js";
26
26
  import { timestampToUtc8Tool } from "./tools/timestamp-to-utc8-tool.js";
27
- import { sendCommandToCarTool } from "./tools/send-command-to-car-tool.js";
28
27
  import { xiaoyiCollectionTool } from "./tools/xiaoyi-collection-tool.js";
29
28
  import { xiaoyiAddCollectionTool } from "./tools/xiaoyi-add-collection-tool.js";
30
29
  import { xiaoyiDeleteCollectionTool } from "./tools/xiaoyi-delete-collection-tool.js";
31
30
  import { saveMediaToGalleryTool } from "./tools/save-media-to-gallery-tool.js";
31
+ import { saveFileToPhoneTool } from "./tools/save-file-to-phone-tool.js";
32
32
  import { filterToolsByDevice } from "./tools/device-tool-map.js";
33
33
  import { getCurrentSessionContext } from "./tools/session-manager.js";
34
34
  import { logger } from "./utils/logger.js";
@@ -71,7 +71,7 @@ export const xyPlugin = {
71
71
  },
72
72
  outbound: xyOutbound,
73
73
  agentTools: () => {
74
- const allTools = [locationTool, noteTool, searchNoteTool, modifyNoteTool, calendarTool, searchCalendarTool, searchContactTool, searchPhotoGalleryTool, uploadPhotoTool, xiaoyiGuiTool, callPhoneTool, searchMessageTool, sendMessageTool, searchFileTool, uploadFileTool, createAlarmTool, searchAlarmTool, modifyAlarmTool, deleteAlarmTool, sendFileToUserTool, viewPushResultTool, imageReadingTool, timestampToUtc8Tool, sendCommandToCarTool, xiaoyiCollectionTool, xiaoyiAddCollectionTool, xiaoyiDeleteCollectionTool, saveMediaToGalleryTool];
74
+ const allTools = [locationTool, noteTool, searchNoteTool, modifyNoteTool, calendarTool, searchCalendarTool, searchContactTool, searchPhotoGalleryTool, uploadPhotoTool, xiaoyiGuiTool, callPhoneTool, searchMessageTool, sendMessageTool, searchFileTool, uploadFileTool, createAlarmTool, searchAlarmTool, modifyAlarmTool, deleteAlarmTool, sendFileToUserTool, viewPushResultTool, imageReadingTool, timestampToUtc8Tool, xiaoyiCollectionTool, xiaoyiAddCollectionTool, xiaoyiDeleteCollectionTool, saveMediaToGalleryTool, saveFileToPhoneTool];
75
75
  const ctx = getCurrentSessionContext();
76
76
  const filtered = filterToolsByDevice(allTools, ctx?.deviceType);
77
77
  logger.log(`[DEVICE-FILTER] deviceType=${ctx?.deviceType ?? "(none)"}, tools: ${allTools.length} → ${filtered.length} (${filtered.map(t => t.name).join(", ")})`);
@@ -1,4 +1,4 @@
1
- // CSPL API 请求模块
1
+ // SENTINEL HOOK API 请求模块
2
2
  import https from "node:https";
3
3
  import { URL } from "node:url";
4
4
  import { randomBytes } from "node:crypto";
@@ -8,8 +8,10 @@ function generateTraceId() {
8
8
  return randomBytes(16).toString("hex");
9
9
  }
10
10
  function buildHeaders(config) {
11
+ const traceId = generateTraceId();
12
+ console.log(`[SENTINEL HOOK] trace-id: ${traceId}`);
11
13
  return {
12
- "x-hag-trace-id": generateTraceId(),
14
+ "x-hag-trace-id": traceId,
13
15
  "x-uid": config.uid,
14
16
  "x-api-key": config.apiKey,
15
17
  "x-request-from": config.requestFrom,
@@ -30,13 +32,13 @@ function buildRequestOptions(url, headers, timeout) {
30
32
  }
31
33
  function parseResponse(data) {
32
34
  if (!data?.trim())
33
- throw new Error("[CSPL] API response is empty");
35
+ throw new Error("[SENTINEL HOOK] API response is empty");
34
36
  const json = JSON.parse(data);
35
37
  if (json.retCode && json.retCode !== "0") {
36
- throw new Error(`[CSPL] API error: ${json.retMsg || "unknown"}`);
38
+ throw new Error(`[SENTINEL HOOK] API error: ${json.retMsg || "unknown"}`);
37
39
  }
38
40
  if (!json.retCode && json.code) {
39
- throw new Error(`[CSPL] Backend error: ${json.desc || "unknown"}`);
41
+ throw new Error(`[SENTINEL HOOK] Backend error: ${json.desc || "unknown"}`);
40
42
  }
41
43
  return json;
42
44
  }
@@ -47,12 +49,13 @@ export async function callCsplApi(questionText, cfg) {
47
49
  questionText,
48
50
  textSource: config.textSource,
49
51
  action: config.action,
52
+ extra: JSON.stringify({ userId: config.uid }),
50
53
  };
51
54
  return new Promise((resolve, reject) => {
52
55
  const options = buildRequestOptions(config.api.url, headers, config.api.timeout);
53
56
  const req = https.request(options, (res) => {
54
57
  if (res.statusCode && res.statusCode >= HTTP_STATUS_BAD_REQUEST) {
55
- reject(new Error(`[CSPL] HTTP error: ${res.statusCode}`));
58
+ reject(new Error(`[SENTINEL HOOK] HTTP error: ${res.statusCode}`));
56
59
  return;
57
60
  }
58
61
  let data = "";
@@ -62,23 +65,23 @@ export async function callCsplApi(questionText, cfg) {
62
65
  res.on("end", () => {
63
66
  try {
64
67
  const result = parseResponse(data);
65
- console.log(`[CSPL API] ✅ 请求成功`);
68
+ console.log(`[SENTINEL HOOK] ✅ 请求成功`);
66
69
  resolve(result);
67
70
  }
68
71
  catch (e) {
69
- console.error(`[CSPL API] ❌ 请求失败: ${e instanceof Error ? e.message : String(e)}`);
72
+ console.error(`[SENTINEL HOOK] ❌ 请求失败: ${e instanceof Error ? e.message : String(e)}`);
70
73
  reject(e);
71
74
  }
72
75
  });
73
76
  });
74
77
  req.on("error", (error) => {
75
- console.error(`[CSPL API] ❌ 请求错误: ${error instanceof Error ? error.message : String(error)}`);
78
+ console.error(`[SENTINEL HOOK] ❌ 请求错误: ${error instanceof Error ? error.message : String(error)}`);
76
79
  reject(error);
77
80
  });
78
81
  req.on("timeout", () => {
79
- console.error(`[CSPL API] ⏰ 请求超时 (${config.api.timeout}ms)`);
82
+ console.error(`[SENTINEL HOOK] ⏰ 请求超时 (${config.api.timeout}ms)`);
80
83
  req.destroy();
81
- reject(new Error("[CSPL] Request timeout"));
84
+ reject(new Error("[SENTINEL HOOK] Request timeout"));
82
85
  });
83
86
  req.write(JSON.stringify(payload));
84
87
  req.end();
@@ -7,7 +7,7 @@ import { logger } from "../utils/logger.js";
7
7
  let cachedConfig = null;
8
8
  function readServiceUrl() {
9
9
  if (!fs.existsSync(ENV_FILE_PATH)) {
10
- throw new Error(`[CSPL] Environment file not found: ${ENV_FILE_PATH}`);
10
+ throw new Error(`[SENTINEL HOOK] Environment file not found: ${ENV_FILE_PATH}`);
11
11
  }
12
12
  const envData = fs.readFileSync(ENV_FILE_PATH, "utf-8");
13
13
  for (const line of envData.split("\n")) {
@@ -22,7 +22,7 @@ function readServiceUrl() {
22
22
  if (key === "SERVICE_URL" && value)
23
23
  return value;
24
24
  }
25
- throw new Error("[CSPL] Missing SERVICE_URL in env file");
25
+ throw new Error("[SENTINEL HOOK] Missing SERVICE_URL in env file");
26
26
  }
27
27
  /**
28
28
  * 构建 CSPL 配置。uid 和 apiKey 复用 XYChannelConfig,避免重复配置。
@@ -45,6 +45,6 @@ export function getCsplConfig(cfg) {
45
45
  textSource: CSPL_STATIC_CONFIG.textSource,
46
46
  action: CSPL_STATIC_CONFIG.action,
47
47
  };
48
- logger.log("[CSPL] Config loaded (uid/apiKey from XYChannelConfig)");
48
+ logger.log("[SENTINEL HOOK] Config loaded (uid/apiKey from XYChannelConfig)");
49
49
  return cachedConfig;
50
50
  }
@@ -10,6 +10,7 @@ export interface ApiPayload {
10
10
  questionText: string;
11
11
  textSource: string;
12
12
  action: string;
13
+ extra: string;
13
14
  }
14
15
  export interface ApiResponse {
15
16
  data?: {
@@ -1,20 +1,27 @@
1
+ // Xiaoyi Provider
2
+ // Wraps any OpenAI-compatible endpoint and injects dynamic headers
3
+ // (taskId, sessionId, conversationId) from the current XY channel session.
4
+ // Falls back to uid-based values when no session context is available.
5
+ //
6
+ // Users configure the underlying model in config:
7
+ // models.providers.xiaoyiprovider.baseUrl = "https://..."
8
+ // models.providers.xiaoyiprovider.api = "openai-completions"
9
+ // models.providers.xiaoyiprovider.models = [...]
10
+ import { createHash } from "crypto";
1
11
  import { getCurrentSessionContext } from "./tools/session-manager.js";
2
12
  /**
3
13
  * Dynamic header keys injected via extraParams and forwarded to the HTTP request.
4
14
  * Correspond to the three fields written to .xiaoyiruntime:
5
15
  * TASK_ID, SESSION_ID, CONVERSATION_ID
6
16
  */
7
- const HEADER_TASK_ID = "x-task-id";
17
+ const HEADER_TRACE_ID = "x-hag-trace-id";
8
18
  const HEADER_SESSION_ID = "x-session-id";
9
- const HEADER_CONVERSATION_ID = "x-conversation-id";
10
- const EXTRA_PARAM_TASK_ID = "x-task-id";
11
- const EXTRA_PARAM_SESSION_ID = "x-session-id";
12
- const EXTRA_PARAM_CONVERSATION_ID = "x-conversation-id";
19
+ const HEADER_INTERACTION_ID = "x-interaction-id";
13
20
  /**
14
- * Encode uid to base64 and take first 32 chars.
21
+ * Encode uid via SHA-256 and take first 32 hex chars.
15
22
  */
16
23
  function encodeUid(uid) {
17
- return Buffer.from(uid).toString("base64").slice(0, 32);
24
+ return createHash("sha256").update(uid).digest("hex").slice(0, 32);
18
25
  }
19
26
  /**
20
27
  * Get uid from plugin config (OpenClawConfig -> plugins -> xiaoyi-channel -> config).
@@ -27,23 +34,27 @@ export const xiaoyiProvider = {
27
34
  label: "Xiaoyi Provider",
28
35
  docsPath: "/providers/models",
29
36
  auth: [],
37
+ isCacheTtlEligible: () => true,
30
38
  /**
31
39
  * Inject dynamic session params into extraParams so they flow
32
40
  * through to wrapStreamFn's ctx.extraParams.
33
41
  *
34
42
  * Priority:
35
43
  * 1. Session context (from AsyncLocalStorage, set by bot.ts)
36
- * 2. uid-based fallback: base64(uid)[:32]_timestamp
44
+ * 2. uid-based fallback: sha256(uid).hex[:32]_timestamp
37
45
  * 3. No uid available → return undefined (no headers injected)
38
46
  */
39
47
  prepareExtraParams: (ctx) => {
40
48
  const sessionCtx = getCurrentSessionContext();
41
49
  if (sessionCtx) {
50
+ const taskId = sessionCtx.taskId;
51
+ const sessionId = taskId.split("&")[0];
52
+ const interactionId = taskId.split("&")[1] || "";
42
53
  return {
43
54
  ...ctx.extraParams,
44
- [EXTRA_PARAM_TASK_ID]: sessionCtx.taskId,
45
- [EXTRA_PARAM_SESSION_ID]: sessionCtx.sessionId,
46
- [EXTRA_PARAM_CONVERSATION_ID]: sessionCtx.messageId,
55
+ [HEADER_TRACE_ID]: taskId,
56
+ [HEADER_SESSION_ID]: sessionId,
57
+ [HEADER_INTERACTION_ID]: interactionId,
47
58
  };
48
59
  }
49
60
  // Fallback: uid-based values
@@ -55,9 +66,9 @@ export const xiaoyiProvider = {
55
66
  const fallbackValue = `${prefix}_${ts}`;
56
67
  return {
57
68
  ...ctx.extraParams,
58
- [EXTRA_PARAM_TASK_ID]: fallbackValue,
59
- [EXTRA_PARAM_SESSION_ID]: fallbackValue,
60
- [EXTRA_PARAM_CONVERSATION_ID]: fallbackValue,
69
+ [HEADER_TRACE_ID]: fallbackValue,
70
+ [HEADER_SESSION_ID]: fallbackValue,
71
+ [HEADER_INTERACTION_ID]: fallbackValue,
61
72
  };
62
73
  },
63
74
  /**
@@ -71,27 +82,25 @@ export const xiaoyiProvider = {
71
82
  const underlying = ctx.streamFn;
72
83
  if (!underlying)
73
84
  return underlying;
74
- const dynamicHeaders = {};
75
- if (ctx.extraParams) {
76
- const taskId = ctx.extraParams[EXTRA_PARAM_TASK_ID];
77
- const sessionId = ctx.extraParams[EXTRA_PARAM_SESSION_ID];
78
- const conversationId = ctx.extraParams[EXTRA_PARAM_CONVERSATION_ID];
79
- if (typeof taskId === "string")
80
- dynamicHeaders[HEADER_TASK_ID] = taskId;
81
- if (typeof sessionId === "string")
82
- dynamicHeaders[HEADER_SESSION_ID] = sessionId;
83
- if (typeof conversationId === "string")
84
- dynamicHeaders[HEADER_CONVERSATION_ID] = conversationId;
85
- }
86
- if (Object.keys(dynamicHeaders).length === 0)
87
- return underlying;
88
85
  return async (model, context, options) => {
86
+ // 每次请求时从 ctx.extraParams 动态读取 header
87
+ const dynamicHeaders = {};
88
+ if (ctx.extraParams) {
89
+ const traceId = ctx.extraParams[HEADER_TRACE_ID];
90
+ const sessionId = ctx.extraParams[HEADER_SESSION_ID];
91
+ const interactionId = ctx.extraParams[HEADER_INTERACTION_ID];
92
+ if (typeof traceId === "string")
93
+ dynamicHeaders[HEADER_TRACE_ID] = traceId;
94
+ if (typeof sessionId === "string")
95
+ dynamicHeaders[HEADER_SESSION_ID] = sessionId;
96
+ if (typeof interactionId === "string")
97
+ dynamicHeaders[HEADER_INTERACTION_ID] = interactionId;
98
+ }
89
99
  // 记录输入
90
- console.log(`[xiaoyiprovider] input messages count: ${context.messages.length}`);
100
+ console.log(`[xiaoyiprovider] input messages count: ${context.messages?.length ?? 0}`);
91
101
  if (context.systemPrompt) {
92
102
  console.log(`[xiaoyiprovider] system prompt length: ${context.systemPrompt.length}`);
93
103
  }
94
- console.log(`[xiaoyiprovider] headers: ${JSON.stringify(dynamicHeaders)}`);
95
104
  const stream = await underlying(model, context, {
96
105
  ...options,
97
106
  headers: {
@@ -100,7 +109,7 @@ export const xiaoyiProvider = {
100
109
  },
101
110
  });
102
111
  // 异步监听输出(不阻塞 stream 返回)
103
- stream.result().then((msg) => console.log(`[xiaoyiprovider] output: ${JSON.stringify(msg)}`), (err) => console.log(`[xiaoyiprovider] error: ${err}`));
112
+ stream.result().then((err) => console.log(`[xiaoyiprovider] error: ${err}`));
104
113
  return stream;
105
114
  };
106
115
  },
@@ -13,7 +13,6 @@ const DEVICE_TOOL_POLICY = {
13
13
  "call_phone",
14
14
  "send_message",
15
15
  "search_message",
16
- "send_command_to_car",
17
16
  "search_contact",
18
17
  "QueryCollection",
19
18
  "AddCollection",
@@ -0,0 +1,5 @@
1
+ /**
2
+ * XY find PC devices tool - finds all PC devices associated with the user.
3
+ * Returns device IDs for use in subsequent file search operations.
4
+ */
5
+ export declare const findPcDevicesTool: any;
@@ -0,0 +1,98 @@
1
+ import { getXYWebSocketManager } from "../client.js";
2
+ import { sendCommand } from "../formatter.js";
3
+ import { getCurrentSessionContext } from "./session-manager.js";
4
+ /**
5
+ * XY find PC devices tool - finds all PC devices associated with the user.
6
+ * Returns device IDs for use in subsequent file search operations.
7
+ */
8
+ export const findPcDevicesTool = {
9
+ name: "find_pc_devices",
10
+ label: "Find PC Devices",
11
+ description: `查找用户所有PC/电脑设备,获取设备ID列表。当用户说"帮我找一下PC/电脑上的xxx文件"、"帮我搜索电脑上的xxx"等涉及PC设备的请求时,先调用此工具获取设备ID,再进行后续操作。注意:操作超时时间为60秒,请勿重复调用此工具,如果超时或失败,最多重试一次。回复约束:如果工具返回没有授权或者其他报错,只需要完整描述没有授权或者其他报错内容即可,不需要主动给用户提供解决方案,请严格遵守。`,
12
+ parameters: {
13
+ type: "object",
14
+ properties: {},
15
+ required: [],
16
+ },
17
+ async execute(toolCallId, params) {
18
+ // Get session context
19
+ const sessionContext = getCurrentSessionContext();
20
+ if (!sessionContext) {
21
+ throw new Error("No active XY session found. Find PC devices tool can only be used during an active conversation.");
22
+ }
23
+ const { config, sessionId, taskId, messageId } = sessionContext;
24
+ // Get WebSocket manager
25
+ const wsManager = getXYWebSocketManager(config);
26
+ // Build GetAllDevice command
27
+ const command = {
28
+ header: {
29
+ namespace: "Common",
30
+ name: "Action",
31
+ },
32
+ payload: {
33
+ cardParam: {},
34
+ executeParam: {
35
+ achieveType: "INTENT",
36
+ actionResponse: true,
37
+ bundleName: "com.huawei.hmos.aidispatchservice",
38
+ dimension: "",
39
+ executeMode: "background",
40
+ intentName: "GetAllDevice",
41
+ intentParam: {},
42
+ needUnlock: true,
43
+ permissionId: [],
44
+ timeOut: 5,
45
+ },
46
+ needUploadResult: true,
47
+ pageControlRelated: false,
48
+ responses: [{
49
+ displayText: "",
50
+ resultCode: "",
51
+ ttsText: "",
52
+ }],
53
+ },
54
+ };
55
+ // Send command and wait for response (60 second timeout)
56
+ return new Promise((resolve, reject) => {
57
+ const timeout = setTimeout(() => {
58
+ wsManager.off("data-event", handler);
59
+ reject(new Error("查找PC设备超时(60秒)"));
60
+ }, 60000);
61
+ // Listen for data events from WebSocket
62
+ const handler = (event) => {
63
+ if (event.intentName === "GetAllDevice") {
64
+ clearTimeout(timeout);
65
+ wsManager.off("data-event", handler);
66
+ if (event.status === "success" && event.outputs) {
67
+ resolve({
68
+ content: [
69
+ {
70
+ type: "text",
71
+ text: JSON.stringify(event.outputs),
72
+ }
73
+ ]
74
+ });
75
+ }
76
+ else {
77
+ reject(new Error(`查找PC设备失败: ${event.status}`));
78
+ }
79
+ }
80
+ };
81
+ // Register event handler
82
+ wsManager.on("data-event", handler);
83
+ // Send the command
84
+ sendCommand({
85
+ config,
86
+ sessionId,
87
+ taskId,
88
+ messageId,
89
+ command,
90
+ }).then(() => {
91
+ }).catch((error) => {
92
+ clearTimeout(timeout);
93
+ wsManager.off("data-event", handler);
94
+ reject(error);
95
+ });
96
+ });
97
+ },
98
+ };
@@ -0,0 +1,5 @@
1
+ /**
2
+ * XY save file to phone tool - saves files to user's device file manager.
3
+ * Supports local file paths (auto-uploaded to get public URL) and public URLs.
4
+ */
5
+ export declare const saveFileToPhoneTool: any;
@@ -0,0 +1,170 @@
1
+ import { getXYWebSocketManager } from "../client.js";
2
+ import { sendCommand } from "../formatter.js";
3
+ import { getCurrentSessionContext } from "./session-manager.js";
4
+ import { XYFileUploadService } from "../file-upload.js";
5
+ /**
6
+ * Duck-typed ToolInputError: openclaw 按 .name 字段匹配,不用 instanceof。
7
+ * 抛出此错误会让 openclaw 返回 HTTP 400 而非 500,
8
+ * LLM 会将其识别为参数错误而非瞬时故障,不会触发重试。
9
+ */
10
+ class ToolInputError extends Error {
11
+ status = 400;
12
+ constructor(message) {
13
+ super(message);
14
+ this.name = "ToolInputError";
15
+ }
16
+ }
17
+ /**
18
+ * XY save file to phone tool - saves files to user's device file manager.
19
+ * Supports local file paths (auto-uploaded to get public URL) and public URLs.
20
+ */
21
+ export const saveFileToPhoneTool = {
22
+ name: "save_file_to_file_manager",
23
+ label: "Save File to Phone",
24
+ description: `将文件保存到手机文件管理器。
25
+ 工具参数说明:
26
+ a. fileName:必填,string类型,文件名称。
27
+ b. url:必填,string类型,支持本地路径或者公网url路径。如果是本地路径,会先上传获取公网url再保存到手机。
28
+ c. suffix:必填,string类型,文件后缀,例如 ppt、doc、pdf 等。
29
+
30
+ 注意:
31
+ a. 操作超时时间为60秒,请勿重复调用此工具
32
+ b. 如果遇到各类调用失败场景,不可以重试,直接返回错误。
33
+ c. 调用工具前需认真检查调用参数是否满足工具要求
34
+
35
+ 回复约束:如果工具返回没有授权或者其他报错,只需要完整描述没有授权或者其他报错内容即可,不需要主动给用户提供解决方案,例如告诉用户如何授权,如何解决报错等都是不需要的,请严格遵守。
36
+ `,
37
+ parameters: {
38
+ type: "object",
39
+ properties: {
40
+ fileName: {
41
+ type: "string",
42
+ description: "必填,文件名称。",
43
+ },
44
+ url: {
45
+ type: "string",
46
+ description: "必填,支持本地路径或者公网url路径。如果是本地路径会先上传获取公网url。",
47
+ },
48
+ suffix: {
49
+ type: "string",
50
+ description: "必填,文件后缀,例如 ppt、doc、pdf 等。",
51
+ },
52
+ },
53
+ required: ["fileName", "url", "suffix"],
54
+ },
55
+ async execute(toolCallId, params) {
56
+ // Validate parameters
57
+ const { fileName, url, suffix } = params;
58
+ if (!url || typeof url !== "string") {
59
+ throw new ToolInputError("缺少必填参数: url");
60
+ }
61
+ if (!fileName || typeof fileName !== "string") {
62
+ throw new ToolInputError("缺少必填参数: fileName");
63
+ }
64
+ if (!suffix || typeof suffix !== "string") {
65
+ throw new ToolInputError("缺少必填参数: suffix");
66
+ }
67
+ // Get session context
68
+ const sessionContext = getCurrentSessionContext();
69
+ if (!sessionContext) {
70
+ throw new Error("No active XY session found. SaveFileToFileManager tool can only be used during an active conversation.");
71
+ }
72
+ const { config, sessionId, taskId, messageId } = sessionContext;
73
+ // Get WebSocket manager
74
+ const wsManager = getXYWebSocketManager(config);
75
+ // Determine the URL: if it's a local path, upload first to get public URL
76
+ let publicUrl = url;
77
+ if (!url.startsWith("http://") && !url.startsWith("https://")) {
78
+ // Local file path - upload to get public URL
79
+ const uploadService = new XYFileUploadService(config.fileUploadUrl, config.apiKey, config.uid);
80
+ publicUrl = await uploadService.uploadFileAndGetUrl(url);
81
+ if (!publicUrl) {
82
+ throw new Error("本地文件上传失败,无法获取公网URL");
83
+ }
84
+ }
85
+ // Build intentParam
86
+ const intentParam = {
87
+ fileName: fileName,
88
+ url: publicUrl,
89
+ suffix: suffix,
90
+ };
91
+ // Build SaveFileToFileManager command
92
+ const command = {
93
+ header: {
94
+ namespace: "Common",
95
+ name: "Action",
96
+ },
97
+ payload: {
98
+ cardParam: {},
99
+ executeParam: {
100
+ executeMode: "background",
101
+ intentName: "SaveFileToFileManager",
102
+ bundleName: "com.huawei.hmos.vassistant",
103
+ dimension: "",
104
+ needUnlock: true,
105
+ actionResponse: true,
106
+ appType: "OHOS_APP",
107
+ timeOut: 5,
108
+ timeout: 55000,
109
+ intentParam,
110
+ permissionId: ["ohos.permission.WRITE_IMAGEVIDEO"],
111
+ achieveType: "INTENT",
112
+ },
113
+ responses: [
114
+ {
115
+ resultCode: "",
116
+ displayText: "",
117
+ ttsText: "",
118
+ },
119
+ ],
120
+ needUploadResult: true,
121
+ noHalfPage: false,
122
+ pageControlRelated: false,
123
+ },
124
+ };
125
+ // Send command and wait for response (60 second timeout)
126
+ return new Promise((resolve, reject) => {
127
+ const timeout = setTimeout(() => {
128
+ wsManager.off("data-event", handler);
129
+ reject(new Error("保存文件到手机超时(60秒)"));
130
+ }, 60000);
131
+ // Listen for data events from WebSocket
132
+ const handler = (event) => {
133
+ if (event.intentName === "SaveFileToFileManager") {
134
+ clearTimeout(timeout);
135
+ wsManager.off("data-event", handler);
136
+ if (event.status === "success" && event.outputs) {
137
+ resolve({
138
+ content: [
139
+ {
140
+ type: "text",
141
+ text: JSON.stringify(event.outputs),
142
+ }
143
+ ]
144
+ });
145
+ }
146
+ else {
147
+ reject(new Error(`保存文件到手机失败: ${event.status}`));
148
+ }
149
+ }
150
+ };
151
+ // Register event handler
152
+ wsManager.on("data-event", handler);
153
+ // Send the command
154
+ sendCommand({
155
+ config,
156
+ sessionId,
157
+ taskId,
158
+ messageId,
159
+ command,
160
+ })
161
+ .then(() => {
162
+ })
163
+ .catch((error) => {
164
+ clearTimeout(timeout);
165
+ wsManager.off("data-event", handler);
166
+ reject(error);
167
+ });
168
+ });
169
+ },
170
+ };
@@ -19,7 +19,7 @@ class ToolInputError extends Error {
19
19
  * Supports local file paths (auto-uploaded to get public URL) and public URLs.
20
20
  */
21
21
  export const saveMediaToGalleryTool = {
22
- name: "SaveMediaToGallery",
22
+ name: "save_media_to_gallery",
23
23
  label: "Save Media to Gallery",
24
24
  description: `将图片文件或者视频文件保存到手机图库。
25
25
  工具参数说明:
@@ -18,9 +18,16 @@ class ToolInputError extends Error {
18
18
  * XY add collection tool - adds data to user's XiaoYi collection.
19
19
  */
20
20
  export const xiaoyiAddCollectionTool = {
21
- name: "AddCollection",
21
+ name: "add_collection",
22
22
  label: "Add XiaoYi Collection",
23
- description: `向小艺收藏中添加公共知识数据,可以给用户提供个性化体验。用户希望保存到个人化知识库中的数据都可以调用本技能。不同类型的数据对应的数据要求如下:
23
+ description: `向小艺收藏中添加公共知识数据,可以给用户提供个性化体验。任何用户希望保存到个人化知识库中的数据都可以调用本技能。不同类型的数据对应的数据要求如下:
24
+ 请求入参说明:
25
+ ● content:必填字段,数据类型为string,功能描述是该字段是用户添加收藏的链接url或文本原文。适用于HYPER_LINK和TEXT类型。
26
+ ● uri:必填字段,数据类型为string,功能描述是该字段是图片或文件的端存储地址链接。适用于IMAGE和FILE类型。
27
+ ● sourceAppBundleName:非必填字段,数据类型为string,功能描述是标识该数据的来源应用。
28
+ ● dataType:必填字段,数据类型为string,功能描述是标识数据类型。HYPER_LINK标识网页,TEXT标识文本,IMAGE标识图片,FILE标识文件。
29
+ ● title:非必填字段,数据类型为string,功能描述是标识文件类型数据的文件名称。适用于FILE类型。
30
+ 说明:如果dataType为HYPER_LINK或TEXT,则content字段必填且不能为空;如果dataType为IMAGE或FILE,则uri字段必填且不能为空。当用户希望收藏海报、截图等图片类数据时,请将数据以图片IMAGE的形式存入到小艺帮记;当用户希望收藏电子书、笔记、报告、素材、文档、合同、协议、简历、证书、报表、日志、安装包、压缩包等描述的文件时,请将数据以文件FILE的形式存入到小艺帮记。
24
31
  注意:
25
32
  a. 操作超时时间为60秒,请勿重复调用此工具
26
33
  b. 如果遇到各类调用失败场景,最多只能重试一次,不可以重复调用多次。
@@ -47,12 +54,16 @@ export const xiaoyiAddCollectionTool = {
47
54
  type: "string",
48
55
  description: "必填字段。标识数据类型:HYPER_LINK表示网页,TEXT表示文本,IMAGE表示图片,FILE表示文件。",
49
56
  },
57
+ title: {
58
+ type: "string",
59
+ description: "非必填字段。标识文件类型数据的文件名称。适用于FILE类型。",
60
+ },
50
61
  },
51
62
  required: ["dataType"],
52
63
  },
53
64
  async execute(toolCallId, params) {
54
65
  // Validate parameters
55
- const { content, uri, sourceAppBundleName, dataType } = params;
66
+ const { content, uri, sourceAppBundleName, dataType, title } = params;
56
67
  const validTypes = ["HYPER_LINK", "TEXT", "IMAGE", "FILE"];
57
68
  if (!dataType || !validTypes.includes(dataType)) {
58
69
  throw new ToolInputError(`dataType必填且必须为 HYPER_LINK、TEXT、IMAGE、FILE 之一,当前值: ${dataType}`);
@@ -93,6 +104,9 @@ export const xiaoyiAddCollectionTool = {
93
104
  if (sourceAppBundleName) {
94
105
  intentParam.sourceAppBundleName = sourceAppBundleName;
95
106
  }
107
+ if (title) {
108
+ intentParam.title = title;
109
+ }
96
110
  // Build AddCollection command
97
111
  const command = {
98
112
  header: {
@@ -18,7 +18,7 @@ class ToolInputError extends Error {
18
18
  * Returns personalized knowledge data saved in user's collection.
19
19
  */
20
20
  export const xiaoyiCollectionTool = {
21
- name: "QueryCollection",
21
+ name: "query_collection",
22
22
  label: "XiaoYi Collection",
23
23
  description: `检索用户在小艺收藏中记下来的公共知识数据,本技能支持查询用户收藏的公共知识数据,也可以根据特定语义化描述进行特定内容的检索,通过参数进行控制。本技能返回结果中,linkTitle是收藏内容的标题,description是对收藏内容的总结,label是收藏内容的标签,linkUrl是可以直接访问的原始内容链接。如果你认为某条数据对用户交互有用,可以通过linkUrl抓取更加丰富的原始数据。
24
24
  注意:
@@ -17,7 +17,7 @@ class ToolInputError extends Error {
17
17
  * XY delete collection tool - deletes data from user's XiaoYi collection.
18
18
  */
19
19
  export const xiaoyiDeleteCollectionTool = {
20
- name: "DeleteCollection",
20
+ name: "delete_collection",
21
21
  label: "Delete XiaoYi Collection",
22
22
  description: `从小艺收藏中删除之前已保存的公共知识数据。任何用户希望删除已保存到个人知识库的数据都可以调用本技能。如果用户想更新之前的收藏数据,需要先query获取itemId然后再delete,最后执行Add,按照这个步骤完成收藏数据更新。
23
23
  注意:
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "id": "xiaoyi-channel",
3
3
  "channels": ["xiaoyi-channel"],
4
+ "providers": ["xiaoyiprovider"],
4
5
  "skills": [],
5
6
  "configSchema": {
6
7
  "type": "object",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ynhcj/xiaoyi-channel",
3
- "version": "1.1.16",
3
+ "version": "1.1.17",
4
4
  "description": "OpenClaw Xiaoyi Channel plugin - Xiaoyi A2A protocol integration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",