@ynhcj/xiaoyi-channel 0.0.192-beta → 0.0.192-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 (143) hide show
  1. package/dist/index.js +56 -3
  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 +101 -34
  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/cspl/call_api.js +2 -0
  9. package/dist/src/cspl/sentinel_hook.js +9 -8
  10. package/dist/src/formatter.d.ts +5 -0
  11. package/dist/src/formatter.js +24 -34
  12. package/dist/src/log-reporter/config-loader.d.ts +11 -0
  13. package/dist/src/log-reporter/config-loader.js +68 -0
  14. package/dist/src/log-reporter/cursor-store.d.ts +5 -0
  15. package/dist/src/log-reporter/cursor-store.js +26 -0
  16. package/dist/src/log-reporter/index.d.ts +10 -0
  17. package/dist/src/log-reporter/index.js +77 -0
  18. package/dist/src/log-reporter/reporter.d.ts +6 -0
  19. package/dist/src/log-reporter/reporter.js +17 -0
  20. package/dist/src/log-reporter/scanner.d.ts +6 -0
  21. package/dist/src/log-reporter/scanner.js +82 -0
  22. package/dist/src/log-reporter/types.d.ts +59 -0
  23. package/dist/src/log-reporter/types.js +2 -0
  24. package/dist/src/log-reporter/uploader.d.ts +6 -0
  25. package/dist/src/log-reporter/uploader.js +32 -0
  26. package/dist/src/memory-query-handler.js +123 -4
  27. package/dist/src/monitor.js +20 -9
  28. package/dist/src/outbound.d.ts +7 -0
  29. package/dist/src/outbound.js +77 -5
  30. package/dist/src/parser.d.ts +12 -0
  31. package/dist/src/parser.js +32 -0
  32. package/dist/src/provider.js +69 -15
  33. package/dist/src/reply-dispatcher.d.ts +3 -0
  34. package/dist/src/reply-dispatcher.js +184 -154
  35. package/dist/src/self-evolution-handler.js +2 -1
  36. package/dist/src/subagent-wait-state.d.ts +66 -0
  37. package/dist/src/subagent-wait-state.js +226 -0
  38. package/dist/src/task-manager.d.ts +14 -7
  39. package/dist/src/task-manager.js +54 -15
  40. package/dist/src/tools/agent-as-skill-tool.d.ts +41 -2
  41. package/dist/src/tools/agent-as-skill-tool.js +144 -151
  42. package/dist/src/tools/calendar-tool.d.ts +24 -2
  43. package/dist/src/tools/calendar-tool.js +115 -117
  44. package/dist/src/tools/call-device-tool.d.ts +20 -6
  45. package/dist/src/tools/call-device-tool.js +116 -138
  46. package/dist/src/tools/call-phone-tool.d.ts +21 -2
  47. package/dist/src/tools/call-phone-tool.js +112 -114
  48. package/dist/src/tools/check-plugin-privilege-tool.d.ts +16 -2
  49. package/dist/src/tools/check-plugin-privilege-tool.js +141 -143
  50. package/dist/src/tools/create-alarm-tool.d.ts +39 -2
  51. package/dist/src/tools/create-alarm-tool.js +229 -231
  52. package/dist/src/tools/create-all-tools.js +2 -2
  53. package/dist/src/tools/delete-alarm-tool.d.ts +15 -2
  54. package/dist/src/tools/delete-alarm-tool.js +134 -136
  55. package/dist/src/tools/device-tool-map.js +1 -1
  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 +123 -125
  82. package/dist/src/tools/modify-alarm-tool.d.ts +47 -2
  83. package/dist/src/tools/modify-alarm-tool.js +250 -252
  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 +140 -143
  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 +174 -176
  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 +11 -50
  127. package/dist/src/tools/session-manager.js +40 -231
  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/utils/logger.js +3 -14
  141. package/dist/src/websocket.d.ts +1 -1
  142. package/dist/src/websocket.js +4 -4
  143. package/package.json +2 -2
