@ynhcj/xiaoyi-channel 1.1.27 → 1.1.29

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 (85) hide show
  1. package/dist/index.js +26 -69
  2. package/dist/src/bot.js +132 -73
  3. package/dist/src/channel.js +59 -5
  4. package/dist/src/client.js +13 -23
  5. package/dist/src/cron-query-handler.d.ts +1 -11
  6. package/dist/src/cron-query-handler.js +96 -8
  7. package/dist/src/cspl/call_api.d.ts +1 -1
  8. package/dist/src/cspl/call_api.js +2 -2
  9. package/dist/src/cspl/config.d.ts +4 -17
  10. package/dist/src/cspl/config.js +100 -70
  11. package/dist/src/cspl/constants.d.ts +49 -24
  12. package/dist/src/cspl/constants.js +46 -16
  13. package/dist/src/cspl/sentinel_hook.js +11 -6
  14. package/dist/src/cspl/steer-context.js +1 -1
  15. package/dist/src/cspl/utils.d.ts +17 -2
  16. package/dist/src/cspl/utils.js +271 -15
  17. package/dist/src/file-upload.d.ts +5 -0
  18. package/dist/src/file-upload.js +102 -0
  19. package/dist/src/formatter.d.ts +43 -1
  20. package/dist/src/formatter.js +171 -41
  21. package/dist/src/monitor.js +64 -43
  22. package/dist/src/outbound.js +8 -9
  23. package/dist/src/parser.d.ts +8 -1
  24. package/dist/src/parser.js +71 -0
  25. package/dist/src/provider.js +51 -17
  26. package/dist/src/push.d.ts +11 -1
  27. package/dist/src/push.js +101 -17
  28. package/dist/src/reply-dispatcher.js +152 -59
  29. package/dist/src/self-evolution-handler.d.ts +1 -1
  30. package/dist/src/self-evolution-handler.js +14 -3
  31. package/dist/src/task-manager.js +6 -10
  32. package/dist/src/tools/calendar-tool.js +3 -2
  33. package/dist/src/tools/call-phone-tool.js +3 -2
  34. package/dist/src/tools/check-plugin-privilege-tool.d.ts +6 -0
  35. package/dist/src/tools/check-plugin-privilege-tool.js +182 -0
  36. package/dist/src/tools/create-alarm-tool.js +3 -2
  37. package/dist/src/tools/create-all-tools.js +11 -3
  38. package/dist/src/tools/delete-alarm-tool.js +3 -2
  39. package/dist/src/tools/device-tool-map.js +1 -0
  40. package/dist/src/tools/display-a2ui-card-tool.d.ts +2 -0
  41. package/dist/src/tools/display-a2ui-card-tool.js +85 -0
  42. package/dist/src/tools/get-collection-tool-schema.js +1 -1
  43. package/dist/src/tools/location-tool.js +3 -2
  44. package/dist/src/tools/modify-alarm-tool.js +20 -2
  45. package/dist/src/tools/modify-note-tool.js +3 -2
  46. package/dist/src/tools/note-tool.js +3 -2
  47. package/dist/src/tools/query-app-message-tool.js +4 -3
  48. package/dist/src/tools/query-memory-data-tool.js +4 -3
  49. package/dist/src/tools/query-todo-task-tool.js +4 -3
  50. package/dist/src/tools/save-file-to-phone-tool.js +3 -2
  51. package/dist/src/tools/save-media-to-gallery-tool.js +3 -2
  52. package/dist/src/tools/schema-tool-factory.js +1 -1
  53. package/dist/src/tools/search-alarm-tool.js +3 -2
  54. package/dist/src/tools/search-calendar-tool.js +3 -2
  55. package/dist/src/tools/search-contact-tool.js +3 -2
  56. package/dist/src/tools/search-email-tool.js +4 -3
  57. package/dist/src/tools/search-file-tool.js +8 -9
  58. package/dist/src/tools/search-message-tool.js +2 -1
  59. package/dist/src/tools/search-note-tool.js +3 -2
  60. package/dist/src/tools/search-photo-gallery-tool.js +5 -4
  61. package/dist/src/tools/send-email-tool.js +4 -3
  62. package/dist/src/tools/send-file-to-user-tool.d.ts +1 -1
  63. package/dist/src/tools/send-file-to-user-tool.js +37 -8
  64. package/dist/src/tools/send-html-card-tool.d.ts +7 -0
  65. package/dist/src/tools/send-html-card-tool.js +113 -0
  66. package/dist/src/tools/send-message-tool.js +2 -1
  67. package/dist/src/tools/session-manager.d.ts +17 -1
  68. package/dist/src/tools/session-manager.js +87 -1
  69. package/dist/src/tools/upload-file-tool.js +9 -7
  70. package/dist/src/tools/upload-photo-tool.js +5 -4
  71. package/dist/src/tools/xiaoyi-add-collection-tool.js +5 -3
  72. package/dist/src/tools/xiaoyi-collection-tool.js +4 -3
  73. package/dist/src/tools/xiaoyi-delete-collection-tool.js +4 -3
  74. package/dist/src/tools/xiaoyi-gui-tool.js +8 -2
  75. package/dist/src/trigger-handler.js +4 -7
  76. package/dist/src/types.d.ts +25 -1
  77. package/dist/src/utils/config-manager.js +3 -6
  78. package/dist/src/utils/logger.d.ts +8 -0
  79. package/dist/src/utils/logger.js +69 -34
  80. package/dist/src/utils/pushdata-manager.js +1 -5
  81. package/dist/src/utils/pushid-manager.js +1 -2
  82. package/dist/src/utils/runtime-manager.js +1 -4
  83. package/dist/src/websocket.d.ts +3 -0
  84. package/dist/src/websocket.js +242 -38
  85. package/package.json +1 -1
