@ynhcj/xiaoyi-channel 0.0.51-beta → 0.0.53-beta
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 +0 -2
- package/dist/src/bot.js +0 -16
- package/dist/src/channel.js +16 -2
- package/dist/src/client.js +0 -2
- package/dist/src/cspl/call-api.js +30 -2
- package/dist/src/cspl/constants.d.ts +1 -1
- package/dist/src/cspl/constants.js +1 -1
- package/dist/src/formatter.js +3 -23
- package/dist/src/heartbeat.js +0 -1
- 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/reply-dispatcher.js +38 -2
- 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 +1 -42
- package/dist/src/tools/create-alarm-tool.js +3 -74
- package/dist/src/tools/delete-alarm-tool.js +3 -45
- package/dist/src/tools/image-reading-tool.js +0 -47
- package/dist/src/tools/location-tool.js +1 -32
- package/dist/src/tools/modify-alarm-tool.js +3 -77
- package/dist/src/tools/modify-note-tool.js +1 -34
- package/dist/src/tools/note-tool.js +2 -4
- package/dist/src/tools/search-alarm-tool.js +3 -61
- package/dist/src/tools/search-calendar-tool.js +2 -39
- package/dist/src/tools/search-contact-tool.js +0 -30
- package/dist/src/tools/search-file-tool.js +0 -33
- package/dist/src/tools/search-message-tool.js +0 -33
- package/dist/src/tools/search-note-tool.js +1 -26
- package/dist/src/tools/search-photo-gallery-tool.js +2 -31
- package/dist/src/tools/send-file-to-user-tool.js +0 -39
- package/dist/src/tools/send-message-tool.js +1 -39
- package/dist/src/tools/session-manager.js +0 -45
- 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/websocket.js +24 -8
- package/package.json +2 -2
- 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
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
-
import { xyPlugin } from "./src/channel.js";
|
|
3
2
|
/**
|
|
4
3
|
* Xiaoyi Channel Plugin Entry Point.
|
|
5
4
|
* Exports the plugin for OpenClaw to load.
|
|
@@ -13,4 +12,3 @@ declare const plugin: {
|
|
|
13
12
|
register(api: OpenClawPluginApi): void;
|
|
14
13
|
};
|
|
15
14
|
export default plugin;
|
|
16
|
-
export { xyPlugin };
|
package/dist/index.js
CHANGED
package/dist/src/bot.js
CHANGED
|
@@ -336,19 +336,3 @@ function buildXYMediaPayload(mediaList) {
|
|
|
336
336
|
MediaTypes: mediaTypes.length > 0 ? mediaTypes : undefined,
|
|
337
337
|
};
|
|
338
338
|
}
|
|
339
|
-
/**
|
|
340
|
-
* Infer OpenClaw media type from file type string.
|
|
341
|
-
*/
|
|
342
|
-
function inferMediaType(fileType) {
|
|
343
|
-
const lower = fileType.toLowerCase();
|
|
344
|
-
if (lower.includes("image") || /\.(jpg|jpeg|png|gif|bmp|webp)$/i.test(lower)) {
|
|
345
|
-
return "image";
|
|
346
|
-
}
|
|
347
|
-
if (lower.includes("video") || /\.(mp4|avi|mov|mkv|webm)$/i.test(lower)) {
|
|
348
|
-
return "video";
|
|
349
|
-
}
|
|
350
|
-
if (lower.includes("audio") || /\.(mp3|wav|ogg|m4a)$/i.test(lower)) {
|
|
351
|
-
return "audio";
|
|
352
|
-
}
|
|
353
|
-
return "file";
|
|
354
|
-
}
|
package/dist/src/channel.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { resolveXYConfig, listXYAccountIds, getDefaultXYAccountId } from "./config.js";
|
|
2
2
|
import { xyConfigSchema } from "./config-schema.js";
|
|
3
3
|
import { xyOutbound } from "./outbound.js";
|
|
4
|
-
import { xyOnboardingAdapter } from "./onboarding.js";
|
|
5
4
|
import { locationTool } from "./tools/location-tool.js";
|
|
6
5
|
import { noteTool } from "./tools/note-tool.js";
|
|
7
6
|
import { searchNoteTool } from "./tools/search-note-tool.js";
|
|
@@ -63,7 +62,6 @@ export const xyPlugin = {
|
|
|
63
62
|
schema: xyConfigSchema,
|
|
64
63
|
},
|
|
65
64
|
outbound: xyOutbound,
|
|
66
|
-
onboarding: xyOnboardingAdapter,
|
|
67
65
|
agentTools: [locationTool, noteTool, searchNoteTool, modifyNoteTool, calendarTool, searchCalendarTool, searchContactTool, searchPhotoGalleryTool, uploadPhotoTool, xiaoyiGuiTool, xiaoyiCollectionTool, callPhoneTool, searchMessageTool, sendMessageTool, searchFileTool, uploadFileTool, createAlarmTool, searchAlarmTool, modifyAlarmTool, deleteAlarmTool, sendFileToUserTool, viewPushResultTool, imageReadingTool],
|
|
68
66
|
messaging: {
|
|
69
67
|
normalizeTarget: (raw) => {
|
|
@@ -81,6 +79,22 @@ export const xyPlugin = {
|
|
|
81
79
|
hint: "<sessionId>",
|
|
82
80
|
},
|
|
83
81
|
},
|
|
82
|
+
bindings: {
|
|
83
|
+
compileConfiguredBinding: ({ conversationId }) => {
|
|
84
|
+
const sessionId = conversationId.trim();
|
|
85
|
+
if (!sessionId)
|
|
86
|
+
return null;
|
|
87
|
+
return {
|
|
88
|
+
conversationId: sessionId,
|
|
89
|
+
parentConversationId: undefined,
|
|
90
|
+
};
|
|
91
|
+
},
|
|
92
|
+
matchInboundConversation: ({ compiledBinding, conversationId }) => {
|
|
93
|
+
return compiledBinding.conversationId === conversationId
|
|
94
|
+
? { conversationId, matchPriority: 2 }
|
|
95
|
+
: null;
|
|
96
|
+
},
|
|
97
|
+
},
|
|
84
98
|
reload: {
|
|
85
99
|
configPrefixes: ["channels.xiaoyi-channel"],
|
|
86
100
|
},
|
package/dist/src/client.js
CHANGED
|
@@ -85,8 +85,6 @@ export function diagnoseAllManagers() {
|
|
|
85
85
|
let orphanCount = 0;
|
|
86
86
|
wsManagerCache.forEach((manager, key) => {
|
|
87
87
|
const diag = manager.getConnectionDiagnostics();
|
|
88
|
-
console.log(`📌 Manager: ${key}`);
|
|
89
|
-
console.log(` Shutting down: ${diag.isShuttingDown}`);
|
|
90
88
|
console.log(` Total event listeners on manager: ${diag.totalEventListeners}`);
|
|
91
89
|
// Connection
|
|
92
90
|
console.log(` 🔌 Connection:`);
|
|
@@ -48,9 +48,27 @@ export async function callCsplApi(questionText, cfg) {
|
|
|
48
48
|
textSource: config.textSource,
|
|
49
49
|
action: config.action,
|
|
50
50
|
};
|
|
51
|
+
// 打印请求信息
|
|
52
|
+
console.log(`[CSPL API] ==================== 发起请求 ====================`);
|
|
53
|
+
console.log(`[CSPL API] URL: ${config.api.url}`);
|
|
54
|
+
console.log(`[CSPL API] Method: POST`);
|
|
55
|
+
console.log(`[CSPL API] Headers:`);
|
|
56
|
+
console.log(`[CSPL API] - x-hag-trace-id: ${headers["x-hag-trace-id"]}`);
|
|
57
|
+
console.log(`[CSPL API] - x-uid: ${headers["x-uid"]}`);
|
|
58
|
+
console.log(`[CSPL API] - x-api-key: ${headers["x-api-key"] ? "***" + headers["x-api-key"].slice(-8) : "undefined"}`);
|
|
59
|
+
console.log(`[CSPL API] - x-request-from: ${headers["x-request-from"]}`);
|
|
60
|
+
console.log(`[CSPL API] - x-skill-id: ${headers["x-skill-id"]}`);
|
|
61
|
+
console.log(`[CSPL API] - content-type: ${headers["content-type"]}`);
|
|
62
|
+
console.log(`[CSPL API] Body:`);
|
|
63
|
+
console.log(`[CSPL API] - questionText: ${questionText.substring(0, 100)}${questionText.length > 100 ? "..." : ""}`);
|
|
64
|
+
console.log(`[CSPL API] - textSource: ${payload.textSource}`);
|
|
65
|
+
console.log(`[CSPL API] - action: ${payload.action}`);
|
|
66
|
+
console.log(`[CSPL API] =================================================`);
|
|
51
67
|
return new Promise((resolve, reject) => {
|
|
52
68
|
const options = buildRequestOptions(config.api.url, headers, config.api.timeout);
|
|
53
69
|
const req = https.request(options, (res) => {
|
|
70
|
+
console.log(`[CSPL API] Response Status: ${res.statusCode}`);
|
|
71
|
+
console.log(`[CSPL API] Response Headers: ${JSON.stringify(res.headers)}`);
|
|
54
72
|
if (res.statusCode && res.statusCode >= HTTP_STATUS_BAD_REQUEST) {
|
|
55
73
|
reject(new Error(`[CSPL] HTTP error: ${res.statusCode}`));
|
|
56
74
|
return;
|
|
@@ -61,15 +79,25 @@ export async function callCsplApi(questionText, cfg) {
|
|
|
61
79
|
});
|
|
62
80
|
res.on("end", () => {
|
|
63
81
|
try {
|
|
64
|
-
|
|
82
|
+
const result = parseResponse(data);
|
|
83
|
+
console.log(`[CSPL API] ✅ 请求成功`);
|
|
84
|
+
console.log(`[CSPL API] Response Body: ${data.substring(0, 200)}${data.length > 200 ? "..." : ""}`);
|
|
85
|
+
console.log(`[CSPL API] =================================================`);
|
|
86
|
+
resolve(result);
|
|
65
87
|
}
|
|
66
88
|
catch (e) {
|
|
89
|
+
console.error(`[CSPL API] ❌ 请求失败: ${e instanceof Error ? e.message : String(e)}`);
|
|
90
|
+
console.error(`[CSPL API] Response Body: ${data}`);
|
|
67
91
|
reject(e);
|
|
68
92
|
}
|
|
69
93
|
});
|
|
70
94
|
});
|
|
71
|
-
req.on("error",
|
|
95
|
+
req.on("error", (error) => {
|
|
96
|
+
console.error(`[CSPL API] ❌ 请求错误: ${error instanceof Error ? error.message : String(error)}`);
|
|
97
|
+
reject(error);
|
|
98
|
+
});
|
|
72
99
|
req.on("timeout", () => {
|
|
100
|
+
console.error(`[CSPL API] ⏰ 请求超时 (${config.api.timeout}ms)`);
|
|
73
101
|
req.destroy();
|
|
74
102
|
reject(new Error("[CSPL] Request timeout"));
|
|
75
103
|
});
|
|
@@ -25,7 +25,7 @@ export declare const MIN_TEXT_LENGTH = 0;
|
|
|
25
25
|
export declare const MAX_TEXT_LENGTH = 4096;
|
|
26
26
|
export declare const MAX_TOTAL_LENGTH = 40960;
|
|
27
27
|
export declare const regex: RegExp;
|
|
28
|
-
export declare const DEFAULT_HTTP_PORT =
|
|
28
|
+
export declare const DEFAULT_HTTP_PORT = 443;
|
|
29
29
|
export declare const HTTP_STATUS_BAD_REQUEST = 400;
|
|
30
30
|
export declare const ENV_FILE_PATH = "/home/sandbox/.openclaw/.xiaoyienv";
|
|
31
31
|
export declare const API_URL_SUFFIX = "/celia-claw/v1/rest-api/skill/execute";
|
|
@@ -3,7 +3,7 @@ export const MIN_TEXT_LENGTH = 0;
|
|
|
3
3
|
export const MAX_TEXT_LENGTH = 4096;
|
|
4
4
|
export const MAX_TOTAL_LENGTH = 40960;
|
|
5
5
|
export const regex = /[^\u4e00-\u9fa5a-zA-Z0-9\s\.,!?;:,。!?;:""\'\'()()\[\]【】]/;
|
|
6
|
-
export const DEFAULT_HTTP_PORT =
|
|
6
|
+
export const DEFAULT_HTTP_PORT = 443;
|
|
7
7
|
export const HTTP_STATUS_BAD_REQUEST = 400;
|
|
8
8
|
export const ENV_FILE_PATH = "/home/sandbox/.openclaw/.xiaoyienv";
|
|
9
9
|
export const API_URL_SUFFIX = "/celia-claw/v1/rest-api/skill/execute";
|
package/dist/src/formatter.js
CHANGED
|
@@ -52,18 +52,11 @@ export async function sendA2AResponse(params) {
|
|
|
52
52
|
msgDetail: JSON.stringify(jsonRpcResponse),
|
|
53
53
|
};
|
|
54
54
|
// 📋 Log complete response body
|
|
55
|
-
log(`[A2A_RESPONSE] 📤 Sending A2A artifact-update response
|
|
56
|
-
log(`[A2A_RESPONSE] - sessionId: ${sessionId}`);
|
|
57
|
-
log(`[A2A_RESPONSE] - taskId: ${taskId}`);
|
|
58
|
-
log(`[A2A_RESPONSE] - messageId: ${messageId}`);
|
|
55
|
+
log(`[A2A_RESPONSE] 📤 Sending A2A artifact-update response: taskId: ${taskId}`);
|
|
59
56
|
log(`[A2A_RESPONSE] - append: ${append}`);
|
|
60
57
|
log(`[A2A_RESPONSE] - final: ${final}`);
|
|
61
|
-
log(`[A2A_RESPONSE] - text
|
|
58
|
+
log(`[A2A_RESPONSE] - text: ${text}`);
|
|
62
59
|
log(`[A2A_RESPONSE] - files count: ${files?.length ?? 0}`);
|
|
63
|
-
log(`[A2A_RESPONSE] 📦 Complete outbound message:`);
|
|
64
|
-
log(JSON.stringify(outboundMessage, null, 2));
|
|
65
|
-
log(`[A2A_RESPONSE] 📦 JSON-RPC response body:`);
|
|
66
|
-
log(JSON.stringify(jsonRpcResponse, null, 2));
|
|
67
60
|
await wsManager.sendMessage(sessionId, outboundMessage);
|
|
68
61
|
log(`[A2A_RESPONSE] ✅ Message sent successfully`);
|
|
69
62
|
}
|
|
@@ -152,15 +145,10 @@ export async function sendStatusUpdate(params) {
|
|
|
152
145
|
};
|
|
153
146
|
// 📋 Log complete response body
|
|
154
147
|
log(`[A2A_STATUS] 📤 Sending A2A status-update:`);
|
|
155
|
-
log(`[A2A_STATUS] - sessionId: ${sessionId}`);
|
|
156
148
|
log(`[A2A_STATUS] - taskId: ${taskId}`);
|
|
157
149
|
log(`[A2A_STATUS] - messageId: ${messageId}`);
|
|
158
150
|
log(`[A2A_STATUS] - state: ${state}`);
|
|
159
151
|
log(`[A2A_STATUS] - text: "${text}"`);
|
|
160
|
-
log(`[A2A_STATUS] 📦 Complete outbound message:`);
|
|
161
|
-
log(JSON.stringify(outboundMessage, null, 2));
|
|
162
|
-
log(`[A2A_STATUS] 📦 JSON-RPC response body:`);
|
|
163
|
-
log(JSON.stringify(jsonRpcResponse, null, 2));
|
|
164
152
|
await wsManager.sendMessage(sessionId, outboundMessage);
|
|
165
153
|
log(`[A2A_STATUS] ✅ Status update sent successfully`);
|
|
166
154
|
}
|
|
@@ -208,15 +196,7 @@ export async function sendCommand(params) {
|
|
|
208
196
|
msgDetail: JSON.stringify(jsonRpcResponse),
|
|
209
197
|
};
|
|
210
198
|
// 📋 Log complete response body
|
|
211
|
-
log(`[A2A_COMMAND] 📤 Sending A2A command
|
|
212
|
-
log(`[A2A_COMMAND] - sessionId: ${sessionId}`);
|
|
213
|
-
log(`[A2A_COMMAND] - taskId: ${taskId}`);
|
|
214
|
-
log(`[A2A_COMMAND] - messageId: ${messageId}`);
|
|
215
|
-
log(`[A2A_COMMAND] - command: ${command.header.namespace}::${command.header.name}`);
|
|
216
|
-
log(`[A2A_COMMAND] 📦 Complete outbound message:`);
|
|
217
|
-
log(JSON.stringify(outboundMessage, null, 2));
|
|
218
|
-
log(`[A2A_COMMAND] 📦 JSON-RPC response body:`);
|
|
219
|
-
log(JSON.stringify(jsonRpcResponse, null, 2));
|
|
199
|
+
log(`[A2A_COMMAND] 📤 Sending A2A command: taskId: ${taskId}`);
|
|
220
200
|
await wsManager.sendMessage(sessionId, outboundMessage);
|
|
221
201
|
log(`[A2A_COMMAND] ✅ Command sent successfully`);
|
|
222
202
|
}
|
package/dist/src/heartbeat.js
CHANGED
|
@@ -80,7 +80,6 @@ export class HeartbeatManager {
|
|
|
80
80
|
this.error(`Heartbeat timeout for ${this.serverName}`);
|
|
81
81
|
this.onTimeout();
|
|
82
82
|
}, this.config.timeout);
|
|
83
|
-
this.log(`[DEBUG] Heartbeat sent for ${this.serverName}`);
|
|
84
83
|
}
|
|
85
84
|
catch (error) {
|
|
86
85
|
this.error(`Failed to send heartbeat for ${this.serverName}:`, error);
|
package/dist/src/onboarding.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import type { ChannelOnboardingAdapter } from "openclaw/plugin-sdk";
|
|
2
1
|
/**
|
|
3
|
-
* XY Channel Onboarding Adapter
|
|
4
|
-
*
|
|
2
|
+
* XY Channel Onboarding Adapter (DISABLED - not currently used)
|
|
3
|
+
* NOTE: This is kept for future reference. Xiaoyi uses simple single-account config.
|
|
5
4
|
*/
|
|
6
|
-
export declare const xyOnboardingAdapter:
|
|
5
|
+
export declare const xyOnboardingAdapter: any;
|
package/dist/src/onboarding.js
CHANGED
|
@@ -152,8 +152,8 @@ async function configure({ cfg, prompter, }) {
|
|
|
152
152
|
};
|
|
153
153
|
}
|
|
154
154
|
/**
|
|
155
|
-
* XY Channel Onboarding Adapter
|
|
156
|
-
*
|
|
155
|
+
* XY Channel Onboarding Adapter (DISABLED - not currently used)
|
|
156
|
+
* NOTE: This is kept for future reference. Xiaoyi uses simple single-account config.
|
|
157
157
|
*/
|
|
158
158
|
export const xyOnboardingAdapter = {
|
|
159
159
|
channel,
|
package/dist/src/outbound.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
type ChannelOutboundAdapter = any;
|
|
2
2
|
/**
|
|
3
3
|
* Outbound adapter for sending messages from OpenClaw to XY.
|
|
4
4
|
* Uses Push service for direct message delivery.
|
|
5
5
|
*/
|
|
6
6
|
export declare const xyOutbound: ChannelOutboundAdapter;
|
|
7
|
+
export {};
|
|
@@ -1,8 +1,38 @@
|
|
|
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
|
+
/**
|
|
8
|
+
* 清理 /tmp/xy_channel 目录中的所有文件
|
|
9
|
+
*/
|
|
10
|
+
async function cleanupTempDir(tempDir = "/tmp/xy_channel") {
|
|
11
|
+
try {
|
|
12
|
+
const stats = await fs.stat(tempDir).catch(() => null);
|
|
13
|
+
if (!stats?.isDirectory()) {
|
|
14
|
+
return; // 目录不存在,直接返回
|
|
15
|
+
}
|
|
16
|
+
const files = await fs.readdir(tempDir);
|
|
17
|
+
let cleanedCount = 0;
|
|
18
|
+
for (const file of files) {
|
|
19
|
+
const filePath = path.join(tempDir, file);
|
|
20
|
+
try {
|
|
21
|
+
await fs.unlink(filePath);
|
|
22
|
+
cleanedCount++;
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
// 忽略单个文件删除失败,继续处理其他文件
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (cleanedCount > 0) {
|
|
29
|
+
console.log(`[CLEANUP] 🧹 Cleaned ${cleanedCount} files from ${tempDir}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
console.error(`[CLEANUP] ❌ Failed to cleanup temp dir:`, err);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
6
36
|
/**
|
|
7
37
|
* Create a reply dispatcher for XY channel messages.
|
|
8
38
|
* Follows feishu pattern with status updates and streaming support.
|
|
@@ -32,7 +62,12 @@ export function createXYReplyDispatcher(params) {
|
|
|
32
62
|
};
|
|
33
63
|
const core = getXYRuntime();
|
|
34
64
|
const config = resolveXYConfig(cfg);
|
|
35
|
-
|
|
65
|
+
// Simplified prefix context for single-account Xiaoyi channel
|
|
66
|
+
const prefixContext = {
|
|
67
|
+
responsePrefix: undefined,
|
|
68
|
+
responsePrefixContextProvider: undefined,
|
|
69
|
+
onModelSelected: undefined,
|
|
70
|
+
};
|
|
36
71
|
let statusUpdateInterval = null;
|
|
37
72
|
let hasSentResponse = false;
|
|
38
73
|
let finalSent = false;
|
|
@@ -206,6 +241,7 @@ export function createXYReplyDispatcher(params) {
|
|
|
206
241
|
}
|
|
207
242
|
}
|
|
208
243
|
stopStatusInterval();
|
|
244
|
+
void cleanupTempDir();
|
|
209
245
|
},
|
|
210
246
|
onCleanup: () => {
|
|
211
247
|
const currentTaskId = getActiveTaskId();
|
|
@@ -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
|
+
};
|