@ynhcj/xiaoyi-channel 1.1.26 → 1.1.28

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.
Files changed (104) hide show
  1. package/dist/index.js +26 -69
  2. package/dist/src/approval-bridge.d.ts +48 -0
  3. package/dist/src/approval-bridge.js +382 -0
  4. package/dist/src/bot.js +132 -73
  5. package/dist/src/channel.js +59 -5
  6. package/dist/src/client.js +13 -23
  7. package/dist/src/cron-command.d.ts +15 -0
  8. package/dist/src/cron-command.js +49 -0
  9. package/dist/src/cron-query-handler.d.ts +7 -0
  10. package/dist/src/cron-query-handler.js +189 -0
  11. package/dist/src/cspl/call_api.d.ts +2 -0
  12. package/dist/src/cspl/call_api.js +107 -0
  13. package/dist/src/cspl/config.d.ts +4 -17
  14. package/dist/src/cspl/config.js +100 -70
  15. package/dist/src/cspl/configs.json +10 -0
  16. package/dist/src/cspl/constants.d.ts +49 -24
  17. package/dist/src/cspl/constants.js +46 -16
  18. package/dist/src/cspl/sentinel_hook.d.ts +2 -0
  19. package/dist/src/cspl/sentinel_hook.js +103 -0
  20. package/dist/src/cspl/steer-context.js +1 -1
  21. package/dist/src/cspl/upload_file.d.ts +1 -0
  22. package/dist/src/cspl/upload_file.js +211 -0
  23. package/dist/src/cspl/utils.d.ts +17 -2
  24. package/dist/src/cspl/utils.js +271 -15
  25. package/dist/src/file-upload.d.ts +5 -0
  26. package/dist/src/file-upload.js +102 -0
  27. package/dist/src/formatter.d.ts +43 -1
  28. package/dist/src/formatter.js +171 -41
  29. package/dist/src/monitor.js +64 -43
  30. package/dist/src/outbound.js +8 -9
  31. package/dist/src/parser.d.ts +8 -1
  32. package/dist/src/parser.js +71 -0
  33. package/dist/src/provider.js +51 -17
  34. package/dist/src/push.d.ts +11 -1
  35. package/dist/src/push.js +101 -17
  36. package/dist/src/reply-dispatcher.js +152 -59
  37. package/dist/src/self-evolution-handler.d.ts +1 -1
  38. package/dist/src/self-evolution-handler.js +14 -3
  39. package/dist/src/sensitive-redactor.d.ts +4 -0
  40. package/dist/src/sensitive-redactor.js +364 -0
  41. package/dist/src/task-manager.js +6 -10
  42. package/dist/src/tools/agent-as-skill-tool.d.ts +7 -0
  43. package/dist/src/tools/agent-as-skill-tool.js +190 -0
  44. package/dist/src/tools/calendar-tool.js +3 -2
  45. package/dist/src/tools/call-phone-tool.js +3 -2
  46. package/dist/src/tools/check-plugin-privilege-tool.d.ts +6 -0
  47. package/dist/src/tools/check-plugin-privilege-tool.js +182 -0
  48. package/dist/src/tools/create-alarm-tool.js +3 -2
  49. package/dist/src/tools/create-all-tools.js +11 -3
  50. package/dist/src/tools/delete-alarm-tool.js +3 -2
  51. package/dist/src/tools/device-tool-map.d.ts +1 -1
  52. package/dist/src/tools/device-tool-map.js +12 -5
  53. package/dist/src/tools/discover-cross-devices-tool.d.ts +2 -0
  54. package/dist/src/tools/discover-cross-devices-tool.js +235 -0
  55. package/dist/src/tools/display-a2ui-card-tool.d.ts +2 -0
  56. package/dist/src/tools/display-a2ui-card-tool.js +85 -0
  57. package/dist/src/tools/find-pc-devices-tool.d.ts +2 -1
  58. package/dist/src/tools/find-pc-devices-tool.js +85 -88
  59. package/dist/src/tools/get-collection-tool-schema.js +1 -1
  60. package/dist/src/tools/location-tool.js +3 -2
  61. package/dist/src/tools/modify-alarm-tool.js +3 -2
  62. package/dist/src/tools/modify-note-tool.js +3 -2
  63. package/dist/src/tools/note-tool.js +3 -2
  64. package/dist/src/tools/query-app-message-tool.js +4 -3
  65. package/dist/src/tools/query-memory-data-tool.js +4 -3
  66. package/dist/src/tools/query-todo-task-tool.js +4 -3
  67. package/dist/src/tools/save-file-to-phone-tool.js +3 -2
  68. package/dist/src/tools/save-media-to-gallery-tool.js +3 -2
  69. package/dist/src/tools/schema-tool-factory.js +1 -1
  70. package/dist/src/tools/search-alarm-tool.js +3 -2
  71. package/dist/src/tools/search-calendar-tool.js +3 -2
  72. package/dist/src/tools/search-contact-tool.js +3 -2
  73. package/dist/src/tools/search-email-tool.js +4 -3
  74. package/dist/src/tools/search-file-tool.js +8 -9
  75. package/dist/src/tools/search-message-tool.js +2 -1
  76. package/dist/src/tools/search-note-tool.js +3 -2
  77. package/dist/src/tools/search-photo-gallery-tool.js +5 -4
  78. package/dist/src/tools/send-cross-device-task-tool.d.ts +2 -0
  79. package/dist/src/tools/send-cross-device-task-tool.js +299 -0
  80. package/dist/src/tools/send-email-tool.js +4 -3
  81. package/dist/src/tools/send-file-to-user-tool.d.ts +1 -1
  82. package/dist/src/tools/send-file-to-user-tool.js +37 -8
  83. package/dist/src/tools/send-html-card-tool.d.ts +7 -0
  84. package/dist/src/tools/send-html-card-tool.js +113 -0
  85. package/dist/src/tools/send-message-tool.js +2 -1
  86. package/dist/src/tools/session-manager.d.ts +17 -1
  87. package/dist/src/tools/session-manager.js +87 -1
  88. package/dist/src/tools/upload-file-tool.js +9 -7
  89. package/dist/src/tools/upload-photo-tool.js +5 -4
  90. package/dist/src/tools/xiaoyi-add-collection-tool.js +5 -3
  91. package/dist/src/tools/xiaoyi-collection-tool.js +4 -3
  92. package/dist/src/tools/xiaoyi-delete-collection-tool.js +4 -3
  93. package/dist/src/tools/xiaoyi-gui-tool.js +8 -2
  94. package/dist/src/trigger-handler.js +4 -7
  95. package/dist/src/types.d.ts +25 -1
  96. package/dist/src/utils/config-manager.js +3 -6
  97. package/dist/src/utils/logger.d.ts +8 -0
  98. package/dist/src/utils/logger.js +69 -34
  99. package/dist/src/utils/pushdata-manager.js +1 -5
  100. package/dist/src/utils/pushid-manager.js +1 -2
  101. package/dist/src/utils/runtime-manager.js +1 -4
  102. package/dist/src/websocket.d.ts +3 -0
  103. package/dist/src/websocket.js +242 -38
  104. package/package.json +1 -1
