@ynhcj/xiaoyi-channel 0.0.53-beta → 0.0.53-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.
- package/dist/index.js +7 -4
- package/dist/src/bot.d.ts +1 -0
- package/dist/src/bot.js +45 -32
- package/dist/src/channel.js +16 -2
- package/dist/src/client.js +0 -6
- package/dist/src/cspl/call-api.js +14 -32
- package/dist/src/cspl/config.js +3 -3
- package/dist/src/cspl/constants.d.ts +1 -0
- package/dist/src/file-download.js +5 -6
- package/dist/src/file-upload.js +1 -11
- package/dist/src/formatter.d.ts +2 -0
- package/dist/src/formatter.js +11 -6
- package/dist/src/monitor.js +8 -10
- package/dist/src/outbound.js +1 -19
- package/dist/src/parser.d.ts +6 -0
- package/dist/src/parser.js +16 -0
- package/dist/src/provider.d.ts +2 -0
- package/dist/src/provider.js +124 -0
- package/dist/src/push.js +0 -21
- package/dist/src/reply-dispatcher.d.ts +4 -0
- package/dist/src/reply-dispatcher.js +15 -11
- package/dist/src/tools/call-phone-tool.js +2 -18
- package/dist/src/tools/create-alarm-tool.js +3 -7
- package/dist/src/tools/device-tool-map.d.ts +4 -0
- package/dist/src/tools/device-tool-map.js +35 -0
- package/dist/src/tools/find-pc-devices-tool.d.ts +5 -0
- package/dist/src/tools/find-pc-devices-tool.js +98 -0
- package/dist/src/tools/image-reading-tool.js +3 -3
- package/dist/src/tools/modify-alarm-tool.js +3 -7
- package/dist/src/tools/save-file-to-phone-tool.d.ts +5 -0
- package/dist/src/tools/save-file-to-phone-tool.js +170 -0
- package/dist/src/tools/save-media-to-gallery-tool.d.ts +5 -0
- package/dist/src/tools/save-media-to-gallery-tool.js +178 -0
- package/dist/src/tools/send-command-to-car-tool.d.ts +5 -0
- package/dist/src/tools/send-command-to-car-tool.js +85 -0
- package/dist/src/tools/send-file-to-user-tool.js +1 -0
- package/dist/src/tools/session-manager.d.ts +1 -0
- package/dist/src/tools/timestamp-to-utc8-tool.d.ts +12 -0
- package/dist/src/tools/timestamp-to-utc8-tool.js +104 -0
- package/dist/src/tools/upload-file-tool.js +2 -2
- package/dist/src/tools/xiaoyi-add-collection-tool.d.ts +4 -0
- package/dist/src/tools/xiaoyi-add-collection-tool.js +188 -0
- package/dist/src/tools/xiaoyi-collection-tool.js +42 -7
- package/dist/src/tools/xiaoyi-delete-collection-tool.d.ts +4 -0
- package/dist/src/tools/xiaoyi-delete-collection-tool.js +163 -0
- package/dist/src/utils/runtime-manager.d.ts +7 -0
- package/dist/src/utils/runtime-manager.js +42 -0
- package/dist/src/websocket.js +15 -9
- package/openclaw.plugin.json +1 -0
- package/package.json +2 -3
package/dist/src/monitor.js
CHANGED
|
@@ -4,6 +4,7 @@ import { handleXYMessage } from "./bot.js";
|
|
|
4
4
|
import { parseA2AMessage } from "./parser.js";
|
|
5
5
|
import { hasActiveTask } from "./task-manager.js";
|
|
6
6
|
import { handleTriggerEvent } from "./trigger-handler.js";
|
|
7
|
+
import { cleanupStaleTempFiles } from "./reply-dispatcher.js";
|
|
7
8
|
/**
|
|
8
9
|
* Per-session serial queue that ensures messages from the same session are processed
|
|
9
10
|
* in arrival order while allowing different sessions to run concurrently.
|
|
@@ -67,7 +68,7 @@ export async function monitorXYProvider(opts = {}) {
|
|
|
67
68
|
// Event handlers (defined early so they can be referenced in cleanup)
|
|
68
69
|
const messageHandler = (message, sessionId, serverId) => {
|
|
69
70
|
const messageKey = `${sessionId}::${message.id}`;
|
|
70
|
-
log(`[MONITOR-HANDLER] ####### messageHandler triggered:
|
|
71
|
+
log(`[MONITOR-HANDLER] ####### messageHandler triggered: sessionId=${sessionId}, messageId=${message.id} #######`);
|
|
71
72
|
// ✅ Report health: received a message
|
|
72
73
|
trackEvent?.();
|
|
73
74
|
// Check for duplicate message handling
|
|
@@ -75,17 +76,15 @@ export async function monitorXYProvider(opts = {}) {
|
|
|
75
76
|
error(`[MONITOR-HANDLER] ⚠️ WARNING: Duplicate message detected! messageKey=${messageKey}, this may cause duplicate dispatchers!`);
|
|
76
77
|
}
|
|
77
78
|
activeMessages.add(messageKey);
|
|
78
|
-
log(`[MONITOR-HANDLER] 📝 Active messages count: ${activeMessages.size}, messageKey: ${messageKey}`);
|
|
79
79
|
const task = async () => {
|
|
80
80
|
try {
|
|
81
|
-
log(`[MONITOR-HANDLER] 🚀 Starting handleXYMessage for messageKey=${messageKey}`);
|
|
82
81
|
await handleXYMessage({
|
|
83
82
|
cfg,
|
|
84
83
|
runtime,
|
|
85
84
|
message,
|
|
86
85
|
accountId, // ✅ Pass accountId ("default")
|
|
86
|
+
webSocketSessionId: sessionId, // ✅ 传递 WebSocket 层级的 sessionId
|
|
87
87
|
});
|
|
88
|
-
log(`[MONITOR-HANDLER] ✅ Completed handleXYMessage for messageKey=${messageKey}`);
|
|
89
88
|
}
|
|
90
89
|
catch (err) {
|
|
91
90
|
// ✅ Only log error, don't re-throw to prevent gateway restart
|
|
@@ -94,7 +93,6 @@ export async function monitorXYProvider(opts = {}) {
|
|
|
94
93
|
finally {
|
|
95
94
|
// Remove from active messages when done
|
|
96
95
|
activeMessages.delete(messageKey);
|
|
97
|
-
log(`[MONITOR-HANDLER] 🧹 Cleaned up messageKey=${messageKey}, remaining active: ${activeMessages.size}`);
|
|
98
96
|
}
|
|
99
97
|
};
|
|
100
98
|
// 🔑 核心改造:检测steer模式
|
|
@@ -107,7 +105,6 @@ export async function monitorXYProvider(opts = {}) {
|
|
|
107
105
|
// Steer模式且有活跃任务:不入队列,直接并发执行
|
|
108
106
|
log(`[MONITOR-HANDLER] 🔄 STEER MODE: Executing concurrently for messageKey=${messageKey}`);
|
|
109
107
|
log(`[MONITOR-HANDLER] - sessionId: ${parsed.sessionId}`);
|
|
110
|
-
log(`[MONITOR-HANDLER] - Bypassing queue to allow message insertion`);
|
|
111
108
|
void task().catch((err) => {
|
|
112
109
|
error(`XY gateway: concurrent steer task failed for ${messageKey}: ${String(err)}`);
|
|
113
110
|
activeMessages.delete(messageKey);
|
|
@@ -115,7 +112,6 @@ export async function monitorXYProvider(opts = {}) {
|
|
|
115
112
|
}
|
|
116
113
|
else {
|
|
117
114
|
// 正常模式:入队列串行执行
|
|
118
|
-
log(`[MONITOR-HANDLER] 📋 NORMAL MODE: Enqueuing for messageKey=${messageKey}`);
|
|
119
115
|
void enqueue(sessionId, task).catch((err) => {
|
|
120
116
|
error(`XY gateway: queue processing failed for session ${sessionId}: ${String(err)}`);
|
|
121
117
|
activeMessages.delete(messageKey);
|
|
@@ -207,8 +203,8 @@ export async function monitorXYProvider(opts = {}) {
|
|
|
207
203
|
wsManager.on("disconnected", disconnectedHandler);
|
|
208
204
|
wsManager.on("error", errorHandler);
|
|
209
205
|
wsManager.on("trigger-event", triggerEventHandler);
|
|
210
|
-
// Start periodic health check (every
|
|
211
|
-
console.log("🏥 Starting periodic health check (every
|
|
206
|
+
// Start periodic health check (every 6 hours)
|
|
207
|
+
console.log("🏥 Starting periodic health check (every 6 hours)...");
|
|
212
208
|
healthCheckInterval = setInterval(() => {
|
|
213
209
|
console.log("🏥 [HEALTH CHECK] Periodic WebSocket diagnostics...");
|
|
214
210
|
diagnoseAllManagers();
|
|
@@ -217,7 +213,9 @@ export async function monitorXYProvider(opts = {}) {
|
|
|
217
213
|
if (cleaned > 0) {
|
|
218
214
|
console.log(`🧹 [HEALTH CHECK] Auto-cleaned ${cleaned} manager(s) with orphan connections`);
|
|
219
215
|
}
|
|
220
|
-
|
|
216
|
+
// Cleanup stale temp files (older than 24 hours)
|
|
217
|
+
void cleanupStaleTempFiles();
|
|
218
|
+
}, 6 * 60 * 60 * 1000); // 6 hours
|
|
221
219
|
// Connect to WebSocket servers
|
|
222
220
|
wsManager.connect()
|
|
223
221
|
.then(() => {
|
package/dist/src/outbound.js
CHANGED
|
@@ -90,13 +90,6 @@ export const xyOutbound = {
|
|
|
90
90
|
};
|
|
91
91
|
},
|
|
92
92
|
sendText: async ({ cfg, to, text, accountId }) => {
|
|
93
|
-
// Log parameters
|
|
94
|
-
console.log(`[xyOutbound.sendText] Called with:`, {
|
|
95
|
-
to,
|
|
96
|
-
accountId,
|
|
97
|
-
textLength: text?.length || 0,
|
|
98
|
-
textPreview: text?.slice(0, 100),
|
|
99
|
-
});
|
|
100
93
|
// Resolve configuration
|
|
101
94
|
const config = resolveXYConfig(cfg);
|
|
102
95
|
// Handle default push marker (for cron jobs without explicit target)
|
|
@@ -112,7 +105,7 @@ export const xyOutbound = {
|
|
|
112
105
|
let pushDataId;
|
|
113
106
|
try {
|
|
114
107
|
pushDataId = await savePushData(text);
|
|
115
|
-
console.log(`[xyOutbound.sendText] ✅ Push data saved with ID: ${pushDataId}`);
|
|
108
|
+
console.log(`[xyOutbound.sendText] ✅ Push data saved with ID: ${pushDataId.substring(0, 20)}`);
|
|
116
109
|
}
|
|
117
110
|
catch (error) {
|
|
118
111
|
console.error(`[xyOutbound.sendText] ❌ Failed to save push data:`, error);
|
|
@@ -146,7 +139,6 @@ export const xyOutbound = {
|
|
|
146
139
|
let failureCount = 0;
|
|
147
140
|
for (const pushId of pushIdList) {
|
|
148
141
|
try {
|
|
149
|
-
console.log(`[xyOutbound.sendText] Sending to pushId: ${pushId.substring(0, 20)}...`);
|
|
150
142
|
// 传入 pushId 和 pushDataId,使用 kind="data" 格式
|
|
151
143
|
await pushService.sendPush(pushText, title, undefined, actualTo, pushDataId, pushId);
|
|
152
144
|
successCount++;
|
|
@@ -158,8 +150,6 @@ export const xyOutbound = {
|
|
|
158
150
|
// 单个 pushId 发送失败不影响其他,继续处理下一个
|
|
159
151
|
}
|
|
160
152
|
}
|
|
161
|
-
console.log(`[xyOutbound.sendText] 📊 Broadcast summary: ${successCount} success, ${failureCount} failures`);
|
|
162
|
-
console.log(`[xyOutbound.sendText] Completed successfully`);
|
|
163
153
|
// Return message info
|
|
164
154
|
return {
|
|
165
155
|
channel: "xiaoyi-channel",
|
|
@@ -168,14 +158,6 @@ export const xyOutbound = {
|
|
|
168
158
|
};
|
|
169
159
|
},
|
|
170
160
|
sendMedia: async ({ cfg, to, text, mediaUrl, accountId, mediaLocalRoots }) => {
|
|
171
|
-
// Log parameters
|
|
172
|
-
console.log(`[xyOutbound.sendMedia] Called with:`, {
|
|
173
|
-
to,
|
|
174
|
-
accountId,
|
|
175
|
-
text,
|
|
176
|
-
mediaUrl,
|
|
177
|
-
mediaLocalRoots,
|
|
178
|
-
});
|
|
179
161
|
// Parse to: "sessionId::taskId"
|
|
180
162
|
const parts = to.split("::");
|
|
181
163
|
if (parts.length !== 2) {
|
package/dist/src/parser.d.ts
CHANGED
|
@@ -43,6 +43,12 @@ export declare function isTasksCancelMessage(method: string): boolean;
|
|
|
43
43
|
* Looks for push_id in data parts under variables.systemVariables.push_id
|
|
44
44
|
*/
|
|
45
45
|
export declare function extractPushId(parts: A2AMessagePart[]): string | null;
|
|
46
|
+
/**
|
|
47
|
+
* Extract deviceType from message parts.
|
|
48
|
+
* Looks for deviceType in data parts under variables.systemVariables.deviceType
|
|
49
|
+
* (same level as push_id).
|
|
50
|
+
*/
|
|
51
|
+
export declare function extractDeviceType(parts: A2AMessagePart[]): string | null;
|
|
46
52
|
/**
|
|
47
53
|
* Extract Trigger event data from message parts.
|
|
48
54
|
* Looks for Trigger events with pushDataId in data parts.
|
package/dist/src/parser.js
CHANGED
|
@@ -72,6 +72,22 @@ export function extractPushId(parts) {
|
|
|
72
72
|
}
|
|
73
73
|
return null;
|
|
74
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Extract deviceType from message parts.
|
|
77
|
+
* Looks for deviceType in data parts under variables.systemVariables.deviceType
|
|
78
|
+
* (same level as push_id).
|
|
79
|
+
*/
|
|
80
|
+
export function extractDeviceType(parts) {
|
|
81
|
+
for (const part of parts) {
|
|
82
|
+
if (part.kind === "data" && part.data) {
|
|
83
|
+
const deviceType = part.data.variables?.systemVariables?.device_type;
|
|
84
|
+
if (deviceType && typeof deviceType === "string") {
|
|
85
|
+
return deviceType;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
75
91
|
/**
|
|
76
92
|
* Extract Trigger event data from message parts.
|
|
77
93
|
* Looks for Trigger events with pushDataId in data parts.
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// Xiaoyi Provider
|
|
2
|
+
// Wraps any OpenAI-compatible endpoint and injects dynamic headers
|
|
3
|
+
// (taskId, sessionId, conversationId) from the current XY channel session.
|
|
4
|
+
// Falls back to uid-based values when no session context is available.
|
|
5
|
+
//
|
|
6
|
+
// Users configure the underlying model in config:
|
|
7
|
+
// models.providers.xiaoyiprovider.baseUrl = "https://..."
|
|
8
|
+
// models.providers.xiaoyiprovider.api = "openai-completions"
|
|
9
|
+
// models.providers.xiaoyiprovider.models = [...]
|
|
10
|
+
import { createHash } from "crypto";
|
|
11
|
+
import { getCurrentSessionContext } from "./tools/session-manager.js";
|
|
12
|
+
/**
|
|
13
|
+
* Dynamic header keys injected via extraParams and forwarded to the HTTP request.
|
|
14
|
+
* Correspond to the three fields written to .xiaoyiruntime:
|
|
15
|
+
* TASK_ID, SESSION_ID, CONVERSATION_ID
|
|
16
|
+
*/
|
|
17
|
+
const HEADER_TRACE_ID = "x-hag-trace-id";
|
|
18
|
+
const HEADER_SESSION_ID = "x-session-id";
|
|
19
|
+
const HEADER_INTERACTION_ID = "x-interaction-id";
|
|
20
|
+
/**
|
|
21
|
+
* Encode uid via SHA-256 and take first 32 hex chars.
|
|
22
|
+
*/
|
|
23
|
+
function encodeUid(uid) {
|
|
24
|
+
return createHash("sha256").update(uid).digest("hex").slice(0, 32);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Get uid from plugin config (OpenClawConfig -> plugins -> xiaoyi-channel -> config).
|
|
28
|
+
*/
|
|
29
|
+
function getUidFromConfig(config) {
|
|
30
|
+
return config?.plugins?.entries?.["xiaoyi-channel"]?.config?.uid;
|
|
31
|
+
}
|
|
32
|
+
export const xiaoyiProvider = {
|
|
33
|
+
id: "xiaoyiprovider",
|
|
34
|
+
label: "Xiaoyi Provider",
|
|
35
|
+
docsPath: "/providers/models",
|
|
36
|
+
auth: [],
|
|
37
|
+
isCacheTtlEligible: () => true,
|
|
38
|
+
/**
|
|
39
|
+
* Inject dynamic session params into extraParams so they flow
|
|
40
|
+
* through to wrapStreamFn's ctx.extraParams.
|
|
41
|
+
*
|
|
42
|
+
* Priority:
|
|
43
|
+
* 1. Session context (from AsyncLocalStorage, set by bot.ts)
|
|
44
|
+
* 2. uid-based fallback: sha256(uid).hex[:32]_timestamp
|
|
45
|
+
* 3. No uid available → return undefined (no headers injected)
|
|
46
|
+
*/
|
|
47
|
+
prepareExtraParams: (ctx) => {
|
|
48
|
+
const sessionCtx = getCurrentSessionContext();
|
|
49
|
+
if (sessionCtx) {
|
|
50
|
+
const taskId = sessionCtx.taskId;
|
|
51
|
+
const sessionId = taskId.split("&")[0];
|
|
52
|
+
const interactionId = taskId.split("&")[1] || "";
|
|
53
|
+
return {
|
|
54
|
+
...ctx.extraParams,
|
|
55
|
+
[HEADER_TRACE_ID]: taskId,
|
|
56
|
+
[HEADER_SESSION_ID]: sessionId,
|
|
57
|
+
[HEADER_INTERACTION_ID]: interactionId,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
// Fallback: uid-based values
|
|
61
|
+
const uid = getUidFromConfig(ctx.config);
|
|
62
|
+
if (!uid)
|
|
63
|
+
return undefined;
|
|
64
|
+
const prefix = encodeUid(uid);
|
|
65
|
+
const ts = Date.now();
|
|
66
|
+
const fallbackValue = `${prefix}_${ts}`;
|
|
67
|
+
return {
|
|
68
|
+
...ctx.extraParams,
|
|
69
|
+
[HEADER_TRACE_ID]: fallbackValue,
|
|
70
|
+
[HEADER_SESSION_ID]: fallbackValue,
|
|
71
|
+
[HEADER_INTERACTION_ID]: fallbackValue,
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
/**
|
|
75
|
+
* Wrap the stream function to inject dynamic headers into every
|
|
76
|
+
* HTTP request to the model provider.
|
|
77
|
+
*
|
|
78
|
+
* Reads the values injected by prepareExtraParams and adds them
|
|
79
|
+
* as HTTP headers on the outgoing request.
|
|
80
|
+
*/
|
|
81
|
+
wrapStreamFn: (ctx) => {
|
|
82
|
+
const underlying = ctx.streamFn;
|
|
83
|
+
if (!underlying)
|
|
84
|
+
return underlying;
|
|
85
|
+
return async (model, context, options) => {
|
|
86
|
+
// 每次请求时从 ctx.extraParams 动态读取 header
|
|
87
|
+
const dynamicHeaders = {};
|
|
88
|
+
if (ctx.extraParams) {
|
|
89
|
+
const traceId = ctx.extraParams[HEADER_TRACE_ID];
|
|
90
|
+
const sessionId = ctx.extraParams[HEADER_SESSION_ID];
|
|
91
|
+
const interactionId = ctx.extraParams[HEADER_INTERACTION_ID];
|
|
92
|
+
if (typeof traceId === "string")
|
|
93
|
+
dynamicHeaders[HEADER_TRACE_ID] = traceId;
|
|
94
|
+
if (typeof sessionId === "string")
|
|
95
|
+
dynamicHeaders[HEADER_SESSION_ID] = sessionId;
|
|
96
|
+
if (typeof interactionId === "string")
|
|
97
|
+
dynamicHeaders[HEADER_INTERACTION_ID] = interactionId;
|
|
98
|
+
}
|
|
99
|
+
// 记录输入
|
|
100
|
+
console.log(`[xiaoyiprovider] input messages count: ${context.messages?.length ?? 0}`);
|
|
101
|
+
if (context.systemPrompt) {
|
|
102
|
+
console.log(`[xiaoyiprovider] system prompt length: ${context.systemPrompt.length}`);
|
|
103
|
+
}
|
|
104
|
+
// 在发送给模型前,删除 systemPrompt 中 ## Tooling 与 TOOLS.md 声明之间的内容
|
|
105
|
+
if (context.systemPrompt) {
|
|
106
|
+
const before = context.systemPrompt.length;
|
|
107
|
+
context.systemPrompt = context.systemPrompt.replace(/(## Tooling)[\s\S]*?(TOOLS\.md does not control tool availability; it is user guidance for how to use external tools\.)/, "$1\n\n$2");
|
|
108
|
+
console.log(`[xiaoyiprovider] system prompt trimmed: ${before} -> ${context.systemPrompt.length}`);
|
|
109
|
+
}
|
|
110
|
+
const stream = await underlying(model, context, {
|
|
111
|
+
...options,
|
|
112
|
+
headers: {
|
|
113
|
+
...options?.headers,
|
|
114
|
+
...dynamicHeaders,
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
// 异步监听输出(不阻塞 stream 返回)
|
|
118
|
+
stream.result().then((result) => {
|
|
119
|
+
console.log(`[xiaoyiprovider] stream completed, usage: input=${result.usage?.input} output=${result.usage?.output}`);
|
|
120
|
+
}, (err) => console.log(`[xiaoyiprovider] stream error: ${JSON.stringify(err)}`));
|
|
121
|
+
return stream;
|
|
122
|
+
};
|
|
123
|
+
},
|
|
124
|
+
};
|
package/dist/src/push.js
CHANGED
|
@@ -34,15 +34,7 @@ export class XYPushService {
|
|
|
34
34
|
// Use provided pushId or fall back to config pushId
|
|
35
35
|
const actualPushId = pushId || this.config.pushId;
|
|
36
36
|
console.log(`[PUSH] 📤 Preparing to send push message`);
|
|
37
|
-
console.log(`[PUSH] - Title: "${title}"`);
|
|
38
|
-
console.log(`[PUSH] - Content length: ${content.length} chars`);
|
|
39
|
-
console.log(`[PUSH] - Session ID: ${sessionId || 'none'}`);
|
|
40
|
-
console.log(`[PUSH] - Trace ID: ${traceId}`);
|
|
41
|
-
console.log(`[PUSH] - Push URL: ${pushUrl}`);
|
|
42
37
|
console.log(`[PUSH] - Using pushId: ${actualPushId.substring(0, 20)}...`);
|
|
43
|
-
console.log(`[PUSH] - Full pushId: ${actualPushId}`);
|
|
44
|
-
console.log(`[PUSH] - API ID: ${this.config.apiId}`);
|
|
45
|
-
console.log(`[PUSH] - UID: ${this.config.uid}`);
|
|
46
38
|
try {
|
|
47
39
|
const requestBody = {
|
|
48
40
|
jsonrpc: "2.0",
|
|
@@ -75,7 +67,6 @@ export class XYPushService {
|
|
|
75
67
|
],
|
|
76
68
|
},
|
|
77
69
|
};
|
|
78
|
-
console.log(`[PUSH] Full request body:`, JSON.stringify(requestBody, null, 2));
|
|
79
70
|
const response = await fetch(pushUrl, {
|
|
80
71
|
method: "POST",
|
|
81
72
|
headers: {
|
|
@@ -91,21 +82,16 @@ export class XYPushService {
|
|
|
91
82
|
// Log response status and headers
|
|
92
83
|
console.log(`[PUSH] 📥 Response received`);
|
|
93
84
|
console.log(`[PUSH] - HTTP Status: ${response.status} ${response.statusText}`);
|
|
94
|
-
console.log(`[PUSH] - Content-Type: ${response.headers.get('content-type')}`);
|
|
95
|
-
console.log(`[PUSH] - Content-Length: ${response.headers.get('content-length')}`);
|
|
96
85
|
if (!response.ok) {
|
|
97
86
|
const errorText = await response.text();
|
|
98
87
|
console.log(`[PUSH] ❌ Push request failed`);
|
|
99
88
|
console.log(`[PUSH] - HTTP Status: ${response.status}`);
|
|
100
|
-
console.log(`[PUSH] - Response body: ${errorText}`);
|
|
101
89
|
throw new Error(`Push failed: HTTP ${response.status} - ${errorText}`);
|
|
102
90
|
}
|
|
103
91
|
// Try to parse JSON response with detailed error handling
|
|
104
92
|
let result;
|
|
105
93
|
try {
|
|
106
94
|
const responseText = await response.text();
|
|
107
|
-
console.log(`[PUSH] 📄 Response body length: ${responseText.length} chars`);
|
|
108
|
-
console.log(`[PUSH] 📄 Response body preview: ${responseText.substring(0, 200)}`);
|
|
109
95
|
if (!responseText || responseText.trim() === '') {
|
|
110
96
|
console.log(`[PUSH] ⚠️ Received empty response body`);
|
|
111
97
|
result = {};
|
|
@@ -120,20 +106,13 @@ export class XYPushService {
|
|
|
120
106
|
throw new Error(`Invalid JSON response from push service: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
|
|
121
107
|
}
|
|
122
108
|
console.log(`[PUSH] ✅ Push message sent successfully`);
|
|
123
|
-
console.log(`[PUSH] - Title: "${title}"`);
|
|
124
109
|
console.log(`[PUSH] - Trace ID: ${traceId}`);
|
|
125
|
-
console.log(`[PUSH] - Used pushId: ${actualPushId.substring(0, 20)}...`);
|
|
126
|
-
console.log(`[PUSH] - Response:`, result);
|
|
127
110
|
}
|
|
128
111
|
catch (error) {
|
|
129
112
|
console.log(`[PUSH] ❌ Failed to send push message`);
|
|
130
|
-
console.log(`[PUSH] - Trace ID: ${traceId}`);
|
|
131
|
-
console.log(`[PUSH] - Target URL: ${pushUrl}`);
|
|
132
|
-
console.log(`[PUSH] - Push ID: ${actualPushId.substring(0, 20)}...`);
|
|
133
113
|
if (error instanceof Error) {
|
|
134
114
|
console.log(`[PUSH] - Error name: ${error.name}`);
|
|
135
115
|
console.log(`[PUSH] - Error message: ${error.message}`);
|
|
136
|
-
console.log(`[PUSH] - Error stack:`, error.stack);
|
|
137
116
|
}
|
|
138
117
|
else {
|
|
139
118
|
console.log(`[PUSH] - Error:`, error);
|
|
@@ -8,6 +8,10 @@ export interface CreateXYReplyDispatcherParams {
|
|
|
8
8
|
accountId: string;
|
|
9
9
|
isSteerFollower?: boolean;
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* 清理 /tmp/xy_channel 目录中超过 24 小时的旧文件
|
|
13
|
+
*/
|
|
14
|
+
export declare function cleanupStaleTempFiles(tempDir?: string): Promise<void>;
|
|
11
15
|
/**
|
|
12
16
|
* Create a reply dispatcher for XY channel messages.
|
|
13
17
|
* Follows feishu pattern with status updates and streaming support.
|
|
@@ -4,29 +4,34 @@ import { resolveXYConfig } from "./config.js";
|
|
|
4
4
|
import { getCurrentTaskId, getCurrentMessageId } from "./task-manager.js";
|
|
5
5
|
import fs from "fs/promises";
|
|
6
6
|
import path from "path";
|
|
7
|
+
const TEMP_FILE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
7
8
|
/**
|
|
8
|
-
* 清理 /tmp/xy_channel
|
|
9
|
+
* 清理 /tmp/xy_channel 目录中超过 24 小时的旧文件
|
|
9
10
|
*/
|
|
10
|
-
async function
|
|
11
|
+
export async function cleanupStaleTempFiles(tempDir = "/tmp/xy_channel") {
|
|
11
12
|
try {
|
|
12
13
|
const stats = await fs.stat(tempDir).catch(() => null);
|
|
13
14
|
if (!stats?.isDirectory()) {
|
|
14
|
-
return;
|
|
15
|
+
return;
|
|
15
16
|
}
|
|
16
17
|
const files = await fs.readdir(tempDir);
|
|
18
|
+
const now = Date.now();
|
|
17
19
|
let cleanedCount = 0;
|
|
18
20
|
for (const file of files) {
|
|
19
21
|
const filePath = path.join(tempDir, file);
|
|
20
22
|
try {
|
|
21
|
-
await fs.
|
|
22
|
-
|
|
23
|
+
const fileStat = await fs.stat(filePath);
|
|
24
|
+
if (now - fileStat.mtimeMs > TEMP_FILE_TTL_MS) {
|
|
25
|
+
await fs.unlink(filePath);
|
|
26
|
+
cleanedCount++;
|
|
27
|
+
}
|
|
23
28
|
}
|
|
24
29
|
catch (err) {
|
|
25
|
-
//
|
|
30
|
+
// 忽略单个文件处理失败
|
|
26
31
|
}
|
|
27
32
|
}
|
|
28
33
|
if (cleanedCount > 0) {
|
|
29
|
-
console.log(`[CLEANUP] 🧹 Cleaned ${cleanedCount} files from ${tempDir}`);
|
|
34
|
+
console.log(`[CLEANUP] 🧹 Cleaned ${cleanedCount} stale files (>${TEMP_FILE_TTL_MS / 1000 / 3600}h) from ${tempDir}`);
|
|
30
35
|
}
|
|
31
36
|
}
|
|
32
37
|
catch (err) {
|
|
@@ -43,9 +48,7 @@ export function createXYReplyDispatcher(params) {
|
|
|
43
48
|
const log = runtime?.log ?? console.log;
|
|
44
49
|
const error = runtime?.error ?? console.error;
|
|
45
50
|
log(`[DISPATCHER-CREATE] ******* Creating dispatcher *******`);
|
|
46
|
-
log(`[DISPATCHER-CREATE] - sessionId: ${sessionId}`);
|
|
47
51
|
log(`[DISPATCHER-CREATE] - taskId: ${taskId}`);
|
|
48
|
-
log(`[DISPATCHER-CREATE] - messageId: ${messageId}`);
|
|
49
52
|
log(`[DISPATCHER-CREATE] - isSteerFollower: ${isSteerFollower ?? false}`);
|
|
50
53
|
// 初始taskId和messageId(作为fallback)
|
|
51
54
|
const initialTaskId = taskId;
|
|
@@ -232,16 +235,17 @@ export function createXYReplyDispatcher(params) {
|
|
|
232
235
|
text: "任务执行异常,请重试~",
|
|
233
236
|
append: false,
|
|
234
237
|
final: true,
|
|
238
|
+
errorCode: 99921111,
|
|
239
|
+
errorMessage: "任务执行异常,请重试",
|
|
235
240
|
});
|
|
236
241
|
finalSent = true;
|
|
237
|
-
log(`[ON_IDLE] ✅ Sent error response`);
|
|
242
|
+
log(`[ON_IDLE] ✅ Sent error response with code: 99921111`);
|
|
238
243
|
}
|
|
239
244
|
catch (err) {
|
|
240
245
|
error(`[ON_IDLE] Failed to send error response:`, err);
|
|
241
246
|
}
|
|
242
247
|
}
|
|
243
248
|
stopStatusInterval();
|
|
244
|
-
void cleanupTempDir();
|
|
245
249
|
},
|
|
246
250
|
onCleanup: () => {
|
|
247
251
|
const currentTaskId = getActiveTaskId();
|
|
@@ -90,33 +90,17 @@ export const callPhoneTool = {
|
|
|
90
90
|
clearTimeout(timeout);
|
|
91
91
|
wsManager.off("data-event", handler);
|
|
92
92
|
if (event.status === "success" && event.outputs) {
|
|
93
|
-
// Check for error code in outputs
|
|
94
|
-
const code = event.outputs.code !== undefined ? event.outputs.code : null;
|
|
95
|
-
if (code !== null && code !== 0) {
|
|
96
|
-
const errorMsg = event.outputs.errorMsg || event.outputs.errMsg || "未知错误";
|
|
97
|
-
reject(new Error(`拨打电话失败: ${errorMsg} (错误代码: ${code})`));
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
// Return the outputs directly
|
|
101
|
-
const result = {
|
|
102
|
-
success: true,
|
|
103
|
-
code: code,
|
|
104
|
-
phoneNumber: params.phoneNumber,
|
|
105
|
-
slotId: slotId,
|
|
106
|
-
message: "电话拨打成功",
|
|
107
|
-
};
|
|
108
93
|
resolve({
|
|
109
94
|
content: [
|
|
110
95
|
{
|
|
111
96
|
type: "text",
|
|
112
|
-
text: JSON.stringify(
|
|
97
|
+
text: JSON.stringify(event.outputs),
|
|
113
98
|
},
|
|
114
99
|
],
|
|
115
100
|
});
|
|
116
101
|
}
|
|
117
102
|
else {
|
|
118
|
-
|
|
119
|
-
reject(new Error(`拨打电话失败: ${errorDetail}`));
|
|
103
|
+
reject(new Error(`拨打电话失败: ${event.status}`));
|
|
120
104
|
}
|
|
121
105
|
}
|
|
122
106
|
};
|
|
@@ -6,7 +6,7 @@ const ALARM_SNOOZE_DURATION_VALUES = [5, 10, 15, 20, 25, 30];
|
|
|
6
6
|
const ALARM_SNOOZE_TOTAL_VALUES = [0, 1, 3, 5, 10];
|
|
7
7
|
const ALARM_RING_DURATION_VALUES = [1, 5, 10, 15, 20, 30];
|
|
8
8
|
const DAYS_OF_WAKE_TYPE_VALUES = [0, 1, 2, 3, 4];
|
|
9
|
-
const DAYS_OF_WEEK_VALUES = ["Mon", "
|
|
9
|
+
const DAYS_OF_WEEK_VALUES = ["Mon", "Tues", "Wed", "Thur", "Fri", "Sat", "Sun"];
|
|
10
10
|
/**
|
|
11
11
|
* XY create alarm tool - creates an alarm on user's device.
|
|
12
12
|
* Requires alarmTime parameter. Other parameters are optional with sensible defaults.
|
|
@@ -27,7 +27,7 @@ export const createAlarmTool = {
|
|
|
27
27
|
- alarmSnoozeTotal: 再响次数,枚举值:0,1,3,5,10,默认0(表示不再响)
|
|
28
28
|
- alarmRingDuration: 响铃时长(分钟),枚举值:1,5,10,15,20,30,默认5
|
|
29
29
|
- daysOfWakeType: 闹钟响铃类型,枚举值:0=单次响铃,1=法定节假日,2=每天,3=自定义时间,4=法定工作日,默认0
|
|
30
|
-
- daysOfWeek: 自定义响铃星期,仅当daysOfWakeType=3(自定义时间)时必需且有效,其他情况不要传递此参数。数组或JSON字符串,枚举值:Mon,
|
|
30
|
+
- daysOfWeek: 自定义响铃星期,仅当daysOfWakeType=3(自定义时间)时必需且有效,其他情况不要传递此参数。数组或JSON字符串,枚举值:Mon,Tues,Wed,Thur,Fri,Sat,Sun。
|
|
31
31
|
|
|
32
32
|
注意事项:
|
|
33
33
|
a. 操作超时时间为60秒,请勿重复调用此工具,如果超时或失败,最多重试一次。
|
|
@@ -64,7 +64,7 @@ b. 使用该工具之前需获取当前真实时间
|
|
|
64
64
|
daysOfWeek: {
|
|
65
65
|
// 不指定 type,允许传入数组或 JSON 字符串
|
|
66
66
|
// 具体的类型验证和转换在 execute 函数内部进行
|
|
67
|
-
description: "自定义响铃星期(仅当daysOfWakeType=3时需要,其他情况不要传递),数组或JSON字符串,枚举值:Mon,
|
|
67
|
+
description: "自定义响铃星期(仅当daysOfWakeType=3时需要,其他情况不要传递),数组或JSON字符串,枚举值:Mon,Tues,Wed,Thur,Fri,Sat,Sun。",
|
|
68
68
|
},
|
|
69
69
|
},
|
|
70
70
|
required: ["alarmTime"],
|
|
@@ -163,10 +163,6 @@ b. 使用该工具之前需获取当前真实时间
|
|
|
163
163
|
if (!normalizedDaysOfWeek || normalizedDaysOfWeek.length === 0) {
|
|
164
164
|
throw new Error("daysOfWeek array cannot be empty");
|
|
165
165
|
}
|
|
166
|
-
// 验证数组长度必须为1
|
|
167
|
-
if (normalizedDaysOfWeek.length !== 1) {
|
|
168
|
-
throw new Error("daysOfWeek 仅支持长度为1的数组。如果需要一周中不同的几天,需要多次调用此工具");
|
|
169
|
-
}
|
|
170
166
|
// Validate each day
|
|
171
167
|
for (const day of normalizedDaysOfWeek) {
|
|
172
168
|
if (typeof day !== "string" || !DAYS_OF_WEEK_VALUES.includes(day)) {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Device type to tool name mapping.
|
|
2
|
+
// Supports two modes:
|
|
3
|
+
// - allowlist: only listed tools are available (used for restrictive devices like car)
|
|
4
|
+
// - denylist: listed tools are blocked, everything else is available (used for permissive devices like pc)
|
|
5
|
+
// Tools NOT listed in any device entry → available to all devices (no restriction).
|
|
6
|
+
/** Known device type enum. */
|
|
7
|
+
export const DEVICE_TYPES = ["car", "2in1", "phone"];
|
|
8
|
+
const DEVICE_TOOL_POLICY = {
|
|
9
|
+
"2in1": {
|
|
10
|
+
allowlist: false,
|
|
11
|
+
tools: [
|
|
12
|
+
"xiaoyi_gui_agent",
|
|
13
|
+
"call_phone",
|
|
14
|
+
"send_message",
|
|
15
|
+
"search_message",
|
|
16
|
+
"search_contact",
|
|
17
|
+
"QueryCollection",
|
|
18
|
+
"AddCollection",
|
|
19
|
+
"DeleteCollection",
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
export function filterToolsByDevice(tools, deviceType) {
|
|
24
|
+
if (!deviceType)
|
|
25
|
+
return tools;
|
|
26
|
+
const policy = DEVICE_TOOL_POLICY[deviceType];
|
|
27
|
+
if (!policy)
|
|
28
|
+
return tools; // unrecognized device → no filtering
|
|
29
|
+
if (policy.allowlist) {
|
|
30
|
+
return tools.filter((tool) => policy.tools.includes(tool.name));
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
return tools.filter((tool) => !policy.tools.includes(tool.name));
|
|
34
|
+
}
|
|
35
|
+
}
|