@ynhcj/xiaoyi-channel 0.0.163-beta → 0.0.164-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.
@@ -1,2 +1,2 @@
1
1
  import { ApiResponse } from './constants.js';
2
- export declare function callApi(questionText: string, api: any, sessionId: string): Promise<ApiResponse>;
2
+ export declare function callApi(questionText: string, api: any, sessionId: string, action: string): Promise<ApiResponse>;
@@ -78,13 +78,13 @@ function handleResponse(res, resolve, reject) {
78
78
  }
79
79
  });
80
80
  }
81
- export async function callApi(questionText, api, sessionId) {
81
+ export async function callApi(questionText, api, sessionId, action) {
82
82
  const config = getConfig(api);
83
83
  const headersForCelia = buildHeadersForCelia(config, sessionId);
84
84
  const payload = {
85
85
  questionText: questionText,
86
86
  textSource: config.textSource,
87
- action: config.action,
87
+ action: action,
88
88
  extra: `${JSON.stringify({ userId: config.uid })}`
89
89
  };
90
90
  const httpBody = JSON.stringify(payload);
@@ -2,9 +2,9 @@
2
2
  * 版权所有 (c) 华为技术有限公司 2026-2026
3
3
  */
4
4
  import fs from 'fs';
5
- import { ENV_FILE_PATH, REQUIRED_ENV_VARS } from './constants.js';
5
+ import path from 'path';
6
+ import { CONFIG_FILE_NAME, ENV_FILE_PATH, REQUIRED_ENV_VARS } from './constants.js';
6
7
  import { logger } from '../utils/logger.js';
7
- import defaultConfig from './configs.json' with { type: 'json' };
8
8
  let cachedConfig = null;
9
9
  function readEnvFile() {
10
10
  if (!fs.existsSync(ENV_FILE_PATH)) {
@@ -41,25 +41,45 @@ export function getConfig(api) {
41
41
  if (cachedConfig) {
42
42
  return cachedConfig;
43
43
  }
44
- // Use imported JSON (bundled at compile time, no runtime file read needed)
45
- const config = { ...defaultConfig };
44
+ const configPath = path.join(__dirname, CONFIG_FILE_NAME);
45
+ if (!fs.existsSync(configPath)) {
46
+ throw new Error(`Config file not found: ${CONFIG_FILE_NAME}`);
47
+ }
48
+ let configData;
49
+ try {
50
+ configData = fs.readFileSync(configPath, 'utf-8');
51
+ }
52
+ catch (error) {
53
+ throw new Error(`Failed to read config file: ${CONFIG_FILE_NAME}.`);
54
+ }
55
+ let parsedConfig;
56
+ try {
57
+ parsedConfig = JSON.parse(configData);
58
+ }
59
+ catch (error) {
60
+ throw new Error(`Failed to parse config file: ${CONFIG_FILE_NAME}.`);
61
+ }
62
+ if (!parsedConfig || typeof parsedConfig !== 'object') {
63
+ throw new Error(`Invalid config structure: ${CONFIG_FILE_NAME}. Expected an object.`);
64
+ }
65
+ const config = parsedConfig;
46
66
  if (!config.api || typeof config.api !== 'object') {
47
- throw new Error(`Invalid config: missing or invalid 'api' section`);
67
+ throw new Error(`Invalid config: missing or invalid 'api' section in ${CONFIG_FILE_NAME}`);
48
68
  }
49
69
  if (!config.api.timeout || typeof config.api.timeout !== 'number') {
50
- throw new Error(`Invalid config: missing or invalid 'api.timeout'`);
70
+ throw new Error(`Invalid config: missing or invalid 'api.timeout' in ${CONFIG_FILE_NAME}`);
51
71
  }
52
72
  if (!config.skillId || typeof config.skillId !== 'string') {
53
- throw new Error(`Invalid config: missing or invalid 'skillId'`);
73
+ throw new Error(`Invalid config: missing or invalid 'skillId' in ${CONFIG_FILE_NAME}`);
54
74
  }
55
75
  if (!config.requestFrom || typeof config.requestFrom !== 'string') {
56
- throw new Error(`Invalid config: missing or invalid 'requestFrom'`);
76
+ throw new Error(`Invalid config: missing or invalid 'requestFrom' in ${CONFIG_FILE_NAME}`);
57
77
  }
58
78
  if (!config.textSource || typeof config.textSource !== 'string') {
59
- throw new Error(`Invalid config: missing or invalid 'textSource'`);
79
+ throw new Error(`Invalid config: missing or invalid 'textSource' in ${CONFIG_FILE_NAME}`);
60
80
  }
61
81
  if (!config.action || typeof config.action !== 'string') {
62
- throw new Error(`Invalid config: missing or invalid 'action'`);
82
+ throw new Error(`Invalid config: missing or invalid 'action' in ${CONFIG_FILE_NAME}`);
63
83
  }
64
84
  let env;
65
85
  try {
@@ -43,6 +43,8 @@ export declare const TOOL_INPUT_DEFAULT: {
43
43
  readonly source: "";
44
44
  readonly content: "";
45
45
  };
46
+ export declare const TOOL_INPUT_ACTION = "TOOL_INPUT_SCAN";
47
+ export declare const TOOL_OUTPUT_ACTION = "TOOL_OUTPUT_SCAN";
46
48
  export declare const MAX_TIMES = 3;
47
49
  export declare const CONNECT_TIMEOUT = 15000;
48
50
  export declare const READ_TIMEOUT = 300000;
@@ -47,6 +47,9 @@ export const TOOL_INPUT_DEFAULT = {
47
47
  source: '',
48
48
  content: ''
49
49
  };
50
+ // 安全扫描 action 常量
51
+ export const TOOL_INPUT_ACTION = 'TOOL_INPUT_SCAN';
52
+ export const TOOL_OUTPUT_ACTION = 'TOOL_OUTPUT_SCAN';
50
53
  // OBS上传相关常量
51
54
  export const MAX_TIMES = 3;
52
55
  export const CONNECT_TIMEOUT = 15000;
@@ -4,7 +4,7 @@
4
4
  import crypto from 'crypto';
5
5
  import { callApi } from './call_api.js';
6
6
  import { processText, extractResultText, validateAndTruncateText, parseSecurityResult, handleExecToolInput, handleMessageToolInput, handleOtherToolInput } from './utils.js';
7
- import { ALLOWED_TOOLS, MAX_TEXT_LENGTH, MAX_TOTAL_LENGTH, MIN_TEXT_LENGTH, STEER_ABORT_MESSAGE } from './constants.js';
7
+ import { ALLOWED_TOOLS, MAX_TEXT_LENGTH, MAX_TOTAL_LENGTH, MIN_TEXT_LENGTH, STEER_ABORT_MESSAGE, TOOL_OUTPUT_ACTION } from './constants.js';
8
8
  import { logger } from '../utils/logger.js';
9
9
  import { getSessionContext } from '../tools/session-manager.js';
10
10
  import { tryInjectSteer } from './steer-context.js';
@@ -68,7 +68,7 @@ export default function register(api) {
68
68
  const postText = JSON.stringify(questionText);
69
69
  logger.log(`[SENTINEL HOOK] Content extracted successfully. Length: ${postText.length}`);
70
70
  try {
71
- const response = await callApi(postText, api, sessionId);
71
+ const response = await callApi(postText, api, sessionId, TOOL_OUTPUT_ACTION);
72
72
  const result = parseSecurityResult(response);
73
73
  logger.log(`[SENTINEL HOOK] TOOL_OUTPUT response: status=${result.status}.`);
74
74
  if (result.status === 'REJECT') {
@@ -1,7 +1,7 @@
1
1
  /*
2
2
  * 版权所有 (c) 华为技术有限公司 2026-2026
3
3
  */
4
- import { MAX_TEXT_LENGTH, regex, SECURITY_NOTICE, MAX_FILE_COUNT, MAX_COMMAND_LENGTH, CODE_FILE_EXTENSIONS, TOOL_INPUT_DEFAULT, FILE_EXTENSION_REGEX } from './constants.js';
4
+ import { MAX_TEXT_LENGTH, regex, SECURITY_NOTICE, MAX_FILE_COUNT, MAX_COMMAND_LENGTH, CODE_FILE_EXTENSIONS, TOOL_INPUT_DEFAULT, FILE_EXTENSION_REGEX, TOOL_INPUT_ACTION } from './constants.js';
5
5
  import crypto from 'crypto';
6
6
  import fs from 'fs';
7
7
  import path from 'path';
@@ -215,7 +215,7 @@ export function adjustContentLength(data, api, fields) {
215
215
  }
216
216
  // 发送TOOL_INPUT请求并处理响应
217
217
  async function sendToolInputRequest(postText, api, sessionId) {
218
- const response = await callApi(postText, api, sessionId);
218
+ const response = await callApi(postText, api, sessionId, TOOL_INPUT_ACTION);
219
219
  const result = parseSecurityResult(response);
220
220
  logger.log(`[SENTINEL HOOK] TOOL_INPUT response: status=${result.status}`);
221
221
  }
@@ -2,7 +2,7 @@ import { getXYRuntime } from "./runtime.js";
2
2
  import { sendA2AResponse, sendStatusUpdate, sendReasoningTextUpdate, sendCommand } from "./formatter.js";
3
3
  import { resolveXYConfig } from "./config.js";
4
4
  import { getCurrentTaskId, getCurrentMessageId } from "./task-manager.js";
5
- import { getCurrentSessionContext } from "./tools/session-manager.js";
5
+ import { clearRunCrossTaskSentFiles, getCurrentSessionContext } from "./tools/session-manager.js";
6
6
  import fs from "fs/promises";
7
7
  import path from "path";
8
8
  import { logger } from "./utils/logger.js";
@@ -48,7 +48,8 @@ async function sendRunCrossTaskResult(params) {
48
48
  messageId,
49
49
  commands: [statusCommand, resultCommand],
50
50
  });
51
- logger.log(`${RUN_CROSS_TASK_LOG_TAG} sent cross-task result, sessionId=${sessionId}, taskId=${taskId}, code=${resultCode}, sentFileCount=${sentFiles.length}, messageLength=${resultMessage.length}`);
51
+ clearRunCrossTaskSentFiles(context);
52
+ logger.log(`${RUN_CROSS_TASK_LOG_TAG} sent cross-task result, sessionId=${sessionId}, taskId=${taskId}, code=${resultCode}, sentFileCount=${sentFiles.length}, clearedSentFileCount=${sentFiles.length}, messageLength=${resultMessage.length}`);
52
53
  }
53
54
  /**
54
55
  * 清理 /tmp/xy_channel 目录中超过 24 小时的旧文件
@@ -18,6 +18,7 @@ import { createLoginTokenTool } from "./login-token-tool.js";
18
18
  import { createAgentAsSkillTool } from "./agent-as-skill-tool.js";
19
19
  import { createDiscoverCrossDevicesTool } from "./discover-cross-devices-tool.js";
20
20
  import { createSendCrossDeviceTaskTool } from "./send-cross-device-task-tool.js";
21
+ import { createDisplayA2UICardTool } from "./display-a2ui-card-tool.js";
21
22
  import { logger } from "../utils/logger.js";
22
23
  /**
23
24
  * Create all XY channel tools for the given session context.
@@ -35,6 +36,7 @@ export function createAllTools(ctx) {
35
36
  createLocationTool(ctx),
36
37
  createDiscoverCrossDevicesTool(ctx),
37
38
  createSendCrossDeviceTaskTool(ctx),
39
+ createDisplayA2UICardTool(ctx),
38
40
  createCallDeviceTool(ctx),
39
41
  createGetNoteToolSchemaTool(ctx),
40
42
  createGetCalendarToolSchemaTool(ctx),
@@ -25,6 +25,7 @@ const DEVICE_TOOL_POLICY = {
25
25
  "image_reading",
26
26
  "convert_time_to_utc8_time",
27
27
  "save_self_evolution_skill",
28
+ "displayA2UICard",
28
29
  ],
29
30
  },
30
31
  };
@@ -0,0 +1,2 @@
1
+ import type { SessionContext } from "./session-manager.js";
2
+ export declare function createDisplayA2UICardTool(ctx: SessionContext): any;
@@ -0,0 +1,82 @@
1
+ import { sendCommand } from "../formatter.js";
2
+ import { getCurrentMessageId, getCurrentTaskId } from "../task-manager.js";
3
+ import { logger } from "../utils/logger.js";
4
+ class ToolInputError extends Error {
5
+ status = 400;
6
+ constructor(message) {
7
+ super(message);
8
+ this.name = "ToolInputError";
9
+ }
10
+ }
11
+ function isPlainObject(value) {
12
+ return !!value && typeof value === "object" && !Array.isArray(value);
13
+ }
14
+ export function createDisplayA2UICardTool(ctx) {
15
+ const { config, sessionId, taskId, messageId } = ctx;
16
+ return {
17
+ name: "displayA2UICard",
18
+ label: "Display A2UI Card",
19
+ description: "当模型根据 MCP 工具返回结果判断需要向端侧下发 A2UI card 时调用。参数 cardId 和 cardData 由模型根据 MCP 工具返回结果传入,本工具只负责下发卡片展示指令。",
20
+ parameters: {
21
+ type: "object",
22
+ properties: {
23
+ cardId: {
24
+ type: "string",
25
+ description: "A2UI card 的唯一标识。",
26
+ },
27
+ cardData: {
28
+ type: "object",
29
+ description: "A2UI card 渲染所需的数据对象,由模型根据 MCP 工具返回结果填充。",
30
+ },
31
+ },
32
+ required: ["cardId", "cardData"],
33
+ },
34
+ async execute(toolCallId, params) {
35
+ const cardId = typeof params?.cardId === "string" ? params.cardId.trim() : "";
36
+ const cardData = params?.cardData;
37
+ if (!cardId) {
38
+ throw new ToolInputError("缺少必填参数: cardId");
39
+ }
40
+ if (!isPlainObject(cardData)) {
41
+ throw new ToolInputError("缺少必填参数: cardData,且必须是对象");
42
+ }
43
+ const currentTaskId = getCurrentTaskId(sessionId) ?? taskId;
44
+ const currentMessageId = getCurrentMessageId(sessionId) ?? messageId;
45
+ const command = {
46
+ header: {
47
+ namespace: "Common",
48
+ name: "DisplayFACard",
49
+ },
50
+ payload: {
51
+ isA2ui: true,
52
+ a2uiParam: {
53
+ cardId,
54
+ cardData,
55
+ },
56
+ },
57
+ };
58
+ logger.log(`[DISPLAY-A2UI-CARD] sending card, cardId=${cardId}`);
59
+ await sendCommand({
60
+ config,
61
+ sessionId,
62
+ taskId: currentTaskId,
63
+ messageId: currentMessageId,
64
+ command,
65
+ toolCallId,
66
+ });
67
+ logger.log(`[DISPLAY-A2UI-CARD] card sent successfully, cardId=${cardId}`);
68
+ return {
69
+ content: [
70
+ {
71
+ type: "text",
72
+ text: JSON.stringify({
73
+ success: true,
74
+ cardId,
75
+ message: "A2UI card display command sent successfully.",
76
+ }),
77
+ },
78
+ ],
79
+ };
80
+ },
81
+ };
82
+ }
@@ -77,4 +77,5 @@ export declare function cleanupStaleSessions(): number;
77
77
  */
78
78
  export declare function getActiveSessionCount(): number;
79
79
  export declare function appendRunCrossTaskSentFiles(sentFiles: SentFileParams[], explicitRunCrossTaskContext?: RunCrossTaskContext): SentFileParams[];
80
+ export declare function clearRunCrossTaskSentFiles(explicitRunCrossTaskContext?: RunCrossTaskContext): void;
80
81
  export {};
@@ -274,6 +274,19 @@ export function appendRunCrossTaskSentFiles(sentFiles, explicitRunCrossTaskConte
274
274
  }
275
275
  return merged;
276
276
  }
277
+ export function clearRunCrossTaskSentFiles(explicitRunCrossTaskContext) {
278
+ const context = asyncLocalStorage.getStore() ?? null;
279
+ const runCrossTaskContext = explicitRunCrossTaskContext ?? context?.runCrossTaskContext;
280
+ if (!runCrossTaskContext) {
281
+ return;
282
+ }
283
+ runCrossTaskContext.sentFiles = [];
284
+ for (const sessionWithRef of activeSessions.values()) {
285
+ if (sessionWithRef.runCrossTaskContext === runCrossTaskContext) {
286
+ sessionWithRef.runCrossTaskContext.sentFiles = [];
287
+ }
288
+ }
289
+ }
277
290
  /**
278
291
  * Enrich a base session context with the latest taskId/messageId
279
292
  * from task-manager (supports interruption scenarios).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ynhcj/xiaoyi-channel",
3
- "version": "0.0.163-beta",
3
+ "version": "0.0.164-beta",
4
4
  "description": "OpenClaw Xiaoyi Channel plugin - Xiaoyi A2A protocol integration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",