@ynhcj/xiaoyi-channel 0.0.129-beta → 0.0.129-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 +36 -7
- package/dist/src/bot.d.ts +8 -0
- package/dist/src/bot.js +151 -139
- package/dist/src/client.d.ts +1 -5
- package/dist/src/client.js +25 -39
- package/dist/src/cspl/call-api.d.ts +6 -0
- package/dist/src/cspl/call-api.js +33 -13
- 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 +21 -0
- package/dist/src/cspl/steer-context.js +78 -0
- package/dist/src/file-download.js +3 -3
- package/dist/src/formatter.d.ts +0 -2
- package/dist/src/formatter.js +12 -14
- package/dist/src/heartbeat.js +3 -2
- package/dist/src/login-token-handler.js +13 -10
- package/dist/src/message-queue.js +2 -1
- package/dist/src/monitor.js +82 -48
- package/dist/src/outbound.js +3 -0
- package/dist/src/provider.js +82 -34
- package/dist/src/push.js +9 -9
- package/dist/src/reply-dispatcher.d.ts +3 -1
- package/dist/src/reply-dispatcher.js +61 -68
- package/dist/src/self-evolution-handler.js +11 -14
- package/dist/src/skill-retriever/hooks.js +0 -1
- package/dist/src/skill-retriever/tool-search.js +7 -12
- 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 +0 -1
- 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 +3 -15
- 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 +3 -1
- package/dist/src/trigger-handler.js +8 -9
- package/dist/src/utils/logger.js +106 -22
- package/dist/src/utils/throw.d.ts +5 -0
- package/dist/src/utils/throw.js +10 -0
- package/dist/src/websocket.js +4 -2
- package/dist/src/xy-session-store.d.ts +79 -0
- package/dist/src/xy-session-store.js +153 -0
- package/openclaw.plugin.json +3 -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
|
/**
|
|
@@ -44,25 +35,23 @@ export function getXYWebSocketManager(config) {
|
|
|
44
35
|
* Disconnects the manager and removes it from the cache.
|
|
45
36
|
*/
|
|
46
37
|
export function removeXYWebSocketManager(config) {
|
|
47
|
-
const log = runtime?.log ?? console.log;
|
|
48
38
|
const cacheKey = `${config.apiKey}-${config.agentId}`;
|
|
49
39
|
const manager = wsManagerCache.get(cacheKey);
|
|
50
40
|
if (manager) {
|
|
51
|
-
log(`🗑️ [WS-MANAGER-CACHE] Removing manager from cache: ${cacheKey}`);
|
|
41
|
+
logger.log(`🗑️ [WS-MANAGER-CACHE] Removing manager from cache: ${cacheKey}`);
|
|
52
42
|
manager.disconnect();
|
|
53
43
|
wsManagerCache.delete(cacheKey);
|
|
54
|
-
log(`🗑️ [WS-MANAGER-CACHE] Manager removed, remaining managers: ${wsManagerCache.size}`);
|
|
44
|
+
logger.log(`🗑️ [WS-MANAGER-CACHE] Manager removed, remaining managers: ${wsManagerCache.size}`);
|
|
55
45
|
}
|
|
56
46
|
else {
|
|
57
|
-
log(`⚠️ [WS-MANAGER-CACHE] Manager not found in cache: ${cacheKey}`);
|
|
47
|
+
logger.log(`⚠️ [WS-MANAGER-CACHE] Manager not found in cache: ${cacheKey}`);
|
|
58
48
|
}
|
|
59
49
|
}
|
|
60
50
|
/**
|
|
61
51
|
* Clear all cached WebSocket managers.
|
|
62
52
|
*/
|
|
63
53
|
export function clearXYWebSocketManagers() {
|
|
64
|
-
|
|
65
|
-
log("Clearing all WebSocket manager caches");
|
|
54
|
+
logger.log("Clearing all WebSocket manager caches");
|
|
66
55
|
for (const manager of wsManagerCache.values()) {
|
|
67
56
|
manager.disconnect();
|
|
68
57
|
}
|
|
@@ -79,37 +68,35 @@ export function getCachedManagerCount() {
|
|
|
79
68
|
* Helps identify connection issues and orphan connections.
|
|
80
69
|
*/
|
|
81
70
|
export function diagnoseAllManagers() {
|
|
82
|
-
|
|
83
|
-
log(`Total cached managers: ${wsManagerCache.size}`);
|
|
71
|
+
logger.log(`Total cached managers: ${wsManagerCache.size}`);
|
|
84
72
|
if (wsManagerCache.size === 0) {
|
|
85
|
-
log("ℹ️ No managers in cache");
|
|
73
|
+
logger.log("ℹ️ No managers in cache");
|
|
86
74
|
return;
|
|
87
75
|
}
|
|
88
76
|
let orphanCount = 0;
|
|
89
77
|
wsManagerCache.forEach((manager, key) => {
|
|
90
78
|
const diag = manager.getConnectionDiagnostics();
|
|
91
|
-
log(` Total event listeners on manager: ${diag.totalEventListeners}`);
|
|
79
|
+
logger.log(` Total event listeners on manager: ${diag.totalEventListeners}`);
|
|
92
80
|
// Connection
|
|
93
|
-
log(` 🔌 Connection:`);
|
|
94
|
-
log(` - Exists: ${diag.connection.exists}`);
|
|
95
|
-
log(` - ReadyState: ${diag.connection.readyState}`);
|
|
96
|
-
log(` - State connected/ready: ${diag.connection.stateConnected}/${diag.connection.stateReady}`);
|
|
97
|
-
log(` - Reconnect attempts: ${diag.connection.reconnectAttempts}`);
|
|
98
|
-
log(` - Listeners on WebSocket: ${diag.connection.listenerCount}`);
|
|
99
|
-
log(` - Heartbeat active: ${diag.connection.heartbeatActive}`);
|
|
100
|
-
log(` - Has reconnect timer: ${diag.connection.hasReconnectTimer}`);
|
|
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}`);
|
|
101
89
|
if (diag.connection.isOrphan) {
|
|
102
|
-
log(` ⚠️ ORPHAN CONNECTION DETECTED!`);
|
|
90
|
+
logger.log(` ⚠️ ORPHAN CONNECTION DETECTED!`);
|
|
103
91
|
orphanCount++;
|
|
104
92
|
}
|
|
105
|
-
log("");
|
|
106
93
|
});
|
|
107
94
|
if (orphanCount > 0) {
|
|
108
|
-
log(`⚠️ Total orphan connections found: ${orphanCount}`);
|
|
109
|
-
log(`💡 Suggestion: These connections should be cleaned up`);
|
|
95
|
+
logger.log(`⚠️ Total orphan connections found: ${orphanCount}`);
|
|
96
|
+
logger.log(`💡 Suggestion: These connections should be cleaned up`);
|
|
110
97
|
}
|
|
111
98
|
else {
|
|
112
|
-
log(`✅ No orphan connections found`);
|
|
99
|
+
logger.log(`✅ No orphan connections found`);
|
|
113
100
|
}
|
|
114
101
|
}
|
|
115
102
|
/**
|
|
@@ -117,18 +104,17 @@ export function diagnoseAllManagers() {
|
|
|
117
104
|
* Returns the number of managers that had orphan connections.
|
|
118
105
|
*/
|
|
119
106
|
export function cleanupOrphanConnections() {
|
|
120
|
-
const log = runtime?.log ?? console.log;
|
|
121
107
|
let cleanedCount = 0;
|
|
122
108
|
wsManagerCache.forEach((manager, key) => {
|
|
123
109
|
const diag = manager.getConnectionDiagnostics();
|
|
124
110
|
if (diag.connection.isOrphan) {
|
|
125
|
-
log(`🧹 Cleaning up orphan connections in manager: ${key}`);
|
|
111
|
+
logger.log(`🧹 Cleaning up orphan connections in manager: ${key}`);
|
|
126
112
|
manager.disconnect();
|
|
127
113
|
cleanedCount++;
|
|
128
114
|
}
|
|
129
115
|
});
|
|
130
116
|
if (cleanedCount > 0) {
|
|
131
|
-
log(`🧹 Cleaned up ${cleanedCount} manager(s) with orphan connections`);
|
|
117
|
+
logger.log(`🧹 Cleaned up ${cleanedCount} manager(s) with orphan connections`);
|
|
132
118
|
}
|
|
133
119
|
return cleanedCount;
|
|
134
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,4 +1,5 @@
|
|
|
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";
|
|
@@ -43,18 +44,12 @@ function parseResponse(data) {
|
|
|
43
44
|
}
|
|
44
45
|
return json;
|
|
45
46
|
}
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
questionText,
|
|
51
|
-
textSource: config.textSource,
|
|
52
|
-
action: config.action,
|
|
53
|
-
extra: JSON.stringify({ userId: config.uid }),
|
|
54
|
-
};
|
|
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);
|
|
55
51
|
return new Promise((resolve, reject) => {
|
|
56
|
-
const
|
|
57
|
-
const req = https.request(options, (res) => {
|
|
52
|
+
const req = module.request(options, (res) => {
|
|
58
53
|
if (res.statusCode && res.statusCode >= HTTP_STATUS_BAD_REQUEST) {
|
|
59
54
|
reject(new Error(`[SENTINEL HOOK] HTTP error: ${res.statusCode}`));
|
|
60
55
|
return;
|
|
@@ -66,7 +61,7 @@ export async function callCsplApi(questionText, cfg) {
|
|
|
66
61
|
res.on("end", () => {
|
|
67
62
|
try {
|
|
68
63
|
const result = parseResponse(data);
|
|
69
|
-
logger.log(`[SENTINEL HOOK] ✅
|
|
64
|
+
logger.log(`[SENTINEL HOOK] ✅ 请求成功, securityResult=${result?.data?.securityResult ?? "N/A"}`);
|
|
70
65
|
resolve(result);
|
|
71
66
|
}
|
|
72
67
|
catch (e) {
|
|
@@ -80,7 +75,7 @@ export async function callCsplApi(questionText, cfg) {
|
|
|
80
75
|
reject(error);
|
|
81
76
|
});
|
|
82
77
|
req.on("timeout", () => {
|
|
83
|
-
logger.error(`[SENTINEL HOOK] ⏰ 请求超时 (${
|
|
78
|
+
logger.error(`[SENTINEL HOOK] ⏰ 请求超时 (${timeout}ms)`);
|
|
84
79
|
req.destroy();
|
|
85
80
|
reject(new Error("[SENTINEL HOOK] Request timeout"));
|
|
86
81
|
});
|
|
@@ -88,3 +83,28 @@ export async function callCsplApi(questionText, cfg) {
|
|
|
88
83
|
req.end();
|
|
89
84
|
});
|
|
90
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,21 @@
|
|
|
1
|
+
import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk";
|
|
2
|
+
/** Called from handleXYMessage on every inbound A2A message to keep cfg/runtime fresh. */
|
|
3
|
+
export declare function setCsplSteerContext(cfg: ClawdbotConfig, runtime: RuntimeEnv): void;
|
|
4
|
+
/** Parameters for steer message injection. */
|
|
5
|
+
export interface SteerInjectionParams {
|
|
6
|
+
sessionId: string;
|
|
7
|
+
taskId: string;
|
|
8
|
+
message: string;
|
|
9
|
+
/** Human-readable source label for logging (e.g. "cspl", "self-evolution"). */
|
|
10
|
+
source: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Inject a steer message into an active session by constructing a synthetic
|
|
14
|
+
* A2A tasks/send message and dispatching it through handleXYMessage.
|
|
15
|
+
*
|
|
16
|
+
* Uses skipRegistration so the steer message doesn't register a new taskId,
|
|
17
|
+
* increment session refCount, or send extra status updates.
|
|
18
|
+
*
|
|
19
|
+
* Returns true if the injection was dispatched successfully.
|
|
20
|
+
*/
|
|
21
|
+
export declare function tryInjectSteer(params: SteerInjectionParams): Promise<boolean>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { handleXYMessage } from "../bot.js";
|
|
2
|
+
import { logger } from "../utils/logger.js";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
// Use globalThis to ensure a single cache across all module copies.
|
|
5
|
+
// The xy_channel plugin may be loaded by openclaw from different module
|
|
6
|
+
// resolution paths, causing steer-context.ts to be instantiated multiple
|
|
7
|
+
// times. globalThis guarantees all copies share the same cfg/runtime.
|
|
8
|
+
const _g = globalThis;
|
|
9
|
+
if (!_g.__xySteerCachedCfg) {
|
|
10
|
+
_g.__xySteerCachedCfg = null;
|
|
11
|
+
}
|
|
12
|
+
if (!_g.__xySteerCachedRuntime) {
|
|
13
|
+
_g.__xySteerCachedRuntime = null;
|
|
14
|
+
}
|
|
15
|
+
function getCachedCfg() {
|
|
16
|
+
return _g.__xySteerCachedCfg;
|
|
17
|
+
}
|
|
18
|
+
function setCachedCfg(cfg) {
|
|
19
|
+
_g.__xySteerCachedCfg = cfg;
|
|
20
|
+
}
|
|
21
|
+
function getCachedRuntime() {
|
|
22
|
+
return _g.__xySteerCachedRuntime;
|
|
23
|
+
}
|
|
24
|
+
function setCachedRuntime(runtime) {
|
|
25
|
+
_g.__xySteerCachedRuntime = runtime;
|
|
26
|
+
}
|
|
27
|
+
/** Called from handleXYMessage on every inbound A2A message to keep cfg/runtime fresh. */
|
|
28
|
+
export function setCsplSteerContext(cfg, runtime) {
|
|
29
|
+
setCachedCfg(cfg);
|
|
30
|
+
setCachedRuntime(runtime);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Inject a steer message into an active session by constructing a synthetic
|
|
34
|
+
* A2A tasks/send message and dispatching it through handleXYMessage.
|
|
35
|
+
*
|
|
36
|
+
* Uses skipRegistration so the steer message doesn't register a new taskId,
|
|
37
|
+
* increment session refCount, or send extra status updates.
|
|
38
|
+
*
|
|
39
|
+
* Returns true if the injection was dispatched successfully.
|
|
40
|
+
*/
|
|
41
|
+
export async function tryInjectSteer(params) {
|
|
42
|
+
const { sessionId, taskId, message, source } = params;
|
|
43
|
+
const cfg = getCachedCfg();
|
|
44
|
+
const runtime = getCachedRuntime();
|
|
45
|
+
if (!cfg || !runtime) {
|
|
46
|
+
logger.error(`[STEER:${source}] No cached cfg/runtime, cannot inject steer`);
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
const syntheticMessage = {
|
|
50
|
+
jsonrpc: "2.0",
|
|
51
|
+
method: "tasks/send",
|
|
52
|
+
id: `steer-${source}-${randomUUID()}`,
|
|
53
|
+
params: {
|
|
54
|
+
sessionId,
|
|
55
|
+
id: taskId,
|
|
56
|
+
agentLoginSessionId: "",
|
|
57
|
+
message: {
|
|
58
|
+
role: "user",
|
|
59
|
+
parts: [{ kind: "text", text: message }],
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
logger.log(`[STEER:${source}] Injecting steer for sessionId=${sessionId}, taskId=${taskId}`);
|
|
64
|
+
try {
|
|
65
|
+
await handleXYMessage({
|
|
66
|
+
cfg,
|
|
67
|
+
runtime,
|
|
68
|
+
message: syntheticMessage,
|
|
69
|
+
accountId: "default",
|
|
70
|
+
skipRegistration: true,
|
|
71
|
+
});
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
logger.error(`[STEER:${source}] Failed to inject steer: ${err}`);
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -20,10 +20,10 @@ export async function downloadFile(url, destPath) {
|
|
|
20
20
|
}
|
|
21
21
|
catch (error) {
|
|
22
22
|
if (error.name === 'AbortError') {
|
|
23
|
-
logger.
|
|
23
|
+
logger.error(`Download timeout (30s) for ${url}`);
|
|
24
24
|
throw new Error(`Download timeout after 30 seconds`);
|
|
25
25
|
}
|
|
26
|
-
logger.
|
|
26
|
+
logger.error(`Failed to download file from ${url}:`);
|
|
27
27
|
throw error;
|
|
28
28
|
}
|
|
29
29
|
finally {
|
|
@@ -52,7 +52,7 @@ export async function downloadFilesFromParts(fileParts, tempDir = "/tmp/xy_chann
|
|
|
52
52
|
});
|
|
53
53
|
}
|
|
54
54
|
catch (error) {
|
|
55
|
-
logger.
|
|
55
|
+
logger.error(`Failed to download file ${name}:`);
|
|
56
56
|
// Continue with other files
|
|
57
57
|
}
|
|
58
58
|
}
|
package/dist/src/formatter.d.ts
CHANGED
|
@@ -17,7 +17,6 @@ export interface SendA2AResponseParams {
|
|
|
17
17
|
}>;
|
|
18
18
|
errorCode?: number | string;
|
|
19
19
|
errorMessage?: string;
|
|
20
|
-
runtime?: any;
|
|
21
20
|
}
|
|
22
21
|
/**
|
|
23
22
|
* Send an A2A artifact update response.
|
|
@@ -50,7 +49,6 @@ export interface SendStatusUpdateParams {
|
|
|
50
49
|
messageId: string;
|
|
51
50
|
text: string;
|
|
52
51
|
state: "submitted" | "working" | "input-required" | "completed" | "canceled" | "failed" | "unknown";
|
|
53
|
-
runtime?: any;
|
|
54
52
|
}
|
|
55
53
|
/**
|
|
56
54
|
* Send an A2A task status update.
|
package/dist/src/formatter.js
CHANGED
|
@@ -7,8 +7,7 @@ import { getCurrentTaskId, getCurrentMessageId } from "./task-manager.js";
|
|
|
7
7
|
* Send an A2A artifact update response.
|
|
8
8
|
*/
|
|
9
9
|
export async function sendA2AResponse(params) {
|
|
10
|
-
const { config, sessionId, taskId, messageId, text, append, final, files, errorCode, errorMessage
|
|
11
|
-
const log = runtime?.log ?? console.log;
|
|
10
|
+
const { config, sessionId, taskId, messageId, text, append, final, files, errorCode, errorMessage } = params;
|
|
12
11
|
// Build artifact update event
|
|
13
12
|
const artifact = {
|
|
14
13
|
taskId,
|
|
@@ -47,7 +46,7 @@ export async function sendA2AResponse(params) {
|
|
|
47
46
|
code: errorCode,
|
|
48
47
|
message: errorMessage ?? "任务执行异常,请重试",
|
|
49
48
|
};
|
|
50
|
-
log(`[A2A_RESPONSE] ⚠️ Including error code: ${errorCode}`);
|
|
49
|
+
logger.log(`[A2A_RESPONSE] ⚠️ Including error code: ${errorCode}`);
|
|
51
50
|
}
|
|
52
51
|
// Send via WebSocket
|
|
53
52
|
const wsManager = getXYWebSocketManager(config);
|
|
@@ -59,13 +58,13 @@ export async function sendA2AResponse(params) {
|
|
|
59
58
|
msgDetail: JSON.stringify(jsonRpcResponse),
|
|
60
59
|
};
|
|
61
60
|
// 📋 Log complete response body
|
|
62
|
-
log(`[A2A_RESPONSE] 📤 Sending A2A artifact-update response: taskId: ${taskId}`);
|
|
63
|
-
log(`[A2A_RESPONSE] - append: ${append}`);
|
|
64
|
-
log(`[A2A_RESPONSE] - final: ${final}`);
|
|
65
|
-
log(`[A2A_RESPONSE] - text: ${text.length <= 10 ? text : text.slice(0, 5) + '***' + text.slice(-5)}`);
|
|
66
|
-
log(`[A2A_RESPONSE] - files count: ${files?.length ?? 0}`);
|
|
61
|
+
logger.log(`[A2A_RESPONSE] 📤 Sending A2A artifact-update response: taskId: ${taskId}`);
|
|
62
|
+
logger.log(`[A2A_RESPONSE] - append: ${append}`);
|
|
63
|
+
logger.log(`[A2A_RESPONSE] - final: ${final}`);
|
|
64
|
+
logger.log(`[A2A_RESPONSE] - text: ${text.length <= 10 ? text : text.slice(0, 5) + '***' + text.slice(-5)}`);
|
|
65
|
+
logger.log(`[A2A_RESPONSE] - files count: ${files?.length ?? 0}`);
|
|
67
66
|
await wsManager.sendMessage(sessionId, outboundMessage);
|
|
68
|
-
log(`[A2A_RESPONSE] ✅ Message sent successfully`);
|
|
67
|
+
logger.log(`[A2A_RESPONSE] ✅ Message sent successfully`);
|
|
69
68
|
}
|
|
70
69
|
/**
|
|
71
70
|
* Send an A2A artifact-update with reasoningText part.
|
|
@@ -110,8 +109,7 @@ export async function sendReasoningTextUpdate(params) {
|
|
|
110
109
|
* Follows A2A protocol standard format with nested status object.
|
|
111
110
|
*/
|
|
112
111
|
export async function sendStatusUpdate(params) {
|
|
113
|
-
const { config, sessionId, taskId, messageId, text, state
|
|
114
|
-
const log = runtime?.log ?? console.log;
|
|
112
|
+
const { config, sessionId, taskId, messageId, text, state } = params;
|
|
115
113
|
// Dynamic lookup: use latest taskId/messageId from task-manager (handles steer/interrupt),
|
|
116
114
|
// fall back to closure-captured values
|
|
117
115
|
const currentTaskId = getCurrentTaskId(sessionId) ?? taskId;
|
|
@@ -150,9 +148,9 @@ export async function sendStatusUpdate(params) {
|
|
|
150
148
|
msgDetail: JSON.stringify(jsonRpcResponse),
|
|
151
149
|
};
|
|
152
150
|
// 📋 Log complete response body
|
|
153
|
-
log(`[A2A_STATUS] 📤 Sending A2A status-update:`);
|
|
154
|
-
log(`[A2A_STATUS] - taskId: ${currentTaskId}`);
|
|
155
|
-
log(`[A2A_STATUS] - text: "${text}"`);
|
|
151
|
+
logger.log(`[A2A_STATUS] 📤 Sending A2A status-update:`);
|
|
152
|
+
logger.log(`[A2A_STATUS] - taskId: ${currentTaskId}`);
|
|
153
|
+
logger.log(`[A2A_STATUS] - text: "${text}"`);
|
|
156
154
|
await wsManager.sendMessage(sessionId, outboundMessage);
|
|
157
155
|
}
|
|
158
156
|
/**
|
package/dist/src/heartbeat.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Heartbeat management for WebSocket connections
|
|
2
2
|
import WebSocket from "ws";
|
|
3
|
+
import { logger } from "./utils/logger.js";
|
|
3
4
|
/**
|
|
4
5
|
* Manages heartbeat for a WebSocket connection.
|
|
5
6
|
* Supports both application-level (30s) and protocol-level (20s) heartbeats.
|
|
@@ -23,8 +24,8 @@ export class HeartbeatManager {
|
|
|
23
24
|
this.onTimeout = onTimeout;
|
|
24
25
|
this.serverName = serverName;
|
|
25
26
|
this.onHeartbeatSuccess = onHeartbeatSuccess;
|
|
26
|
-
this.log = logFn ??
|
|
27
|
-
this.error = errorFn ??
|
|
27
|
+
this.log = logFn ?? ((msg, ...args) => logger.log(msg, ...args));
|
|
28
|
+
this.error = errorFn ?? ((msg, ...args) => logger.error(msg, ...args));
|
|
28
29
|
}
|
|
29
30
|
/**
|
|
30
31
|
* Start heartbeat monitoring.
|