@ynhcj/xiaoyi-channel 0.0.185-beta → 0.0.185-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 (150) 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 +62 -33
  5. package/dist/src/channel.js +68 -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 +36 -0
  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 +72 -36
  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/outbound.js +5 -0
  33. package/dist/src/parser.d.ts +12 -0
  34. package/dist/src/parser.js +39 -13
  35. package/dist/src/provider.js +79 -16
  36. package/dist/src/reply-dispatcher.d.ts +3 -0
  37. package/dist/src/reply-dispatcher.js +166 -164
  38. package/dist/src/self-evolution-handler.js +2 -1
  39. package/dist/src/task-manager.d.ts +5 -6
  40. package/dist/src/task-manager.js +5 -9
  41. package/dist/src/tools/agent-as-skill-tool.d.ts +41 -2
  42. package/dist/src/tools/agent-as-skill-tool.js +144 -151
  43. package/dist/src/tools/calendar-tool.d.ts +24 -2
  44. package/dist/src/tools/calendar-tool.js +115 -117
  45. package/dist/src/tools/call-device-tool.d.ts +20 -6
  46. package/dist/src/tools/call-device-tool.js +116 -138
  47. package/dist/src/tools/call-phone-tool.d.ts +21 -2
  48. package/dist/src/tools/call-phone-tool.js +112 -114
  49. package/dist/src/tools/check-plugin-privilege-tool.d.ts +16 -2
  50. package/dist/src/tools/check-plugin-privilege-tool.js +141 -143
  51. package/dist/src/tools/create-alarm-tool.d.ts +39 -2
  52. package/dist/src/tools/create-alarm-tool.js +229 -231
  53. package/dist/src/tools/create-all-tools.js +8 -4
  54. package/dist/src/tools/delete-alarm-tool.d.ts +15 -2
  55. package/dist/src/tools/delete-alarm-tool.js +134 -136
  56. package/dist/src/tools/device-tool-map.d.ts +1 -1
  57. package/dist/src/tools/device-tool-map.js +9 -2
  58. package/dist/src/tools/discover-cross-devices-tool.d.ts +16 -2
  59. package/dist/src/tools/discover-cross-devices-tool.js +121 -124
  60. package/dist/src/tools/display-a2ui-card-bypath-tool.d.ts +21 -0
  61. package/dist/src/tools/display-a2ui-card-bypath-tool.js +65 -0
  62. package/dist/src/tools/get-alarm-tool-schema.d.ts +1 -2
  63. package/dist/src/tools/get-alarm-tool-schema.js +10 -16
  64. package/dist/src/tools/get-calendar-tool-schema.d.ts +1 -2
  65. package/dist/src/tools/get-calendar-tool-schema.js +8 -12
  66. package/dist/src/tools/get-collection-tool-schema.d.ts +1 -2
  67. package/dist/src/tools/get-collection-tool-schema.js +9 -11
  68. package/dist/src/tools/get-contact-tool-schema.d.ts +1 -2
  69. package/dist/src/tools/get-contact-tool-schema.js +10 -16
  70. package/dist/src/tools/get-device-file-tool-schema.d.ts +1 -2
  71. package/dist/src/tools/get-device-file-tool-schema.js +9 -13
  72. package/dist/src/tools/get-email-tool-schema.d.ts +1 -2
  73. package/dist/src/tools/get-email-tool-schema.js +8 -11
  74. package/dist/src/tools/get-note-tool-schema.d.ts +1 -2
  75. package/dist/src/tools/get-note-tool-schema.js +9 -14
  76. package/dist/src/tools/get-photo-tool-schema.d.ts +1 -2
  77. package/dist/src/tools/get-photo-tool-schema.js +9 -12
  78. package/dist/src/tools/image-reading-tool.d.ts +28 -2
  79. package/dist/src/tools/image-reading-tool.js +76 -76
  80. package/dist/src/tools/location-tool.d.ts +11 -2
  81. package/dist/src/tools/location-tool.js +93 -92
  82. package/dist/src/tools/login-token-tool.d.ts +20 -2
  83. package/dist/src/tools/login-token-tool.js +123 -125
  84. package/dist/src/tools/modify-alarm-tool.d.ts +47 -2
  85. package/dist/src/tools/modify-alarm-tool.js +250 -252
  86. package/dist/src/tools/modify-note-tool.d.ts +20 -2
  87. package/dist/src/tools/modify-note-tool.js +107 -109
  88. package/dist/src/tools/note-tool.d.ts +20 -2
  89. package/dist/src/tools/note-tool.js +106 -108
  90. package/dist/src/tools/query-app-message-tool.d.ts +28 -2
  91. package/dist/src/tools/query-app-message-tool.js +111 -113
  92. package/dist/src/tools/query-memory-data-tool.d.ts +28 -2
  93. package/dist/src/tools/query-memory-data-tool.js +112 -114
  94. package/dist/src/tools/query-todo-task-tool.d.ts +24 -2
  95. package/dist/src/tools/query-todo-task-tool.js +106 -108
  96. package/dist/src/tools/save-file-to-phone-tool.d.ts +24 -2
  97. package/dist/src/tools/save-file-to-phone-tool.js +130 -132
  98. package/dist/src/tools/save-media-to-gallery-tool.d.ts +24 -2
  99. package/dist/src/tools/save-media-to-gallery-tool.js +137 -139
  100. package/dist/src/tools/save-self-evolution-skill-tool.d.ts +54 -2
  101. package/dist/src/tools/save-self-evolution-skill-tool.js +194 -194
  102. package/dist/src/tools/search-alarm-tool.d.ts +34 -2
  103. package/dist/src/tools/search-alarm-tool.js +174 -176
  104. package/dist/src/tools/search-calendar-tool.d.ts +24 -2
  105. package/dist/src/tools/search-calendar-tool.js +148 -150
  106. package/dist/src/tools/search-contact-tool.d.ts +16 -2
  107. package/dist/src/tools/search-contact-tool.js +101 -103
  108. package/dist/src/tools/search-email-tool.d.ts +21 -2
  109. package/dist/src/tools/search-email-tool.js +110 -112
  110. package/dist/src/tools/search-file-tool.d.ts +16 -2
  111. package/dist/src/tools/search-file-tool.js +104 -106
  112. package/dist/src/tools/search-message-tool.d.ts +16 -2
  113. package/dist/src/tools/search-message-tool.js +103 -105
  114. package/dist/src/tools/search-note-tool.d.ts +16 -2
  115. package/dist/src/tools/search-note-tool.js +98 -100
  116. package/dist/src/tools/search-photo-gallery-tool.d.ts +21 -2
  117. package/dist/src/tools/search-photo-gallery-tool.js +35 -37
  118. package/dist/src/tools/send-cross-device-task-tool.d.ts +35 -2
  119. package/dist/src/tools/send-cross-device-task-tool.js +216 -150
  120. package/dist/src/tools/send-email-tool.d.ts +24 -2
  121. package/dist/src/tools/send-email-tool.js +108 -110
  122. package/dist/src/tools/send-file-to-user-tool.d.ts +20 -2
  123. package/dist/src/tools/send-file-to-user-tool.js +174 -178
  124. package/dist/src/tools/send-html-card-tool.d.ts +20 -2
  125. package/dist/src/tools/send-html-card-tool.js +85 -87
  126. package/dist/src/tools/send-message-tool.d.ts +20 -2
  127. package/dist/src/tools/send-message-tool.js +122 -124
  128. package/dist/src/tools/session-manager.d.ts +19 -52
  129. package/dist/src/tools/session-manager.js +95 -247
  130. package/dist/src/tools/upload-file-tool.d.ts +20 -2
  131. package/dist/src/tools/upload-file-tool.js +80 -82
  132. package/dist/src/tools/upload-photo-tool.d.ts +20 -2
  133. package/dist/src/tools/upload-photo-tool.js +68 -70
  134. package/dist/src/tools/xiaoyi-add-collection-tool.d.ts +32 -2
  135. package/dist/src/tools/xiaoyi-add-collection-tool.js +146 -148
  136. package/dist/src/tools/xiaoyi-collection-tool.d.ts +20 -2
  137. package/dist/src/tools/xiaoyi-collection-tool.js +114 -116
  138. package/dist/src/tools/xiaoyi-delete-collection-tool.d.ts +15 -2
  139. package/dist/src/tools/xiaoyi-delete-collection-tool.js +127 -129
  140. package/dist/src/tools/xiaoyi-gui-tool.d.ts +16 -2
  141. package/dist/src/tools/xiaoyi-gui-tool.js +92 -95
  142. package/dist/src/types.d.ts +6 -6
  143. package/dist/src/utils/config-manager.d.ts +3 -2
  144. package/dist/src/utils/config-manager.js +22 -2
  145. package/dist/src/utils/cron-push-map.d.ts +26 -0
  146. package/dist/src/utils/cron-push-map.js +131 -0
  147. package/dist/src/utils/logger.js +3 -14
  148. package/dist/src/websocket.d.ts +1 -1
  149. package/dist/src/websocket.js +29 -13
  150. 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
  },
