@ynhcj/xiaoyi-channel 1.1.26 → 1.1.27

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.
@@ -0,0 +1,107 @@
1
+ /*
2
+ * 版权所有 (c) 华为技术有限公司 2026-2026
3
+ */
4
+ import https from 'https';
5
+ import { URL } from 'url';
6
+ import { getConfig } from './config.js';
7
+ import { DEFAULT_HTTP_PORT, HTTP_STATUS_BAD_REQUEST, API_URL_SUFFIX } from './constants.js';
8
+ function buildHeadersForCelia(config, sessionId) {
9
+ if (!config.uid || !config.apiKey || !config.skillId || !config.requestFrom) {
10
+ throw new Error('[SENTINEL HOOK] Missing required configuration: uid, apiKey, skillId, or requestFrom is not defined');
11
+ }
12
+ return {
13
+ 'x-hag-trace-id': sessionId,
14
+ 'x-uid': config.uid,
15
+ 'x-api-key': config.apiKey,
16
+ 'x-request-from': config.requestFrom,
17
+ 'x-skill-id': config.skillId,
18
+ 'content-type': 'application/json'
19
+ };
20
+ }
21
+ function buildRequestOptions(url, headers, timeout) {
22
+ const urlObj = new URL(url);
23
+ return {
24
+ hostname: urlObj.hostname,
25
+ port: urlObj.port || DEFAULT_HTTP_PORT,
26
+ path: urlObj.pathname,
27
+ method: "POST",
28
+ headers: headers,
29
+ timeout: timeout
30
+ };
31
+ }
32
+ function checkHttpStatus(res) {
33
+ if (res.statusCode && res.statusCode >= HTTP_STATUS_BAD_REQUEST) {
34
+ throw new Error(`HTTP error! status: ${res.statusCode}`);
35
+ }
36
+ }
37
+ function parseResponseData(data) {
38
+ try {
39
+ if (data === undefined || data === null || data.trim() === '') {
40
+ throw new Error('API response data is empty or invalid');
41
+ }
42
+ const jsonData = JSON.parse(data);
43
+ if (jsonData.retCode && jsonData.retCode !== "0") {
44
+ const errorMsg = jsonData.retMsg || 'Unknown API error';
45
+ throw new Error(`API error: ${errorMsg}`);
46
+ }
47
+ if (!jsonData.retCode && jsonData.code) {
48
+ const errorMsg = jsonData.desc || 'Unknown backend error';
49
+ throw new Error(`Backend error: ${errorMsg}`);
50
+ }
51
+ return jsonData;
52
+ }
53
+ catch (e) {
54
+ if (e instanceof Error) {
55
+ throw new Error(`[SENTINEL HOOK] Failed to parse response:${e.message}`);
56
+ }
57
+ return data;
58
+ }
59
+ }
60
+ function handleResponse(res, resolve, reject) {
61
+ let data = '';
62
+ try {
63
+ checkHttpStatus(res);
64
+ }
65
+ catch (e) {
66
+ reject(e);
67
+ }
68
+ res.on('data', (chunk) => {
69
+ data += chunk;
70
+ });
71
+ res.on('end', () => {
72
+ try {
73
+ const result = parseResponseData(data);
74
+ resolve(result);
75
+ }
76
+ catch (e) {
77
+ reject(e);
78
+ }
79
+ });
80
+ }
81
+ export async function callApi(questionText, api, sessionId) {
82
+ const config = getConfig(api);
83
+ const headersForCelia = buildHeadersForCelia(config, sessionId);
84
+ const payload = {
85
+ questionText: questionText,
86
+ textSource: config.textSource,
87
+ action: config.action,
88
+ extra: `${JSON.stringify({ userId: config.uid })}`
89
+ };
90
+ const httpBody = JSON.stringify(payload);
91
+ const apiUrl = `${config.api.url}${API_URL_SUFFIX}`;
92
+ return new Promise((resolve, reject) => {
93
+ const options = buildRequestOptions(apiUrl, headersForCelia, config.api.timeout);
94
+ const req = https.request(options, (res) => {
95
+ handleResponse(res, resolve, reject);
96
+ });
97
+ req.on('error', (error) => {
98
+ reject(error);
99
+ });
100
+ req.on('timeout', () => {
101
+ req.destroy();
102
+ reject(new Error('[SENTINEL HOOK] Request timeout'));
103
+ });
104
+ req.write(httpBody);
105
+ req.end();
106
+ });
107
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "api": {
3
+ "timeout": 5000
4
+ },
5
+ "headers": {},
6
+ "skillId": "skill-scope",
7
+ "requestFrom": "openclaw",
8
+ "textSource": "question",
9
+ "action": "TOOL_OUTPUT_SCAN"
10
+ }
@@ -0,0 +1,2 @@
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
+ export default function register(api: OpenClawPluginApi): void;
@@ -0,0 +1,98 @@
1
+ /*
2
+ * 版权所有 (c) 华为技术有限公司 2026-2026
3
+ */
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 } from './constants.js';
8
+ import { logger } from '../utils/logger.js';
9
+ import { getSessionContext } from '../tools/session-manager.js';
10
+ import { tryInjectSteer } from './steer-context.js';
11
+ // 主入口模块
12
+ export default function register(api) {
13
+ api.on("before_tool_call", async (event, ctx) => {
14
+ logger.log(`[SENTINEL HOOK] before_tool_call_event toolName: ${event.toolName}`);
15
+ // 生成sessionID
16
+ const sessionId = (event.runId?.replace(/-/g, '') || crypto.randomBytes(16).toString('hex'));
17
+ logger.log(`[SENTINEL HOOK] Generated Session ID: ${sessionId}`);
18
+ // 处理 TOOL_INPUT 数据采集、发送数据
19
+ try {
20
+ if (event.toolName === 'exec') {
21
+ await handleExecToolInput(event, api, sessionId);
22
+ }
23
+ else if (event.toolName === 'message') {
24
+ await handleMessageToolInput(event, api, sessionId);
25
+ }
26
+ else {
27
+ await handleOtherToolInput(event, api, sessionId);
28
+ }
29
+ }
30
+ catch (error) {
31
+ logger.error(`[SENTINEL HOOK] Extracted TOOL_INPUT data processing exception: ${error}`);
32
+ }
33
+ });
34
+ api.on("after_tool_call", async (event, ctx) => {
35
+ // 检查是否在输出白名单中
36
+ if (!ALLOWED_TOOLS.includes(event.toolName)) {
37
+ return;
38
+ }
39
+ try {
40
+ logger.log(`[SENTINEL HOOK] after_tool_call_event toolName: ${event.toolName}`);
41
+ // 生成sessionID
42
+ const sessionId = (event.runId?.replace(/-/g, '') || crypto.randomBytes(16).toString('hex'));
43
+ logger.log(`[SENTINEL HOOK] Generated Session ID: ${sessionId}`);
44
+ // 处理TOOL_OUTPUT数据采集(保持现有逻辑)
45
+ const resultText = extractResultText(event, event.toolName);
46
+ const resultTextLength = resultText.length;
47
+ if (resultTextLength > MAX_TOTAL_LENGTH) {
48
+ logger.warn(`[SENTINEL HOOK] Text exceeds ${MAX_TOTAL_LENGTH} character limit. Actual length: ${resultTextLength}`);
49
+ return;
50
+ }
51
+ if (resultTextLength <= MIN_TEXT_LENGTH) {
52
+ logger.log("[SENTINEL HOOK] No valid information at collection point");
53
+ return;
54
+ }
55
+ // 处理和验证文本
56
+ const questionText = { subSceneID: 'TOOL_OUTPUT', tool: `${event.toolName}`, output: [{ content: "" }] };
57
+ const originText = processText(resultText, api);
58
+ questionText.output[0].content = `${originText}`;
59
+ const finalText = JSON.stringify(questionText);
60
+ if (finalText.length > MAX_TEXT_LENGTH) {
61
+ const diff_length = finalText.length - MAX_TEXT_LENGTH;
62
+ const { text: filterText, truncated } = validateAndTruncateText(originText, MAX_TEXT_LENGTH - diff_length);
63
+ if (truncated) {
64
+ questionText.output[0].content = `${filterText}`;
65
+ logger.warn(`[SENTINEL HOOK] postText exceeds ${MAX_TEXT_LENGTH}.`);
66
+ }
67
+ }
68
+ const postText = JSON.stringify(questionText);
69
+ logger.log(`[SENTINEL HOOK] Content extracted successfully. Length: ${postText.length}`);
70
+ try {
71
+ const response = await callApi(postText, api, sessionId);
72
+ const result = parseSecurityResult(response);
73
+ logger.log(`[SENTINEL HOOK] TOOL_OUTPUT response: status=${result.status}.`);
74
+ if (result.status === 'REJECT') {
75
+ logger.warn('[SENTINEL HOOK] REJECT detected, attempting steer injection');
76
+ const sessionCtx = ctx.sessionKey ? getSessionContext(ctx.sessionKey) : null;
77
+ if (sessionCtx?.sessionId && sessionCtx?.taskId) {
78
+ await tryInjectSteer({
79
+ sessionId: sessionCtx.sessionId,
80
+ taskId: sessionCtx.taskId,
81
+ message: STEER_ABORT_MESSAGE,
82
+ source: 'cspl',
83
+ });
84
+ }
85
+ else {
86
+ logger.warn(`[SENTINEL HOOK] Cannot inject steer: sessionKey=${ctx.sessionKey}, sessionCtx found=${!!sessionCtx}`);
87
+ }
88
+ }
89
+ }
90
+ catch (error) {
91
+ throw new Error(`[SENTINEL HOOK] API call failed: ${error}`);
92
+ }
93
+ }
94
+ catch (error) {
95
+ logger.error(`[SENTINEL HOOK] Extracted TOOL_OUTPUT data processing exception: ${error}`);
96
+ }
97
+ });
98
+ }
@@ -0,0 +1 @@
1
+ export declare function uploadFileToObsMain(filePath: string, api: any, fileHash: string, sessionId: string): Promise<string>;
@@ -0,0 +1,211 @@
1
+ /*
2
+ * 版权所有 (c) 华为技术有限公司 2026-2026
3
+ */
4
+ import fs from 'fs';
5
+ import path from 'path';
6
+ import https from 'https';
7
+ import { URL } from 'url';
8
+ import { getConfig } from './config.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
+ function buildOsmsHeaders(config, traceId) {
11
+ return {
12
+ 'content-type': 'application/json',
13
+ 'x-request-from': 'openclaw',
14
+ 'x-uid': config.uid,
15
+ 'x-api-key': config.apiKey,
16
+ 'x-hag-trace-id': traceId,
17
+ 'x-skill-id': ''
18
+ };
19
+ }
20
+ function httpRequest(url, method, headers, body, timeout) {
21
+ return new Promise((resolve, reject) => {
22
+ const urlObj = new URL(url);
23
+ const options = {
24
+ hostname: urlObj.hostname,
25
+ port: urlObj.port || DEFAULT_HTTP_PORT,
26
+ path: urlObj.pathname + urlObj.search,
27
+ method: method,
28
+ headers: headers,
29
+ timeout: timeout,
30
+ rejectUnauthorized: false
31
+ };
32
+ const req = https.request(options, (res) => {
33
+ let data = '';
34
+ res.on('data', (chunk) => {
35
+ data += chunk;
36
+ });
37
+ res.on('end', () => {
38
+ if (res.statusCode && res.statusCode >= 400) {
39
+ reject(new Error(`HTTP error! status: ${res.statusCode}, body: ${data}`));
40
+ }
41
+ else {
42
+ resolve(data);
43
+ }
44
+ });
45
+ });
46
+ req.on('error', (error) => {
47
+ reject(error);
48
+ });
49
+ req.on('timeout', () => {
50
+ req.destroy();
51
+ reject(new Error('Request timeout'));
52
+ });
53
+ if (body) {
54
+ req.write(body);
55
+ }
56
+ req.end();
57
+ });
58
+ }
59
+ async function invokingOsmsPrepare(filePath, config, fileSha256, fileSize, sessionId) {
60
+ const headers = buildOsmsHeaders(config, sessionId);
61
+ const fileName = path.basename(filePath);
62
+ const body = JSON.stringify({
63
+ useEdge: false,
64
+ objectType: TEMPORARY_MATERIAL_PACKAGE,
65
+ fileName: fileName,
66
+ fileSha256: fileSha256,
67
+ fileSize: fileSize,
68
+ fileOwnerInfo: {
69
+ uid: FILE_OWNER_UID,
70
+ teamId: FILE_OWNER_TEAM_ID
71
+ }
72
+ });
73
+ const prepareUrl = `${config.serviceUrl}${OSMS_PREPARE_URL}`;
74
+ for (let times = 0; times < MAX_TIMES; times++) {
75
+ try {
76
+ const responseData = await httpRequest(prepareUrl, 'POST', headers, body, CONNECT_TIMEOUT);
77
+ const resp = JSON.parse(responseData);
78
+ if (!resp.objectId || !resp.draftId || !resp.uploadInfos) {
79
+ throw new Error('The hag osms prepare interface returns an exception');
80
+ }
81
+ if (!resp.uploadInfos || resp.uploadInfos.length === 0) {
82
+ throw new Error('The hag osms prepare interface uploadInfos returns is empty');
83
+ }
84
+ const uploadInfo = resp.uploadInfos[0];
85
+ if (!uploadInfo.url || !uploadInfo.headers) {
86
+ throw new Error('The hag osms prepare interface url and headers for uploadInfos map returns is empty');
87
+ }
88
+ return resp;
89
+ }
90
+ catch (e) {
91
+ if (times === MAX_TIMES - 1) {
92
+ throw e;
93
+ }
94
+ }
95
+ }
96
+ throw new Error('Failed to invoke OSMS prepare interface after max retries');
97
+ }
98
+ function readFileAsBytes(filePath) {
99
+ try {
100
+ return fs.readFileSync(filePath);
101
+ }
102
+ catch (error) {
103
+ const err = error;
104
+ throw new Error(`Failed to read file: ${filePath}. Error: ${err.message}`);
105
+ }
106
+ }
107
+ async function uploadFileToObs(uploadInfo, fileBytes) {
108
+ let retryDelay = 1;
109
+ let retryTime = 1;
110
+ while (true) {
111
+ try {
112
+ const urlObj = new URL(uploadInfo.url);
113
+ const options = {
114
+ hostname: urlObj.hostname,
115
+ port: urlObj.port || DEFAULT_HTTPS_PORT,
116
+ path: urlObj.pathname + urlObj.search,
117
+ method: 'PUT',
118
+ headers: {
119
+ ...uploadInfo.headers,
120
+ 'content-length': fileBytes.length.toString(),
121
+ },
122
+ timeout: READ_TIMEOUT,
123
+ rejectUnauthorized: false
124
+ };
125
+ await new Promise((resolve, reject) => {
126
+ const req = https.request(options, (res) => {
127
+ let data = '';
128
+ res.on('data', (chunk) => {
129
+ data += chunk;
130
+ });
131
+ res.on('end', () => {
132
+ if (res.statusCode && res.statusCode >= 400) {
133
+ reject(new Error(`Upload failed with status: ${res.statusCode}, body: ${data}`));
134
+ }
135
+ else {
136
+ resolve();
137
+ }
138
+ });
139
+ });
140
+ req.on('error', (error) => {
141
+ reject(error);
142
+ });
143
+ req.on('timeout', () => {
144
+ req.destroy();
145
+ reject(new Error('Upload timeout'));
146
+ });
147
+ req.write(fileBytes);
148
+ req.end();
149
+ });
150
+ return true;
151
+ }
152
+ catch (e) {
153
+ retryTime++;
154
+ if (retryTime > MAX_TIMES) {
155
+ throw new Error(`Upload file to obs failed: ${e.message}`);
156
+ }
157
+ await new Promise(resolve => setTimeout(resolve, retryDelay * Math.pow(2, retryTime)));
158
+ }
159
+ }
160
+ }
161
+ async function invokingOsmsComplete(objectId, draftId, config, sessionId) {
162
+ const headers = buildOsmsHeaders(config, sessionId);
163
+ const body = JSON.stringify({
164
+ objectId: objectId,
165
+ draftId: draftId,
166
+ expireTime: EXPIRE_TIME
167
+ });
168
+ const completeUrl = `${config.serviceUrl}${OSMS_COMPLETE_URL}`;
169
+ for (let times = 0; times < MAX_TIMES; times++) {
170
+ try {
171
+ const responseData = await httpRequest(completeUrl, 'POST', headers, body, CONNECT_TIMEOUT);
172
+ const resp = JSON.parse(responseData);
173
+ return resp;
174
+ }
175
+ catch (e) {
176
+ if (times === MAX_TIMES - 1) {
177
+ throw e;
178
+ }
179
+ }
180
+ }
181
+ throw new Error('Failed to invoke OSMS complete interface after max retries');
182
+ }
183
+ export async function uploadFileToObsMain(filePath, api, fileHash, sessionId) {
184
+ const config = getConfig(api);
185
+ const serviceUrl = config.api.url;
186
+ const obsConfig = {
187
+ uid: config.uid,
188
+ apiKey: config.apiKey,
189
+ serviceUrl: serviceUrl
190
+ };
191
+ const fileSize = fs.statSync(filePath).size;
192
+ const prepareResponse = await invokingOsmsPrepare(filePath, obsConfig, fileHash, fileSize, sessionId);
193
+ let fileBytes;
194
+ try {
195
+ fileBytes = readFileAsBytes(filePath);
196
+ }
197
+ catch (error) {
198
+ const err = error;
199
+ throw new Error(`Failed to read file for upload: ${filePath}. Error: ${err.message}`);
200
+ }
201
+ const uploadInfo = {
202
+ url: prepareResponse.uploadInfos[0].url,
203
+ headers: prepareResponse.uploadInfos[0].headers
204
+ };
205
+ await uploadFileToObs(uploadInfo, fileBytes);
206
+ const completeResponse = await invokingOsmsComplete(prepareResponse.objectId, prepareResponse.draftId, obsConfig, sessionId);
207
+ if (!completeResponse.fileDetailInfo || !completeResponse.fileDetailInfo.url) {
208
+ throw new Error('Failed to get download URL from complete response');
209
+ }
210
+ return completeResponse.fileDetailInfo.url;
211
+ }
@@ -0,0 +1,4 @@
1
+ export declare function refreshSensitivePatterns(): void;
2
+ export declare function redactSensitiveText(text: any): any;
3
+ export declare function redactSensitiveObject(obj: any): any;
4
+ export declare function containsSensitiveInfo(text: any): boolean;