@ynhcj/xiaoyi-channel 0.0.178-beta → 0.0.178-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 (148) hide show
  1. package/dist/index.js +141 -1
  2. package/dist/src/acp-session-binding.d.ts +37 -0
  3. package/dist/src/acp-session-binding.js +237 -0
  4. package/dist/src/bot.js +50 -33
  5. package/dist/src/channel.js +59 -64
  6. package/dist/src/client.d.ts +5 -0
  7. package/dist/src/client.js +10 -0
  8. package/dist/src/cron-command.d.ts +2 -0
  9. package/dist/src/cron-command.js +14 -8
  10. package/dist/src/cron-query-handler.js +45 -8
  11. package/dist/src/cspl/call_api.js +4 -2
  12. package/dist/src/cspl/sentinel_hook.js +9 -8
  13. package/dist/src/cspl/upload_file.js +2 -2
  14. package/dist/src/formatter.js +66 -28
  15. package/dist/src/log-reporter/config-loader.d.ts +11 -0
  16. package/dist/src/log-reporter/config-loader.js +68 -0
  17. package/dist/src/log-reporter/cursor-store.d.ts +5 -0
  18. package/dist/src/log-reporter/cursor-store.js +26 -0
  19. package/dist/src/log-reporter/index.d.ts +10 -0
  20. package/dist/src/log-reporter/index.js +77 -0
  21. package/dist/src/log-reporter/reporter.d.ts +6 -0
  22. package/dist/src/log-reporter/reporter.js +17 -0
  23. package/dist/src/log-reporter/scanner.d.ts +6 -0
  24. package/dist/src/log-reporter/scanner.js +82 -0
  25. package/dist/src/log-reporter/types.d.ts +59 -0
  26. package/dist/src/log-reporter/types.js +2 -0
  27. package/dist/src/log-reporter/uploader.d.ts +6 -0
  28. package/dist/src/log-reporter/uploader.js +32 -0
  29. package/dist/src/memory-query-handler.d.ts +1 -0
  30. package/dist/src/memory-query-handler.js +283 -0
  31. package/dist/src/monitor.js +29 -9
  32. package/dist/src/parser.d.ts +12 -0
  33. package/dist/src/parser.js +39 -13
  34. package/dist/src/provider.js +65 -67
  35. package/dist/src/reply-dispatcher.d.ts +3 -0
  36. package/dist/src/reply-dispatcher.js +149 -124
  37. package/dist/src/task-manager.d.ts +5 -6
  38. package/dist/src/task-manager.js +5 -9
  39. package/dist/src/tools/agent-as-skill-tool.d.ts +41 -2
  40. package/dist/src/tools/agent-as-skill-tool.js +144 -151
  41. package/dist/src/tools/calendar-tool.d.ts +24 -2
  42. package/dist/src/tools/calendar-tool.js +115 -117
  43. package/dist/src/tools/call-device-tool.d.ts +20 -6
  44. package/dist/src/tools/call-device-tool.js +116 -138
  45. package/dist/src/tools/call-phone-tool.d.ts +21 -2
  46. package/dist/src/tools/call-phone-tool.js +112 -114
  47. package/dist/src/tools/check-plugin-privilege-tool.d.ts +16 -2
  48. package/dist/src/tools/check-plugin-privilege-tool.js +141 -143
  49. package/dist/src/tools/create-alarm-tool.d.ts +39 -2
  50. package/dist/src/tools/create-alarm-tool.js +229 -231
  51. package/dist/src/tools/create-all-tools.js +8 -4
  52. package/dist/src/tools/delete-alarm-tool.d.ts +15 -2
  53. package/dist/src/tools/delete-alarm-tool.js +134 -136
  54. package/dist/src/tools/device-tool-map.d.ts +1 -1
  55. package/dist/src/tools/device-tool-map.js +9 -2
  56. package/dist/src/tools/discover-cross-devices-tool.d.ts +16 -2
  57. package/dist/src/tools/discover-cross-devices-tool.js +121 -124
  58. package/dist/src/tools/display-a2ui-card-bypath-tool.d.ts +21 -0
  59. package/dist/src/tools/display-a2ui-card-bypath-tool.js +65 -0
  60. package/dist/src/tools/get-alarm-tool-schema.d.ts +1 -2
  61. package/dist/src/tools/get-alarm-tool-schema.js +10 -16
  62. package/dist/src/tools/get-calendar-tool-schema.d.ts +1 -2
  63. package/dist/src/tools/get-calendar-tool-schema.js +8 -12
  64. package/dist/src/tools/get-collection-tool-schema.d.ts +1 -2
  65. package/dist/src/tools/get-collection-tool-schema.js +9 -11
  66. package/dist/src/tools/get-contact-tool-schema.d.ts +1 -2
  67. package/dist/src/tools/get-contact-tool-schema.js +10 -16
  68. package/dist/src/tools/get-device-file-tool-schema.d.ts +1 -2
  69. package/dist/src/tools/get-device-file-tool-schema.js +9 -13
  70. package/dist/src/tools/get-email-tool-schema.d.ts +1 -2
  71. package/dist/src/tools/get-email-tool-schema.js +8 -11
  72. package/dist/src/tools/get-note-tool-schema.d.ts +1 -2
  73. package/dist/src/tools/get-note-tool-schema.js +9 -14
  74. package/dist/src/tools/get-photo-tool-schema.d.ts +1 -2
  75. package/dist/src/tools/get-photo-tool-schema.js +9 -12
  76. package/dist/src/tools/image-reading-tool.d.ts +28 -2
  77. package/dist/src/tools/image-reading-tool.js +76 -76
  78. package/dist/src/tools/location-tool.d.ts +11 -2
  79. package/dist/src/tools/location-tool.js +93 -92
  80. package/dist/src/tools/login-token-tool.d.ts +20 -2
  81. package/dist/src/tools/login-token-tool.js +122 -125
  82. package/dist/src/tools/modify-alarm-tool.d.ts +47 -2
  83. package/dist/src/tools/modify-alarm-tool.js +252 -237
  84. package/dist/src/tools/modify-note-tool.d.ts +20 -2
  85. package/dist/src/tools/modify-note-tool.js +107 -109
  86. package/dist/src/tools/note-tool.d.ts +20 -2
  87. package/dist/src/tools/note-tool.js +106 -108
  88. package/dist/src/tools/query-app-message-tool.d.ts +28 -2
  89. package/dist/src/tools/query-app-message-tool.js +111 -113
  90. package/dist/src/tools/query-memory-data-tool.d.ts +28 -2
  91. package/dist/src/tools/query-memory-data-tool.js +112 -114
  92. package/dist/src/tools/query-todo-task-tool.d.ts +24 -2
  93. package/dist/src/tools/query-todo-task-tool.js +106 -108
  94. package/dist/src/tools/save-file-to-phone-tool.d.ts +24 -2
  95. package/dist/src/tools/save-file-to-phone-tool.js +130 -132
  96. package/dist/src/tools/save-media-to-gallery-tool.d.ts +24 -2
  97. package/dist/src/tools/save-media-to-gallery-tool.js +137 -139
  98. package/dist/src/tools/save-self-evolution-skill-tool.d.ts +54 -2
  99. package/dist/src/tools/save-self-evolution-skill-tool.js +194 -194
  100. package/dist/src/tools/search-alarm-tool.d.ts +34 -2
  101. package/dist/src/tools/search-alarm-tool.js +174 -176
  102. package/dist/src/tools/search-calendar-tool.d.ts +24 -2
  103. package/dist/src/tools/search-calendar-tool.js +148 -150
  104. package/dist/src/tools/search-contact-tool.d.ts +16 -2
  105. package/dist/src/tools/search-contact-tool.js +101 -103
  106. package/dist/src/tools/search-email-tool.d.ts +21 -2
  107. package/dist/src/tools/search-email-tool.js +110 -112
  108. package/dist/src/tools/search-file-tool.d.ts +16 -2
  109. package/dist/src/tools/search-file-tool.js +104 -106
  110. package/dist/src/tools/search-message-tool.d.ts +16 -2
  111. package/dist/src/tools/search-message-tool.js +103 -105
  112. package/dist/src/tools/search-note-tool.d.ts +16 -2
  113. package/dist/src/tools/search-note-tool.js +98 -100
  114. package/dist/src/tools/search-photo-gallery-tool.d.ts +21 -2
  115. package/dist/src/tools/search-photo-gallery-tool.js +35 -37
  116. package/dist/src/tools/send-cross-device-task-tool.d.ts +35 -2
  117. package/dist/src/tools/send-cross-device-task-tool.js +214 -150
  118. package/dist/src/tools/send-email-tool.d.ts +24 -2
  119. package/dist/src/tools/send-email-tool.js +108 -110
  120. package/dist/src/tools/send-file-to-user-tool.d.ts +20 -2
  121. package/dist/src/tools/send-file-to-user-tool.js +172 -178
  122. package/dist/src/tools/send-html-card-tool.d.ts +20 -2
  123. package/dist/src/tools/send-html-card-tool.js +85 -87
  124. package/dist/src/tools/send-message-tool.d.ts +20 -2
  125. package/dist/src/tools/send-message-tool.js +122 -124
  126. package/dist/src/tools/session-manager.d.ts +19 -52
  127. package/dist/src/tools/session-manager.js +95 -239
  128. package/dist/src/tools/upload-file-tool.d.ts +20 -2
  129. package/dist/src/tools/upload-file-tool.js +80 -82
  130. package/dist/src/tools/upload-photo-tool.d.ts +20 -2
  131. package/dist/src/tools/upload-photo-tool.js +68 -70
  132. package/dist/src/tools/xiaoyi-add-collection-tool.d.ts +32 -2
  133. package/dist/src/tools/xiaoyi-add-collection-tool.js +146 -148
  134. package/dist/src/tools/xiaoyi-collection-tool.d.ts +20 -2
  135. package/dist/src/tools/xiaoyi-collection-tool.js +114 -116
  136. package/dist/src/tools/xiaoyi-delete-collection-tool.d.ts +15 -2
  137. package/dist/src/tools/xiaoyi-delete-collection-tool.js +127 -129
  138. package/dist/src/tools/xiaoyi-gui-tool.d.ts +16 -2
  139. package/dist/src/tools/xiaoyi-gui-tool.js +92 -95
  140. package/dist/src/types.d.ts +6 -6
  141. package/dist/src/utils/config-manager.d.ts +3 -2
  142. package/dist/src/utils/config-manager.js +22 -2
  143. package/dist/src/utils/cron-push-map.d.ts +26 -0
  144. package/dist/src/utils/cron-push-map.js +131 -0
  145. package/dist/src/utils/logger.js +3 -14
  146. package/dist/src/websocket.d.ts +1 -1
  147. package/dist/src/websocket.js +29 -13
  148. package/package.json +1 -1