@@ -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
+ }
@@ -1,10 +1,25 @@
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
1
2
  export declare function filterText(text: string): string;
2
3
  export declare function validateAndTruncateText(text: string, maxLength: number): {
3
4
  text: string;
4
5
  truncated: boolean;
5
6
  };
6
7
  export declare function extractResultText(event: any, toolName: string): string;
7
- export declare function processText(resultText: string): string;
8
+ export declare function processText(resultText: string, api: OpenClawPluginApi): string;
8
9
  export declare function parseSecurityResult(response: any): {
9
- status: "ACCEPT" | "REJECT";
10
+ status: 'ACCEPT' | 'REJECT';
10
11
  };
12
+ export declare function extractInputParams(event: any, toolName: string): string;
13
+ export declare function extractFilePathsFromCommand(command: string): string[];
14
+ export declare function calculateContentHash(content: string): string;
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';
19
+ } | null>;
20
+ export declare function handleMessageToolInput(event: any, api: OpenClawPluginApi, sessionId: string): Promise<{
21
+ status: 'ACCEPT' | 'REJECT';
22
+ } | null>;
23
+ export declare function handleOtherToolInput(event: any, api: OpenClawPluginApi, sessionId: string): Promise<{
24
+ status: 'ACCEPT' | 'REJECT';
25
+ } | null>;
@@ -1,10 +1,21 @@
1
- // CSPL Hook 工具函数
2
- import { MAX_TEXT_LENGTH, regex, SECURITY_NOTICE } from "./constants.js";
1
+ /*
2
+ * 版权所有 (c) 华为技术有限公司 2026-2026
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';
5
+ import crypto from 'crypto';
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import { Buffer } from 'buffer';
9
+ import { callApi } from './call_api.js';
10
+ import { uploadFileToObsMain } from './upload_file.js';
11
+ import { logger } from '../utils/logger.js';
12
+ // 文本过滤函数:仅保留中文、英文、数字、标点符号
3
13
  export function filterText(text) {
4
14
  if (!text)
5
15
  return "";
6
- return text.replace(new RegExp(regex.source, "g"), "");
16
+ return text.replace(new RegExp(regex.source, 'g'), '');
7
17
  }
18
+ // 文本验证和截断函数
8
19
  export function validateAndTruncateText(text, maxLength) {
9
20
  if (text.length > maxLength) {
10
21
  const halfMaxLength = Math.floor(maxLength / 2);
@@ -16,7 +27,8 @@ export function validateAndTruncateText(text, maxLength) {
16
27
  }
17
28
  export function extractResultText(event, toolName) {
18
29
  const resultTexts = [];
19
- if (toolName === "web_fetch") {
30
+ // web_fetch工具特殊处理:从details.text提取
31
+ if (toolName === 'web_fetch') {
20
32
  if (event.result?.details?.text) {
21
33
  let text = event.result.details.text;
22
34
  text = text.replace(SECURITY_NOTICE, '');
@@ -24,6 +36,7 @@ export function extractResultText(event, toolName) {
24
36
  }
25
37
  return resultTexts.length > 0 ? resultTexts.join("; ") : "";
26
38
  }
39
+ // 白名单工具:从content[].text提取
27
40
  if (event.result?.content && Array.isArray(event.result.content)) {
28
41
  for (const item of event.result.content) {
29
42
  if (item?.text) {
@@ -33,27 +46,270 @@ export function extractResultText(event, toolName) {
33
46
  }
34
47
  return resultTexts.length > 0 ? resultTexts.join("; ") : "";
35
48
  }
36
- export function processText(resultText) {
49
+ export function processText(resultText, api) {
37
50
  const questionText = filterText(resultText);
38
- const { text: finalText } = validateAndTruncateText(questionText, MAX_TEXT_LENGTH);
51
+ // 检查是否超过4096字符限制,进行截断
52
+ const { text: finalText, truncated } = validateAndTruncateText(questionText, MAX_TEXT_LENGTH);
53
+ if (truncated) {
54
+ logger.warn(`[SENTINEL HOOK] filterText exceeds ${MAX_TEXT_LENGTH}. Original length: ${questionText.length}`);
55
+ }
39
56
  return finalText;
40
57
  }
41
58
  export function parseSecurityResult(response) {
42
59
  if (response === null || response === undefined) {
43
- throw new Error("Response is null or undefined");
60
+ throw new Error('Response is null or undefined');
44
61
  }
45
- if (!response.data || typeof response.data !== "object") {
46
- throw new Error("Response.data is missing or not an object");
62
+ if (response.data === null || response.data === undefined || typeof response.data !== 'object') {
63
+ throw new Error('Response.data is null, undefined or not an object');
47
64
  }
48
- const securityResult = response.data.securityResult;
49
- if (typeof securityResult !== "string") {
50
- throw new Error("Response.data.securityResult is missing or not a string");
65
+ if (!('securityResult' in response.data) || typeof response.data.securityResult !== 'string') {
66
+ throw new Error('Response.data.securityResult is missing or not a string');
51
67
  }
68
+ const securityResult = response.data.securityResult;
52
69
  if (securityResult !== securityResult.trim()) {
53
- throw new Error("Response.data.securityResult contains leading or trailing spaces");
70
+ throw new Error('Response.data.securityResult contains leading or trailing spaces');
54
71
  }
55
- if (securityResult !== "ACCEPT" && securityResult !== "REJECT") {
56
- throw new Error(`Response.data.securityResult must be "ACCEPT" or "REJECT". Actual: "${securityResult}"`);
72
+ if (securityResult !== 'ACCEPT' && securityResult !== 'REJECT') {
73
+ throw new Error(`Response.data.securityResult must be "ACCEPT" or "REJECT". Actual value: "${securityResult}"`);
57
74
  }
58
75
  return { status: securityResult };
59
76
  }
77
+ // 从event对象中提取工具输入参数
78
+ export function extractInputParams(event, toolName) {
79
+ if (toolName === 'exec') {
80
+ return event.params?.command || '';
81
+ }
82
+ else if (toolName === 'message') {
83
+ return event.params?.message || '';
84
+ }
85
+ return '';
86
+ }
87
+ // 从shell命令中提取文件路径
88
+ export function extractFilePathsFromCommand(command) {
89
+ if (!command) {
90
+ return [];
91
+ }
92
+ // 命令字符串超过1K则截断
93
+ let processedCommand = command;
94
+ if (command.length > MAX_COMMAND_LENGTH) {
95
+ processedCommand = command.substring(0, MAX_COMMAND_LENGTH);
96
+ }
97
+ // 使用空格分割命令字符串
98
+ const parts = processedCommand.split(' ');
99
+ const results = [];
100
+ let currentBaseDir = ''; // 当前基础目录
101
+ let expectBaseDir = false; // flag:下一个元素是cd后的基础目录
102
+ // 遍历分割后的命令部分
103
+ for (const part of parts) {
104
+ // 忽略空字符串
105
+ if (!part) {
106
+ continue;
107
+ }
108
+ // 处理cd命令后的基础目录
109
+ if (expectBaseDir) {
110
+ currentBaseDir = part;
111
+ expectBaseDir = false;
112
+ continue;
113
+ }
114
+ // 识别cd命令
115
+ if (part === 'cd') {
116
+ expectBaseDir = true;
117
+ continue;
118
+ }
119
+ // 处理代码文件
120
+ const absolutePath = processCodeFile(part, currentBaseDir);
121
+ if (absolutePath && !results.includes(absolutePath)) {
122
+ results.push(absolutePath);
123
+ if (results.length >= MAX_FILE_COUNT) {
124
+ break;
125
+ }
126
+ }
127
+ }
128
+ return results;
129
+ }
130
+ // 检查是否为代码文件
131
+ function isCodeFile(filePath) {
132
+ const lastDotIndex = filePath.lastIndexOf('.');
133
+ if (lastDotIndex === -1) {
134
+ return { isCodeFile: false, cleanPath: null };
135
+ }
136
+ let orign_extension = filePath.substring(lastDotIndex + 1).toLowerCase();
137
+ orign_extension = orign_extension.replace(FILE_EXTENSION_REGEX, ' ');
138
+ const extension = orign_extension.split(' ')[0];
139
+ if (!CODE_FILE_EXTENSIONS.includes(extension)) {
140
+ return { isCodeFile: false, cleanPath: null };
141
+ }
142
+ const cleanPath = `${filePath.substring(0, lastDotIndex + 1)}${extension}`;
143
+ return { isCodeFile: true, cleanPath: cleanPath };
144
+ }
145
+ // 构建绝对路径
146
+ function buildAbsolutePath(filePath, baseDir) {
147
+ if (path.isAbsolute(filePath)) {
148
+ return filePath;
149
+ }
150
+ if (baseDir) {
151
+ return `${baseDir}/${filePath}`;
152
+ }
153
+ return filePath;
154
+ }
155
+ // 处理代码文件,返回绝对路径
156
+ function processCodeFile(part, currentBaseDir) {
157
+ const { isCodeFile: isCodeFileResult, cleanPath } = isCodeFile(part);
158
+ if (!isCodeFileResult) {
159
+ return null;
160
+ }
161
+ return buildAbsolutePath(cleanPath, currentBaseDir);
162
+ }
163
+ // 计算字符串的SHA256哈希值
164
+ export function calculateContentHash(content) {
165
+ if (!content) {
166
+ return '';
167
+ }
168
+ return crypto.createHash('sha256').update(content, 'utf8').digest('hex');
169
+ }
170
+ // 获取文件大小(KB)
171
+ export function getFileSizeInKB(filePath) {
172
+ try {
173
+ const stats = fs.statSync(filePath);
174
+ return Math.ceil(stats.size / 1024);
175
+ }
176
+ catch (error) {
177
+ return 0;
178
+ }
179
+ }
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
+ }
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`);
203
+ }
204
+ // 检查是否满足要求
205
+ bodyStr = JSON.stringify(adjusted);
206
+ if (bodyStr.length <= MAX_TEXT_LENGTH) {
207
+ break;
208
+ }
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;
215
+ }
216
+ // 发送TOOL_INPUT请求并处理响应,返回扫描结果
217
+ async function sendToolInputRequest(postText, api, sessionId) {
218
+ const response = await callApi(postText, api, sessionId, TOOL_INPUT_ACTION);
219
+ const result = parseSecurityResult(response);
220
+ logger.log(`[SENTINEL HOOK] TOOL_INPUT response: status=${result.status}`);
221
+ return result;
222
+ }
223
+ // 处理exec工具的TOOL_INPUT数据采集,返回最终扫描结果
224
+ export async function handleExecToolInput(event, api, sessionId) {
225
+ const command = extractInputParams(event, 'exec');
226
+ if (!command) {
227
+ logger.log('[SENTINEL HOOK] No command found for exec tool');
228
+ return null;
229
+ }
230
+ // 解析命令提取文件路径
231
+ const filePaths = extractFilePathsFromCommand(command);
232
+ if (filePaths.length > 0) {
233
+ // 场景1:执行代码文件
234
+ logger.log(`[SENTINEL HOOK] Found ${filePaths.length} file(s) in command`);
235
+ const nonExistingFiles = [];
236
+ let lastResult = null;
237
+ for (const filePath of filePaths) {
238
+ if (!fs.existsSync(filePath)) {
239
+ nonExistingFiles.push(filePath);
240
+ continue;
241
+ }
242
+ const fileContent = fs.readFileSync(filePath, 'utf8');
243
+ const fileHash = calculateContentHash(fileContent);
244
+ const fileSize = getFileSizeInKB(filePath);
245
+ 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}`);
251
+ try {
252
+ lastResult = await sendToolInputRequest(postText, api, sessionId);
253
+ if (lastResult.status === 'REJECT') {
254
+ return lastResult;
255
+ }
256
+ }
257
+ catch (e) {
258
+ logger.error(`[SENTINEL HOOK] Sending TOOL_INPUT Failed: ${e}`);
259
+ }
260
+ }
261
+ // 输出不存在的文件列表
262
+ if (nonExistingFiles.length > 0) {
263
+ const fileNames = nonExistingFiles.map(f => path.basename(f)).join(', ');
264
+ logger.log(`[SENTINEL HOOK] Non-existing files: ${fileNames}`);
265
+ }
266
+ return lastResult;
267
+ }
268
+ else {
269
+ // 场景2:直接执行代码(heredoc场景)
270
+ 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);
278
+ }
279
+ }
280
+ // 处理message工具的TOOL_INPUT数据采集,返回扫描结果
281
+ export async function handleMessageToolInput(event, api, sessionId) {
282
+ const message = extractInputParams(event, 'message');
283
+ if (!message) {
284
+ logger.log('[SENTINEL HOOK] No message found for message tool');
285
+ return null;
286
+ }
287
+ 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);
295
+ }
296
+ // 处理其他工具(非 exec 和非 message)的 TOOL_INPUT 数据采集,返回扫描结果
297
+ export async function handleOtherToolInput(event, api, sessionId) {
298
+ const params = event.params;
299
+ if (!params) {
300
+ logger.log('[SENTINEL HOOK] No params found for tool');
301
+ return null;
302
+ }
303
+ logger.log(`[SENTINEL HOOK] Processing other tool input, toolName: ${event.toolName}`);
304
+ // 将 params 序列化为 JSON 字符串
305
+ 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);
315
+ }
@@ -17,6 +17,11 @@ export declare class XYFileUploadService {
17
17
  * Uses completeAndQuery endpoint to get the file URL directly.
18
18
  */
