@ynhcj/xiaoyi-channel 0.0.65-beta → 0.0.65-next

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/dist/index.js +7 -4
  2. package/dist/src/bot.js +1 -0
  3. package/dist/src/channel.js +14 -19
  4. package/dist/src/cspl/call-api.js +14 -11
  5. package/dist/src/cspl/config.js +3 -3
  6. package/dist/src/cspl/constants.d.ts +2 -0
  7. package/dist/src/cspl/constants.js +12 -0
  8. package/dist/src/cspl/utils.js +4 -2
  9. package/dist/src/file-download.js +3 -6
  10. package/dist/src/file-upload.js +52 -5
  11. package/dist/src/login-token-handler.d.ts +8 -0
  12. package/dist/src/login-token-handler.js +60 -0
  13. package/dist/src/monitor.js +15 -0
  14. package/dist/src/outbound.js +2 -7
  15. package/dist/src/provider.d.ts +2 -0
  16. package/dist/src/provider.js +368 -0
  17. package/dist/src/reply-dispatcher.js +6 -0
  18. package/dist/src/self-evolution-handler.d.ts +1 -0
  19. package/dist/src/self-evolution-handler.js +47 -0
  20. package/dist/src/skill-retriever/config.d.ts +4 -0
  21. package/dist/src/skill-retriever/config.js +23 -0
  22. package/dist/src/skill-retriever/hooks.d.ts +3 -0
  23. package/dist/src/skill-retriever/hooks.js +97 -0
  24. package/dist/src/skill-retriever/tool-search.d.ts +16 -0
  25. package/dist/src/skill-retriever/tool-search.js +166 -0
  26. package/dist/src/skill-retriever/types.d.ts +34 -0
  27. package/dist/src/skill-retriever/types.js +1 -0
  28. package/dist/src/tools/call-device-tool.d.ts +5 -0
  29. package/dist/src/tools/call-device-tool.js +130 -0
  30. package/dist/src/tools/create-alarm-tool.js +5 -16
  31. package/dist/src/tools/delete-alarm-tool.js +1 -4
  32. package/dist/src/tools/device-tool-map.js +6 -0
  33. package/dist/src/tools/find-pc-devices-tool.d.ts +5 -0
  34. package/dist/src/tools/find-pc-devices-tool.js +98 -0
  35. package/dist/src/tools/get-alarm-tool-schema.d.ts +16 -0
  36. package/dist/src/tools/get-alarm-tool-schema.js +11 -0
  37. package/dist/src/tools/get-calendar-tool-schema.d.ts +16 -0
  38. package/dist/src/tools/get-calendar-tool-schema.js +9 -0
  39. package/dist/src/tools/get-collection-tool-schema.d.ts +16 -0
  40. package/dist/src/tools/get-collection-tool-schema.js +10 -0
  41. package/dist/src/tools/get-contact-tool-schema.d.ts +16 -0
  42. package/dist/src/tools/get-contact-tool-schema.js +11 -0
  43. package/dist/src/tools/get-device-file-tool-schema.d.ts +16 -0
  44. package/dist/src/tools/get-device-file-tool-schema.js +10 -0
  45. package/dist/src/tools/get-email-tool-schema.d.ts +16 -0
  46. package/dist/src/tools/get-email-tool-schema.js +9 -0
  47. package/dist/src/tools/get-note-tool-schema.d.ts +16 -0
  48. package/dist/src/tools/get-note-tool-schema.js +10 -0
  49. package/dist/src/tools/get-photo-tool-schema.d.ts +16 -0
  50. package/dist/src/tools/get-photo-tool-schema.js +10 -0
  51. package/dist/src/tools/image-reading-tool.js +4 -7
  52. package/dist/src/tools/login-token-tool.d.ts +5 -0
  53. package/dist/src/tools/login-token-tool.js +136 -0
  54. package/dist/src/tools/modify-alarm-tool.js +10 -23
  55. package/dist/src/tools/query-app-message-tool.d.ts +4 -0
  56. package/dist/src/tools/query-app-message-tool.js +138 -0
  57. package/dist/src/tools/query-memory-data-tool.d.ts +4 -0
  58. package/dist/src/tools/query-memory-data-tool.js +154 -0
  59. package/dist/src/tools/query-todo-task-tool.d.ts +4 -0
  60. package/dist/src/tools/query-todo-task-tool.js +133 -0
  61. package/dist/src/tools/save-file-to-phone-tool.d.ts +5 -0
  62. package/dist/src/tools/save-file-to-phone-tool.js +166 -0
  63. package/dist/src/tools/save-media-to-gallery-tool.d.ts +5 -0
  64. package/dist/src/tools/save-media-to-gallery-tool.js +174 -0
  65. package/dist/src/tools/schema-tool-factory.d.ts +27 -0
  66. package/dist/src/tools/schema-tool-factory.js +32 -0
  67. package/dist/src/tools/search-alarm-tool.js +6 -13
  68. package/dist/src/tools/search-calendar-tool.js +2 -0
  69. package/dist/src/tools/search-email-tool.d.ts +5 -0
  70. package/dist/src/tools/search-email-tool.js +137 -0
  71. package/dist/src/tools/search-file-tool.js +4 -4
  72. package/dist/src/tools/search-message-tool.js +1 -0
  73. package/dist/src/tools/search-photo-gallery-tool.js +2 -2
  74. package/dist/src/tools/send-email-tool.d.ts +4 -0
  75. package/dist/src/tools/send-email-tool.js +134 -0
  76. package/dist/src/tools/send-file-to-user-tool.js +2 -4
  77. package/dist/src/tools/upload-file-tool.js +4 -4
  78. package/dist/src/tools/upload-photo-tool.js +2 -2
  79. package/dist/src/tools/xiaoyi-add-collection-tool.d.ts +4 -0
  80. package/dist/src/tools/xiaoyi-add-collection-tool.js +192 -0
  81. package/dist/src/tools/xiaoyi-collection-tool.js +43 -7
  82. package/dist/src/tools/xiaoyi-delete-collection-tool.d.ts +4 -0
  83. package/dist/src/tools/xiaoyi-delete-collection-tool.js +163 -0
  84. package/dist/src/websocket.js +19 -0
  85. package/openclaw.plugin.json +1 -0
  86. package/package.json +1 -1