@@ -135,20 +126,33 @@ export const xyPlugin = {
135
126
  ? { conversationId, matchPriority: 2 }
136
127
  : null;
137
128
  },
129
+ resolveCommandConversation: ({ accountId }) => {
130
+ // xiaoyi-channel: the A2A sessionId IS the conversationId.
131
+ // Read from the current ALS session context.
132
+ const ctx = getCurrentSessionContext();
133
+ const sessionId = ctx?.sessionId?.trim();
134
+ if (!sessionId)
135
+ return null;
136
+ return { conversationId: sessionId };
137
+ },
138
138
  },
139
139
  reload: {
140
140
  configPrefixes: ["channels.xiaoyi-channel"],
141
141
  },
142
- // Gateway adapter for receiving messages
143
142
  gateway: {
144
143
  async startAccount(context) {
145
144
  const { monitorXYProvider } = await import("./monitor.js");
145
+ const { createXyAcpBindingManager } = await import("./acp-session-binding.js");
146
146
  const account = resolveXYConfig(context.cfg);
147
147
  context.setStatus?.({
148
148
  accountId: context.accountId,
149
149
  wsUrl: account.wsUrl,
150
150
  });
151
151
  context.log?.info(`[${context.accountId}] starting xiaoyi channel (wsUrl: ${account.wsUrl})`);
152
+ // Register ACP session binding adapter for this account.
153
+ // Enables sessions_spawn(runtime="acp") to bind subagent sessions
154
+ // to the current A2A conversation.
155
+ createXyAcpBindingManager({ accountId: context.accountId, cfg: context.cfg });
152
156
  return monitorXYProvider({
153
157
  config: context.cfg,
154
158
  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";
@@ -39,6 +41,11 @@ export async function handleCronQueryEvent(context, cfg) {
39
41
  break;
40
42
  case "add":
41
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
+ });
42
49
  break;
43
50
  case "update":
44
51
  result = await callGatewayTool("cron.update", { timeoutMs: GATEWAY_TIMEOUT_MS }, {
@@ -106,6 +113,35 @@ export async function handleCronQueryEvent(context, cfg) {
106
113
  log.warn(`[CRON-QUERY] Missing cfg/sessionId/taskId/messageId, skipping sendCommand`);
107
114
  }
108
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;
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`);
144
+ }
109
145
  /**
110
146
  * Read local cron folder directly (bypassing openclaw RPC) and return
111
147
  * run records from the last 7 days, grouped by date and sorted by time.
@@ -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,
@@ -1,11 +1,14 @@
1
1
  // OpenClaw → A2A format conversion
2
+ import os from "os";
2
3
  import { v4 as uuidv4 } from "uuid";
3
4
  import { getXYWebSocketManager } from "./client.js";
4
5
  import { logger } from "./utils/logger.js";
5
- import { getCurrentTaskId, getCurrentMessageId } from "./task-manager.js";
6
6
  import { redactSensitiveText, containsSensitiveInfo } from "./sensitive-redactor.js";
7
7
  import { rewriteOutboundApprovalText } from "./approval-bridge.js";
8
- import { isCronToolCall } from "./tools/session-manager.js";
8
+ import { isCronToolCall, getCurrentCronJobId } from "./tools/session-manager.js";
9
+ import { configManager } from "./utils/config-manager.js";
10
+ import { getPushIdByJobId } from "./utils/cron-push-map.js";
11
+ import { getAllPushIds } from "./utils/pushid-manager.js";
9
12
  // ─────────────────────────────────────────────────────────────
10
13
  // 敏感信息脱敏辅助函数
11
14
  // ─────────────────────────────────────────────────────────────
@@ -95,7 +98,7 @@ export async function sendA2AResponse(params) {
95
98
  agentId: config.agentId,
96
99
  sessionId,
97
100
  taskId,
98
- msgDetail: JSON.stringify(jsonRpcResponse),
101
+ msgDetail: JSON.stringify({ ...jsonRpcResponse, hostname: os.hostname() }),
99
102
  };
100
103
  if (shouldLog) {
101
104
  const redactedText = redactSensitiveText(bridgedText ?? "");
@@ -145,7 +148,7 @@ export async function sendReasoningTextUpdate(params) {
145
148
  agentId: config.agentId,
146
149
  sessionId,
147
150
  taskId,
148
- msgDetail: JSON.stringify(jsonRpcResponse),
151
+ msgDetail: JSON.stringify({ ...jsonRpcResponse, hostname: os.hostname() }),
149
152
  };
150
153
  await wsManager.sendMessage(sessionId, outboundMessage);
151
154
  }
@@ -155,11 +158,7 @@ export async function sendReasoningTextUpdate(params) {
155
158
  */
156
159
  export async function sendStatusUpdate(params) {
157
160
  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);
161
+ const log = logger.withContext(sessionId, taskId);
163
162
  // 审批桥接和脱敏
164
163
  const bridgedText = rewriteOutboundApprovalText(sessionId, text);
165
164
  const redactedText = redactSensitiveText(bridgedText);
@@ -174,7 +173,7 @@ export async function sendStatusUpdate(params) {
174
173
  ],
175
174
  });
176
175
  const statusUpdate = {
177
- taskId: currentTaskId,
176
+ taskId,
178
177
  kind: "status-update",
179
178
  final: false, // Status updates should not end the stream
180
179
  status: {
@@ -185,7 +184,7 @@ export async function sendStatusUpdate(params) {
185
184
  // Build JSON-RPC response
186
185
  const jsonRpcResponse = {
187
186
  jsonrpc: "2.0",
188
- id: currentMessageId,
187
+ id: messageId,
189
188
  result: statusUpdate,
190
189
  };
191
190
  // Send via WebSocket
@@ -194,13 +193,52 @@ export async function sendStatusUpdate(params) {
194
193
  msgType: "agent_response",
195
194
  agentId: config.agentId,
196
195
  sessionId,
197
- taskId: currentTaskId,
198
- msgDetail: JSON.stringify(jsonRpcResponse),
196
+ taskId,
197
+ msgDetail: JSON.stringify({ ...jsonRpcResponse, hostname: os.hostname() }),
199
198
  };
200
199
  // Log complete response body
201
200
  log.log(`[A2A_STATUS] Sending status-update, text="${redactedText}"`);
202
201
  await wsManager.sendMessage(sessionId, outboundMessage);
203
202
  }
203
+ /**
204
+ * 解析 cron fire 时应使用的 pushId(多设备路由)。
205
+ *
206
+ * 查询链(逐级回退):
207
+ * 1. 合成 sessionId → jobId → cron-push-map.json → 创建时记录的设备 pushId
208
+ * 2. configManager 同进程的 sessionId→pushId(进程未重启时兜底)
209
+ * 3. getAllPushIds()[0](单设备兼容旧行为)
210
+ * 返回 undefined 表示走兜底(由 sendCommandViaPush 内部处理)。
211
+ */
212
+ async function resolveCronPushId(sessionId, config) {
213
+ // 1. jobId → 持久化映射
214
+ const jobId = getCurrentCronJobId(sessionId);
215
+ if (jobId) {
216
+ const hit = await getPushIdByJobId(jobId);
217
+ if (hit?.pushId) {
218
+ logger.log(`[CRON-PUSH] Resolved pushId via map, jobId=${jobId}`);
219
+ return hit.pushId;
220
+ }
221
+ }
222
+ // 2. 同进程 configManager 兜底
223
+ const sessionPushId = configManager.getPushId(sessionId);
224
+ if (sessionPushId) {
225
+ logger.log(`[CRON-PUSH] Resolved pushId via configManager (fallback)`);
226
+ return sessionPushId;
227
+ }
228
+ // 3. config.pushId / getAllPushIds()[0] 交给 sendCommandViaPush 内部处理
229
+ void config;
230
+ try {
231
+ const all = await getAllPushIds();
232
+ if (all.length > 0) {
233
+ logger.log(`[CRON-PUSH] Resolved pushId via getAllPushIds[0] (legacy fallback)`);
234
+ return all[0];
235
+ }
236
+ }
237
+ catch (error) {
238
+ logger.error(`[CRON-PUSH] getAllPushIds failed:`, error);
239
+ }
240
+ return undefined;
241
+ }
204
242
  /**
205
243
  * Send a command as an artifact update (final=false).
206
244
  *
@@ -217,22 +255,22 @@ export async function sendCommand(params) {
217
255
  if (commands.length === 0) {
218
256
  throw new Error("sendCommand requires command or commands.");
219
257
  }
220
- // ── Cron mode: disabled ────────────────────────────────────────
221
- // sendCommandViaPush is disabled in this version. Cron-triggered
222
- // tool calls that try to send commands will be rejected.
258
+ // ── Cron mode: route through push channel ──────────────────────
259
+ // Detected via: (a) sessionId "cron-" prefix from synthetic session, OR
260
+ // (b) toolCallId marked by before_tool_call hook from openclaw's sessionKey.
223
261
  if (sessionId.startsWith("cron-") || isCronToolCall(toolCallId)) {
224
- throw new Error("sendCommandViaPush is disabled in this version");
262
+ const { sendCommandViaPush } = await import("./cron-command.js");
263
+ // 解析正确设备的 pushId:合成 sessionId → jobId → cron-push-map。
264
+ // provider.ts 在 isCron 分支已把 jobId 绑定到该 sessionId。
265
+ const pushId = await resolveCronPushId(sessionId, config);
266
+ return sendCommandViaPush({ config, command: commands[0], pushId });
225
267
  }
226
268
  // ── 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);
269
+ const log = logger.withContext(sessionId, taskId);
232
270
  // Build artifact update with command as data
233
271
  // Wrap command in commands array as per protocol requirement
234
272
  const artifact = {
235
- taskId: currentTaskId,
273
+ taskId,
236
274
  kind: "artifact-update",
237
275
  append: false,
238
276
  lastChunk: true,
@@ -254,7 +292,7 @@ export async function sendCommand(params) {
254
292
  // Build JSON-RPC response
255
293
  const jsonRpcResponse = {
256
294
  jsonrpc: "2.0",
257
- id: currentMessageId,
295
+ id: messageId,
258
296
  result: artifact,
259
297
  };
260
298
  // Send via WebSocket
@@ -263,8 +301,8 @@ export async function sendCommand(params) {
263
301
  msgType: "agent_response",
264
302
  agentId: config.agentId,
265
303
  sessionId,
266
- taskId: currentTaskId,
267
- msgDetail: JSON.stringify(jsonRpcResponse),
304
+ taskId,
305
+ msgDetail: JSON.stringify({ ...jsonRpcResponse, hostname: os.hostname() }),
268
306
  };
269
307
  // Log complete response body
270
308
  log.log(`[A2A_COMMAND] Sending command`);
@@ -283,12 +321,10 @@ export async function sendCard(params) {
283
321
  throw new Error("sendCard does not support cron mode");
284
322
  }
285
323
  // ── Normal mode: WebSocket ─────────────────────────────────────
286
- const currentTaskId = getCurrentTaskId(sessionId) ?? taskId;
287
- const currentMessageId = getCurrentMessageId(sessionId) ?? messageId;
288
- const log = logger.withContext(sessionId, currentTaskId);
324
+ const log = logger.withContext(sessionId, taskId);
289
325
  // Build artifact update with cardsInfo as data
290
326
  const artifact = {
291
- taskId: currentTaskId,
327
+ taskId,
292
328
  kind: "artifact-update",
293
329
  append: false,
294
330
  lastChunk: true,
@@ -308,7 +344,7 @@ export async function sendCard(params) {
308
344
  // Build JSON-RPC response
309
345
  const jsonRpcResponse = {
310
346
  jsonrpc: "2.0",
311
- id: currentMessageId,
347
+ id: messageId,
312
348
  result: artifact,
313
349
  };
314
350
  // Send via WebSocket
@@ -317,8 +353,8 @@ export async function sendCard(params) {
317
353
  msgType: "agent_response",
318
354
  agentId: config.agentId,
319
355
  sessionId,
320
- taskId: currentTaskId,
321
- msgDetail: JSON.stringify(jsonRpcResponse),
356
+ taskId,
357
+ msgDetail: JSON.stringify({ ...jsonRpcResponse, hostname: os.hostname() }),
322
358
  };
323
359
  log.log(`[A2A_CARD] Sending card`);
324
360
  await wsManager.sendMessage(sessionId, outboundMessage);
@@ -352,7 +388,7 @@ export async function sendClearContextResponse(params) {
352
388
  agentId: config.agentId,
353
389
  sessionId,
354
390
  taskId: sessionId, // Use sessionId as taskId for clearContext
355
- msgDetail: JSON.stringify(jsonRpcResponse),
391
+ msgDetail: JSON.stringify({ ...jsonRpcResponse, hostname: os.hostname() }),
356
392
  };
357
393
  await wsManager.sendMessage(sessionId, outboundMessage);
358
394
  log.log(`[CLEAR_CONTEXT] Sent clearContext response`);
@@ -386,7 +422,7 @@ export async function sendTasksCancelResponse(params) {
386
422
  agentId: config.agentId,
387
423
  sessionId,
388
424
  taskId,
389
- msgDetail: JSON.stringify(jsonRpcResponse),
425
+ msgDetail: JSON.stringify({ ...jsonRpcResponse, hostname: os.hostname() }),
390
426
  };
391
427
  await wsManager.sendMessage(sessionId, outboundMessage);
392
428
  log.log(`[TASKS_CANCEL] Sent tasks/cancel response`);
@@ -434,7 +470,7 @@ export async function sendTriggerResponse(params) {
434
470
  agentId: config.agentId,
435
471
  sessionId,
436
472
  taskId,
437
- msgDetail: JSON.stringify(jsonRpcResponse),
473
+ msgDetail: JSON.stringify({ ...jsonRpcResponse, hostname: os.hostname() }),
438
474
  };
439
475
  log.log(`[TRIGGER_RESPONSE] Sending Trigger response, text=${buildTextPreview(redactedContent)}`);
440
476
  await wsManager.sendMessage(sessionId, outboundMessage);
@@ -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[];