@ynhcj/xiaoyi-channel 0.0.157-beta → 0.0.157-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/bot.js +13 -5
- package/dist/src/cron-query-handler.d.ts +1 -11
- package/dist/src/cron-query-handler.js +88 -1
- package/dist/src/cspl/call_api.d.ts +1 -1
- package/dist/src/cspl/call_api.js +2 -2
- package/dist/src/cspl/config.js +30 -10
- package/dist/src/cspl/constants.d.ts +3 -0
- package/dist/src/cspl/constants.js +5 -0
- package/dist/src/cspl/sentinel_hook.js +26 -7
- package/dist/src/cspl/utils.d.ts +9 -3
- package/dist/src/cspl/utils.js +17 -11
- package/dist/src/file-upload.d.ts +5 -0
- package/dist/src/file-upload.js +102 -0
- package/dist/src/formatter.d.ts +30 -0
- package/dist/src/formatter.js +61 -5
- package/dist/src/monitor.js +35 -23
- package/dist/src/parser.d.ts +6 -0
- package/dist/src/parser.js +47 -1
- package/dist/src/provider.js +25 -17
- package/dist/src/reply-dispatcher.js +86 -23
- package/dist/src/self-evolution-handler.d.ts +1 -1
- package/dist/src/self-evolution-handler.js +12 -1
- package/dist/src/tools/agent-as-skill-tool.js +55 -4
- package/dist/src/tools/calendar-tool.js +1 -1
- package/dist/src/tools/call-device-tool.js +0 -3
- package/dist/src/tools/call-phone-tool.js +1 -1
- package/dist/src/tools/check-plugin-privilege-tool.d.ts +6 -0
- package/dist/src/tools/check-plugin-privilege-tool.js +182 -0
- package/dist/src/tools/create-alarm-tool.js +1 -1
- package/dist/src/tools/create-all-tools.js +8 -4
- package/dist/src/tools/delete-alarm-tool.js +1 -1
- package/dist/src/tools/device-tool-map.js +1 -0
- package/dist/src/tools/discover-cross-devices-tool.js +1 -1
- package/dist/src/tools/display-a2ui-card-tool.d.ts +2 -0
- package/dist/src/tools/display-a2ui-card-tool.js +85 -0
- package/dist/src/tools/get-collection-tool-schema.js +1 -1
- package/dist/src/tools/get-device-file-tool-schema.js +2 -3
- package/dist/src/tools/location-tool.js +1 -1
- package/dist/src/tools/modify-alarm-tool.js +1 -1
- package/dist/src/tools/modify-note-tool.js +1 -1
- package/dist/src/tools/note-tool.js +1 -1
- package/dist/src/tools/query-app-message-tool.js +1 -1
- package/dist/src/tools/query-memory-data-tool.js +1 -1
- package/dist/src/tools/query-todo-task-tool.js +1 -1
- package/dist/src/tools/save-file-to-phone-tool.js +1 -1
- package/dist/src/tools/save-media-to-gallery-tool.js +1 -1
- package/dist/src/tools/schema-tool-factory.js +1 -1
- package/dist/src/tools/search-alarm-tool.js +1 -1
- package/dist/src/tools/search-calendar-tool.js +1 -1
- package/dist/src/tools/search-contact-tool.js +1 -1
- package/dist/src/tools/search-email-tool.js +1 -1
- package/dist/src/tools/search-file-tool.js +5 -10
- package/dist/src/tools/search-note-tool.js +1 -1
- package/dist/src/tools/search-photo-gallery-tool.js +1 -1
- package/dist/src/tools/send-cross-device-task-tool.js +18 -22
- package/dist/src/tools/send-email-tool.js +1 -1
- package/dist/src/tools/send-file-to-user-tool.d.ts +1 -1
- package/dist/src/tools/send-file-to-user-tool.js +35 -6
- package/dist/src/tools/send-html-card-tool.d.ts +7 -0
- package/dist/src/tools/send-html-card-tool.js +115 -0
- package/dist/src/tools/session-manager.d.ts +6 -2
- package/dist/src/tools/session-manager.js +42 -7
- package/dist/src/tools/upload-file-tool.d.ts +1 -1
- package/dist/src/tools/upload-file-tool.js +3 -17
- package/dist/src/tools/upload-photo-tool.js +1 -1
- package/dist/src/tools/xiaoyi-add-collection-tool.js +3 -2
- package/dist/src/tools/xiaoyi-collection-tool.js +2 -2
- package/dist/src/tools/xiaoyi-delete-collection-tool.js +2 -2
- package/dist/src/tools/xiaoyi-gui-tool.js +6 -1
- package/dist/src/types.d.ts +10 -3
- package/dist/src/websocket.js +29 -8
- package/package.json +1 -1
package/dist/src/bot.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getXYRuntime } from "./runtime.js";
|
|
2
2
|
import { createXYReplyDispatcher } from "./reply-dispatcher.js";
|
|
3
|
-
import { parseA2AMessage, extractTextFromParts, extractFileParts, extractPushId, extractDeviceType, extractTriggerData, extractRunCrossTaskContext } from "./parser.js";
|
|
3
|
+
import { parseA2AMessage, extractTextFromParts, extractFileParts, extractPushId, extractDeviceType, extractModelName, extractTriggerData, extractRunCrossTaskContext } from "./parser.js";
|
|
4
4
|
import { downloadFilesFromParts } from "./file-download.js";
|
|
5
5
|
import { resolveXYConfig } from "./config.js";
|
|
6
6
|
import { sendStatusUpdate, sendClearContextResponse, sendTasksCancelResponse, sendA2AResponse } from "./formatter.js";
|
|
@@ -32,9 +32,10 @@ export async function handleXYMessage(params) {
|
|
|
32
32
|
try {
|
|
33
33
|
// Check for special messages BEFORE parsing (these have different param structures)
|
|
34
34
|
const messageMethod = message.method;
|
|
35
|
-
|
|
35
|
+
logger.log(`[BOT] Received A2A message: ${JSON.stringify(message)}`);
|
|
36
|
+
// Handle clearContext messages (sessionId at top level, no params)
|
|
36
37
|
if (messageMethod === "clearContext" || messageMethod === "clear_context") {
|
|
37
|
-
const sessionId = message.params?.sessionId;
|
|
38
|
+
const sessionId = message.sessionId ?? message.params?.sessionId;
|
|
38
39
|
if (!sessionId) {
|
|
39
40
|
throw new Error("clearContext request missing sessionId in params");
|
|
40
41
|
}
|
|
@@ -48,9 +49,9 @@ export async function handleXYMessage(params) {
|
|
|
48
49
|
});
|
|
49
50
|
return;
|
|
50
51
|
}
|
|
51
|
-
// Handle tasks/cancel messages
|
|
52
|
+
// Handle tasks/cancel messages (sessionId at top level, no params)
|
|
52
53
|
if (messageMethod === "tasks/cancel" || messageMethod === "tasks_cancel") {
|
|
53
|
-
const sessionId = message.params?.sessionId;
|
|
54
|
+
const sessionId = message.sessionId ?? message.params?.sessionId;
|
|
54
55
|
const taskId = message.params?.id || message.id;
|
|
55
56
|
if (!sessionId) {
|
|
56
57
|
throw new Error("tasks/cancel request missing sessionId in params");
|
|
@@ -138,6 +139,11 @@ export async function handleXYMessage(params) {
|
|
|
138
139
|
if (deviceType) {
|
|
139
140
|
log.log(`[BOT] Extracted deviceType: ${deviceType}`);
|
|
140
141
|
}
|
|
142
|
+
// Extract modelName if present (used by provider.ts to override model.id)
|
|
143
|
+
const modelName = extractModelName(parsed.parts);
|
|
144
|
+
if (modelName) {
|
|
145
|
+
log.log(`[BOT] Extracted modelName: ${modelName}`);
|
|
146
|
+
}
|
|
141
147
|
const runCrossTaskContext = extractRunCrossTaskContext(parsed.parts);
|
|
142
148
|
// Resolve configuration (needed for status updates)
|
|
143
149
|
const config = resolveXYConfig(cfg);
|
|
@@ -164,6 +170,7 @@ export async function handleXYMessage(params) {
|
|
|
164
170
|
messageId: parsed.messageId,
|
|
165
171
|
agentId: route.accountId,
|
|
166
172
|
deviceType,
|
|
173
|
+
modelName,
|
|
167
174
|
runCrossTaskContext: runCrossTaskContext ?? undefined,
|
|
168
175
|
});
|
|
169
176
|
// 🔑 发送初始状态更新
|
|
@@ -311,6 +318,7 @@ export async function handleXYMessage(params) {
|
|
|
311
318
|
messageId: parsed.messageId,
|
|
312
319
|
agentId: route.accountId,
|
|
313
320
|
deviceType,
|
|
321
|
+
modelName,
|
|
314
322
|
runCrossTaskContext: runCrossTaskContext ?? undefined,
|
|
315
323
|
};
|
|
316
324
|
log.log(`[BOT-DISPATCH] withReplyDispatcher starting, sessionKey=${route.sessionKey}`);
|
|
@@ -1,17 +1,7 @@
|
|
|
1
|
-
export type CronQueryAction = "list" | "status" | "runs" | "add" | "update" | "remove" | "run";
|
|
2
|
-
export interface CronQueryEventContext {
|
|
3
|
-
action: CronQueryAction;
|
|
4
|
-
jobId?: string;
|
|
5
|
-
params?: Record<string, unknown>;
|
|
6
|
-
/** Original A2A message fields for routing the response. */
|
|
7
|
-
sessionId?: string;
|
|
8
|
-
taskId?: string;
|
|
9
|
-
messageId?: string;
|
|
10
|
-
}
|
|
11
1
|
/**
|
|
12
2
|
* Handle a cron-query-event.
|
|
13
3
|
*
|
|
14
4
|
* Calls the Gateway cron RPC and sends the result back through sendCommand
|
|
15
5
|
* as a System.CronQuery command with the full result object in payload.ans.
|
|
16
6
|
*/
|
|
17
|
-
export declare function handleCronQueryEvent(context:
|
|
7
|
+
export declare function handleCronQueryEvent(context: any, cfg: any): Promise<void>;
|
|
@@ -4,9 +4,12 @@
|
|
|
4
4
|
// result back to the client via sendCommand as a System.CronQuery
|
|
5
5
|
// command with the result in payload.ans.
|
|
6
6
|
import { callGatewayTool } from "openclaw/plugin-sdk/agent-harness-runtime";
|
|
7
|
+
import * as os from "os";
|
|
7
8
|
import { sendCommand } from "./formatter.js";
|
|
8
9
|
import { resolveXYConfig } from "./config.js";
|
|
9
10
|
import { logger } from "./utils/logger.js";
|
|
11
|
+
import { readFileSync, readdirSync } from "fs";
|
|
12
|
+
import { join } from "path";
|
|
10
13
|
const GATEWAY_TIMEOUT_MS = 60_000;
|
|
11
14
|
/**
|
|
12
15
|
* Handle a cron-query-event.
|
|
@@ -54,6 +57,9 @@ export async function handleCronQueryEvent(context, cfg) {
|
|
|
54
57
|
...params,
|
|
55
58
|
});
|
|
56
59
|
break;
|
|
60
|
+
case "queryTimeList":
|
|
61
|
+
result = await queryTimeListLocal();
|
|
62
|
+
break;
|
|
57
63
|
default:
|
|
58
64
|
error = `Unknown action: ${context.action}`;
|
|
59
65
|
logger.error(`[CRON-QUERY] ${error}`);
|
|
@@ -73,7 +79,7 @@ export async function handleCronQueryEvent(context, cfg) {
|
|
|
73
79
|
const config = resolveXYConfig(cfg);
|
|
74
80
|
const command = {
|
|
75
81
|
header: {
|
|
76
|
-
namespace: "
|
|
82
|
+
namespace: "AgentEvent",
|
|
77
83
|
name: "CronQuery",
|
|
78
84
|
},
|
|
79
85
|
payload: {
|
|
@@ -99,3 +105,84 @@ export async function handleCronQueryEvent(context, cfg) {
|
|
|
99
105
|
logger.warn(`[CRON-QUERY] Missing cfg/sessionId/taskId/messageId, skipping sendCommand`);
|
|
100
106
|
}
|
|
101
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Read local cron folder directly (bypassing openclaw RPC) and return
|
|
110
|
+
* run records from the last 7 days, grouped by date and sorted by time.
|
|
111
|
+
*
|
|
112
|
+
* Data sources:
|
|
113
|
+
* - state/cron/jobs.json → job id → name mapping
|
|
114
|
+
* - state/cron/runs/*.jsonl → run records (one JSON per line)
|
|
115
|
+
*
|
|
116
|
+
* Return format:
|
|
117
|
+
* [ { "YYYY-MM-DD": [ { run record with .name }, ... ] }, ... ]
|
|
118
|
+
*/
|
|
119
|
+
async function queryTimeListLocal() {
|
|
120
|
+
const cronDir = join(os.homedir(), ".openclaw", "cron");
|
|
121
|
+
const jobsPath = join(cronDir, "jobs.json");
|
|
122
|
+
const runsDir = join(cronDir, "runs");
|
|
123
|
+
// 1. Build jobId → name map from jobs.json
|
|
124
|
+
const jobNameMap = {};
|
|
125
|
+
try {
|
|
126
|
+
const jobsRaw = readFileSync(jobsPath, "utf-8");
|
|
127
|
+
const jobsData = JSON.parse(jobsRaw);
|
|
128
|
+
for (const job of jobsData.jobs || []) {
|
|
129
|
+
jobNameMap[job.id] = job.name || job.id;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
logger.error(`[CRON-QUERY] Failed to read jobs.json: ${err.message}`);
|
|
134
|
+
}
|
|
135
|
+
// 2. Read all run files, collect runs within last 7 days
|
|
136
|
+
const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
|
|
137
|
+
const allRuns = [];
|
|
138
|
+
let files = [];
|
|
139
|
+
try {
|
|
140
|
+
files = readdirSync(runsDir);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
files = [];
|
|
144
|
+
}
|
|
145
|
+
for (const file of files) {
|
|
146
|
+
if (!file.endsWith(".jsonl"))
|
|
147
|
+
continue;
|
|
148
|
+
try {
|
|
149
|
+
const content = readFileSync(join(runsDir, file), "utf-8");
|
|
150
|
+
const lines = content.trim().split("\n");
|
|
151
|
+
for (const line of lines) {
|
|
152
|
+
if (!line.trim())
|
|
153
|
+
continue;
|
|
154
|
+
try {
|
|
155
|
+
const run = JSON.parse(line);
|
|
156
|
+
if (run.ts && run.ts >= sevenDaysAgo) {
|
|
157
|
+
run.name = jobNameMap[run.jobId] || run.jobId || "";
|
|
158
|
+
allRuns.push(run);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
// skip malformed line
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
logger.error(`[CRON-QUERY] Failed to read run file ${file}: ${err.message}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// 3. Sort by ts ascending
|
|
171
|
+
allRuns.sort((a, b) => a.ts - b.ts);
|
|
172
|
+
// 4. Group by date (YYYY-MM-DD in local time)
|
|
173
|
+
const grouped = new Map();
|
|
174
|
+
for (const run of allRuns) {
|
|
175
|
+
const d = new Date(run.ts);
|
|
176
|
+
const label = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
177
|
+
if (!grouped.has(label)) {
|
|
178
|
+
grouped.set(label, []);
|
|
179
|
+
}
|
|
180
|
+
grouped.get(label).push(run);
|
|
181
|
+
}
|
|
182
|
+
// 5. Convert to ordered array of single-key objects
|
|
183
|
+
const result = [];
|
|
184
|
+
for (const [date, runs] of grouped) {
|
|
185
|
+
result.push({ [date]: runs });
|
|
186
|
+
}
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import { ApiResponse } from './constants.js';
|
|
2
|
-
export declare function callApi(questionText: string, api: any, sessionId: string): Promise<ApiResponse>;
|
|
2
|
+
export declare function callApi(questionText: string, api: any, sessionId: string, action: string): Promise<ApiResponse>;
|
|
@@ -78,13 +78,13 @@ function handleResponse(res, resolve, reject) {
|
|
|
78
78
|
}
|
|
79
79
|
});
|
|
80
80
|
}
|
|
81
|
-
export async function callApi(questionText, api, sessionId) {
|
|
81
|
+
export async function callApi(questionText, api, sessionId, action) {
|
|
82
82
|
const config = getConfig(api);
|
|
83
83
|
const headersForCelia = buildHeadersForCelia(config, sessionId);
|
|
84
84
|
const payload = {
|
|
85
85
|
questionText: questionText,
|
|
86
86
|
textSource: config.textSource,
|
|
87
|
-
action:
|
|
87
|
+
action: action,
|
|
88
88
|
extra: `${JSON.stringify({ userId: config.uid })}`
|
|
89
89
|
};
|
|
90
90
|
const httpBody = JSON.stringify(payload);
|
package/dist/src/cspl/config.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* 版权所有 (c) 华为技术有限公司 2026-2026
|
|
3
3
|
*/
|
|
4
4
|
import fs from 'fs';
|
|
5
|
-
import
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { CONFIG_FILE_NAME, ENV_FILE_PATH, REQUIRED_ENV_VARS } from './constants.js';
|
|
6
7
|
import { logger } from '../utils/logger.js';
|
|
7
|
-
import defaultConfig from './configs.json' with { type: 'json' };
|
|
8
8
|
let cachedConfig = null;
|
|
9
9
|
function readEnvFile() {
|
|
10
10
|
if (!fs.existsSync(ENV_FILE_PATH)) {
|
|
@@ -41,25 +41,45 @@ export function getConfig(api) {
|
|
|
41
41
|
if (cachedConfig) {
|
|
42
42
|
return cachedConfig;
|
|
43
43
|
}
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
const configPath = path.join(__dirname, CONFIG_FILE_NAME);
|
|
45
|
+
if (!fs.existsSync(configPath)) {
|
|
46
|
+
throw new Error(`Config file not found: ${CONFIG_FILE_NAME}`);
|
|
47
|
+
}
|
|
48
|
+
let configData;
|
|
49
|
+
try {
|
|
50
|
+
configData = fs.readFileSync(configPath, 'utf-8');
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
throw new Error(`Failed to read config file: ${CONFIG_FILE_NAME}.`);
|
|
54
|
+
}
|
|
55
|
+
let parsedConfig;
|
|
56
|
+
try {
|
|
57
|
+
parsedConfig = JSON.parse(configData);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
throw new Error(`Failed to parse config file: ${CONFIG_FILE_NAME}.`);
|
|
61
|
+
}
|
|
62
|
+
if (!parsedConfig || typeof parsedConfig !== 'object') {
|
|
63
|
+
throw new Error(`Invalid config structure: ${CONFIG_FILE_NAME}. Expected an object.`);
|
|
64
|
+
}
|
|
65
|
+
const config = parsedConfig;
|
|
46
66
|
if (!config.api || typeof config.api !== 'object') {
|
|
47
|
-
throw new Error(`Invalid config: missing or invalid 'api' section`);
|
|
67
|
+
throw new Error(`Invalid config: missing or invalid 'api' section in ${CONFIG_FILE_NAME}`);
|
|
48
68
|
}
|
|
49
69
|
if (!config.api.timeout || typeof config.api.timeout !== 'number') {
|
|
50
|
-
throw new Error(`Invalid config: missing or invalid 'api.timeout'`);
|
|
70
|
+
throw new Error(`Invalid config: missing or invalid 'api.timeout' in ${CONFIG_FILE_NAME}`);
|
|
51
71
|
}
|
|
52
72
|
if (!config.skillId || typeof config.skillId !== 'string') {
|
|
53
|
-
throw new Error(`Invalid config: missing or invalid 'skillId'`);
|
|
73
|
+
throw new Error(`Invalid config: missing or invalid 'skillId' in ${CONFIG_FILE_NAME}`);
|
|
54
74
|
}
|
|
55
75
|
if (!config.requestFrom || typeof config.requestFrom !== 'string') {
|
|
56
|
-
throw new Error(`Invalid config: missing or invalid 'requestFrom'`);
|
|
76
|
+
throw new Error(`Invalid config: missing or invalid 'requestFrom' in ${CONFIG_FILE_NAME}`);
|
|
57
77
|
}
|
|
58
78
|
if (!config.textSource || typeof config.textSource !== 'string') {
|
|
59
|
-
throw new Error(`Invalid config: missing or invalid 'textSource'`);
|
|
79
|
+
throw new Error(`Invalid config: missing or invalid 'textSource' in ${CONFIG_FILE_NAME}`);
|
|
60
80
|
}
|
|
61
81
|
if (!config.action || typeof config.action !== 'string') {
|
|
62
|
-
throw new Error(`Invalid config: missing or invalid 'action'`);
|
|
82
|
+
throw new Error(`Invalid config: missing or invalid 'action' in ${CONFIG_FILE_NAME}`);
|
|
63
83
|
}
|
|
64
84
|
let env;
|
|
65
85
|
try {
|
|
@@ -29,6 +29,7 @@ export declare const ENV_FILE_PATH = "/home/sandbox/.openclaw/.xiaoyienv";
|
|
|
29
29
|
export declare const API_URL_SUFFIX = "/celia-claw/v1/rest-api/skill/execute";
|
|
30
30
|
export declare const REQUIRED_ENV_VARS: string[];
|
|
31
31
|
export declare const ALLOWED_TOOLS: string[];
|
|
32
|
+
export declare const STEER_ABORT_MESSAGE = "\u5F53\u524D\u884C\u4E3A\u5B58\u5728\u5B89\u5168\u9690\u60A3\uFF0C\u7EC8\u6B62\u6240\u6709\u64CD\u4F5C\uFF0C\u5E76\u4E14\u5728\u6700\u7EC8\u56DE\u590D\u4E2D\u8BF4\u660E\uFF0C\u8BC6\u522B\u5230\u5F53\u524D\u6D41\u7A0B\u4E2D\u5B58\u5728\u6F5C\u5728\u5B89\u5168\u98CE\u9669\uFF0C\u5DF2\u5E2E\u60A8\u4E2D\u6B62\u5F53\u524D\u6D41\u7A0B\uFF0C\u8BF7\u6E05\u7A7A\u4E0A\u4E0B\u6587\u540E\u91CD\u8BD5";
|
|
32
33
|
export declare const MAX_FILE_COUNT = 10;
|
|
33
34
|
export declare const MAX_COMMAND_LENGTH = 1024;
|
|
34
35
|
export declare const CODE_FILE_EXTENSIONS: string[];
|
|
@@ -42,6 +43,8 @@ export declare const TOOL_INPUT_DEFAULT: {
|
|
|
42
43
|
readonly source: "";
|
|
43
44
|
readonly content: "";
|
|
44
45
|
};
|
|
46
|
+
export declare const TOOL_INPUT_ACTION = "TOOL_INPUT_SCAN";
|
|
47
|
+
export declare const TOOL_OUTPUT_ACTION = "TOOL_OUTPUT_SCAN";
|
|
45
48
|
export declare const MAX_TIMES = 3;
|
|
46
49
|
export declare const CONNECT_TIMEOUT = 15000;
|
|
47
50
|
export declare const READ_TIMEOUT = 300000;
|
|
@@ -28,6 +28,8 @@ export const API_URL_SUFFIX = '/celia-claw/v1/rest-api/skill/execute';
|
|
|
28
28
|
export const REQUIRED_ENV_VARS = ['PERSONAL-API-KEY', 'PERSONAL-UID', 'SERVICE_URL'];
|
|
29
29
|
// 工具白名单:除了web_fetch特殊处理外,只有这些工具才处理result文本
|
|
30
30
|
export const ALLOWED_TOOLS = ['web_fetch', 'read', 'pdf', 'exec', 'message'];
|
|
31
|
+
// Steer 注入话术:CSPL REJECT 时通过 steer 插话打断模型
|
|
32
|
+
export const STEER_ABORT_MESSAGE = '当前行为存在安全隐患,终止所有操作,并且在最终回复中说明,识别到当前流程中存在潜在安全风险,已帮您中止当前流程,请清空上下文后重试';
|
|
31
33
|
// 文件数量限制
|
|
32
34
|
export const MAX_FILE_COUNT = 10;
|
|
33
35
|
// 命令字符串截断长度(字节)
|
|
@@ -45,6 +47,9 @@ export const TOOL_INPUT_DEFAULT = {
|
|
|
45
47
|
source: '',
|
|
46
48
|
content: ''
|
|
47
49
|
};
|
|
50
|
+
// 安全扫描 action 常量
|
|
51
|
+
export const TOOL_INPUT_ACTION = 'TOOL_INPUT_SCAN';
|
|
52
|
+
export const TOOL_OUTPUT_ACTION = 'TOOL_OUTPUT_SCAN';
|
|
48
53
|
// OBS上传相关常量
|
|
49
54
|
export const MAX_TIMES = 3;
|
|
50
55
|
export const CONNECT_TIMEOUT = 15000;
|
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
import crypto from 'crypto';
|
|
5
5
|
import { callApi } from './call_api.js';
|
|
6
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 } from './constants.js';
|
|
7
|
+
import { ALLOWED_TOOLS, MAX_TEXT_LENGTH, MAX_TOTAL_LENGTH, MIN_TEXT_LENGTH, STEER_ABORT_MESSAGE, TOOL_OUTPUT_ACTION } from './constants.js';
|
|
8
8
|
import { logger } from '../utils/logger.js';
|
|
9
|
+
import { getSessionContext } from '../tools/session-manager.js';
|
|
10
|
+
import { tryInjectSteer } from './steer-context.js';
|
|
9
11
|
// 主入口模块
|
|
10
12
|
export default function register(api) {
|
|
11
13
|
api.on("before_tool_call", async (event, ctx) => {
|
|
@@ -13,16 +15,21 @@ export default function register(api) {
|
|
|
13
15
|
// 生成sessionID
|
|
14
16
|
const sessionId = (event.runId?.replace(/-/g, '') || crypto.randomBytes(16).toString('hex'));
|
|
15
17
|
logger.log(`[SENTINEL HOOK] Generated Session ID: ${sessionId}`);
|
|
16
|
-
// 处理 TOOL_INPUT
|
|
18
|
+
// 处理 TOOL_INPUT 数据采集、发送数据,根据扫描结果决定是否阻塞
|
|
17
19
|
try {
|
|
20
|
+
let scanResult = null;
|
|
18
21
|
if (event.toolName === 'exec') {
|
|
19
|
-
await handleExecToolInput(event, api, sessionId);
|
|
22
|
+
scanResult = await handleExecToolInput(event, api, sessionId);
|
|
20
23
|
}
|
|
21
24
|
else if (event.toolName === 'message') {
|
|
22
|
-
await handleMessageToolInput(event, api, sessionId);
|
|
25
|
+
scanResult = await handleMessageToolInput(event, api, sessionId);
|
|
23
26
|
}
|
|
24
27
|
else {
|
|
25
|
-
await handleOtherToolInput(event, api, sessionId);
|
|
28
|
+
scanResult = await handleOtherToolInput(event, api, sessionId);
|
|
29
|
+
}
|
|
30
|
+
if (scanResult?.status === 'REJECT') {
|
|
31
|
+
logger.warn(`[SENTINEL HOOK] TOOL_INPUT REJECT, blocking tool call: ${event.toolName}`);
|
|
32
|
+
return { block: true, blockReason: `安全扫描检测到风险,已阻止工具调用: ${event.toolName}` };
|
|
26
33
|
}
|
|
27
34
|
}
|
|
28
35
|
catch (error) {
|
|
@@ -66,11 +73,23 @@ export default function register(api) {
|
|
|
66
73
|
const postText = JSON.stringify(questionText);
|
|
67
74
|
logger.log(`[SENTINEL HOOK] Content extracted successfully. Length: ${postText.length}`);
|
|
68
75
|
try {
|
|
69
|
-
const response = await callApi(postText, api, sessionId);
|
|
76
|
+
const response = await callApi(postText, api, sessionId, TOOL_OUTPUT_ACTION);
|
|
70
77
|
const result = parseSecurityResult(response);
|
|
71
78
|
logger.log(`[SENTINEL HOOK] TOOL_OUTPUT response: status=${result.status}.`);
|
|
72
79
|
if (result.status === 'REJECT') {
|
|
73
|
-
logger.warn('[SENTINEL HOOK]
|
|
80
|
+
logger.warn('[SENTINEL HOOK] REJECT detected, attempting steer injection');
|
|
81
|
+
const sessionCtx = ctx.sessionKey ? getSessionContext(ctx.sessionKey) : null;
|
|
82
|
+
if (sessionCtx?.sessionId && sessionCtx?.taskId) {
|
|
83
|
+
await tryInjectSteer({
|
|
84
|
+
sessionId: sessionCtx.sessionId,
|
|
85
|
+
taskId: sessionCtx.taskId,
|
|
86
|
+
message: STEER_ABORT_MESSAGE,
|
|
87
|
+
source: 'cspl',
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
logger.warn(`[SENTINEL HOOK] Cannot inject steer: sessionKey=${ctx.sessionKey}, sessionCtx found=${!!sessionCtx}`);
|
|
92
|
+
}
|
|
74
93
|
}
|
|
75
94
|
}
|
|
76
95
|
catch (error) {
|
package/dist/src/cspl/utils.d.ts
CHANGED
|
@@ -14,6 +14,12 @@ 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
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
|
-
|
|
19
|
-
|
|
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>;
|
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, TOOL_INPUT_DEFAULT, FILE_EXTENSION_REGEX } from './constants.js';
|
|
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
5
|
import crypto from 'crypto';
|
|
6
6
|
import fs from 'fs';
|
|
7
7
|
import path from 'path';
|
|
@@ -213,13 +213,14 @@ export function adjustContentLength(data, api, fields) {
|
|
|
213
213
|
}
|
|
214
214
|
return adjusted;
|
|
215
215
|
}
|
|
216
|
-
// 发送TOOL_INPUT
|
|
216
|
+
// 发送TOOL_INPUT请求并处理响应,返回扫描结果
|
|
217
217
|
async function sendToolInputRequest(postText, api, sessionId) {
|
|
218
|
-
const response = await callApi(postText, api, sessionId);
|
|
218
|
+
const response = await callApi(postText, api, sessionId, TOOL_INPUT_ACTION);
|
|
219
219
|
const result = parseSecurityResult(response);
|
|
220
220
|
logger.log(`[SENTINEL HOOK] TOOL_INPUT response: status=${result.status}`);
|
|
221
|
+
return result;
|
|
221
222
|
}
|
|
222
|
-
// 处理exec工具的TOOL_INPUT
|
|
223
|
+
// 处理exec工具的TOOL_INPUT数据采集,返回最终扫描结果
|
|
223
224
|
export async function handleExecToolInput(event, api, sessionId) {
|
|
224
225
|
const command = extractInputParams(event, 'exec');
|
|
225
226
|
if (!command) {
|
|
@@ -232,6 +233,7 @@ export async function handleExecToolInput(event, api, sessionId) {
|
|
|
232
233
|
// 场景1:执行代码文件
|
|
233
234
|
logger.log(`[SENTINEL HOOK] Found ${filePaths.length} file(s) in command`);
|
|
234
235
|
const nonExistingFiles = [];
|
|
236
|
+
let lastResult = null;
|
|
235
237
|
for (const filePath of filePaths) {
|
|
236
238
|
if (!fs.existsSync(filePath)) {
|
|
237
239
|
nonExistingFiles.push(filePath);
|
|
@@ -247,7 +249,10 @@ export async function handleExecToolInput(event, api, sessionId) {
|
|
|
247
249
|
const postText = JSON.stringify(adjustedData);
|
|
248
250
|
logger.log(`[SENTINEL HOOK] Sending TOOL_INPUT for file: ${path.basename(filePath)}, body length: ${postText.length}`);
|
|
249
251
|
try {
|
|
250
|
-
await sendToolInputRequest(postText, api, sessionId);
|
|
252
|
+
lastResult = await sendToolInputRequest(postText, api, sessionId);
|
|
253
|
+
if (lastResult.status === 'REJECT') {
|
|
254
|
+
return lastResult;
|
|
255
|
+
}
|
|
251
256
|
}
|
|
252
257
|
catch (e) {
|
|
253
258
|
logger.error(`[SENTINEL HOOK] Sending TOOL_INPUT Failed: ${e}`);
|
|
@@ -258,6 +263,7 @@ export async function handleExecToolInput(event, api, sessionId) {
|
|
|
258
263
|
const fileNames = nonExistingFiles.map(f => path.basename(f)).join(', ');
|
|
259
264
|
logger.log(`[SENTINEL HOOK] Non-existing files: ${fileNames}`);
|
|
260
265
|
}
|
|
266
|
+
return lastResult;
|
|
261
267
|
}
|
|
262
268
|
else {
|
|
263
269
|
// 场景2:直接执行代码(heredoc场景)
|
|
@@ -268,10 +274,10 @@ export async function handleExecToolInput(event, api, sessionId) {
|
|
|
268
274
|
const adjustedData = adjustContentLength(toolInputData, api, ['source']);
|
|
269
275
|
const postText = JSON.stringify(adjustedData);
|
|
270
276
|
logger.log(`[SENTINEL HOOK] Sending TOOL_INPUT for direct code execution, body length: ${postText.length}`);
|
|
271
|
-
await sendToolInputRequest(postText, api, sessionId);
|
|
277
|
+
return await sendToolInputRequest(postText, api, sessionId);
|
|
272
278
|
}
|
|
273
279
|
}
|
|
274
|
-
// 处理message工具的TOOL_INPUT
|
|
280
|
+
// 处理message工具的TOOL_INPUT数据采集,返回扫描结果
|
|
275
281
|
export async function handleMessageToolInput(event, api, sessionId) {
|
|
276
282
|
const message = extractInputParams(event, 'message');
|
|
277
283
|
if (!message) {
|
|
@@ -285,14 +291,14 @@ export async function handleMessageToolInput(event, api, sessionId) {
|
|
|
285
291
|
const adjustedData = adjustContentLength(toolInputData, api, ['content']);
|
|
286
292
|
const postText = JSON.stringify(adjustedData);
|
|
287
293
|
logger.log(`[SENTINEL HOOK] Sending TOOL_INPUT for message, body length: ${postText.length}`);
|
|
288
|
-
await sendToolInputRequest(postText, api, sessionId);
|
|
294
|
+
return await sendToolInputRequest(postText, api, sessionId);
|
|
289
295
|
}
|
|
290
|
-
// 处理其他工具(非 exec 和非 message)的 TOOL_INPUT
|
|
296
|
+
// 处理其他工具(非 exec 和非 message)的 TOOL_INPUT 数据采集,返回扫描结果
|
|
291
297
|
export async function handleOtherToolInput(event, api, sessionId) {
|
|
292
298
|
const params = event.params;
|
|
293
299
|
if (!params) {
|
|
294
300
|
logger.log('[SENTINEL HOOK] No params found for tool');
|
|
295
|
-
return;
|
|
301
|
+
return null;
|
|
296
302
|
}
|
|
297
303
|
logger.log(`[SENTINEL HOOK] Processing other tool input, toolName: ${event.toolName}`);
|
|
298
304
|
// 将 params 序列化为 JSON 字符串
|
|
@@ -305,5 +311,5 @@ export async function handleOtherToolInput(event, api, sessionId) {
|
|
|
305
311
|
const adjustedData = adjustContentLength(toolInputData, api, ['content']);
|
|
306
312
|
const postText = JSON.stringify(adjustedData);
|
|
307
313
|
logger.log(`[SENTINEL HOOK] Sending TOOL_INPUT for ${event.toolName}, body length: ${postText.length}`);
|
|
308
|
-
await sendToolInputRequest(postText, api, sessionId);
|
|
314
|
+
return await sendToolInputRequest(postText, api, sessionId);
|
|
309
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
|
*/
|
package/dist/src/file-upload.js
CHANGED
|
@@ -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
|
*/
|
package/dist/src/formatter.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ export interface SendA2AResponseParams {
|
|
|
17
17
|
}>;
|
|
18
18
|
errorCode?: number | string;
|
|
19
19
|
errorMessage?: string;
|
|
20
|
+
log?: boolean;
|
|
20
21
|
}
|
|
21
22
|
/**
|
|
22
23
|
* Send an A2A artifact update response.
|
|
@@ -81,6 +82,35 @@ export interface SendCommandParams {
|
|
|
81
82
|
* listening in the calling tool works unchanged.
|
|
82
83
|
*/
|
|
83
84
|
export declare function sendCommand(params: SendCommandParams): Promise<void>;
|
|
85
|
+
/**
|
|
86
|
+
* Parameters for sending a card (e.g., HTML H5 card).
|
|
87
|
+
*/
|
|
88
|
+
export interface SendCardParams {
|
|
89
|
+
config: XYChannelConfig;
|
|
90
|
+
sessionId: string;
|
|
91
|
+
taskId: string;
|
|
92
|
+
messageId: string;
|
|
93
|
+
/** toolCallId from the tool's execute() — used for cron detection via hook-set Map. */
|
|
94
|
+
toolCallId?: string;
|
|
95
|
+
/** When true, the artifact-update is sent with final=true. Default: false. */
|
|
96
|
+
final?: boolean;
|
|
97
|
+
/** Array of card data objects to send. */
|
|
98
|
+
cardsInfo: CardDataObject[];
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Card data object for sending display cards.
|
|
102
|
+
*/
|
|
103
|
+
export interface CardDataObject {
|
|
104
|
+
cardName: string;
|
|
105
|
+
cardData: Record<string, any>;
|
|
106
|
+
displayType: string;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Send a card (e.g., HTML H5 card) as an artifact update (final=false).
|
|
110
|
+
*
|
|
111
|
+
* Cron-aware: same routing logic as sendCommand.
|
|
112
|
+
*/
|
|
113
|
+
export declare function sendCard(params: SendCardParams): Promise<void>;
|
|
84
114
|
/**
|
|
85
115
|
* Parameters for sending a clearContext response.
|
|
86
116
|
*/
|