@ynhcj/xiaoyi-channel 0.0.163-beta → 0.0.163-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 +87 -1
- package/dist/src/bot.js +60 -5
- package/dist/src/cron-command.d.ts +2 -0
- package/dist/src/cron-command.js +14 -8
- package/dist/src/cron-query-handler.d.ts +1 -11
- package/dist/src/cron-query-handler.js +132 -8
- package/dist/src/cspl/call_api.d.ts +1 -1
- package/dist/src/cspl/call_api.js +4 -4
- package/dist/src/cspl/config.js +30 -10
- package/dist/src/cspl/constants.d.ts +2 -0
- package/dist/src/cspl/constants.js +3 -0
- package/dist/src/cspl/sentinel_hook.js +11 -6
- package/dist/src/cspl/upload_file.js +2 -2
- 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 +108 -7
- package/dist/src/monitor.js +35 -23
- package/dist/src/parser.d.ts +6 -0
- package/dist/src/parser.js +23 -13
- package/dist/src/provider.js +41 -1
- package/dist/src/reply-dispatcher.js +70 -26
- package/dist/src/self-evolution-handler.d.ts +1 -1
- package/dist/src/self-evolution-handler.js +12 -1
- package/dist/src/tools/calendar-tool.js +1 -1
- 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 -2
- package/dist/src/tools/delete-alarm-tool.js +1 -1
- package/dist/src/tools/device-tool-map.d.ts +1 -1
- package/dist/src/tools/device-tool-map.js +9 -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/location-tool.js +1 -1
- package/dist/src/tools/modify-alarm-tool.js +18 -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/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-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 +84 -15
- package/dist/src/tools/send-email-tool.js +1 -1
- package/dist/src/tools/send-file-to-user-tool.js +9 -11
- package/dist/src/tools/send-html-card-tool.d.ts +7 -0
- package/dist/src/tools/send-html-card-tool.js +113 -0
- package/dist/src/tools/session-manager.d.ts +12 -2
- package/dist/src/tools/session-manager.js +78 -18
- package/dist/src/tools/upload-file-tool.js +1 -1
- package/dist/src/tools/upload-photo-tool.js +1 -1
- package/dist/src/tools/xiaoyi-add-collection-tool.js +1 -1
- package/dist/src/tools/xiaoyi-collection-tool.js +1 -1
- package/dist/src/tools/xiaoyi-delete-collection-tool.js +1 -1
- package/dist/src/tools/xiaoyi-gui-tool.js +1 -1
- package/dist/src/types.d.ts +9 -7
- package/dist/src/utils/cron-push-map.d.ts +26 -0
- package/dist/src/utils/cron-push-map.js +131 -0
- package/dist/src/websocket.js +11 -13
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3,7 +3,9 @@ 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
|
+
import { markCronToolCall, clearCronToolCall, getSessionContext } from "./src/tools/session-manager.js";
|
|
7
|
+
import { configManager } from "./src/utils/config-manager.js";
|
|
8
|
+
import { setJobPushId } from "./src/utils/cron-push-map.js";
|
|
7
9
|
import { registerSelfEvolutionToolResultNudge } from "./src/self-evolution-tool-result-nudge.js";
|
|
8
10
|
import { createBeforePromptBuildHandler } from "./src/skill-retriever/hooks.js";
|
|
9
11
|
import { normalizeToolRetrieverConfig } from "./src/skill-retriever/config.js";
|
|
@@ -25,8 +27,92 @@ function registerCronDetectionHook(api) {
|
|
|
25
27
|
if (event.toolCallId) {
|
|
26
28
|
clearCronToolCall(event.toolCallId);
|
|
27
29
|
}
|
|
30
|
+
// 捕获对话创建的 cron job:agent 调 cron(add) 后,从 result 拿 jobId,
|
|
31
|
+
// 配合当前会话的 pushId,写入 jobId↔pushId 映射,供 fire 时反查设备。
|
|
32
|
+
await captureCronAddMapping(event, ctx).catch((err) => {
|
|
33
|
+
// 捕获失败不影响工具结果
|
|
34
|
+
console.error("[xy] captureCronAddMapping failed:", err);
|
|
35
|
+
});
|
|
28
36
|
});
|
|
29
37
|
}
|
|
38
|
+
/** 从 cron add 工具结果中提取 jobId 并写入 pushId 映射。 */
|
|
39
|
+
async function captureCronAddMapping(event, ctx) {
|
|
40
|
+
// 诊断:先看 after_tool_call 是否为 cron 工具触发
|
|
41
|
+
if (event.toolName !== "cron")
|
|
42
|
+
return;
|
|
43
|
+
const action = typeof event.params?.action === "string" ? event.params.action : "";
|
|
44
|
+
console.log(`[CRONMAP] after_tool_call cron, action=${action}, resultType=${typeof event.result}`);
|
|
45
|
+
if (action !== "add") {
|
|
46
|
+
console.log(`[CRONMAP] skip: action !== "add" (got ${action})`);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const jobId = readJobIdFromResult(event.result);
|
|
50
|
+
if (!jobId) {
|
|
51
|
+
console.log(`[CRONMAP] skip: could not extract jobId from result. preview=${preview(event.result)}`);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
console.log(`[CRONMAP] extracted jobId=${jobId}`);
|
|
55
|
+
const sessionCtx = ctx.sessionKey ? getSessionContext(ctx.sessionKey) : null;
|
|
56
|
+
const sessionId = sessionCtx?.sessionId;
|
|
57
|
+
if (!sessionId) {
|
|
58
|
+
console.log(`[CRONMAP] skip: no sessionId (sessionKey=${ctx.sessionKey}, ctxFound=${!!sessionCtx})`);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const pushId = configManager.getPushId(sessionId);
|
|
62
|
+
if (!pushId) {
|
|
63
|
+
console.log(`[CRONMAP] skip: configManager has no pushId for sessionId=${sessionId}`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
console.log(`[CRONMAP] writing map: jobId=${jobId}, sessionId=${sessionId}, pushId=${pushId.substring(0, 16)}...`);
|
|
67
|
+
await setJobPushId(jobId, {
|
|
68
|
+
pushId,
|
|
69
|
+
sessionId,
|
|
70
|
+
deviceType: sessionCtx?.deviceType,
|
|
71
|
+
source: "conversation",
|
|
72
|
+
});
|
|
73
|
+
console.log(`[CRONMAP] map written OK`);
|
|
74
|
+
}
|
|
75
|
+
/** 取结果的短预览,用于诊断。 */
|
|
76
|
+
function preview(value) {
|
|
77
|
+
if (value == null)
|
|
78
|
+
return String(value);
|
|
79
|
+
const s = typeof value === "string" ? value : JSON.stringify(value);
|
|
80
|
+
return s.length > 200 ? s.slice(0, 200) + "…" : s;
|
|
81
|
+
}
|
|
82
|
+
/** 防御性地从 cron add 结果中取 job id(可能是对象、JSON 字符串或工具输出文本)。 */
|
|
83
|
+
function readJobIdFromResult(result) {
|
|
84
|
+
if (!result)
|
|
85
|
+
return undefined;
|
|
86
|
+
let obj = result;
|
|
87
|
+
if (typeof result === "string") {
|
|
88
|
+
// 优先尝试 JSON 解析
|
|
89
|
+
try {
|
|
90
|
+
obj = JSON.parse(result);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// 解析失败:可能是纯文本工具输出,尝试从文本里抓 "id":"..." 或 id=...
|
|
94
|
+
const m = result.match(/"id"\s*:\s*"([^"]+)"/);
|
|
95
|
+
if (m)
|
|
96
|
+
return m[1];
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (obj && typeof obj === "object") {
|
|
101
|
+
const id = obj.id;
|
|
102
|
+
if (typeof id === "string" && id.trim())
|
|
103
|
+
return id.trim();
|
|
104
|
+
// 兜底:result 可能把 job 包在 data/result 字段里
|
|
105
|
+
for (const k of ["data", "result", "job"]) {
|
|
106
|
+
const inner = obj[k];
|
|
107
|
+
if (inner && typeof inner === "object") {
|
|
108
|
+
const innerId = inner.id;
|
|
109
|
+
if (typeof innerId === "string" && innerId.trim())
|
|
110
|
+
return innerId.trim();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
30
116
|
function registerFullHooks(api) {
|
|
31
117
|
// SKILL RETRIEVER HOOK: before_prompt_build hook
|
|
32
118
|
const pluginConfig = api.pluginConfig || {};
|
package/dist/src/bot.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { updateSessionStoreEntry, updateSessionStore, resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
|
|
1
2
|
import { getXYRuntime } from "./runtime.js";
|
|
2
3
|
import { createXYReplyDispatcher } from "./reply-dispatcher.js";
|
|
3
|
-
import { parseA2AMessage, extractTextFromParts, extractFileParts, extractPushId, extractDeviceType, extractTriggerData, extractRunCrossTaskContext } from "./parser.js";
|
|
4
|
+
import { parseA2AMessage, extractTextFromParts, extractFileParts, extractPushId, extractDeviceType, extractModelName, extractTriggerData, extractRunCrossTaskContext } from "./parser.js";
|
|
4
5
|
import { downloadFilesFromParts } from "./file-download.js";
|
|
5
6
|
import { resolveXYConfig } from "./config.js";
|
|
6
7
|
import { sendStatusUpdate, sendClearContextResponse, sendTasksCancelResponse, sendA2AResponse } from "./formatter.js";
|
|
@@ -32,9 +33,9 @@ export async function handleXYMessage(params) {
|
|
|
32
33
|
try {
|
|
33
34
|
// Check for special messages BEFORE parsing (these have different param structures)
|
|
34
35
|
const messageMethod = message.method;
|
|
35
|
-
// Handle clearContext messages (
|
|
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,8 +170,56 @@ 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
|
});
|
|
176
|
+
// 🔑 Sync A2A modelName to OpenClaw session store so that session_status
|
|
177
|
+
// reports the correct model. Without this, session_status returns the
|
|
178
|
+
// configured default model instead of the A2A-specified one.
|
|
179
|
+
if (modelName && modelName.trim() !== "" && modelName.toLowerCase() !== "none") {
|
|
180
|
+
try {
|
|
181
|
+
const storePath = resolveStorePath();
|
|
182
|
+
const result = await updateSessionStoreEntry({
|
|
183
|
+
storePath,
|
|
184
|
+
sessionKey: route.sessionKey,
|
|
185
|
+
update: async () => ({
|
|
186
|
+
providerOverride: "xiaoyiprovider",
|
|
187
|
+
modelOverride: modelName,
|
|
188
|
+
modelOverrideSource: "user",
|
|
189
|
+
model: "",
|
|
190
|
+
modelProvider: "",
|
|
191
|
+
contextTokens: 256_000,
|
|
192
|
+
}),
|
|
193
|
+
});
|
|
194
|
+
if (!result) {
|
|
195
|
+
// Session entry doesn't exist yet (first message, xy_channel
|
|
196
|
+
// bypasses the standard turn kernel). Create a minimal entry
|
|
197
|
+
// with the override via updateSessionStore.
|
|
198
|
+
await updateSessionStore(storePath, (store) => {
|
|
199
|
+
if (!store[route.sessionKey]) {
|
|
200
|
+
store[route.sessionKey] = {
|
|
201
|
+
// sessionId must pass validateSessionId regex /^[a-z0-9][a-z0-9._-]{0,127}$/i
|
|
202
|
+
// route.sessionKey like "agent:main:direct:xxx" contains colons which are invalid.
|
|
203
|
+
// Use parsed.sessionId (raw UUID from A2A) which is always safe.
|
|
204
|
+
sessionId: parsed.sessionId,
|
|
205
|
+
updatedAt: Date.now(),
|
|
206
|
+
providerOverride: "xiaoyiprovider",
|
|
207
|
+
modelOverride: modelName,
|
|
208
|
+
modelOverrideSource: "user",
|
|
209
|
+
contextTokens: 256_000,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
log.log(`[BOT] Created session entry with model override: xiaoyiprovider/${modelName}`);
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
log.log(`[BOT] Patched session store model override: xiaoyiprovider/${modelName}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
catch (patchErr) {
|
|
220
|
+
log.error(`[BOT] Failed to patch session model override:`, patchErr);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
169
223
|
// 🔑 发送初始状态更新
|
|
170
224
|
log.log(`[BOT] Sending initial status update`);
|
|
171
225
|
void sendStatusUpdate({
|
|
@@ -311,6 +365,7 @@ export async function handleXYMessage(params) {
|
|
|
311
365
|
messageId: parsed.messageId,
|
|
312
366
|
agentId: route.accountId,
|
|
313
367
|
deviceType,
|
|
368
|
+
modelName,
|
|
314
369
|
runCrossTaskContext: runCrossTaskContext ?? undefined,
|
|
315
370
|
};
|
|
316
371
|
log.log(`[BOT-DISPATCH] withReplyDispatcher starting, sessionKey=${route.sessionKey}`);
|
|
@@ -2,6 +2,8 @@ import type { XYChannelConfig, A2ACommand } from "./types.js";
|
|
|
2
2
|
export interface SendCommandViaPushParams {
|
|
3
3
|
config: XYChannelConfig;
|
|
4
4
|
command: A2ACommand;
|
|
5
|
+
/** 指定设备的 pushId(多设备路由)。未传时回退到 getAllPushIds()[0]。 */
|
|
6
|
+
pushId?: string;
|
|
5
7
|
}
|
|
6
8
|
/**
|
|
7
9
|
* Send a tool command through the push channel (for cron-triggered tool calls).
|
package/dist/src/cron-command.js
CHANGED
|
@@ -24,16 +24,22 @@ export async function sendCommandViaPush(params) {
|
|
|
24
24
|
command.header?.name ??
|
|
25
25
|
"Command";
|
|
26
26
|
logger.log(`[CRON-CMD] Sending command via push, intent=${intentName}`);
|
|
27
|
-
// 1.
|
|
27
|
+
// 1. 选 pushId:优先用调用方解析出的设备 pushId(多设备路由正确);
|
|
28
|
+
// 未提供时回退到 getAllPushIds()[0](单设备兼容旧行为)。
|
|
28
29
|
let pushId = config.pushId;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (pushIdList.length > 0) {
|
|
32
|
-
pushId = pushIdList[0];
|
|
33
|
-
}
|
|
30
|
+
if (params.pushId) {
|
|
31
|
+
pushId = params.pushId;
|
|
34
32
|
}
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
else {
|
|
34
|
+
try {
|
|
35
|
+
const pushIdList = await getAllPushIds();
|
|
36
|
+
if (pushIdList.length > 0) {
|
|
37
|
+
pushId = pushIdList[0];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
logger.error("[CRON-CMD] Failed to load pushIds:", error);
|
|
42
|
+
}
|
|
37
43
|
}
|
|
38
44
|
// 2. Build and send push notification with command in directives
|
|
39
45
|
const pushService = new XYPushService(config);
|
|
@@ -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,14 @@
|
|
|
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";
|
|
10
|
+
import { configManager } from "./utils/config-manager.js";
|
|
11
|
+
import { setJobPushId } from "./utils/cron-push-map.js";
|
|
9
12
|
import { logger } from "./utils/logger.js";
|
|
13
|
+
import { readFileSync, readdirSync } from "fs";
|
|
14
|
+
import { join } from "path";
|
|
10
15
|
const GATEWAY_TIMEOUT_MS = 60_000;
|
|
11
16
|
/**
|
|
12
17
|
* Handle a cron-query-event.
|
|
@@ -16,7 +21,8 @@ const GATEWAY_TIMEOUT_MS = 60_000;
|
|
|
16
21
|
*/
|
|
17
22
|
export async function handleCronQueryEvent(context, cfg) {
|
|
18
23
|
const { action, jobId, params, sessionId, taskId, messageId } = context;
|
|
19
|
-
logger.
|
|
24
|
+
const log = logger.withContext(sessionId ?? "", taskId ?? "");
|
|
25
|
+
log.log(`[CRON-QUERY] Received event: action=${action}, jobId=${jobId ?? "(none)"}`);
|
|
20
26
|
let result;
|
|
21
27
|
let error;
|
|
22
28
|
try {
|
|
@@ -35,6 +41,11 @@ export async function handleCronQueryEvent(context, cfg) {
|
|
|
35
41
|
break;
|
|
36
42
|
case "add":
|
|
37
43
|
result = await callGatewayTool("cron.add", { timeoutMs: GATEWAY_TIMEOUT_MS }, params ?? {});
|
|
44
|
+
// 捕获 jobId↔pushId:cron-query 路径由 channel 自己建 job,
|
|
45
|
+
// 此处 context 握着 sessionId,configManager 有对应设备 pushId。
|
|
46
|
+
await persistCronPushMap(context.sessionId, result).catch((err) => {
|
|
47
|
+
logger.error(`[CRON-QUERY] Failed to persist cron-push-map:`, err);
|
|
48
|
+
});
|
|
38
49
|
break;
|
|
39
50
|
case "update":
|
|
40
51
|
result = await callGatewayTool("cron.update", { timeoutMs: GATEWAY_TIMEOUT_MS }, {
|
|
@@ -54,19 +65,22 @@ export async function handleCronQueryEvent(context, cfg) {
|
|
|
54
65
|
...params,
|
|
55
66
|
});
|
|
56
67
|
break;
|
|
68
|
+
case "queryTimeList":
|
|
69
|
+
result = await queryTimeListLocal();
|
|
70
|
+
break;
|
|
57
71
|
default:
|
|
58
72
|
error = `Unknown action: ${context.action}`;
|
|
59
|
-
|
|
73
|
+
log.error(`[CRON-QUERY] ${error}`);
|
|
60
74
|
result = { error };
|
|
61
75
|
}
|
|
62
76
|
}
|
|
63
77
|
catch (err) {
|
|
64
78
|
error = err instanceof Error ? err.message : String(err);
|
|
65
|
-
|
|
79
|
+
log.error(`[CRON-QUERY] RPC call failed for action=${action}:`, err);
|
|
66
80
|
result = { error };
|
|
67
81
|
}
|
|
68
82
|
// Log the result
|
|
69
|
-
|
|
83
|
+
log.log(`[CRON-QUERY] RPC result for action=${action}: ${JSON.stringify(result, null, 2)}`);
|
|
70
84
|
// Send result back via sendCommand as System.CronQuery with payload.ans
|
|
71
85
|
if (cfg && sessionId && taskId && messageId) {
|
|
72
86
|
try {
|
|
@@ -87,15 +101,125 @@ export async function handleCronQueryEvent(context, cfg) {
|
|
|
87
101
|
taskId,
|
|
88
102
|
messageId,
|
|
89
103
|
command,
|
|
90
|
-
final:
|
|
104
|
+
final: sessionId.toLowerCase().endsWith("cronquery"),
|
|
91
105
|
});
|
|
92
|
-
|
|
106
|
+
log.log(`[CRON-QUERY] Sent response via sendCommand, action=${action}`);
|
|
93
107
|
}
|
|
94
108
|
catch (sendErr) {
|
|
95
|
-
|
|
109
|
+
log.error(`[CRON-QUERY] Failed to send response via sendCommand:`, sendErr);
|
|
96
110
|
}
|
|
97
111
|
}
|
|
98
112
|
else {
|
|
99
|
-
|
|
113
|
+
log.warn(`[CRON-QUERY] Missing cfg/sessionId/taskId/messageId, skipping sendCommand`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* 从 cron.add 结果中提取 jobId,配合 sessionId 对应的 pushId 写入映射。
|
|
118
|
+
*/
|
|
119
|
+
async function persistCronPushMap(sessionId, result) {
|
|
120
|
+
logger.log(`[CRONMAP] cron-query persist: sessionId=${sessionId ?? "(none)"}, resultType=${typeof result}`);
|
|
121
|
+
if (!sessionId) {
|
|
122
|
+
logger.log(`[CRONMAP] cron-query skip: no sessionId in context`);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
let jobId;
|
|
126
|
+
if (result && typeof result === "object") {
|
|
127
|
+
const id = result.id;
|
|
128
|
+
if (typeof id === "string" && id.trim())
|
|
129
|
+
jobId = id.trim();
|
|
130
|
+
}
|
|
131
|
+
if (!jobId) {
|
|
132
|
+
const preview = typeof result === "string" ? result.slice(0, 200) : JSON.stringify(result)?.slice(0, 200);
|
|
133
|
+
logger.log(`[CRONMAP] cron-query skip: no jobId in result. preview=${preview ?? "(empty)"}`);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const pushId = configManager.getPushId(sessionId);
|
|
137
|
+
if (!pushId) {
|
|
138
|
+
logger.log(`[CRONMAP] cron-query skip: configManager has no pushId for sessionId=${sessionId}`);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
logger.log(`[CRONMAP] cron-query writing map: jobId=${jobId}, pushId=${pushId.substring(0, 16)}...`);
|
|
142
|
+
await setJobPushId(jobId, { pushId, sessionId, source: "cron-query" });
|
|
143
|
+
logger.log(`[CRONMAP] cron-query map written OK`);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Read local cron folder directly (bypassing openclaw RPC) and return
|
|
147
|
+
* run records from the last 7 days, grouped by date and sorted by time.
|
|
148
|
+
*
|
|
149
|
+
* Data sources:
|
|
150
|
+
* - state/cron/jobs.json → job id → name mapping
|
|
151
|
+
* - state/cron/runs/*.jsonl → run records (one JSON per line)
|
|
152
|
+
*
|
|
153
|
+
* Return format:
|
|
154
|
+
* [ { "YYYY-MM-DD": [ { run record with .name }, ... ] }, ... ]
|
|
155
|
+
*/
|
|
156
|
+
async function queryTimeListLocal() {
|
|
157
|
+
const cronDir = join(os.homedir(), ".openclaw", "cron");
|
|
158
|
+
const jobsPath = join(cronDir, "jobs.json");
|
|
159
|
+
const runsDir = join(cronDir, "runs");
|
|
160
|
+
// 1. Build jobId → name map from jobs.json
|
|
161
|
+
const jobNameMap = {};
|
|
162
|
+
try {
|
|
163
|
+
const jobsRaw = readFileSync(jobsPath, "utf-8");
|
|
164
|
+
const jobsData = JSON.parse(jobsRaw);
|
|
165
|
+
for (const job of jobsData.jobs || []) {
|
|
166
|
+
jobNameMap[job.id] = job.name || job.id;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
logger.error(`[CRON-QUERY] Failed to read jobs.json: ${err.message}`);
|
|
171
|
+
}
|
|
172
|
+
// 2. Read all run files, collect runs within last 7 days
|
|
173
|
+
const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
|
|
174
|
+
const allRuns = [];
|
|
175
|
+
let files = [];
|
|
176
|
+
try {
|
|
177
|
+
files = readdirSync(runsDir);
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
files = [];
|
|
181
|
+
}
|
|
182
|
+
for (const file of files) {
|
|
183
|
+
if (!file.endsWith(".jsonl"))
|
|
184
|
+
continue;
|
|
185
|
+
try {
|
|
186
|
+
const content = readFileSync(join(runsDir, file), "utf-8");
|
|
187
|
+
const lines = content.trim().split("\n");
|
|
188
|
+
for (const line of lines) {
|
|
189
|
+
if (!line.trim())
|
|
190
|
+
continue;
|
|
191
|
+
try {
|
|
192
|
+
const run = JSON.parse(line);
|
|
193
|
+
if (run.ts && run.ts >= sevenDaysAgo) {
|
|
194
|
+
run.name = jobNameMap[run.jobId] || run.jobId || "";
|
|
195
|
+
allRuns.push(run);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
// skip malformed line
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
catch (err) {
|
|
204
|
+
logger.error(`[CRON-QUERY] Failed to read run file ${file}: ${err.message}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// 3. Sort by ts ascending
|
|
208
|
+
allRuns.sort((a, b) => a.ts - b.ts);
|
|
209
|
+
// 4. Group by date (YYYY-MM-DD in local time)
|
|
210
|
+
const grouped = new Map();
|
|
211
|
+
for (const run of allRuns) {
|
|
212
|
+
const d = new Date(run.ts);
|
|
213
|
+
const label = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
214
|
+
if (!grouped.has(label)) {
|
|
215
|
+
grouped.set(label, []);
|
|
216
|
+
}
|
|
217
|
+
grouped.get(label).push(run);
|
|
218
|
+
}
|
|
219
|
+
// 5. Convert to ordered array of single-key objects
|
|
220
|
+
const result = [];
|
|
221
|
+
for (const [date, runs] of grouped) {
|
|
222
|
+
result.push({ [date]: runs });
|
|
100
223
|
}
|
|
224
|
+
return result;
|
|
101
225
|
}
|
|
@@ -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>;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import https from 'https';
|
|
5
5
|
import { URL } from 'url';
|
|
6
6
|
import { getConfig } from './config.js';
|
|
7
|
-
import {
|
|
7
|
+
import { DEFAULT_HTTPS_PORT, HTTP_STATUS_BAD_REQUEST, API_URL_SUFFIX } from './constants.js';
|
|
8
8
|
function buildHeadersForCelia(config, sessionId) {
|
|
9
9
|
if (!config.uid || !config.apiKey || !config.skillId || !config.requestFrom) {
|
|
10
10
|
throw new Error('[SENTINEL HOOK] Missing required configuration: uid, apiKey, skillId, or requestFrom is not defined');
|
|
@@ -22,7 +22,7 @@ function buildRequestOptions(url, headers, timeout) {
|
|
|
22
22
|
const urlObj = new URL(url);
|
|
23
23
|
return {
|
|
24
24
|
hostname: urlObj.hostname,
|
|
25
|
-
port: urlObj.port ||
|
|
25
|
+
port: urlObj.port || DEFAULT_HTTPS_PORT,
|
|
26
26
|
path: urlObj.pathname,
|
|
27
27
|
method: "POST",
|
|
28
28
|
headers: headers,
|
|
@@ -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 {
|
|
@@ -43,6 +43,8 @@ export declare const TOOL_INPUT_DEFAULT: {
|
|
|
43
43
|
readonly source: "";
|
|
44
44
|
readonly content: "";
|
|
45
45
|
};
|
|
46
|
+
export declare const TOOL_INPUT_ACTION = "TOOL_INPUT_SCAN";
|
|
47
|
+
export declare const TOOL_OUTPUT_ACTION = "TOOL_OUTPUT_SCAN";
|
|
46
48
|
export declare const MAX_TIMES = 3;
|
|
47
49
|
export declare const CONNECT_TIMEOUT = 15000;
|
|
48
50
|
export declare const READ_TIMEOUT = 300000;
|
|
@@ -47,6 +47,9 @@ export const TOOL_INPUT_DEFAULT = {
|
|
|
47
47
|
source: '',
|
|
48
48
|
content: ''
|
|
49
49
|
};
|
|
50
|
+
// 安全扫描 action 常量
|
|
51
|
+
export const TOOL_INPUT_ACTION = 'TOOL_INPUT_SCAN';
|
|
52
|
+
export const TOOL_OUTPUT_ACTION = 'TOOL_OUTPUT_SCAN';
|
|
50
53
|
// OBS上传相关常量
|
|
51
54
|
export const MAX_TIMES = 3;
|
|
52
55
|
export const CONNECT_TIMEOUT = 15000;
|
|
@@ -4,7 +4,7 @@
|
|
|
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, STEER_ABORT_MESSAGE } 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
9
|
import { getSessionContext } from '../tools/session-manager.js';
|
|
10
10
|
import { tryInjectSteer } from './steer-context.js';
|
|
@@ -15,16 +15,21 @@ export default function register(api) {
|
|
|
15
15
|
// 生成sessionID
|
|
16
16
|
const sessionId = (event.runId?.replace(/-/g, '') || crypto.randomBytes(16).toString('hex'));
|
|
17
17
|
logger.log(`[SENTINEL HOOK] Generated Session ID: ${sessionId}`);
|
|
18
|
-
// 处理 TOOL_INPUT
|
|
18
|
+
// 处理 TOOL_INPUT 数据采集、发送数据,根据扫描结果决定是否阻塞
|
|
19
19
|
try {
|
|
20
|
+
let scanResult = null;
|
|
20
21
|
if (event.toolName === 'exec') {
|
|
21
|
-
await handleExecToolInput(event, api, sessionId);
|
|
22
|
+
scanResult = await handleExecToolInput(event, api, sessionId);
|
|
22
23
|
}
|
|
23
24
|
else if (event.toolName === 'message') {
|
|
24
|
-
await handleMessageToolInput(event, api, sessionId);
|
|
25
|
+
scanResult = await handleMessageToolInput(event, api, sessionId);
|
|
25
26
|
}
|
|
26
27
|
else {
|
|
27
|
-
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}` };
|
|
28
33
|
}
|
|
29
34
|
}
|
|
30
35
|
catch (error) {
|
|
@@ -68,7 +73,7 @@ export default function register(api) {
|
|
|
68
73
|
const postText = JSON.stringify(questionText);
|
|
69
74
|
logger.log(`[SENTINEL HOOK] Content extracted successfully. Length: ${postText.length}`);
|
|
70
75
|
try {
|
|
71
|
-
const response = await callApi(postText, api, sessionId);
|
|
76
|
+
const response = await callApi(postText, api, sessionId, TOOL_OUTPUT_ACTION);
|
|
72
77
|
const result = parseSecurityResult(response);
|
|
73
78
|
logger.log(`[SENTINEL HOOK] TOOL_OUTPUT response: status=${result.status}.`);
|
|
74
79
|
if (result.status === 'REJECT') {
|
|
@@ -6,7 +6,7 @@ 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 {
|
|
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';
|
|
10
10
|
function buildOsmsHeaders(config, traceId) {
|
|
11
11
|
return {
|
|
12
12
|
'content-type': 'application/json',
|
|
@@ -22,7 +22,7 @@ function httpRequest(url, method, headers, body, timeout) {
|
|
|
22
22
|
const urlObj = new URL(url);
|
|
23
23
|
const options = {
|
|
24
24
|
hostname: urlObj.hostname,
|
|
25
|
-
port: urlObj.port ||
|
|
25
|
+
port: urlObj.port || DEFAULT_HTTPS_PORT,
|
|
26
26
|
path: urlObj.pathname + urlObj.search,
|
|
27
27
|
method: method,
|
|
28
28
|
headers: headers,
|