@@ -0,0 +1,368 @@
1
+ // Xiaoyi Provider
2
+ // Wraps any OpenAI-compatible endpoint and injects dynamic headers
3
+ // (taskId, sessionId, conversationId) from the current XY channel session.
4
+ // Falls back to uid-based values when no session context is available.
5
+ //
6
+ // Users configure the underlying model in config:
7
+ // models.providers.xiaoyiprovider.baseUrl = "https://..."
8
+ // models.providers.xiaoyiprovider.api = "openai-completions"
9
+ // models.providers.xiaoyiprovider.models = [...]
10
+ import { createHash } from "crypto";
11
+ import { getCurrentSessionContext } from "./tools/session-manager.js";
12
+ // ── Retry config ──────────────────────────────────────────────
13
+ const RETRY_DELAYS_MS = [10_000, 20_000, 40_000, 60_000];
14
+ const MAX_RETRY_ATTEMPTS = 8;
15
+ /** Check if an errorMessage indicates a retryable provider error by type. */
16
+ function isRetryableProviderError(message) {
17
+ if (!message)
18
+ return false;
19
+ const lower = message.toLowerCase();
20
+ if (lower.includes("the server had an error while processing your request"))
21
+ return true;
22
+ if (lower.includes("rate limit reached for requests"))
23
+ return true;
24
+ return false;
25
+ }
26
+ /** Check if the request is triggered by a cron job by inspecting the first user message. */
27
+ function isCronTriggered(messages) {
28
+ if (!messages)
29
+ return false;
30
+ const firstUser = messages.find(m => m.role === "user");
31
+ if (!firstUser)
32
+ return false;
33
+ let text = "";
34
+ if (typeof firstUser.content === "string") {
35
+ text = firstUser.content;
36
+ }
37
+ else if (Array.isArray(firstUser.content)) {
38
+ const block = firstUser.content.find(b => b.type === "text" && typeof b.text === "string");
39
+ if (block)
40
+ text = block.text;
41
+ }
42
+ return /^\[cron:/i.test(text.trim());
43
+ }
44
+ /** Compute retry delay in ms for the given 1-based attempt, with up to 10s jitter. */
45
+ function getRetryDelayMs(attempt, isCron = false) {
46
+ if (isCron) {
47
+ return 60_000 + Math.floor(Math.random() * 10_000);
48
+ }
49
+ const base = attempt <= RETRY_DELAYS_MS.length
50
+ ? RETRY_DELAYS_MS[attempt - 1]
51
+ : RETRY_DELAYS_MS[RETRY_DELAYS_MS.length - 1];
52
+ const jitter = Math.floor(Math.random() * 10_000);
53
+ return base + jitter;
54
+ }
55
+ function sleep(ms) {
56
+ return new Promise((resolve) => setTimeout(resolve, ms));
57
+ }
58
+ /**
59
+ * Build a minimal EventStream-compatible object that replays a single
60
+ * done/error event. This avoids importing @mariozechner/pi-ai at runtime
61
+ * (the package is not available in the extension sandbox).
62
+ */
63
+ function buildReplayStream(result) {
64
+ let settled = false;
65
+ const queued = [
66
+ result.stopReason === "error"
67
+ ? { type: "error", reason: "error", error: result }
68
+ : { type: "done", reason: result.stopReason, message: result },
69
+ ];
70
+ return {
71
+ result: () => Promise.resolve(result),
72
+ push: () => { },
73
+ end: () => { },
74
+ [Symbol.asyncIterator]: () => {
75
+ return {
76
+ next: async () => {
77
+ if (settled || queued.length === 0) {
78
+ settled = true;
79
+ return { value: undefined, done: true };
80
+ }
81
+ settled = true;
82
+ return { value: queued.shift(), done: false };
83
+ },
84
+ };
85
+ },
86
+ };
87
+ }
88
+ /**
89
+ * Wrap the underlying stream with retry logic while preserving real-time streaming.
90
+ *
91
+ * Strategy:
92
+ * 1. Buffer events until the first content-bearing event is seen.
93
+ * 2. If the stream errors before any content, the buffer is tiny (start + error)
94
+ * and we can safely retry with a fresh API call.
95
+ * 3. Once content events appear, flush the buffer and switch to pass-through mode
96
+ * — the consumer sees every text_delta in real time.
97
+ */
98
+ function createRetryingStream(createStream, cronJob) {
99
+ let resultResolve;
100
+ const resultPromise = new Promise(resolve => { resultResolve = resolve; });
101
+ const CONTENT_EVENT_TYPES = new Set([
102
+ "text_start", "text_delta", "text_end",
103
+ "thinking_start", "thinking_delta", "thinking_end",
104
+ "toolcall_start", "toolcall_delta", "toolcall_end",
105
+ ]);
106
+ async function* retryGenerator() {
107
+ for (let attempt = 0; attempt < MAX_RETRY_ATTEMPTS; attempt++) {
108
+ const stream = await createStream();
109
+ let hasContent = false;
110
+ const buffer = [];
111
+ let errorResult = null;
112
+ for await (const event of stream) {
113
+ const isContent = CONTENT_EVENT_TYPES.has(event.type);
114
+ if (!hasContent && !isContent) {
115
+ // ── Buffer phase (no content yet) ──
116
+ if (event.type === "done") {
117
+ console.log(`[xiaoyiprovider] stream completed (no content), usage: input=${event.message?.usage?.input} output=${event.message?.usage?.output}`);
118
+ for (const b of buffer)
119
+ yield b;
120
+ resultResolve(event.message);
121
+ yield event;
122
+ return;
123
+ }
124
+ if (event.type === "error") {
125
+ errorResult = event.error;
126
+ }
127
+ buffer.push(event);
128
+ }
129
+ else {
130
+ // ── Streaming phase ──
131
+ if (!hasContent) {
132
+ console.log("[xiaoyiprovider] first content event received, switching to streaming mode");
133
+ hasContent = true;
134
+ for (const b of buffer)
135
+ yield b;
136
+ }
137
+ // IMPORTANT: resolve result() BEFORE yielding terminal events to avoid deadlock.
138
+ // The SDK calls result() when it sees done/error — if we yield first, the generator
139
+ // suspends and can never reach resolve, causing a permanent deadlock.
140
+ if (event.type === "done") {
141
+ console.log(`[xiaoyiprovider] stream completed, usage: input=${event.message?.usage?.input} output=${event.message?.usage?.output}`);
142
+ resultResolve(event.message);
143
+ yield event;
144
+ return;
145
+ }
146
+ if (event.type === "error") {
147
+ console.log(`[xiaoyiprovider] stream error after content: ${event.error?.errorMessage}`);
148
+ resultResolve(event.error);
149
+ yield event;
150
+ return;
151
+ }
152
+ yield event;
153
+ }
154
+ }
155
+ // Stream ended during buffer phase — decide whether to retry
156
+ if (errorResult?.stopReason === "error" && isRetryableProviderError(errorResult.errorMessage)) {
157
+ if (attempt < MAX_RETRY_ATTEMPTS - 1) {
158
+ const delayMs = getRetryDelayMs(attempt + 1, cronJob);
159
+ console.log(`[xiaoyiprovider] retryable error (attempt ${attempt + 1}/${MAX_RETRY_ATTEMPTS}): ` +
160
+ `${errorResult.errorMessage} — retrying in ${delayMs}ms`);
161
+ await sleep(delayMs);
162
+ continue; // discard buffer, retry with a new stream
163
+ }
164
+ console.log(`[xiaoyiprovider] all ${MAX_RETRY_ATTEMPTS} retries exhausted, surfacing last error`);
165
+ }
166
+ else if (errorResult) {
167
+ console.log(`[xiaoyiprovider] non-retryable error: ${errorResult.errorMessage}`);
168
+ }
169
+ // Non-retryable or retries exhausted — yield buffered events.
170
+ // Resolve before yielding the terminal event to avoid the same deadlock.
171
+ for (const b of buffer) {
172
+ if (b.type === "done") {
173
+ resultResolve(b.message);
174
+ }
175
+ else if (b.type === "error") {
176
+ resultResolve(b.error);
177
+ }
178
+ yield b;
179
+ }
180
+ if (errorResult && buffer.every(b => b.type !== "done" && b.type !== "error")) {
181
+ resultResolve(errorResult);
182
+ }
183
+ return;
184
+ }
185
+ // Safety: final fallback attempt
186
+ console.log("[xiaoyiprovider] entering final fallback attempt");
187
+ const lastStream = await createStream();
188
+ for await (const event of lastStream) {
189
+ if (event.type === "done") {
190
+ resultResolve(event.message);
191
+ yield event;
192
+ return;
193
+ }
194
+ if (event.type === "error") {
195
+ resultResolve(event.error);
196
+ yield event;
197
+ return;
198
+ }
199
+ yield event;
200
+ }
201
+ }
202
+ const gen = retryGenerator();
203
+ return {
204
+ result: () => resultPromise,
205
+ push: () => { },
206
+ end: () => { },
207
+ [Symbol.asyncIterator]: () => gen,
208
+ };
209
+ }
210
+ /**
211
+ * Dynamic header keys injected via extraParams and forwarded to the HTTP request.
212
+ * Correspond to the three fields written to .xiaoyiruntime:
213
+ * TASK_ID, SESSION_ID, CONVERSATION_ID
214
+ */
215
+ const HEADER_TRACE_ID = "x-hag-trace-id";
216
+ const HEADER_SESSION_ID = "x-session-id";
217
+ const HEADER_INTERACTION_ID = "x-interaction-id";
218
+ /** Internal key for passing fallback uid prefix from prepareExtraParams to wrapStreamFn. */
219
+ const FALLBACK_PREFIX_KEY = "_xiaoyi_fallback_prefix";
220
+ /**
221
+ * Encode uid via SHA-256 and take first 32 hex chars.
222
+ */
223
+ function encodeUid(uid) {
224
+ return createHash("sha256").update(uid).digest("hex").slice(0, 32);
225
+ }
226
+ /**
227
+ * Get uid from channel config (OpenClawConfig -> channels -> xiaoyi-channel -> uid).
228
+ */
229
+ function getUidFromConfig(config) {
230
+ return config?.channels?.["xiaoyi-channel"]?.uid;
231
+ }
232
+ export const xiaoyiProvider = {
233
+ id: "xiaoyiprovider",
234
+ label: "Xiaoyi Provider",
235
+ docsPath: "/providers/models",
236
+ auth: [],
237
+ isCacheTtlEligible: () => true,
238
+ /**
239
+ * Inject dynamic session params into extraParams so they flow
240
+ * through to wrapStreamFn's ctx.extraParams.
241
+ *
242
+ * Priority:
243
+ * 1. Session context (from AsyncLocalStorage, set by bot.ts)
244
+ * 2. uid-based fallback: sha256(uid).hex[:32]_timestamp
245
+ * 3. No uid available → return undefined (no headers injected)
246
+ */
247
+ prepareExtraParams: (ctx) => {
248
+ const sessionCtx = getCurrentSessionContext();
249
+ if (sessionCtx) {
250
+ const taskId = sessionCtx.taskId;
251
+ const sessionId = taskId.split("&")[0];
252
+ const interactionId = taskId.split("&")[1] || "";
253
+ return {
254
+ ...ctx.extraParams,
255
+ [HEADER_TRACE_ID]: taskId,
256
+ [HEADER_SESSION_ID]: sessionId,
257
+ [HEADER_INTERACTION_ID]: interactionId,
258
+ };
259
+ }
260
+ // Fallback: store uid prefix for lazy timestamp generation in wrapStreamFn.
261
+ // This ensures each model call gets a fresh timestamp instead of reusing
262
+ // the same one across tool-use loops and retries.
263
+ const uid = getUidFromConfig(ctx.config);
264
+ if (!uid)
265
+ return undefined;
266
+ return {
267
+ ...ctx.extraParams,
268
+ [FALLBACK_PREFIX_KEY]: encodeUid(uid),
269
+ };
270
+ },
271
+ /**
272
+ * Wrap the stream function to inject dynamic headers into every
273
+ * HTTP request to the model provider, and retry on retryable errors
274
+ * (server_error / rate_limit_error) with backoff: 10s, 20s, 40s, 60s (cap).
275
+ *
276
+ * The retry loop awaits stream.result() to detect errors before deciding
277
+ * whether to retry. This keeps the agent loop waiting (no timeout risk
278
+ * since the default agent timeout is 48 hours).
279
+ */
280
+ wrapStreamFn: (ctx) => {
281
+ const underlying = ctx.streamFn;
282
+ if (!underlying)
283
+ return underlying;
284
+ return async (model, context, options) => {
285
+ // 每次请求时从 ctx.extraParams 动态读取 header
286
+ const dynamicHeaders = {};
287
+ if (ctx.extraParams) {
288
+ const fallbackPrefix = ctx.extraParams[FALLBACK_PREFIX_KEY];
289
+ if (typeof fallbackPrefix === "string") {
290
+ // Fallback mode: generate fresh timestamp per request
291
+ const isCron = isCronTriggered(context.messages);
292
+ const fallbackValue = `${fallbackPrefix}_${Date.now()}`;
293
+ dynamicHeaders[HEADER_TRACE_ID] = isCron ? `cron_${fallbackValue}` : fallbackValue;
294
+ dynamicHeaders[HEADER_SESSION_ID] = fallbackValue;
295
+ dynamicHeaders[HEADER_INTERACTION_ID] = fallbackValue;
296
+ }
297
+ else {
298
+ // Session mode: use pre-resolved session headers
299
+ const traceId = ctx.extraParams[HEADER_TRACE_ID];
300
+ const sessionId = ctx.extraParams[HEADER_SESSION_ID];
301
+ const interactionId = ctx.extraParams[HEADER_INTERACTION_ID];
302
+ if (typeof traceId === "string") {
303
+ const isCron = isCronTriggered(context.messages);
304
+ dynamicHeaders[HEADER_TRACE_ID] = isCron ? `cron_${traceId}` : traceId;
305
+ }
306
+ if (typeof sessionId === "string")
307
+ dynamicHeaders[HEADER_SESSION_ID] = sessionId;
308
+ if (typeof interactionId === "string")
309
+ dynamicHeaders[HEADER_INTERACTION_ID] = interactionId;
310
+ }
311
+ }
312
+ // 记录输入
313
+ console.log(`[xiaoyiprovider] input messages count: ${context.messages?.length ?? 0}`);
314
+ if (context.systemPrompt) {
315
+ console.log(`[xiaoyiprovider] system prompt length: ${context.systemPrompt.length}`);
316
+ }
317
+ // 在发送给模型前,优化 systemPrompt 结构
318
+ if (context.systemPrompt) {
319
+ let sp = context.systemPrompt;
320
+ const beforeLen = sp.length;
321
+ // 删除 ## Tooling 与 TOOLS.md 声明之间的内容
322
+ sp = sp.replace(/(## Tooling)[\s\S]*?(TOOLS\.md does not control tool availability; it is user guidance for how to use external tools\.)/, "$1\n\n$2");
323
+ // (1) 提取 ## Skills (mandatory) 到 </available_skills> 作为第一部分
324
+ const skillsMatch = sp.match(/(## Skills \(mandatory\)[\s\S]*?<\/available_skills>)/);
325
+ const part1 = skillsMatch ? skillsMatch[0] : '';
326
+ // (2) 提取 ## /home/sandbox/.openclaw/workspace/SOUL.md 到 ## /home/sandbox/.openclaw/workspace/TOOLS.md 之前的内容作为第二部分
327
+ const soulMatch = sp.match(/(## \/home\/sandbox\/\.openclaw\/workspace\/SOUL\.md[\s\S]*?)(?=## \/home\/sandbox\/\.openclaw\/workspace\/TOOLS\.md)/);
328
+ const part2 = soulMatch ? soulMatch[1].trim() : '';
329
+ if (part1 || part2) {
330
+ // 从原始位置删除已提取的部分
331
+ if (skillsMatch)
332
+ sp = sp.replace(skillsMatch[0], '');
333
+ if (soulMatch)
334
+ sp = sp.replace(soulMatch[1], '');
335
+ // 清理多余空行
336
+ sp = sp.replace(/\n{3,}/g, '\n\n');
337
+ // (3) 将 第二部分 + 第一部分 插入到 ## Runtime 上面
338
+ const combined = (part2 + '\n\n' + part1).trim();
339
+ if (combined && sp.includes('## Runtime')) {
340
+ sp = sp.replace('## Runtime', combined + '\n\n## Runtime');
341
+ }
342
+ }
343
+ console.log(`[xiaoyiprovider] system prompt optimized: ${beforeLen} -> ${sp.length}`);
344
+ context.systemPrompt = sp;
345
+ }
346
+ // Append device context to systemPrompt
347
+ const sessionCtx = getCurrentSessionContext();
348
+ if (sessionCtx?.deviceType) {
349
+ const rawDevice = sessionCtx.deviceType;
350
+ const displayDevice = (rawDevice === "2in1") ? "鸿蒙PC" : rawDevice;
351
+ const deviceSection = `\n\n## Current User Device Context\nThe current user is using the following device: ${displayDevice}\nYou need to be aware of the user's current device and provide guidance accordingly. If the response involves device-related tools or actions, you must tailor the reply based on the user's current device, using device-specific references such as "saved to the Notes/Calendar on your {deviceType}.\n"`;
352
+ context.systemPrompt = (context.systemPrompt ?? "") + deviceSection;
353
+ }
354
+ // ── Retry-capable streaming ──────────────────────────────
355
+ const cronJob = isCronTriggered(context.messages);
356
+ if (cronJob)
357
+ console.log("[xiaoyiprovider] detected cron-triggered request, using extended retry delays");
358
+ const makeStream = () => underlying(model, context, {
359
+ ...options,
360
+ headers: {
361
+ ...options?.headers,
362
+ ...dynamicHeaders,
363
+ },
364
+ });
365
+ return createRetryingStream(makeStream, cronJob);
366
+ };
367
+ },
368
+ };
@@ -267,6 +267,12 @@ export function createXYReplyDispatcher(params) {
267
267
  log(`[TOOL START] Tool: ${name}, phase: ${phase}, taskId: ${currentTaskId}`);
268
268
  if (phase === "start") {
269
269
  const toolName = name || "unknown";
270
+ // call_device_tool 由自身 execute() 内部发送具体子工具名的状态更新
271
+ // get_xxx_tool_schema 是给 LLM 查 schema 用的,无需向用户展示
272
+ if (toolName === "call_device_tool" || toolName.endsWith("_tool_schema")) {
273
+ log(`[TOOL START] Skipping generic status for ${toolName}`);
274
+ return;
275
+ }
270
276
  try {
271
277
  await sendStatusUpdate({
272
278
  config,
@@ -0,0 +1 @@
1
+ export declare function handleSelfEvolutionEvent(context: any, runtime: any): void;
@@ -0,0 +1,47 @@
1
+ import { readFileSync, writeFileSync } from "fs";
2
+ const XIAOYIENV_PATH = "/home/sandbox/.openclaw/.xiaoyienv";
3
+ export function handleSelfEvolutionEvent(context, runtime) {
4
+ const log = runtime?.log ?? console.log;
5
+ const error = runtime?.error ?? console.error;
6
+ try {
7
+ const state = context.event?.payload?.selfEvolutionState;
8
+ if (typeof state !== "string") {
9
+ error("[SELF_EVOLUTION] invalid payload: missing selfEvolutionState");
10
+ return;
11
+ }
12
+ log(`[SELF_EVOLUTION] received state: ${state}`);
13
+ let content;
14
+ try {
15
+ content = readFileSync(XIAOYIENV_PATH, "utf-8");
16
+ }
17
+ catch {
18
+ // File doesn't exist yet — create it
19
+ log(`[SELF_EVOLUTION] ${XIAOYIENV_PATH} not found, creating new file`);
20
+ writeFileSync(XIAOYIENV_PATH, `selfEvolutionState=${state}\n`, "utf-8");
21
+ log(`[SELF_EVOLUTION] wrote selfEvolutionState=${state}`);
22
+ return;
23
+ }
24
+ const lines = content.split("\n");
25
+ const key = "selfEvolutionState";
26
+ let found = false;
27
+ const updated = lines.map((line) => {
28
+ if (line.startsWith(`${key}=`)) {
29
+ found = true;
30
+ return `${key}=${state}`;
31
+ }
32
+ return line;
33
+ });
34
+ if (!found) {
35
+ // Ensure trailing newline before appending
36
+ const trimmed = content.trimEnd();
37
+ writeFileSync(XIAOYIENV_PATH, `${trimmed}\n${key}=${state}\n`, "utf-8");
38
+ }
39
+ else {
40
+ writeFileSync(XIAOYIENV_PATH, updated.join("\n"), "utf-8");
41
+ }
42
+ log(`[SELF_EVOLUTION] updated selfEvolutionState=${state} in ${XIAOYIENV_PATH}`);
43
+ }
44
+ catch (err) {
45
+ error("[SELF_EVOLUTION] failed to handle event:", err);
46
+ }
47
+ }
@@ -0,0 +1,4 @@
1
+ import type { ToolRetrieverConfig } from "./types.js";
2
+ export interface NormalizedConfig extends ToolRetrieverConfig {
3
+ }
4
+ export declare function normalizeToolRetrieverConfig(raw?: unknown): NormalizedConfig;
@@ -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,3 @@
1
+ import type { PluginHookBeforePromptBuildEvent, PluginHookBeforePromptBuildResult, PluginHookAgentContext } from "openclaw/plugins/hooks.js";
2
+ import type { ToolRetrieverConfig } from "./types.js";
3
+ export declare function createBeforePromptBuildHandler(config: ToolRetrieverConfig): (event: PluginHookBeforePromptBuildEvent, ctx?: PluginHookAgentContext) => Promise<PluginHookBeforePromptBuildResult | undefined>;
@@ -0,0 +1,97 @@
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
+ console.log(`${PLUGIN_LOG_PREFIX} [SKIP] Sub-agent detected, skipping search`);
42
+ return undefined;
43
+ }
44
+ if (!config.enabled) {
45
+ console.log(`${PLUGIN_LOG_PREFIX} [SKIP] Plugin disabled, original query: "${userPrompt}"`);
46
+ return undefined;
47
+ }
48
+ if (!userPrompt || userPrompt.trim().length === 0) {
49
+ console.log(`${PLUGIN_LOG_PREFIX} [SKIP] Empty query`);
50
+ return undefined;
51
+ }
52
+ console.log(`${PLUGIN_LOG_PREFIX} [RECEIVED] Original user query (len=${userPrompt.length}): "${userPrompt}"`);
53
+ const extractedQuery = extractUserQuery(userPrompt);
54
+ console.log(`${PLUGIN_LOG_PREFIX} [EXTRACTED] Extracted user query: "${extractedQuery}"`);
55
+ if (!extractedQuery || extractedQuery.length === 0) {
56
+ console.log(`${PLUGIN_LOG_PREFIX} [SKIP] No valid user query after extraction, skipping search`);
57
+ return undefined;
58
+ }
59
+ const skipReason = shouldSkipSearch(extractedQuery);
60
+ if (skipReason) {
61
+ console.log(`${PLUGIN_LOG_PREFIX} [SKIP] ${skipReason}, extracted query: "${extractedQuery}"`);
62
+ return undefined;
63
+ }
64
+ console.log(`${PLUGIN_LOG_PREFIX} [PROCEED] Calling skill search API (timeout=${config.timeoutMs}ms) for query: "${extractedQuery}"`);
65
+ try {
66
+ const searchResult = await searchTools({
67
+ query: extractedQuery,
68
+ maxTools: config.maxTools,
69
+ includeUninstalledOnly: config.includeUninstalledOnly,
70
+ envFilePath: config.envFilePath,
71
+ serviceUrl: config.serviceUrl,
72
+ apiKey: config.apiKey,
73
+ uid: config.uid,
74
+ timeoutMs: config.timeoutMs,
75
+ });
76
+ if (!searchResult || searchResult.tools.length === 0) {
77
+ console.log(`${PLUGIN_LOG_PREFIX} [RESULT] No skills found for query: "${extractedQuery}"`);
78
+ return undefined;
79
+ }
80
+ console.log(`${PLUGIN_LOG_PREFIX} [RESULT] Found ${searchResult.tools.length} skills, building context...`);
81
+ const toolsContext = formatToolsForContext(searchResult, config.includeUninstalledOnly);
82
+ if (!toolsContext) {
83
+ console.log(`${PLUGIN_LOG_PREFIX} [ERROR] Failed to format skills context for query: "${extractedQuery}"`);
84
+ return undefined;
85
+ }
86
+ console.log(`${PLUGIN_LOG_PREFIX} [SUCCESS] Built context with ${searchResult.tools.length} skills for query: "${extractedQuery}"`);
87
+ return {
88
+ prependContext: TOOL_RETRIEVER_HEADER + toolsContext + TOOL_RETRIEVER_FOOTER,
89
+ };
90
+ }
91
+ catch (error) {
92
+ const errorMessage = error instanceof Error ? error.message : String(error);
93
+ console.error(`${PLUGIN_LOG_PREFIX} [ERROR] ${errorMessage}, original query: "${extractedQuery}"`);
94
+ return undefined;
95
+ }
96
+ };
97
+ }
@@ -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;