@ynhcj/xiaoyi-channel 0.0.208-beta → 0.0.210-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +3 -8
- package/dist/index.js +5 -1
- package/dist/src/bot.js +16 -77
- package/dist/src/cspl/call_api.d.ts +1 -1
- package/dist/src/cspl/call_api.js +6 -12
- 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 +26 -22
- package/dist/src/cspl/upload_file.js +5 -6
- package/dist/src/cspl/utils.d.ts +20 -9
- package/dist/src/cspl/utils.js +119 -75
- package/dist/src/log-reporter/index.d.ts +1 -1
- package/dist/src/log-reporter/index.js +159 -35
- package/dist/src/log-reporter/openclaw-parser.d.ts +18 -0
- package/dist/src/log-reporter/openclaw-parser.js +94 -0
- package/dist/src/log-reporter/path-resolver.d.ts +5 -0
- package/dist/src/log-reporter/path-resolver.js +50 -0
- package/dist/src/log-reporter/reporter.d.ts +4 -4
- package/dist/src/log-reporter/reporter.js +52 -13
- package/dist/src/log-reporter/scanner.d.ts +7 -3
- package/dist/src/log-reporter/scanner.js +3 -7
- package/dist/src/log-reporter/types.d.ts +26 -27
- package/dist/src/log-reporter/uploader.d.ts +4 -3
- package/dist/src/log-reporter/uploader.js +32 -9
- package/dist/src/monitor.js +0 -1
- package/dist/src/skill-retriever/config.js +2 -0
- package/dist/src/skill-retriever/hooks.js +1 -0
- package/dist/src/skill-retriever/tool-search.d.ts +3 -0
- package/dist/src/skill-retriever/tool-search.js +27 -11
- package/dist/src/skill-retriever/types.d.ts +1 -0
- package/dist/src/tools/device-tool-map.js +0 -6
- package/dist/src/tools/hmos-cli.d.ts +103 -0
- package/dist/src/tools/hmos-cli.js +561 -0
- package/dist/src/tools/invoke.d.ts +48 -0
- package/dist/src/tools/invoke.js +1201 -0
- package/dist/src/websocket.js +10 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
description: string;
|
|
5
|
-
configSchema: import("openclaw/plugin-sdk").OpenClawPluginConfigSchema;
|
|
6
|
-
register: NonNullable<import("openclaw/plugin-sdk/core").OpenClawPluginDefinition["register"]>;
|
|
7
|
-
} & Pick<import("openclaw/plugin-sdk/core").OpenClawPluginDefinition, "kind" | "reload" | "nodeHostCommands" | "securityAuditCollectors">;
|
|
8
|
-
export default _default;
|
|
1
|
+
import { definePluginEntry } from "openclaw/plugin-sdk/core";
|
|
2
|
+
declare const pluginEntry: ReturnType<typeof definePluginEntry>;
|
|
3
|
+
export default pluginEntry;
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import { getAllPushIds } from "./src/utils/pushid-manager.js";
|
|
|
10
10
|
import { registerSelfEvolutionToolResultNudge } from "./src/self-evolution-tool-result-nudge.js";
|
|
11
11
|
import { createBeforePromptBuildHandler } from "./src/skill-retriever/hooks.js";
|
|
12
12
|
import { normalizeToolRetrieverConfig } from "./src/skill-retriever/config.js";
|
|
13
|
+
import { registerCLIHook } from "./src/tools/hmos-cli.js";
|
|
13
14
|
/**
|
|
14
15
|
* Register the cron detection hook.
|
|
15
16
|
*
|
|
@@ -181,7 +182,7 @@ function registerFullHooks(api) {
|
|
|
181
182
|
api.on("before_prompt_build", beforePromptBuildHandler);
|
|
182
183
|
registerSelfEvolutionToolResultNudge(api);
|
|
183
184
|
}
|
|
184
|
-
|
|
185
|
+
const pluginEntry = definePluginEntry({
|
|
185
186
|
id: "xiaoyi-channel",
|
|
186
187
|
name: "Xiaoyi Channel",
|
|
187
188
|
description: "Xiaoyi channel plugin - Xiaoyi A2A protocol integration",
|
|
@@ -208,6 +209,9 @@ export default definePluginEntry({
|
|
|
208
209
|
registerSentinelHook(api);
|
|
209
210
|
// Cron detection hook: marks toolCallIds from cron sessions
|
|
210
211
|
registerCronDetectionHook(api);
|
|
212
|
+
// CLI exec hook: intercepts built-in exec for HarmonyOS CLI skill tools
|
|
213
|
+
registerCLIHook(api);
|
|
211
214
|
}
|
|
212
215
|
},
|
|
213
216
|
});
|
|
217
|
+
export default pluginEntry;
|
package/dist/src/bot.js
CHANGED
|
@@ -7,6 +7,7 @@ import { downloadFilesFromParts } from "./file-download.js";
|
|
|
7
7
|
import { resolveXYConfig } from "./config.js";
|
|
8
8
|
import { sendStatusUpdate, sendClearContextResponse, sendTasksCancelResponse, sendA2AResponse } from "./formatter.js";
|
|
9
9
|
import { appendSelfEvolutionKeywordNudge, shouldNudgeForSelfEvolutionKeyword, } from "./self-evolution-keyword.js";
|
|
10
|
+
import { queueAgentHarnessMessage } from "openclaw/plugin-sdk/agent-harness-runtime";
|
|
10
11
|
import { runWithSessionContext } from "./tools/session-manager.js";
|
|
11
12
|
import { configManager } from "./utils/config-manager.js";
|
|
12
13
|
import { addPushId } from "./utils/pushid-manager.js";
|
|
@@ -552,7 +553,7 @@ function enqueueSteer(params) {
|
|
|
552
553
|
return next;
|
|
553
554
|
}
|
|
554
555
|
async function dispatchSteerWhenReady(params) {
|
|
555
|
-
const { sessionId,
|
|
556
|
+
const { sessionId, steerText } = params;
|
|
556
557
|
const log = logger.withContext(sessionId, params.parsed.taskId);
|
|
557
558
|
// 1. 等待第一条消息开始 streaming
|
|
558
559
|
// signal 可能尚未创建(第一条消息还在文件下载等耗时操作中),
|
|
@@ -578,7 +579,7 @@ async function dispatchSteerWhenReady(params) {
|
|
|
578
579
|
}
|
|
579
580
|
else {
|
|
580
581
|
// 轮询超时且 hasActiveTask 仍为 true——说明第一条消息可能卡在异常路径,
|
|
581
|
-
// 没有创建 signal
|
|
582
|
+
// 没有创建 signal。此时放弃,避免并发碰撞。
|
|
582
583
|
log.log(`[STEER-QUEUE] Signal never appeared after polling, skip steer to avoid collision`);
|
|
583
584
|
return;
|
|
584
585
|
}
|
|
@@ -587,84 +588,22 @@ async function dispatchSteerWhenReady(params) {
|
|
|
587
588
|
log.log(`[STEER-QUEUE] First message completed, skip steer`);
|
|
588
589
|
return;
|
|
589
590
|
}
|
|
590
|
-
// 3.
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
// 如果有文件附件,把路径拼到 steer 文本末尾,让模型通过工具读取
|
|
591
|
+
// 3. 直接注入到活跃的 Pi run 中,不创建独立 dispatcher。
|
|
592
|
+
// 模型回复通过第一条消息的 dispatcher 的 onPartialReply 流式发出,
|
|
593
|
+
// 使用 registerTaskId + updateFallbackTaskId 已同步的最新 taskId。
|
|
594
594
|
const mediaPaths = params.mediaPayload?.MediaPaths;
|
|
595
595
|
const fileHint = mediaPaths && mediaPaths.length > 0
|
|
596
596
|
? `\n【用户上传附件】:${JSON.stringify(mediaPaths)}`
|
|
597
597
|
: "";
|
|
598
|
-
const
|
|
599
|
-
|
|
600
|
-
const
|
|
601
|
-
|
|
602
|
-
channel: "xiaoyi-channel",
|
|
603
|
-
from: speaker,
|
|
604
|
-
timestamp: new Date(),
|
|
605
|
-
envelope: envelopeOptions,
|
|
606
|
-
body: messageBody,
|
|
607
|
-
});
|
|
608
|
-
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
|
609
|
-
Body: body,
|
|
610
|
-
RawBody: steerCommand,
|
|
611
|
-
CommandBody: steerCommand,
|
|
612
|
-
From: sessionId,
|
|
613
|
-
To: sessionId,
|
|
614
|
-
SessionKey: params.route.sessionKey,
|
|
615
|
-
AccountId: params.route.accountId,
|
|
616
|
-
ChatType: "direct",
|
|
617
|
-
GroupSubject: undefined,
|
|
618
|
-
SenderName: sessionId,
|
|
619
|
-
SenderId: sessionId,
|
|
620
|
-
Provider: "xiaoyi-channel",
|
|
621
|
-
Surface: "xiaoyi-channel",
|
|
622
|
-
MessageSid: `xiaoyi_${params.parsed.taskId}_${params.deviceType}`,
|
|
623
|
-
Timestamp: Date.now(),
|
|
624
|
-
WasMentioned: false,
|
|
625
|
-
CommandAuthorized: true,
|
|
626
|
-
OriginatingChannel: "xiaoyi-channel",
|
|
627
|
-
OriginatingTo: sessionId,
|
|
628
|
-
ReplyToBody: undefined,
|
|
629
|
-
...params.mediaPayload,
|
|
630
|
-
});
|
|
631
|
-
const steerState = { steered: true };
|
|
632
|
-
const { dispatcher, replyOptions } = createXYReplyDispatcher({
|
|
633
|
-
cfg: params.cfg,
|
|
634
|
-
runtime: params.runtime,
|
|
635
|
-
sessionId,
|
|
636
|
-
taskId: params.parsed.taskId,
|
|
637
|
-
messageId: params.parsed.messageId,
|
|
638
|
-
accountId: params.route.accountId,
|
|
639
|
-
steerState,
|
|
640
|
-
});
|
|
641
|
-
const sessionContext = {
|
|
642
|
-
config: resolveXYConfig(params.cfg),
|
|
643
|
-
sessionId,
|
|
644
|
-
taskId: params.parsed.taskId,
|
|
645
|
-
messageId: params.parsed.messageId,
|
|
646
|
-
agentId: params.route.accountId,
|
|
647
|
-
deviceType: params.deviceType,
|
|
648
|
-
};
|
|
649
|
-
log.log(`[STEER-QUEUE] Dispatching steer`);
|
|
650
|
-
await core.channel.reply.withReplyDispatcher({
|
|
651
|
-
dispatcher,
|
|
652
|
-
onSettled: () => {
|
|
653
|
-
log.log(`[STEER-QUEUE] Steer dispatch settled`);
|
|
654
|
-
},
|
|
655
|
-
run: () => {
|
|
656
|
-
return runWithSessionContext(sessionContext, async () => {
|
|
657
|
-
log.log(`[ALS-PROOF] bot entered steer dispatch scope sessionId=${sessionContext.sessionId} taskId=${sessionContext.taskId} isSteer=true`);
|
|
658
|
-
const result = await core.channel.reply.dispatchReplyFromConfig({
|
|
659
|
-
ctx: ctxPayload,
|
|
660
|
-
cfg: params.cfg,
|
|
661
|
-
dispatcher,
|
|
662
|
-
replyOptions,
|
|
663
|
-
});
|
|
664
|
-
log.log(`[STEER-QUEUE] dispatch result: ${JSON.stringify(result)}`);
|
|
665
|
-
return result;
|
|
666
|
-
});
|
|
667
|
-
},
|
|
598
|
+
const steerMessage = `${steerText}${fileHint}`;
|
|
599
|
+
log.log(`[STEER-QUEUE] Injecting steer message directly into active run`);
|
|
600
|
+
const injected = queueAgentHarnessMessage(sessionId, steerMessage, {
|
|
601
|
+
steeringMode: "all",
|
|
668
602
|
});
|
|
669
|
-
|
|
603
|
+
if (injected) {
|
|
604
|
+
log.log(`[STEER-QUEUE] Steer message injected successfully`);
|
|
605
|
+
}
|
|
606
|
+
else {
|
|
607
|
+
log.log(`[STEER-QUEUE] Steer injection failed — run may not be accepting messages`);
|
|
608
|
+
}
|
|
670
609
|
}
|
|
@@ -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, 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, API_URL_SUFFIX } 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,18 +78,13 @@ 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);
|
|
84
|
+
// 确保 uid 存在于消息体中(从 config 注入)
|
|
85
|
+
const payloadWithUid = { ...payload, uid: config.uid };
|
|
86
|
+
const httpBody = JSON.stringify(payloadWithUid);
|
|
92
87
|
const apiUrl = `${config.api.url}${API_URL_SUFFIX}`;
|
|
93
|
-
logger.log(`[SENTINEL HOOK] callApi: action=${action}, x-hag-trace-id=${sessionId}, url=${apiUrl}`);
|
|
94
88
|
return new Promise((resolve, reject) => {
|
|
95
89
|
const options = buildRequestOptions(apiUrl, headersForCelia, config.api.timeout);
|
|
96
90
|
const req = https.request(options, (res) => {
|
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, extractSessionId } 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,23 @@ 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})`);
|
|
20
|
+
// 请求体中的sessionID从taskId中提取(第一个&之前的内容)
|
|
21
|
+
const payloadSessionId = extractSessionId(taskId) || sessionId;
|
|
19
22
|
// 处理 TOOL_INPUT 数据采集、发送数据,根据扫描结果决定是否阻塞
|
|
20
23
|
try {
|
|
21
24
|
let scanResult = null;
|
|
22
25
|
if (event.toolName === 'exec') {
|
|
23
|
-
scanResult = await handleExecToolInput(event, api,
|
|
26
|
+
scanResult = await handleExecToolInput(event, api, payloadSessionId, taskId);
|
|
24
27
|
}
|
|
25
28
|
else if (event.toolName === 'message') {
|
|
26
|
-
scanResult = await handleMessageToolInput(event, api,
|
|
29
|
+
scanResult = await handleMessageToolInput(event, api, payloadSessionId, taskId);
|
|
27
30
|
}
|
|
28
31
|
else {
|
|
29
|
-
scanResult = await handleOtherToolInput(event, api,
|
|
32
|
+
scanResult = await handleOtherToolInput(event, api, payloadSessionId, taskId);
|
|
30
33
|
}
|
|
31
|
-
if (scanResult?.status === '
|
|
34
|
+
if (scanResult?.status === 'reject') {
|
|
32
35
|
logger.warn(`[SENTINEL HOOK] TOOL_INPUT REJECT, blocking tool call: ${event.toolName}`);
|
|
33
36
|
return { block: true, blockReason: `安全扫描检测到风险,已阻止工具调用: ${event.toolName}` };
|
|
34
37
|
}
|
|
@@ -47,8 +50,11 @@ export default function register(api) {
|
|
|
47
50
|
// 获取真实sessionID:优先使用ALS中的A2A sessionId,降级到OpenClaw runId或随机值
|
|
48
51
|
const sessionCtx = getCurrentSessionContext();
|
|
49
52
|
const sessionId = sessionCtx?.sessionId || (event.runId?.replace(/-/g, '') || crypto.randomBytes(16).toString('hex'));
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
const taskId = sessionCtx?.taskId || event.runId;
|
|
54
|
+
logger.log(`[SENTINEL HOOK] Session ID: ${sessionId}, Task ID: ${taskId} (fromALS: ${!!sessionCtx?.sessionId})`);
|
|
55
|
+
// 请求体中的sessionID从taskId中提取(第一个&之前的内容)
|
|
56
|
+
const payloadSessionId = extractSessionId(taskId) || sessionId;
|
|
57
|
+
// 处理TOOL_OUTPUT数据采集
|
|
52
58
|
const resultText = extractResultText(event, event.toolName);
|
|
53
59
|
const resultTextLength = resultText.length;
|
|
54
60
|
if (resultTextLength > MAX_TOTAL_LENGTH) {
|
|
@@ -60,25 +66,23 @@ export default function register(api) {
|
|
|
60
66
|
return;
|
|
61
67
|
}
|
|
62
68
|
// 处理和验证文本
|
|
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);
|
|
69
|
+
const originText = processText(resultText);
|
|
70
|
+
let content = originText;
|
|
71
|
+
if (originText.length > MAX_TEXT_LENGTH) {
|
|
72
|
+
const { text: filterText, truncated } = validateAndTruncateText(originText, MAX_TEXT_LENGTH);
|
|
70
73
|
if (truncated) {
|
|
71
|
-
|
|
74
|
+
content = filterText;
|
|
72
75
|
logger.warn(`[SENTINEL HOOK] postText exceeds ${MAX_TEXT_LENGTH}.`);
|
|
73
76
|
}
|
|
74
77
|
}
|
|
75
|
-
const
|
|
76
|
-
|
|
78
|
+
const interActionID = extractInterActionId(taskId);
|
|
79
|
+
const outputPayload = buildToolOutputPayload(taskId, payloadSessionId, event.toolName, content, event.toolCallId, interActionID);
|
|
80
|
+
logger.log(`[SENTINEL HOOK] Content extracted successfully. Length: ${JSON.stringify(outputPayload).length}`);
|
|
77
81
|
try {
|
|
78
|
-
const response = await callApi(
|
|
82
|
+
const response = await callApi(outputPayload, api, sessionId);
|
|
79
83
|
const result = parseSecurityResult(response);
|
|
80
84
|
logger.log(`[SENTINEL HOOK] TOOL_OUTPUT response: status=${result.status}.`);
|
|
81
|
-
if (result.status === '
|
|
85
|
+
if (result.status === 'reject') {
|
|
82
86
|
logger.warn('[SENTINEL HOOK] REJECT detected, attempting steer injection');
|
|
83
87
|
if (sessionCtx?.sessionId && sessionCtx?.taskId) {
|
|
84
88
|
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,32 @@ 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 extractSessionId(taskId: string): string;
|
|
17
|
+
export declare function extractInterActionId(taskId: string): number;
|
|
18
|
+
export declare function buildToolInputPayload(taskID: string, sessionID: string, toolName: string, toolArguments: string, toolCallId: string, interActionID: number, options?: {
|
|
19
|
+
file?: {
|
|
20
|
+
type: string;
|
|
21
|
+
url: string;
|
|
22
|
+
hash: string;
|
|
23
|
+
size: number;
|
|
24
|
+
body: string;
|
|
25
|
+
};
|
|
26
|
+
}): object;
|
|
27
|
+
export declare function buildToolOutputPayload(taskID: string, sessionID: string, funcName: string, content: string, toolCallId: string, interActionID: number): object;
|
|
28
|
+
export declare function handleExecToolInput(event: any, api: OpenClawPluginApi, sessionId: string, taskId: string): Promise<{
|
|
29
|
+
status: 'accept' | 'reject';
|
|
19
30
|
} | null>;
|
|
20
|
-
export declare function handleMessageToolInput(event: any, api: OpenClawPluginApi, sessionId: string): Promise<{
|
|
21
|
-
status: '
|
|
31
|
+
export declare function handleMessageToolInput(event: any, api: OpenClawPluginApi, sessionId: string, taskId: string): Promise<{
|
|
32
|
+
status: 'accept' | 'reject';
|
|
22
33
|
} | null>;
|
|
23
|
-
export declare function handleOtherToolInput(event: any, api: OpenClawPluginApi, sessionId: string): Promise<{
|
|
24
|
-
status: '
|
|
34
|
+
export declare function handleOtherToolInput(event: any, api: OpenClawPluginApi, sessionId: string, taskId: string): Promise<{
|
|
35
|
+
status: 'accept' | 'reject';
|
|
25
36
|
} | null>;
|