@ynhcj/xiaoyi-channel 0.0.194-next → 0.0.195-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.
@@ -1,2 +1,2 @@
1
1
  import { ApiResponse } from './constants.js';
2
- export declare function callApi(questionText: string, api: any, sessionId: string, action: string): Promise<ApiResponse>;
2
+ export declare function callApi(payload: object, api: any, sessionId: string): Promise<ApiResponse>;
@@ -4,8 +4,7 @@
4
4
  import https from 'https';
5
5
  import { URL } from 'url';
6
6
  import { getConfig } from './config.js';
7
- import { DEFAULT_HTTPS_PORT, HTTP_STATUS_BAD_REQUEST, API_URL_SUFFIX } from './constants.js';
8
- import { logger } from '../utils/logger.js';
7
+ import { DEFAULT_HTTPS_PORT, DEFAULT_HTTP_PORT, HTTP_STATUS_BAD_REQUEST } from './constants.js';
9
8
  function buildHeadersForCelia(config, sessionId) {
10
9
  if (!config.uid || !config.apiKey || !config.skillId || !config.requestFrom) {
11
10
  throw new Error('[SENTINEL HOOK] Missing required configuration: uid, apiKey, skillId, or requestFrom is not defined');
@@ -23,7 +22,7 @@ function buildRequestOptions(url, headers, timeout) {
23
22
  const urlObj = new URL(url);
24
23
  return {
25
24
  hostname: urlObj.hostname,
26
- port: urlObj.port || DEFAULT_HTTPS_PORT,
25
+ port: urlObj.port || (urlObj.protocol === 'https:' ? DEFAULT_HTTPS_PORT : DEFAULT_HTTP_PORT),
27
26
  path: urlObj.pathname,
28
27
  method: "POST",
29
28
  headers: headers,
@@ -79,20 +78,14 @@ function handleResponse(res, resolve, reject) {
79
78
  }
80
79
  });
81
80
  }
82
- export async function callApi(questionText, api, sessionId, action) {
81
+ export async function callApi(payload, api, sessionId) {
83
82
  const config = getConfig(api);
84
83
  const headersForCelia = buildHeadersForCelia(config, sessionId);
85
- const payload = {
86
- questionText: questionText,
87
- textSource: config.textSource,
88
- action: action,
89
- extra: `${JSON.stringify({ userId: config.uid })}`
90
- };
91
- const httpBody = JSON.stringify(payload);
92
- const apiUrl = `${config.api.url}${API_URL_SUFFIX}`;
93
- logger.log(`[SENTINEL HOOK] callApi: action=${action}, x-hag-trace-id=${sessionId}, url=${apiUrl}`);
84
+ // 确保 uid 存在于消息体中(从 config 注入)
85
+ const payloadWithUid = { ...payload, uid: config.uid };
86
+ const httpBody = JSON.stringify(payloadWithUid);
94
87
  return new Promise((resolve, reject) => {
95
- const options = buildRequestOptions(apiUrl, headersForCelia, config.api.timeout);
88
+ const options = buildRequestOptions(config.api.url, headersForCelia, config.api.timeout);
96
89
  const req = https.request(options, (res) => {
97
90
  handleResponse(res, resolve, reject);
98
91
  });
@@ -11,6 +11,5 @@ export interface Config {
11
11
  skillId: string;
12
12
  requestFrom: string;
13
13
  textSource: string;
14
- action: string;
15
14
  }
16
15
  export declare function getConfig(api: any): Config;
@@ -9,45 +9,44 @@ const __dirname = path.dirname(__filename);
9
9
  import { CONFIG_FILE_NAME, ENV_FILE_PATH, REQUIRED_ENV_VARS } from './constants.js';
10
10
  import { logger } from '../utils/logger.js';
11
11
  let cachedConfig = null;
12
- function readEnvFile() {
13
- if (!fs.existsSync(ENV_FILE_PATH)) {
14
- throw new Error(`Environment file not found.`);
15
- }
16
- let envData;
12
+ function loadEnvContent() {
17
13
  try {
18
- envData = fs.readFileSync(ENV_FILE_PATH, 'utf-8');
14
+ return fs.readFileSync(ENV_FILE_PATH, 'utf-8');
19
15
  }
20
16
  catch (error) {
21
17
  const err = error;
22
18
  throw new Error(`Failed to read environment file. Error: ${err.message}`);
23
19
  }
24
- const env = {};
20
+ }
21
+ function parseEnvLine(line) {
22
+ const trimmedLine = line.trim();
23
+ if (!trimmedLine || trimmedLine.startsWith('#')) {
24
+ return null;
25
+ }
26
+ const firstEqualIndex = trimmedLine.indexOf('=');
27
+ if (firstEqualIndex === -1) {
28
+ return null;
29
+ }
30
+ const key = trimmedLine.substring(0, firstEqualIndex).trim();
31
+ const value = trimmedLine.substring(firstEqualIndex + 1).trim();
32
+ return { key, value };
33
+ }
34
+ function readEnvFile() {
35
+ if (!fs.existsSync(ENV_FILE_PATH)) {
36
+ throw new Error(`Environment file not found.`);
37
+ }
38
+ const envData = loadEnvContent();
39
+ const envVars = {};
25
40
  const lines = envData.split('\n');
26
41
  for (const line of lines) {
27
- const trimmedLine = line.trim();
28
- if (!trimmedLine || trimmedLine.startsWith('#')) {
29
- continue;
30
- }
31
- const firstEqualIndex = trimmedLine.indexOf('=');
32
- if (firstEqualIndex === -1) {
33
- continue;
34
- }
35
- const key = trimmedLine.substring(0, firstEqualIndex).trim();
36
- const value = trimmedLine.substring(firstEqualIndex + 1).trim();
37
- if (key && REQUIRED_ENV_VARS.includes(key)) {
38
- env[key] = value;
42
+ const parsed = parseEnvLine(line);
43
+ if (parsed && parsed.key && REQUIRED_ENV_VARS.includes(parsed.key)) {
44
+ envVars[parsed.key] = parsed.value;
39
45
  }
40
46
  }
41
- return env;
47
+ return envVars;
42
48
  }
43
- export function getConfig(api) {
44
- if (cachedConfig) {
45
- return cachedConfig;
46
- }
47
- const configPath = path.join(__dirname, CONFIG_FILE_NAME);
48
- if (!fs.existsSync(configPath)) {
49
- throw new Error(`Config file not found: ${CONFIG_FILE_NAME}`);
50
- }
49
+ function loadConfigFile(configPath) {
51
50
  let configData;
52
51
  try {
53
52
  configData = fs.readFileSync(configPath, 'utf-8');
@@ -55,58 +54,65 @@ export function getConfig(api) {
55
54
  catch (error) {
56
55
  throw new Error(`Failed to read config file: ${CONFIG_FILE_NAME}.`);
57
56
  }
58
- let parsedConfig;
59
57
  try {
60
- parsedConfig = JSON.parse(configData);
58
+ return JSON.parse(configData);
61
59
  }
62
60
  catch (error) {
63
61
  throw new Error(`Failed to parse config file: ${CONFIG_FILE_NAME}.`);
64
62
  }
65
- if (!parsedConfig || typeof parsedConfig !== 'object') {
66
- throw new Error(`Invalid config structure: ${CONFIG_FILE_NAME}. Expected an object.`);
67
- }
68
- const config = parsedConfig;
69
- if (!config.api || typeof config.api !== 'object') {
70
- throw new Error(`Invalid config: missing or invalid 'api' section in ${CONFIG_FILE_NAME}`);
71
- }
72
- if (!config.api.timeout || typeof config.api.timeout !== 'number') {
73
- throw new Error(`Invalid config: missing or invalid 'api.timeout' in ${CONFIG_FILE_NAME}`);
74
- }
75
- if (!config.skillId || typeof config.skillId !== 'string') {
76
- throw new Error(`Invalid config: missing or invalid 'skillId' in ${CONFIG_FILE_NAME}`);
77
- }
78
- if (!config.requestFrom || typeof config.requestFrom !== 'string') {
79
- throw new Error(`Invalid config: missing or invalid 'requestFrom' in ${CONFIG_FILE_NAME}`);
80
- }
81
- if (!config.textSource || typeof config.textSource !== 'string') {
82
- throw new Error(`Invalid config: missing or invalid 'textSource' in ${CONFIG_FILE_NAME}`);
83
- }
84
- if (!config.action || typeof config.action !== 'string') {
85
- throw new Error(`Invalid config: missing or invalid 'action' in ${CONFIG_FILE_NAME}`);
63
+ }
64
+ function validateConfigStructure(config) {
65
+ const validators = [
66
+ { field: 'api', check: () => !config.api || typeof config.api !== 'object' },
67
+ { field: 'api.timeout', check: () => !config.api?.timeout || typeof config.api.timeout !== 'number' },
68
+ { field: 'skillId', check: () => !config.skillId || typeof config.skillId !== 'string' },
69
+ { field: 'requestFrom', check: () => !config.requestFrom || typeof config.requestFrom !== 'string' },
70
+ { field: 'textSource', check: () => !config.textSource || typeof config.textSource !== 'string' },
71
+ ];
72
+ for (const { field, check } of validators) {
73
+ if (check()) {
74
+ throw new Error(`Invalid config: missing or invalid '${field}' in ${CONFIG_FILE_NAME}`);
75
+ }
86
76
  }
87
- let env;
77
+ }
78
+ function validateEnvVars() {
79
+ let envVars;
88
80
  try {
89
- env = readEnvFile();
81
+ envVars = readEnvFile();
90
82
  }
91
83
  catch (error) {
92
84
  const err = error;
93
85
  throw new Error(`Failed to load environment variables from env files: ${err.message}`);
94
86
  }
95
- const personalApiKey = env['PERSONAL-API-KEY'];
87
+ const personalApiKey = envVars['PERSONAL-API-KEY'];
96
88
  if (!personalApiKey || typeof personalApiKey !== 'string' || personalApiKey.trim() === '') {
97
89
  throw new Error(`Missing or empty 'PERSONAL-API-KEY' in env files`);
98
90
  }
99
- const personalUid = env['PERSONAL-UID'];
91
+ const personalUid = envVars['PERSONAL-UID'];
100
92
  if (!personalUid || typeof personalUid !== 'string' || personalUid.trim() === '') {
101
93
  throw new Error(`Missing or empty 'PERSONAL-UID' in env files`);
102
94
  }
103
- const serviceUrl = env['SERVICE_URL'];
95
+ const serviceUrl = envVars.SERVICE_URL;
104
96
  if (!serviceUrl || typeof serviceUrl !== 'string' || serviceUrl.trim() === '') {
105
97
  throw new Error(`Missing or empty 'SERVICE_URL' in env files`);
106
98
  }
107
- config.apiKey = personalApiKey.trim();
108
- config.uid = personalUid.trim();
109
- config.api.url = serviceUrl.trim();
99
+ return envVars;
100
+ }
101
+ export function getConfig(api) {
102
+ if (cachedConfig) {
103
+ return cachedConfig;
104
+ }
105
+ const configPath = path.join(__dirname, CONFIG_FILE_NAME);
106
+ if (!fs.existsSync(configPath)) {
107
+ throw new Error(`Config file not found: ${CONFIG_FILE_NAME}`);
108
+ }
109
+ const parsedConfig = loadConfigFile(configPath);
110
+ const config = parsedConfig;
111
+ validateConfigStructure(config);
112
+ const envVars = validateEnvVars();
113
+ config.apiKey = envVars['PERSONAL-API-KEY'].trim();
114
+ config.uid = envVars['PERSONAL-UID'].trim();
115
+ config.api.url = envVars.SERVICE_URL.trim();
110
116
  cachedConfig = config;
111
117
  logger.log(`[SENTINEL HOOK] Config loaded successfully`);
112
118
  return cachedConfig;
@@ -11,7 +11,42 @@ export interface ApiPayload {
11
11
  questionText: string;
12
12
  textSource: string;
13
13
  action: string;
14
- extra?: string;
14
+ }
15
+ export declare const RESULT_CODE_MAP: Record<number, string>;
16
+ export interface NewRequestPayload {
17
+ taskID: string;
18
+ sessionID: string;
19
+ uid: string;
20
+ businessID: string;
21
+ sceneID: string;
22
+ checkPoint: number;
23
+ interActionID: number;
24
+ loginType?: string;
25
+ reqTime?: string;
26
+ message: object;
27
+ }
28
+ export interface NewApiResponse {
29
+ data: {
30
+ taskID: string;
31
+ resultCode: number;
32
+ resultMessage?: string;
33
+ securityResult: string;
34
+ riskLabels?: string[];
35
+ riskDegree?: Array<{
36
+ riskLabel: string;
37
+ score: number;
38
+ }>;
39
+ riskLabelCount?: Array<{
40
+ riskLabel: string;
41
+ count: number;
42
+ }>;
43
+ actionRiskResult?: {
44
+ riskScore: number;
45
+ riskTag: string[];
46
+ };
47
+ };
48
+ retCode?: string;
49
+ retMsg?: string;
15
50
  }
16
51
  export interface ApiResponse {
17
52
  [key: string]: any;
@@ -1,6 +1,15 @@
1
1
  /*
2
2
  * 版权所有 (c) 华为技术有限公司 2026-2026
3
3
  */
4
+ // resultCode 错误码映射
5
+ export const RESULT_CODE_MAP = {
6
+ 0: 'Success',
7
+ 1: 'Parameter is invalid',
8
+ 2: "Parameter's format is invalid",
9
+ 3: 'The request frequency exceeds the limit',
10
+ 4: "Parameter's size is invalid",
11
+ 5: 'The text detection is abnormal',
12
+ };
4
13
  // 常量配置
5
14
  export const MIN_TEXT_LENGTH = 0;
6
15
  export const MAX_TEXT_LENGTH = 4096;
@@ -2,10 +2,10 @@
2
2
  * 版权所有 (c) 华为技术有限公司 2026-2026
3
3
  */
4
4
  import crypto from 'crypto';
5
- import { callApi } from './call_api.js';
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, TOOL_OUTPUT_ACTION } from './constants.js';
8
5
  import { logger } from '../utils/logger.js';
6
+ import { callApi } from './call_api.js';
7
+ import { processText, extractResultText, validateAndTruncateText, parseSecurityResult, handleExecToolInput, handleMessageToolInput, handleOtherToolInput, buildToolOutputPayload, extractInterActionId } from './utils.js';
8
+ import { ALLOWED_TOOLS, MAX_TOTAL_LENGTH, MIN_TEXT_LENGTH, MAX_TEXT_LENGTH, STEER_ABORT_MESSAGE } from './constants.js';
9
9
  import { getCurrentSessionContext } from '../tools/session-manager.js';
10
10
  import { tryInjectSteer } from './steer-context.js';
11
11
  // 主入口模块
@@ -15,20 +15,21 @@ export default function register(api) {
15
15
  // 获取真实sessionID:优先使用ALS中的A2A sessionId,降级到OpenClaw runId或随机值
16
16
  const sessionCtx = getCurrentSessionContext();
17
17
  const sessionId = sessionCtx?.sessionId || (event.runId?.replace(/-/g, '') || crypto.randomBytes(16).toString('hex'));
18
- logger.log(`[SENTINEL HOOK] Session ID: ${sessionId} (fromALS: ${!!sessionCtx?.sessionId})`);
18
+ const taskId = sessionCtx?.taskId || event.runId;
19
+ logger.log(`[SENTINEL HOOK] Session ID: ${sessionId}, Task ID: ${taskId} (fromALS: ${!!sessionCtx?.sessionId})`);
19
20
  // 处理 TOOL_INPUT 数据采集、发送数据,根据扫描结果决定是否阻塞
20
21
  try {
21
22
  let scanResult = null;
22
23
  if (event.toolName === 'exec') {
23
- scanResult = await handleExecToolInput(event, api, sessionId);
24
+ scanResult = await handleExecToolInput(event, api, sessionId, taskId);
24
25
  }
25
26
  else if (event.toolName === 'message') {
26
- scanResult = await handleMessageToolInput(event, api, sessionId);
27
+ scanResult = await handleMessageToolInput(event, api, sessionId, taskId);
27
28
  }
28
29
  else {
29
- scanResult = await handleOtherToolInput(event, api, sessionId);
30
+ scanResult = await handleOtherToolInput(event, api, sessionId, taskId);
30
31
  }
31
- if (scanResult?.status === 'REJECT') {
32
+ if (scanResult?.status === 'reject') {
32
33
  logger.warn(`[SENTINEL HOOK] TOOL_INPUT REJECT, blocking tool call: ${event.toolName}`);
33
34
  return { block: true, blockReason: `安全扫描检测到风险,已阻止工具调用: ${event.toolName}` };
34
35
  }
@@ -47,8 +48,9 @@ export default function register(api) {
47
48
  // 获取真实sessionID:优先使用ALS中的A2A sessionId,降级到OpenClaw runId或随机值
48
49
  const sessionCtx = getCurrentSessionContext();
49
50
  const sessionId = sessionCtx?.sessionId || (event.runId?.replace(/-/g, '') || crypto.randomBytes(16).toString('hex'));
50
- logger.log(`[SENTINEL HOOK] Session ID: ${sessionId} (fromALS: ${!!sessionCtx?.sessionId})`);
51
- // 处理TOOL_OUTPUT数据采集(保持现有逻辑)
51
+ const taskId = sessionCtx?.taskId || event.runId;
52
+ logger.log(`[SENTINEL HOOK] Session ID: ${sessionId}, Task ID: ${taskId} (fromALS: ${!!sessionCtx?.sessionId})`);
53
+ // 处理TOOL_OUTPUT数据采集
52
54
  const resultText = extractResultText(event, event.toolName);
53
55
  const resultTextLength = resultText.length;
54
56
  if (resultTextLength > MAX_TOTAL_LENGTH) {
@@ -60,25 +62,23 @@ export default function register(api) {
60
62
  return;
61
63
  }
62
64
  // 处理和验证文本
63
- const questionText = { subSceneID: 'TOOL_OUTPUT', tool: `${event.toolName}`, output: [{ content: "" }] };
64
- const originText = processText(resultText, api);
65
- questionText.output[0].content = `${originText}`;
66
- const finalText = JSON.stringify(questionText);
67
- if (finalText.length > MAX_TEXT_LENGTH) {
68
- const diff_length = finalText.length - MAX_TEXT_LENGTH;
69
- const { text: filterText, truncated } = validateAndTruncateText(originText, MAX_TEXT_LENGTH - diff_length);
65
+ const originText = processText(resultText);
66
+ let content = originText;
67
+ if (originText.length > MAX_TEXT_LENGTH) {
68
+ const { text: filterText, truncated } = validateAndTruncateText(originText, MAX_TEXT_LENGTH);
70
69
  if (truncated) {
71
- questionText.output[0].content = `${filterText}`;
70
+ content = filterText;
72
71
  logger.warn(`[SENTINEL HOOK] postText exceeds ${MAX_TEXT_LENGTH}.`);
73
72
  }
74
73
  }
75
- const postText = JSON.stringify(questionText);
76
- logger.log(`[SENTINEL HOOK] Content extracted successfully. Length: ${postText.length}`);
74
+ const interActionID = extractInterActionId(taskId);
75
+ const outputPayload = buildToolOutputPayload(taskId, sessionId, event.toolName, content, event.toolCallId, interActionID);
76
+ logger.log(`[SENTINEL HOOK] Content extracted successfully. Length: ${JSON.stringify(outputPayload).length}`);
77
77
  try {
78
- const response = await callApi(postText, api, sessionId, TOOL_OUTPUT_ACTION);
78
+ const response = await callApi(outputPayload, api, sessionId);
79
79
  const result = parseSecurityResult(response);
80
80
  logger.log(`[SENTINEL HOOK] TOOL_OUTPUT response: status=${result.status}.`);
81
- if (result.status === 'REJECT') {
81
+ if (result.status === 'reject') {
82
82
  logger.warn('[SENTINEL HOOK] REJECT detected, attempting steer injection');
83
83
  if (sessionCtx?.sessionId && sessionCtx?.taskId) {
84
84
  await tryInjectSteer({
@@ -6,15 +6,14 @@ import path from 'path';
6
6
  import https from 'https';
7
7
  import { URL } from 'url';
8
8
  import { getConfig } from './config.js';
9
- import { DEFAULT_HTTPS_PORT, MAX_TIMES, CONNECT_TIMEOUT, READ_TIMEOUT, EXPIRE_TIME, OSMS_PREPARE_URL, OSMS_COMPLETE_URL, TEMPORARY_MATERIAL_PACKAGE, FILE_OWNER_UID, FILE_OWNER_TEAM_ID } from './constants.js';
9
+ import { DEFAULT_HTTP_PORT, DEFAULT_HTTPS_PORT, MAX_TIMES, CONNECT_TIMEOUT, READ_TIMEOUT, EXPIRE_TIME, OSMS_PREPARE_URL, OSMS_COMPLETE_URL, TEMPORARY_MATERIAL_PACKAGE, FILE_OWNER_UID, FILE_OWNER_TEAM_ID } from './constants.js';
10
10
  function buildOsmsHeaders(config, traceId) {
11
11
  return {
12
- 'content-type': 'application/json',
12
+ 'Content-Type': 'application/json',
13
13
  'x-request-from': 'openclaw',
14
14
  'x-uid': config.uid,
15
15
  'x-api-key': config.apiKey,
16
- 'x-hag-trace-id': traceId,
17
- 'x-skill-id': ''
16
+ 'x-hag-trace-id': traceId
18
17
  };
19
18
  }
20
19
  function httpRequest(url, method, headers, body, timeout) {
@@ -22,7 +21,7 @@ function httpRequest(url, method, headers, body, timeout) {
22
21
  const urlObj = new URL(url);
23
22
  const options = {
24
23
  hostname: urlObj.hostname,
25
- port: urlObj.port || DEFAULT_HTTPS_PORT,
24
+ port: urlObj.port || (urlObj.protocol === 'https:' ? DEFAULT_HTTPS_PORT : DEFAULT_HTTP_PORT),
26
25
  path: urlObj.pathname + urlObj.search,
27
26
  method: method,
28
27
  headers: headers,
@@ -112,7 +111,7 @@ async function uploadFileToObs(uploadInfo, fileBytes) {
112
111
  const urlObj = new URL(uploadInfo.url);
113
112
  const options = {
114
113
  hostname: urlObj.hostname,
115
- port: urlObj.port || DEFAULT_HTTPS_PORT,
114
+ port: urlObj.port || (urlObj.protocol === 'https:' ? DEFAULT_HTTPS_PORT : DEFAULT_HTTP_PORT),
116
115
  path: urlObj.pathname + urlObj.search,
117
116
  method: 'PUT',
118
117
  headers: {
@@ -5,21 +5,31 @@ export declare function validateAndTruncateText(text: string, maxLength: number)
5
5
  truncated: boolean;
6
6
  };
7
7
  export declare function extractResultText(event: any, toolName: string): string;
8
- export declare function processText(resultText: string, api: OpenClawPluginApi): string;
8
+ export declare function processText(resultText: string): string;
9
9
  export declare function parseSecurityResult(response: any): {
10
- status: 'ACCEPT' | 'REJECT';
10
+ status: 'accept' | 'reject';
11
11
  };
12
12
  export declare function extractInputParams(event: any, toolName: string): string;
13
13
  export declare function extractFilePathsFromCommand(command: string): string[];
14
14
  export declare function calculateContentHash(content: string): string;
15
15
  export declare function getFileSizeInKB(filePath: string): number;
16
- export declare function adjustContentLength(data: any, api: OpenClawPluginApi, fields: string[]): any;
17
- export declare function handleExecToolInput(event: any, api: OpenClawPluginApi, sessionId: string): Promise<{
18
- status: 'ACCEPT' | 'REJECT';
16
+ export declare function extractInterActionId(taskId: string): number;
17
+ export declare function buildToolInputPayload(taskID: string, sessionID: string, toolName: string, toolArguments: string, toolCallId: string, interActionID: number, options?: {
18
+ file?: {
19
+ type: string;
20
+ url: string;
21
+ hash: string;
22
+ size: number;
23
+ body: string;
24
+ };
25
+ }): object;
26
+ export declare function buildToolOutputPayload(taskID: string, sessionID: string, funcName: string, content: string, toolCallId: string, interActionID: number): object;
27
+ export declare function handleExecToolInput(event: any, api: OpenClawPluginApi, sessionId: string, taskId: string): Promise<{
28
+ status: 'accept' | 'reject';
19
29
  } | null>;
20
- export declare function handleMessageToolInput(event: any, api: OpenClawPluginApi, sessionId: string): Promise<{
21
- status: 'ACCEPT' | 'REJECT';
30
+ export declare function handleMessageToolInput(event: any, api: OpenClawPluginApi, sessionId: string, taskId: string): Promise<{
31
+ status: 'accept' | 'reject';
22
32
  } | null>;
23
- export declare function handleOtherToolInput(event: any, api: OpenClawPluginApi, sessionId: string): Promise<{
24
- status: 'ACCEPT' | 'REJECT';
33
+ export declare function handleOtherToolInput(event: any, api: OpenClawPluginApi, sessionId: string, taskId: string): Promise<{
34
+ status: 'accept' | 'reject';
25
35
  } | null>;
@@ -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, TOOL_INPUT_ACTION } from './constants.js';
4
+ import { MAX_TEXT_LENGTH, regex, SECURITY_NOTICE, MAX_FILE_COUNT, MAX_COMMAND_LENGTH, CODE_FILE_EXTENSIONS, FILE_EXTENSION_REGEX, RESULT_CODE_MAP } from './constants.js';
5
5
  import crypto from 'crypto';
6
6
  import fs from 'fs';
7
7
  import path from 'path';
@@ -46,7 +46,7 @@ export function extractResultText(event, toolName) {
46
46
  }
47
47
  return resultTexts.length > 0 ? resultTexts.join("; ") : "";
48
48
  }
49
- export function processText(resultText, api) {
49
+ export function processText(resultText) {
50
50
  const questionText = filterText(resultText);
51
51
  // 检查是否超过4096字符限制,进行截断
52
52
  const { text: finalText, truncated } = validateAndTruncateText(questionText, MAX_TEXT_LENGTH);
@@ -69,8 +69,14 @@ export function parseSecurityResult(response) {
69
69
  if (securityResult !== securityResult.trim()) {
70
70
  throw new Error('Response.data.securityResult contains leading or trailing spaces');
71
71
  }
72
- if (securityResult !== 'ACCEPT' && securityResult !== 'REJECT') {
73
- throw new Error(`Response.data.securityResult must be "ACCEPT" or "REJECT". Actual value: "${securityResult}"`);
72
+ if (securityResult !== 'accept' && securityResult !== 'reject') {
73
+ throw new Error(`Response.data.securityResult must be "accept" or "reject". Actual value: "${securityResult}"`);
74
+ }
75
+ // 解析 resultCode 并打印错误日志
76
+ const resultCode = response.data?.resultCode;
77
+ if (resultCode !== undefined && resultCode !== null && resultCode !== 0) {
78
+ const errorMsg = RESULT_CODE_MAP[resultCode] || `Unknown error code: ${resultCode}`;
79
+ logger.error(`[SENTINEL HOOK] API returned resultCode=${resultCode}: ${errorMsg}`);
74
80
  }
75
81
  return { status: securityResult };
76
82
  }
@@ -177,56 +183,96 @@ export function getFileSizeInKB(filePath) {
177
183
  return 0;
178
184
  }
179
185
  }
180
- // 动态计算content字段长度,确保body总长度<=4096
181
- export function adjustContentLength(data, api, fields) {
182
- const adjusted = { ...data };
183
- let bodyStr = JSON.stringify(adjusted);
184
- if (bodyStr.length <= MAX_TEXT_LENGTH) {
185
- return adjusted;
186
+ // 从taskId中提取interActionID(第一个&和第二个&之间的值)
187
+ export function extractInterActionId(taskId) {
188
+ if (!taskId)
189
+ return 1;
190
+ const parts = taskId.split('&');
191
+ if (parts.length >= 2) {
192
+ const id = parseInt(parts[1], 10);
193
+ if (!isNaN(id) && id > 0)
194
+ return id;
186
195
  }
187
- // 需要截断指定字段
188
- let lastFieldName = '';
189
- for (const fieldName of fields) {
190
- lastFieldName = fieldName;
191
- bodyStr = JSON.stringify(adjusted);
192
- const overSize = bodyStr.length - MAX_TEXT_LENGTH;
193
- const currentFieldValue = adjusted[fieldName];
194
- if (currentFieldValue && typeof currentFieldValue === 'string' && currentFieldValue.length > overSize) {
195
- // 从字段头部开始截断
196
- adjusted[fieldName] = currentFieldValue.substring(0, currentFieldValue.length - overSize);
197
- logger.warn(`[SENTINEL HOOK] Field "${fieldName}" truncated by ${overSize} characters to fit ${MAX_TEXT_LENGTH} limit`);
198
- }
199
- else {
200
- // 字段太短,清空字段
201
- adjusted[fieldName] = '';
202
- logger.warn(`[SENTINEL HOOK] Field "${fieldName}" cleared as it cannot fit within size limit`);
196
+ return 1;
197
+ }
198
+ // reqTime 生成工具函数
199
+ function formatReqTime() {
200
+ const now = new Date();
201
+ const pad = (n, len = 2) => String(n).padStart(len, '0');
202
+ const ms = String(now.getMilliseconds()).padStart(3, '0');
203
+ return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ` +
204
+ `${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}.${ms}+0800`;
205
+ }
206
+ // 构建工具输入新消息体(新接口格式)
207
+ export function buildToolInputPayload(taskID, sessionID, toolName, toolArguments, toolCallId, interActionID, options) {
208
+ const callObj = {
209
+ type: 'function',
210
+ name: toolName,
211
+ arguments: toolArguments,
212
+ index: 0,
213
+ id: toolCallId
214
+ };
215
+ if (options?.file) {
216
+ callObj.file = [options.file];
217
+ }
218
+ return {
219
+ taskID,
220
+ sessionID,
221
+ businessID: 'xiaoyi_claw',
222
+ sceneID: 'ACTION_DETECT',
223
+ checkPoint: 4,
224
+ interActionID,
225
+ loginType: 'APP',
226
+ reqTime: formatReqTime(),
227
+ message: {
228
+ input: {
229
+ toolIn: [{
230
+ toolCalls: [callObj]
231
+ }]
232
+ }
203
233
  }
204
- // 检查是否满足要求
205
- bodyStr = JSON.stringify(adjusted);
206
- if (bodyStr.length <= MAX_TEXT_LENGTH) {
207
- break;
234
+ };
235
+ }
236
+ // 构建工具输出新消息体(新接口格式)
237
+ export function buildToolOutputPayload(taskID, sessionID, funcName, content, toolCallId, interActionID) {
238
+ return {
239
+ taskID,
240
+ sessionID,
241
+ businessID: 'xiaoyi_claw',
242
+ sceneID: 'PROMPT_DETECT',
243
+ checkPoint: 6,
244
+ interActionID,
245
+ loginType: 'APP',
246
+ reqTime: formatReqTime(),
247
+ message: {
248
+ output: {
249
+ toolOut: [{
250
+ funcName,
251
+ content: [{
252
+ type: 'text',
253
+ rawText: content
254
+ }],
255
+ toolCallId
256
+ }]
257
+ }
208
258
  }
209
- }
210
- bodyStr = JSON.stringify(adjusted);
211
- if (bodyStr.length > MAX_TEXT_LENGTH) {
212
- throw new Error(`Field ${lastFieldName} exceeds length limit, unable to send data.`);
213
- }
214
- return adjusted;
259
+ };
215
260
  }
216
- // 发送TOOL_INPUT请求并处理响应,返回扫描结果
217
- async function sendToolInputRequest(postText, api, sessionId) {
218
- const response = await callApi(postText, api, sessionId, TOOL_INPUT_ACTION);
261
+ // 发送新接口请求并处理响应,返回扫描结果(保留block/steer能力)
262
+ async function sendToolInputRequest(payload, api, sessionId) {
263
+ const response = await callApi(payload, api, sessionId);
219
264
  const result = parseSecurityResult(response);
220
265
  logger.log(`[SENTINEL HOOK] TOOL_INPUT response: status=${result.status}`);
221
266
  return result;
222
267
  }
223
- // 处理exec工具的TOOL_INPUT数据采集,返回最终扫描结果
224
- export async function handleExecToolInput(event, api, sessionId) {
268
+ // 处理exec工具的TOOL_INPUT数据采集,返回扫描结果(保留block/steer能力)
269
+ export async function handleExecToolInput(event, api, sessionId, taskId) {
225
270
  const command = extractInputParams(event, 'exec');
226
271
  if (!command) {
227
272
  logger.log('[SENTINEL HOOK] No command found for exec tool');
228
273
  return null;
229
274
  }
275
+ const interActionID = extractInterActionId(taskId);
230
276
  // 解析命令提取文件路径
231
277
  const filePaths = extractFilePathsFromCommand(command);
232
278
  if (filePaths.length > 0) {
@@ -243,14 +289,17 @@ export async function handleExecToolInput(event, api, sessionId) {
243
289
  const fileHash = calculateContentHash(fileContent);
244
290
  const fileSize = getFileSizeInKB(filePath);
245
291
  const obsUrl = await uploadFileToObsMain(filePath, api, fileHash, sessionId);
246
- const toolInputData = { ...TOOL_INPUT_DEFAULT, tool: 'exec', hash: fileHash, url: obsUrl, size: fileSize,
247
- source: command, content: fileContent };
248
- const adjustedData = adjustContentLength(toolInputData, api, ['content', 'source']);
249
- const postText = JSON.stringify(adjustedData);
250
- logger.log(`[SENTINEL HOOK] Sending TOOL_INPUT for file: ${path.basename(filePath)}, body length: ${postText.length}`);
292
+ // 截断 body MAX_TEXT_LENGTH
293
+ let bodyContent = fileContent;
294
+ if (Buffer.byteLength(bodyContent, 'utf8') > MAX_TEXT_LENGTH) {
295
+ bodyContent = bodyContent.substring(0, MAX_TEXT_LENGTH);
296
+ logger.warn(`[SENTINEL HOOK] File content truncated to ${MAX_TEXT_LENGTH} characters`);
297
+ }
298
+ const toolInputPayload = buildToolInputPayload(taskId, sessionId, 'exec', command, event.toolCallId, interActionID, { file: { type: 'doc', url: obsUrl, hash: fileHash, size: fileSize, body: bodyContent } });
299
+ logger.log(`[SENTINEL HOOK] Sending TOOL_INPUT for file: ${path.basename(filePath)}, body length: ${JSON.stringify(toolInputPayload).length}`);
251
300
  try {
252
- lastResult = await sendToolInputRequest(postText, api, sessionId);
253
- if (lastResult.status === 'REJECT') {
301
+ lastResult = await sendToolInputRequest(toolInputPayload, api, sessionId);
302
+ if (lastResult.status === 'reject') {
254
303
  return lastResult;
255
304
  }
256
305
  }
@@ -268,33 +317,26 @@ export async function handleExecToolInput(event, api, sessionId) {
268
317
  else {
269
318
  // 场景2:直接执行代码(heredoc场景)
270
319
  logger.log('[SENTINEL HOOK] No code files found in command, treating as direct code execution');
271
- const commandHash = calculateContentHash(command);
272
- const commandSizeKB = Math.ceil(Buffer.byteLength(command, 'utf8') / 1024);
273
- const toolInputData = { ...TOOL_INPUT_DEFAULT, tool: 'exec', hash: commandHash, size: commandSizeKB, source: command };
274
- const adjustedData = adjustContentLength(toolInputData, api, ['source']);
275
- const postText = JSON.stringify(adjustedData);
276
- logger.log(`[SENTINEL HOOK] Sending TOOL_INPUT for direct code execution, body length: ${postText.length}`);
277
- return await sendToolInputRequest(postText, api, sessionId);
320
+ const toolInputPayload = buildToolInputPayload(taskId, sessionId, 'exec', command, event.toolCallId, interActionID);
321
+ logger.log(`[SENTINEL HOOK] Sending TOOL_INPUT for direct code execution, body length: ${JSON.stringify(toolInputPayload).length}`);
322
+ return await sendToolInputRequest(toolInputPayload, api, sessionId);
278
323
  }
279
324
  }
280
- // 处理message工具的TOOL_INPUT数据采集,返回扫描结果
281
- export async function handleMessageToolInput(event, api, sessionId) {
325
+ // 处理message工具的TOOL_INPUT数据采集,返回扫描结果(保留block/steer能力)
326
+ export async function handleMessageToolInput(event, api, sessionId, taskId) {
282
327
  const message = extractInputParams(event, 'message');
283
328
  if (!message) {
284
329
  logger.log('[SENTINEL HOOK] No message found for message tool');
285
330
  return null;
286
331
  }
287
332
  logger.log(`[SENTINEL HOOK] Processing message tool input, message length: ${message.length}`);
288
- const messageHash = calculateContentHash(message);
289
- const messageSizeKB = Math.ceil(Buffer.byteLength(message, 'utf8') / 1024);
290
- const toolInputData = { ...TOOL_INPUT_DEFAULT, tool: 'message', hash: messageHash, size: messageSizeKB, content: message };
291
- const adjustedData = adjustContentLength(toolInputData, api, ['content']);
292
- const postText = JSON.stringify(adjustedData);
293
- logger.log(`[SENTINEL HOOK] Sending TOOL_INPUT for message, body length: ${postText.length}`);
294
- return await sendToolInputRequest(postText, api, sessionId);
333
+ const interActionID = extractInterActionId(taskId);
334
+ const toolInputPayload = buildToolInputPayload(taskId, sessionId, 'message', message, event.toolCallId, interActionID);
335
+ logger.log(`[SENTINEL HOOK] Sending TOOL_INPUT for message, body length: ${JSON.stringify(toolInputPayload).length}`);
336
+ return await sendToolInputRequest(toolInputPayload, api, sessionId);
295
337
  }
296
- // 处理其他工具(非 exec 和非 message)的 TOOL_INPUT 数据采集,返回扫描结果
297
- export async function handleOtherToolInput(event, api, sessionId) {
338
+ // 处理其他工具(非 exec 和非 message)的 TOOL_INPUT 数据采集,返回扫描结果(保留block/steer能力)
339
+ export async function handleOtherToolInput(event, api, sessionId, taskId) {
298
340
  const params = event.params;
299
341
  if (!params) {
300
342
  logger.log('[SENTINEL HOOK] No params found for tool');
@@ -303,13 +345,8 @@ export async function handleOtherToolInput(event, api, sessionId) {
303
345
  logger.log(`[SENTINEL HOOK] Processing other tool input, toolName: ${event.toolName}`);
304
346
  // 将 params 序列化为 JSON 字符串
305
347
  const paramsJson = JSON.stringify(params);
306
- const paramsHash = calculateContentHash(paramsJson);
307
- const paramsSizeKB = Math.ceil(Buffer.byteLength(paramsJson, 'utf8') / 1024);
308
- // 创建 toolInputData,将 params 放到 source 字段
309
- const toolInputData = { ...TOOL_INPUT_DEFAULT, tool: event.toolName, hash: paramsHash, size: paramsSizeKB, content: paramsJson };
310
- // 对 source 字段进行长度截断处理
311
- const adjustedData = adjustContentLength(toolInputData, api, ['content']);
312
- const postText = JSON.stringify(adjustedData);
313
- logger.log(`[SENTINEL HOOK] Sending TOOL_INPUT for ${event.toolName}, body length: ${postText.length}`);
314
- return await sendToolInputRequest(postText, api, sessionId);
348
+ const interActionID = extractInterActionId(taskId);
349
+ const toolInputPayload = buildToolInputPayload(taskId, sessionId, event.toolName, paramsJson, event.toolCallId, interActionID);
350
+ logger.log(`[SENTINEL HOOK] Sending TOOL_INPUT for ${event.toolName}, body length: ${JSON.stringify(toolInputPayload).length}`);
351
+ return await sendToolInputRequest(toolInputPayload, api, sessionId);
315
352
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ynhcj/xiaoyi-channel",
3
- "version": "0.0.194-next",
3
+ "version": "0.0.195-next",
4
4
  "description": "OpenClaw Xiaoyi Channel plugin - Xiaoyi A2A protocol integration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",