@ynhcj/xiaoyi-channel 0.0.74-beta → 0.0.76-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,6 +19,7 @@ const plugin = {
18
19
  register(api) {
19
20
  setXYRuntime(api.runtime);
20
21
  api.registerChannel({ plugin: xyPlugin });
22
+ api.registerProvider(xiaoyiProvider);
21
23
  // CSPL after_tool_call hook: 监听工具结果,发送至 CSPL API 进行安全检测
22
24
  // 如果响应为 REJECT,注入 steer 消息中止当前对话
23
25
  api.on("after_tool_call", async (event, ctx) => {
@@ -24,7 +24,6 @@ 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";
@@ -71,7 +70,7 @@ export const xyPlugin = {
71
70
  },
72
71
  outbound: xyOutbound,
73
72
  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];
73
+ 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];
75
74
  const ctx = getCurrentSessionContext();
76
75
  const filtered = filterToolsByDevice(allTools, ctx?.deviceType);
77
76
  logger.log(`[DEVICE-FILTER] deviceType=${ctx?.deviceType ?? "(none)"}, tools: ${allTools.length} → ${filtered.length} (${filtered.map(t => t.name).join(", ")})`);
@@ -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",
@@ -20,7 +20,14 @@ class ToolInputError extends Error {
20
20
  export const xiaoyiAddCollectionTool = {
21
21
  name: "AddCollection",
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: {
@@ -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": "0.0.74-beta",
3
+ "version": "0.0.76-beta",
4
4
  "description": "OpenClaw Xiaoyi Channel plugin - Xiaoyi A2A protocol integration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",