@@ -2,15 +2,55 @@ import { resolveXYConfig, listXYAccountIds, getDefaultXYAccountId } from "./conf
2
2
  import { xyConfigSchema } from "./config-schema.js";
3
3
  import { xyOutbound } from "./outbound.js";
4
4
  import { filterToolsByDevice } from "./tools/device-tool-map.js";
5
- import { getCurrentSessionContext, registerSession } from "./tools/session-manager.js";
6
- import { createAllTools } from "./tools/create-all-tools.js";
5
+ import { getCurrentSessionContext } from "./tools/session-manager.js";
7
6
  import { logger } from "./utils/logger.js";
8
- /**
9
- * Prefix used for synthetic sessionIds created during cron-triggered tool
10
- * execution. `sendCommand()` checks this prefix to route commands through
11
- * the push channel instead of the (non-existent) WebSocket session.
12
- */
13
- const CRON_SESSION_PREFIX = "cron-";
7
+ // Static tool imports (3.24 pattern)
8
+ import { locationTool } from "./tools/location-tool.js";
9
+ import { xiaoyiGuiTool } from "./tools/xiaoyi-gui-tool.js";
10
+ import { sendFileToUserTool } from "./tools/send-file-to-user-tool.js";
11
+ import { sendHtmlCardTool } from "./tools/send-html-card-tool.js";
12
+ import { viewPushResultTool } from "./tools/view-push-result-tool.js";
13
+ import { imageReadingTool } from "./tools/image-reading-tool.js";
14
+ import { timestampToUtc8Tool } from "./tools/timestamp-to-utc8-tool.js";
15
+ import { saveSelfEvolutionSkillTool } from "./tools/save-self-evolution-skill-tool.js";
16
+ import { callDeviceTool } from "./tools/call-device-tool.js";
17
+ import { getNoteToolSchemaTool } from "./tools/get-note-tool-schema.js";
18
+ import { getCalendarToolSchemaTool } from "./tools/get-calendar-tool-schema.js";
19
+ import { getContactToolSchemaTool } from "./tools/get-contact-tool-schema.js";
20
+ import { getPhotoToolSchemaTool } from "./tools/get-photo-tool-schema.js";
21
+ import { getDeviceFileToolSchemaTool } from "./tools/get-device-file-tool-schema.js";
22
+ import { getAlarmToolSchemaTool } from "./tools/get-alarm-tool-schema.js";
23
+ import { getCollectionToolSchemaTool } from "./tools/get-collection-tool-schema.js";
24
+ import { loginTokenTool } from "./tools/login-token-tool.js";
25
+ import { agentAsSkillTool } from "./tools/agent-as-skill-tool.js";
26
+ import { discoverCrossDevicesTool } from "./tools/discover-cross-devices-tool.js";
27
+ import { sendCrossDeviceTaskTool } from "./tools/send-cross-device-task-tool.js";
28
+ import { displayA2UICardByPathTool } from "./tools/display-a2ui-card-bypath-tool.js";
29
+ import { checkPluginPrivilegeTool } from "./tools/check-plugin-privilege-tool.js";
30
+ const ALL_TOOLS = [
31
+ locationTool,
32
+ discoverCrossDevicesTool,
33
+ sendCrossDeviceTaskTool,
34
+ displayA2UICardByPathTool,
35
+ callDeviceTool,
36
+ getNoteToolSchemaTool,
37
+ getCalendarToolSchemaTool,
38
+ getContactToolSchemaTool,
39
+ getPhotoToolSchemaTool,
40
+ xiaoyiGuiTool,
41
+ getDeviceFileToolSchemaTool,
42
+ getAlarmToolSchemaTool,
43
+ getCollectionToolSchemaTool,
44
+ sendFileToUserTool,
45
+ sendHtmlCardTool,
46
+ viewPushResultTool,
47
+ imageReadingTool,
48
+ timestampToUtc8Tool,
49
+ saveSelfEvolutionSkillTool,
50
+ loginTokenTool,
51
+ agentAsSkillTool,
52
+ checkPluginPrivilegeTool,
53
+ ];
14
54
  /**
15
55
  * Xiaoyi Channel Plugin for OpenClaw.
16
56
  * Implements Xiaoyi A2A protocol with dual WebSocket connections.
@@ -32,7 +72,7 @@ export const xyPlugin = {
32
72
  ],
33
73
  },
34
74
  capabilities: {
35
- chatTypes: ["direct"], // Only private chat (no group support)
75
+ chatTypes: ["direct"],
36
76
  polls: false,
37
77
  threads: false,
38
78
  media: true,
@@ -49,59 +89,11 @@ export const xyPlugin = {
49
89
  schema: xyConfigSchema,
50
90
  },
51
91
  outbound: xyOutbound,
52
- /**
53
- * Provide channel-specific agent tools.
54
- *
55
- * Two execution contexts are supported:
56
- *
57
- * 1. **Normal (WebSocket) session** – `getCurrentSessionContext()` returns
58
- * a context that was registered by bot.ts during message processing.
59
- * Tools send commands through the WebSocket and listen for responses.
60
- *
61
- * 2. **Cron / scheduled-task session** – openclaw's cron runner calls
62
- * `agentTools({ cfg })` without an active WebSocket session. When no
63
- * session context exists but `cfg` is provided, we create a synthetic
64
- * "cron session" with `isCron: true` and a `cron-`-prefixed sessionId.
65
- * `sendCommand()` detects this prefix and routes commands through the
66
- * push channel. Response listening (WebSocket events) works unchanged
67
- * because the gateway WebSocket connection is always active.
68
- */
69
- agentTools: (params) => {
70
- let ctx = getCurrentSessionContext();
71
- // ── Cron / non-session fallback ──────────────────────────────
72
- // When no active xy WebSocket session exists but the openclaw cfg
73
- // is provided (framework calls agentTools({ cfg })), create a
74
- // synthetic "cron session". This enables cron-triggered agent
75
- // turns and cross-channel tool calls to use xiaoyi tools via the
76
- // push channel. sendCommand() detects the "cron-" sessionId
77
- // prefix and routes commands through push instead of WebSocket.
78
- if (!ctx && params?.cfg) {
79
- try {
80
- const config = resolveXYConfig(params.cfg);
81
- const cronId = `${CRON_SESSION_PREFIX}${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
82
- ctx = {
83
- config,
84
- sessionId: cronId,
85
- taskId: cronId,
86
- messageId: cronId,
87
- agentId: "default",
88
- isCron: true,
89
- };
90
- // Register so getCurrentSessionContext() fallback can find it
91
- registerSession(`__cron__${cronId}`, ctx);
92
- logger.log(`[CRON-TOOLS] Created cron session context: ${cronId}`);
93
- }
94
- catch (err) {
95
- logger.error("[CRON-TOOLS] Failed to create cron context:", err);
96
- }
97
- }
98
- if (!ctx) {
99
- logger.log("[CREATE-ALL-TOOLS] no session context, returning empty tools list");
100
- return [];
101
- }
102
- const allTools = createAllTools(ctx);
103
- const filtered = filterToolsByDevice(allTools, ctx.deviceType);
104
- logger.log(`[DEVICE-FILTER] deviceType=${ctx.deviceType ?? "(none)"}, tools: ${allTools.length} → ${filtered.length} (${filtered.map(t => t.name).join(", ")})`);
92
+ /** Static tool list (3.24 pattern). Tools read SessionContext at execute time via ALS. */
93
+ agentTools: () => {
94
+ const ctx = getCurrentSessionContext();
95
+ const filtered = filterToolsByDevice(ALL_TOOLS, ctx?.deviceType);
96
+ logger.log(`[DEVICE-FILTER] deviceType=${ctx?.deviceType ?? "(none)"}, tools: ${ALL_TOOLS.length} → ${filtered.length} (${filtered.map(t => t.name).join(", ")})`);
105
97
  return filtered;
106
98
  },
107
99
  messaging: {
@@ -113,7 +105,6 @@ export const xyPlugin = {
113
105
  },
114
106
  targetResolver: {
115
107
  looksLikeId: (raw) => {
116
- // 信任所有非空字符串作为有效的 sessionId
117
108
  const trimmed = raw.trim();
118
109
  return trimmed.length > 0;
119
110
  },
@@ -139,16 +130,20 @@ export const xyPlugin = {
139
130
  reload: {
140
131
  configPrefixes: ["channels.xiaoyi-channel"],
141
132
  },
142
- // Gateway adapter for receiving messages
143
133
  gateway: {
144
134
  async startAccount(context) {
145
135
  const { monitorXYProvider } = await import("./monitor.js");
136
+ const { createXyAcpBindingManager } = await import("./acp-session-binding.js");
146
137
  const account = resolveXYConfig(context.cfg);
147
138
  context.setStatus?.({
148
139
  accountId: context.accountId,
149
140
  wsUrl: account.wsUrl,
150
141
  });
151
142
  context.log?.info(`[${context.accountId}] starting xiaoyi channel (wsUrl: ${account.wsUrl})`);
143
+ // Register ACP session binding adapter for this account.
144
+ // Enables sessions_spawn(runtime="acp") to bind subagent sessions
145
+ // to the current A2A conversation.
146
+ createXyAcpBindingManager({ accountId: context.accountId, cfg: context.cfg });
152
147
  return monitorXYProvider({
153
148
  config: context.cfg,
154
149
  runtime: context.runtime,
@@ -1,6 +1,11 @@
1
1
  import { XYWebSocketManager } from "./websocket.js";
2
2
  import type { XYChannelConfig } from "./types.js";
3
3
  import type { RuntimeEnv } from "openclaw/plugin-sdk";
4
+ /**
5
+ * Get a cached WebSocket manager without requiring config.
6
+ * Returns the first available manager. Use when ALS has no SessionContext.
7
+ */
8
+ export declare function getCachedXYWebSocketManager(): XYWebSocketManager;
4
9
  /**
5
10
  * Get or create a WebSocket manager for the given configuration.
6
11
  * Reuses existing managers if config matches.
@@ -13,6 +13,16 @@ if (!_g.__xyWsManagerCache) {
13
13
  _g.__xyWsManagerCache = new Map();
14
14
  }
15
15
  const wsManagerCache = _g.__xyWsManagerCache;
16
+ /**
17
+ * Get a cached WebSocket manager without requiring config.
18
+ * Returns the first available manager. Use when ALS has no SessionContext.
19
+ */
20
+ export function getCachedXYWebSocketManager() {
21
+ if (wsManagerCache.size === 0) {
22
+ throw new Error("No WebSocket manager available in cache");
23
+ }
24
+ return wsManagerCache.values().next().value;
25
+ }
16
26
  /**
17
27
  * Get or create a WebSocket manager for the given configuration.
18
28
  * Reuses existing managers if config matches.
@@ -2,6 +2,8 @@ import type { XYChannelConfig, A2ACommand } from "./types.js";
2
2
  export interface SendCommandViaPushParams {
3
3
  config: XYChannelConfig;
4
4
  command: A2ACommand;
5
+ /** 指定设备的 pushId(多设备路由)。未传时回退到 getAllPushIds()[0]。 */
6
+ pushId?: string;
5
7
  }
6
8
  /**
7
9
  * Send a tool command through the push channel (for cron-triggered tool calls).
@@ -24,16 +24,22 @@ export async function sendCommandViaPush(params) {
24
24
  command.header?.name ??
25
25
  "Command";
26
26
  logger.log(`[CRON-CMD] Sending command via push, intent=${intentName}`);
27
- // 1. Load push IDs, use first one
27
+ // 1. pushId:优先用调用方解析出的设备 pushId(多设备路由正确);
28
+ // 未提供时回退到 getAllPushIds()[0](单设备兼容旧行为)。
28
29
  let pushId = config.pushId;
29
- try {
30
- const pushIdList = await getAllPushIds();
31
- if (pushIdList.length > 0) {
32
- pushId = pushIdList[0];
33
- }
30
+ if (params.pushId) {
31
+ pushId = params.pushId;
34
32
  }
35
- catch (error) {
36
- logger.error("[CRON-CMD] Failed to load pushIds:", error);
33
+ else {
34
+ try {
35
+ const pushIdList = await getAllPushIds();
36
+ if (pushIdList.length > 0) {
37
+ pushId = pushIdList[0];
38
+ }
39
+ }
40
+ catch (error) {
41
+ logger.error("[CRON-CMD] Failed to load pushIds:", error);
42
+ }
37
43
  }
38
44
  // 2. Build and send push notification with command in directives
39
45
  const pushService = new XYPushService(config);
@@ -7,6 +7,8 @@ import { callGatewayTool } from "openclaw/plugin-sdk/agent-harness-runtime";
7
7
  import * as os from "os";
8
8
  import { sendCommand } from "./formatter.js";
9
9
  import { resolveXYConfig } from "./config.js";
10
+ import { configManager } from "./utils/config-manager.js";
11
+ import { setJobPushId } from "./utils/cron-push-map.js";
10
12
  import { logger } from "./utils/logger.js";
11
13
  import { readFileSync, readdirSync } from "fs";
12
14
  import { join } from "path";
@@ -19,7 +21,8 @@ const GATEWAY_TIMEOUT_MS = 60_000;
19
21
  */
20
22
  export async function handleCronQueryEvent(context, cfg) {
21
23
  const { action, jobId, params, sessionId, taskId, messageId } = context;
22
- logger.log(`[CRON-QUERY] Received event: action=${action}, jobId=${jobId ?? "(none)"}`);
24
+ const log = logger.withContext(sessionId ?? "", taskId ?? "");
25
+ log.log(`[CRON-QUERY] Received event: action=${action}, jobId=${jobId ?? "(none)"}`);
23
26
  let result;
24
27
  let error;
25
28
  try {
@@ -38,6 +41,11 @@ export async function handleCronQueryEvent(context, cfg) {
38
41
  break;
39
42
  case "add":
40
43
  result = await callGatewayTool("cron.add", { timeoutMs: GATEWAY_TIMEOUT_MS }, params ?? {});
44
+ // 捕获 jobId↔pushId:cron-query 路径由 channel 自己建 job,
45
+ // 此处 context 握着 sessionId,configManager 有对应设备 pushId。
46
+ await persistCronPushMap(context.sessionId, result).catch((err) => {
47
+ logger.error(`[CRON-QUERY] Failed to persist cron-push-map:`, err);
48
+ });
41
49
  break;
42
50
  case "update":
43
51
  result = await callGatewayTool("cron.update", { timeoutMs: GATEWAY_TIMEOUT_MS }, {
@@ -62,17 +70,17 @@ export async function handleCronQueryEvent(context, cfg) {
62
70
  break;
63
71
  default:
64
72
  error = `Unknown action: ${context.action}`;
65
- logger.error(`[CRON-QUERY] ${error}`);
73
+ log.error(`[CRON-QUERY] ${error}`);
66
74
  result = { error };
67
75
  }
68
76
  }
69
77
  catch (err) {
70
78
  error = err instanceof Error ? err.message : String(err);
71
- logger.error(`[CRON-QUERY] RPC call failed for action=${action}:`, err);
79
+ log.error(`[CRON-QUERY] RPC call failed for action=${action}:`, err);
72
80
  result = { error };
73
81
  }
74
82
  // Log the result
75
- logger.log(`[CRON-QUERY] RPC result for action=${action}: ${JSON.stringify(result, null, 2)}`);
83
+ log.log(`[CRON-QUERY] RPC result for action=${action}: ${JSON.stringify(result, null, 2)}`);
76
84
  // Send result back via sendCommand as System.CronQuery with payload.ans
77
85
  if (cfg && sessionId && taskId && messageId) {
78
86
  try {
@@ -93,17 +101,46 @@ export async function handleCronQueryEvent(context, cfg) {
93
101
  taskId,
94
102
  messageId,
95
103
  command,
96
- final: false,
104
+ final: sessionId.toLowerCase().endsWith("cronquery"),
97
105
  });
98
- logger.log(`[CRON-QUERY] Sent response via sendCommand, action=${action}`);
106
+ log.log(`[CRON-QUERY] Sent response via sendCommand, action=${action}`);
99
107
  }
100
108
  catch (sendErr) {
101
- logger.error(`[CRON-QUERY] Failed to send response via sendCommand:`, sendErr);
109
+ log.error(`[CRON-QUERY] Failed to send response via sendCommand:`, sendErr);
102
110
  }
103
111
  }
104
112
  else {
105
- logger.warn(`[CRON-QUERY] Missing cfg/sessionId/taskId/messageId, skipping sendCommand`);
113
+ log.warn(`[CRON-QUERY] Missing cfg/sessionId/taskId/messageId, skipping sendCommand`);
114
+ }
115
+ }
116
+ /**
117
+ * 从 cron.add 结果中提取 jobId,配合 sessionId 对应的 pushId 写入映射。
118
+ */
119
+ async function persistCronPushMap(sessionId, result) {
120
+ logger.log(`[CRONMAP] cron-query persist: sessionId=${sessionId ?? "(none)"}, resultType=${typeof result}`);
121
+ if (!sessionId) {
122
+ logger.log(`[CRONMAP] cron-query skip: no sessionId in context`);
123
+ return;
124
+ }
125
+ let jobId;
126
+ if (result && typeof result === "object") {
127
+ const id = result.id;
128
+ if (typeof id === "string" && id.trim())
129
+ jobId = id.trim();
130
+ }
131
+ if (!jobId) {
132
+ const preview = typeof result === "string" ? result.slice(0, 200) : JSON.stringify(result)?.slice(0, 200);
133
+ logger.log(`[CRONMAP] cron-query skip: no jobId in result. preview=${preview ?? "(empty)"}`);
134
+ return;
135
+ }
136
+ const pushId = configManager.getPushId(sessionId);
137
+ if (!pushId) {
138
+ logger.log(`[CRONMAP] cron-query skip: configManager has no pushId for sessionId=${sessionId}`);
139
+ return;
106
140
  }
141
+ logger.log(`[CRONMAP] cron-query writing map: jobId=${jobId}, pushId=${pushId.substring(0, 16)}...`);
142
+ await setJobPushId(jobId, { pushId, sessionId, source: "cron-query" });
143
+ logger.log(`[CRONMAP] cron-query map written OK`);
107
144
  }
108
145
  /**
109
146
  * Read local cron folder directly (bypassing openclaw RPC) and return
@@ -4,7 +4,8 @@
4
4
  import https from 'https';
5
5
  import { URL } from 'url';
6
6
  import { getConfig } from './config.js';
7
- import { DEFAULT_HTTP_PORT, HTTP_STATUS_BAD_REQUEST, API_URL_SUFFIX } from './constants.js';
7
+ import { DEFAULT_HTTPS_PORT, HTTP_STATUS_BAD_REQUEST, API_URL_SUFFIX } from './constants.js';
8
+ import { logger } from '../utils/logger.js';
8
9
  function buildHeadersForCelia(config, sessionId) {
9
10
  if (!config.uid || !config.apiKey || !config.skillId || !config.requestFrom) {
10
11
  throw new Error('[SENTINEL HOOK] Missing required configuration: uid, apiKey, skillId, or requestFrom is not defined');
@@ -22,7 +23,7 @@ function buildRequestOptions(url, headers, timeout) {
22
23
  const urlObj = new URL(url);
23
24
  return {
24
25
  hostname: urlObj.hostname,
25
- port: urlObj.port || DEFAULT_HTTP_PORT,
26
+ port: urlObj.port || DEFAULT_HTTPS_PORT,
26
27
  path: urlObj.pathname,
27
28
  method: "POST",
28
29
  headers: headers,
@@ -89,6 +90,7 @@ export async function callApi(questionText, api, sessionId, action) {
89
90
  };
90
91
  const httpBody = JSON.stringify(payload);
91
92
  const apiUrl = `${config.api.url}${API_URL_SUFFIX}`;
93
+ logger.log(`[SENTINEL HOOK] callApi: action=${action}, x-hag-trace-id=${sessionId}, url=${apiUrl}`);
92
94
  return new Promise((resolve, reject) => {
93
95
  const options = buildRequestOptions(apiUrl, headersForCelia, config.api.timeout);
94
96
  const req = https.request(options, (res) => {
@@ -6,15 +6,16 @@ import { callApi } from './call_api.js';
6
6
  import { processText, extractResultText, validateAndTruncateText, parseSecurityResult, handleExecToolInput, handleMessageToolInput, handleOtherToolInput } from './utils.js';
7
7
  import { ALLOWED_TOOLS, MAX_TEXT_LENGTH, MAX_TOTAL_LENGTH, MIN_TEXT_LENGTH, STEER_ABORT_MESSAGE, TOOL_OUTPUT_ACTION } from './constants.js';
8
8
  import { logger } from '../utils/logger.js';
9
- import { getSessionContext } from '../tools/session-manager.js';
9
+ import { getCurrentSessionContext } from '../tools/session-manager.js';
10
10
  import { tryInjectSteer } from './steer-context.js';
11
11
  // 主入口模块
12
12
  export default function register(api) {
13
13
  api.on("before_tool_call", async (event, ctx) => {
14
14
  logger.log(`[SENTINEL HOOK] before_tool_call_event toolName: ${event.toolName}`);
15
- // 生成sessionID
16
- const sessionId = (event.runId?.replace(/-/g, '') || crypto.randomBytes(16).toString('hex'));
17
- logger.log(`[SENTINEL HOOK] Generated Session ID: ${sessionId}`);
15
+ // 获取真实sessionID:优先使用ALS中的A2A sessionId,降级到OpenClaw runId或随机值
16
+ const sessionCtx = getCurrentSessionContext();
17
+ const sessionId = sessionCtx?.sessionId || (event.runId?.replace(/-/g, '') || crypto.randomBytes(16).toString('hex'));
18
+ logger.log(`[SENTINEL HOOK] Session ID: ${sessionId} (fromALS: ${!!sessionCtx?.sessionId})`);
18
19
  // 处理 TOOL_INPUT 数据采集、发送数据,根据扫描结果决定是否阻塞
19
20
  try {
20
21
  let scanResult = null;
@@ -43,9 +44,10 @@ export default function register(api) {
43
44
  }
44
45
  try {
45
46
  logger.log(`[SENTINEL HOOK] after_tool_call_event toolName: ${event.toolName}`);
46
- // 生成sessionID
47
- const sessionId = (event.runId?.replace(/-/g, '') || crypto.randomBytes(16).toString('hex'));
48
- logger.log(`[SENTINEL HOOK] Generated Session ID: ${sessionId}`);
47
+ // 获取真实sessionID:优先使用ALS中的A2A sessionId,降级到OpenClaw runId或随机值
48
+ const sessionCtx = getCurrentSessionContext();
49
+ const sessionId = sessionCtx?.sessionId || (event.runId?.replace(/-/g, '') || crypto.randomBytes(16).toString('hex'));
50
+ logger.log(`[SENTINEL HOOK] Session ID: ${sessionId} (fromALS: ${!!sessionCtx?.sessionId})`);
49
51
  // 处理TOOL_OUTPUT数据采集(保持现有逻辑)
50
52
  const resultText = extractResultText(event, event.toolName);
51
53
  const resultTextLength = resultText.length;
@@ -78,7 +80,6 @@ export default function register(api) {
78
80
  logger.log(`[SENTINEL HOOK] TOOL_OUTPUT response: status=${result.status}.`);
79
81
  if (result.status === 'REJECT') {
80
82
  logger.warn('[SENTINEL HOOK] REJECT detected, attempting steer injection');
81
- const sessionCtx = ctx.sessionKey ? getSessionContext(ctx.sessionKey) : null;
82
83
  if (sessionCtx?.sessionId && sessionCtx?.taskId) {
83
84
  await tryInjectSteer({
84
85
  sessionId: sessionCtx.sessionId,
@@ -6,7 +6,7 @@ import path from 'path';
6
6
  import https from 'https';
7
7
  import { URL } from 'url';
8
8
  import { getConfig } from './config.js';
9
- import { DEFAULT_HTTP_PORT, DEFAULT_HTTPS_PORT, MAX_TIMES, CONNECT_TIMEOUT, READ_TIMEOUT, EXPIRE_TIME, OSMS_PREPARE_URL, OSMS_COMPLETE_URL, TEMPORARY_MATERIAL_PACKAGE, FILE_OWNER_UID, FILE_OWNER_TEAM_ID } from './constants.js';
9
+ import { DEFAULT_HTTPS_PORT, MAX_TIMES, CONNECT_TIMEOUT, READ_TIMEOUT, EXPIRE_TIME, OSMS_PREPARE_URL, OSMS_COMPLETE_URL, TEMPORARY_MATERIAL_PACKAGE, FILE_OWNER_UID, FILE_OWNER_TEAM_ID } from './constants.js';
10
10
  function buildOsmsHeaders(config, traceId) {
11
11
  return {
12
12
  'content-type': 'application/json',
@@ -22,7 +22,7 @@ function httpRequest(url, method, headers, body, timeout) {
22
22
  const urlObj = new URL(url);
23
23
  const options = {
24
24
  hostname: urlObj.hostname,
25
- port: urlObj.port || DEFAULT_HTTP_PORT,
25
+ port: urlObj.port || DEFAULT_HTTPS_PORT,
26
26
  path: urlObj.pathname + urlObj.search,
27
27
  method: method,
28
28
  headers: headers,
@@ -2,10 +2,12 @@
2
2
  import { v4 as uuidv4 } from "uuid";
3
3
  import { getXYWebSocketManager } from "./client.js";
4
4
  import { logger } from "./utils/logger.js";
5
- import { getCurrentTaskId, getCurrentMessageId } from "./task-manager.js";
6
5
  import { redactSensitiveText, containsSensitiveInfo } from "./sensitive-redactor.js";
7
6
  import { rewriteOutboundApprovalText } from "./approval-bridge.js";
8
- import { isCronToolCall } from "./tools/session-manager.js";
7
+ import { isCronToolCall, getCurrentCronJobId } from "./tools/session-manager.js";
8
+ import { configManager } from "./utils/config-manager.js";
9
+ import { getPushIdByJobId } from "./utils/cron-push-map.js";
10
+ import { getAllPushIds } from "./utils/pushid-manager.js";
9
11
  // ─────────────────────────────────────────────────────────────
10
12
  // 敏感信息脱敏辅助函数
11
13
  // ─────────────────────────────────────────────────────────────
@@ -139,6 +141,8 @@ export async function sendReasoningTextUpdate(params) {
139
141
  id: messageId,
140
142
  result: artifact,
141
143
  };
144
+ const redactedText = redactSensitiveText(bridgedText);
145
+ log.log(`[A2A_REASONING] Sending reasoning update, append=${append}, taskId=${taskId}, messageId=${messageId}, text=${buildTextPreview(redactedText)}, sensitive=${containsSensitiveInfo(bridgedText)}`);
142
146
  const wsManager = getXYWebSocketManager(config);
143
147
  const outboundMessage = {
144
148
  msgType: "agent_response",
@@ -148,6 +152,7 @@ export async function sendReasoningTextUpdate(params) {
148
152
  msgDetail: JSON.stringify(jsonRpcResponse),
149
153
  };
150
154
  await wsManager.sendMessage(sessionId, outboundMessage);
155
+ log.log(`[A2A_REASONING] Reasoning update sent successfully`);
151
156
  }
152
157
  /**
153
158
  * Send an A2A task status update.
@@ -155,11 +160,7 @@ export async function sendReasoningTextUpdate(params) {
155
160
  */
156
161
  export async function sendStatusUpdate(params) {
157
162
  const { config, sessionId, taskId, messageId, text, state } = params;
158
- // Dynamic lookup: use latest taskId/messageId from task-manager (handles steer/interrupt),
159
- // fall back to closure-captured values
160
- const currentTaskId = getCurrentTaskId(sessionId) ?? taskId;
161
- const currentMessageId = getCurrentMessageId(sessionId) ?? messageId;
162
- const log = logger.withContext(sessionId, currentTaskId);
163
+ const log = logger.withContext(sessionId, taskId);
163
164
  // 审批桥接和脱敏
164
165
  const bridgedText = rewriteOutboundApprovalText(sessionId, text);
165
166
  const redactedText = redactSensitiveText(bridgedText);
@@ -174,7 +175,7 @@ export async function sendStatusUpdate(params) {
174
175
  ],
175
176
  });
176
177
  const statusUpdate = {
177
- taskId: currentTaskId,
178
+ taskId,
178
179
  kind: "status-update",
179
180
  final: false, // Status updates should not end the stream
180
181
  status: {
@@ -185,7 +186,7 @@ export async function sendStatusUpdate(params) {
185
186
  // Build JSON-RPC response
186
187
  const jsonRpcResponse = {
187
188
  jsonrpc: "2.0",
188
- id: currentMessageId,
189
+ id: messageId,
189
190
  result: statusUpdate,
190
191
  };
191
192
  // Send via WebSocket
@@ -194,13 +195,52 @@ export async function sendStatusUpdate(params) {
194
195
  msgType: "agent_response",
195
196
  agentId: config.agentId,
196
197
  sessionId,
197
- taskId: currentTaskId,
198
+ taskId,
198
199
  msgDetail: JSON.stringify(jsonRpcResponse),
199
200
  };
200
201
  // Log complete response body
201
202
  log.log(`[A2A_STATUS] Sending status-update, text="${redactedText}"`);
202
203
  await wsManager.sendMessage(sessionId, outboundMessage);
203
204
  }
205
+ /**
206
+ * 解析 cron fire 时应使用的 pushId(多设备路由)。
207
+ *
208
+ * 查询链(逐级回退):
209
+ * 1. 合成 sessionId → jobId → cron-push-map.json → 创建时记录的设备 pushId
210
+ * 2. configManager 同进程的 sessionId→pushId(进程未重启时兜底)
211
+ * 3. getAllPushIds()[0](单设备兼容旧行为)
212
+ * 返回 undefined 表示走兜底(由 sendCommandViaPush 内部处理)。
213
+ */
214
+ async function resolveCronPushId(sessionId, config) {
215
+ // 1. jobId → 持久化映射
216
+ const jobId = getCurrentCronJobId(sessionId);
217
+ if (jobId) {
218
+ const hit = await getPushIdByJobId(jobId);
219
+ if (hit?.pushId) {
220
+ logger.log(`[CRON-PUSH] Resolved pushId via map, jobId=${jobId}`);
221
+ return hit.pushId;
222
+ }
223
+ }
224
+ // 2. 同进程 configManager 兜底
225
+ const sessionPushId = configManager.getPushId(sessionId);
226
+ if (sessionPushId) {
227
+ logger.log(`[CRON-PUSH] Resolved pushId via configManager (fallback)`);
228
+ return sessionPushId;
229
+ }
230
+ // 3. config.pushId / getAllPushIds()[0] 交给 sendCommandViaPush 内部处理
231
+ void config;
232
+ try {
233
+ const all = await getAllPushIds();
234
+ if (all.length > 0) {
235
+ logger.log(`[CRON-PUSH] Resolved pushId via getAllPushIds[0] (legacy fallback)`);
236
+ return all[0];
237
+ }
238
+ }
239
+ catch (error) {
240
+ logger.error(`[CRON-PUSH] getAllPushIds failed:`, error);
241
+ }
242
+ return undefined;
243
+ }
204
244
  /**
205
245
  * Send a command as an artifact update (final=false).
206
246
  *
@@ -217,22 +257,22 @@ export async function sendCommand(params) {
217
257
  if (commands.length === 0) {
218
258
  throw new Error("sendCommand requires command or commands.");
219
259
  }
220
- // ── Cron mode: disabled ────────────────────────────────────────
221
- // sendCommandViaPush is disabled in this version. Cron-triggered
222
- // tool calls that try to send commands will be rejected.
260
+ // ── Cron mode: route through push channel ──────────────────────
261
+ // Detected via: (a) sessionId "cron-" prefix from synthetic session, OR
262
+ // (b) toolCallId marked by before_tool_call hook from openclaw's sessionKey.
223
263
  if (sessionId.startsWith("cron-") || isCronToolCall(toolCallId)) {
224
- throw new Error("sendCommandViaPush is disabled in this version");
264
+ const { sendCommandViaPush } = await import("./cron-command.js");
265
+ // 解析正确设备的 pushId:合成 sessionId → jobId → cron-push-map。
266
+ // provider.ts 在 isCron 分支已把 jobId 绑定到该 sessionId。
267
+ const pushId = await resolveCronPushId(sessionId, config);
268
+ return sendCommandViaPush({ config, command: commands[0], pushId });
225
269
  }
226
270
  // ── Normal mode: WebSocket ─────────────────────────────────────
227
- // Dynamic lookup: use latest taskId/messageId from task-manager (handles steer/interrupt),
228
- // fall back to closure-captured values
229
- const currentTaskId = getCurrentTaskId(sessionId) ?? taskId;
230
- const currentMessageId = getCurrentMessageId(sessionId) ?? messageId;
231
- const log = logger.withContext(sessionId, currentTaskId);
271
+ const log = logger.withContext(sessionId, taskId);
232
272
  // Build artifact update with command as data
233
273
  // Wrap command in commands array as per protocol requirement
234
274
  const artifact = {
235
- taskId: currentTaskId,
275
+ taskId,
236
276
  kind: "artifact-update",
237
277
  append: false,
238
278
  lastChunk: true,
@@ -254,7 +294,7 @@ export async function sendCommand(params) {
254
294
  // Build JSON-RPC response
255
295
  const jsonRpcResponse = {
256
296
  jsonrpc: "2.0",
257
- id: currentMessageId,
297
+ id: messageId,
258
298
  result: artifact,
259
299
  };
260
300
  // Send via WebSocket
@@ -263,7 +303,7 @@ export async function sendCommand(params) {
263
303
  msgType: "agent_response",
264
304
  agentId: config.agentId,
265
305
  sessionId,
266
- taskId: currentTaskId,
306
+ taskId,
267
307
  msgDetail: JSON.stringify(jsonRpcResponse),
268
308
  };
269
309
  // Log complete response body
@@ -283,12 +323,10 @@ export async function sendCard(params) {
283
323
  throw new Error("sendCard does not support cron mode");
284
324
  }
285
325
  // ── Normal mode: WebSocket ─────────────────────────────────────
286
- const currentTaskId = getCurrentTaskId(sessionId) ?? taskId;
287
- const currentMessageId = getCurrentMessageId(sessionId) ?? messageId;
288
- const log = logger.withContext(sessionId, currentTaskId);
326
+ const log = logger.withContext(sessionId, taskId);
289
327
  // Build artifact update with cardsInfo as data
290
328
  const artifact = {
291
- taskId: currentTaskId,
329
+ taskId,
292
330
  kind: "artifact-update",
293
331
  append: false,
294
332
  lastChunk: true,
@@ -308,7 +346,7 @@ export async function sendCard(params) {
308
346
  // Build JSON-RPC response
309
347
  const jsonRpcResponse = {
310
348
  jsonrpc: "2.0",
311
- id: currentMessageId,
349
+ id: messageId,
312
350
  result: artifact,
313
351
  };
314
352
  // Send via WebSocket
@@ -317,7 +355,7 @@ export async function sendCard(params) {
317
355
  msgType: "agent_response",
318
356
  agentId: config.agentId,
319
357
  sessionId,
320
- taskId: currentTaskId,
358
+ taskId,
321
359
  msgDetail: JSON.stringify(jsonRpcResponse),
322
360
  };
323
361
  log.log(`[A2A_CARD] Sending card`);
@@ -0,0 +1,11 @@
1
+ import type { LogReporterConfig } from "./types.js";
2
+ /**
3
+ * Load and validate the JSON config file.
4
+ * Falls back to defaults for optional fields.
5
+ */
6
+ export declare function loadConfig(configPath: string): LogReporterConfig;
7
+ /**
8
+ * Resolve a path template with date wildcards to actual file paths on disk.
9
+ * Scans the directory and returns all files matching the pattern.
10
+ */
11
+ export declare function resolveLogFiles(templatePath: string): string[];