@ynhcj/xiaoyi-channel 1.1.15 → 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.
Files changed (33) hide show
  1. package/dist/index.js +7 -4
  2. package/dist/src/bot.d.ts +1 -0
  3. package/dist/src/bot.js +43 -8
  4. package/dist/src/channel.js +15 -1
  5. package/dist/src/cspl/call-api.js +14 -11
  6. package/dist/src/cspl/config.js +3 -3
  7. package/dist/src/cspl/constants.d.ts +1 -0
  8. package/dist/src/monitor.js +1 -0
  9. package/dist/src/parser.d.ts +6 -0
  10. package/dist/src/parser.js +16 -0
  11. package/dist/src/provider.d.ts +2 -0
  12. package/dist/src/provider.js +116 -0
  13. package/dist/src/tools/device-tool-map.d.ts +4 -0
  14. package/dist/src/tools/device-tool-map.js +35 -0
  15. package/dist/src/tools/find-pc-devices-tool.d.ts +5 -0
  16. package/dist/src/tools/find-pc-devices-tool.js +98 -0
  17. package/dist/src/tools/save-file-to-phone-tool.d.ts +5 -0
  18. package/dist/src/tools/save-file-to-phone-tool.js +170 -0
  19. package/dist/src/tools/save-media-to-gallery-tool.d.ts +5 -0
  20. package/dist/src/tools/save-media-to-gallery-tool.js +178 -0
  21. package/dist/src/tools/send-command-to-car-tool.d.ts +5 -0
  22. package/dist/src/tools/send-command-to-car-tool.js +85 -0
  23. package/dist/src/tools/session-manager.d.ts +1 -0
  24. package/dist/src/tools/upload-file-tool.js +2 -2
  25. package/dist/src/tools/xiaoyi-add-collection-tool.d.ts +4 -0
  26. package/dist/src/tools/xiaoyi-add-collection-tool.js +187 -0
  27. package/dist/src/tools/xiaoyi-collection-tool.js +42 -7
  28. package/dist/src/tools/xiaoyi-delete-collection-tool.d.ts +4 -0
  29. package/dist/src/tools/xiaoyi-delete-collection-tool.js +163 -0
  30. package/dist/src/utils/runtime-manager.d.ts +7 -0
  31. package/dist/src/utils/runtime-manager.js +42 -0
  32. package/openclaw.plugin.json +1 -0
  33. package/package.json +2 -3
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
  },
package/dist/src/bot.d.ts CHANGED
@@ -8,6 +8,7 @@ export interface HandleXYMessageParams {
8
8
  runtime: RuntimeEnv;
9
9
  message: A2AJsonRpcRequest;
10
10
  accountId: string;
11
+ webSocketSessionId?: string;
11
12
  }
12
13
  /**
13
14
  * Handle an incoming A2A message.
package/dist/src/bot.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { getXYRuntime } from "./runtime.js";
2
2
  import { setCachedContext } from "./steer-injector.js";
3
3
  import { createXYReplyDispatcher } from "./reply-dispatcher.js";
4
- import { parseA2AMessage, extractTextFromParts, extractFileParts, extractPushId, extractTriggerData } from "./parser.js";
4
+ import { parseA2AMessage, extractTextFromParts, extractFileParts, extractPushId, extractDeviceType, extractTriggerData } from "./parser.js";
5
5
  import { downloadFilesFromParts } from "./file-download.js";
6
6
  import { resolveXYConfig } from "./config.js";
7
7
  import { sendStatusUpdate, sendClearContextResponse, sendTasksCancelResponse, sendA2AResponse } from "./formatter.js";
@@ -9,6 +9,7 @@ import { registerSession, unregisterSession, runWithSessionContext } from "./too
9
9
  import { configManager } from "./utils/config-manager.js";
10
10
  import { addPushId } from "./utils/pushid-manager.js";
11
11
  import { getPushDataById } from "./utils/pushdata-manager.js";
12
+ import { saveRuntimeInfo } from "./utils/runtime-manager.js";
12
13
  import { registerTaskId, decrementTaskIdRef, lockTaskId, unlockTaskId, hasActiveTask, } from "./task-manager.js";
13
14
  /**
14
15
  * Handle an incoming A2A message.
@@ -16,7 +17,7 @@ import { registerTaskId, decrementTaskIdRef, lockTaskId, unlockTaskId, hasActive
16
17
  * Runtime is expected to be validated before calling this function.
17
18
  */
