@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.
Files changed (76) hide show
  1. package/dist/index.js +36 -7
  2. package/dist/src/bot.d.ts +8 -0
  3. package/dist/src/bot.js +151 -139
  4. package/dist/src/client.d.ts +1 -5
  5. package/dist/src/client.js +25 -39
  6. package/dist/src/cspl/call-api.d.ts +6 -0
  7. package/dist/src/cspl/call-api.js +33 -13
  8. package/dist/src/cspl/config.d.ts +11 -1
  9. package/dist/src/cspl/config.js +30 -0
  10. package/dist/src/cspl/middleware.d.ts +8 -0
  11. package/dist/src/cspl/middleware.js +90 -0
  12. package/dist/src/cspl/steer-context.d.ts +21 -0
  13. package/dist/src/cspl/steer-context.js +78 -0
  14. package/dist/src/file-download.js +3 -3
  15. package/dist/src/formatter.d.ts +0 -2
  16. package/dist/src/formatter.js +12 -14
  17. package/dist/src/heartbeat.js +3 -2
  18. package/dist/src/login-token-handler.js +13 -10
  19. package/dist/src/message-queue.js +2 -1
  20. package/dist/src/monitor.js +82 -48
  21. package/dist/src/outbound.js +3 -0
  22. package/dist/src/provider.js +82 -34
  23. package/dist/src/push.js +9 -9
  24. package/dist/src/reply-dispatcher.d.ts +3 -1
  25. package/dist/src/reply-dispatcher.js +61 -68
  26. package/dist/src/self-evolution-handler.js +11 -14
  27. package/dist/src/skill-retriever/hooks.js +0 -1
  28. package/dist/src/skill-retriever/tool-search.js +7 -12
  29. package/dist/src/task-manager.d.ts +4 -27
  30. package/dist/src/task-manager.js +13 -78
  31. package/dist/src/tools/calendar-tool.js +5 -1
  32. package/dist/src/tools/call-phone-tool.js +5 -1
  33. package/dist/src/tools/create-alarm-tool.js +5 -1
  34. package/dist/src/tools/delete-alarm-tool.js +5 -1
  35. package/dist/src/tools/image-reading-tool.d.ts +1 -1
  36. package/dist/src/tools/image-reading-tool.js +38 -114
  37. package/dist/src/tools/location-tool.js +5 -1
  38. package/dist/src/tools/login-token-tool.js +13 -2
  39. package/dist/src/tools/modify-alarm-tool.js +5 -1
  40. package/dist/src/tools/modify-note-tool.js +5 -1
  41. package/dist/src/tools/note-tool.js +5 -1
  42. package/dist/src/tools/query-app-message-tool.js +5 -1
  43. package/dist/src/tools/query-memory-data-tool.js +5 -1
  44. package/dist/src/tools/query-todo-task-tool.js +5 -1
  45. package/dist/src/tools/save-file-to-phone-tool.js +5 -1
  46. package/dist/src/tools/save-media-to-gallery-tool.js +5 -1
  47. package/dist/src/tools/search-alarm-tool.js +5 -1
  48. package/dist/src/tools/search-calendar-tool.js +5 -1
  49. package/dist/src/tools/search-contact-tool.js +5 -1
  50. package/dist/src/tools/search-email-tool.js +5 -1
  51. package/dist/src/tools/search-file-tool.js +5 -1
  52. package/dist/src/tools/search-message-tool.js +5 -1
  53. package/dist/src/tools/search-note-tool.js +5 -1
  54. package/dist/src/tools/search-photo-gallery-tool.js +5 -1
  55. package/dist/src/tools/send-email-tool.js +5 -1
  56. package/dist/src/tools/send-file-to-user-tool.js +0 -1
  57. package/dist/src/tools/send-message-tool.js +5 -1
  58. package/dist/src/tools/session-helper.d.ts +24 -0
  59. package/dist/src/tools/session-helper.js +45 -0
  60. package/dist/src/tools/session-manager.d.ts +8 -0
  61. package/dist/src/tools/session-manager.js +3 -15
  62. package/dist/src/tools/upload-file-tool.js +5 -1
  63. package/dist/src/tools/upload-photo-tool.js +5 -1
  64. package/dist/src/tools/xiaoyi-add-collection-tool.js +5 -1
  65. package/dist/src/tools/xiaoyi-collection-tool.js +5 -1
  66. package/dist/src/tools/xiaoyi-delete-collection-tool.js +5 -1
  67. package/dist/src/tools/xiaoyi-gui-tool.js +3 -1
  68. package/dist/src/trigger-handler.js +8 -9
  69. package/dist/src/utils/logger.js +106 -22
  70. package/dist/src/utils/throw.d.ts +5 -0
  71. package/dist/src/utils/throw.js +10 -0
  72. package/dist/src/websocket.js +4 -2
  73. package/dist/src/xy-session-store.d.ts +79 -0
  74. package/dist/src/xy-session-store.js +153 -0
  75. package/openclaw.plugin.json +3 -0
  76. package/package.json +6 -5
@@ -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
- // Runtime reference for logging
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
- const log = runtime?.log ?? console.log;
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
- const log = runtime?.log ?? console.log;
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
- const log = runtime?.log ?? console.log;
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
- export async function callCsplApi(questionText, cfg) {
47
- const config = getCsplConfig(cfg);
48
- const headers = buildHeaders(config);
49
- const payload = {
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 options = buildRequestOptions(config.api.url, headers, config.api.timeout);
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] ⏰ 请求超时 (${config.api.timeout}ms)`);
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: ClawdbotConfig): CsplConfig;
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;
@@ -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.log(`Download timeout (30s) for ${url}`);
23
+ logger.error(`Download timeout (30s) for ${url}`);
24
24
  throw new Error(`Download timeout after 30 seconds`);
25
25
  }
26
- logger.log(`Failed to download file from ${url}:`);
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.log(`Failed to download file ${name}:`);
55
+ logger.error(`Failed to download file ${name}:`);
56
56
  // Continue with other files
57
57
  }
58
58
  }
@@ -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.
@@ -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, runtime } = params;
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, runtime } = params;
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
  /**
@@ -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 ?? console.log;
27
- this.error = errorFn ?? console.error;
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.