@ynhcj/xiaoyi-channel 0.0.44-beta → 0.0.44-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.d.ts +0 -2
- package/dist/index.js +42 -2
- package/dist/src/bot.d.ts +1 -0
- package/dist/src/bot.js +59 -61
- package/dist/src/channel.js +30 -4
- package/dist/src/client.js +0 -9
- package/dist/src/cspl/call-api.d.ts +3 -0
- package/dist/src/cspl/call-api.js +86 -0
- package/dist/src/cspl/config.d.ts +19 -0
- package/dist/src/cspl/config.js +50 -0
- package/dist/src/cspl/constants.d.ts +43 -0
- package/dist/src/cspl/constants.js +22 -0
- package/dist/src/cspl/utils.d.ts +10 -0
- package/dist/src/cspl/utils.js +57 -0
- package/dist/src/file-upload.d.ts +5 -0
- package/dist/src/file-upload.js +88 -6
- package/dist/src/formatter.d.ts +2 -0
- package/dist/src/formatter.js +13 -30
- package/dist/src/heartbeat.js +0 -4
- package/dist/src/monitor.js +8 -10
- package/dist/src/onboarding.d.ts +3 -4
- package/dist/src/onboarding.js +2 -2
- package/dist/src/outbound.d.ts +2 -1
- 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/push.js +0 -21
- package/dist/src/reply-dispatcher.d.ts +4 -0
- package/dist/src/reply-dispatcher.js +46 -8
- package/dist/src/steer-injector.d.ts +16 -0
- package/dist/src/steer-injector.js +74 -0
- package/dist/src/thread-bindings.d.ts +54 -0
- package/dist/src/thread-bindings.js +214 -0
- package/dist/src/tools/calendar-tool.js +2 -37
- package/dist/src/tools/call-phone-tool.js +3 -60
- package/dist/src/tools/create-alarm-tool.js +8 -109
- package/dist/src/tools/delete-alarm-tool.js +5 -69
- package/dist/src/tools/device-tool-map.d.ts +4 -0
- package/dist/src/tools/device-tool-map.js +24 -0
- package/dist/src/tools/image-reading-tool.d.ts +5 -0
- package/dist/src/tools/image-reading-tool.js +328 -0
- package/dist/src/tools/location-tool.js +6 -40
- package/dist/src/tools/modify-alarm-tool.js +8 -114
- package/dist/src/tools/modify-note-tool.js +3 -41
- package/dist/src/tools/note-tool.js +4 -16
- package/dist/src/tools/search-alarm-tool.js +12 -118
- package/dist/src/tools/search-calendar-tool.js +4 -81
- package/dist/src/tools/search-contact-tool.js +2 -55
- package/dist/src/tools/search-file-tool.js +4 -61
- package/dist/src/tools/search-message-tool.js +2 -59
- package/dist/src/tools/search-note-tool.js +4 -22
- package/dist/src/tools/search-photo-gallery-tool.js +8 -57
- 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 -39
- package/dist/src/tools/send-message-tool.js +5 -56
- package/dist/src/tools/session-manager.d.ts +1 -0
- package/dist/src/tools/session-manager.js +0 -45
- 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 +0 -49
- package/dist/src/tools/upload-photo-tool.js +0 -42
- package/dist/src/tools/view-push-result-tool.js +0 -11
- package/dist/src/tools/xiaoyi-collection-tool.js +4 -82
- package/dist/src/tools/xiaoyi-gui-tool.js +0 -34
- package/dist/src/utils/runtime-manager.d.ts +7 -0
- package/dist/src/utils/runtime-manager.js +42 -0
- package/dist/src/websocket.js +33 -13
- package/package.json +3 -4
- package/dist/src/tools/search-photo-tool.d.ts +0 -9
- package/dist/src/tools/search-photo-tool.js +0 -270
- package/dist/src/utils/session.d.ts +0 -34
- package/dist/src/utils/session.js +0 -50
|
@@ -1,8 +1,43 @@
|
|
|
1
|
-
import { createReplyPrefixContext } from "openclaw/plugin-sdk";
|
|
2
1
|
import { getXYRuntime } from "./runtime.js";
|
|
3
2
|
import { sendA2AResponse, sendStatusUpdate, sendReasoningTextUpdate } from "./formatter.js";
|
|
4
3
|
import { resolveXYConfig } from "./config.js";
|
|
5
4
|
import { getCurrentTaskId, getCurrentMessageId } from "./task-manager.js";
|
|
5
|
+
import fs from "fs/promises";
|
|
6
|
+
import path from "path";
|
|
7
|
+
const TEMP_FILE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
8
|
+
/**
|
|
9
|
+
* 清理 /tmp/xy_channel 目录中超过 24 小时的旧文件
|
|
10
|
+
*/
|
|
11
|
+
export async function cleanupStaleTempFiles(tempDir = "/tmp/xy_channel") {
|
|
12
|
+
try {
|
|
13
|
+
const stats = await fs.stat(tempDir).catch(() => null);
|
|
14
|
+
if (!stats?.isDirectory()) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const files = await fs.readdir(tempDir);
|
|
18
|
+
const now = Date.now();
|
|
19
|
+
let cleanedCount = 0;
|
|
20
|
+
for (const file of files) {
|
|
21
|
+
const filePath = path.join(tempDir, file);
|
|
22
|
+
try {
|
|
23
|
+
const fileStat = await fs.stat(filePath);
|
|
24
|
+
if (now - fileStat.mtimeMs > TEMP_FILE_TTL_MS) {
|
|
25
|
+
await fs.unlink(filePath);
|
|
26
|
+
cleanedCount++;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
// 忽略单个文件处理失败
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (cleanedCount > 0) {
|
|
34
|
+
console.log(`[CLEANUP] 🧹 Cleaned ${cleanedCount} stale files (>${TEMP_FILE_TTL_MS / 1000 / 3600}h) from ${tempDir}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
console.error(`[CLEANUP] ❌ Failed to cleanup temp dir:`, err);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
6
41
|
/**
|
|
7
42
|
* Create a reply dispatcher for XY channel messages.
|
|
8
43
|
* Follows feishu pattern with status updates and streaming support.
|
|
@@ -13,9 +48,7 @@ export function createXYReplyDispatcher(params) {
|
|
|
13
48
|
const log = runtime?.log ?? console.log;
|
|
14
49
|
const error = runtime?.error ?? console.error;
|
|
15
50
|
log(`[DISPATCHER-CREATE] ******* Creating dispatcher *******`);
|
|
16
|
-
log(`[DISPATCHER-CREATE] - sessionId: ${sessionId}`);
|
|
17
51
|
log(`[DISPATCHER-CREATE] - taskId: ${taskId}`);
|
|
18
|
-
log(`[DISPATCHER-CREATE] - messageId: ${messageId}`);
|
|
19
52
|
log(`[DISPATCHER-CREATE] - isSteerFollower: ${isSteerFollower ?? false}`);
|
|
20
53
|
// 初始taskId和messageId(作为fallback)
|
|
21
54
|
const initialTaskId = taskId;
|
|
@@ -32,7 +65,12 @@ export function createXYReplyDispatcher(params) {
|
|
|
32
65
|
};
|
|
33
66
|
const core = getXYRuntime();
|
|
34
67
|
const config = resolveXYConfig(cfg);
|
|
35
|
-
|
|
68
|
+
// Simplified prefix context for single-account Xiaoyi channel
|
|
69
|
+
const prefixContext = {
|
|
70
|
+
responsePrefix: undefined,
|
|
71
|
+
responsePrefixContextProvider: undefined,
|
|
72
|
+
onModelSelected: undefined,
|
|
73
|
+
};
|
|
36
74
|
let statusUpdateInterval = null;
|
|
37
75
|
let hasSentResponse = false;
|
|
38
76
|
let finalSent = false;
|
|
@@ -54,7 +92,7 @@ export function createXYReplyDispatcher(params) {
|
|
|
54
92
|
sessionId,
|
|
55
93
|
taskId: currentTaskId, // 🔑 动态taskId
|
|
56
94
|
messageId: currentMessageId, // 🔑 动态messageId
|
|
57
|
-
text: "
|
|
95
|
+
text: "任务正在处理中,请稍候~",
|
|
58
96
|
state: "working",
|
|
59
97
|
}).catch((err) => {
|
|
60
98
|
error(`Failed to send status update:`, err);
|
|
@@ -197,9 +235,11 @@ export function createXYReplyDispatcher(params) {
|
|
|
197
235
|
text: "任务执行异常,请重试~",
|
|
198
236
|
append: false,
|
|
199
237
|
final: true,
|
|
238
|
+
errorCode: 99921111,
|
|
239
|
+
errorMessage: "任务执行异常,请重试",
|
|
200
240
|
});
|
|
201
241
|
finalSent = true;
|
|
202
|
-
log(`[ON_IDLE] ✅ Sent error response`);
|
|
242
|
+
log(`[ON_IDLE] ✅ Sent error response with code: 99921111`);
|
|
203
243
|
}
|
|
204
244
|
catch (err) {
|
|
205
245
|
error(`[ON_IDLE] Failed to send error response:`, err);
|
|
@@ -289,7 +329,6 @@ export function createXYReplyDispatcher(params) {
|
|
|
289
329
|
const currentTaskId = getActiveTaskId();
|
|
290
330
|
const currentMessageId = getActiveMessageId();
|
|
291
331
|
const text = payload.text ?? "";
|
|
292
|
-
log(`[PARTIAL REPLY] Partial reply chunk received, taskId: ${currentTaskId}`);
|
|
293
332
|
try {
|
|
294
333
|
if (text.length > 0) {
|
|
295
334
|
await sendReasoningTextUpdate({
|
|
@@ -300,7 +339,6 @@ export function createXYReplyDispatcher(params) {
|
|
|
300
339
|
text,
|
|
301
340
|
append: false,
|
|
302
341
|
});
|
|
303
|
-
log(`[PARTIAL REPLY] ✅ Sent partial reply as reasoningText update`);
|
|
304
342
|
}
|
|
305
343
|
}
|
|
306
344
|
catch (err) {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk";
|
|
2
|
+
/**
|
|
3
|
+
* 在 handleXYMessage 入口处调用,缓存 cfg/runtime 供 steer 注入使用。
|
|
4
|
+
*/
|
|
5
|
+
export declare function setCachedContext(cfg: ClawdbotConfig, runtime: RuntimeEnv, accountId: string): void;
|
|
6
|
+
/**
|
|
7
|
+
* 尝试向当前活跃会话注入 steer 消息。
|
|
8
|
+
* 两层保险:
|
|
9
|
+
* 1. getSessionContext(sessionKey) 确认是当前 XY 活跃 session
|
|
10
|
+
* 2. hasActiveTask(sessionId) 确认任务仍在运行
|
|
11
|
+
*
|
|
12
|
+
* @param sessionKey 来自 after_tool_call ctx.sessionKey(per-peer 下精确对应一个 XY session)
|
|
13
|
+
* @param message 要注入的用户消息文本
|
|
14
|
+
* @returns true=已注入,false=跳过
|
|
15
|
+
*/
|
|
16
|
+
export declare function tryInjectSteer(sessionKey: string | undefined, message: string): Promise<boolean>;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Steer message injector for CSPL hook integration
|
|
2
|
+
import { getSessionContext } from "./tools/session-manager.js";
|
|
3
|
+
import { hasActiveTask, getCurrentTaskId } from "./task-manager.js";
|
|
4
|
+
import { handleXYMessage } from "./bot.js";
|
|
5
|
+
import { logger } from "./utils/logger.js";
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
let cachedCfg = null;
|
|
8
|
+
let cachedRuntime = null;
|
|
9
|
+
let cachedAccountId = "default";
|
|
10
|
+
/**
|
|
11
|
+
* 在 handleXYMessage 入口处调用,缓存 cfg/runtime 供 steer 注入使用。
|
|
12
|
+
*/
|
|
13
|
+
export function setCachedContext(cfg, runtime, accountId) {
|
|
14
|
+
cachedCfg = cfg;
|
|
15
|
+
cachedRuntime = runtime;
|
|
16
|
+
cachedAccountId = accountId;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* 尝试向当前活跃会话注入 steer 消息。
|
|
20
|
+
* 两层保险:
|
|
21
|
+
* 1. getSessionContext(sessionKey) 确认是当前 XY 活跃 session
|
|
22
|
+
* 2. hasActiveTask(sessionId) 确认任务仍在运行
|
|
23
|
+
*
|
|
24
|
+
* @param sessionKey 来自 after_tool_call ctx.sessionKey(per-peer 下精确对应一个 XY session)
|
|
25
|
+
* @param message 要注入的用户消息文本
|
|
26
|
+
* @returns true=已注入,false=跳过
|
|
27
|
+
*/
|
|
28
|
+
export async function tryInjectSteer(sessionKey, message) {
|
|
29
|
+
if (!sessionKey) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
const sessionCtx = getSessionContext(sessionKey);
|
|
33
|
+
if (!sessionCtx) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
const { sessionId } = sessionCtx;
|
|
37
|
+
const activeTaskId = getCurrentTaskId(sessionId);
|
|
38
|
+
if (!hasActiveTask(sessionId)) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
if (!cachedCfg || !cachedRuntime) {
|
|
42
|
+
logger.error("[STEER] No cached cfg/runtime available, cannot inject");
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
// 3. 构造合成 A2A 消息(伪装成用户在当前会话中发送的新消息)
|
|
46
|
+
const syntheticMessage = {
|
|
47
|
+
jsonrpc: "2.0",
|
|
48
|
+
method: "tasks/send",
|
|
49
|
+
id: `steer-msg-${randomUUID()}`,
|
|
50
|
+
params: {
|
|
51
|
+
sessionId,
|
|
52
|
+
id: activeTaskId ?? `steer-task-${randomUUID()}`,
|
|
53
|
+
agentLoginSessionId: "",
|
|
54
|
+
message: {
|
|
55
|
+
role: "user",
|
|
56
|
+
parts: [{ kind: "text", text: message }],
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
console.log(`[STEER] Injecting steer for sessionId=${sessionId}, taskId=${syntheticMessage.params.id}`);
|
|
61
|
+
try {
|
|
62
|
+
await handleXYMessage({
|
|
63
|
+
cfg: cachedCfg,
|
|
64
|
+
runtime: cachedRuntime,
|
|
65
|
+
message: syntheticMessage,
|
|
66
|
+
accountId: cachedAccountId,
|
|
67
|
+
});
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
logger.error(`[STEER] ❌ Failed to inject steer: ${err}`);
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
|
2
|
+
import { type BindingTargetKind } from "openclaw/plugin-sdk/conversation-runtime";
|
|
3
|
+
type XYBindingTargetKind = "subagent" | "session";
|
|
4
|
+
/**
|
|
5
|
+
* Xiaoyi thread binding record.
|
|
6
|
+
* Simplified from feishu - uses sessionId as conversationId, no parentConversationId.
|
|
7
|
+
*/
|
|
8
|
+
type XYThreadBindingRecord = {
|
|
9
|
+
accountId: string;
|
|
10
|
+
sessionId: string;
|
|
11
|
+
targetKind: XYBindingTargetKind;
|
|
12
|
+
targetSessionKey: string;
|
|
13
|
+
agentId?: string;
|
|
14
|
+
boundAt: number;
|
|
15
|
+
lastActivityAt: number;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Thread binding manager for Xiaoyi channel.
|
|
19
|
+
* Manages session bindings for single-account mode.
|
|
20
|
+
*/
|
|
21
|
+
type XYThreadBindingManager = {
|
|
22
|
+
accountId: string;
|
|
23
|
+
getBySessionId: (sessionId: string) => XYThreadBindingRecord | undefined;
|
|
24
|
+
listBySessionKey: (targetSessionKey: string) => XYThreadBindingRecord[];
|
|
25
|
+
bindSession: (params: {
|
|
26
|
+
sessionId: string;
|
|
27
|
+
targetKind: BindingTargetKind;
|
|
28
|
+
targetSessionKey: string;
|
|
29
|
+
metadata?: Record<string, unknown>;
|
|
30
|
+
}) => XYThreadBindingRecord | null;
|
|
31
|
+
touchSession: (sessionId: string, at?: number) => XYThreadBindingRecord | null;
|
|
32
|
+
unbindSession: (sessionId: string) => XYThreadBindingRecord | null;
|
|
33
|
+
unbindBySessionKey: (targetSessionKey: string) => XYThreadBindingRecord[];
|
|
34
|
+
stop: () => void;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Creates a thread binding manager for Xiaoyi channel.
|
|
38
|
+
* Based on feishu implementation but simplified for single-account mode.
|
|
39
|
+
*/
|
|
40
|
+
export declare function createXYThreadBindingManager(params: {
|
|
41
|
+
accountId?: string;
|
|
42
|
+
cfg: OpenClawConfig;
|
|
43
|
+
}): XYThreadBindingManager;
|
|
44
|
+
/**
|
|
45
|
+
* Gets the thread binding manager for a given account ID.
|
|
46
|
+
*/
|
|
47
|
+
export declare function getXYThreadBindingManager(accountId?: string): XYThreadBindingManager | null;
|
|
48
|
+
/**
|
|
49
|
+
* Testing utilities for thread bindings.
|
|
50
|
+
*/
|
|
51
|
+
export declare const __testing: {
|
|
52
|
+
resetXYThreadBindingsForTests(): void;
|
|
53
|
+
};
|
|
54
|
+
export {};
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { resolveThreadBindingIdleTimeoutMsForChannel, resolveThreadBindingMaxAgeMsForChannel, registerSessionBindingAdapter, resolveThreadBindingConversationIdFromBindingId, unregisterSessionBindingAdapter, } from "openclaw/plugin-sdk/conversation-runtime";
|
|
2
|
+
import { normalizeAccountId, resolveAgentIdFromSessionKey } from "openclaw/plugin-sdk/routing";
|
|
3
|
+
import { resolveGlobalSingleton } from "openclaw/plugin-sdk/text-runtime";
|
|
4
|
+
const XY_THREAD_BINDINGS_STATE_KEY = Symbol.for("openclaw.xyThreadBindingsState");
|
|
5
|
+
const state = resolveGlobalSingleton(XY_THREAD_BINDINGS_STATE_KEY, () => ({
|
|
6
|
+
managersByAccountId: new Map(),
|
|
7
|
+
bindingsByAccountSession: new Map(),
|
|
8
|
+
}));
|
|
9
|
+
function getState() {
|
|
10
|
+
return state;
|
|
11
|
+
}
|
|
12
|
+
function resolveBindingKey(params) {
|
|
13
|
+
return `${params.accountId}:${params.sessionId}`;
|
|
14
|
+
}
|
|
15
|
+
function toSessionBindingTargetKind(raw) {
|
|
16
|
+
return raw === "subagent" ? "subagent" : "session";
|
|
17
|
+
}
|
|
18
|
+
function toXYTargetKind(raw) {
|
|
19
|
+
return raw === "subagent" ? "subagent" : "session";
|
|
20
|
+
}
|
|
21
|
+
function toSessionBindingRecord(record, defaults) {
|
|
22
|
+
const idleExpiresAt = defaults.idleTimeoutMs > 0 ? record.lastActivityAt + defaults.idleTimeoutMs : undefined;
|
|
23
|
+
const maxAgeExpiresAt = defaults.maxAgeMs > 0 ? record.boundAt + defaults.maxAgeMs : undefined;
|
|
24
|
+
const expiresAt = idleExpiresAt != null && maxAgeExpiresAt != null
|
|
25
|
+
? Math.min(idleExpiresAt, maxAgeExpiresAt)
|
|
26
|
+
: (idleExpiresAt ?? maxAgeExpiresAt);
|
|
27
|
+
return {
|
|
28
|
+
bindingId: resolveBindingKey({
|
|
29
|
+
accountId: record.accountId,
|
|
30
|
+
sessionId: record.sessionId,
|
|
31
|
+
}),
|
|
32
|
+
targetSessionKey: record.targetSessionKey,
|
|
33
|
+
targetKind: toSessionBindingTargetKind(record.targetKind),
|
|
34
|
+
conversation: {
|
|
35
|
+
channel: "xiaoyi-channel",
|
|
36
|
+
accountId: record.accountId,
|
|
37
|
+
conversationId: record.sessionId, // sessionId is the conversationId for Xiaoyi
|
|
38
|
+
parentConversationId: undefined, // Xiaoyi doesn't have parent conversations
|
|
39
|
+
},
|
|
40
|
+
status: "active",
|
|
41
|
+
boundAt: record.boundAt,
|
|
42
|
+
expiresAt,
|
|
43
|
+
metadata: {
|
|
44
|
+
agentId: record.agentId,
|
|
45
|
+
lastActivityAt: record.lastActivityAt,
|
|
46
|
+
idleTimeoutMs: defaults.idleTimeoutMs,
|
|
47
|
+
maxAgeMs: defaults.maxAgeMs,
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Creates a thread binding manager for Xiaoyi channel.
|
|
53
|
+
* Based on feishu implementation but simplified for single-account mode.
|
|
54
|
+
*/
|
|
55
|
+
export function createXYThreadBindingManager(params) {
|
|
56
|
+
const accountId = normalizeAccountId(params.accountId);
|
|
57
|
+
const existing = getState().managersByAccountId.get(accountId);
|
|
58
|
+
if (existing) {
|
|
59
|
+
return existing;
|
|
60
|
+
}
|
|
61
|
+
const idleTimeoutMs = resolveThreadBindingIdleTimeoutMsForChannel({
|
|
62
|
+
cfg: params.cfg,
|
|
63
|
+
channel: "xiaoyi-channel",
|
|
64
|
+
accountId,
|
|
65
|
+
});
|
|
66
|
+
const maxAgeMs = resolveThreadBindingMaxAgeMsForChannel({
|
|
67
|
+
cfg: params.cfg,
|
|
68
|
+
channel: "xiaoyi-channel",
|
|
69
|
+
accountId,
|
|
70
|
+
});
|
|
71
|
+
const manager = {
|
|
72
|
+
accountId,
|
|
73
|
+
getBySessionId: (sessionId) => getState().bindingsByAccountSession.get(resolveBindingKey({ accountId, sessionId })),
|
|
74
|
+
listBySessionKey: (targetSessionKey) => [...getState().bindingsByAccountSession.values()].filter((record) => record.accountId === accountId && record.targetSessionKey === targetSessionKey),
|
|
75
|
+
bindSession: ({ sessionId, targetKind, targetSessionKey, metadata, }) => {
|
|
76
|
+
const normalizedSessionId = sessionId.trim();
|
|
77
|
+
if (!normalizedSessionId || !targetSessionKey.trim()) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
const now = Date.now();
|
|
81
|
+
const record = {
|
|
82
|
+
accountId,
|
|
83
|
+
sessionId: normalizedSessionId,
|
|
84
|
+
targetKind: toXYTargetKind(targetKind),
|
|
85
|
+
targetSessionKey: targetSessionKey.trim(),
|
|
86
|
+
agentId: typeof metadata?.agentId === "string" && metadata.agentId.trim()
|
|
87
|
+
? metadata.agentId.trim()
|
|
88
|
+
: resolveAgentIdFromSessionKey(targetSessionKey),
|
|
89
|
+
boundAt: now,
|
|
90
|
+
lastActivityAt: now,
|
|
91
|
+
};
|
|
92
|
+
getState().bindingsByAccountSession.set(resolveBindingKey({ accountId, sessionId: normalizedSessionId }), record);
|
|
93
|
+
return record;
|
|
94
|
+
},
|
|
95
|
+
touchSession: (sessionId, at = Date.now()) => {
|
|
96
|
+
const key = resolveBindingKey({ accountId, sessionId });
|
|
97
|
+
const existingRecord = getState().bindingsByAccountSession.get(key);
|
|
98
|
+
if (!existingRecord) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
const updated = { ...existingRecord, lastActivityAt: at };
|
|
102
|
+
getState().bindingsByAccountSession.set(key, updated);
|
|
103
|
+
return updated;
|
|
104
|
+
},
|
|
105
|
+
unbindSession: (sessionId) => {
|
|
106
|
+
const key = resolveBindingKey({ accountId, sessionId });
|
|
107
|
+
const existingRecord = getState().bindingsByAccountSession.get(key);
|
|
108
|
+
if (!existingRecord) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
getState().bindingsByAccountSession.delete(key);
|
|
112
|
+
return existingRecord;
|
|
113
|
+
},
|
|
114
|
+
unbindBySessionKey: (targetSessionKey) => {
|
|
115
|
+
const removed = [];
|
|
116
|
+
for (const record of [...getState().bindingsByAccountSession.values()]) {
|
|
117
|
+
if (record.accountId !== accountId || record.targetSessionKey !== targetSessionKey) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
getState().bindingsByAccountSession.delete(resolveBindingKey({ accountId, sessionId: record.sessionId }));
|
|
121
|
+
removed.push(record);
|
|
122
|
+
}
|
|
123
|
+
return removed;
|
|
124
|
+
},
|
|
125
|
+
stop: () => {
|
|
126
|
+
for (const key of [...getState().bindingsByAccountSession.keys()]) {
|
|
127
|
+
if (key.startsWith(`${accountId}:`)) {
|
|
128
|
+
getState().bindingsByAccountSession.delete(key);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
getState().managersByAccountId.delete(accountId);
|
|
132
|
+
unregisterSessionBindingAdapter({
|
|
133
|
+
channel: "xiaoyi-channel",
|
|
134
|
+
accountId,
|
|
135
|
+
adapter: sessionBindingAdapter,
|
|
136
|
+
});
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
const sessionBindingAdapter = {
|
|
140
|
+
channel: "xiaoyi-channel",
|
|
141
|
+
accountId,
|
|
142
|
+
capabilities: {
|
|
143
|
+
placements: ["current"],
|
|
144
|
+
},
|
|
145
|
+
bind: async (input) => {
|
|
146
|
+
if (input.conversation.channel !== "xiaoyi-channel" || input.placement === "child") {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
const bound = manager.bindSession({
|
|
150
|
+
sessionId: input.conversation.conversationId,
|
|
151
|
+
targetKind: input.targetKind,
|
|
152
|
+
targetSessionKey: input.targetSessionKey,
|
|
153
|
+
metadata: input.metadata,
|
|
154
|
+
});
|
|
155
|
+
return bound ? toSessionBindingRecord(bound, { idleTimeoutMs, maxAgeMs }) : null;
|
|
156
|
+
},
|
|
157
|
+
listBySession: (targetSessionKey) => manager
|
|
158
|
+
.listBySessionKey(targetSessionKey)
|
|
159
|
+
.map((entry) => toSessionBindingRecord(entry, { idleTimeoutMs, maxAgeMs })),
|
|
160
|
+
resolveByConversation: (ref) => {
|
|
161
|
+
if (ref.channel !== "xiaoyi-channel") {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
const found = manager.getBySessionId(ref.conversationId);
|
|
165
|
+
return found ? toSessionBindingRecord(found, { idleTimeoutMs, maxAgeMs }) : null;
|
|
166
|
+
},
|
|
167
|
+
touch: (bindingId, at) => {
|
|
168
|
+
const sessionId = resolveThreadBindingConversationIdFromBindingId({
|
|
169
|
+
accountId,
|
|
170
|
+
bindingId,
|
|
171
|
+
});
|
|
172
|
+
if (sessionId) {
|
|
173
|
+
manager.touchSession(sessionId, at);
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
unbind: async (input) => {
|
|
177
|
+
if (input.targetSessionKey?.trim()) {
|
|
178
|
+
return manager
|
|
179
|
+
.unbindBySessionKey(input.targetSessionKey.trim())
|
|
180
|
+
.map((entry) => toSessionBindingRecord(entry, { idleTimeoutMs, maxAgeMs }));
|
|
181
|
+
}
|
|
182
|
+
const sessionId = resolveThreadBindingConversationIdFromBindingId({
|
|
183
|
+
accountId,
|
|
184
|
+
bindingId: input.bindingId,
|
|
185
|
+
});
|
|
186
|
+
if (!sessionId) {
|
|
187
|
+
return [];
|
|
188
|
+
}
|
|
189
|
+
const removed = manager.unbindSession(sessionId);
|
|
190
|
+
return removed ? [toSessionBindingRecord(removed, { idleTimeoutMs, maxAgeMs })] : [];
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
registerSessionBindingAdapter(sessionBindingAdapter);
|
|
194
|
+
getState().managersByAccountId.set(accountId, manager);
|
|
195
|
+
return manager;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Gets the thread binding manager for a given account ID.
|
|
199
|
+
*/
|
|
200
|
+
export function getXYThreadBindingManager(accountId) {
|
|
201
|
+
return getState().managersByAccountId.get(normalizeAccountId(accountId)) ?? null;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Testing utilities for thread bindings.
|
|
205
|
+
*/
|
|
206
|
+
export const __testing = {
|
|
207
|
+
resetXYThreadBindingsForTests() {
|
|
208
|
+
for (const manager of getState().managersByAccountId.values()) {
|
|
209
|
+
manager.stop();
|
|
210
|
+
}
|
|
211
|
+
getState().managersByAccountId.clear();
|
|
212
|
+
getState().bindingsByAccountSession.clear();
|
|
213
|
+
},
|
|
214
|
+
};
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { getXYWebSocketManager } from "../client.js";
|
|
2
2
|
import { sendCommand } from "../formatter.js";
|
|
3
3
|
import { getCurrentSessionContext } from "./session-manager.js";
|
|
4
|
-
import { logger } from "../utils/logger.js";
|
|
5
4
|
/**
|
|
6
5
|
* XY calendar event tool - creates a calendar event on user's device.
|
|
7
6
|
* Requires title, dtStart (start time), and dtEnd (end time) parameters.
|
|
@@ -12,6 +11,8 @@ export const calendarTool = {
|
|
|
12
11
|
label: "Create Calendar Event",
|
|
13
12
|
description: `在用户设备上创建日程。需要提供日程标题、开始时间和结束时间。时间格式必须为:yyyy-mm-dd hh:mm:ss(例如:2024-01-15 14:30:00)。注意:该工具执行时间较长(最多60秒),请勿重复调用,超时或失败时最多重试一次。
|
|
14
13
|
注意事项:使用该工具之前需获取当前真实时间
|
|
14
|
+
|
|
15
|
+
回复约束:如果工具返回没有授权或者其他报错,只需要完整描述没有授权或者其他报错内容即可,不需要主动给用户提供解决方案,例如告诉用户如何授权,如何解决报错等都是不需要的,请严格遵守。
|
|
15
16
|
`,
|
|
16
17
|
parameters: {
|
|
17
18
|
type: "object",
|
|
@@ -32,47 +33,25 @@ export const calendarTool = {
|
|
|
32
33
|
required: ["title", "dtStart", "dtEnd"],
|
|
33
34
|
},
|
|
34
35
|
async execute(toolCallId, params) {
|
|
35
|
-
logger.log(`[CALENDAR_TOOL] 🚀 Starting execution`);
|
|
36
|
-
logger.log(`[CALENDAR_TOOL] - toolCallId: ${toolCallId}`);
|
|
37
|
-
logger.log(`[CALENDAR_TOOL] - params:`, JSON.stringify(params));
|
|
38
|
-
logger.log(`[CALENDAR_TOOL] - timestamp: ${new Date().toISOString()}`);
|
|
39
36
|
// Validate parameters
|
|
40
37
|
if (!params.title || !params.dtStart || !params.dtEnd) {
|
|
41
|
-
logger.error(`[CALENDAR_TOOL] ❌ Missing required parameters`);
|
|
42
38
|
throw new Error("Missing required parameters: title, dtStart, and dtEnd are required");
|
|
43
39
|
}
|
|
44
40
|
// Convert time strings to millisecond timestamps
|
|
45
|
-
logger.log(`[CALENDAR_TOOL] 🕒 Converting time strings to timestamps...`);
|
|
46
|
-
logger.log(`[CALENDAR_TOOL] - dtStart input: ${params.dtStart}`);
|
|
47
|
-
logger.log(`[CALENDAR_TOOL] - dtEnd input: ${params.dtEnd}`);
|
|
48
41
|
const dtStartMs = new Date(params.dtStart).getTime();
|
|
49
42
|
const dtEndMs = new Date(params.dtEnd).getTime();
|
|
50
43
|
if (isNaN(dtStartMs) || isNaN(dtEndMs)) {
|
|
51
|
-
logger.error(`[CALENDAR_TOOL] ❌ Invalid time format`);
|
|
52
44
|
throw new Error("Invalid time format. Required format: yyyy-mm-dd hh:mm:ss (e.g., 2024-01-15 14:30:00)");
|
|
53
45
|
}
|
|
54
|
-
logger.log(`[CALENDAR_TOOL] ✅ Time conversion successful`);
|
|
55
|
-
logger.log(`[CALENDAR_TOOL] - dtStart timestamp: ${dtStartMs}`);
|
|
56
|
-
logger.log(`[CALENDAR_TOOL] - dtEnd timestamp: ${dtEndMs}`);
|
|
57
46
|
// Get session context
|
|
58
|
-
logger.log(`[CALENDAR_TOOL] 🔍 Attempting to get session context...`);
|
|
59
47
|
const sessionContext = getCurrentSessionContext();
|
|
60
48
|
if (!sessionContext) {
|
|
61
|
-
logger.error(`[CALENDAR_TOOL] ❌ FAILED: No active session found!`);
|
|
62
|
-
logger.error(`[CALENDAR_TOOL] - toolCallId: ${toolCallId}`);
|
|
63
49
|
throw new Error("No active XY session found. Calendar tool can only be used during an active conversation.");
|
|
64
50
|
}
|
|
65
|
-
logger.log(`[CALENDAR_TOOL] ✅ Session context found`);
|
|
66
|
-
logger.log(`[CALENDAR_TOOL] - sessionId: ${sessionContext.sessionId}`);
|
|
67
|
-
logger.log(`[CALENDAR_TOOL] - taskId: ${sessionContext.taskId}`);
|
|
68
|
-
logger.log(`[CALENDAR_TOOL] - messageId: ${sessionContext.messageId}`);
|
|
69
51
|
const { config, sessionId, taskId, messageId } = sessionContext;
|
|
70
52
|
// Get WebSocket manager
|
|
71
|
-
logger.log(`[CALENDAR_TOOL] 🔌 Getting WebSocket manager...`);
|
|
72
53
|
const wsManager = getXYWebSocketManager(config);
|
|
73
|
-
logger.log(`[CALENDAR_TOOL] ✅ WebSocket manager obtained`);
|
|
74
54
|
// Build CreateCalendarEvent command
|
|
75
|
-
logger.log(`[CALENDAR_TOOL] 📦 Building CreateCalendarEvent command...`);
|
|
76
55
|
const command = {
|
|
77
56
|
header: {
|
|
78
57
|
namespace: "Common",
|
|
@@ -108,25 +87,17 @@ export const calendarTool = {
|
|
|
108
87
|
},
|
|
109
88
|
};
|
|
110
89
|
// Send command and wait for response (60 second timeout)
|
|
111
|
-
logger.log(`[CALENDAR_TOOL] ⏳ Setting up promise to wait for calendar event response...`);
|
|
112
|
-
logger.log(`[CALENDAR_TOOL] - Timeout: 60 seconds`);
|
|
113
90
|
return new Promise((resolve, reject) => {
|
|
114
91
|
const timeout = setTimeout(() => {
|
|
115
|
-
logger.error(`[CALENDAR_TOOL] ⏰ Timeout: No response received within 60 seconds`);
|
|
116
92
|
wsManager.off("data-event", handler);
|
|
117
93
|
reject(new Error("创建日程超时(60秒)"));
|
|
118
94
|
}, 60000);
|
|
119
95
|
// Listen for data events from WebSocket
|
|
120
96
|
const handler = (event) => {
|
|
121
|
-
logger.log(`[CALENDAR_TOOL] 📨 Received data event:`, JSON.stringify(event));
|
|
122
97
|
if (event.intentName === "CreateCalendarEvent") {
|
|
123
|
-
logger.log(`[CALENDAR_TOOL] 🎯 CreateCalendarEvent event received`);
|
|
124
|
-
logger.log(`[CALENDAR_TOOL] - status: ${event.status}`);
|
|
125
98
|
clearTimeout(timeout);
|
|
126
99
|
wsManager.off("data-event", handler);
|
|
127
100
|
if (event.status === "success" && event.outputs) {
|
|
128
|
-
logger.log(`[CALENDAR_TOOL] ✅ Calendar event created successfully`);
|
|
129
|
-
logger.log(`[CALENDAR_TOOL] - outputs:`, JSON.stringify(event.outputs));
|
|
130
101
|
resolve({
|
|
131
102
|
content: [
|
|
132
103
|
{
|
|
@@ -137,17 +108,13 @@ export const calendarTool = {
|
|
|
137
108
|
});
|
|
138
109
|
}
|
|
139
110
|
else {
|
|
140
|
-
logger.error(`[CALENDAR_TOOL] ❌ Calendar event creation failed`);
|
|
141
|
-
logger.error(`[CALENDAR_TOOL] - status: ${event.status}`);
|
|
142
111
|
reject(new Error(`创建日程失败: ${event.status}`));
|
|
143
112
|
}
|
|
144
113
|
}
|
|
145
114
|
};
|
|
146
115
|
// Register event handler
|
|
147
|
-
logger.log(`[CALENDAR_TOOL] 📡 Registering data-event handler on WebSocket manager`);
|
|
148
116
|
wsManager.on("data-event", handler);
|
|
149
117
|
// Send the command
|
|
150
|
-
logger.log(`[CALENDAR_TOOL] 📤 Sending CreateCalendarEvent command...`);
|
|
151
118
|
sendCommand({
|
|
152
119
|
config,
|
|
153
120
|
sessionId,
|
|
@@ -156,10 +123,8 @@ export const calendarTool = {
|
|
|
156
123
|
command,
|
|
157
124
|
})
|
|
158
125
|
.then(() => {
|
|
159
|
-
logger.log(`[CALENDAR_TOOL] ✅ Command sent successfully, waiting for response...`);
|
|
160
126
|
})
|
|
161
127
|
.catch((error) => {
|
|
162
|
-
logger.error(`[CALENDAR_TOOL] ❌ Failed to send command:`, error);
|
|
163
128
|
clearTimeout(timeout);
|
|
164
129
|
wsManager.off("data-event", handler);
|
|
165
130
|
reject(error);
|