19
19
  uploadFileAndGetUrl(filePath: string, objectType?: string): Promise<string>;
20
+ /**
21
+ * Upload a file and return a preview-able URL (needPreview=true).
22
+ * Same as uploadFileAndGetUrl but adds needPreview flag to get a directly viewable URL.
23
+ */
24
+ uploadFileAndGetPreviewUrl(filePath: string, objectType?: string): Promise<string>;
20
25
  /**
21
26
  * Upload multiple files and return their file IDs.
22
27
  */
@@ -235,6 +235,108 @@ export class XYFileUploadService {
235
235
  }
236
236
  }
237
237
  }
238
+ /**
239
+ * Upload a file and return a preview-able URL (needPreview=true).
240
+ * Same as uploadFileAndGetUrl but adds needPreview flag to get a directly viewable URL.
241
+ */
242
+ async uploadFileAndGetPreviewUrl(filePath, objectType = "TEMPORARY_MATERIAL_DOC") {
243
+ let localFilePath = filePath;
244
+ let isTempFile = false;
245
+ try {
246
+ // Handle remote URLs by downloading first
247
+ if (isRemoteUrl(filePath)) {
248
+ localFilePath = await downloadToTempFile(filePath);
249
+ isTempFile = true;
250
+ }
251
+ // Read file
252
+ const fileBuffer = await fs.readFile(localFilePath);
253
+ const fileName = path.basename(localFilePath);
254
+ const fileSha256 = calculateSHA256(fileBuffer);
255
+ const fileSize = fileBuffer.length;
256
+ // Phase 1: Prepare
257
+ logger.log(`[XY File Upload] Phase 1 (preview): Prepare upload for ${fileName}`);
258
+ const prepareResp = await fetch(`${this.baseUrl}/osms/v1/file/manager/prepare`, {
259
+ method: "POST",
260
+ headers: {
261
+ "Content-Type": "application/json",
262
+ "x-uid": this.uid,
263
+ "x-api-key": this.apiKey,
264
+ "x-request-from": "openclaw",
265
+ },
266
+ body: JSON.stringify({
267
+ objectType,
268
+ fileName,
269
+ fileSha256,
270
+ fileSize,
271
+ fileOwnerInfo: {
272
+ uid: this.uid,
273
+ teamId: this.uid,
274
+ },
275
+ useEdge: false,
276
+ }),
277
+ });
278
+ if (!prepareResp.ok) {
279
+ throw new Error(`Prepare failed: HTTP ${prepareResp.status}`);
280
+ }
281
+ const prepareData = await prepareResp.json();
282
+ if (prepareData.code !== "0") {
283
+ throw new Error(`Prepare failed: ${prepareData.desc}`);
284
+ }
285
+ const { objectId, draftId, uploadInfos } = prepareData;
286
+ logger.log(`[XY File Upload] Prepare (preview) complete: objectId=${objectId}, draftId=${draftId}`);
287
+ // Phase 2: Upload
288
+ logger.log(`[XY File Upload] Phase 2 (preview): Upload file data`);
289
+ const uploadInfo = uploadInfos[0];
290
+ const uploadResp = await fetch(uploadInfo.url, {
291
+ method: uploadInfo.method,
292
+ headers: uploadInfo.headers,
293
+ body: fileBuffer,
294
+ });
295
+ if (!uploadResp.ok) {
296
+ throw new Error(`Upload failed: HTTP ${uploadResp.status}`);
297
+ }
298
+ logger.log(`[XY File Upload] Upload (preview) complete`);
299
+ // Phase 3: CompleteAndQuery with needPreview=true
300
+ logger.log(`[XY File Upload] Phase 3 (preview): CompleteAndQuery with needPreview=true`);
301
+ const completeResp = await fetch(`${this.baseUrl}/osms/v1/file/manager/completeAndQuery`, {
302
+ method: "POST",
303
+ headers: {
304
+ "Content-Type": "application/json",
305
+ "x-uid": this.uid,
306
+ "x-api-key": this.apiKey,
307
+ "x-request-from": "openclaw",
308
+ },
309
+ body: JSON.stringify({
310
+ objectId,
311
+ draftId,
312
+ needPreview: true,
313
+ expireTime: 259200,
314
+ }),
315
+ });
316
+ if (!completeResp.ok) {
317
+ throw new Error(`CompleteAndQuery (preview) failed: HTTP ${completeResp.status}`);
318
+ }
319
+ const completeData = await completeResp.json();
320
+ const fileUrl = completeData?.fileDetailInfo?.url || "";
321
+ if (!fileUrl) {
322
+ throw new Error("No file URL returned from completeAndQuery (preview)");
323
+ }
324
+ logger.log(`[XY File Upload] File upload with preview URL successful`);
325
+ return fileUrl;
326
+ }
327
+ catch (error) {
328
+ logger.error(`[XY File Upload] File upload with preview URL failed for ${filePath}:`, error);
329
+ throw error;
330
+ }
331
+ finally {
332
+ if (isTempFile) {
333
+ try {
334
+ await fs.unlink(localFilePath);
335
+ }
336
+ catch { }
337
+ }
338
+ }
339
+ }
238
340
  /**
239
341
  * Upload multiple files and return their file IDs.
240
342
  */