@@ -2,9 +2,15 @@ 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 } from "./tools/session-manager.js";
5
+ import { getCurrentSessionContext, registerSession } from "./tools/session-manager.js";
6
6
  import { createAllTools } from "./tools/create-all-tools.js";
7
7
  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-";
8
14
  /**
9
15
  * Xiaoyi Channel Plugin for OpenClaw.
10
16
  * Implements Xiaoyi A2A protocol with dual WebSocket connections.
@@ -43,11 +49,59 @@ export const xyPlugin = {
43
49
  schema: xyConfigSchema,
44
50
  },
45
51
  outbound: xyOutbound,
46
- agentTools: () => {
47
- const ctx = getCurrentSessionContext();
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
+ }
48
102
  const allTools = createAllTools(ctx);
49
- const filtered = filterToolsByDevice(allTools, ctx?.deviceType);
50
- logger.log(`[DEVICE-FILTER] deviceType=${ctx?.deviceType ?? "(none)"}, tools: ${allTools.length} → ${filtered.length} (${filtered.map(t => t.name).join(", ")})`);
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(", ")})`);
51
105
  return filtered;
52
106
  },
53
107
  messaging: {
@@ -24,10 +24,10 @@ export function getXYWebSocketManager(config, runtime) {
24
24
  return cached;
25
25
  }
26
26
  // Create new manager
27
- logger.log(`[WS-MANAGER-CACHE] 🆕 Creating new WebSocket manager: ${cacheKey}, total managers before: ${wsManagerCache.size}`);
27
+ logger.log(`[WS-MANAGER-CACHE] Creating new WebSocket manager: ${cacheKey}, total managers before: ${wsManagerCache.size}`);
28
28
  cached = new XYWebSocketManager(config, runtime);
29
29
  wsManagerCache.set(cacheKey, cached);
30
- logger.log(`[WS-MANAGER-CACHE] 📊 Total managers after creation: ${wsManagerCache.size}`);
30
+ logger.log(`[WS-MANAGER-CACHE] Total managers after creation: ${wsManagerCache.size}`);
31
31
  return cached;
32
32
  }
33
33
  /**
@@ -38,13 +38,13 @@ export function removeXYWebSocketManager(config) {
38
38
  const cacheKey = `${config.apiKey}-${config.agentId}`;
39
39
  const manager = wsManagerCache.get(cacheKey);
40
40
  if (manager) {
41
- logger.log(`🗑️ [WS-MANAGER-CACHE] Removing manager from cache: ${cacheKey}`);
41
+ logger.log(`[WS-MANAGER-CACHE] Removing manager from cache: ${cacheKey}`);
42
42
  manager.disconnect();
43
43
  wsManagerCache.delete(cacheKey);
44
- logger.log(`🗑️ [WS-MANAGER-CACHE] Manager removed, remaining managers: ${wsManagerCache.size}`);
44
+ logger.log(`[WS-MANAGER-CACHE] Manager removed, remaining managers: ${wsManagerCache.size}`);
45
45
  }
46
46
  else {
47
- logger.log(`⚠️ [WS-MANAGER-CACHE] Manager not found in cache: ${cacheKey}`);
47
+ logger.log(`[WS-MANAGER-CACHE] Manager not found in cache: ${cacheKey}`);
48
48
  }
49
49
  }
50
50
  /**
@@ -68,35 +68,25 @@ export function getCachedManagerCount() {
68
68
  * Helps identify connection issues and orphan connections.
69
69
  */
