@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.
- package/dist/index.js +26 -69
- package/dist/src/bot.js +132 -73
- package/dist/src/channel.js +59 -5
- package/dist/src/client.js +13 -23
- package/dist/src/cron-query-handler.d.ts +1 -11
- package/dist/src/cron-query-handler.js +96 -8
- package/dist/src/cspl/call_api.d.ts +1 -1
- package/dist/src/cspl/call_api.js +2 -2
- package/dist/src/cspl/config.d.ts +4 -17
- package/dist/src/cspl/config.js +100 -70
- package/dist/src/cspl/constants.d.ts +49 -24
- package/dist/src/cspl/constants.js +46 -16
- package/dist/src/cspl/sentinel_hook.js +11 -6
- package/dist/src/cspl/steer-context.js +1 -1
- package/dist/src/cspl/utils.d.ts +17 -2
- package/dist/src/cspl/utils.js +271 -15
- package/dist/src/file-upload.d.ts +5 -0
- package/dist/src/file-upload.js +102 -0
- package/dist/src/formatter.d.ts +43 -1
- package/dist/src/formatter.js +171 -41
- package/dist/src/monitor.js +64 -43
- package/dist/src/outbound.js +8 -9
- package/dist/src/parser.d.ts +8 -1
- package/dist/src/parser.js +71 -0
- package/dist/src/provider.js +51 -17
- package/dist/src/push.d.ts +11 -1
- package/dist/src/push.js +101 -17
- package/dist/src/reply-dispatcher.js +152 -59
- package/dist/src/self-evolution-handler.d.ts +1 -1
- package/dist/src/self-evolution-handler.js +14 -3
- package/dist/src/task-manager.js +6 -10
- package/dist/src/tools/calendar-tool.js +3 -2
- package/dist/src/tools/call-phone-tool.js +3 -2
- package/dist/src/tools/check-plugin-privilege-tool.d.ts +6 -0
- package/dist/src/tools/check-plugin-privilege-tool.js +182 -0
- package/dist/src/tools/create-alarm-tool.js +3 -2
- package/dist/src/tools/create-all-tools.js +11 -3
- package/dist/src/tools/delete-alarm-tool.js +3 -2
- package/dist/src/tools/device-tool-map.js +1 -0
- package/dist/src/tools/display-a2ui-card-tool.d.ts +2 -0
- package/dist/src/tools/display-a2ui-card-tool.js +85 -0
- package/dist/src/tools/get-collection-tool-schema.js +1 -1
- package/dist/src/tools/location-tool.js +3 -2
- package/dist/src/tools/modify-alarm-tool.js +20 -2
- package/dist/src/tools/modify-note-tool.js +3 -2
- package/dist/src/tools/note-tool.js +3 -2
- package/dist/src/tools/query-app-message-tool.js +4 -3
- package/dist/src/tools/query-memory-data-tool.js +4 -3
- package/dist/src/tools/query-todo-task-tool.js +4 -3
- package/dist/src/tools/save-file-to-phone-tool.js +3 -2
- package/dist/src/tools/save-media-to-gallery-tool.js +3 -2
- package/dist/src/tools/schema-tool-factory.js +1 -1
- package/dist/src/tools/search-alarm-tool.js +3 -2
- package/dist/src/tools/search-calendar-tool.js +3 -2
- package/dist/src/tools/search-contact-tool.js +3 -2
- package/dist/src/tools/search-email-tool.js +4 -3
- package/dist/src/tools/search-file-tool.js +8 -9
- package/dist/src/tools/search-message-tool.js +2 -1
- package/dist/src/tools/search-note-tool.js +3 -2
- package/dist/src/tools/search-photo-gallery-tool.js +5 -4
- package/dist/src/tools/send-email-tool.js +4 -3
- package/dist/src/tools/send-file-to-user-tool.d.ts +1 -1
- package/dist/src/tools/send-file-to-user-tool.js +37 -8
- package/dist/src/tools/send-html-card-tool.d.ts +7 -0
- package/dist/src/tools/send-html-card-tool.js +113 -0
- package/dist/src/tools/send-message-tool.js +2 -1
- package/dist/src/tools/session-manager.d.ts +17 -1
- package/dist/src/tools/session-manager.js +87 -1
- package/dist/src/tools/upload-file-tool.js +9 -7
- package/dist/src/tools/upload-photo-tool.js +5 -4
- package/dist/src/tools/xiaoyi-add-collection-tool.js +5 -3
- package/dist/src/tools/xiaoyi-collection-tool.js +4 -3
- package/dist/src/tools/xiaoyi-delete-collection-tool.js +4 -3
- package/dist/src/tools/xiaoyi-gui-tool.js +8 -2
- package/dist/src/trigger-handler.js +4 -7
- package/dist/src/types.d.ts +25 -1
- package/dist/src/utils/config-manager.js +3 -6
- package/dist/src/utils/logger.d.ts +8 -0
- package/dist/src/utils/logger.js +69 -34
- package/dist/src/utils/pushdata-manager.js +1 -5
- package/dist/src/utils/pushid-manager.js +1 -2
- package/dist/src/utils/runtime-manager.js +1 -4
- package/dist/src/websocket.d.ts +3 -0
- package/dist/src/websocket.js +242 -38
- package/package.json +1 -1
package/dist/src/channel.js
CHANGED
|
@@ -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
|
-
|
|
47
|
-
|
|
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
|
|
50
|
-
logger.log(`[DEVICE-FILTER] deviceType=${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(", ")})`);
|
|
51
105
|
return filtered;
|
|
52
106
|
},
|
|
53
107
|
messaging: {
|
package/dist/src/client.js
CHANGED
|
@@ -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]
|
|
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]
|
|
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(
|
|
41
|
+
logger.log(`[WS-MANAGER-CACHE] Removing manager from cache: ${cacheKey}`);
|
|
42
42
|
manager.disconnect();
|
|
43
43
|
wsManagerCache.delete(cacheKey);
|
|
44
|
-
logger.log(
|
|
44
|
+
logger.log(`[WS-MANAGER-CACHE] Manager removed, remaining managers: ${wsManagerCache.size}`);
|
|
45
45
|
}
|
|
46
46
|
else {
|
|
47
|
-
logger.log(
|
|
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("
|
|
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(`
|
|
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(`
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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:
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
72
|
+
log.error(`[CRON-QUERY] RPC call failed for action=${action}:`, err);
|
|
66
73
|
result = { error };
|
|
67
74
|
}
|
|
68
75
|
// Log the result
|
|
69
|
-
|
|
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:
|
|
97
|
+
final: sessionId.toLowerCase().endsWith("cronquery"),
|
|
91
98
|
});
|
|
92
|
-
|
|
99
|
+
log.log(`[CRON-QUERY] Sent response via sendCommand, action=${action}`);
|
|
93
100
|
}
|
|
94
101
|
catch (sendErr) {
|
|
95
|
-
|
|
102
|
+
log.error(`[CRON-QUERY] Failed to send response via sendCommand:`, sendErr);
|
|
96
103
|
}
|
|
97
104
|
}
|
|
98
105
|
else {
|
|
99
|
-
|
|
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:
|
|
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
|
|
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
|
|
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;
|
package/dist/src/cspl/config.js
CHANGED
|
@@ -1,80 +1,110 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
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
|
|
9
|
+
function readEnvFile() {
|
|
9
10
|
if (!fs.existsSync(ENV_FILE_PATH)) {
|
|
10
|
-
throw new Error(`
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
18
|
-
|
|
27
|
+
}
|
|
28
|
+
const firstEqualIndex = trimmedLine.indexOf('=');
|
|
29
|
+
if (firstEqualIndex === -1) {
|
|
19
30
|
continue;
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
38
|
+
return env;
|
|
26
39
|
}
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
}
|