@ynhcj/xiaoyi-channel 0.0.50-beta → 0.0.52-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +42 -0
- package/dist/src/bot.js +3 -16
- package/dist/src/client.js +0 -3
- package/dist/src/cspl/call-api.d.ts +3 -0
- package/dist/src/cspl/call-api.js +79 -0
- package/dist/src/cspl/config.d.ts +19 -0
- package/dist/src/cspl/config.js +50 -0
- package/dist/src/cspl/constants.d.ts +43 -0
- package/dist/src/cspl/constants.js +22 -0
- package/dist/src/cspl/utils.d.ts +10 -0
- package/dist/src/cspl/utils.js +57 -0
- package/dist/src/formatter.js +3 -23
- package/dist/src/heartbeat.js +0 -4
- package/dist/src/reply-dispatcher.js +32 -0
- package/dist/src/steer-injector.d.ts +16 -0
- package/dist/src/steer-injector.js +74 -0
- package/dist/src/tools/calendar-tool.js +2 -37
- package/dist/src/tools/call-phone-tool.js +1 -42
- package/dist/src/tools/create-alarm-tool.js +3 -74
- package/dist/src/tools/delete-alarm-tool.js +3 -45
- package/dist/src/tools/image-reading-tool.js +0 -47
- package/dist/src/tools/location-tool.js +1 -32
- package/dist/src/tools/modify-alarm-tool.js +3 -77
- package/dist/src/tools/modify-note-tool.js +1 -34
- package/dist/src/tools/note-tool.js +2 -4
- package/dist/src/tools/search-alarm-tool.js +3 -61
- package/dist/src/tools/search-calendar-tool.js +2 -39
- package/dist/src/tools/search-contact-tool.js +0 -30
- package/dist/src/tools/search-file-tool.js +0 -33
- package/dist/src/tools/search-message-tool.js +0 -33
- package/dist/src/tools/search-note-tool.js +1 -26
- package/dist/src/tools/search-photo-gallery-tool.js +2 -31
- package/dist/src/tools/send-file-to-user-tool.js +0 -39
- package/dist/src/tools/send-message-tool.js +1 -39
- package/dist/src/tools/session-manager.js +0 -45
- package/dist/src/tools/upload-file-tool.js +0 -49
- package/dist/src/tools/upload-photo-tool.js +0 -42
- package/dist/src/tools/view-push-result-tool.js +0 -11
- package/dist/src/tools/xiaoyi-collection-tool.js +4 -82
- package/dist/src/tools/xiaoyi-gui-tool.js +0 -34
- package/dist/src/websocket.js +24 -10
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
|
|
2
2
|
import { xyPlugin } from "./src/channel.js";
|
|
3
3
|
import { setXYRuntime } from "./src/runtime.js";
|
|
4
|
+
import { tryInjectSteer } from "./src/steer-injector.js";
|
|
5
|
+
import { callCsplApi } from "./src/cspl/call-api.js";
|
|
6
|
+
import { extractResultText, processText, parseSecurityResult, validateAndTruncateText } from "./src/cspl/utils.js";
|
|
7
|
+
import { ALLOWED_TOOLS, MIN_TEXT_LENGTH, MAX_TOTAL_LENGTH, MAX_TEXT_LENGTH, STEER_ABORT_MESSAGE, } from "./src/cspl/constants.js";
|
|
4
8
|
/**
|
|
5
9
|
* Xiaoyi Channel Plugin Entry Point.
|
|
6
10
|
* Exports the plugin for OpenClaw to load.
|
|
@@ -14,6 +18,44 @@ const plugin = {
|
|
|
14
18
|
register(api) {
|
|
15
19
|
setXYRuntime(api.runtime);
|
|
16
20
|
api.registerChannel({ plugin: xyPlugin });
|
|
21
|
+
// CSPL after_tool_call hook: 监听工具结果,发送至 CSPL API 进行安全检测
|
|
22
|
+
// 如果响应为 REJECT,注入 steer 消息中止当前对话
|
|
23
|
+
api.on("after_tool_call", async (event, ctx) => {
|
|
24
|
+
if (!ALLOWED_TOOLS.includes(event.toolName)) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
console.log(`[CSPL] after_tool_call triggered: toolName=${event.toolName}, sessionKey=${ctx.sessionKey ?? "none"}`);
|
|
28
|
+
try {
|
|
29
|
+
const resultText = extractResultText(event, event.toolName);
|
|
30
|
+
const resultLength = resultText.length;
|
|
31
|
+
if (resultLength <= MIN_TEXT_LENGTH || resultLength > MAX_TOTAL_LENGTH) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
// 构造 sentinel_hook 格式的 payload: { tool, output: [{ content }] }
|
|
35
|
+
const questionText = {
|
|
36
|
+
tool: event.toolName,
|
|
37
|
+
output: [{ content: "" }],
|
|
38
|
+
};
|
|
39
|
+
const originText = processText(resultText);
|
|
40
|
+
questionText.output[0].content = originText;
|
|
41
|
+
let finalJson = JSON.stringify(questionText);
|
|
42
|
+
if (finalJson.length > MAX_TEXT_LENGTH) {
|
|
43
|
+
const diff = finalJson.length - MAX_TEXT_LENGTH;
|
|
44
|
+
const { text: trimmed } = validateAndTruncateText(originText, MAX_TEXT_LENGTH - diff);
|
|
45
|
+
questionText.output[0].content = trimmed;
|
|
46
|
+
finalJson = JSON.stringify(questionText);
|
|
47
|
+
}
|
|
48
|
+
const response = await callCsplApi(finalJson, api.config);
|
|
49
|
+
const result = parseSecurityResult(response);
|
|
50
|
+
console.log(`[CSPL] Security result: status=${result.status}`);
|
|
51
|
+
if (result.status === "REJECT") {
|
|
52
|
+
await tryInjectSteer(ctx.sessionKey, STEER_ABORT_MESSAGE);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
api.logger.error(`[CSPL] after_tool_call error: ${err}`);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
17
59
|
},
|
|
18
60
|
};
|
|
19
61
|
export default plugin;
|
package/dist/src/bot.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getXYRuntime } from "./runtime.js";
|
|
2
|
+
import { setCachedContext } from "./steer-injector.js";
|
|
2
3
|
import { createXYReplyDispatcher } from "./reply-dispatcher.js";
|
|
3
4
|
import { parseA2AMessage, extractTextFromParts, extractFileParts, extractPushId, extractTriggerData } from "./parser.js";
|
|
4
5
|
import { downloadFilesFromParts } from "./file-download.js";
|
|
@@ -18,6 +19,8 @@ export async function handleXYMessage(params) {
|
|
|
18
19
|
const { cfg, runtime, message, accountId } = params;
|
|
19
20
|
const log = runtime?.log ?? console.log;
|
|
20
21
|
const error = runtime?.error ?? console.error;
|
|
22
|
+
// 每次收到消息时更新缓存,供 steer 注入使用
|
|
23
|
+
setCachedContext(cfg, runtime, accountId);
|
|
21
24
|
// Get runtime (already validated in monitor.ts, but get reference for use)
|
|
22
25
|
const core = getXYRuntime();
|
|
23
26
|
try {
|
|
@@ -333,19 +336,3 @@ function buildXYMediaPayload(mediaList) {
|
|
|
333
336
|
MediaTypes: mediaTypes.length > 0 ? mediaTypes : undefined,
|
|
334
337
|
};
|
|
335
338
|
}
|
|
336
|
-
/**
|
|
337
|
-
* Infer OpenClaw media type from file type string.
|
|
338
|
-
*/
|
|
339
|
-
function inferMediaType(fileType) {
|
|
340
|
-
const lower = fileType.toLowerCase();
|
|
341
|
-
if (lower.includes("image") || /\.(jpg|jpeg|png|gif|bmp|webp)$/i.test(lower)) {
|
|
342
|
-
return "image";
|
|
343
|
-
}
|
|
344
|
-
if (lower.includes("video") || /\.(mp4|avi|mov|mkv|webm)$/i.test(lower)) {
|
|
345
|
-
return "video";
|
|
346
|
-
}
|
|
347
|
-
if (lower.includes("audio") || /\.(mp3|wav|ogg|m4a)$/i.test(lower)) {
|
|
348
|
-
return "audio";
|
|
349
|
-
}
|
|
350
|
-
return "file";
|
|
351
|
-
}
|
package/dist/src/client.js
CHANGED
|
@@ -23,7 +23,6 @@ export function getXYWebSocketManager(config) {
|
|
|
23
23
|
let cached = wsManagerCache.get(cacheKey);
|
|
24
24
|
if (cached && cached.isConfigMatch(config)) {
|
|
25
25
|
const log = runtime?.log ?? console.log;
|
|
26
|
-
log(`[WS-MANAGER-CACHE] ✅ Reusing cached WebSocket manager: ${cacheKey}, total managers: ${wsManagerCache.size}`);
|
|
27
26
|
return cached;
|
|
28
27
|
}
|
|
29
28
|
// Create new manager
|
|
@@ -86,8 +85,6 @@ export function diagnoseAllManagers() {
|
|
|
86
85
|
let orphanCount = 0;
|
|
87
86
|
wsManagerCache.forEach((manager, key) => {
|
|
88
87
|
const diag = manager.getConnectionDiagnostics();
|
|
89
|
-
console.log(`📌 Manager: ${key}`);
|
|
90
|
-
console.log(` Shutting down: ${diag.isShuttingDown}`);
|
|
91
88
|
console.log(` Total event listeners on manager: ${diag.totalEventListeners}`);
|
|
92
89
|
// Connection
|
|
93
90
|
console.log(` 🔌 Connection:`);
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// CSPL API 请求模块
|
|
2
|
+
import https from "node:https";
|
|
3
|
+
import { URL } from "node:url";
|
|
4
|
+
import { randomBytes } from "node:crypto";
|
|
5
|
+
import { getCsplConfig } from "./config.js";
|
|
6
|
+
import { DEFAULT_HTTP_PORT, HTTP_STATUS_BAD_REQUEST } from "./constants.js";
|
|
7
|
+
function generateTraceId() {
|
|
8
|
+
return randomBytes(16).toString("hex");
|
|
9
|
+
}
|
|
10
|
+
function buildHeaders(config) {
|
|
11
|
+
return {
|
|
12
|
+
"x-hag-trace-id": generateTraceId(),
|
|
13
|
+
"x-uid": config.uid,
|
|
14
|
+
"x-api-key": config.apiKey,
|
|
15
|
+
"x-request-from": config.requestFrom,
|
|
16
|
+
"x-skill-id": config.skillId,
|
|
17
|
+
"content-type": "application/json",
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function buildRequestOptions(url, headers, timeout) {
|
|
21
|
+
const urlObj = new URL(url);
|
|
22
|
+
return {
|
|
23
|
+
hostname: urlObj.hostname,
|
|
24
|
+
port: urlObj.port || DEFAULT_HTTP_PORT,
|
|
25
|
+
path: urlObj.pathname,
|
|
26
|
+
method: "POST",
|
|
27
|
+
headers: headers,
|
|
28
|
+
timeout,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function parseResponse(data) {
|
|
32
|
+
if (!data?.trim())
|
|
33
|
+
throw new Error("[CSPL] API response is empty");
|
|
34
|
+
const json = JSON.parse(data);
|
|
35
|
+
if (json.retCode && json.retCode !== "0") {
|
|
36
|
+
throw new Error(`[CSPL] API error: ${json.retMsg || "unknown"}`);
|
|
37
|
+
}
|
|
38
|
+
if (!json.retCode && json.code) {
|
|
39
|
+
throw new Error(`[CSPL] Backend error: ${json.desc || "unknown"}`);
|
|
40
|
+
}
|
|
41
|
+
return json;
|
|
42
|
+
}
|
|
43
|
+
export async function callCsplApi(questionText, cfg) {
|
|
44
|
+
const config = getCsplConfig(cfg);
|
|
45
|
+
const headers = buildHeaders(config);
|
|
46
|
+
const payload = {
|
|
47
|
+
questionText,
|
|
48
|
+
textSource: config.textSource,
|
|
49
|
+
action: config.action,
|
|
50
|
+
};
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
const options = buildRequestOptions(config.api.url, headers, config.api.timeout);
|
|
53
|
+
const req = https.request(options, (res) => {
|
|
54
|
+
if (res.statusCode && res.statusCode >= HTTP_STATUS_BAD_REQUEST) {
|
|
55
|
+
reject(new Error(`[CSPL] HTTP error: ${res.statusCode}`));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
let data = "";
|
|
59
|
+
res.on("data", (chunk) => {
|
|
60
|
+
data += chunk;
|
|
61
|
+
});
|
|
62
|
+
res.on("end", () => {
|
|
63
|
+
try {
|
|
64
|
+
resolve(parseResponse(data));
|
|
65
|
+
}
|
|
66
|
+
catch (e) {
|
|
67
|
+
reject(e);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
req.on("error", reject);
|
|
72
|
+
req.on("timeout", () => {
|
|
73
|
+
req.destroy();
|
|
74
|
+
reject(new Error("[CSPL] Request timeout"));
|
|
75
|
+
});
|
|
76
|
+
req.write(JSON.stringify(payload));
|
|
77
|
+
req.end();
|
|
78
|
+
});
|
|
79
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ClawdbotConfig } from "openclaw/plugin-sdk";
|
|
2
|
+
export interface ApiConfig {
|
|
3
|
+
url: string;
|
|
4
|
+
timeout: number;
|
|
5
|
+
}
|
|
6
|
+
export interface CsplConfig {
|
|
7
|
+
api: ApiConfig;
|
|
8
|
+
uid: string;
|
|
9
|
+
apiKey: string;
|
|
10
|
+
skillId: string;
|
|
11
|
+
requestFrom: string;
|
|
12
|
+
textSource: string;
|
|
13
|
+
action: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* 构建 CSPL 配置。uid 和 apiKey 复用 XYChannelConfig,避免重复配置。
|
|
17
|
+
* serviceUrl 从 .xiaoyienv 文件读取,skillId 写死在常量中。
|
|
18
|
+
*/
|
|
19
|
+
export declare function getCsplConfig(cfg: ClawdbotConfig): CsplConfig;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// CSPL Hook 配置管理
|
|
2
|
+
// uid 和 apiKey 复用 XYChannelConfig,skillId 写死在常量中
|
|
3
|
+
import { resolveXYConfig } from "../config.js";
|
|
4
|
+
import { CSPL_STATIC_CONFIG, API_URL_SUFFIX, ENV_FILE_PATH } from "./constants.js";
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import { logger } from "../utils/logger.js";
|
|
7
|
+
let cachedConfig = null;
|
|
8
|
+
function readServiceUrl() {
|
|
9
|
+
if (!fs.existsSync(ENV_FILE_PATH)) {
|
|
10
|
+
throw new Error(`[CSPL] Environment file not found: ${ENV_FILE_PATH}`);
|
|
11
|
+
}
|
|
12
|
+
const envData = fs.readFileSync(ENV_FILE_PATH, "utf-8");
|
|
13
|
+
for (const line of envData.split("\n")) {
|
|
14
|
+
const trimmed = line.trim();
|
|
15
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
16
|
+
continue;
|
|
17
|
+
const eqIdx = trimmed.indexOf("=");
|
|
18
|
+
if (eqIdx === -1)
|
|
19
|
+
continue;
|
|
20
|
+
const key = trimmed.substring(0, eqIdx).trim();
|
|
21
|
+
const value = trimmed.substring(eqIdx + 1).trim();
|
|
22
|
+
if (key === "SERVICE_URL" && value)
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
throw new Error("[CSPL] Missing SERVICE_URL in env file");
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* 构建 CSPL 配置。uid 和 apiKey 复用 XYChannelConfig,避免重复配置。
|
|
29
|
+
* serviceUrl 从 .xiaoyienv 文件读取,skillId 写死在常量中。
|
|
30
|
+
*/
|
|
31
|
+
export function getCsplConfig(cfg) {
|
|
32
|
+
if (cachedConfig)
|
|
33
|
+
return cachedConfig;
|
|
34
|
+
const xyConfig = resolveXYConfig(cfg);
|
|
35
|
+
const serviceUrl = readServiceUrl();
|
|
36
|
+
cachedConfig = {
|
|
37
|
+
api: {
|
|
38
|
+
url: `${serviceUrl}${API_URL_SUFFIX}`,
|
|
39
|
+
timeout: CSPL_STATIC_CONFIG.api.timeout,
|
|
40
|
+
},
|
|
41
|
+
uid: xyConfig.uid,
|
|
42
|
+
apiKey: xyConfig.apiKey,
|
|
43
|
+
skillId: CSPL_STATIC_CONFIG.skillId,
|
|
44
|
+
requestFrom: CSPL_STATIC_CONFIG.requestFrom,
|
|
45
|
+
textSource: CSPL_STATIC_CONFIG.textSource,
|
|
46
|
+
action: CSPL_STATIC_CONFIG.action,
|
|
47
|
+
};
|
|
48
|
+
logger.log("[CSPL] Config loaded (uid/apiKey from XYChannelConfig)");
|
|
49
|
+
return cachedConfig;
|
|
50
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export interface HttpHeaders {
|
|
2
|
+
"x-hag-trace-id": string;
|
|
3
|
+
"x-uid": string;
|
|
4
|
+
"x-api-key": string;
|
|
5
|
+
"x-request-from": string;
|
|
6
|
+
"x-skill-id": string;
|
|
7
|
+
"content-type": string;
|
|
8
|
+
}
|
|
9
|
+
export interface ApiPayload {
|
|
10
|
+
questionText: string;
|
|
11
|
+
textSource: string;
|
|
12
|
+
action: string;
|
|
13
|
+
}
|
|
14
|
+
export interface ApiResponse {
|
|
15
|
+
data?: {
|
|
16
|
+
securityResult?: string;
|
|
17
|
+
};
|
|
18
|
+
retCode?: string;
|
|
19
|
+
retMsg?: string;
|
|
20
|
+
code?: string;
|
|
21
|
+
desc?: string;
|
|
22
|
+
[key: string]: any;
|
|
23
|
+
}
|
|
24
|
+
export declare const MIN_TEXT_LENGTH = 0;
|
|
25
|
+
export declare const MAX_TEXT_LENGTH = 4096;
|
|
26
|
+
export declare const MAX_TOTAL_LENGTH = 40960;
|
|
27
|
+
export declare const regex: RegExp;
|
|
28
|
+
export declare const DEFAULT_HTTP_PORT = 80;
|
|
29
|
+
export declare const HTTP_STATUS_BAD_REQUEST = 400;
|
|
30
|
+
export declare const ENV_FILE_PATH = "/home/sandbox/.openclaw/.xiaoyienv";
|
|
31
|
+
export declare const API_URL_SUFFIX = "/celia-claw/v1/rest-api/skill/execute";
|
|
32
|
+
export declare const REQUIRED_ENV_VARS: string[];
|
|
33
|
+
export declare const ALLOWED_TOOLS: string[];
|
|
34
|
+
export declare const CSPL_STATIC_CONFIG: {
|
|
35
|
+
readonly api: {
|
|
36
|
+
readonly timeout: 5000;
|
|
37
|
+
};
|
|
38
|
+
readonly skillId: "skill-scope";
|
|
39
|
+
readonly requestFrom: "openclaw";
|
|
40
|
+
readonly textSource: "question";
|
|
41
|
+
readonly action: "TOOL_OUTPUT_SCAN";
|
|
42
|
+
};
|
|
43
|
+
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";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// CSPL Hook 常量与类型定义
|
|
2
|
+
export const MIN_TEXT_LENGTH = 0;
|
|
3
|
+
export const MAX_TEXT_LENGTH = 4096;
|
|
4
|
+
export const MAX_TOTAL_LENGTH = 40960;
|
|
5
|
+
export const regex = /[^\u4e00-\u9fa5a-zA-Z0-9\s\.,!?;:,。!?;:""\'\'()()\[\]【】]/;
|
|
6
|
+
export const DEFAULT_HTTP_PORT = 80;
|
|
7
|
+
export const HTTP_STATUS_BAD_REQUEST = 400;
|
|
8
|
+
export const ENV_FILE_PATH = "/home/sandbox/.openclaw/.xiaoyienv";
|
|
9
|
+
export const API_URL_SUFFIX = "/celia-claw/v1/rest-api/skill/execute";
|
|
10
|
+
export const REQUIRED_ENV_VARS = ["PERSONAL-API-KEY", "PERSONAL-UID", "SERVICE_URL"];
|
|
11
|
+
// 工具白名单
|
|
12
|
+
export const ALLOWED_TOOLS = ["web_fetch", "read", "pdf"];
|
|
13
|
+
// 静态配置(非敏感,敏感值从 ENV 文件读取)
|
|
14
|
+
export const CSPL_STATIC_CONFIG = {
|
|
15
|
+
api: { timeout: 5000 },
|
|
16
|
+
skillId: "skill-scope",
|
|
17
|
+
requestFrom: "openclaw",
|
|
18
|
+
textSource: "question",
|
|
19
|
+
action: "TOOL_OUTPUT_SCAN",
|
|
20
|
+
};
|
|
21
|
+
// Steer 注入常量
|
|
22
|
+
export const STEER_ABORT_MESSAGE = "当前行为存在安全隐患,终止所有操作,并且在最终回复中说明,识别到当前流程中存在潜在安全风险,已帮您中止当前流程,请清空上下文后重试";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare function filterText(text: string): string;
|
|
2
|
+
export declare function validateAndTruncateText(text: string, maxLength: number): {
|
|
3
|
+
text: string;
|
|
4
|
+
truncated: boolean;
|
|
5
|
+
};
|
|
6
|
+
export declare function extractResultText(event: any, toolName: string): string;
|
|
7
|
+
export declare function processText(resultText: string): string;
|
|
8
|
+
export declare function parseSecurityResult(response: any): {
|
|
9
|
+
status: "ACCEPT" | "REJECT";
|
|
10
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// CSPL Hook 工具函数
|
|
2
|
+
import { MAX_TEXT_LENGTH, regex } from "./constants.js";
|
|
3
|
+
export function filterText(text) {
|
|
4
|
+
if (!text)
|
|
5
|
+
return "";
|
|
6
|
+
return text.replace(new RegExp(regex.source, "g"), "");
|
|
7
|
+
}
|
|
8
|
+
export function validateAndTruncateText(text, maxLength) {
|
|
9
|
+
if (text.length > maxLength) {
|
|
10
|
+
const halfMaxLength = Math.floor(maxLength / 2);
|
|
11
|
+
const startText = text.substring(0, halfMaxLength);
|
|
12
|
+
const endText = text.substring(text.length - halfMaxLength);
|
|
13
|
+
return { text: startText + endText, truncated: true };
|
|
14
|
+
}
|
|
15
|
+
return { text, truncated: false };
|
|
16
|
+
}
|
|
17
|
+
export function extractResultText(event, toolName) {
|
|
18
|
+
const resultTexts = [];
|
|
19
|
+
if (toolName === "web_fetch") {
|
|
20
|
+
if (event.result?.details?.text) {
|
|
21
|
+
resultTexts.push(event.result.details.text);
|
|
22
|
+
}
|
|
23
|
+
return resultTexts.length > 0 ? resultTexts.join("; ") : "";
|
|
24
|
+
}
|
|
25
|
+
if (event.result?.content && Array.isArray(event.result.content)) {
|
|
26
|
+
for (const item of event.result.content) {
|
|
27
|
+
if (item?.text) {
|
|
28
|
+
resultTexts.push(item.text);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return resultTexts.length > 0 ? resultTexts.join("; ") : "";
|
|
33
|
+
}
|
|
34
|
+
export function processText(resultText) {
|
|
35
|
+
const questionText = filterText(resultText);
|
|
36
|
+
const { text: finalText } = validateAndTruncateText(questionText, MAX_TEXT_LENGTH);
|
|
37
|
+
return finalText;
|
|
38
|
+
}
|
|
39
|
+
export function parseSecurityResult(response) {
|
|
40
|
+
if (response === null || response === undefined) {
|
|
41
|
+
throw new Error("Response is null or undefined");
|
|
42
|
+
}
|
|
43
|
+
if (!response.data || typeof response.data !== "object") {
|
|
44
|
+
throw new Error("Response.data is missing or not an object");
|
|
45
|
+
}
|
|
46
|
+
const securityResult = response.data.securityResult;
|
|
47
|
+
if (typeof securityResult !== "string") {
|
|
48
|
+
throw new Error("Response.data.securityResult is missing or not a string");
|
|
49
|
+
}
|
|
50
|
+
if (securityResult !== securityResult.trim()) {
|
|
51
|
+
throw new Error("Response.data.securityResult contains leading or trailing spaces");
|
|
52
|
+
}
|
|
53
|
+
if (securityResult !== "ACCEPT" && securityResult !== "REJECT") {
|
|
54
|
+
throw new Error(`Response.data.securityResult must be "ACCEPT" or "REJECT". Actual: "${securityResult}"`);
|
|
55
|
+
}
|
|
56
|
+
return { status: securityResult };
|
|
57
|
+
}
|
package/dist/src/formatter.js
CHANGED
|
@@ -52,18 +52,11 @@ export async function sendA2AResponse(params) {
|
|
|
52
52
|
msgDetail: JSON.stringify(jsonRpcResponse),
|
|
53
53
|
};
|
|
54
54
|
// 📋 Log complete response body
|
|
55
|
-
log(`[A2A_RESPONSE] 📤 Sending A2A artifact-update response
|
|
56
|
-
log(`[A2A_RESPONSE] - sessionId: ${sessionId}`);
|
|
57
|
-
log(`[A2A_RESPONSE] - taskId: ${taskId}`);
|
|
58
|
-
log(`[A2A_RESPONSE] - messageId: ${messageId}`);
|
|
55
|
+
log(`[A2A_RESPONSE] 📤 Sending A2A artifact-update response: taskId: ${taskId}`);
|
|
59
56
|
log(`[A2A_RESPONSE] - append: ${append}`);
|
|
60
57
|
log(`[A2A_RESPONSE] - final: ${final}`);
|
|
61
|
-
log(`[A2A_RESPONSE] - text
|
|
58
|
+
log(`[A2A_RESPONSE] - text: ${text}`);
|
|
62
59
|
log(`[A2A_RESPONSE] - files count: ${files?.length ?? 0}`);
|
|
63
|
-
log(`[A2A_RESPONSE] 📦 Complete outbound message:`);
|
|
64
|
-
log(JSON.stringify(outboundMessage, null, 2));
|
|
65
|
-
log(`[A2A_RESPONSE] 📦 JSON-RPC response body:`);
|
|
66
|
-
log(JSON.stringify(jsonRpcResponse, null, 2));
|
|
67
60
|
await wsManager.sendMessage(sessionId, outboundMessage);
|
|
68
61
|
log(`[A2A_RESPONSE] ✅ Message sent successfully`);
|
|
69
62
|
}
|
|
@@ -152,15 +145,10 @@ export async function sendStatusUpdate(params) {
|
|
|
152
145
|
};
|
|
153
146
|
// 📋 Log complete response body
|
|
154
147
|
log(`[A2A_STATUS] 📤 Sending A2A status-update:`);
|
|
155
|
-
log(`[A2A_STATUS] - sessionId: ${sessionId}`);
|
|
156
148
|
log(`[A2A_STATUS] - taskId: ${taskId}`);
|
|
157
149
|
log(`[A2A_STATUS] - messageId: ${messageId}`);
|
|
158
150
|
log(`[A2A_STATUS] - state: ${state}`);
|
|
159
151
|
log(`[A2A_STATUS] - text: "${text}"`);
|
|
160
|
-
log(`[A2A_STATUS] 📦 Complete outbound message:`);
|
|
161
|
-
log(JSON.stringify(outboundMessage, null, 2));
|
|
162
|
-
log(`[A2A_STATUS] 📦 JSON-RPC response body:`);
|
|
163
|
-
log(JSON.stringify(jsonRpcResponse, null, 2));
|
|
164
152
|
await wsManager.sendMessage(sessionId, outboundMessage);
|
|
165
153
|
log(`[A2A_STATUS] ✅ Status update sent successfully`);
|
|
166
154
|
}
|
|
@@ -208,15 +196,7 @@ export async function sendCommand(params) {
|
|
|
208
196
|
msgDetail: JSON.stringify(jsonRpcResponse),
|
|
209
197
|
};
|
|
210
198
|
// 📋 Log complete response body
|
|
211
|
-
log(`[A2A_COMMAND] 📤 Sending A2A command
|
|
212
|
-
log(`[A2A_COMMAND] - sessionId: ${sessionId}`);
|
|
213
|
-
log(`[A2A_COMMAND] - taskId: ${taskId}`);
|
|
214
|
-
log(`[A2A_COMMAND] - messageId: ${messageId}`);
|
|
215
|
-
log(`[A2A_COMMAND] - command: ${command.header.namespace}::${command.header.name}`);
|
|
216
|
-
log(`[A2A_COMMAND] 📦 Complete outbound message:`);
|
|
217
|
-
log(JSON.stringify(outboundMessage, null, 2));
|
|
218
|
-
log(`[A2A_COMMAND] 📦 JSON-RPC response body:`);
|
|
219
|
-
log(JSON.stringify(jsonRpcResponse, null, 2));
|
|
199
|
+
log(`[A2A_COMMAND] 📤 Sending A2A command: taskId: ${taskId}`);
|
|
220
200
|
await wsManager.sendMessage(sessionId, outboundMessage);
|
|
221
201
|
log(`[A2A_COMMAND] ✅ Command sent successfully`);
|
|
222
202
|
}
|
package/dist/src/heartbeat.js
CHANGED
|
@@ -72,18 +72,14 @@ export class HeartbeatManager {
|
|
|
72
72
|
}
|
|
73
73
|
try {
|
|
74
74
|
// Send application-level heartbeat message
|
|
75
|
-
console.log(`[WS-${this.serverName}-SEND] Sending heartbeat frame:`, this.config.message);
|
|
76
75
|
this.ws.send(this.config.message);
|
|
77
|
-
console.log(`[WS-${this.serverName}-SEND] Heartbeat message sent, size: ${this.config.message.length} bytes`);
|
|
78
76
|
// Send protocol-level ping
|
|
79
77
|
this.ws.ping();
|
|
80
|
-
console.log(`[WS-${this.serverName}-SEND] Protocol-level ping sent`);
|
|
81
78
|
// Setup timeout timer
|
|
82
79
|
this.timeoutTimer = setTimeout(() => {
|
|
83
80
|
this.error(`Heartbeat timeout for ${this.serverName}`);
|
|
84
81
|
this.onTimeout();
|
|
85
82
|
}, this.config.timeout);
|
|
86
|
-
this.log(`[DEBUG] Heartbeat sent for ${this.serverName}`);
|
|
87
83
|
}
|
|
88
84
|
catch (error) {
|
|
89
85
|
this.error(`Failed to send heartbeat for ${this.serverName}:`, error);
|
|
@@ -3,6 +3,37 @@ import { getXYRuntime } from "./runtime.js";
|
|
|
3
3
|
import { sendA2AResponse, sendStatusUpdate, sendReasoningTextUpdate } from "./formatter.js";
|
|
4
4
|
import { resolveXYConfig } from "./config.js";
|
|
5
5
|
import { getCurrentTaskId, getCurrentMessageId } from "./task-manager.js";
|
|
6
|
+
import fs from "fs/promises";
|
|
7
|
+
import path from "path";
|
|
8
|
+
/**
|
|
9
|
+
* 清理 /tmp/xy_channel 目录中的所有文件
|
|
10
|
+
*/
|
|
11
|
+
async function cleanupTempDir(tempDir = "/tmp/xy_channel") {
|
|
12
|
+
try {
|
|
13
|
+
const stats = await fs.stat(tempDir).catch(() => null);
|
|
14
|
+
if (!stats?.isDirectory()) {
|
|
15
|
+
return; // 目录不存在,直接返回
|
|
16
|
+
}
|
|
17
|
+
const files = await fs.readdir(tempDir);
|
|
18
|
+
let cleanedCount = 0;
|
|
19
|
+
for (const file of files) {
|
|
20
|
+
const filePath = path.join(tempDir, file);
|
|
21
|
+
try {
|
|
22
|
+
await fs.unlink(filePath);
|
|
23
|
+
cleanedCount++;
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
// 忽略单个文件删除失败,继续处理其他文件
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (cleanedCount > 0) {
|
|
30
|
+
console.log(`[CLEANUP] 🧹 Cleaned ${cleanedCount} files from ${tempDir}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
console.error(`[CLEANUP] ❌ Failed to cleanup temp dir:`, err);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
6
37
|
/**
|
|
7
38
|
* Create a reply dispatcher for XY channel messages.
|
|
8
39
|
* Follows feishu pattern with status updates and streaming support.
|
|
@@ -206,6 +237,7 @@ export function createXYReplyDispatcher(params) {
|
|
|
206
237
|
}
|
|
207
238
|
}
|
|
208
239
|
stopStatusInterval();
|
|
240
|
+
void cleanupTempDir();
|
|
209
241
|
},
|
|
210
242
|
onCleanup: () => {
|
|
211
243
|
const currentTaskId = getActiveTaskId();
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk";
|
|
2
|
+
/**
|
|
3
|
+
* 在 handleXYMessage 入口处调用,缓存 cfg/runtime 供 steer 注入使用。
|
|
4
|
+
*/
|
|
5
|
+
export declare function setCachedContext(cfg: ClawdbotConfig, runtime: RuntimeEnv, accountId: string): void;
|
|
6
|
+
/**
|
|
7
|
+
* 尝试向当前活跃会话注入 steer 消息。
|
|
8
|
+
* 两层保险:
|
|
9
|
+
* 1. getSessionContext(sessionKey) 确认是当前 XY 活跃 session
|
|
10
|
+
* 2. hasActiveTask(sessionId) 确认任务仍在运行
|
|
11
|
+
*
|
|
12
|
+
* @param sessionKey 来自 after_tool_call ctx.sessionKey(per-peer 下精确对应一个 XY session)
|
|
13
|
+
* @param message 要注入的用户消息文本
|
|
14
|
+
* @returns true=已注入,false=跳过
|
|
15
|
+
*/
|
|
16
|
+
export declare function tryInjectSteer(sessionKey: string | undefined, message: string): Promise<boolean>;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Steer message injector for CSPL hook integration
|
|
2
|
+
import { getSessionContext } from "./tools/session-manager.js";
|
|
3
|
+
import { hasActiveTask, getCurrentTaskId } from "./task-manager.js";
|
|
4
|
+
import { handleXYMessage } from "./bot.js";
|
|
5
|
+
import { logger } from "./utils/logger.js";
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
let cachedCfg = null;
|
|
8
|
+
let cachedRuntime = null;
|
|
9
|
+
let cachedAccountId = "default";
|
|
10
|
+
/**
|
|
11
|
+
* 在 handleXYMessage 入口处调用,缓存 cfg/runtime 供 steer 注入使用。
|
|
12
|
+
*/
|
|
13
|
+
export function setCachedContext(cfg, runtime, accountId) {
|
|
14
|
+
cachedCfg = cfg;
|
|
15
|
+
cachedRuntime = runtime;
|
|
16
|
+
cachedAccountId = accountId;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* 尝试向当前活跃会话注入 steer 消息。
|
|
20
|
+
* 两层保险:
|
|
21
|
+
* 1. getSessionContext(sessionKey) 确认是当前 XY 活跃 session
|
|
22
|
+
* 2. hasActiveTask(sessionId) 确认任务仍在运行
|
|
23
|
+
*
|
|
24
|
+
* @param sessionKey 来自 after_tool_call ctx.sessionKey(per-peer 下精确对应一个 XY session)
|
|
25
|
+
* @param message 要注入的用户消息文本
|
|
26
|
+
* @returns true=已注入,false=跳过
|
|
27
|
+
*/
|
|
28
|
+
export async function tryInjectSteer(sessionKey, message) {
|
|
29
|
+
if (!sessionKey) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
const sessionCtx = getSessionContext(sessionKey);
|
|
33
|
+
if (!sessionCtx) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
const { sessionId } = sessionCtx;
|
|
37
|
+
const activeTaskId = getCurrentTaskId(sessionId);
|
|
38
|
+
if (!hasActiveTask(sessionId)) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
if (!cachedCfg || !cachedRuntime) {
|
|
42
|
+
logger.error("[STEER] No cached cfg/runtime available, cannot inject");
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
// 3. 构造合成 A2A 消息(伪装成用户在当前会话中发送的新消息)
|
|
46
|
+
const syntheticMessage = {
|
|
47
|
+
jsonrpc: "2.0",
|
|
48
|
+
method: "tasks/send",
|
|
49
|
+
id: `steer-msg-${randomUUID()}`,
|
|
50
|
+
params: {
|
|
51
|
+
sessionId,
|
|
52
|
+
id: activeTaskId ?? `steer-task-${randomUUID()}`,
|
|
53
|
+
agentLoginSessionId: "",
|
|
54
|
+
message: {
|
|
55
|
+
role: "user",
|
|
56
|
+
parts: [{ kind: "text", text: message }],
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
console.log(`[STEER] Injecting steer for sessionId=${sessionId}, taskId=${syntheticMessage.params.id}`);
|
|
61
|
+
try {
|
|
62
|
+
await handleXYMessage({
|
|
63
|
+
cfg: cachedCfg,
|
|
64
|
+
runtime: cachedRuntime,
|
|
65
|
+
message: syntheticMessage,
|
|
66
|
+
accountId: cachedAccountId,
|
|
67
|
+
});
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
logger.error(`[STEER] ❌ Failed to inject steer: ${err}`);
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|