@ynhcj/xiaoyi-channel 0.0.199-beta → 0.0.199-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 (161) hide show
  1. package/dist/index.d.ts +3 -8
  2. package/dist/index.js +146 -2
  3. package/dist/src/bot.js +77 -109
  4. package/dist/src/channel.js +83 -64
  5. package/dist/src/client.d.ts +5 -0
  6. package/dist/src/client.js +10 -0
  7. package/dist/src/cron-command.d.ts +2 -0
  8. package/dist/src/cron-command.js +14 -8
  9. package/dist/src/cron-query-handler.js +36 -0
  10. package/dist/src/cspl/call_api.d.ts +1 -1
  11. package/dist/src/cspl/call_api.js +6 -10
  12. package/dist/src/cspl/config.d.ts +0 -1
  13. package/dist/src/cspl/config.js +68 -59
  14. package/dist/src/cspl/constants.d.ts +36 -1
  15. package/dist/src/cspl/constants.js +9 -0
  16. package/dist/src/cspl/sentinel_hook.js +33 -28
  17. package/dist/src/cspl/upload_file.js +5 -6
  18. package/dist/src/cspl/utils.d.ts +20 -9
  19. package/dist/src/cspl/utils.js +119 -75
  20. package/dist/src/formatter.js +72 -36
  21. package/dist/src/log-reporter/index.d.ts +1 -1
  22. package/dist/src/log-reporter/index.js +159 -35
  23. package/dist/src/log-reporter/openclaw-parser.d.ts +18 -0
  24. package/dist/src/log-reporter/openclaw-parser.js +94 -0
  25. package/dist/src/log-reporter/path-resolver.d.ts +5 -0
  26. package/dist/src/log-reporter/path-resolver.js +50 -0
  27. package/dist/src/log-reporter/reporter.d.ts +4 -4
  28. package/dist/src/log-reporter/reporter.js +52 -13
  29. package/dist/src/log-reporter/scanner.d.ts +7 -3
  30. package/dist/src/log-reporter/scanner.js +3 -7
  31. package/dist/src/log-reporter/types.d.ts +26 -27
  32. package/dist/src/log-reporter/uploader.d.ts +4 -3
  33. package/dist/src/log-reporter/uploader.js +32 -9
  34. package/dist/src/monitor.js +19 -9
  35. package/dist/src/outbound.js +5 -0
  36. package/dist/src/parser.d.ts +12 -0
  37. package/dist/src/parser.js +39 -13
  38. package/dist/src/provider.js +78 -16
  39. package/dist/src/reply-dispatcher.d.ts +3 -0
  40. package/dist/src/reply-dispatcher.js +178 -164
  41. package/dist/src/self-evolution-handler.js +2 -1
  42. package/dist/src/skill-retriever/config.js +2 -0
  43. package/dist/src/skill-retriever/hooks.js +1 -0
  44. package/dist/src/skill-retriever/tool-search.d.ts +3 -0
  45. package/dist/src/skill-retriever/tool-search.js +27 -11
  46. package/dist/src/skill-retriever/types.d.ts +1 -0
  47. package/dist/src/subagent-wait-state.d.ts +66 -0
  48. package/dist/src/subagent-wait-state.js +226 -0
  49. package/dist/src/task-manager.d.ts +5 -6
  50. package/dist/src/task-manager.js +5 -9
  51. package/dist/src/tools/agent-as-skill-tool.d.ts +41 -2
  52. package/dist/src/tools/agent-as-skill-tool.js +144 -151
  53. package/dist/src/tools/calendar-tool.d.ts +24 -2
  54. package/dist/src/tools/calendar-tool.js +115 -117
  55. package/dist/src/tools/call-device-tool.d.ts +20 -6
  56. package/dist/src/tools/call-device-tool.js +116 -138
  57. package/dist/src/tools/call-phone-tool.d.ts +21 -2
  58. package/dist/src/tools/call-phone-tool.js +112 -114
  59. package/dist/src/tools/check-plugin-privilege-tool.d.ts +16 -2
  60. package/dist/src/tools/check-plugin-privilege-tool.js +141 -143
  61. package/dist/src/tools/create-alarm-tool.d.ts +39 -2
  62. package/dist/src/tools/create-alarm-tool.js +229 -231
  63. package/dist/src/tools/create-all-tools.js +10 -5
  64. package/dist/src/tools/delete-alarm-tool.d.ts +15 -2
  65. package/dist/src/tools/delete-alarm-tool.js +134 -136
  66. package/dist/src/tools/device-tool-map.js +1 -7
  67. package/dist/src/tools/discover-cross-devices-tool.d.ts +16 -2
  68. package/dist/src/tools/discover-cross-devices-tool.js +121 -124
  69. package/dist/src/tools/display-a2ui-card-bypath-tool.d.ts +21 -0
  70. package/dist/src/tools/display-a2ui-card-bypath-tool.js +65 -0
  71. package/dist/src/tools/get-alarm-tool-schema.d.ts +1 -2
  72. package/dist/src/tools/get-alarm-tool-schema.js +10 -16
  73. package/dist/src/tools/get-calendar-tool-schema.d.ts +1 -2
  74. package/dist/src/tools/get-calendar-tool-schema.js +8 -12
  75. package/dist/src/tools/get-collection-tool-schema.d.ts +1 -2
  76. package/dist/src/tools/get-collection-tool-schema.js +9 -11
  77. package/dist/src/tools/get-contact-tool-schema.d.ts +1 -2
  78. package/dist/src/tools/get-contact-tool-schema.js +10 -16
  79. package/dist/src/tools/get-device-file-tool-schema.d.ts +1 -2
  80. package/dist/src/tools/get-device-file-tool-schema.js +9 -13
  81. package/dist/src/tools/get-email-tool-schema.d.ts +1 -2
  82. package/dist/src/tools/get-email-tool-schema.js +8 -11
  83. package/dist/src/tools/get-note-tool-schema.d.ts +1 -2
  84. package/dist/src/tools/get-note-tool-schema.js +9 -14
  85. package/dist/src/tools/get-photo-tool-schema.d.ts +1 -2
  86. package/dist/src/tools/get-photo-tool-schema.js +9 -12
  87. package/dist/src/tools/hmos-cli.d.ts +103 -0
  88. package/dist/src/tools/hmos-cli.js +562 -0
  89. package/dist/src/tools/image-reading-tool.d.ts +28 -2
  90. package/dist/src/tools/image-reading-tool.js +76 -76
  91. package/dist/src/tools/invoke.d.ts +88 -0
  92. package/dist/src/tools/invoke.js +1201 -0
  93. package/dist/src/tools/location-tool.d.ts +11 -2
  94. package/dist/src/tools/location-tool.js +93 -92
  95. package/dist/src/tools/login-token-tool.d.ts +20 -2
  96. package/dist/src/tools/login-token-tool.js +123 -125
  97. package/dist/src/tools/modify-alarm-tool.d.ts +47 -2
  98. package/dist/src/tools/modify-alarm-tool.js +250 -252
  99. package/dist/src/tools/modify-note-tool.d.ts +20 -2
  100. package/dist/src/tools/modify-note-tool.js +107 -109
  101. package/dist/src/tools/note-tool.d.ts +20 -2
  102. package/dist/src/tools/note-tool.js +106 -108
  103. package/dist/src/tools/query-app-message-tool.d.ts +28 -2
  104. package/dist/src/tools/query-app-message-tool.js +111 -113
  105. package/dist/src/tools/query-memory-data-tool.d.ts +28 -2
  106. package/dist/src/tools/query-memory-data-tool.js +112 -114
  107. package/dist/src/tools/query-todo-task-tool.d.ts +24 -2
  108. package/dist/src/tools/query-todo-task-tool.js +106 -108
  109. package/dist/src/tools/save-file-to-phone-tool.d.ts +24 -2
  110. package/dist/src/tools/save-file-to-phone-tool.js +130 -132
  111. package/dist/src/tools/save-media-to-gallery-tool.d.ts +24 -2
  112. package/dist/src/tools/save-media-to-gallery-tool.js +137 -139
  113. package/dist/src/tools/save-self-evolution-skill-tool.d.ts +54 -2
  114. package/dist/src/tools/save-self-evolution-skill-tool.js +194 -194
  115. package/dist/src/tools/search-alarm-tool.d.ts +34 -2
  116. package/dist/src/tools/search-alarm-tool.js +174 -176
  117. package/dist/src/tools/search-calendar-tool.d.ts +24 -2
  118. package/dist/src/tools/search-calendar-tool.js +148 -150
  119. package/dist/src/tools/search-contact-tool.d.ts +16 -2
  120. package/dist/src/tools/search-contact-tool.js +101 -103
  121. package/dist/src/tools/search-email-tool.d.ts +21 -2
  122. package/dist/src/tools/search-email-tool.js +110 -112
  123. package/dist/src/tools/search-file-tool.d.ts +16 -2
  124. package/dist/src/tools/search-file-tool.js +104 -106
  125. package/dist/src/tools/search-message-tool.d.ts +16 -2
  126. package/dist/src/tools/search-message-tool.js +103 -105
  127. package/dist/src/tools/search-note-tool.d.ts +16 -2
  128. package/dist/src/tools/search-note-tool.js +98 -100
  129. package/dist/src/tools/search-photo-gallery-tool.d.ts +21 -2
  130. package/dist/src/tools/search-photo-gallery-tool.js +35 -37
  131. package/dist/src/tools/send-cross-device-task-tool.d.ts +35 -2
  132. package/dist/src/tools/send-cross-device-task-tool.js +216 -150
  133. package/dist/src/tools/send-email-tool.d.ts +24 -2
  134. package/dist/src/tools/send-email-tool.js +108 -110
  135. package/dist/src/tools/send-file-to-user-tool.d.ts +20 -2
  136. package/dist/src/tools/send-file-to-user-tool.js +174 -178
  137. package/dist/src/tools/send-html-card-tool.d.ts +20 -2
  138. package/dist/src/tools/send-html-card-tool.js +85 -87
  139. package/dist/src/tools/send-message-tool.d.ts +20 -2
  140. package/dist/src/tools/send-message-tool.js +122 -124
  141. package/dist/src/tools/session-manager.d.ts +19 -52
  142. package/dist/src/tools/session-manager.js +95 -247
  143. package/dist/src/tools/upload-file-tool.d.ts +20 -2
  144. package/dist/src/tools/upload-file-tool.js +80 -82
  145. package/dist/src/tools/upload-photo-tool.d.ts +20 -2
  146. package/dist/src/tools/upload-photo-tool.js +68 -70
  147. package/dist/src/tools/xiaoyi-add-collection-tool.d.ts +32 -2
  148. package/dist/src/tools/xiaoyi-add-collection-tool.js +146 -148
  149. package/dist/src/tools/xiaoyi-collection-tool.d.ts +20 -2
  150. package/dist/src/tools/xiaoyi-collection-tool.js +114 -116
  151. package/dist/src/tools/xiaoyi-delete-collection-tool.d.ts +15 -2
  152. package/dist/src/tools/xiaoyi-delete-collection-tool.js +127 -129
  153. package/dist/src/tools/xiaoyi-gui-tool.d.ts +16 -2
  154. package/dist/src/tools/xiaoyi-gui-tool.js +92 -95
  155. package/dist/src/types.d.ts +6 -6
  156. package/dist/src/utils/config-manager.d.ts +3 -2
  157. package/dist/src/utils/config-manager.js +22 -2
  158. package/dist/src/utils/logger.js +3 -14
  159. package/dist/src/websocket.d.ts +1 -1
  160. package/dist/src/websocket.js +22 -13
  161. package/package.json +2 -2