70
70
  export function diagnoseAllManagers() {
71
- logger.log(`Total cached managers: ${wsManagerCache.size}`);
71
+ logger.log(`[DIAG] Total cached managers: ${wsManagerCache.size}`);
72
72
  if (wsManagerCache.size === 0) {
73
- logger.log("ℹ️ No managers in cache");
73
+ logger.log("[DIAG] No managers in cache");
74
74
  return;
75
75
  }
76
76
  let orphanCount = 0;
77
77
  wsManagerCache.forEach((manager, key) => {
78
78
  const diag = manager.getConnectionDiagnostics();
79
- logger.log(` Total event listeners on manager: ${diag.totalEventListeners}`);
80
- // Connection
81
- logger.log(` 🔌 Connection:`);
82
- logger.log(` - Exists: ${diag.connection.exists}`);
83
- logger.log(` - ReadyState: ${diag.connection.readyState}`);
84
- logger.log(` - State connected/ready: ${diag.connection.stateConnected}/${diag.connection.stateReady}`);
85
- logger.log(` - Reconnect attempts: ${diag.connection.reconnectAttempts}`);
86
- logger.log(` - Listeners on WebSocket: ${diag.connection.listenerCount}`);
87
- logger.log(` - Heartbeat active: ${diag.connection.heartbeatActive}`);
88
- logger.log(` - Has reconnect timer: ${diag.connection.hasReconnectTimer}`);
79
+ logger.log(`[DIAG] Manager ${key} — event listeners: ${diag.totalEventListeners} | Connection: exists=${diag.connection.exists}, readyState=${diag.connection.readyState}, stateConnected=${diag.connection.stateConnected}/${diag.connection.stateReady}, reconnectAttempts=${diag.connection.reconnectAttempts}, wsListeners=${diag.connection.listenerCount}, heartbeatActive=${diag.connection.heartbeatActive}, hasReconnectTimer=${diag.connection.hasReconnectTimer}`);
89
80
  if (diag.connection.isOrphan) {
90
- logger.log(` ⚠️ ORPHAN CONNECTION DETECTED!`);
81
+ logger.log(`[DIAG] ORPHAN CONNECTION DETECTED on manager: ${key}`);
91
82
  orphanCount++;
92
83
  }
93
84
  });
94
85
  if (orphanCount > 0) {
95
- logger.log(`⚠️ Total orphan connections found: ${orphanCount}`);
96
- logger.log(`💡 Suggestion: These connections should be cleaned up`);
86
+ logger.log(`[DIAG] Total orphan connections found: ${orphanCount} — these connections should be cleaned up`);
97
87
  }
98
88
  else {
99
- logger.log(`✅ No orphan connections found`);
89
+ logger.log("[DIAG] No orphan connections found");
100
90
  }
101
91
  }
102
92
  /**
@@ -108,13 +98,13 @@ export function cleanupOrphanConnections() {
108
98
  wsManagerCache.forEach((manager, key) => {
109
99
  const diag = manager.getConnectionDiagnostics();
110
100
  if (diag.connection.isOrphan) {
111
- logger.log(`🧹 Cleaning up orphan connections in manager: ${key}`);
101
+ logger.log(`[CLEANUP] Cleaning up orphan connections in manager: ${key}`);
112
102
  manager.disconnect();
113
103
  cleanedCount++;
114
104
  }
115
105
  });
116
106
  if (cleanedCount > 0) {
117
- logger.log(`🧹 Cleaned up ${cleanedCount} manager(s) with orphan connections`);
107
+ logger.log(`[CLEANUP] Cleaned up ${cleanedCount} manager(s) with orphan connections`);
118
108
  }
119
109
  return cleanedCount;
120
110
  }
@@ -1,17 +1,7 @@
1
- export type CronQueryAction = "list" | "status" | "runs" | "add" | "update" | "remove" | "run";
2
- export interface CronQueryEventContext {
3
- action: CronQueryAction;
4
- jobId?: string;
5
- params?: Record<string, unknown>;
6
- /** Original A2A message fields for routing the response. */
7
- sessionId?: string;
8
- taskId?: string;
9
- messageId?: string;
10
- }
11
1
  /**
12
2
  * Handle a cron-query-event.
13
3
  *
14
4
  * Calls the Gateway cron RPC and sends the result back through sendCommand
15
5
  * as a System.CronQuery command with the full result object in payload.ans.
16
6
  */