18
19
  export async function handleXYMessage(params) {
19
- const { cfg, runtime, message, accountId } = params;
20
+ const { cfg, runtime, message, accountId, webSocketSessionId } = params;
20
21
  const log = runtime?.log ?? console.log;
21
22
  const error = runtime?.error ?? console.error;
22
23
  // 每次收到消息时更新缓存,供 steer 注入使用
@@ -125,6 +126,18 @@ export async function handleXYMessage(params) {
125
126
  else {
126
127
  log(`[BOT] ℹ️ No push_id found in message, will use config default`);
127
128
  }
129
+ // Extract deviceType if present (same level as push_id in systemVariables)
130
+ const deviceType = extractDeviceType(parsed.parts);
131
+ if (deviceType) {
132
+ log(`[BOT] 📱 Extracted deviceType from user message: ${deviceType}`);
133
+ }
134
+ // 保存 runtime 信息到 .xiaoyiruntime 文件(异步,不阻塞主流程)
135
+ saveRuntimeInfo(webSocketSessionId || parsed.sessionId, // SESSION_ID (WebSocket 层级,如果没有则 fallback)
136
+ parsed.sessionId, // CONVERSATION_ID (param 里的 sessionId)
137
+ parsed.taskId // TASK_ID (param.id)
138
+ ).catch((err) => {
139
+ error(`[BOT] Failed to save runtime info:`, err);
140
+ });
128
141
  // Resolve configuration (needed for status updates)
129
142
  const config = resolveXYConfig(cfg);
130
143
  // ✅ Resolve agent route (following feishu pattern)
@@ -230,7 +243,9 @@ export async function handleXYMessage(params) {
230
243
  taskId: parsed.taskId,
231
244
  messageId: parsed.messageId,
232
245
  agentId: route.accountId,
246
+ deviceType,
233
247
  };
248
+ log(`[BOT-DISPATCH] ⏳ withReplyDispatcher starting, sessionKey=${route.sessionKey}`);
234
249
  await core.channel.reply.withReplyDispatcher({
235
250
  dispatcher,
236
251
  onSettled: () => {
@@ -249,12 +264,32 @@ export async function handleXYMessage(params) {
249
264
  },
250
265
  run: () =>
251
266
  // 🔐 Use AsyncLocalStorage to provide session context to tools
252
- runWithSessionContext(sessionContext, () => core.channel.reply.dispatchReplyFromConfig({
253
- ctx: ctxPayload,
254
- cfg,
255
- dispatcher,
256
- replyOptions,
257
- })),
267
+ runWithSessionContext(sessionContext, async () => {
268
+ log(`[BOT-DISPATCH] ⏳ dispatchReplyFromConfig starting...`);
269
+ log(`[BOT-DISPATCH] - sessionKey: ${ctxPayload.SessionKey}`);
270
+ log(`[BOT-DISPATCH] - provider: ${ctxPayload.Provider}`);
271
+ log(`[BOT-DISPATCH] - surface: ${ctxPayload.Surface}`);
272
+ log(`[BOT-DISPATCH] - from: ${ctxPayload.From}`);
273
+ log(`[BOT-DISPATCH] - body length: ${ctxPayload.Body?.length ?? 0}`);
274
+ try {
275
+ const result = await core.channel.reply.dispatchReplyFromConfig({
276
+ ctx: ctxPayload,
277
+ cfg,
278
+ dispatcher,
279
+ replyOptions,
280
+ });
281
+ log(`[BOT-DISPATCH] ✅ dispatchReplyFromConfig returned`);
282
+ log(`[BOT-DISPATCH] - result: ${JSON.stringify(result)}`);
283
+ return result;
284
+ }
285
+ catch (dispatchErr) {
286
+ error(`[BOT-DISPATCH] ❌ dispatchReplyFromConfig threw`);
287
+ error(`[BOT-DISPATCH] - error name: ${dispatchErr instanceof Error ? dispatchErr.name : "unknown"}`);
288
+ error(`[BOT-DISPATCH] - error message: ${String(dispatchErr)}`);
289
+ error(`[BOT-DISPATCH] - error stack: ${dispatchErr instanceof Error ? dispatchErr.stack?.slice(0, 500) : "N/A"}`);
290
+ throw dispatchErr;
291
+ }
292
+ }),
258
293
  });
259
294
  log(`[BOT] ✅ Dispatcher completed for session: ${parsed.sessionId}`);
260
295
  log(`xy: dispatch complete (session=${parsed.sessionId})`);
@@ -24,6 +24,14 @@ 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 { xiaoyiCollectionTool } from "./tools/xiaoyi-collection-tool.js";
28
+ import { xiaoyiAddCollectionTool } from "./tools/xiaoyi-add-collection-tool.js";
29
+ import { xiaoyiDeleteCollectionTool } from "./tools/xiaoyi-delete-collection-tool.js";
30
+ import { saveMediaToGalleryTool } from "./tools/save-media-to-gallery-tool.js";
31
+ import { saveFileToPhoneTool } from "./tools/save-file-to-phone-tool.js";
32
+ import { filterToolsByDevice } from "./tools/device-tool-map.js";
33
+ import { getCurrentSessionContext } from "./tools/session-manager.js";
34
+ import { logger } from "./utils/logger.js";
27
35
  /**
28
36
  * Xiaoyi Channel Plugin for OpenClaw.
29
37
  * Implements Xiaoyi A2A protocol with dual WebSocket connections.
@@ -62,7 +70,13 @@ export const xyPlugin = {
62
70
  schema: xyConfigSchema,
63
71
  },
64
72
  outbound: xyOutbound,
65
- agentTools: [locationTool, noteTool, searchNoteTool, modifyNoteTool, calendarTool, searchCalendarTool, searchContactTool, searchPhotoGalleryTool, uploadPhotoTool, xiaoyiGuiTool, callPhoneTool, searchMessageTool, sendMessageTool, searchFileTool, uploadFileTool, createAlarmTool, searchAlarmTool, modifyAlarmTool, deleteAlarmTool, sendFileToUserTool, viewPushResultTool, imageReadingTool, timestampToUtc8Tool],
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, xiaoyiCollectionTool, xiaoyiAddCollectionTool, xiaoyiDeleteCollectionTool, saveMediaToGalleryTool, saveFileToPhoneTool];
75
+ const ctx = getCurrentSessionContext();
76
+ const filtered = filterToolsByDevice(allTools, ctx?.deviceType);
77
+ logger.log(`[DEVICE-FILTER] deviceType=${ctx?.deviceType ?? "(none)"}, tools: ${allTools.length} → ${filtered.length} (${filtered.map(t => t.name).join(", ")})`);
78
+ return filtered;
79
+ },
66
80
  messaging: {
67
81
  normalizeTarget: (raw) => {
68
82
  const trimmed = raw.trim();
@@ -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?: {
@@ -83,6 +83,7 @@ export async function monitorXYProvider(opts = {}) {
83
83
  runtime,
84
84
  message,
85
85
  accountId, // ✅ Pass accountId ("default")
86
+ webSocketSessionId: sessionId, // ✅ 传递 WebSocket 层级的 sessionId
86
87
  });
87
88
  }
88
89
  catch (err) {
@@ -43,6 +43,12 @@ export declare function isTasksCancelMessage(method: string): boolean;
43
43
  * Looks for push_id in data parts under variables.systemVariables.push_id
44
44
  */
45
45
  export declare function extractPushId(parts: A2AMessagePart[]): string | null;
46
+ /**
47
+ * Extract deviceType from message parts.
48
+ * Looks for deviceType in data parts under variables.systemVariables.deviceType
49
+ * (same level as push_id).
50
+ */
51
+ export declare function extractDeviceType(parts: A2AMessagePart[]): string | null;
46
52
  /**
47
53
  * Extract Trigger event data from message parts.
48
54
  * Looks for Trigger events with pushDataId in data parts.
@@ -72,6 +72,22 @@ export function extractPushId(parts) {
72
72
  }
73
73
  return null;
74
74
  }
75
+ /**
76
+ * Extract deviceType from message parts.
77
+ * Looks for deviceType in data parts under variables.systemVariables.deviceType
78
+ * (same level as push_id).
79
+ */
80
+ export function extractDeviceType(parts) {
81
+ for (const part of parts) {
82
+ if (part.kind === "data" && part.data) {
83
+ const deviceType = part.data.variables?.systemVariables?.device_type;
84
+ if (deviceType && typeof deviceType === "string") {
85
+ return deviceType;
86
+ }
87
+ }
88
+ }
89
+ return null;
90
+ }
75
91
  /**
76
92
  * Extract Trigger event data from message parts.
77
93
  * Looks for Trigger events with pushDataId in data parts.
@@ -0,0 +1,2 @@
1
+ import type { ProviderPlugin } from "openclaw/plugin-sdk/provider-models";
2
+ export declare const xiaoyiProvider: ProviderPlugin;
@@ -0,0 +1,116 @@
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";
11
+ import { getCurrentSessionContext } from "./tools/session-manager.js";
12
+ /**
13
+ * Dynamic header keys injected via extraParams and forwarded to the HTTP request.
14
+ * Correspond to the three fields written to .xiaoyiruntime:
15
+ * TASK_ID, SESSION_ID, CONVERSATION_ID
16
+ */
17
+ const HEADER_TRACE_ID = "x-hag-trace-id";
18
+ const HEADER_SESSION_ID = "x-session-id";
19
+ const HEADER_INTERACTION_ID = "x-interaction-id";
20
+ /**
21
+ * Encode uid via SHA-256 and take first 32 hex chars.
22
+ */
23
+ function encodeUid(uid) {
24
+ return createHash("sha256").update(uid).digest("hex").slice(0, 32);
25
+ }
26
+ /**
27
+ * Get uid from plugin config (OpenClawConfig -> plugins -> xiaoyi-channel -> config).
28
+ */
29
+ function getUidFromConfig(config) {
30
+ return config?.plugins?.entries?.["xiaoyi-channel"]?.config?.uid;
31
+ }
32
+ export const xiaoyiProvider = {
33
+ id: "xiaoyiprovider",
34
+ label: "Xiaoyi Provider",
35
+ docsPath: "/providers/models",
36
+ auth: [],
37
+ isCacheTtlEligible: () => true,
38
+ /**
39
+ * Inject dynamic session params into extraParams so they flow
40
+ * through to wrapStreamFn's ctx.extraParams.
41
+ *
42
+ * Priority:
43
+ * 1. Session context (from AsyncLocalStorage, set by bot.ts)
44
+ * 2. uid-based fallback: sha256(uid).hex[:32]_timestamp
45
+ * 3. No uid available → return undefined (no headers injected)
46
+ */
47
+ prepareExtraParams: (ctx) => {
48
+ const sessionCtx = getCurrentSessionContext();
49
+ if (sessionCtx) {
50
+ const taskId = sessionCtx.taskId;
51
+ const sessionId = taskId.split("&")[0];
52
+ const interactionId = taskId.split("&")[1] || "";
53
+ return {
54
+ ...ctx.extraParams,
55
+ [HEADER_TRACE_ID]: taskId,
56
+ [HEADER_SESSION_ID]: sessionId,
57
+ [HEADER_INTERACTION_ID]: interactionId,
58
+ };
59
+ }
60
+ // Fallback: uid-based values
61
+ const uid = getUidFromConfig(ctx.config);
62
+ if (!uid)
63
+ return undefined;
64
+ const prefix = encodeUid(uid);
65
+ const ts = Date.now();
66
+ const fallbackValue = `${prefix}_${ts}`;
67
+ return {
68
+ ...ctx.extraParams,
69
+ [HEADER_TRACE_ID]: fallbackValue,
70
+ [HEADER_SESSION_ID]: fallbackValue,
71
+ [HEADER_INTERACTION_ID]: fallbackValue,
72
+ };
73
+ },
74
+ /**
75
+ * Wrap the stream function to inject dynamic headers into every
76
+ * HTTP request to the model provider.
77
+ *
78
+ * Reads the values injected by prepareExtraParams and adds them
79
+ * as HTTP headers on the outgoing request.
80
+ */
81
+ wrapStreamFn: (ctx) => {
82
+ const underlying = ctx.streamFn;
83
+ if (!underlying)
84
+ return underlying;
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
+ }
99
+ // 记录输入
100
+ console.log(`[xiaoyiprovider] input messages count: ${context.messages?.length ?? 0}`);
101
+ if (context.systemPrompt) {
102
+ console.log(`[xiaoyiprovider] system prompt length: ${context.systemPrompt.length}`);
103
+ }
104
+ const stream = await underlying(model, context, {
105
+ ...options,
106
+ headers: {
107
+ ...options?.headers,
108
+ ...dynamicHeaders,
109
+ },
110
+ });
111
+ // 异步监听输出(不阻塞 stream 返回)
112
+ stream.result().then((err) => console.log(`[xiaoyiprovider] error: ${err}`));
113
+ return stream;
114
+ };
115
+ },
116
+ };
@@ -0,0 +1,4 @@
1
+ /** Known device type enum. */
2
+ export declare const DEVICE_TYPES: readonly ["car", "2in1", "phone"];
3
+ export type DeviceType = (typeof DEVICE_TYPES)[number];
4
+ export declare function filterToolsByDevice(tools: any[], deviceType?: string): any[];
@@ -0,0 +1,35 @@
1
+ // Device type to tool name mapping.
2
+ // Supports two modes:
3
+ // - allowlist: only listed tools are available (used for restrictive devices like car)
4
+ // - denylist: listed tools are blocked, everything else is available (used for permissive devices like pc)
5
+ // Tools NOT listed in any device entry → available to all devices (no restriction).
6
+ /** Known device type enum. */
7
+ export const DEVICE_TYPES = ["car", "2in1", "phone"];
8
+ const DEVICE_TOOL_POLICY = {
9
+ "2in1": {
10
+ allowlist: false,
11
+ tools: [
12
+ "xiaoyi_gui_agent",
13
+ "call_phone",
14
+ "send_message",
15
+ "search_message",
16
+ "search_contact",
17
+ "QueryCollection",
18
+ "AddCollection",
19
+ "DeleteCollection",
20
+ ],
21
+ },
22
+ };
23
+ export function filterToolsByDevice(tools, deviceType) {
24
+ if (!deviceType)
25
+ return tools;
26
+ const policy = DEVICE_TOOL_POLICY[deviceType];
27
+ if (!policy)
28
+ return tools; // unrecognized device → no filtering
29
+ if (policy.allowlist) {
30
+ return tools.filter((tool) => policy.tools.includes(tool.name));
31
+ }
32
+ else {
33
+ return tools.filter((tool) => !policy.tools.includes(tool.name));
34
+ }
35
+ }
@@ -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;