@ynhcj/xiaoyi-channel 0.0.124-beta → 0.0.124-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.d.ts +3 -6
- package/dist/index.js +85 -54
- package/dist/provider-discovery.d.ts +2 -0
- package/dist/provider-discovery.js +4 -0
- package/dist/src/bot.d.ts +2 -0
- package/dist/src/bot.js +104 -103
- package/dist/src/client.d.ts +1 -5
- package/dist/src/client.js +25 -36
- package/dist/src/cspl/call-api.d.ts +6 -0
- package/dist/src/cspl/call-api.js +37 -16
- package/dist/src/cspl/config.d.ts +11 -1
- package/dist/src/cspl/config.js +30 -0
- package/dist/src/cspl/middleware.d.ts +8 -0
- package/dist/src/cspl/middleware.js +90 -0
- package/dist/src/cspl/steer-context.d.ts +7 -0
- package/dist/src/cspl/steer-context.js +47 -0
- package/dist/src/file-download.js +4 -3
- package/dist/src/file-upload.js +19 -18
- package/dist/src/formatter.js +32 -44
- package/dist/src/heartbeat.js +4 -3
- package/dist/src/login-token-handler.js +13 -10
- package/dist/src/message-queue.js +2 -1
- package/dist/src/monitor.js +54 -42
- package/dist/src/outbound.js +22 -18
- package/dist/src/provider.js +82 -30
- package/dist/src/push.js +16 -15
- package/dist/src/reply-dispatcher.d.ts +3 -1
- package/dist/src/reply-dispatcher.js +64 -62
- package/dist/src/self-evolution-handler.js +11 -14
- package/dist/src/skill-retriever/hooks.js +4 -4
- package/dist/src/skill-retriever/tool-search.js +13 -17
- package/dist/src/steer-injector.js +1 -1
- package/dist/src/task-manager.d.ts +4 -27
- package/dist/src/task-manager.js +13 -78
- package/dist/src/tools/calendar-tool.js +5 -1
- package/dist/src/tools/call-phone-tool.js +5 -1
- package/dist/src/tools/create-alarm-tool.js +5 -1
- package/dist/src/tools/delete-alarm-tool.js +5 -1
- package/dist/src/tools/image-reading-tool.d.ts +1 -1
- package/dist/src/tools/image-reading-tool.js +38 -114
- package/dist/src/tools/location-tool.js +5 -1
- package/dist/src/tools/login-token-tool.js +13 -2
- package/dist/src/tools/modify-alarm-tool.js +5 -1
- package/dist/src/tools/modify-note-tool.js +5 -1
- package/dist/src/tools/note-tool.js +5 -1
- package/dist/src/tools/query-app-message-tool.js +5 -1
- package/dist/src/tools/query-memory-data-tool.js +5 -1
- package/dist/src/tools/query-todo-task-tool.js +5 -1
- package/dist/src/tools/save-file-to-phone-tool.js +5 -1
- package/dist/src/tools/save-media-to-gallery-tool.js +5 -1
- package/dist/src/tools/search-alarm-tool.js +5 -1
- package/dist/src/tools/search-calendar-tool.js +5 -1
- package/dist/src/tools/search-contact-tool.js +5 -1
- package/dist/src/tools/search-email-tool.js +5 -1
- package/dist/src/tools/search-file-tool.js +5 -1
- package/dist/src/tools/search-message-tool.js +5 -1
- package/dist/src/tools/search-note-tool.js +5 -1
- package/dist/src/tools/search-photo-gallery-tool.js +5 -1
- package/dist/src/tools/send-email-tool.js +5 -1
- package/dist/src/tools/send-file-to-user-tool.js +10 -4
- package/dist/src/tools/send-message-tool.js +5 -1
- package/dist/src/tools/session-helper.d.ts +24 -0
- package/dist/src/tools/session-helper.js +45 -0
- package/dist/src/tools/session-manager.d.ts +8 -0
- package/dist/src/tools/session-manager.js +28 -24
- package/dist/src/tools/upload-file-tool.js +5 -1
- package/dist/src/tools/upload-photo-tool.js +5 -1
- package/dist/src/tools/xiaoyi-add-collection-tool.js +5 -1
- package/dist/src/tools/xiaoyi-collection-tool.js +5 -1
- package/dist/src/tools/xiaoyi-delete-collection-tool.js +5 -1
- package/dist/src/tools/xiaoyi-gui-tool.js +7 -2
- package/dist/src/trigger-handler.js +8 -9
- package/dist/src/utils/logger.js +105 -19
- package/dist/src/utils/self-evolution-manager.js +3 -2
- package/dist/src/utils/throw.d.ts +5 -0
- package/dist/src/utils/throw.js +10 -0
- package/dist/src/websocket.js +35 -31
- package/dist/src/xy-session-store.d.ts +79 -0
- package/dist/src/xy-session-store.js +153 -0
- package/openclaw.plugin.json +4 -0
- package/package.json +6 -5
package/dist/src/client.js
CHANGED
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
// WebSocket client cache management
|
|
2
2
|
// Follows feishu/client.ts pattern for caching client instances
|
|
3
3
|
import { XYWebSocketManager } from "./websocket.js";
|
|
4
|
-
|
|
5
|
-
let runtime;
|
|
6
|
-
/**
|
|
7
|
-
* Set the runtime for logging in client module.
|
|
8
|
-
*/
|
|
9
|
-
export function setClientRuntime(rt) {
|
|
10
|
-
runtime = rt;
|
|
11
|
-
}
|
|
4
|
+
import { logger } from "./utils/logger.js";
|
|
12
5
|
/**
|
|
13
6
|
* Global cache for WebSocket managers.
|
|
14
7
|
* Key format: `${apiKey}-${agentId}`
|
|
@@ -24,19 +17,17 @@ const wsManagerCache = _g.__xyWsManagerCache;
|
|
|
24
17
|
* Get or create a WebSocket manager for the given configuration.
|
|
25
18
|
* Reuses existing managers if config matches.
|
|
26
19
|
*/
|
|
27
|
-
export function getXYWebSocketManager(config) {
|
|
20
|
+
export function getXYWebSocketManager(config, runtime) {
|
|
28
21
|
const cacheKey = `${config.apiKey}-${config.agentId}`;
|
|
29
22
|
let cached = wsManagerCache.get(cacheKey);
|
|
30
23
|
if (cached && cached.isConfigMatch(config)) {
|
|
31
|
-
const log = runtime?.log ?? console.log;
|
|
32
24
|
return cached;
|
|
33
25
|
}
|
|
34
26
|
// Create new manager
|
|
35
|
-
|
|
36
|
-
log(`[WS-MANAGER-CACHE] 🆕 Creating new WebSocket manager: ${cacheKey}, total managers before: ${wsManagerCache.size}`);
|
|
27
|
+
logger.log(`[WS-MANAGER-CACHE] 🆕 Creating new WebSocket manager: ${cacheKey}, total managers before: ${wsManagerCache.size}`);
|
|
37
28
|
cached = new XYWebSocketManager(config, runtime);
|
|
38
29
|
wsManagerCache.set(cacheKey, cached);
|
|
39
|
-
log(`[WS-MANAGER-CACHE] 📊 Total managers after creation: ${wsManagerCache.size}`);
|
|
30
|
+
logger.log(`[WS-MANAGER-CACHE] 📊 Total managers after creation: ${wsManagerCache.size}`);
|
|
40
31
|
return cached;
|
|
41
32
|
}
|
|
42
33
|
/**
|
|
@@ -47,21 +38,20 @@ export function removeXYWebSocketManager(config) {
|
|
|
47
38
|
const cacheKey = `${config.apiKey}-${config.agentId}`;
|
|
48
39
|
const manager = wsManagerCache.get(cacheKey);
|
|
49
40
|
if (manager) {
|
|
50
|
-
|
|
41
|
+
logger.log(`🗑️ [WS-MANAGER-CACHE] Removing manager from cache: ${cacheKey}`);
|
|
51
42
|
manager.disconnect();
|
|
52
43
|
wsManagerCache.delete(cacheKey);
|
|
53
|
-
|
|
44
|
+
logger.log(`🗑️ [WS-MANAGER-CACHE] Manager removed, remaining managers: ${wsManagerCache.size}`);
|
|
54
45
|
}
|
|
55
46
|
else {
|
|
56
|
-
|
|
47
|
+
logger.log(`⚠️ [WS-MANAGER-CACHE] Manager not found in cache: ${cacheKey}`);
|
|
57
48
|
}
|
|
58
49
|
}
|
|
59
50
|
/**
|
|
60
51
|
* Clear all cached WebSocket managers.
|
|
61
52
|
*/
|
|
62
53
|
export function clearXYWebSocketManagers() {
|
|
63
|
-
|
|
64
|
-
log("Clearing all WebSocket manager caches");
|
|
54
|
+
logger.log("Clearing all WebSocket manager caches");
|
|
65
55
|
for (const manager of wsManagerCache.values()) {
|
|
66
56
|
manager.disconnect();
|
|
67
57
|
}
|
|
@@ -78,36 +68,35 @@ export function getCachedManagerCount() {
|
|
|
78
68
|
* Helps identify connection issues and orphan connections.
|
|
79
69
|
*/
|
|
80
70
|
export function diagnoseAllManagers() {
|
|
81
|
-
|
|
71
|
+
logger.log(`Total cached managers: ${wsManagerCache.size}`);
|
|
82
72
|
if (wsManagerCache.size === 0) {
|
|
83
|
-
|
|
73
|
+
logger.log("ℹ️ No managers in cache");
|
|
84
74
|
return;
|
|
85
75
|
}
|
|
86
76
|
let orphanCount = 0;
|
|
87
77
|
wsManagerCache.forEach((manager, key) => {
|
|
88
78
|
const diag = manager.getConnectionDiagnostics();
|
|
89
|
-
|
|
79
|
+
logger.log(` Total event listeners on manager: ${diag.totalEventListeners}`);
|
|
90
80
|
// Connection
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
81
|
+
logger.log(` 🔌 Connection:`);
|
|
82
|
+
logger.log(` - Exists: ${diag.connection.exists}`);
|
|
83
|
+
logger.log(` - ReadyState: ${diag.connection.readyState}`);
|
|
84
|
+
logger.log(` - State connected/ready: ${diag.connection.stateConnected}/${diag.connection.stateReady}`);
|
|
85
|
+
logger.log(` - Reconnect attempts: ${diag.connection.reconnectAttempts}`);
|
|
86
|
+
logger.log(` - Listeners on WebSocket: ${diag.connection.listenerCount}`);
|
|
87
|
+
logger.log(` - Heartbeat active: ${diag.connection.heartbeatActive}`);
|
|
88
|
+
logger.log(` - Has reconnect timer: ${diag.connection.hasReconnectTimer}`);
|
|
99
89
|
if (diag.connection.isOrphan) {
|
|
100
|
-
|
|
90
|
+
logger.log(` ⚠️ ORPHAN CONNECTION DETECTED!`);
|
|
101
91
|
orphanCount++;
|
|
102
92
|
}
|
|
103
|
-
console.log("");
|
|
104
93
|
});
|
|
105
94
|
if (orphanCount > 0) {
|
|
106
|
-
|
|
107
|
-
|
|
95
|
+
logger.log(`⚠️ Total orphan connections found: ${orphanCount}`);
|
|
96
|
+
logger.log(`💡 Suggestion: These connections should be cleaned up`);
|
|
108
97
|
}
|
|
109
98
|
else {
|
|
110
|
-
|
|
99
|
+
logger.log(`✅ No orphan connections found`);
|
|
111
100
|
}
|
|
112
101
|
}
|
|
113
102
|
/**
|
|
@@ -119,13 +108,13 @@ export function cleanupOrphanConnections() {
|
|
|
119
108
|
wsManagerCache.forEach((manager, key) => {
|
|
120
109
|
const diag = manager.getConnectionDiagnostics();
|
|
121
110
|
if (diag.connection.isOrphan) {
|
|
122
|
-
|
|
111
|
+
logger.log(`🧹 Cleaning up orphan connections in manager: ${key}`);
|
|
123
112
|
manager.disconnect();
|
|
124
113
|
cleanedCount++;
|
|
125
114
|
}
|
|
126
115
|
});
|
|
127
116
|
if (cleanedCount > 0) {
|
|
128
|
-
|
|
117
|
+
logger.log(`🧹 Cleaned up ${cleanedCount} manager(s) with orphan connections`);
|
|
129
118
|
}
|
|
130
119
|
return cleanedCount;
|
|
131
120
|
}
|
|
@@ -1,3 +1,9 @@
|
|
|
1
1
|
import type { ClawdbotConfig } from "openclaw/plugin-sdk";
|
|
2
|
+
import { type CsplConfig } from "./config.js";
|
|
2
3
|
import type { ApiResponse } from "./constants.js";
|
|
3
4
|
export declare function callCsplApi(questionText: string, cfg: ClawdbotConfig): Promise<ApiResponse>;
|
|
5
|
+
/**
|
|
6
|
+
* Call CSPL API with a pre-resolved CsplConfig.
|
|
7
|
+
* Used by after_tool_call hook which has session context but not ClawdbotConfig.
|
|
8
|
+
*/
|
|
9
|
+
export declare function callCsplApiWithConfig(questionText: string, config: CsplConfig): Promise<ApiResponse>;
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
// SENTINEL HOOK API 请求模块
|
|
2
|
+
import http from "node:http";
|
|
2
3
|
import https from "node:https";
|
|
3
4
|
import { URL } from "node:url";
|
|
4
5
|
import { randomBytes } from "node:crypto";
|
|
5
6
|
import { getCsplConfig } from "./config.js";
|
|
6
7
|
import { DEFAULT_HTTP_PORT, HTTP_STATUS_BAD_REQUEST } from "./constants.js";
|
|
8
|
+
import { logger } from "../utils/logger.js";
|
|
7
9
|
function generateTraceId() {
|
|
8
10
|
return randomBytes(16).toString("hex");
|
|
9
11
|
}
|
|
10
12
|
function buildHeaders(config) {
|
|
11
13
|
const traceId = generateTraceId();
|
|
12
|
-
|
|
14
|
+
logger.log(`[SENTINEL HOOK] trace-id: ${traceId}`);
|
|
13
15
|
return {
|
|
14
16
|
"x-hag-trace-id": traceId,
|
|
15
17
|
"x-uid": config.uid,
|
|
@@ -42,18 +44,12 @@ function parseResponse(data) {
|
|
|
42
44
|
}
|
|
43
45
|
return json;
|
|
44
46
|
}
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
questionText,
|
|
50
|
-
textSource: config.textSource,
|
|
51
|
-
action: config.action,
|
|
52
|
-
extra: JSON.stringify({ userId: config.uid }),
|
|
53
|
-
};
|
|
47
|
+
function doApiRequest(url, headers, payload, timeout) {
|
|
48
|
+
const isHttp = url.startsWith("http://");
|
|
49
|
+
const module = isHttp ? http : https;
|
|
50
|
+
const options = buildRequestOptions(url, headers, timeout);
|
|
54
51
|
return new Promise((resolve, reject) => {
|
|
55
|
-
const
|
|
56
|
-
const req = https.request(options, (res) => {
|
|
52
|
+
const req = module.request(options, (res) => {
|
|
57
53
|
if (res.statusCode && res.statusCode >= HTTP_STATUS_BAD_REQUEST) {
|
|
58
54
|
reject(new Error(`[SENTINEL HOOK] HTTP error: ${res.statusCode}`));
|
|
59
55
|
return;
|
|
@@ -65,21 +61,21 @@ export async function callCsplApi(questionText, cfg) {
|
|
|
65
61
|
res.on("end", () => {
|
|
66
62
|
try {
|
|
67
63
|
const result = parseResponse(data);
|
|
68
|
-
|
|
64
|
+
logger.log(`[SENTINEL HOOK] ✅ 请求成功, securityResult=${result?.data?.securityResult ?? "N/A"}`);
|
|
69
65
|
resolve(result);
|
|
70
66
|
}
|
|
71
67
|
catch (e) {
|
|
72
|
-
|
|
68
|
+
logger.error(`[SENTINEL HOOK] ❌ 请求失败: ${e instanceof Error ? e.message : String(e)}`);
|
|
73
69
|
reject(e);
|
|
74
70
|
}
|
|
75
71
|
});
|
|
76
72
|
});
|
|
77
73
|
req.on("error", (error) => {
|
|
78
|
-
|
|
74
|
+
logger.error(`[SENTINEL HOOK] ❌ 请求错误: ${error instanceof Error ? error.message : String(error)}`);
|
|
79
75
|
reject(error);
|
|
80
76
|
});
|
|
81
77
|
req.on("timeout", () => {
|
|
82
|
-
|
|
78
|
+
logger.error(`[SENTINEL HOOK] ⏰ 请求超时 (${timeout}ms)`);
|
|
83
79
|
req.destroy();
|
|
84
80
|
reject(new Error("[SENTINEL HOOK] Request timeout"));
|
|
85
81
|
});
|
|
@@ -87,3 +83,28 @@ export async function callCsplApi(questionText, cfg) {
|
|
|
87
83
|
req.end();
|
|
88
84
|
});
|
|
89
85
|
}
|
|
86
|
+
export async function callCsplApi(questionText, cfg) {
|
|
87
|
+
const config = getCsplConfig(cfg);
|
|
88
|
+
const headers = buildHeaders(config);
|
|
89
|
+
const payload = {
|
|
90
|
+
questionText,
|
|
91
|
+
textSource: config.textSource,
|
|
92
|
+
action: config.action,
|
|
93
|
+
extra: JSON.stringify({ userId: config.uid }),
|
|
94
|
+
};
|
|
95
|
+
return doApiRequest(config.api.url, headers, payload, config.api.timeout);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Call CSPL API with a pre-resolved CsplConfig.
|
|
99
|
+
* Used by after_tool_call hook which has session context but not ClawdbotConfig.
|
|
100
|
+
*/
|
|
101
|
+
export async function callCsplApiWithConfig(questionText, config) {
|
|
102
|
+
const headers = buildHeaders(config);
|
|
103
|
+
const payload = {
|
|
104
|
+
questionText,
|
|
105
|
+
textSource: config.textSource,
|
|
106
|
+
action: config.action,
|
|
107
|
+
extra: JSON.stringify({ userId: config.uid }),
|
|
108
|
+
};
|
|
109
|
+
return doApiRequest(config.api.url, headers, payload, config.api.timeout);
|
|
110
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ClawdbotConfig } from "openclaw/plugin-sdk";
|
|
2
|
+
import type { XYChannelConfig } from "../types.js";
|
|
2
3
|
export interface ApiConfig {
|
|
3
4
|
url: string;
|
|
4
5
|
timeout: number;
|
|
@@ -15,5 +16,14 @@ export interface CsplConfig {
|
|
|
15
16
|
/**
|
|
16
17
|
* 构建 CSPL 配置。uid 和 apiKey 复用 XYChannelConfig,避免重复配置。
|
|
17
18
|
* serviceUrl 从 .xiaoyienv 文件读取,skillId 写死在常量中。
|
|
19
|
+
*
|
|
20
|
+
* Accepts either ClawdbotConfig (legacy after_tool_call path) or
|
|
21
|
+
* XYChannelConfig (AgentToolResultMiddleware path). Config is cached
|
|
22
|
+
* after the first successful call so subsequent calls can omit the arg.
|
|
18
23
|
*/
|
|
19
|
-
export declare function getCsplConfig(cfg
|
|
24
|
+
export declare function getCsplConfig(cfg?: ClawdbotConfig): CsplConfig;
|
|
25
|
+
/**
|
|
26
|
+
* Initialize CSPL config from an already-resolved XYChannelConfig.
|
|
27
|
+
* Used by AgentToolResultMiddleware which has session context but not ClawdbotConfig.
|
|
28
|
+
*/
|
|
29
|
+
export declare function initCsplConfigFromXYConfig(xyConfig: XYChannelConfig): CsplConfig;
|
package/dist/src/cspl/config.js
CHANGED
|
@@ -27,10 +27,17 @@ function readServiceUrl() {
|
|
|
27
27
|
/**
|
|
28
28
|
* 构建 CSPL 配置。uid 和 apiKey 复用 XYChannelConfig,避免重复配置。
|
|
29
29
|
* serviceUrl 从 .xiaoyienv 文件读取,skillId 写死在常量中。
|
|
30
|
+
*
|
|
31
|
+
* Accepts either ClawdbotConfig (legacy after_tool_call path) or
|
|
32
|
+
* XYChannelConfig (AgentToolResultMiddleware path). Config is cached
|
|
33
|
+
* after the first successful call so subsequent calls can omit the arg.
|
|
30
34
|
*/
|
|
31
35
|
export function getCsplConfig(cfg) {
|
|
32
36
|
if (cachedConfig)
|
|
33
37
|
return cachedConfig;
|
|
38
|
+
if (!cfg) {
|
|
39
|
+
throw new Error("[SENTINEL HOOK] CSPL config not initialized: pass ClawdbotConfig on first call");
|
|
40
|
+
}
|
|
34
41
|
const xyConfig = resolveXYConfig(cfg);
|
|
35
42
|
const serviceUrl = readServiceUrl();
|
|
36
43
|
cachedConfig = {
|
|
@@ -48,3 +55,26 @@ export function getCsplConfig(cfg) {
|
|
|
48
55
|
logger.log("[SENTINEL HOOK] Config loaded (uid/apiKey from XYChannelConfig)");
|
|
49
56
|
return cachedConfig;
|
|
50
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Initialize CSPL config from an already-resolved XYChannelConfig.
|
|
60
|
+
* Used by AgentToolResultMiddleware which has session context but not ClawdbotConfig.
|
|
61
|
+
*/
|
|
62
|
+
export function initCsplConfigFromXYConfig(xyConfig) {
|
|
63
|
+
if (cachedConfig)
|
|
64
|
+
return cachedConfig;
|
|
65
|
+
const serviceUrl = readServiceUrl();
|
|
66
|
+
cachedConfig = {
|
|
67
|
+
api: {
|
|
68
|
+
url: `${serviceUrl}${API_URL_SUFFIX}`,
|
|
69
|
+
timeout: CSPL_STATIC_CONFIG.api.timeout,
|
|
70
|
+
},
|
|
71
|
+
uid: xyConfig.uid,
|
|
72
|
+
apiKey: xyConfig.apiKey,
|
|
73
|
+
skillId: CSPL_STATIC_CONFIG.skillId,
|
|
74
|
+
requestFrom: CSPL_STATIC_CONFIG.requestFrom,
|
|
75
|
+
textSource: CSPL_STATIC_CONFIG.textSource,
|
|
76
|
+
action: CSPL_STATIC_CONFIG.action,
|
|
77
|
+
};
|
|
78
|
+
logger.log("[SENTINEL HOOK] Config loaded via XYChannelConfig");
|
|
79
|
+
return cachedConfig;
|
|
80
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AgentToolResultMiddleware } from "openclaw/plugin-sdk/agent-harness-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Create the CSPL AgentToolResultMiddleware.
|
|
4
|
+
*
|
|
5
|
+
* Gets XYChannelConfig from session context (via sessionKey) to initialize
|
|
6
|
+
* the CSPL API config on first call, then caches it for subsequent calls.
|
|
7
|
+
*/
|
|
8
|
+
export declare function createCsplMiddleware(): AgentToolResultMiddleware;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// CSPL AgentToolResultMiddleware
|
|
2
|
+
// Replaces the after_tool_call hook with a middleware that intercepts tool results
|
|
3
|
+
// BEFORE they reach the LLM, enabling true security interruption.
|
|
4
|
+
import { callCsplApiWithConfig } from "./call-api.js";
|
|
5
|
+
import { getCsplConfig, initCsplConfigFromXYConfig } from "./config.js";
|
|
6
|
+
import { ALLOWED_TOOLS, MAX_TEXT_LENGTH, MAX_TOTAL_LENGTH, MIN_TEXT_LENGTH, STEER_ABORT_MESSAGE, } from "./constants.js";
|
|
7
|
+
import { parseSecurityResult, processText, validateAndTruncateText, } from "./utils.js";
|
|
8
|
+
import { getSessionContext } from "../tools/session-manager.js";
|
|
9
|
+
import { logger } from "../utils/logger.js";
|
|
10
|
+
/**
|
|
11
|
+
* Extract text content from an OpenClawAgentToolResult.
|
|
12
|
+
*/
|
|
13
|
+
function extractMiddlewareResultText(event) {
|
|
14
|
+
const result = event.result;
|
|
15
|
+
if (!result?.content || !Array.isArray(result.content)) {
|
|
16
|
+
return "";
|
|
17
|
+
}
|
|
18
|
+
const texts = [];
|
|
19
|
+
// Special handling for web_fetch: text is in details.text
|
|
20
|
+
if (event.toolName === "web_fetch" && result.details?.text) {
|
|
21
|
+
texts.push(String(result.details.text));
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
for (const item of result.content) {
|
|
25
|
+
if (item?.type === "text" && typeof item.text === "string") {
|
|
26
|
+
texts.push(item.text);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return texts.length > 0 ? texts.join("; ") : "";
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Create the CSPL AgentToolResultMiddleware.
|
|
34
|
+
*
|
|
35
|
+
* Gets XYChannelConfig from session context (via sessionKey) to initialize
|
|
36
|
+
* the CSPL API config on first call, then caches it for subsequent calls.
|
|
37
|
+
*/
|
|
38
|
+
export function createCsplMiddleware() {
|
|
39
|
+
return async (event, ctx) => {
|
|
40
|
+
if (!ALLOWED_TOOLS.includes(event.toolName)) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const resultText = extractMiddlewareResultText(event);
|
|
45
|
+
const resultLength = resultText.length;
|
|
46
|
+
if (resultLength <= MIN_TEXT_LENGTH || resultLength > MAX_TOTAL_LENGTH) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
logger.log(`[CSPL MIDDLEWARE] Scanning tool result: toolName=${event.toolName}, textLength=${resultLength}`);
|
|
50
|
+
// Build CSPL request payload
|
|
51
|
+
const questionText = {
|
|
52
|
+
subSceneID: "TOOL_OUTPUT",
|
|
53
|
+
tool: event.toolName,
|
|
54
|
+
output: [{ content: "" }],
|
|
55
|
+
};
|
|
56
|
+
const originText = processText(resultText);
|
|
57
|
+
questionText.output[0].content = originText;
|
|
58
|
+
let finalJson = JSON.stringify(questionText);
|
|
59
|
+
if (finalJson.length > MAX_TEXT_LENGTH) {
|
|
60
|
+
const diff = finalJson.length - MAX_TEXT_LENGTH;
|
|
61
|
+
const { text: trimmed } = validateAndTruncateText(originText, MAX_TEXT_LENGTH - diff);
|
|
62
|
+
questionText.output[0].content = trimmed;
|
|
63
|
+
finalJson = JSON.stringify(questionText);
|
|
64
|
+
}
|
|
65
|
+
// Get CSPL config (cached after first call)
|
|
66
|
+
// Try session context first (XYChannelConfig), then fall back to cached config
|
|
67
|
+
const sessionCtx = getSessionContext(ctx.sessionKey ?? "");
|
|
68
|
+
const csplConfig = sessionCtx
|
|
69
|
+
? initCsplConfigFromXYConfig(sessionCtx.config)
|
|
70
|
+
: getCsplConfig();
|
|
71
|
+
const csplStartTime = Date.now();
|
|
72
|
+
const response = await callCsplApiWithConfig(finalJson, csplConfig);
|
|
73
|
+
const csplElapsed = Date.now() - csplStartTime;
|
|
74
|
+
const result = parseSecurityResult(response);
|
|
75
|
+
logger.log(`[CSPL MIDDLEWARE] Security result: status=${result.status}, toolName=${event.toolName}, elapsed=${csplElapsed}ms`);
|
|
76
|
+
if (result.status === "REJECT") {
|
|
77
|
+
logger.log(`[CSPL MIDDLEWARE] REJECT - replacing tool result with security message`);
|
|
78
|
+
return {
|
|
79
|
+
result: {
|
|
80
|
+
content: [{ type: "text", text: STEER_ABORT_MESSAGE }],
|
|
81
|
+
details: {},
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
logger.error(`[CSPL MIDDLEWARE] Error: ${err}`);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk";
|
|
2
|
+
export declare function setCsplSteerContext(cfg: ClawdbotConfig, runtime: RuntimeEnv): void;
|
|
3
|
+
/**
|
|
4
|
+
* Inject a steer message into the given session by constructing a synthetic
|
|
5
|
+
* A2A message and dispatching it through handleXYMessage.
|
|
6
|
+
*/
|
|
7
|
+
export declare function injectCsplSteer(sessionId: string, taskId: string, message: string): Promise<boolean>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { handleXYMessage } from "../bot.js";
|
|
2
|
+
import { logger } from "../utils/logger.js";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
let cachedCfg = null;
|
|
5
|
+
let cachedRuntime = null;
|
|
6
|
+
export function setCsplSteerContext(cfg, runtime) {
|
|
7
|
+
cachedCfg = cfg;
|
|
8
|
+
cachedRuntime = runtime;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Inject a steer message into the given session by constructing a synthetic
|
|
12
|
+
* A2A message and dispatching it through handleXYMessage.
|
|
13
|
+
*/
|
|
14
|
+
export async function injectCsplSteer(sessionId, taskId, message) {
|
|
15
|
+
if (!cachedCfg || !cachedRuntime) {
|
|
16
|
+
logger.error("[CSPL STEER] No cached cfg/runtime, cannot inject steer");
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
const syntheticMessage = {
|
|
20
|
+
jsonrpc: "2.0",
|
|
21
|
+
method: "tasks/send",
|
|
22
|
+
id: `cspl-steer-${randomUUID()}`,
|
|
23
|
+
params: {
|
|
24
|
+
sessionId,
|
|
25
|
+
id: taskId,
|
|
26
|
+
agentLoginSessionId: "",
|
|
27
|
+
message: {
|
|
28
|
+
role: "user",
|
|
29
|
+
parts: [{ kind: "text", text: message }],
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
logger.log(`[CSPL STEER] Injecting steer for sessionId=${sessionId}, taskId=${taskId}`);
|
|
34
|
+
try {
|
|
35
|
+
await handleXYMessage({
|
|
36
|
+
cfg: cachedCfg,
|
|
37
|
+
runtime: cachedRuntime,
|
|
38
|
+
message: syntheticMessage,
|
|
39
|
+
accountId: "default",
|
|
40
|
+
});
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
logger.error(`[CSPL STEER] Failed to inject steer: ${err}`);
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import fetch from "node-fetch";
|
|
3
3
|
import fs from "fs/promises";
|
|
4
4
|
import path from "path";
|
|
5
|
+
import { logger } from "./utils/logger.js";
|
|
5
6
|
/**
|
|
6
7
|
* Download a file from URL to local path.
|
|
7
8
|
*/
|
|
@@ -19,10 +20,10 @@ export async function downloadFile(url, destPath) {
|
|
|
19
20
|
}
|
|
20
21
|
catch (error) {
|
|
21
22
|
if (error.name === 'AbortError') {
|
|
22
|
-
|
|
23
|
+
logger.error(`Download timeout (30s) for ${url}`);
|
|
23
24
|
throw new Error(`Download timeout after 30 seconds`);
|
|
24
25
|
}
|
|
25
|
-
|
|
26
|
+
logger.error(`Failed to download file from ${url}:`);
|
|
26
27
|
throw error;
|
|
27
28
|
}
|
|
28
29
|
finally {
|
|
@@ -51,7 +52,7 @@ export async function downloadFilesFromParts(fileParts, tempDir = "/tmp/xy_chann
|
|
|
51
52
|
});
|
|
52
53
|
}
|
|
53
54
|
catch (error) {
|
|
54
|
-
|
|
55
|
+
logger.error(`Failed to download file ${name}:`);
|
|
55
56
|
// Continue with other files
|
|
56
57
|
}
|
|
57
58
|
}
|
package/dist/src/file-upload.js
CHANGED
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
import fetch from "node-fetch";
|
|
4
4
|
import fs from "fs/promises";
|
|
5
5
|
import os from "os";
|
|
6
|
+
import { logger } from "./utils/logger.js";
|
|
6
7
|
import path from "path";
|
|
7
8
|
import { calculateSHA256 } from "./utils/crypto.js";
|
|
8
9
|
function isRemoteUrl(filePath) {
|
|
9
10
|
return filePath.startsWith("http://") || filePath.startsWith("https://");
|
|
10
11
|
}
|
|
11
12
|
async function downloadToTempFile(url) {
|
|
12
|
-
|
|
13
|
+
logger.log(`[XY File Upload] Downloading remote file: ${url}`);
|
|
13
14
|
const response = await fetch(url);
|
|
14
15
|
if (!response.ok) {
|
|
15
16
|
throw new Error(`Failed to download remote file: HTTP ${response.status}`);
|
|
@@ -18,7 +19,7 @@ async function downloadToTempFile(url) {
|
|
|
18
19
|
const urlFileName = path.basename(new URL(url).pathname) || "download";
|
|
19
20
|
const tempPath = path.join(os.tmpdir(), `xy-upload-${Date.now()}-${urlFileName}`);
|
|
20
21
|
await fs.writeFile(tempPath, buffer);
|
|
21
|
-
|
|
22
|
+
logger.log(`[XY File Upload] Downloaded to temp file: ${tempPath}`);
|
|
22
23
|
return tempPath;
|
|
23
24
|
}
|
|
24
25
|
/**
|
|
@@ -39,7 +40,7 @@ export class XYFileUploadService {
|
|
|
39
40
|
* Returns the objectId (as fileId) for use in A2A messages.
|
|
40
41
|
*/
|
|
41
42
|
async uploadFile(filePath, objectType = "TEMPORARY_MATERIAL_DOC") {
|
|
42
|
-
|
|
43
|
+
logger.log(`[XY File Upload] Starting file upload: ${filePath}`);
|
|
43
44
|
let localFilePath = filePath;
|
|
44
45
|
let isTempFile = false;
|
|
45
46
|
try {
|
|
@@ -54,7 +55,7 @@ export class XYFileUploadService {
|
|
|
54
55
|
const fileSha256 = calculateSHA256(fileBuffer);
|
|
55
56
|
const fileSize = fileBuffer.length;
|
|
56
57
|
// Phase 1: Prepare
|
|
57
|
-
|
|
58
|
+
logger.log(`[XY File Upload] Phase 1: Prepare upload for ${fileName}`);
|
|
58
59
|
const prepareResp = await fetch(`${this.baseUrl}/osms/v1/file/manager/prepare`, {
|
|
59
60
|
method: "POST",
|
|
60
61
|
headers: {
|
|
@@ -84,7 +85,7 @@ export class XYFileUploadService {
|
|
|
84
85
|
}
|
|
85
86
|
const { objectId, draftId, uploadInfos } = prepareData;
|
|
86
87
|
// Phase 2: Upload
|
|
87
|
-
|
|
88
|
+
logger.log(`[XY File Upload] Phase 2: Upload file data`);
|
|
88
89
|
const uploadInfo = uploadInfos[0]; // Single-part upload
|
|
89
90
|
const uploadResp = await fetch(uploadInfo.url, {
|
|
90
91
|
method: uploadInfo.method,
|
|
@@ -95,9 +96,9 @@ export class XYFileUploadService {
|
|
|
95
96
|
const uploadErrorText = await uploadResp.text();
|
|
96
97
|
throw new Error(`Upload failed: HTTP ${uploadResp.status}`);
|
|
97
98
|
}
|
|
98
|
-
|
|
99
|
+
logger.log(`[XY File Upload] Upload complete`);
|
|
99
100
|
// Phase 3: Complete
|
|
100
|
-
|
|
101
|
+
logger.log(`[XY File Upload] Phase 3: Complete upload`);
|
|
101
102
|
const completeResp = await fetch(`${this.baseUrl}/osms/v1/file/manager/complete`, {
|
|
102
103
|
method: "POST",
|
|
103
104
|
headers: {
|
|
@@ -115,11 +116,11 @@ export class XYFileUploadService {
|
|
|
115
116
|
throw new Error(`Complete failed: HTTP ${completeResp.status}`);
|
|
116
117
|
}
|
|
117
118
|
const completeData = await completeResp.json();
|
|
118
|
-
|
|
119
|
+
logger.log(`[XY File Upload] File upload successful: ${fileName} → objectId=${objectId}`);
|
|
119
120
|
return objectId;
|
|
120
121
|
}
|
|
121
122
|
catch (error) {
|
|
122
|
-
|
|
123
|
+
logger.error(`[XY File Upload] File upload failed for ${filePath}:`, error);
|
|
123
124
|
throw error;
|
|
124
125
|
}
|
|
125
126
|
finally {
|
|
@@ -150,7 +151,7 @@ export class XYFileUploadService {
|
|
|
150
151
|
const fileSha256 = calculateSHA256(fileBuffer);
|
|
151
152
|
const fileSize = fileBuffer.length;
|
|
152
153
|
// Phase 1: Prepare
|
|
153
|
-
|
|
154
|
+
logger.log(`[XY File Upload] Phase 1: Prepare upload for ${fileName}`);
|
|
154
155
|
const prepareResp = await fetch(`${this.baseUrl}/osms/v1/file/manager/prepare`, {
|
|
155
156
|
method: "POST",
|
|
156
157
|
headers: {
|
|
@@ -179,23 +180,23 @@ export class XYFileUploadService {
|
|
|
179
180
|
throw new Error(`Prepare failed: ${prepareData.desc}`);
|
|
180
181
|
}
|
|
181
182
|
const { objectId, draftId, uploadInfos } = prepareData;
|
|
182
|
-
|
|
183
|
+
logger.log(`[XY File Upload] Prepare complete: objectId=${objectId}, draftId=${draftId}`);
|
|
183
184
|
// Phase 2: Upload
|
|
184
|
-
|
|
185
|
+
logger.log(`[XY File Upload] Phase 2: Upload file data`);
|
|
185
186
|
const uploadInfo = uploadInfos[0]; // Single-part upload
|
|
186
187
|
const uploadResp = await fetch(uploadInfo.url, {
|
|
187
188
|
method: uploadInfo.method,
|
|
188
189
|
headers: uploadInfo.headers,
|
|
189
190
|
body: fileBuffer,
|
|
190
191
|
});
|
|
191
|
-
|
|
192
|
+
logger.log(`[XY File Upload] Upload response status: ${uploadResp.status}`);
|
|
192
193
|
if (!uploadResp.ok) {
|
|
193
194
|
const uploadErrorText = await uploadResp.text();
|
|
194
195
|
throw new Error(`Upload failed: HTTP ${uploadResp.status}`);
|
|
195
196
|
}
|
|
196
|
-
|
|
197
|
+
logger.log(`[XY File Upload] Upload complete`);
|
|
197
198
|
// Phase 3: CompleteAndQuery - get file URL
|
|
198
|
-
|
|
199
|
+
logger.log(`[XY File Upload] Phase 3: CompleteAndQuery to get file URL`);
|
|
199
200
|
const completeResp = await fetch(`${this.baseUrl}/osms/v1/file/manager/completeAndQuery`, {
|
|
200
201
|
method: "POST",
|
|
201
202
|
headers: {
|
|
@@ -218,11 +219,11 @@ export class XYFileUploadService {
|
|
|
218
219
|
if (!fileUrl) {
|
|
219
220
|
throw new Error("No file URL returned from completeAndQuery");
|
|
220
221
|
}
|
|
221
|
-
|
|
222
|
+
logger.log(`[XY File Upload] File upload successful`);
|
|
222
223
|
return fileUrl;
|
|
223
224
|
}
|
|
224
225
|
catch (error) {
|
|
225
|
-
|
|
226
|
+
logger.error(`[XY File Upload] File upload with URL retrieval failed for ${filePath}:`, error);
|
|
226
227
|
throw error;
|
|
227
228
|
}
|
|
228
229
|
finally {
|
|
@@ -249,7 +250,7 @@ export class XYFileUploadService {
|
|
|
249
250
|
});
|
|
250
251
|
}
|
|
251
252
|
catch (error) {
|
|
252
|
-
|
|
253
|
+
logger.error(`[XY File Upload] Failed to upload ${filePath}, skipping:`, error);
|
|
253
254
|
// Continue with other files
|
|
254
255
|
}
|
|
255
256
|
}
|