17
- export declare function handleCronQueryEvent(context: CronQueryEventContext, cfg?: unknown): Promise<void>;
7
+ export declare function handleCronQueryEvent(context: any, cfg: any): Promise<void>;
@@ -4,9 +4,12 @@
4
4
  // result back to the client via sendCommand as a System.CronQuery
5
5
  // command with the result in payload.ans.
6
6
  import { callGatewayTool } from "openclaw/plugin-sdk/agent-harness-runtime";
7
+ import * as os from "os";
7
8
  import { sendCommand } from "./formatter.js";
8
9
  import { resolveXYConfig } from "./config.js";
9
10
  import { logger } from "./utils/logger.js";
11
+ import { readFileSync, readdirSync } from "fs";
12
+ import { join } from "path";
10
13
  const GATEWAY_TIMEOUT_MS = 60_000;
11
14
  /**
12
15
  * Handle a cron-query-event.
@@ -16,7 +19,8 @@ const GATEWAY_TIMEOUT_MS = 60_000;
16
19
  */
17
20
  export async function handleCronQueryEvent(context, cfg) {
18
21
  const { action, jobId, params, sessionId, taskId, messageId } = context;
19
- logger.log(`[CRON-QUERY] Received event: action=${action}, jobId=${jobId ?? "(none)"}`);
22
+ const log = logger.withContext(sessionId ?? "", taskId ?? "");
23
+ log.log(`[CRON-QUERY] Received event: action=${action}, jobId=${jobId ?? "(none)"}`);
20
24
  let result;
21
25
  let error;
22
26
  try {
@@ -54,19 +58,22 @@ export async function handleCronQueryEvent(context, cfg) {
54
58
  ...params,
55
59
  });
56
60
  break;
61
+ case "queryTimeList":
62
+ result = await queryTimeListLocal();
63
+ break;
57
64
  default:
58
65
  error = `Unknown action: ${context.action}`;
59
- logger.error(`[CRON-QUERY] ${error}`);
66
+ log.error(`[CRON-QUERY] ${error}`);
60
67
  result = { error };
61
68
  }
62
69
  }
63
70
  catch (err) {
64
71
  error = err instanceof Error ? err.message : String(err);
65
- logger.error(`[CRON-QUERY] RPC call failed for action=${action}:`, err);
72
+ log.error(`[CRON-QUERY] RPC call failed for action=${action}:`, err);
66
73
  result = { error };
67
74
  }
68
75
  // Log the result
69
- logger.log(`[CRON-QUERY] RPC result for action=${action}: ${JSON.stringify(result, null, 2)}`);
76
+ log.log(`[CRON-QUERY] RPC result for action=${action}: ${JSON.stringify(result, null, 2)}`);
70
77
  // Send result back via sendCommand as System.CronQuery with payload.ans
71
78
  if (cfg && sessionId && taskId && messageId) {
72
79
  try {
@@ -87,15 +94,96 @@ export async function handleCronQueryEvent(context, cfg) {
87
94
  taskId,
88
95
  messageId,
89
96
  command,
90
- final: true,
97
+ final: sessionId.toLowerCase().endsWith("cronquery"),
91
98
  });
92
- logger.log(`[CRON-QUERY] Sent response via sendCommand, action=${action}`);
99
+ log.log(`[CRON-QUERY] Sent response via sendCommand, action=${action}`);
93
100
  }
94
101
  catch (sendErr) {
95
- logger.error(`[CRON-QUERY] Failed to send response via sendCommand:`, sendErr);
102
+ log.error(`[CRON-QUERY] Failed to send response via sendCommand:`, sendErr);
96
103
  }
97
104
  }
