@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.
- package/dist/src/cspl/call_api.d.ts +1 -1
- package/dist/src/cspl/call_api.js +7 -14
- package/dist/src/cspl/config.d.ts +0 -1
- package/dist/src/cspl/config.js +65 -59
- package/dist/src/cspl/constants.d.ts +36 -1
- package/dist/src/cspl/constants.js +9 -0
- package/dist/src/cspl/sentinel_hook.js +22 -22
- package/dist/src/cspl/upload_file.js +5 -6
- package/dist/src/cspl/utils.d.ts +19 -9
- package/dist/src/cspl/utils.js +112 -75
- package/package.json +1 -1
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import { ApiResponse } from './constants.js';
|
|
2
|
-
export declare function callApi(
|
|
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,
|
|
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(
|
|
81
|
+
export async function callApi(payload, api, sessionId) {
|
|
83
82
|
const config = getConfig(api);
|
|
84
83
|
const headersForCelia = buildHeadersForCelia(config, sessionId);
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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(
|
|
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
|
});
|
package/dist/src/cspl/config.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
28
|
-
if (
|
|
29
|
-
|
|
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
|
|
47
|
+
return envVars;
|
|
42
48
|
}
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
77
|
+
}
|
|
78
|
+
function validateEnvVars() {
|
|
79
|
+
let envVars;
|
|
88
80
|
try {
|
|
89
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 === '
|
|
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
|
-
|
|
51
|
-
|
|
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
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
70
|
+
content = filterText;
|
|
72
71
|
logger.warn(`[SENTINEL HOOK] postText exceeds ${MAX_TEXT_LENGTH}.`);
|
|
73
72
|
}
|
|
74
73
|
}
|
|
75
|
-
const
|
|
76
|
-
|
|
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(
|
|
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 === '
|
|
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
|
-
'
|
|
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: {
|
package/dist/src/cspl/utils.d.ts
CHANGED
|
@@ -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
|
|
8
|
+
export declare function processText(resultText: string): string;
|
|
9
9
|
export declare function parseSecurityResult(response: any): {
|
|
10
|
-
status: '
|
|
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
|
|
17
|
-
export declare function
|
|
18
|
-
|
|
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: '
|
|
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: '
|
|
33
|
+
export declare function handleOtherToolInput(event: any, api: OpenClawPluginApi, sessionId: string, taskId: string): Promise<{
|
|
34
|
+
status: 'accept' | 'reject';
|
|
25
35
|
} | null>;
|
package/dist/src/cspl/utils.js
CHANGED
|
@@ -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,
|
|
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
|
|
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 !== '
|
|
73
|
-
throw new Error(`Response.data.securityResult must be "
|
|
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
|
-
//
|
|
181
|
-
export function
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
//
|
|
217
|
-
async function sendToolInputRequest(
|
|
218
|
-
const response = await callApi(
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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(
|
|
253
|
-
if (lastResult.status === '
|
|
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
|
|
272
|
-
|
|
273
|
-
|
|
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
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
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
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
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
|
}
|