@@ -2,15 +2,57 @@ 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
+ import { invokeTool } from "./tools/invoke.js";
31
+ const ALL_TOOLS = [
32
+ locationTool,
33
+ discoverCrossDevicesTool,
34
+ sendCrossDeviceTaskTool,
35
+ displayA2UICardByPathTool,
36
+ callDeviceTool,
37
+ getNoteToolSchemaTool,
38
+ getCalendarToolSchemaTool,
39
+ getContactToolSchemaTool,
40
+ getPhotoToolSchemaTool,
41
+ xiaoyiGuiTool,
42
+ getDeviceFileToolSchemaTool,
43
+ getAlarmToolSchemaTool,
44
+ getCollectionToolSchemaTool,
45
+ sendFileToUserTool,
46
+ sendHtmlCardTool,
47
+ viewPushResultTool,
48
+ imageReadingTool,
49
+ timestampToUtc8Tool,
50
+ saveSelfEvolutionSkillTool,
51
+ loginTokenTool,
52
+ agentAsSkillTool,
53
+ checkPluginPrivilegeTool,
54
+ invokeTool,
55
+ ];
14
56
  /**
15
57
  * Xiaoyi Channel Plugin for OpenClaw.
16
58
  * Implements Xiaoyi A2A protocol with dual WebSocket connections.
@@ -32,7 +74,7 @@ export const xyPlugin = {
32
74
  ],
33
75
  },
34
76
  capabilities: {
35
- chatTypes: ["direct"], // Only private chat (no group support)
77
+ chatTypes: ["direct"],
36
78
  polls: false,
37
79
  threads: false,
38
80
  media: true,
@@ -49,59 +91,11 @@ export const xyPlugin = {
49
91
  schema: xyConfigSchema,
50
92
  },
51
93
  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(", ")})`);
94
+ /** Static tool list (3.24 pattern). Tools read SessionContext at execute time via ALS. */
95
+ agentTools: () => {
96
+ const ctx = getCurrentSessionContext();
97
+ const filtered = filterToolsByDevice(ALL_TOOLS, ctx?.deviceType);
98
+ logger.log(`[DEVICE-FILTER] deviceType=${ctx?.deviceType ?? "(none)"}, tools: ${ALL_TOOLS.length} → ${filtered.length} (${filtered.map(t => t.name).join(", ")})`);
105
99
  return filtered;
106
100
  },