@@ -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.
@@ -5,6 +5,7 @@ import https from 'https';
5
5
  import { URL } from 'url';
6
6
  import { getConfig } from './config.js';
7
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');
@@ -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,
@@ -10,6 +10,9 @@ export interface SendA2AResponseParams {
10
10
  text?: string;
11
11
  append: boolean;
12
12
  final: boolean;
13
+ /** Optional: reuse an existing artifactId instead of generating a new one.
14
+ * Used by subagent completion to link results to the original artifact. */
15
+ artifactId?: string;
13
16
  files?: Array<{
14
17
  fileName: string;
15
18
  fileType: string;
@@ -50,6 +53,8 @@ export interface SendStatusUpdateParams {
50
53
  messageId: string;
51
54
  text: string;
52
55
  state: "submitted" | "working" | "input-required" | "completed" | "canceled" | "failed" | "unknown";
56
+ /** When false, use the exact taskId/messageId passed in. Default true (dynamic lookup). */
57
+ useLatestTask?: boolean;
53
58
  }
54
59
  /**
55
60
  * Send an A2A task status update.
@@ -1,8 +1,8 @@
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
8
  import { isCronToolCall, getCurrentCronJobId } from "./tools/session-manager.js";
@@ -45,7 +45,7 @@ function buildTextPreview(text) {
45
45
  * Send an A2A artifact update response.
46
46
  */
47
47
  export async function sendA2AResponse(params) {
48
- const { config, sessionId, taskId, messageId, text, append, final, files, errorCode, errorMessage, log: shouldLog = true } = params;
48
+ const { config, sessionId, taskId, messageId, text, append, final, artifactId, files, errorCode, errorMessage, log: shouldLog = true } = params;
49
49
  const log = logger.withContext(sessionId, taskId);
50
50
  // 审批桥接:将 OpenClaw 的审批提示翻译成用户友好的确认文案
51
51
  const bridgedText = text === undefined ? text : rewriteOutboundApprovalText(sessionId, text);
@@ -57,7 +57,7 @@ export async function sendA2AResponse(params) {
57
57
  lastChunk: true,
58
58
  final,
59
59
  artifact: {
60
- artifactId: uuidv4(),
60
+ artifactId: artifactId ?? uuidv4(),
61
61
  parts: [],
62
62
  },
63
63
  };
@@ -98,7 +98,7 @@ export async function sendA2AResponse(params) {
98
98
  agentId: config.agentId,
99
99
  sessionId,
100
100
  taskId,
101
- msgDetail: JSON.stringify(jsonRpcResponse),
101
+ msgDetail: JSON.stringify({ ...jsonRpcResponse, hostname: os.hostname() }),
102
102
  };
103
103
  if (shouldLog) {
104
104
  const redactedText = redactSensitiveText(bridgedText ?? "");
@@ -148,7 +148,7 @@ export async function sendReasoningTextUpdate(params) {
148
148
  agentId: config.agentId,
149
149
  sessionId,
150
150
  taskId,
151
- msgDetail: JSON.stringify(jsonRpcResponse),
151
+ msgDetail: JSON.stringify({ ...jsonRpcResponse, hostname: os.hostname() }),
152
152
  };
153
153
  await wsManager.sendMessage(sessionId, outboundMessage);
154
154
  }
@@ -157,12 +157,8 @@ export async function sendReasoningTextUpdate(params) {
157
157
  * Follows A2A protocol standard format with nested status object.
158
158
  */
159
159
  export async function sendStatusUpdate(params) {
160
- const { config, sessionId, taskId, messageId, text, state } = params;
161
- // Dynamic lookup: use latest taskId/messageId from task-manager (handles steer/interrupt),
162
- // fall back to closure-captured values
163
- const currentTaskId = getCurrentTaskId(sessionId) ?? taskId;
164
- const currentMessageId = getCurrentMessageId(sessionId) ?? messageId;
165
- const log = logger.withContext(sessionId, currentTaskId);
160
+ const { config, sessionId, taskId, messageId, text, state, useLatestTask = true } = params;
161
+ const log = logger.withContext(sessionId, taskId);
166
162
  // 审批桥接和脱敏
167
163
  const bridgedText = rewriteOutboundApprovalText(sessionId, text);
168
164
  const redactedText = redactSensitiveText(bridgedText);
@@ -177,7 +173,7 @@ export async function sendStatusUpdate(params) {
177
173
  ],
178
174
  });
179
175
  const statusUpdate = {
180
- taskId: currentTaskId,
176
+ taskId,
181
177
  kind: "status-update",
182
178
  final: false, // Status updates should not end the stream
183
179
  status: {
@@ -188,7 +184,7 @@ export async function sendStatusUpdate(params) {
188
184
  // Build JSON-RPC response
189
185
  const jsonRpcResponse = {
190
186
  jsonrpc: "2.0",
191
- id: currentMessageId,
187
+ id: messageId,
192
188
  result: statusUpdate,
193
189
  };
194
190
  // Send via WebSocket
@@ -197,8 +193,8 @@ export async function sendStatusUpdate(params) {
197
193
  msgType: "agent_response",
198
194
  agentId: config.agentId,
199
195
  sessionId,
200
- taskId: currentTaskId,
201
- msgDetail: JSON.stringify(jsonRpcResponse),
196
+ taskId,
197
+ msgDetail: JSON.stringify({ ...jsonRpcResponse, hostname: os.hostname() }),
202
198
  };
203
199
  // Log complete response body
204
200
  log.log(`[A2A_STATUS] Sending status-update, text="${redactedText}"`);
@@ -270,15 +266,11 @@ export async function sendCommand(params) {
270
266
  return sendCommandViaPush({ config, command: commands[0], pushId });
271
267
  }
272
268
  // ── Normal mode: WebSocket ─────────────────────────────────────
273
- // Dynamic lookup: use latest taskId/messageId from task-manager (handles steer/interrupt),
274
- // fall back to closure-captured values
275
- const currentTaskId = getCurrentTaskId(sessionId) ?? taskId;
276
- const currentMessageId = getCurrentMessageId(sessionId) ?? messageId;
277
- const log = logger.withContext(sessionId, currentTaskId);
269
+ const log = logger.withContext(sessionId, taskId);
278
270
  // Build artifact update with command as data
279
271
  // Wrap command in commands array as per protocol requirement
280
272
  const artifact = {
281
- taskId: currentTaskId,
273
+ taskId,
282
274
  kind: "artifact-update",
283
275
  append: false,
284
276
  lastChunk: true,
@@ -300,7 +292,7 @@ export async function sendCommand(params) {
300
292
  // Build JSON-RPC response
301
293
  const jsonRpcResponse = {
302
294
  jsonrpc: "2.0",
303
- id: currentMessageId,
295
+ id: messageId,
304
296
  result: artifact,
305
297
  };
306
298
  // Send via WebSocket
@@ -309,8 +301,8 @@ export async function sendCommand(params) {
309
301
  msgType: "agent_response",
310
302
  agentId: config.agentId,
311
303
  sessionId,
312
- taskId: currentTaskId,
313
- msgDetail: JSON.stringify(jsonRpcResponse),
304
+ taskId,
305
+ msgDetail: JSON.stringify({ ...jsonRpcResponse, hostname: os.hostname() }),
314
306
  };
315
307
  // Log complete response body
316
308
  log.log(`[A2A_COMMAND] Sending command`);
@@ -329,12 +321,10 @@ export async function sendCard(params) {
329
321
  throw new Error("sendCard does not support cron mode");
330
322
  }
331
323
  // ── Normal mode: WebSocket ─────────────────────────────────────
332
- const currentTaskId = getCurrentTaskId(sessionId) ?? taskId;
333
- const currentMessageId = getCurrentMessageId(sessionId) ?? messageId;
334
- const log = logger.withContext(sessionId, currentTaskId);
324
+ const log = logger.withContext(sessionId, taskId);
335
325
  // Build artifact update with cardsInfo as data
336
326
  const artifact = {
337
- taskId: currentTaskId,
327
+ taskId,
338
328
  kind: "artifact-update",
339
329
  append: false,
340
330
  lastChunk: true,
@@ -354,7 +344,7 @@ export async function sendCard(params) {
354
344
  // Build JSON-RPC response
355
345
  const jsonRpcResponse = {
356
346
  jsonrpc: "2.0",
357
- id: currentMessageId,
347
+ id: messageId,
358
348
  result: artifact,
359
349
  };
360
350
  // Send via WebSocket
@@ -363,8 +353,8 @@ export async function sendCard(params) {
363
353
  msgType: "agent_response",
364
354
  agentId: config.agentId,
365
355
  sessionId,
366
- taskId: currentTaskId,
367
- msgDetail: JSON.stringify(jsonRpcResponse),
356
+ taskId,
357
+ msgDetail: JSON.stringify({ ...jsonRpcResponse, hostname: os.hostname() }),
368
358
  };
369
359
  log.log(`[A2A_CARD] Sending card`);
370
360
  await wsManager.sendMessage(sessionId, outboundMessage);
@@ -398,7 +388,7 @@ export async function sendClearContextResponse(params) {
398
388
  agentId: config.agentId,
399
389
  sessionId,
400
390
  taskId: sessionId, // Use sessionId as taskId for clearContext
401
- msgDetail: JSON.stringify(jsonRpcResponse),
391
+ msgDetail: JSON.stringify({ ...jsonRpcResponse, hostname: os.hostname() }),
402
392
  };
403
393
  await wsManager.sendMessage(sessionId, outboundMessage);
404
394
  log.log(`[CLEAR_CONTEXT] Sent clearContext response`);
@@ -432,7 +422,7 @@ export async function sendTasksCancelResponse(params) {
432
422
  agentId: config.agentId,
433
423
  sessionId,
434
424
  taskId,
435
- msgDetail: JSON.stringify(jsonRpcResponse),
425
+ msgDetail: JSON.stringify({ ...jsonRpcResponse, hostname: os.hostname() }),
436
426
  };
437
427
  await wsManager.sendMessage(sessionId, outboundMessage);
438
428
  log.log(`[TASKS_CANCEL] Sent tasks/cancel response`);
@@ -480,7 +470,7 @@ export async function sendTriggerResponse(params) {
480
470
  agentId: config.agentId,
481
471
  sessionId,
482
472
  taskId,
483
- msgDetail: JSON.stringify(jsonRpcResponse),
473
+ msgDetail: JSON.stringify({ ...jsonRpcResponse, hostname: os.hostname() }),
484
474
  };
485
475
  log.log(`[TRIGGER_RESPONSE] Sending Trigger response, text=${buildTextPreview(redactedContent)}`);
486
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[];
@@ -0,0 +1,68 @@
1
+ // Config loader: reads JSON config file, validates fields, resolves date wildcards to actual files
2
+ import { readFileSync, readdirSync } from "fs";
3
+ import { dirname, basename, join } from "path";
4
+ // Replace longer tokens first to avoid partial matches
5
+ const WILDCARD_TOKENS = [
6
+ ["{year-month-day}", "\\d{4}-\\d{2}-\\d{2}"],
7
+ ["{year}{month}{day}", "\\d{8}"],
8
+ ["{year}", "\\d{4}"],
9
+ ["{month}", "\\d{2}"],
10
+ ["{day}", "\\d{2}"],
11
+ ];
12
+ /**
13
+ * Load and validate the JSON config file.
14
+ * Falls back to defaults for optional fields.
15
+ */
16
+ export function loadConfig(configPath) {
17
+ const raw = readFileSync(configPath, "utf-8");
18
+ const parsed = JSON.parse(raw);
19
+ if (!parsed.logFiles || !Array.isArray(parsed.logFiles) || parsed.logFiles.length === 0) {
20
+ throw new Error("log-reporter config: 'logFiles' must be a non-empty array");
21
+ }
22
+ for (const lf of parsed.logFiles) {
23
+ if (!lf.path || !lf.name) {
24
+ throw new Error(`log-reporter config: each logFile must have 'path' and 'name', got ${JSON.stringify(lf)}`);
25
+ }
26
+ }
27
+ return {
28
+ scanIntervalMs: parsed.scanIntervalMs ?? 600000,
29
+ bakDir: parsed.bakDir ?? "/tmp/openclaw",
30
+ reportUrl: parsed.reportUrl ?? "",
31
+ logFiles: parsed.logFiles,
32
+ };
33
+ }
34
+ /**
35
+ * Convert a path with date wildcards into a RegExp that matches the filename part only.
36
+ * Returns { dir, regex } where dir is the directory portion and regex matches filenames.
37
+ */
38
+ function pathToPattern(templatePath) {
39
+ const dir = dirname(templatePath);
40
+ let pattern = basename(templatePath);
41
+ // Escape regex special chars in the literal parts, then replace tokens
42
+ pattern = escapeRegex(pattern);
43
+ for (const [token, replacement] of WILDCARD_TOKENS) {
44
+ pattern = pattern.replaceAll(escapeRegex(token), replacement);
45
+ }
46
+ return { dir, regex: new RegExp(`^${pattern}$`) };
47
+ }
48
+ function escapeRegex(s) {
49
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
50
+ }
51
+ /**
52
+ * Resolve a path template with date wildcards to actual file paths on disk.
53
+ * Scans the directory and returns all files matching the pattern.
54
+ */
55
+ export function resolveLogFiles(templatePath) {
56
+ const { dir, regex } = pathToPattern(templatePath);
57
+ let entries;
58
+ try {
59
+ entries = readdirSync(dir);
60
+ }
61
+ catch {
62
+ return [];
63
+ }
64
+ return entries
65
+ .filter((f) => regex.test(f))
66
+ .map((f) => join(dir, f))
67
+ .sort(); // chronological order for date-named logs
68
+ }
@@ -0,0 +1,5 @@
1
+ import type { CursorStore, FileCursor } from "./types.js";
2
+ export declare function loadCursorStore(storePath: string): CursorStore;
3
+ export declare function saveCursorStore(storePath: string, store: CursorStore): void;
4
+ export declare function getCursor(store: CursorStore, filePath: string): FileCursor | undefined;
5
+ export declare function setCursor(store: CursorStore, filePath: string, cursor: FileCursor): void;
@@ -0,0 +1,26 @@
1
+ // Cursor state persistence — reads/writes FileCursor entries keyed by file path
2
+ import { readFileSync, writeFileSync, mkdirSync } from "fs";
3
+ import { dirname } from "path";
4
+ export function loadCursorStore(storePath) {
5
+ try {
6
+ const raw = readFileSync(storePath, "utf-8");
7
+ const parsed = JSON.parse(raw);
8
+ return { files: parsed.files ?? {} };
9
+ }
10
+ catch {
11
+ return { files: {} };
12
+ }
13
+ }
14
+ export function saveCursorStore(storePath, store) {
15
+ try {
16
+ mkdirSync(dirname(storePath), { recursive: true });
17
+ }
18
+ catch { }
19
+ writeFileSync(storePath, JSON.stringify(store, null, 2), "utf-8");
20
+ }
21
+ export function getCursor(store, filePath) {
22
+ return store.files[filePath];
23
+ }
24
+ export function setCursor(store, filePath, cursor) {
25
+ store.files[filePath] = cursor;
26
+ }
@@ -0,0 +1,10 @@
1
+ import type { LogReporterOptions } from "./types.js";
2
+ /**
3
+ * Start the log reporter. Runs the first scan immediately, then on the configured interval.
4
+ * Returns a stop function.
5
+ */
6
+ export declare function startLogReporter(options: LogReporterOptions): Promise<() => void>;
7
+ /**
8
+ * Stop the log reporter timer.
9
+ */
10
+ export declare function stopLogReporter(): void;