@ynhcj/xiaoyi-channel 0.0.97-beta → 0.0.99-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.js +13 -0
- package/dist/src/channel.js +2 -1
- package/dist/src/login-token-handler.d.ts +8 -0
- package/dist/src/login-token-handler.js +60 -0
- package/dist/src/monitor.js +7 -0
- package/dist/src/provider.js +57 -16
- package/dist/src/skill-retriever/config.d.ts +4 -0
- package/dist/src/skill-retriever/config.js +23 -0
- package/dist/src/skill-retriever/hooks.d.ts +22 -0
- package/dist/src/skill-retriever/hooks.js +87 -0
- package/dist/src/skill-retriever/tool-search.d.ts +16 -0
- package/dist/src/skill-retriever/tool-search.js +159 -0
- package/dist/src/skill-retriever/types.d.ts +34 -0
- package/dist/src/skill-retriever/types.js +1 -0
- package/dist/src/tools/login-token-tool.d.ts +5 -0
- package/dist/src/tools/login-token-tool.js +136 -0
- package/dist/src/websocket.js +12 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6,6 +6,8 @@ import { tryInjectSteer } from "./src/steer-injector.js";
|
|
|
6
6
|
import { callCsplApi } from "./src/cspl/call-api.js";
|
|
7
7
|
import { extractResultText, processText, parseSecurityResult, validateAndTruncateText } from "./src/cspl/utils.js";
|
|
8
8
|
import { ALLOWED_TOOLS, MIN_TEXT_LENGTH, MAX_TOTAL_LENGTH, MAX_TEXT_LENGTH, STEER_ABORT_MESSAGE, } from "./src/cspl/constants.js";
|
|
9
|
+
import { createBeforePromptBuildHandler } from "./src/skill-retriever/hooks.js";
|
|
10
|
+
import { normalizeToolRetrieverConfig } from "./src/skill-retriever/config.js";
|
|
9
11
|
/**
|
|
10
12
|
* Xiaoyi Channel Plugin Entry Point.
|
|
11
13
|
* Exports the plugin for OpenClaw to load.
|
|
@@ -59,6 +61,17 @@ const plugin = {
|
|
|
59
61
|
api.logger.error(`[SENTINEL HOOK] after_tool_call error: ${err}`);
|
|
60
62
|
}
|
|
61
63
|
});
|
|
64
|
+
// SKILL RETRIEVER HOOK: before_prompt_build hook
|
|
65
|
+
const pluginConfig = api.pluginConfig || {};
|
|
66
|
+
const skillRetrieverConfig = normalizeToolRetrieverConfig({
|
|
67
|
+
enabled: pluginConfig.skillRetrieverEnabled ?? true,
|
|
68
|
+
maxTools: pluginConfig.skillRetrieverMaxTools ?? 2,
|
|
69
|
+
includeUninstalledOnly: true,
|
|
70
|
+
envFilePath: "~/.openclaw/.xiaoyienv",
|
|
71
|
+
timeoutMs: pluginConfig.skillRetrieverTimeoutMs ?? 1000,
|
|
72
|
+
});
|
|
73
|
+
const beforePromptBuildHandler = createBeforePromptBuildHandler(skillRetrieverConfig);
|
|
74
|
+
api.on("before_prompt_build", beforePromptBuildHandler);
|
|
62
75
|
},
|
|
63
76
|
};
|
|
64
77
|
export default plugin;
|
package/dist/src/channel.js
CHANGED
|
@@ -19,6 +19,7 @@ import { getCollectionToolSchemaTool } from "./tools/get-collection-tool-schema.
|
|
|
19
19
|
import { queryAppMessageTool } from "./tools/query-app-message-tool.js";
|
|
20
20
|
import { queryMemoryDataTool } from "./tools/query-memory-data-tool.js";
|
|
21
21
|
import { queryTodoTaskTool } from "./tools/query-todo-task-tool.js";
|
|
22
|
+
import { loginTokenTool } from "./tools/login-token-tool.js";
|
|
22
23
|
import { filterToolsByDevice } from "./tools/device-tool-map.js";
|
|
23
24
|
import { getCurrentSessionContext } from "./tools/session-manager.js";
|
|
24
25
|
import { logger } from "./utils/logger.js";
|
|
@@ -61,7 +62,7 @@ export const xyPlugin = {
|
|
|
61
62
|
},
|
|
62
63
|
outbound: xyOutbound,
|
|
63
64
|
agentTools: () => {
|
|
64
|
-
const allTools = [locationTool, callDeviceTool, getNoteToolSchemaTool, getCalendarToolSchemaTool, getContactToolSchemaTool, getPhotoToolSchemaTool, xiaoyiGuiTool, getDeviceFileToolSchemaTool, getAlarmToolSchemaTool, getCollectionToolSchemaTool, sendFileToUserTool, viewPushResultTool, imageReadingTool, timestampToUtc8Tool, getEmailToolSchemaTool, queryAppMessageTool, queryMemoryDataTool, queryTodoTaskTool];
|
|
65
|
+
const allTools = [locationTool, callDeviceTool, getNoteToolSchemaTool, getCalendarToolSchemaTool, getContactToolSchemaTool, getPhotoToolSchemaTool, xiaoyiGuiTool, getDeviceFileToolSchemaTool, getAlarmToolSchemaTool, getCollectionToolSchemaTool, sendFileToUserTool, viewPushResultTool, imageReadingTool, timestampToUtc8Tool, getEmailToolSchemaTool, queryAppMessageTool, queryMemoryDataTool, queryTodoTaskTool, loginTokenTool];
|
|
65
66
|
const ctx = getCurrentSessionContext();
|
|
66
67
|
const filtered = filterToolsByDevice(allTools, ctx?.deviceType);
|
|
67
68
|
logger.log(`[DEVICE-FILTER] deviceType=${ctx?.deviceType ?? "(none)"}, tools: ${allTools.length} → ${filtered.length} (${filtered.map(t => t.name).join(", ")})`);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Login Token 事件处理器
|
|
2
|
+
// 监听 LoginTokenEvent.ClawAutoLogin 事件,将 clientId 写入 .xiaoyitoken.json
|
|
3
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
4
|
+
import { dirname } from "path";
|
|
5
|
+
const TOKEN_FILE_PATH = "/home/sandbox/.openclaw/.xiaoyitoken.json";
|
|
6
|
+
/**
|
|
7
|
+
* 处理 LoginTokenEvent.ClawAutoLogin 事件
|
|
8
|
+
* 将 clientId 和当前时间戳写入 .xiaoyitoken.json 文件
|
|
9
|
+
*
|
|
10
|
+
* @param context - 事件上下文,包含 event 对象
|
|
11
|
+
* @param runtime - 运行时环境
|
|
12
|
+
*/
|
|
13
|
+
export function handleLoginTokenEvent(context, runtime) {
|
|
14
|
+
const log = runtime?.log ?? console.log;
|
|
15
|
+
const error = runtime?.error ?? console.error;
|
|
16
|
+
try {
|
|
17
|
+
const clientId = context.event?.payload?.clientId;
|
|
18
|
+
if (!clientId || typeof clientId !== "string") {
|
|
19
|
+
error("[LOGIN_TOKEN_HANDLER] invalid payload: missing clientId");
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
log(`[LOGIN_TOKEN_HANDLER] received login token event, clientId=${clientId}`);
|
|
23
|
+
// Ensure directory exists
|
|
24
|
+
const dir = dirname(TOKEN_FILE_PATH);
|
|
25
|
+
if (!existsSync(dir)) {
|
|
26
|
+
mkdirSync(dir, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
let tokens = [];
|
|
29
|
+
if (existsSync(TOKEN_FILE_PATH)) {
|
|
30
|
+
try {
|
|
31
|
+
const content = readFileSync(TOKEN_FILE_PATH, "utf-8");
|
|
32
|
+
tokens = JSON.parse(content);
|
|
33
|
+
if (!Array.isArray(tokens)) {
|
|
34
|
+
tokens = [];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
tokens = [];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// Check if clientId already exists
|
|
42
|
+
const now = String(Date.now());
|
|
43
|
+
const existing = tokens.find((t) => t.clientId === clientId);
|
|
44
|
+
if (existing) {
|
|
45
|
+
// Update timestamp
|
|
46
|
+
existing.timestamp = now;
|
|
47
|
+
log(`[LOGIN_TOKEN_HANDLER] updated timestamp for clientId=${clientId}`);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
// Insert new entry
|
|
51
|
+
tokens.push({ clientId, timestamp: now });
|
|
52
|
+
log(`[LOGIN_TOKEN_HANDLER] inserted new entry for clientId=${clientId}`);
|
|
53
|
+
}
|
|
54
|
+
writeFileSync(TOKEN_FILE_PATH, JSON.stringify(tokens, null, 2), "utf-8");
|
|
55
|
+
log(`[LOGIN_TOKEN_HANDLER] wrote token file: ${TOKEN_FILE_PATH}`);
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
error("[LOGIN_TOKEN_HANDLER] failed to handle event:", err);
|
|
59
|
+
}
|
|
60
|
+
}
|
package/dist/src/monitor.js
CHANGED
|
@@ -5,6 +5,7 @@ import { parseA2AMessage } from "./parser.js";
|
|
|
5
5
|
import { hasActiveTask } from "./task-manager.js";
|
|
6
6
|
import { handleTriggerEvent } from "./trigger-handler.js";
|
|
7
7
|
import { handleSelfEvolutionEvent } from "./self-evolution-handler.js";
|
|
8
|
+
import { handleLoginTokenEvent } from "./login-token-handler.js";
|
|
8
9
|
import { cleanupStaleTempFiles } from "./reply-dispatcher.js";
|
|
9
10
|
/**
|
|
10
11
|
* Per-session serial queue that ensures messages from the same session are processed
|
|
@@ -161,6 +162,10 @@ export async function monitorXYProvider(opts = {}) {
|
|
|
161
162
|
log(`[MONITOR] Received self-evolution-event, dispatching to handler...`);
|
|
162
163
|
handleSelfEvolutionEvent(context, runtime);
|
|
163
164
|
};
|
|
165
|
+
const loginTokenEventHandler = (context) => {
|
|
166
|
+
log(`[MONITOR] Received login-token-event, dispatching to handler...`);
|
|
167
|
+
handleLoginTokenEvent(context, runtime);
|
|
168
|
+
};
|
|
164
169
|
const cleanup = () => {
|
|
165
170
|
log("XY gateway: cleaning up...");
|
|
166
171
|
// 🔍 Diagnose before cleanup
|
|
@@ -179,6 +184,7 @@ export async function monitorXYProvider(opts = {}) {
|
|
|
179
184
|
wsManager.off("error", errorHandler);
|
|
180
185
|
wsManager.off("trigger-event", triggerEventHandler);
|
|
181
186
|
wsManager.off("self-evolution-event", selfEvolutionHandler);
|
|
187
|
+
wsManager.off("login-token-event", loginTokenEventHandler);
|
|
182
188
|
// ✅ Disconnect the wsManager to prevent connection leaks
|
|
183
189
|
// This is safe because each gateway lifecycle should have clean connections
|
|
184
190
|
wsManager.disconnect();
|
|
@@ -210,6 +216,7 @@ export async function monitorXYProvider(opts = {}) {
|
|
|
210
216
|
wsManager.on("error", errorHandler);
|
|
211
217
|
wsManager.on("trigger-event", triggerEventHandler);
|
|
212
218
|
wsManager.on("self-evolution-event", selfEvolutionHandler);
|
|
219
|
+
wsManager.on("login-token-event", loginTokenEventHandler);
|
|
213
220
|
// Start periodic health check (every 6 hours)
|
|
214
221
|
console.log("🏥 Starting periodic health check (every 6 hours)...");
|
|
215
222
|
healthCheckInterval = setInterval(() => {
|
package/dist/src/provider.js
CHANGED
|
@@ -41,6 +41,25 @@ function isCronTriggered(messages) {
|
|
|
41
41
|
}
|
|
42
42
|
return /^\[cron:/i.test(text.trim());
|
|
43
43
|
}
|
|
44
|
+
/** Extract cron title from first user message matching `[cron:<uuid> <title>]`. */
|
|
45
|
+
function extractCronTitle(messages) {
|
|
46
|
+
if (!messages)
|
|
47
|
+
return undefined;
|
|
48
|
+
const firstUser = messages.find(m => m.role === "user");
|
|
49
|
+
if (!firstUser)
|
|
50
|
+
return undefined;
|
|
51
|
+
let text = "";
|
|
52
|
+
if (typeof firstUser.content === "string") {
|
|
53
|
+
text = firstUser.content;
|
|
54
|
+
}
|
|
55
|
+
else if (Array.isArray(firstUser.content)) {
|
|
56
|
+
const block = firstUser.content.find(b => b.type === "text" && typeof b.text === "string");
|
|
57
|
+
if (block)
|
|
58
|
+
text = block.text;
|
|
59
|
+
}
|
|
60
|
+
const match = text.trim().match(/^\[cron:[^\s]+\s+(.+)\]$/);
|
|
61
|
+
return match ? match[1] : undefined;
|
|
62
|
+
}
|
|
44
63
|
/** Compute retry delay in ms for the given 1-based attempt, with up to 10s jitter. */
|
|
45
64
|
function getRetryDelayMs(attempt, isCron = false) {
|
|
46
65
|
if (isCron) {
|
|
@@ -215,6 +234,8 @@ function createRetryingStream(createStream, cronJob) {
|
|
|
215
234
|
const HEADER_TRACE_ID = "x-hag-trace-id";
|
|
216
235
|
const HEADER_SESSION_ID = "x-session-id";
|
|
217
236
|
const HEADER_INTERACTION_ID = "x-interaction-id";
|
|
237
|
+
/** Internal key for passing fallback uid prefix from prepareExtraParams to wrapStreamFn. */
|
|
238
|
+
const FALLBACK_PREFIX_KEY = "_xiaoyi_fallback_prefix";
|
|
218
239
|
/**
|
|
219
240
|
* Encode uid via SHA-256 and take first 32 hex chars.
|
|
220
241
|
*/
|
|
@@ -255,18 +276,15 @@ export const xiaoyiProvider = {
|
|
|
255
276
|
[HEADER_INTERACTION_ID]: interactionId,
|
|
256
277
|
};
|
|
257
278
|
}
|
|
258
|
-
// Fallback: uid
|
|
279
|
+
// Fallback: store uid prefix for lazy timestamp generation in wrapStreamFn.
|
|
280
|
+
// This ensures each model call gets a fresh timestamp instead of reusing
|
|
281
|
+
// the same one across tool-use loops and retries.
|
|
259
282
|
const uid = getUidFromConfig(ctx.config);
|
|
260
283
|
if (!uid)
|
|
261
284
|
return undefined;
|
|
262
|
-
const prefix = encodeUid(uid);
|
|
263
|
-
const ts = Date.now();
|
|
264
|
-
const fallbackValue = `${prefix}_${ts}`;
|
|
265
285
|
return {
|
|
266
286
|
...ctx.extraParams,
|
|
267
|
-
[
|
|
268
|
-
[HEADER_SESSION_ID]: fallbackValue,
|
|
269
|
-
[HEADER_INTERACTION_ID]: fallbackValue,
|
|
287
|
+
[FALLBACK_PREFIX_KEY]: encodeUid(uid),
|
|
270
288
|
};
|
|
271
289
|
},
|
|
272
290
|
/**
|
|
@@ -286,17 +304,40 @@ export const xiaoyiProvider = {
|
|
|
286
304
|
// 每次请求时从 ctx.extraParams 动态读取 header
|
|
287
305
|
const dynamicHeaders = {};
|
|
288
306
|
if (ctx.extraParams) {
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
if (typeof traceId === "string") {
|
|
307
|
+
const fallbackPrefix = ctx.extraParams[FALLBACK_PREFIX_KEY];
|
|
308
|
+
if (typeof fallbackPrefix === "string") {
|
|
309
|
+
// Fallback mode: generate fresh timestamp per request
|
|
293
310
|
const isCron = isCronTriggered(context.messages);
|
|
294
|
-
|
|
311
|
+
const fallbackValue = `${fallbackPrefix}_${Date.now()}`;
|
|
312
|
+
dynamicHeaders[HEADER_TRACE_ID] = isCron ? `cron_${fallbackValue}` : fallbackValue;
|
|
313
|
+
dynamicHeaders[HEADER_SESSION_ID] = fallbackValue;
|
|
314
|
+
dynamicHeaders[HEADER_INTERACTION_ID] = fallbackValue;
|
|
315
|
+
if (isCron) {
|
|
316
|
+
const cronTitle = extractCronTitle(context.messages);
|
|
317
|
+
if (cronTitle)
|
|
318
|
+
dynamicHeaders["x-cron-title"] = cronTitle;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
// Session mode: use pre-resolved session headers + fresh timestamp
|
|
323
|
+
const traceId = ctx.extraParams[HEADER_TRACE_ID];
|
|
324
|
+
const sessionId = ctx.extraParams[HEADER_SESSION_ID];
|
|
325
|
+
const interactionId = ctx.extraParams[HEADER_INTERACTION_ID];
|
|
326
|
+
const ts = `_${Date.now()}`;
|
|
327
|
+
if (typeof traceId === "string") {
|
|
328
|
+
const isCron = isCronTriggered(context.messages);
|
|
329
|
+
dynamicHeaders[HEADER_TRACE_ID] = isCron ? `cron_${traceId}${ts}` : `${traceId}${ts}`;
|
|
330
|
+
if (isCron) {
|
|
331
|
+
const cronTitle = extractCronTitle(context.messages);
|
|
332
|
+
if (cronTitle)
|
|
333
|
+
dynamicHeaders["x-cron-title"] = cronTitle;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if (typeof sessionId === "string")
|
|
337
|
+
dynamicHeaders[HEADER_SESSION_ID] = sessionId;
|
|
338
|
+
if (typeof interactionId === "string")
|
|
339
|
+
dynamicHeaders[HEADER_INTERACTION_ID] = interactionId;
|
|
295
340
|
}
|
|
296
|
-
if (typeof sessionId === "string")
|
|
297
|
-
dynamicHeaders[HEADER_SESSION_ID] = sessionId;
|
|
298
|
-
if (typeof interactionId === "string")
|
|
299
|
-
dynamicHeaders[HEADER_INTERACTION_ID] = interactionId;
|
|
300
341
|
}
|
|
301
342
|
// 记录输入
|
|
302
343
|
console.log(`[xiaoyiprovider] input messages count: ${context.messages?.length ?? 0}`);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const DEFAULT_CONFIG = {
|
|
2
|
+
enabled: true,
|
|
3
|
+
maxTools: 2,
|
|
4
|
+
includeUninstalledOnly: true,
|
|
5
|
+
envFilePath: "~/.openclaw/.xiaoyienv",
|
|
6
|
+
timeoutMs: 1000,
|
|
7
|
+
};
|
|
8
|
+
export function normalizeToolRetrieverConfig(raw) {
|
|
9
|
+
if (!raw || typeof raw !== "object") {
|
|
10
|
+
return { ...DEFAULT_CONFIG };
|
|
11
|
+
}
|
|
12
|
+
const cfg = raw;
|
|
13
|
+
return {
|
|
14
|
+
enabled: cfg.enabled ?? DEFAULT_CONFIG.enabled,
|
|
15
|
+
maxTools: Math.min(20, Math.max(1, cfg.maxTools ?? DEFAULT_CONFIG.maxTools)),
|
|
16
|
+
includeUninstalledOnly: cfg.includeUninstalledOnly ?? DEFAULT_CONFIG.includeUninstalledOnly,
|
|
17
|
+
envFilePath: cfg.envFilePath ?? DEFAULT_CONFIG.envFilePath,
|
|
18
|
+
serviceUrl: cfg.serviceUrl,
|
|
19
|
+
apiKey: cfg.apiKey,
|
|
20
|
+
uid: cfg.uid,
|
|
21
|
+
timeoutMs: cfg.timeoutMs ?? DEFAULT_CONFIG.timeoutMs,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
interface PluginHookBeforePromptBuildEvent {
|
|
2
|
+
prompt: string;
|
|
3
|
+
messages: unknown[];
|
|
4
|
+
}
|
|
5
|
+
interface PluginHookBeforePromptBuildResult {
|
|
6
|
+
systemPrompt?: string;
|
|
7
|
+
prependContext?: string;
|
|
8
|
+
prependSystemContext?: string;
|
|
9
|
+
appendSystemContext?: string;
|
|
10
|
+
}
|
|
11
|
+
interface PluginHookAgentContext {
|
|
12
|
+
agentId?: string;
|
|
13
|
+
sessionKey?: string;
|
|
14
|
+
sessionId?: string;
|
|
15
|
+
workspaceDir?: string;
|
|
16
|
+
messageProvider?: string;
|
|
17
|
+
trigger?: string;
|
|
18
|
+
channelId?: string;
|
|
19
|
+
}
|
|
20
|
+
import type { ToolRetrieverConfig } from "./types.js";
|
|
21
|
+
export declare function createBeforePromptBuildHandler(config: ToolRetrieverConfig): (event: PluginHookBeforePromptBuildEvent, ctx?: PluginHookAgentContext) => Promise<PluginHookBeforePromptBuildResult | undefined>;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { searchTools, formatToolsForContext, extractUserQuery } from "./tool-search.js";
|
|
2
|
+
const TOOL_RETRIEVER_HEADER = `## 用户查询相关skill列表如下:
|
|
3
|
+
|
|
4
|
+
`;
|
|
5
|
+
const TOOL_RETRIEVER_FOOTER = `
|
|
6
|
+
以上是检索到的、与当前查询相关但用户尚未安装的 skill,优先推荐强相关的 skill(通用兜底工具如 web-search、xiaoyi-gui-agent 仅在无强相关skill时调用)。
|
|
7
|
+
对于强相关skill,你需要主动提醒用户可以考虑安装,但禁止直接给用户安装。用户明确希望安装后,使用find-skill执行安装。
|
|
8
|
+
若用户已有的skill已能很好地完成当前任务,则无需提醒安装功能相似的skill。
|
|
9
|
+
---以下是用户原始请求---
|
|
10
|
+
`;
|
|
11
|
+
const PLUGIN_LOG_PREFIX = "[skill-retriever]";
|
|
12
|
+
const SKIP_KEYWORDS = ["安装", "装一下", "下载", "查询", "查找", "install", "卸载", "删除", "重载"];
|
|
13
|
+
const SKIP_PATTERNS = [
|
|
14
|
+
"/new",
|
|
15
|
+
"/reset",
|
|
16
|
+
"session was started",
|
|
17
|
+
"a new session was started",
|
|
18
|
+
];
|
|
19
|
+
function shouldSkipSearch(prompt) {
|
|
20
|
+
const trimmedPrompt = prompt.trim();
|
|
21
|
+
if (trimmedPrompt.startsWith("/")) {
|
|
22
|
+
return "query starts with / (built-in command)";
|
|
23
|
+
}
|
|
24
|
+
const lowerPrompt = trimmedPrompt.toLowerCase();
|
|
25
|
+
for (const keyword of SKIP_KEYWORDS) {
|
|
26
|
+
if (lowerPrompt.includes(keyword.toLowerCase())) {
|
|
27
|
+
return `query contains keyword: ${keyword}`;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
for (const pattern of SKIP_PATTERNS) {
|
|
31
|
+
if (lowerPrompt.includes(pattern.toLowerCase())) {
|
|
32
|
+
return `query matches pattern: ${pattern}`;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
export function createBeforePromptBuildHandler(config) {
|
|
38
|
+
return async (event, ctx) => {
|
|
39
|
+
const userPrompt = event.prompt;
|
|
40
|
+
if (ctx?.sessionKey?.includes(":subagent:")) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
if (!config.enabled) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
if (!userPrompt || userPrompt.trim().length === 0) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
const extractedQuery = extractUserQuery(userPrompt);
|
|
50
|
+
if (!extractedQuery || extractedQuery.length === 0) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
const skipReason = shouldSkipSearch(extractedQuery);
|
|
54
|
+
if (skipReason) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const searchResult = await searchTools({
|
|
59
|
+
query: extractedQuery,
|
|
60
|
+
maxTools: config.maxTools,
|
|
61
|
+
includeUninstalledOnly: config.includeUninstalledOnly,
|
|
62
|
+
envFilePath: config.envFilePath,
|
|
63
|
+
serviceUrl: config.serviceUrl,
|
|
64
|
+
apiKey: config.apiKey,
|
|
65
|
+
uid: config.uid,
|
|
66
|
+
timeoutMs: config.timeoutMs,
|
|
67
|
+
});
|
|
68
|
+
if (!searchResult || searchResult.tools.length === 0) {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
console.log(`${PLUGIN_LOG_PREFIX} [RESULT] Found ${searchResult.tools.length} skills, building context...`);
|
|
72
|
+
const toolsContext = formatToolsForContext(searchResult, config.includeUninstalledOnly);
|
|
73
|
+
if (!toolsContext) {
|
|
74
|
+
console.log(`${PLUGIN_LOG_PREFIX} [ERROR] Failed to format skills context`);
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
prependContext: TOOL_RETRIEVER_HEADER + toolsContext + TOOL_RETRIEVER_FOOTER,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
83
|
+
console.error(`${PLUGIN_LOG_PREFIX} [ERROR] ${errorMessage}, original query: "${extractedQuery}"`);
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { EnvConfig, ToolSearchResult } from "./types.js";
|
|
2
|
+
export declare function extractUserQuery(fullPrompt: string): string;
|
|
3
|
+
export declare function readEnvFile(filePath: string): EnvConfig;
|
|
4
|
+
export declare function getInstalledSkills(): string[];
|
|
5
|
+
export interface SearchToolsOptions {
|
|
6
|
+
query: string;
|
|
7
|
+
maxTools?: number;
|
|
8
|
+
includeUninstalledOnly?: boolean;
|
|
9
|
+
envFilePath?: string;
|
|
10
|
+
serviceUrl?: string;
|
|
11
|
+
apiKey?: string;
|
|
12
|
+
uid?: string;
|
|
13
|
+
timeoutMs?: number;
|
|
14
|
+
}
|
|
15
|
+
export declare function searchTools(options: SearchToolsOptions): Promise<ToolSearchResult | null>;
|
|
16
|
+
export declare function formatToolsForContext(result: ToolSearchResult, includeInstallUrl?: boolean): string;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
const SKILL_ID = "celia_find_skills";
|
|
5
|
+
const PLUGIN_LOG_PREFIX = "[skill-retriever]";
|
|
6
|
+
export function extractUserQuery(fullPrompt) {
|
|
7
|
+
const lastNewlineIndex = fullPrompt.lastIndexOf("\n");
|
|
8
|
+
if (lastNewlineIndex === -1) {
|
|
9
|
+
return fullPrompt.trim();
|
|
10
|
+
}
|
|
11
|
+
const afterLastNewline = fullPrompt.slice(lastNewlineIndex + 1).trim();
|
|
12
|
+
if (!afterLastNewline || afterLastNewline === "```") {
|
|
13
|
+
return "";
|
|
14
|
+
}
|
|
15
|
+
return afterLastNewline;
|
|
16
|
+
}
|
|
17
|
+
function expandPath(filePath) {
|
|
18
|
+
if (filePath.startsWith("~")) {
|
|
19
|
+
return path.join(os.homedir(), filePath.slice(1).replace(/^\/+/, ""));
|
|
20
|
+
}
|
|
21
|
+
return filePath;
|
|
22
|
+
}
|
|
23
|
+
export function readEnvFile(filePath) {
|
|
24
|
+
const expandedPath = expandPath(filePath);
|
|
25
|
+
const envDict = {};
|
|
26
|
+
try {
|
|
27
|
+
const content = fs.readFileSync(expandedPath, "utf-8");
|
|
28
|
+
for (const line of content.split("\n")) {
|
|
29
|
+
const trimmed = line.trim();
|
|
30
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const eqIndex = trimmed.indexOf("=");
|
|
34
|
+
if (eqIndex > 0) {
|
|
35
|
+
let key = trimmed.substring(0, eqIndex).trim();
|
|
36
|
+
const value = trimmed.substring(eqIndex + 1).trim();
|
|
37
|
+
key = key.replace(/-/g, "_");
|
|
38
|
+
envDict[key] = value;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// File not found or read error - return empty config
|
|
44
|
+
}
|
|
45
|
+
return envDict;
|
|
46
|
+
}
|
|
47
|
+
export function getInstalledSkills() {
|
|
48
|
+
const skillsDir = expandPath("~/.openclaw/workspace/skills");
|
|
49
|
+
const installedSkills = [];
|
|
50
|
+
try {
|
|
51
|
+
if (fs.existsSync(skillsDir) && fs.statSync(skillsDir).isDirectory()) {
|
|
52
|
+
const entries = fs.readdirSync(skillsDir);
|
|
53
|
+
for (const entry of entries) {
|
|
54
|
+
const entryPath = path.join(skillsDir, entry);
|
|
55
|
+
if (fs.statSync(entryPath).isDirectory()) {
|
|
56
|
+
installedSkills.push(entry);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Directory doesn't exist or read error - return empty list
|
|
63
|
+
}
|
|
64
|
+
return installedSkills;
|
|
65
|
+
}
|
|
66
|
+
function formatSkillData(rawSkills, installedSkills) {
|
|
67
|
+
const formattedSkills = [];
|
|
68
|
+
for (const skill of rawSkills) {
|
|
69
|
+
const isInstalled = installedSkills.includes(skill.skillId);
|
|
70
|
+
formattedSkills.push({
|
|
71
|
+
skillId: skill.skillId,
|
|
72
|
+
skillName: skill.skillName,
|
|
73
|
+
skillDesc: skill.skillDesc,
|
|
74
|
+
downloadPath: skill.packUrl,
|
|
75
|
+
status: isInstalled ? "已安装" : "未安装",
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return formattedSkills;
|
|
79
|
+
}
|
|
80
|
+
export async function searchTools(options) {
|
|
81
|
+
const { query, maxTools = 5, includeUninstalledOnly = true, envFilePath = "~/.openclaw/.xiaoyienv", serviceUrl: configServiceUrl, apiKey: configApiKey, uid: configUid, timeoutMs = 1000, } = options;
|
|
82
|
+
const envConfig = readEnvFile(envFilePath);
|
|
83
|
+
const hasRequiredConfig = !!envConfig.SERVICE_URL && !!envConfig.PERSONAL_API_KEY && !!envConfig.PERSONAL_UID;
|
|
84
|
+
const serviceUrl = configServiceUrl ?? envConfig.SERVICE_URL;
|
|
85
|
+
const apiKey = configApiKey ?? envConfig.PERSONAL_API_KEY;
|
|
86
|
+
const uid = configUid ?? envConfig.PERSONAL_UID;
|
|
87
|
+
if (!serviceUrl || !apiKey || !uid) {
|
|
88
|
+
console.warn(`${PLUGIN_LOG_PREFIX} Missing required configuration. serviceUrl: "${serviceUrl}", apiKey: "${apiKey ? '(set)' : '(missing)'} ", uid: "${uid ? '(set)' : '(missing)'}"`);
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
const traceId = crypto.randomUUID();
|
|
92
|
+
const apiUrl = `${serviceUrl}/celia-claw/v1/rest-api/skill/execute`;
|
|
93
|
+
const headers = {
|
|
94
|
+
"Content-Type": "application/json",
|
|
95
|
+
"x-skill-id": SKILL_ID,
|
|
96
|
+
"x-hag-trace-id": traceId,
|
|
97
|
+
"x-uid": uid,
|
|
98
|
+
"x-api-key": apiKey,
|
|
99
|
+
"x-request-from": "openclaw",
|
|
100
|
+
};
|
|
101
|
+
const payload = { query };
|
|
102
|
+
try {
|
|
103
|
+
const response = await fetch(apiUrl, {
|
|
104
|
+
method: "POST",
|
|
105
|
+
headers,
|
|
106
|
+
body: JSON.stringify(payload),
|
|
107
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
108
|
+
});
|
|
109
|
+
if (!response.ok) {
|
|
110
|
+
console.warn(`${PLUGIN_LOG_PREFIX} HTTP error: ${response.status} ${response.statusText}`);
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
console.log(`${PLUGIN_LOG_PREFIX} Received response, status: ${response.status}`);
|
|
114
|
+
const responseData = await response.json();
|
|
115
|
+
if (responseData.errorCode === "0" &&
|
|
116
|
+
responseData.content &&
|
|
117
|
+
responseData.content.skills) {
|
|
118
|
+
const rawSkills = responseData.content.skills;
|
|
119
|
+
const installedSkills = getInstalledSkills();
|
|
120
|
+
const formattedData = formatSkillData(rawSkills, installedSkills);
|
|
121
|
+
const topTools = formattedData.slice(0, 2);
|
|
122
|
+
const allInstalled = topTools.every((tool) => tool.status === "已安装");
|
|
123
|
+
if (allInstalled) {
|
|
124
|
+
console.log(`${PLUGIN_LOG_PREFIX} [DEBUG] All top 2 skills are installed, returning null`);
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
let filteredTools = topTools.filter((tool) => tool.status === "未安装");
|
|
128
|
+
console.log(`${PLUGIN_LOG_PREFIX} [DEBUG] After filtering uninstalled: ${filteredTools.length}, ids: ${filteredTools.map((t) => t.skillId).join(", ")}`);
|
|
129
|
+
return {
|
|
130
|
+
tools: filteredTools,
|
|
131
|
+
query,
|
|
132
|
+
timestamp: Date.now(),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
console.warn(`${PLUGIN_LOG_PREFIX} Invalid response format: ${JSON.stringify(responseData).slice(0, 200)}`);
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
const errorName = error instanceof Error ? error.name : "Unknown";
|
|
140
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
141
|
+
const errorCause = error instanceof Error && error.cause ? JSON.stringify(error.cause) : "N/A";
|
|
142
|
+
const errorStack = error instanceof Error ? error.stack?.split("\n").slice(0, 3).join(" | ") : "N/A";
|
|
143
|
+
console.warn(`${PLUGIN_LOG_PREFIX} [ERROR] Fetch failed - name: ${errorName}, message: ${errorMessage}, cause: ${errorCause}, stack: ${errorStack}`);
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
export function formatToolsForContext(result, includeInstallUrl = true) {
|
|
148
|
+
if (!result.tools || result.tools.length === 0) {
|
|
149
|
+
return "";
|
|
150
|
+
}
|
|
151
|
+
const toolDescriptions = [];
|
|
152
|
+
for (const tool of result.tools) {
|
|
153
|
+
let description = `### ${tool.skillName}\n`;
|
|
154
|
+
description += `name: ${tool.skillId}\n`;
|
|
155
|
+
description += `description: ${tool.skillDesc}\n`;
|
|
156
|
+
toolDescriptions.push(description);
|
|
157
|
+
}
|
|
158
|
+
return toolDescriptions.join("\n\n");
|
|
159
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export interface ToolRetrieverConfig {
|
|
2
|
+
enabled: boolean;
|
|
3
|
+
maxTools: number;
|
|
4
|
+
includeUninstalledOnly: boolean;
|
|
5
|
+
envFilePath: string;
|
|
6
|
+
serviceUrl?: string;
|
|
7
|
+
apiKey?: string;
|
|
8
|
+
uid?: string;
|
|
9
|
+
timeoutMs?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface RawSkill {
|
|
12
|
+
skillId: string;
|
|
13
|
+
skillName: string;
|
|
14
|
+
skillDesc: string;
|
|
15
|
+
packUrl: string;
|
|
16
|
+
}
|
|
17
|
+
export interface FormattedSkill {
|
|
18
|
+
skillId: string;
|
|
19
|
+
skillName: string;
|
|
20
|
+
skillDesc: string;
|
|
21
|
+
downloadPath: string;
|
|
22
|
+
status: "已安装" | "未安装";
|
|
23
|
+
}
|
|
24
|
+
export interface ToolSearchResult {
|
|
25
|
+
tools: FormattedSkill[];
|
|
26
|
+
query: string;
|
|
27
|
+
timestamp: number;
|
|
28
|
+
}
|
|
29
|
+
export interface EnvConfig {
|
|
30
|
+
PERSONAL_API_KEY?: string;
|
|
31
|
+
PERSONAL_UID?: string;
|
|
32
|
+
SERVICE_URL?: string;
|
|
33
|
+
[key: string]: string | undefined;
|
|
34
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// Login Token tool - 自动获取用户授权信息
|
|
2
|
+
import { v4 as uuidv4 } from "uuid";
|
|
3
|
+
import { getXYWebSocketManager } from "../client.js";
|
|
4
|
+
import { getCurrentSessionContext } from "./session-manager.js";
|
|
5
|
+
import { getCurrentTaskId, getCurrentMessageId } from "../task-manager.js";
|
|
6
|
+
import { readFileSync, existsSync } from "fs";
|
|
7
|
+
import { logger } from "../utils/logger.js";
|
|
8
|
+
const TOKEN_FILE_PATH = "/home/sandbox/.openclaw/.xiaoyitoken.json";
|
|
9
|
+
const POLL_INTERVAL_MS = 5000; // 5 seconds
|
|
10
|
+
const TIMEOUT_MS = 60000; // 1 minute
|
|
11
|
+
const TOKEN_VALIDITY_MS = 5 * 60 * 1000; // 5 minutes
|
|
12
|
+
/**
|
|
13
|
+
* get_login_token 工具
|
|
14
|
+
* 当 skill 依赖用户获取鉴权信息时,此工具协助用户快速获取鉴权信息。
|
|
15
|
+
*/
|
|
16
|
+
export const loginTokenTool = {
|
|
17
|
+
name: "get_login_token",
|
|
18
|
+
label: "Get Login Token",
|
|
19
|
+
description: "获取用户授权信息。当skill需要用户鉴权时调用此工具,工具会向用户端发送授权请求,等待用户完成授权后返回结果。请勿重复调用此工具。",
|
|
20
|
+
parameters: {
|
|
21
|
+
type: "object",
|
|
22
|
+
properties: {
|
|
23
|
+
clientId: {
|
|
24
|
+
type: "string",
|
|
25
|
+
description: "账号服务唯一标识,在执行具体skill过程中会提供",
|
|
26
|
+
},
|
|
27
|
+
skillName: {
|
|
28
|
+
type: "string",
|
|
29
|
+
description: "具体skill的名称",
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
required: ["clientId", "skillName"],
|
|
33
|
+
},
|
|
34
|
+
async execute(toolCallId, params) {
|
|
35
|
+
const { clientId, skillName } = params;
|
|
36
|
+
if (!clientId || typeof clientId !== "string" || clientId.trim() === "") {
|
|
37
|
+
throw new Error("Missing required parameter: clientId must be a non-empty string");
|
|
38
|
+
}
|
|
39
|
+
if (!skillName || typeof skillName !== "string" || skillName.trim() === "") {
|
|
40
|
+
throw new Error("Missing required parameter: skillName must be a non-empty string");
|
|
41
|
+
}
|
|
42
|
+
const sessionContext = getCurrentSessionContext();
|
|
43
|
+
if (!sessionContext) {
|
|
44
|
+
throw new Error("No active XY session found. Login token tool can only be used during an active conversation.");
|
|
45
|
+
}
|
|
46
|
+
const { config, sessionId, taskId, messageId } = sessionContext;
|
|
47
|
+
const currentTaskId = getCurrentTaskId(sessionId) ?? taskId;
|
|
48
|
+
const currentMessageId = getCurrentMessageId(sessionId) ?? messageId;
|
|
49
|
+
// (1) Build and send getLoginToken artifact
|
|
50
|
+
const artifactId = uuidv4();
|
|
51
|
+
const artifact = {
|
|
52
|
+
taskId: currentTaskId,
|
|
53
|
+
kind: "artifact-update",
|
|
54
|
+
append: false,
|
|
55
|
+
lastChunk: true,
|
|
56
|
+
final: false,
|
|
57
|
+
artifact: {
|
|
58
|
+
artifactId,
|
|
59
|
+
parts: [
|
|
60
|
+
{
|
|
61
|
+
kind: "getLoginToken",
|
|
62
|
+
clientId: clientId.trim(),
|
|
63
|
+
skillName: skillName.trim(),
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
const jsonRpcResponse = {
|
|
69
|
+
jsonrpc: "2.0",
|
|
70
|
+
id: currentMessageId,
|
|
71
|
+
result: artifact,
|
|
72
|
+
};
|
|
73
|
+
const wsManager = getXYWebSocketManager(config);
|
|
74
|
+
const outboundMessage = {
|
|
75
|
+
msgType: "agent_response",
|
|
76
|
+
agentId: config.agentId,
|
|
77
|
+
sessionId,
|
|
78
|
+
taskId: currentTaskId,
|
|
79
|
+
msgDetail: JSON.stringify(jsonRpcResponse),
|
|
80
|
+
};
|
|
81
|
+
logger.log(`[LOGIN_TOKEN] Sending getLoginToken artifact for clientId=${clientId}, skillName=${skillName}`);
|
|
82
|
+
await wsManager.sendMessage(sessionId, outboundMessage);
|
|
83
|
+
logger.log(`[LOGIN_TOKEN] Artifact sent successfully`);
|
|
84
|
+
// (2) Poll .xiaoyitoken.json every 5 seconds
|
|
85
|
+
const startTime = Date.now();
|
|
86
|
+
return new Promise((resolve) => {
|
|
87
|
+
const poll = () => {
|
|
88
|
+
const elapsed = Date.now() - startTime;
|
|
89
|
+
if (elapsed >= TIMEOUT_MS) {
|
|
90
|
+
// (4) Timeout after 1 minute
|
|
91
|
+
logger.log(`[LOGIN_TOKEN] Timeout: failed to get login token for clientId=${clientId}`);
|
|
92
|
+
resolve({
|
|
93
|
+
content: [
|
|
94
|
+
{
|
|
95
|
+
type: "text",
|
|
96
|
+
text: "获取用户授权失败",
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
});
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
if (existsSync(TOKEN_FILE_PATH)) {
|
|
104
|
+
const content = readFileSync(TOKEN_FILE_PATH, "utf-8");
|
|
105
|
+
const tokens = JSON.parse(content);
|
|
106
|
+
const match = tokens.find((t) => t.clientId === clientId.trim());
|
|
107
|
+
if (match) {
|
|
108
|
+
const tokenTime = Number(match.timestamp);
|
|
109
|
+
const diff = Date.now() - tokenTime;
|
|
110
|
+
if (diff <= TOKEN_VALIDITY_MS) {
|
|
111
|
+
// (3) Found valid token
|
|
112
|
+
logger.log(`[LOGIN_TOKEN] Successfully got login token for clientId=${clientId}`);
|
|
113
|
+
resolve({
|
|
114
|
+
content: [
|
|
115
|
+
{
|
|
116
|
+
type: "text",
|
|
117
|
+
text: "获取用户授权成功",
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
});
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
logger.log(`[LOGIN_TOKEN] Error reading token file: ${err}`);
|
|
128
|
+
}
|
|
129
|
+
// Not found or not valid, poll again after 5 seconds
|
|
130
|
+
setTimeout(poll, POLL_INTERVAL_MS);
|
|
131
|
+
};
|
|
132
|
+
// Start polling after 5 seconds
|
|
133
|
+
setTimeout(poll, POLL_INTERVAL_MS);
|
|
134
|
+
});
|
|
135
|
+
},
|
|
136
|
+
};
|
package/dist/src/websocket.js
CHANGED
|
@@ -400,6 +400,12 @@ export class XYWebSocketManager extends EventEmitter {
|
|
|
400
400
|
event: item,
|
|
401
401
|
});
|
|
402
402
|
}
|
|
403
|
+
else if (item.header?.namespace === "LoginTokenEvent" && item.header?.name === "ClawAutoLogin") {
|
|
404
|
+
console.log("[XY] LoginTokenEvent.ClawAutoLogin detected, emitting login-token-event");
|
|
405
|
+
this.emit("login-token-event", {
|
|
406
|
+
event: item,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
403
409
|
}
|
|
404
410
|
}
|
|
405
411
|
return;
|
|
@@ -454,6 +460,12 @@ export class XYWebSocketManager extends EventEmitter {
|
|
|
454
460
|
taskId: inboundMsg.taskId || a2aRequest.params?.id,
|
|
455
461
|
});
|
|
456
462
|
}
|
|
463
|
+
else if (item.header?.namespace === "LoginTokenEvent" && item.header?.name === "ClawAutoLogin") {
|
|
464
|
+
console.log("[XY] LoginTokenEvent.ClawAutoLogin detected (wrapped format), emitting login-token-event");
|
|
465
|
+
this.emit("login-token-event", {
|
|
466
|
+
event: item,
|
|
467
|
+
});
|
|
468
|
+
}
|
|
457
469
|
}
|
|
458
470
|
}
|
|
459
471
|
}
|