98
105
  else {
99
- logger.warn(`[CRON-QUERY] Missing cfg/sessionId/taskId/messageId, skipping sendCommand`);
106
+ log.warn(`[CRON-QUERY] Missing cfg/sessionId/taskId/messageId, skipping sendCommand`);
107
+ }
108
+ }
109
+ /**
110
+ * Read local cron folder directly (bypassing openclaw RPC) and return
111
+ * run records from the last 7 days, grouped by date and sorted by time.
112
+ *
113
+ * Data sources:
114
+ * - state/cron/jobs.json → job id → name mapping
115
+ * - state/cron/runs/*.jsonl → run records (one JSON per line)
116
+ *
117
+ * Return format:
118
+ * [ { "YYYY-MM-DD": [ { run record with .name }, ... ] }, ... ]
119
+ */
120
+ async function queryTimeListLocal() {
121
+ const cronDir = join(os.homedir(), ".openclaw", "cron");
122
+ const jobsPath = join(cronDir, "jobs.json");
123
+ const runsDir = join(cronDir, "runs");
124
+ // 1. Build jobId → name map from jobs.json
125
+ const jobNameMap = {};
126
+ try {
127
+ const jobsRaw = readFileSync(jobsPath, "utf-8");
128
+ const jobsData = JSON.parse(jobsRaw);
129
+ for (const job of jobsData.jobs || []) {
130
+ jobNameMap[job.id] = job.name || job.id;
131
+ }
132
+ }
133
+ catch (err) {
134
+ logger.error(`[CRON-QUERY] Failed to read jobs.json: ${err.message}`);
135
+ }
136
+ // 2. Read all run files, collect runs within last 7 days
137
+ const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
138
+ const allRuns = [];
139
+ let files = [];
140
+ try {
141
+ files = readdirSync(runsDir);
142
+ }
143
+ catch {
144
+ files = [];
145
+ }
146
+ for (const file of files) {
147
+ if (!file.endsWith(".jsonl"))
148
+ continue;
149
+ try {
150
+ const content = readFileSync(join(runsDir, file), "utf-8");
151
+ const lines = content.trim().split("\n");
152
+ for (const line of lines) {
153
+ if (!line.trim())
154
+ continue;
155
+ try {
156
+ const run = JSON.parse(line);
157
+ if (run.ts && run.ts >= sevenDaysAgo) {
158
+ run.name = jobNameMap[run.jobId] || run.jobId || "";
159
+ allRuns.push(run);
160
+ }
161
+ }
162
+ catch {
163
+ // skip malformed line
164
+ }
165
+ }
166
+ }
167
+ catch (err) {
168
+ logger.error(`[CRON-QUERY] Failed to read run file ${file}: ${err.message}`);
169
+ }
170
+ }
171
+ // 3. Sort by ts ascending
172
+ allRuns.sort((a, b) => a.ts - b.ts);
173
+ // 4. Group by date (YYYY-MM-DD in local time)
174
+ const grouped = new Map();
175
+ for (const run of allRuns) {
176
+ const d = new Date(run.ts);
177
+ const label = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
178
+ if (!grouped.has(label)) {
179
+ grouped.set(label, []);
180
+ }
181
+ grouped.get(label).push(run);
182
+ }
183
+ // 5. Convert to ordered array of single-key objects
184
+ const result = [];
185
+ for (const [date, runs] of grouped) {
186
+ result.push({ [date]: runs });
100
187
  }
188
+ return result;
101
189
  }
@@ -1,2 +1,2 @@
1
1
  import { ApiResponse } from './constants.js';
2
- export declare function callApi(questionText: string, api: any, sessionId: string): Promise<ApiResponse>;
2
+ export declare function callApi(questionText: string, api: any, sessionId: string, action: string): Promise<ApiResponse>;
@@ -78,13 +78,13 @@ function handleResponse(res, resolve, reject) {
78
78
  }
79
79
  });
80
80
  }
