@ynhcj/xiaoyi-channel 0.0.152-beta → 0.0.152-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/index.js +23 -0
- package/dist/src/bot.js +21 -5
- package/dist/src/channel.js +59 -5
- package/dist/src/cron-command.d.ts +15 -0
- package/dist/src/cron-command.js +49 -0
- package/dist/src/cron-query-handler.d.ts +7 -0
- package/dist/src/cron-query-handler.js +188 -0
- 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/formatter.d.ts +14 -1
- package/dist/src/formatter.js +31 -8
- package/dist/src/monitor.js +44 -23
- package/dist/src/parser.d.ts +8 -1
- package/dist/src/parser.js +71 -0
- package/dist/src/provider.js +25 -17
- package/dist/src/push.d.ts +11 -1
- package/dist/src/push.js +93 -2
- package/dist/src/reply-dispatcher.js +118 -14
- package/dist/src/self-evolution-handler.d.ts +1 -1
- package/dist/src/self-evolution-handler.js +13 -2
- package/dist/src/tools/agent-as-skill-tool.js +56 -4
- package/dist/src/tools/calendar-tool.js +2 -1
- package/dist/src/tools/call-device-tool.js +0 -3
- package/dist/src/tools/call-phone-tool.js +2 -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 +2 -1
- package/dist/src/tools/create-all-tools.js +10 -4
- package/dist/src/tools/delete-alarm-tool.js +2 -1
- package/dist/src/tools/device-tool-map.d.ts +1 -1
- package/dist/src/tools/device-tool-map.js +12 -5
- package/dist/src/tools/discover-cross-devices-tool.d.ts +2 -0
- package/dist/src/tools/discover-cross-devices-tool.js +235 -0
- 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/find-pc-devices-tool.js +1 -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 +2 -1
- package/dist/src/tools/modify-alarm-tool.js +2 -1
- package/dist/src/tools/modify-note-tool.js +2 -1
- package/dist/src/tools/note-tool.js +2 -1
- package/dist/src/tools/query-app-message-tool.js +4 -3
- package/dist/src/tools/query-memory-data-tool.js +4 -3
- package/dist/src/tools/query-todo-task-tool.js +4 -3
- package/dist/src/tools/save-file-to-phone-tool.js +2 -1
- package/dist/src/tools/save-media-to-gallery-tool.js +2 -1
- package/dist/src/tools/schema-tool-factory.js +1 -1
- package/dist/src/tools/search-alarm-tool.js +2 -1
- package/dist/src/tools/search-calendar-tool.js +2 -1
- package/dist/src/tools/search-contact-tool.js +2 -1
- package/dist/src/tools/search-email-tool.js +4 -3
- package/dist/src/tools/search-file-tool.js +6 -10
- package/dist/src/tools/search-message-tool.js +1 -0
- package/dist/src/tools/search-note-tool.js +2 -1
- package/dist/src/tools/search-photo-gallery-tool.js +4 -3
- package/dist/src/tools/send-cross-device-task-tool.d.ts +2 -0
- package/dist/src/tools/send-cross-device-task-tool.js +299 -0
- package/dist/src/tools/send-email-tool.js +4 -3
- 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-message-tool.js +1 -0
- package/dist/src/tools/session-manager.d.ts +17 -1
- package/dist/src/tools/session-manager.js +73 -0
- package/dist/src/tools/upload-file-tool.js +6 -14
- package/dist/src/tools/upload-photo-tool.js +4 -3
- package/dist/src/tools/xiaoyi-add-collection-tool.js +4 -2
- package/dist/src/tools/xiaoyi-collection-tool.js +3 -2
- package/dist/src/tools/xiaoyi-delete-collection-tool.js +3 -2
- package/dist/src/tools/xiaoyi-gui-tool.js +6 -0
- package/dist/src/types.d.ts +25 -1
- package/dist/src/websocket.d.ts +3 -0
- package/dist/src/websocket.js +207 -15
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3,9 +3,30 @@ import { xiaoyiProvider } from "./src/provider.js";
|
|
|
3
3
|
import { xyPlugin } from "./src/channel.js";
|
|
4
4
|
import registerSentinelHook from "./src/cspl/sentinel_hook.js";
|
|
5
5
|
import { setXYRuntime } from "./src/runtime.js";
|
|
6
|
+
import { markCronToolCall, clearCronToolCall } from "./src/tools/session-manager.js";
|
|
6
7
|
import { registerSelfEvolutionToolResultNudge } from "./src/self-evolution-tool-result-nudge.js";
|
|
7
8
|
import { createBeforePromptBuildHandler } from "./src/skill-retriever/hooks.js";
|
|
8
9
|
import { normalizeToolRetrieverConfig } from "./src/skill-retriever/config.js";
|
|
10
|
+
/**
|
|
11
|
+
* Register the cron detection hook.
|
|
12
|
+
*
|
|
13
|
+
* When openclaw's cron runner triggers a tool call, the sessionKey has the
|
|
14
|
+
* format "cron:<jobId>". We use this to mark the toolCallId in a global Map
|
|
15
|
+
* so that sendCommand() can route the command through the push channel
|
|
16
|
+
* instead of the (non-existent) WebSocket session.
|
|
17
|
+
*/
|
|
18
|
+
function registerCronDetectionHook(api) {
|
|
19
|
+
api.on("before_tool_call", async (event, ctx) => {
|
|
20
|
+
if (ctx.sessionKey?.startsWith("cron:") && event.toolCallId) {
|
|
21
|
+
markCronToolCall(event.toolCallId);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
api.on("after_tool_call", async (event, ctx) => {
|
|
25
|
+
if (event.toolCallId) {
|
|
26
|
+
clearCronToolCall(event.toolCallId);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
9
30
|
function registerFullHooks(api) {
|
|
10
31
|
// SKILL RETRIEVER HOOK: before_prompt_build hook
|
|
11
32
|
const pluginConfig = api.pluginConfig || {};
|
|
@@ -45,6 +66,8 @@ export default definePluginEntry({
|
|
|
45
66
|
registerFullHooks(api);
|
|
46
67
|
// CSPL sentinel hook: before_tool_call + after_tool_call security scanning
|
|
47
68
|
registerSentinelHook(api);
|
|
69
|
+
// Cron detection hook: marks toolCallIds from cron sessions
|
|
70
|
+
registerCronDetectionHook(api);
|
|
48
71
|
}
|
|
49
72
|
},
|
|
50
73
|
});
|
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 } 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";
|
|
@@ -22,6 +22,9 @@ import { logger } from "./utils/logger.js";
|
|
|
22
22
|
*/
|
|
23
23
|
export async function handleXYMessage(params) {
|
|
24
24
|
const { cfg, runtime, message, accountId, webSocketSessionId } = params;
|
|
25
|
+
const distributionSessionId = typeof message?.sessionId === "string" && message.sessionId.length > 0
|
|
26
|
+
? message.sessionId
|
|
27
|
+
: undefined;
|
|
25
28
|
// Cache context for CSPL steer injection (after_tool_call hook)
|
|
26
29
|
setCsplSteerContext(cfg, runtime);
|
|
27
30
|
// Get runtime (already validated in monitor.ts, but get reference for use)
|
|
@@ -29,9 +32,10 @@ export async function handleXYMessage(params) {
|
|
|
29
32
|
try {
|
|
30
33
|
// Check for special messages BEFORE parsing (these have different param structures)
|
|
31
34
|
const messageMethod = message.method;
|
|
32
|
-
|
|
35
|
+
logger.log(`[BOT] Received A2A message: ${JSON.stringify(message)}`);
|
|
36
|
+
// Handle clearContext messages (sessionId at top level, no params)
|
|
33
37
|
if (messageMethod === "clearContext" || messageMethod === "clear_context") {
|
|
34
|
-
const sessionId = message.params?.sessionId;
|
|
38
|
+
const sessionId = message.sessionId ?? message.params?.sessionId;
|
|
35
39
|
if (!sessionId) {
|
|
36
40
|
throw new Error("clearContext request missing sessionId in params");
|
|
37
41
|
}
|
|
@@ -45,9 +49,9 @@ export async function handleXYMessage(params) {
|
|
|
45
49
|
});
|
|
46
50
|
return;
|
|
47
51
|
}
|
|
48
|
-
// Handle tasks/cancel messages
|
|
52
|
+
// Handle tasks/cancel messages (sessionId at top level, no params)
|
|
49
53
|
if (messageMethod === "tasks/cancel" || messageMethod === "tasks_cancel") {
|
|
50
|
-
const sessionId = message.params?.sessionId;
|
|
54
|
+
const sessionId = message.sessionId ?? message.params?.sessionId;
|
|
51
55
|
const taskId = message.params?.id || message.id;
|
|
52
56
|
if (!sessionId) {
|
|
53
57
|
throw new Error("tasks/cancel request missing sessionId in params");
|
|
@@ -135,6 +139,12 @@ export async function handleXYMessage(params) {
|
|
|
135
139
|
if (deviceType) {
|
|
136
140
|
log.log(`[BOT] Extracted deviceType: ${deviceType}`);
|
|
137
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
|
+
}
|
|
147
|
+
const runCrossTaskContext = extractRunCrossTaskContext(parsed.parts);
|
|
138
148
|
// Resolve configuration (needed for status updates)
|
|
139
149
|
const config = resolveXYConfig(cfg);
|
|
140
150
|
// ✅ Resolve agent route (following feishu pattern)
|
|
@@ -155,10 +165,13 @@ export async function handleXYMessage(params) {
|
|
|
155
165
|
registerSession(route.sessionKey, {
|
|
156
166
|
config,
|
|
157
167
|
sessionId: parsed.sessionId,
|
|
168
|
+
distributionSessionId,
|
|
158
169
|
taskId: parsed.taskId,
|
|
159
170
|
messageId: parsed.messageId,
|
|
160
171
|
agentId: route.accountId,
|
|
161
172
|
deviceType,
|
|
173
|
+
modelName,
|
|
174
|
+
runCrossTaskContext: runCrossTaskContext ?? undefined,
|
|
162
175
|
});
|
|
163
176
|
// 🔑 发送初始状态更新
|
|
164
177
|
log.log(`[BOT] Sending initial status update`);
|
|
@@ -300,10 +313,13 @@ export async function handleXYMessage(params) {
|
|
|
300
313
|
const sessionContext = {
|
|
301
314
|
config,
|
|
302
315
|
sessionId: parsed.sessionId,
|
|
316
|
+
distributionSessionId,
|
|
303
317
|
taskId: parsed.taskId,
|
|
304
318
|
messageId: parsed.messageId,
|
|
305
319
|
agentId: route.accountId,
|
|
306
320
|
deviceType,
|
|
321
|
+
modelName,
|
|
322
|
+
runCrossTaskContext: runCrossTaskContext ?? undefined,
|
|
307
323
|
};
|
|
308
324
|
log.log(`[BOT-DISPATCH] withReplyDispatcher starting, sessionKey=${route.sessionKey}`);
|
|
309
325
|
await core.channel.reply.withReplyDispatcher({
|
package/dist/src/channel.js
CHANGED
|
@@ -2,9 +2,15 @@ import { resolveXYConfig, listXYAccountIds, getDefaultXYAccountId } from "./conf
|
|
|
2
2
|
import { xyConfigSchema } from "./config-schema.js";
|
|
3
3
|
import { xyOutbound } from "./outbound.js";
|
|
4
4
|
import { filterToolsByDevice } from "./tools/device-tool-map.js";
|
|
5
|
-
import { getCurrentSessionContext } from "./tools/session-manager.js";
|
|
5
|
+
import { getCurrentSessionContext, registerSession } from "./tools/session-manager.js";
|
|
6
6
|
import { createAllTools } from "./tools/create-all-tools.js";
|
|
7
7
|
import { logger } from "./utils/logger.js";
|
|
8
|
+
/**
|
|
9
|
+
* Prefix used for synthetic sessionIds created during cron-triggered tool
|
|
10
|
+
* execution. `sendCommand()` checks this prefix to route commands through
|
|
11
|
+
* the push channel instead of the (non-existent) WebSocket session.
|
|
12
|
+
*/
|
|
13
|
+
const CRON_SESSION_PREFIX = "cron-";
|
|
8
14
|
/**
|
|
9
15
|
* Xiaoyi Channel Plugin for OpenClaw.
|
|
10
16
|
* Implements Xiaoyi A2A protocol with dual WebSocket connections.
|
|
@@ -43,11 +49,59 @@ export const xyPlugin = {
|
|
|
43
49
|
schema: xyConfigSchema,
|
|
44
50
|
},
|
|
45
51
|
outbound: xyOutbound,
|
|
46
|
-
|
|
47
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Provide channel-specific agent tools.
|
|
54
|
+
*
|
|
55
|
+
* Two execution contexts are supported:
|
|
56
|
+
*
|
|
57
|
+
* 1. **Normal (WebSocket) session** – `getCurrentSessionContext()` returns
|
|
58
|
+
* a context that was registered by bot.ts during message processing.
|
|
59
|
+
* Tools send commands through the WebSocket and listen for responses.
|
|
60
|
+
*
|
|
61
|
+
* 2. **Cron / scheduled-task session** – openclaw's cron runner calls
|
|
62
|
+
* `agentTools({ cfg })` without an active WebSocket session. When no
|
|
63
|
+
* session context exists but `cfg` is provided, we create a synthetic
|
|
64
|
+
* "cron session" with `isCron: true` and a `cron-`-prefixed sessionId.
|
|
65
|
+
* `sendCommand()` detects this prefix and routes commands through the
|
|
66
|
+
* push channel. Response listening (WebSocket events) works unchanged
|
|
67
|
+
* because the gateway WebSocket connection is always active.
|
|
68
|
+
*/
|
|
69
|
+
agentTools: (params) => {
|
|
70
|
+
let ctx = getCurrentSessionContext();
|
|
71
|
+
// ── Cron / non-session fallback ──────────────────────────────
|
|
72
|
+
// When no active xy WebSocket session exists but the openclaw cfg
|
|
73
|
+
// is provided (framework calls agentTools({ cfg })), create a
|
|
74
|
+
// synthetic "cron session". This enables cron-triggered agent
|
|
75
|
+
// turns and cross-channel tool calls to use xiaoyi tools via the
|
|
76
|
+
// push channel. sendCommand() detects the "cron-" sessionId
|
|
77
|
+
// prefix and routes commands through push instead of WebSocket.
|
|
78
|
+
if (!ctx && params?.cfg) {
|
|
79
|
+
try {
|
|
80
|
+
const config = resolveXYConfig(params.cfg);
|
|
81
|
+
const cronId = `${CRON_SESSION_PREFIX}${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
|
|
82
|
+
ctx = {
|
|
83
|
+
config,
|
|
84
|
+
sessionId: cronId,
|
|
85
|
+
taskId: cronId,
|
|
86
|
+
messageId: cronId,
|
|
87
|
+
agentId: "default",
|
|
88
|
+
isCron: true,
|
|
89
|
+
};
|
|
90
|
+
// Register so getCurrentSessionContext() fallback can find it
|
|
91
|
+
registerSession(`__cron__${cronId}`, ctx);
|
|
92
|
+
logger.log(`[CRON-TOOLS] Created cron session context: ${cronId}`);
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
logger.error("[CRON-TOOLS] Failed to create cron context:", err);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (!ctx) {
|
|
99
|
+
logger.log("[CREATE-ALL-TOOLS] no session context, returning empty tools list");
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
48
102
|
const allTools = createAllTools(ctx);
|
|
49
|
-
const filtered = filterToolsByDevice(allTools, ctx
|
|
50
|
-
logger.log(`[DEVICE-FILTER] deviceType=${ctx
|
|
103
|
+
const filtered = filterToolsByDevice(allTools, ctx.deviceType);
|
|
104
|
+
logger.log(`[DEVICE-FILTER] deviceType=${ctx.deviceType ?? "(none)"}, tools: ${allTools.length} → ${filtered.length} (${filtered.map(t => t.name).join(", ")})`);
|
|
51
105
|
return filtered;
|
|
52
106
|
},
|
|
53
107
|
messaging: {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { XYChannelConfig, A2ACommand } from "./types.js";
|
|
2
|
+
export interface SendCommandViaPushParams {
|
|
3
|
+
config: XYChannelConfig;
|
|
4
|
+
command: A2ACommand;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Send a tool command through the push channel (for cron-triggered tool calls).
|
|
8
|
+
*
|
|
9
|
+
* Flow:
|
|
10
|
+
* 1. Push notification is sent with command embedded in data.directives
|
|
11
|
+
* 2. Device receives push → extracts directives → executes command
|
|
12
|
+
* 3. Device returns result via WebSocket (data-event / gui-agent-response / …)
|
|
13
|
+
* 4. The calling tool listens on the WebSocket manager as usual
|
|
14
|
+
*/
|
|
15
|
+
export declare function sendCommandViaPush(params: SendCommandViaPushParams): Promise<void>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Cron-triggered tool command delivery via push channel.
|
|
2
|
+
// When a cron/scheduled task executes a tool, there is no active WebSocket
|
|
3
|
+
// session to carry the command. Instead, the command is delivered through
|
|
4
|
+
// the push notification channel (agent-webhook), which reaches the device
|
|
5
|
+
// independently of any session. The device processes the command and returns
|
|
6
|
+
// results through the normal WebSocket connection, so response listening
|
|
7
|
+
// works the same as for regular tool calls.
|
|
8
|
+
import { randomUUID } from "crypto";
|
|
9
|
+
import { XYPushService } from "./push.js";
|
|
10
|
+
import { getAllPushIds } from "./utils/pushid-manager.js";
|
|
11
|
+
import { logger } from "./utils/logger.js";
|
|
12
|
+
/**
|
|
13
|
+
* Send a tool command through the push channel (for cron-triggered tool calls).
|
|
14
|
+
*
|
|
15
|
+
* Flow:
|
|
16
|
+
* 1. Push notification is sent with command embedded in data.directives
|
|
17
|
+
* 2. Device receives push → extracts directives → executes command
|
|
18
|
+
* 3. Device returns result via WebSocket (data-event / gui-agent-response / …)
|
|
19
|
+
* 4. The calling tool listens on the WebSocket manager as usual
|
|
20
|
+
*/
|
|
21
|
+
export async function sendCommandViaPush(params) {
|
|
22
|
+
const { config, command } = params;
|
|
23
|
+
const intentName = command.payload?.executeParam?.intentName ??
|
|
24
|
+
command.header?.name ??
|
|
25
|
+
"Command";
|
|
26
|
+
logger.log(`[CRON-CMD] Sending command via push, intent=${intentName}`);
|
|
27
|
+
// 1. Load push IDs, use first one
|
|
28
|
+
let pushId = config.pushId;
|
|
29
|
+
try {
|
|
30
|
+
const pushIdList = await getAllPushIds();
|
|
31
|
+
if (pushIdList.length > 0) {
|
|
32
|
+
pushId = pushIdList[0];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
logger.error("[CRON-CMD] Failed to load pushIds:", error);
|
|
37
|
+
}
|
|
38
|
+
// 2. Build and send push notification with command in directives
|
|
39
|
+
const pushService = new XYPushService(config);
|
|
40
|
+
const sessionId = randomUUID();
|
|
41
|
+
try {
|
|
42
|
+
await pushService.sendPushWithDirectives(pushId, sessionId, [command]);
|
|
43
|
+
logger.log(`[CRON-CMD] Push sent successfully, intent=${intentName}`);
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
logger.error(`[CRON-CMD] Failed to send push`, error);
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handle a cron-query-event.
|
|
3
|
+
*
|
|
4
|
+
* Calls the Gateway cron RPC and sends the result back through sendCommand
|
|
5
|
+
* as a System.CronQuery command with the full result object in payload.ans.
|
|
6
|
+
*/
|
|
7
|
+
export declare function handleCronQueryEvent(context: any, cfg: any): Promise<void>;
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// Cron query event handler.
|
|
2
|
+
// Listens for cron-query-event from the WebSocket manager,
|
|
3
|
+
// calls Gateway cron RPC via callGatewayTool, and sends the
|
|
4
|
+
// result back to the client via sendCommand as a System.CronQuery
|
|
5
|
+
// command with the result in payload.ans.
|
|
6
|
+
import { callGatewayTool } from "openclaw/plugin-sdk/agent-harness-runtime";
|
|
7
|
+
import * as os from "os";
|
|
8
|
+
import { sendCommand } from "./formatter.js";
|
|
9
|
+
import { resolveXYConfig } from "./config.js";
|
|
10
|
+
import { logger } from "./utils/logger.js";
|
|
11
|
+
import { readFileSync, readdirSync } from "fs";
|
|
12
|
+
import { join } from "path";
|
|
13
|
+
const GATEWAY_TIMEOUT_MS = 60_000;
|
|
14
|
+
/**
|
|
15
|
+
* Handle a cron-query-event.
|
|
16
|
+
*
|
|
17
|
+
* Calls the Gateway cron RPC and sends the result back through sendCommand
|
|
18
|
+
* as a System.CronQuery command with the full result object in payload.ans.
|
|
19
|
+
*/
|
|
20
|
+
export async function handleCronQueryEvent(context, cfg) {
|
|
21
|
+
const { action, jobId, params, sessionId, taskId, messageId } = context;
|
|
22
|
+
logger.log(`[CRON-QUERY] Received event: action=${action}, jobId=${jobId ?? "(none)"}`);
|
|
23
|
+
let result;
|
|
24
|
+
let error;
|
|
25
|
+
try {
|
|
26
|
+
switch (action) {
|
|
27
|
+
case "list":
|
|
28
|
+
result = await callGatewayTool("cron.list", { timeoutMs: GATEWAY_TIMEOUT_MS }, params ?? {});
|
|
29
|
+
break;
|
|
30
|
+
case "status":
|
|
31
|
+
result = await callGatewayTool("cron.status", { timeoutMs: GATEWAY_TIMEOUT_MS }, {});
|
|
32
|
+
break;
|
|
33
|
+
case "runs":
|
|
34
|
+
result = await callGatewayTool("cron.runs", { timeoutMs: GATEWAY_TIMEOUT_MS }, {
|
|
35
|
+
jobId,
|
|
36
|
+
...params,
|
|
37
|
+
});
|
|
38
|
+
break;
|
|
39
|
+
case "add":
|
|
40
|
+
result = await callGatewayTool("cron.add", { timeoutMs: GATEWAY_TIMEOUT_MS }, params ?? {});
|
|
41
|
+
break;
|
|
42
|
+
case "update":
|
|
43
|
+
result = await callGatewayTool("cron.update", { timeoutMs: GATEWAY_TIMEOUT_MS }, {
|
|
44
|
+
jobId,
|
|
45
|
+
...params,
|
|
46
|
+
});
|
|
47
|
+
break;
|
|
48
|
+
case "remove":
|
|
49
|
+
result = await callGatewayTool("cron.remove", { timeoutMs: GATEWAY_TIMEOUT_MS }, {
|
|
50
|
+
jobId,
|
|
51
|
+
});
|
|
52
|
+
break;
|
|
53
|
+
case "run":
|
|
54
|
+
result = await callGatewayTool("cron.run", { timeoutMs: GATEWAY_TIMEOUT_MS }, {
|
|
55
|
+
jobId,
|
|
56
|
+
mode: "force",
|
|
57
|
+
...params,
|
|
58
|
+
});
|
|
59
|
+
break;
|
|
60
|
+
case "queryTimeList":
|
|
61
|
+
result = await queryTimeListLocal();
|
|
62
|
+
break;
|
|
63
|
+
default:
|
|
64
|
+
error = `Unknown action: ${context.action}`;
|
|
65
|
+
logger.error(`[CRON-QUERY] ${error}`);
|
|
66
|
+
result = { error };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
error = err instanceof Error ? err.message : String(err);
|
|
71
|
+
logger.error(`[CRON-QUERY] RPC call failed for action=${action}:`, err);
|
|
72
|
+
result = { error };
|
|
73
|
+
}
|
|
74
|
+
// Log the result
|
|
75
|
+
logger.log(`[CRON-QUERY] RPC result for action=${action}: ${JSON.stringify(result, null, 2)}`);
|
|
76
|
+
// Send result back via sendCommand as System.CronQuery with payload.ans
|
|
77
|
+
if (cfg && sessionId && taskId && messageId) {
|
|
78
|
+
try {
|
|
79
|
+
const config = resolveXYConfig(cfg);
|
|
80
|
+
const command = {
|
|
81
|
+
header: {
|
|
82
|
+
namespace: "AgentEvent",
|
|
83
|
+
name: "CronQuery",
|
|
84
|
+
},
|
|
85
|
+
payload: {
|
|
86
|
+
action,
|
|
87
|
+
ans: result,
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
await sendCommand({
|
|
91
|
+
config,
|
|
92
|
+
sessionId,
|
|
93
|
+
taskId,
|
|
94
|
+
messageId,
|
|
95
|
+
command,
|
|
96
|
+
final: true,
|
|
97
|
+
});
|
|
98
|
+
logger.log(`[CRON-QUERY] Sent response via sendCommand, action=${action}`);
|
|
99
|
+
}
|
|
100
|
+
catch (sendErr) {
|
|
101
|
+
logger.error(`[CRON-QUERY] Failed to send response via sendCommand:`, sendErr);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
logger.warn(`[CRON-QUERY] Missing cfg/sessionId/taskId/messageId, skipping sendCommand`);
|
|
106
|
+
}
|
|
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) {
|