@ynhcj/xiaoyi-channel 0.0.52-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/channel.js +16 -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/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 +6 -2
- package/dist/src/thread-bindings.d.ts +54 -0
- package/dist/src/thread-bindings.js +214 -0
- 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/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
|
},
|
|
@@ -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/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,4 +1,3 @@
|
|
|
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";
|
|
@@ -63,7 +62,12 @@ export function createXYReplyDispatcher(params) {
|
|
|
63
62
|
};
|
|
64
63
|
const core = getXYRuntime();
|
|
65
64
|
const config = resolveXYConfig(cfg);
|
|
66
|
-
|
|
65
|
+
// Simplified prefix context for single-account Xiaoyi channel
|
|
66
|
+
const prefixContext = {
|
|
67
|
+
responsePrefix: undefined,
|
|
68
|
+
responsePrefixContextProvider: undefined,
|
|
69
|
+
onModelSelected: undefined,
|
|
70
|
+
};
|
|
67
71
|
let statusUpdateInterval = null;
|
|
68
72
|
let hasSentResponse = false;
|
|
69
73
|
let finalSent = false;
|
|
@@ -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
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ynhcj/xiaoyi-channel",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.53-beta",
|
|
4
4
|
"description": "OpenClaw Xiaoyi Channel plugin - Xiaoyi A2A protocol integration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
}
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
|
-
"openclaw": ">=2026.3.
|
|
52
|
+
"openclaw": ">=2026.3.24"
|
|
53
53
|
},
|
|
54
54
|
"peerDependenciesMeta": {
|
|
55
55
|
"openclaw": {
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* XY search photo tool - searches photos in user's gallery.
|
|
3
|
-
* Returns publicly accessible URLs of matching photos based on query description.
|
|
4
|
-
*
|
|
5
|
-
* This tool performs a two-step operation:
|
|
6
|
-
* 1. Search for photos using query description
|
|
7
|
-
* 2. Upload found photos to get publicly accessible URLs
|
|
8
|
-
*/
|
|
9
|
-
export declare const searchPhotoTool: any;
|
|
@@ -1,270 +0,0 @@
|
|
|
1
|
-
import { getXYWebSocketManager } from "../client.js";
|
|
2
|
-
import { sendCommand } from "../formatter.js";
|
|
3
|
-
import { getLatestSessionContext } from "./session-manager.js";
|
|
4
|
-
import { logger } from "../utils/logger.js";
|
|
5
|
-
/**
|
|
6
|
-
* XY search photo tool - searches photos in user's gallery.
|
|
7
|
-
* Returns publicly accessible URLs of matching photos based on query description.
|
|
8
|
-
*
|
|
9
|
-
* This tool performs a two-step operation:
|
|
10
|
-
* 1. Search for photos using query description
|
|
11
|
-
* 2. Upload found photos to get publicly accessible URLs
|
|
12
|
-
*/
|
|
13
|
-
export const searchPhotoTool = {
|
|
14
|
-
name: "search_photo",
|
|
15
|
-
label: "Search Photo",
|
|
16
|
-
description: "搜索用户手机图库中的照片。根据图像描述语料检索匹配的照片,并返回照片的可公网访问URL。注意:操作超时时间为120秒,请勿重复调用此工具,如果超时或失败,最多重试一次。",
|
|
17
|
-
parameters: {
|
|
18
|
-
type: "object",
|
|
19
|
-
properties: {
|
|
20
|
-
query: {
|
|
21
|
-
type: "string",
|
|
22
|
-
description: "图像描述语料,用于检索匹配的照片(例如:'小狗的照片'、'带有键盘的图片'等)",
|
|
23
|
-
},
|
|
24
|
-
},
|
|
25
|
-
required: ["query"],
|
|
26
|
-
},
|
|
27
|
-
async execute(toolCallId, params) {
|
|
28
|
-
logger.log(`[SEARCH_PHOTO_TOOL] 🚀 Starting execution`);
|
|
29
|
-
logger.log(`[SEARCH_PHOTO_TOOL] - toolCallId: ${toolCallId}`);
|
|
30
|
-
logger.log(`[SEARCH_PHOTO_TOOL] - params:`, JSON.stringify(params));
|
|
31
|
-
logger.log(`[SEARCH_PHOTO_TOOL] - timestamp: ${new Date().toISOString()}`);
|
|
32
|
-
// Validate parameters
|
|
33
|
-
if (!params.query) {
|
|
34
|
-
logger.error(`[SEARCH_PHOTO_TOOL] ❌ Missing required parameter: query`);
|
|
35
|
-
throw new Error("Missing required parameter: query is required");
|
|
36
|
-
}
|
|
37
|
-
// Get session context
|
|
38
|
-
logger.log(`[SEARCH_PHOTO_TOOL] 🔍 Attempting to get session context...`);
|
|
39
|
-
const sessionContext = getLatestSessionContext();
|
|
40
|
-
if (!sessionContext) {
|
|
41
|
-
logger.error(`[SEARCH_PHOTO_TOOL] ❌ FAILED: No active session found!`);
|
|
42
|
-
logger.error(`[SEARCH_PHOTO_TOOL] - toolCallId: ${toolCallId}`);
|
|
43
|
-
throw new Error("No active XY session found. Search photo tool can only be used during an active conversation.");
|
|
44
|
-
}
|
|
45
|
-
logger.log(`[SEARCH_PHOTO_TOOL] ✅ Session context found`);
|
|
46
|
-
logger.log(`[SEARCH_PHOTO_TOOL] - sessionId: ${sessionContext.sessionId}`);
|
|
47
|
-
logger.log(`[SEARCH_PHOTO_TOOL] - taskId: ${sessionContext.taskId}`);
|
|
48
|
-
logger.log(`[SEARCH_PHOTO_TOOL] - messageId: ${sessionContext.messageId}`);
|
|
49
|
-
const { config, sessionId, taskId, messageId } = sessionContext;
|
|
50
|
-
// Get WebSocket manager
|
|
51
|
-
logger.log(`[SEARCH_PHOTO_TOOL] 🔌 Getting WebSocket manager...`);
|
|
52
|
-
const wsManager = getXYWebSocketManager(config);
|
|
53
|
-
logger.log(`[SEARCH_PHOTO_TOOL] ✅ WebSocket manager obtained`);
|
|
54
|
-
// Step 1: Search for photos
|
|
55
|
-
logger.log(`[SEARCH_PHOTO_TOOL] 📸 STEP 1: Searching for photos...`);
|
|
56
|
-
const mediaUris = await searchPhotos(wsManager, config, sessionId, taskId, messageId, params.query);
|
|
57
|
-
if (!mediaUris || mediaUris.length === 0) {
|
|
58
|
-
logger.warn(`[SEARCH_PHOTO_TOOL] ⚠️ No photos found for query: ${params.query}`);
|
|
59
|
-
return {
|
|
60
|
-
content: [
|
|
61
|
-
{
|
|
62
|
-
type: "text",
|
|
63
|
-
text: JSON.stringify({ imageUrls: [], message: "未找到匹配的照片" }),
|
|
64
|
-
},
|
|
65
|
-
],
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
logger.log(`[SEARCH_PHOTO_TOOL] ✅ Found ${mediaUris.length} photos`);
|
|
69
|
-
logger.log(`[SEARCH_PHOTO_TOOL] - mediaUris:`, JSON.stringify(mediaUris));
|
|
70
|
-
// Step 2: Get public URLs for the photos
|
|
71
|
-
logger.log(`[SEARCH_PHOTO_TOOL] 🌐 STEP 2: Getting public URLs for photos...`);
|
|
72
|
-
const imageUrls = await getPhotoUrls(wsManager, config, sessionId, taskId, messageId, mediaUris);
|
|
73
|
-
logger.log(`[SEARCH_PHOTO_TOOL] 🎉 Successfully retrieved ${imageUrls.length} photo URLs`);
|
|
74
|
-
return {
|
|
75
|
-
content: [
|
|
76
|
-
{
|
|
77
|
-
type: "text",
|
|
78
|
-
text: JSON.stringify({ imageUrls }),
|
|
79
|
-
},
|
|
80
|
-
],
|
|
81
|
-
};
|
|
82
|
-
},
|
|
83
|
-
};
|
|
84
|
-
/**
|
|
85
|
-
* Step 1: Search for photos using query description
|
|
86
|
-
* Returns array of mediaUri strings
|
|
87
|
-
*/
|
|
88
|
-
async function searchPhotos(wsManager, config, sessionId, taskId, messageId, query) {
|
|
89
|
-
logger.log(`[SEARCH_PHOTO_TOOL] 📦 Building SearchPhotoVideo command...`);
|
|
90
|
-
const command = {
|
|
91
|
-
header: {
|
|
92
|
-
namespace: "Common",
|
|
93
|
-
name: "Action",
|
|
94
|
-
},
|
|
95
|
-
payload: {
|
|
96
|
-
cardParam: {},
|
|
97
|
-
executeParam: {
|
|
98
|
-
executeMode: "background",
|
|
99
|
-
intentName: "SearchPhotoVideo",
|
|
100
|
-
bundleName: "com.huawei.hmos.aidispatchservice",
|
|
101
|
-
needUnlock: true,
|
|
102
|
-
actionResponse: true,
|
|
103
|
-
appType: "OHOS_APP",
|
|
104
|
-
timeOut: 5,
|
|
105
|
-
intentParam: {
|
|
106
|
-
query: query,
|
|
107
|
-
},
|
|
108
|
-
permissionId: [],
|
|
109
|
-
achieveType: "INTENT",
|
|
110
|
-
},
|
|
111
|
-
responses: [
|
|
112
|
-
{
|
|
113
|
-
resultCode: "",
|
|
114
|
-
displayText: "",
|
|
115
|
-
ttsText: "",
|
|
116
|
-
},
|
|
117
|
-
],
|
|
118
|
-
needUploadResult: true,
|
|
119
|
-
noHalfPage: false,
|
|
120
|
-
pageControlRelated: false,
|
|
121
|
-
},
|
|
122
|
-
};
|
|
123
|
-
return new Promise((resolve, reject) => {
|
|
124
|
-
const timeout = setTimeout(() => {
|
|
125
|
-
logger.error(`[SEARCH_PHOTO_TOOL] ⏰ Timeout: No response for SearchPhotoVideo within 60 seconds`);
|
|
126
|
-
wsManager.off("data-event", handler);
|
|
127
|
-
reject(new Error("搜索照片超时(60秒)"));
|
|
128
|
-
}, 60000);
|
|
129
|
-
const handler = (event) => {
|
|
130
|
-
logger.log(`[SEARCH_PHOTO_TOOL] 📨 Received data event (Step 1):`, JSON.stringify(event));
|
|
131
|
-
if (event.intentName === "SearchPhotoVideo") {
|
|
132
|
-
logger.log(`[SEARCH_PHOTO_TOOL] 🎯 SearchPhotoVideo event received`);
|
|
133
|
-
logger.log(`[SEARCH_PHOTO_TOOL] - status: ${event.status}`);
|
|
134
|
-
clearTimeout(timeout);
|
|
135
|
-
wsManager.off("data-event", handler);
|
|
136
|
-
if (event.status === "success" && event.outputs) {
|
|
137
|
-
logger.log(`[SEARCH_PHOTO_TOOL] ✅ Photo search completed successfully`);
|
|
138
|
-
const result = event.outputs.result;
|
|
139
|
-
const items = result?.items || [];
|
|
140
|
-
// Extract mediaUri from each item
|
|
141
|
-
const mediaUris = items.map((item) => item.mediaUri).filter(Boolean);
|
|
142
|
-
logger.log(`[SEARCH_PHOTO_TOOL] 📊 Extracted ${mediaUris.length} mediaUris`);
|
|
143
|
-
resolve(mediaUris);
|
|
144
|
-
}
|
|
145
|
-
else {
|
|
146
|
-
logger.error(`[SEARCH_PHOTO_TOOL] ❌ Photo search failed`);
|
|
147
|
-
logger.error(`[SEARCH_PHOTO_TOOL] - status: ${event.status}`);
|
|
148
|
-
reject(new Error(`搜索照片失败: ${event.status}`));
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
};
|
|
152
|
-
logger.log(`[SEARCH_PHOTO_TOOL] 📡 Registering data-event handler for SearchPhotoVideo`);
|
|
153
|
-
wsManager.on("data-event", handler);
|
|
154
|
-
logger.log(`[SEARCH_PHOTO_TOOL] 📤 Sending SearchPhotoVideo command...`);
|
|
155
|
-
sendCommand({
|
|
156
|
-
config,
|
|
157
|
-
sessionId,
|
|
158
|
-
taskId,
|
|
159
|
-
messageId,
|
|
160
|
-
command,
|
|
161
|
-
})
|
|
162
|
-
.then(() => {
|
|
163
|
-
logger.log(`[SEARCH_PHOTO_TOOL] ✅ SearchPhotoVideo command sent successfully`);
|
|
164
|
-
})
|
|
165
|
-
.catch((error) => {
|
|
166
|
-
logger.error(`[SEARCH_PHOTO_TOOL] ❌ Failed to send SearchPhotoVideo command:`, error);
|
|
167
|
-
clearTimeout(timeout);
|
|
168
|
-
wsManager.off("data-event", handler);
|
|
169
|
-
reject(error);
|
|
170
|
-
});
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
/**
|
|
174
|
-
* Step 2: Get public URLs for photos using mediaUris
|
|
175
|
-
* Returns array of publicly accessible image URLs
|
|
176
|
-
*/
|
|
177
|
-
async function getPhotoUrls(wsManager, config, sessionId, taskId, messageId, mediaUris) {
|
|
178
|
-
logger.log(`[SEARCH_PHOTO_TOOL] 📦 Building ImageUploadForClaw command...`);
|
|
179
|
-
// Build imageInfos array from mediaUris
|
|
180
|
-
const imageInfos = mediaUris.map(mediaUri => ({ mediaUri }));
|
|
181
|
-
const command = {
|
|
182
|
-
header: {
|
|
183
|
-
namespace: "Common",
|
|
184
|
-
name: "Action",
|
|
185
|
-
},
|
|
186
|
-
payload: {
|
|
187
|
-
cardParam: {},
|
|
188
|
-
executeParam: {
|
|
189
|
-
executeMode: "background",
|
|
190
|
-
intentName: "ImageUploadForClaw",
|
|
191
|
-
bundleName: "com.huawei.hmos.vassistant",
|
|
192
|
-
needUnlock: true,
|
|
193
|
-
actionResponse: true,
|
|
194
|
-
appType: "OHOS_APP",
|
|
195
|
-
timeOut: 5,
|
|
196
|
-
intentParam: {
|
|
197
|
-
imageInfos: imageInfos,
|
|
198
|
-
},
|
|
199
|
-
permissionId: [],
|
|
200
|
-
achieveType: "INTENT",
|
|
201
|
-
},
|
|
202
|
-
responses: [
|
|
203
|
-
{
|
|
204
|
-
resultCode: "",
|
|
205
|
-
displayText: "",
|
|
206
|
-
ttsText: "",
|
|
207
|
-
},
|
|
208
|
-
],
|
|
209
|
-
needUploadResult: true,
|
|
210
|
-
noHalfPage: false,
|
|
211
|
-
pageControlRelated: false,
|
|
212
|
-
},
|
|
213
|
-
};
|
|
214
|
-
return new Promise((resolve, reject) => {
|
|
215
|
-
const timeout = setTimeout(() => {
|
|
216
|
-
logger.error(`[SEARCH_PHOTO_TOOL] ⏰ Timeout: No response for ImageUploadForClaw within 60 seconds`);
|
|
217
|
-
wsManager.off("data-event", handler);
|
|
218
|
-
reject(new Error("获取照片URL超时(60秒)"));
|
|
219
|
-
}, 60000);
|
|
220
|
-
const handler = (event) => {
|
|
221
|
-
logger.log(`[SEARCH_PHOTO_TOOL] 📨 Received data event (Step 2):`, JSON.stringify(event));
|
|
222
|
-
if (event.intentName === "ImageUploadForClaw") {
|
|
223
|
-
logger.log(`[SEARCH_PHOTO_TOOL] 🎯 ImageUploadForClaw event received`);
|
|
224
|
-
logger.log(`[SEARCH_PHOTO_TOOL] - status: ${event.status}`);
|
|
225
|
-
clearTimeout(timeout);
|
|
226
|
-
wsManager.off("data-event", handler);
|
|
227
|
-
if (event.status === "success" && event.outputs) {
|
|
228
|
-
logger.log(`[SEARCH_PHOTO_TOOL] ✅ Image URL retrieval completed successfully`);
|
|
229
|
-
const result = event.outputs.result;
|
|
230
|
-
let imageUrls = result?.imageUrls || [];
|
|
231
|
-
// Decode Unicode escape sequences in URLs
|
|
232
|
-
// Replace \u003d with = and \u0026 with &
|
|
233
|
-
imageUrls = imageUrls.map((url) => {
|
|
234
|
-
const decodedUrl = url
|
|
235
|
-
.replace(/\\u003d/g, '=')
|
|
236
|
-
.replace(/\\u0026/g, '&');
|
|
237
|
-
logger.log(`[SEARCH_PHOTO_TOOL] 🔄 Decoded URL: ${url} -> ${decodedUrl}`);
|
|
238
|
-
return decodedUrl;
|
|
239
|
-
});
|
|
240
|
-
logger.log(`[SEARCH_PHOTO_TOOL] 📊 Retrieved and decoded ${imageUrls.length} image URLs`);
|
|
241
|
-
resolve(imageUrls);
|
|
242
|
-
}
|
|
243
|
-
else {
|
|
244
|
-
logger.error(`[SEARCH_PHOTO_TOOL] ❌ Image URL retrieval failed`);
|
|
245
|
-
logger.error(`[SEARCH_PHOTO_TOOL] - status: ${event.status}`);
|
|
246
|
-
reject(new Error(`获取照片URL失败: ${event.status}`));
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
};
|
|
250
|
-
logger.log(`[SEARCH_PHOTO_TOOL] 📡 Registering data-event handler for ImageUploadForClaw`);
|
|
251
|
-
wsManager.on("data-event", handler);
|
|
252
|
-
logger.log(`[SEARCH_PHOTO_TOOL] 📤 Sending ImageUploadForClaw command...`);
|
|
253
|
-
sendCommand({
|
|
254
|
-
config,
|
|
255
|
-
sessionId,
|
|
256
|
-
taskId,
|
|
257
|
-
messageId,
|
|
258
|
-
command,
|
|
259
|
-
})
|
|
260
|
-
.then(() => {
|
|
261
|
-
logger.log(`[SEARCH_PHOTO_TOOL] ✅ ImageUploadForClaw command sent successfully`);
|
|
262
|
-
})
|
|
263
|
-
.catch((error) => {
|
|
264
|
-
logger.error(`[SEARCH_PHOTO_TOOL] ❌ Failed to send ImageUploadForClaw command:`, error);
|
|
265
|
-
clearTimeout(timeout);
|
|
266
|
-
wsManager.off("data-event", handler);
|
|
267
|
-
reject(error);
|
|
268
|
-
});
|
|
269
|
-
});
|
|
270
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import type { SessionBinding, ServerIdentifier } from "../types.js";
|
|
2
|
-
/**
|
|
3
|
-
* Session-to-server binding cache.
|
|
4
|
-
* Tracks which WebSocket server each session is bound to.
|
|
5
|
-
*/
|
|
6
|
-
declare class SessionManager {
|
|
7
|
-
private bindings;
|
|
8
|
-
/**
|
|
9
|
-
* Bind a session to a specific server.
|
|
10
|
-
*/
|
|
11
|
-
bind(sessionId: string, server: ServerIdentifier): void;
|
|
12
|
-
/**
|
|
13
|
-
* Get the server binding for a session.
|
|
14
|
-
*/
|
|
15
|
-
getBinding(sessionId: string): ServerIdentifier | null;
|
|
16
|
-
/**
|
|
17
|
-
* Check if a session is bound to a server.
|
|
18
|
-
*/
|
|
19
|
-
isBound(sessionId: string): boolean;
|
|
20
|
-
/**
|
|
21
|
-
* Unbind a session.
|
|
22
|
-
*/
|
|
23
|
-
unbind(sessionId: string): void;
|
|
24
|
-
/**
|
|
25
|
-
* Clear all bindings.
|
|
26
|
-
*/
|
|
27
|
-
clear(): void;
|
|
28
|
-
/**
|
|
29
|
-
* Get all bindings.
|
|
30
|
-
*/
|
|
31
|
-
getAll(): SessionBinding[];
|
|
32
|
-
}
|
|
33
|
-
export declare const sessionManager: SessionManager;
|
|
34
|
-
export {};
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Session-to-server binding cache.
|
|
3
|
-
* Tracks which WebSocket server each session is bound to.
|
|
4
|
-
*/
|
|
5
|
-
class SessionManager {
|
|
6
|
-
bindings = new Map();
|
|
7
|
-
/**
|
|
8
|
-
* Bind a session to a specific server.
|
|
9
|
-
*/
|
|
10
|
-
bind(sessionId, server) {
|
|
11
|
-
this.bindings.set(sessionId, {
|
|
12
|
-
sessionId,
|
|
13
|
-
server,
|
|
14
|
-
boundAt: Date.now(),
|
|
15
|
-
});
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* Get the server binding for a session.
|
|
19
|
-
*/
|
|
20
|
-
getBinding(sessionId) {
|
|
21
|
-
const binding = this.bindings.get(sessionId);
|
|
22
|
-
return binding ? binding.server : null;
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* Check if a session is bound to a server.
|
|
26
|
-
*/
|
|
27
|
-
isBound(sessionId) {
|
|
28
|
-
return this.bindings.has(sessionId);
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Unbind a session.
|
|
32
|
-
*/
|
|
33
|
-
unbind(sessionId) {
|
|
34
|
-
this.bindings.delete(sessionId);
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Clear all bindings.
|
|
38
|
-
*/
|
|
39
|
-
clear() {
|
|
40
|
-
this.bindings.clear();
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Get all bindings.
|
|
44
|
-
*/
|
|
45
|
-
getAll() {
|
|
46
|
-
return Array.from(this.bindings.values());
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
// Singleton instance
|
|
50
|
-
export const sessionManager = new SessionManager();
|