107
101
  messaging: {
@@ -113,7 +107,6 @@ export const xyPlugin = {
113
107
  },
114
108
  targetResolver: {
115
109
  looksLikeId: (raw) => {
116
- // 信任所有非空字符串作为有效的 sessionId
117
110
  const trimmed = raw.trim();
118
111
  return trimmed.length > 0;
119
112
  },
@@ -135,20 +128,46 @@ export const xyPlugin = {
135
128
  ? { conversationId, matchPriority: 2 }
136
129
  : null;
137
130
  },
131
+ resolveCommandConversation: ({ accountId, sessionKey }) => {
132
+ // xiaoyi-channel: the A2A sessionId IS the conversationId.
133
+ // 1. Prefer the current ALS session context (works when the command
134
+ // is processed inside an active A2A message-handling turn).
135
+ const ctx = getCurrentSessionContext();
136
+ const alsSessionId = ctx?.sessionId?.trim();
137
+ if (alsSessionId)
138
+ return { conversationId: alsSessionId };
139
+ // 2. Fall back to parsing the session key. For xiaoyi-channel the
140
+ // session key has the form:
141
+ // agent:<agentId>:xiaoyi-channel:<accountId>:<sessionId>
142
+ // The last `:`-delimited segment is the A2A sessionId.
143
+ if (sessionKey) {
144
+ const lastColon = sessionKey.lastIndexOf(":");
145
+ if (lastColon >= 0) {
146
+ const sessionId = sessionKey.slice(lastColon + 1).trim();
147
+ if (sessionId)
148
+ return { conversationId: sessionId };
149
+ }
150
+ }
151
+ return null;
152
+ },
138
153
  },
139
154
  reload: {
140
155
  configPrefixes: ["channels.xiaoyi-channel"],
141
156
  },
142
- // Gateway adapter for receiving messages
143
157
  gateway: {
144
158
  async startAccount(context) {
145
159
  const { monitorXYProvider } = await import("./monitor.js");
160
+ const { createXyAcpBindingManager } = await import("./acp-session-binding.js");
146
161
  const account = resolveXYConfig(context.cfg);
147
162
  context.setStatus?.({
148
163
  accountId: context.accountId,
149
164
  wsUrl: account.wsUrl,
150
165
  });
151
166
  context.log?.info(`[${context.accountId}] starting xiaoyi channel (wsUrl: ${account.wsUrl})`);
167
+ // Register ACP session binding adapter for this account.
168
+ // Enables sessions_spawn(runtime="acp") to bind subagent sessions
169
+ // to the current A2A conversation.
170
+ createXyAcpBindingManager({ accountId: context.accountId, cfg: context.cfg });
152
171
  return monitorXYProvider({
153
172
  config: context.cfg,
154
173
  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.
@@ -1,2 +1,2 @@
1
1
  import { ApiResponse } from './constants.js';
2
- export declare function callApi(questionText: string, api: any, sessionId: string, action: string): Promise<ApiResponse>;
2
+ export declare function callApi(payload: object, api: any, sessionId: string): Promise<ApiResponse>;
@@ -4,7 +4,7 @@
4
4
  import https from 'https';
5
5
  import { URL } from 'url';
6
6
  import { getConfig } from './config.js';
7
- import { DEFAULT_HTTPS_PORT, HTTP_STATUS_BAD_REQUEST, API_URL_SUFFIX } from './constants.js';
7
+ import { DEFAULT_HTTPS_PORT, DEFAULT_HTTP_PORT, HTTP_STATUS_BAD_REQUEST, API_URL_SUFFIX } from './constants.js';
8
8
  function buildHeadersForCelia(config, sessionId) {
9
9
  if (!config.uid || !config.apiKey || !config.skillId || !config.requestFrom) {
10
10
  throw new Error('[SENTINEL HOOK] Missing required configuration: uid, apiKey, skillId, or requestFrom is not defined');
@@ -22,7 +22,7 @@ function buildRequestOptions(url, headers, timeout) {
22
22
  const urlObj = new URL(url);
23
23
  return {
24
24
  hostname: urlObj.hostname,
25
- port: urlObj.port || DEFAULT_HTTPS_PORT,
25
+ port: urlObj.port || (urlObj.protocol === 'https:' ? DEFAULT_HTTPS_PORT : DEFAULT_HTTP_PORT),
26
26
  path: urlObj.pathname,
27
27
  method: "POST",
28
28
  headers: headers,
@@ -78,16 +78,12 @@ function handleResponse(res, resolve, reject) {
78
78
  }
79
79
  });
80
80
  }
81
- export async function callApi(questionText, api, sessionId, action) {
81
+ export async function callApi(payload, api, sessionId) {
82
82
  const config = getConfig(api);
83
83
  const headersForCelia = buildHeadersForCelia(config, sessionId);
84
- const payload = {
85
- questionText: questionText,
86
- textSource: config.textSource,
87
- action: action,
88
- extra: `${JSON.stringify({ userId: config.uid })}`
89
- };
90
- const httpBody = JSON.stringify(payload);
84
+ // 确保 uid 存在于消息体中(从 config 注入)
85
+ const payloadWithUid = { ...payload, uid: config.uid };
86
+ const httpBody = JSON.stringify(payloadWithUid);
91
87
  const apiUrl = `${config.api.url}${API_URL_SUFFIX}`;
92
88
  return new Promise((resolve, reject) => {
93
89
  const options = buildRequestOptions(apiUrl, headersForCelia, config.api.timeout);
@@ -11,6 +11,5 @@ export interface Config {
11
11
  skillId: string;
12
12
  requestFrom: string;
13
13
  textSource: string;
14
- action: string;
15
14
  }
16
15
  export declare function getConfig(api: any): Config;
@@ -3,48 +3,50 @@
3
3
  */
4
4
  import fs from 'fs';
5
5
  import path from 'path';
6
+ import { fileURLToPath } from 'url';
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
6
9
  import { CONFIG_FILE_NAME, ENV_FILE_PATH, REQUIRED_ENV_VARS } from './constants.js';
7
10
  import { logger } from '../utils/logger.js';
8
11
  let cachedConfig = null;
9
- function readEnvFile() {
10
- if (!fs.existsSync(ENV_FILE_PATH)) {
11
- throw new Error(`Environment file not found.`);
12
- }
13
- let envData;
12
+ function loadEnvContent() {
14
13
  try {
15
- envData = fs.readFileSync(ENV_FILE_PATH, 'utf-8');
14
+ return fs.readFileSync(ENV_FILE_PATH, 'utf-8');
16
15
  }
17
16
  catch (error) {
18
17
  const err = error;
19
18
  throw new Error(`Failed to read environment file. Error: ${err.message}`);
20
19
  }
21
- const env = {};
20
+ }
21
+ function parseEnvLine(line) {
22
+ const trimmedLine = line.trim();
23
+ if (!trimmedLine || trimmedLine.startsWith('#')) {
24
+ return null;
25
+ }
26
+ const firstEqualIndex = trimmedLine.indexOf('=');
27
+ if (firstEqualIndex === -1) {
28
+ return null;
29
+ }
30
+ const key = trimmedLine.substring(0, firstEqualIndex).trim();
31
+ const value = trimmedLine.substring(firstEqualIndex + 1).trim();
32
+ return { key, value };
33
+ }
34
+ function readEnvFile() {
35
+ if (!fs.existsSync(ENV_FILE_PATH)) {
36
+ throw new Error(`Environment file not found.`);
37
+ }
38
+ const envData = loadEnvContent();
39
+ const envVars = {};
22
40
  const lines = envData.split('\n');
23
41
  for (const line of lines) {
24
- const trimmedLine = line.trim();
25
- if (!trimmedLine || trimmedLine.startsWith('#')) {
26
- continue;
27
- }
28
- const firstEqualIndex = trimmedLine.indexOf('=');
29
- if (firstEqualIndex === -1) {
30
- continue;
31
- }
32
- const key = trimmedLine.substring(0, firstEqualIndex).trim();
33
- const value = trimmedLine.substring(firstEqualIndex + 1).trim();
34
- if (key && REQUIRED_ENV_VARS.includes(key)) {
35
- env[key] = value;
42
+ const parsed = parseEnvLine(line);
43
+ if (parsed && parsed.key && REQUIRED_ENV_VARS.includes(parsed.key)) {
44
+ envVars[parsed.key] = parsed.value;
36
45
  }
37
46
  }
38
- return env;
47
+ return envVars;
39
48
  }
40
- export function getConfig(api) {
41
- if (cachedConfig) {
42
- return cachedConfig;
43
- }
44
- const configPath = path.join(__dirname, CONFIG_FILE_NAME);
45
- if (!fs.existsSync(configPath)) {
46
- throw new Error(`Config file not found: ${CONFIG_FILE_NAME}`);
47
- }
49
+ function loadConfigFile(configPath) {
48
50
  let configData;
49
51
  try {
50
52
  configData = fs.readFileSync(configPath, 'utf-8');
@@ -52,58 +54,65 @@ export function getConfig(api) {
52
54
  catch (error) {
53
55
  throw new Error(`Failed to read config file: ${CONFIG_FILE_NAME}.`);
54
56
  }
55
- let parsedConfig;
56
57
  try {
57
- parsedConfig = JSON.parse(configData);
58
+ return JSON.parse(configData);
58
59
  }
59
60
  catch (error) {
60
61
  throw new Error(`Failed to parse config file: ${CONFIG_FILE_NAME}.`);
61
62
  }
62
- if (!parsedConfig || typeof parsedConfig !== 'object') {
63
- throw new Error(`Invalid config structure: ${CONFIG_FILE_NAME}. Expected an object.`);
64
- }
65
- const config = parsedConfig;
66
- if (!config.api || typeof config.api !== 'object') {
67
- throw new Error(`Invalid config: missing or invalid 'api' section in ${CONFIG_FILE_NAME}`);
68
- }
69
- if (!config.api.timeout || typeof config.api.timeout !== 'number') {
70
- throw new Error(`Invalid config: missing or invalid 'api.timeout' in ${CONFIG_FILE_NAME}`);
71
- }
72
- if (!config.skillId || typeof config.skillId !== 'string') {
73
- throw new Error(`Invalid config: missing or invalid 'skillId' in ${CONFIG_FILE_NAME}`);
74
- }
75
- if (!config.requestFrom || typeof config.requestFrom !== 'string') {
76
- throw new Error(`Invalid config: missing or invalid 'requestFrom' in ${CONFIG_FILE_NAME}`);
77
- }
78
- if (!config.textSource || typeof config.textSource !== 'string') {
79
- throw new Error(`Invalid config: missing or invalid 'textSource' in ${CONFIG_FILE_NAME}`);
80
- }
81
- if (!config.action || typeof config.action !== 'string') {
82
- throw new Error(`Invalid config: missing or invalid 'action' in ${CONFIG_FILE_NAME}`);
63
+ }
64
+ function validateConfigStructure(config) {
65
+ const validators = [
66
+ { field: 'api', check: () => !config.api || typeof config.api !== 'object' },
67
+ { field: 'api.timeout', check: () => !config.api?.timeout || typeof config.api.timeout !== 'number' },
68
+ { field: 'skillId', check: () => !config.skillId || typeof config.skillId !== 'string' },
69
+ { field: 'requestFrom', check: () => !config.requestFrom || typeof config.requestFrom !== 'string' },
70
+ { field: 'textSource', check: () => !config.textSource || typeof config.textSource !== 'string' },
71
+ ];
72
+ for (const { field, check } of validators) {
73
+ if (check()) {
74
+ throw new Error(`Invalid config: missing or invalid '${field}' in ${CONFIG_FILE_NAME}`);
75
+ }
83
76
  }
84
- let env;
77
+ }
78
+ function validateEnvVars() {
79
+ let envVars;
85
80
  try {
86
- env = readEnvFile();
81
+ envVars = readEnvFile();
87
82
  }
88
83
  catch (error) {
89
84
  const err = error;
90
85
  throw new Error(`Failed to load environment variables from env files: ${err.message}`);
91
86
  }
92
- const personalApiKey = env['PERSONAL-API-KEY'];
87
+ const personalApiKey = envVars['PERSONAL-API-KEY'];
93
88
  if (!personalApiKey || typeof personalApiKey !== 'string' || personalApiKey.trim() === '') {
94
89
  throw new Error(`Missing or empty 'PERSONAL-API-KEY' in env files`);
95
90
  }
96
- const personalUid = env['PERSONAL-UID'];
91
+ const personalUid = envVars['PERSONAL-UID'];
97
92
  if (!personalUid || typeof personalUid !== 'string' || personalUid.trim() === '') {
98
93
  throw new Error(`Missing or empty 'PERSONAL-UID' in env files`);
99
94
  }
100
- const serviceUrl = env['SERVICE_URL'];
95
+ const serviceUrl = envVars.SERVICE_URL;
101
96
  if (!serviceUrl || typeof serviceUrl !== 'string' || serviceUrl.trim() === '') {
102
97
  throw new Error(`Missing or empty 'SERVICE_URL' in env files`);
103
98
  }
104
- config.apiKey = personalApiKey.trim();
105
- config.uid = personalUid.trim();
106
- config.api.url = serviceUrl.trim();
99
+ return envVars;
100
+ }
101
+ export function getConfig(api) {
102
+ if (cachedConfig) {
103
+ return cachedConfig;
104
+ }
105
+ const configPath = path.join(__dirname, CONFIG_FILE_NAME);
106
+ if (!fs.existsSync(configPath)) {
107
+ throw new Error(`Config file not found: ${CONFIG_FILE_NAME}`);
108
+ }
109
+ const parsedConfig = loadConfigFile(configPath);
110
+ const config = parsedConfig;
111
+ validateConfigStructure(config);
112
+ const envVars = validateEnvVars();
113
+ config.apiKey = envVars['PERSONAL-API-KEY'].trim();
114
+ config.uid = envVars['PERSONAL-UID'].trim();
115
+ config.api.url = envVars.SERVICE_URL.trim();
107
116
  cachedConfig = config;
108
117
  logger.log(`[SENTINEL HOOK] Config loaded successfully`);
109
118
  return cachedConfig;
@@ -11,7 +11,42 @@ export interface ApiPayload {
11
11
  questionText: string;
12
12
  textSource: string;
13
13
  action: string;
14
- extra?: string;
14
+ }
15
+ export declare const RESULT_CODE_MAP: Record<number, string>;
16
+ export interface NewRequestPayload {
17
+ taskID: string;
18
+ sessionID: string;
19
+ uid: string;
20
+ businessID: string;
21
+ sceneID: string;
22
+ checkPoint: number;
23
+ interActionID: number;
24
+ loginType?: string;
25
+ reqTime?: string;
26
+ message: object;
27
+ }
28
+ export interface NewApiResponse {
29
+ data: {
30
+ taskID: string;
31
+ resultCode: number;
32
+ resultMessage?: string;
33
+ securityResult: string;
34
+ riskLabels?: string[];
35
+ riskDegree?: Array<{
36
+ riskLabel: string;
37
+ score: number;
38
+ }>;
39
+ riskLabelCount?: Array<{
40
+ riskLabel: string;
41
+ count: number;
42
+ }>;
43
+ actionRiskResult?: {
44
+ riskScore: number;
45
+ riskTag: string[];
46
+ };
47
+ };
48
+ retCode?: string;
49
+ retMsg?: string;
15
50
  }
16
51
  export interface ApiResponse {
17
52
  [key: string]: any;
@@ -1,6 +1,15 @@
1
1
  /*
2
2
  * 版权所有 (c) 华为技术有限公司 2026-2026
3
3
  */
4
+ // resultCode 错误码映射
5
+ export const RESULT_CODE_MAP = {
6
+ 0: 'Success',
7
+ 1: 'Parameter is invalid',
8
+ 2: "Parameter's format is invalid",
9
+ 3: 'The request frequency exceeds the limit',
10
+ 4: "Parameter's size is invalid",
11
+ 5: 'The text detection is abnormal',
12
+ };
4
13
  // 常量配置
5
14
  export const MIN_TEXT_LENGTH = 0;
6
15
  export const MAX_TEXT_LENGTH = 4096;