81
- export async function callApi(questionText, api, sessionId) {
81
+ export async function callApi(questionText, api, sessionId, action) {
82
82
  const config = getConfig(api);
83
83
  const headersForCelia = buildHeadersForCelia(config, sessionId);
84
84
  const payload = {
85
85
  questionText: questionText,
86
86
  textSource: config.textSource,
87
- action: config.action,
87
+ action: action,
88
88
  extra: `${JSON.stringify({ userId: config.uid })}`
89
89
  };
90
90
  const httpBody = JSON.stringify(payload);
@@ -1,11 +1,11 @@
1
- import type { ClawdbotConfig } from "openclaw/plugin-sdk";
2
- import type { XYChannelConfig } from "../types.js";
1
+ import { HttpHeaders } from './constants.js';
3
2
  export interface ApiConfig {
4
3
  url: string;
5
4
  timeout: number;
6
5
  }
7
- export interface CsplConfig {
6
+ export interface Config {
8
7
  api: ApiConfig;
8
+ headers?: HttpHeaders;
9
9
  uid: string;
10
10
  apiKey: string;
11
11
  skillId: string;
@@ -13,17 +13,4 @@ export interface CsplConfig {
13
13
  textSource: string;
14
14
  action: string;
15
15
  }
16
- /**
17
- * 构建 CSPL 配置。uid 和 apiKey 复用 XYChannelConfig,避免重复配置。
18
- * serviceUrl 从 .xiaoyienv 文件读取,skillId 写死在常量中。
19
- *
20
- * Accepts either ClawdbotConfig (legacy after_tool_call path) or
21
- * XYChannelConfig (AgentToolResultMiddleware path). Config is cached
22
- * after the first successful call so subsequent calls can omit the arg.
23
- */
24
- export declare function getCsplConfig(cfg?: ClawdbotConfig): CsplConfig;
25
- /**
26
- * Initialize CSPL config from an already-resolved XYChannelConfig.
27
- * Used by AgentToolResultMiddleware which has session context but not ClawdbotConfig.
28
- */
29
- export declare function initCsplConfigFromXYConfig(xyConfig: XYChannelConfig): CsplConfig;
16
+ export declare function getConfig(api: any): Config;
@@ -1,80 +1,110 @@
1
- // CSPL Hook 配置管理
2
- // uid apiKey 复用 XYChannelConfig,skillId 写死在常量中
3
- import { resolveXYConfig } from "../config.js";
4
- import { CSPL_STATIC_CONFIG, API_URL_SUFFIX, ENV_FILE_PATH } from "./constants.js";
5
- import fs from "node:fs";
6
- import { logger } from "../utils/logger.js";
1
+ /*
2
+ * 版权所有 (c) 华为技术有限公司 2026-2026
3
+ */
4
+ import fs from 'fs';
5
+ import path from 'path';
6
+ import { CONFIG_FILE_NAME, ENV_FILE_PATH, REQUIRED_ENV_VARS } from './constants.js';
7
+ import { logger } from '../utils/logger.js';
7
8
  let cachedConfig = null;
8
- function readServiceUrl() {
9
+ function readEnvFile() {
9
10
  if (!fs.existsSync(ENV_FILE_PATH)) {
10
- throw new Error(`[SENTINEL HOOK] Environment file not found: ${ENV_FILE_PATH}`);
11
+ throw new Error(`Environment file not found.`);
12
+ }
13
+ let envData;
14
+ try {
15
+ envData = fs.readFileSync(ENV_FILE_PATH, 'utf-8');
11
16
  }
12
- const envData = fs.readFileSync(ENV_FILE_PATH, "utf-8");
13
- for (const line of envData.split("\n")) {
14
- const trimmed = line.trim();
15
- if (!trimmed || trimmed.startsWith("#"))
17
+ catch (error) {
18
+ const err = error;
19
+ throw new Error(`Failed to read environment file. Error: ${err.message}`);
20
+ }
21
+ const env = {};
22
+ const lines = envData.split('\n');
23
+ for (const line of lines) {
24
+ const trimmedLine = line.trim();
25
+ if (!trimmedLine || trimmedLine.startsWith('#')) {
16
26
  continue;
17
- const eqIdx = trimmed.indexOf("=");
18
- if (eqIdx === -1)
27
+ }
28
+ const firstEqualIndex = trimmedLine.indexOf('=');
29
+ if (firstEqualIndex === -1) {
19
30
  continue;
20
- const key = trimmed.substring(0, eqIdx).trim();
21
- const value = trimmed.substring(eqIdx + 1).trim();
22
- if (key === "SERVICE_URL" && value)
23
- return value;
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;
36
+ }
24
37
  }
25
- throw new Error("[SENTINEL HOOK] Missing SERVICE_URL in env file");
38
+ return env;
26
39
  }
27
- /**
28
- * 构建 CSPL 配置。uid 和 apiKey 复用 XYChannelConfig,避免重复配置。
29
- * serviceUrl 从 .xiaoyienv 文件读取,skillId 写死在常量中。
30
- *
31
- * Accepts either ClawdbotConfig (legacy after_tool_call path) or
32
- * XYChannelConfig (AgentToolResultMiddleware path). Config is cached
33
- * after the first successful call so subsequent calls can omit the arg.
34
- */
35
- export function getCsplConfig(cfg) {
36
- if (cachedConfig)
40
+ export function getConfig(api) {
41
+ if (cachedConfig) {
37
42
  return cachedConfig;
38
- if (!cfg) {
39
- throw new Error("[SENTINEL HOOK] CSPL config not initialized: pass ClawdbotConfig on first call");
40
- }
41
- const xyConfig = resolveXYConfig(cfg);
42
- const serviceUrl = readServiceUrl();
43
- cachedConfig = {
44
- api: {
45
- url: `${serviceUrl}${API_URL_SUFFIX}`,
46
- timeout: CSPL_STATIC_CONFIG.api.timeout,
47
- },
48
- uid: xyConfig.uid,
49
- apiKey: xyConfig.apiKey,
50
- skillId: CSPL_STATIC_CONFIG.skillId,
51
- requestFrom: CSPL_STATIC_CONFIG.requestFrom,
52
- textSource: CSPL_STATIC_CONFIG.textSource,
53
- action: CSPL_STATIC_CONFIG.action,
54
- };
55
- logger.log("[SENTINEL HOOK] Config loaded (uid/apiKey from XYChannelConfig)");
56
- return cachedConfig;
57
- }
58
- /**
59
- * Initialize CSPL config from an already-resolved XYChannelConfig.
60
- * Used by AgentToolResultMiddleware which has session context but not ClawdbotConfig.
61
- */
62
- export function initCsplConfigFromXYConfig(xyConfig) {
63
- if (cachedConfig)
64
- return cachedConfig;
65
- const serviceUrl = readServiceUrl();
66
- cachedConfig = {
67
- api: {
68
- url: `${serviceUrl}${API_URL_SUFFIX}`,
69
- timeout: CSPL_STATIC_CONFIG.api.timeout,
70
- },
71
- uid: xyConfig.uid,
72
- apiKey: xyConfig.apiKey,
73
- skillId: CSPL_STATIC_CONFIG.skillId,
74
- requestFrom: CSPL_STATIC_CONFIG.requestFrom,
75
- textSource: CSPL_STATIC_CONFIG.textSource,
76
- action: CSPL_STATIC_CONFIG.action,
77
- };
78
- logger.log("[SENTINEL HOOK] Config loaded via XYChannelConfig");
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
+ }
48
+ let configData;
49
+ try {
50
+ configData = fs.readFileSync(configPath, 'utf-8');
51
+ }
52
+ catch (error) {
53
+ throw new Error(`Failed to read config file: ${CONFIG_FILE_NAME}.`);
54
+ }
55
+ let parsedConfig;
56
+ try {
57
+ parsedConfig = JSON.parse(configData);
58
+ }
59
+ catch (error) {
60
+ throw new Error(`Failed to parse config file: ${CONFIG_FILE_NAME}.`);
61
+ }
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}`);
83
+ }
84
+ let env;
85
+ try {
86
+ env = readEnvFile();
87
+ }
88
+ catch (error) {
89
+ const err = error;
90
+ throw new Error(`Failed to load environment variables from env files: ${err.message}`);
91
+ }
92
+ const personalApiKey = env['PERSONAL-API-KEY'];
93
+ if (!personalApiKey || typeof personalApiKey !== 'string' || personalApiKey.trim() === '') {
94
+ throw new Error(`Missing or empty 'PERSONAL-API-KEY' in env files`);
95
+ }
96
+ const personalUid = env['PERSONAL-UID'];
97
+ if (!personalUid || typeof personalUid !== 'string' || personalUid.trim() === '') {
98
+ throw new Error(`Missing or empty 'PERSONAL-UID' in env files`);
99
+ }
100
+ const serviceUrl = env['SERVICE_URL'];
101
+ if (!serviceUrl || typeof serviceUrl !== 'string' || serviceUrl.trim() === '') {
102
+ throw new Error(`Missing or empty 'SERVICE_URL' in env files`);
103
+ }
104
+ config.apiKey = personalApiKey.trim();
105
+ config.uid = personalUid.trim();
106
+ config.api.url = serviceUrl.trim();
107
+ cachedConfig = config;
108
+ logger.log(`[SENTINEL HOOK] Config loaded successfully`);
79
109
  return cachedConfig;
80
110
  }