@ynhcj/xiaoyi-channel 0.0.67-next → 0.0.68-next

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -5,7 +5,24 @@ import { setXYRuntime } from "./src/runtime.js";
5
5
  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
+ import { selfEvolutionManager } from "./src/utils/self-evolution-manager.js";
9
+ import { TOOL_CALL_NUDGE_THRESHOLD, toolCallNudgeManager } from "./src/utils/tool-call-nudge-manager.js";
8
10
  import { ALLOWED_TOOLS, MIN_TEXT_LENGTH, MAX_TOTAL_LENGTH, MAX_TEXT_LENGTH, STEER_ABORT_MESSAGE, } from "./src/cspl/constants.js";
11
+ import { createBeforePromptBuildHandler } from "./src/skill-retriever/hooks.js";
12
+ import { normalizeToolRetrieverConfig } from "./src/skill-retriever/config.js";
13
+ const SELF_EVOLUTION_NUDGE_MESSAGE = `[系统消息,非用户发送] 当前任务已经调用了较多工具。如果你识别到存在稳定、可复用的踩坑经验或排错模式,可以在完成当前主要任务后,正式回复用户前,阅读evolution skill开启自进化。注意,不要打断当前主任务,已完成任务为最高优先级。`;
14
+ function shouldCountToolCall(toolName) {
15
+ if (toolName === "save_self_evolution_skill") {
16
+ return false;
17
+ }
18
+ if (toolName === "call_device_tool") {
19
+ return false;
20
+ }
21
+ if (toolName.endsWith("_tool_schema")) {
22
+ return false;
23
+ }
24
+ return true;
25
+ }
9
26
  /**
10
27
  * Xiaoyi Channel Plugin Entry Point.
11
28
  * Exports the plugin for OpenClaw to load.
@@ -23,6 +40,21 @@ const plugin = {
23
40
  // SENTINEL HOOK after_tool_call hook: 监听工具结果,发送至安全检测 API 进行安全检测
24
41
  // 如果响应为 REJECT,注入 steer 消息中止当前对话
25
42
  api.on("after_tool_call", async (event, ctx) => {
43
+ if (ctx.sessionKey &&
44
+ await selfEvolutionManager.isEnabled() &&
45
+ shouldCountToolCall(event.toolName)) {
46
+ try {
47
+ const { count, shouldNudge } = toolCallNudgeManager.recordToolCall(ctx.sessionKey);
48
+ api.logger.debug?.(`[SELF_EVOLUTION] Tool call counted: tool=${event.toolName}, count=${count}, threshold=${TOOL_CALL_NUDGE_THRESHOLD}, sessionKey=${ctx.sessionKey}`);
49
+ if (shouldNudge) {
50
+ api.logger.info?.(`[SELF_EVOLUTION] Tool call threshold reached, injecting nudge: count=${count}, sessionKey=${ctx.sessionKey}`);
51
+ await tryInjectSteer(ctx.sessionKey, SELF_EVOLUTION_NUDGE_MESSAGE);
52
+ }
53
+ }
54
+ catch (err) {
55
+ api.logger.error(`[SELF_EVOLUTION] after_tool_call nudge error: ${err}`);
56
+ }
57
+ }
26
58
  if (!ALLOWED_TOOLS.includes(event.toolName)) {
27
59
  return;
28
60
  }
@@ -59,6 +91,17 @@ const plugin = {
59
91
  api.logger.error(`[SENTINEL HOOK] after_tool_call error: ${err}`);
60
92
  }
61
93
  });
94
+ // SKILL RETRIEVER HOOK: before_prompt_build hook
95
+ const pluginConfig = api.pluginConfig || {};
96
+ const skillRetrieverConfig = normalizeToolRetrieverConfig({
97
+ enabled: pluginConfig.skillRetrieverEnabled ?? true,
98
+ maxTools: pluginConfig.skillRetrieverMaxTools ?? 2,
99
+ includeUninstalledOnly: true,
100
+ envFilePath: "~/.openclaw/.xiaoyienv",
101
+ timeoutMs: pluginConfig.skillRetrieverTimeoutMs ?? 1000,
102
+ });
103
+ const beforePromptBuildHandler = createBeforePromptBuildHandler(skillRetrieverConfig);
104
+ api.on("before_prompt_build", beforePromptBuildHandler);
62
105
  },
63
106
  };
64
107
  export default plugin;
@@ -7,6 +7,7 @@ import { sendFileToUserTool } from "./tools/send-file-to-user-tool.js";
7
7
  import { viewPushResultTool } from "./tools/view-push-result-tool.js";
8
8
  import { imageReadingTool } from "./tools/image-reading-tool.js";
9
9
  import { timestampToUtc8Tool } from "./tools/timestamp-to-utc8-tool.js";
10
+ import { saveSelfEvolutionSkillTool } from "./tools/save-self-evolution-skill-tool.js";
10
11
  import { getEmailToolSchemaTool } from "./tools/get-email-tool-schema.js";
11
12
  import { callDeviceTool } from "./tools/call-device-tool.js";
12
13
  import { getNoteToolSchemaTool } from "./tools/get-note-tool-schema.js";
@@ -62,7 +63,7 @@ export const xyPlugin = {
62
63
  },
63
64
  outbound: xyOutbound,
64
65
  agentTools: () => {
65
- const allTools = [locationTool, callDeviceTool, getNoteToolSchemaTool, getCalendarToolSchemaTool, getContactToolSchemaTool, getPhotoToolSchemaTool, xiaoyiGuiTool, getDeviceFileToolSchemaTool, getAlarmToolSchemaTool, getCollectionToolSchemaTool, sendFileToUserTool, viewPushResultTool, imageReadingTool, timestampToUtc8Tool, getEmailToolSchemaTool, queryAppMessageTool, queryMemoryDataTool, queryTodoTaskTool, loginTokenTool];
66
+ const allTools = [locationTool, callDeviceTool, getNoteToolSchemaTool, getCalendarToolSchemaTool, getContactToolSchemaTool, getPhotoToolSchemaTool, xiaoyiGuiTool, getDeviceFileToolSchemaTool, getAlarmToolSchemaTool, getCollectionToolSchemaTool, sendFileToUserTool, viewPushResultTool, imageReadingTool, timestampToUtc8Tool, saveSelfEvolutionSkillTool, getEmailToolSchemaTool, queryAppMessageTool, queryMemoryDataTool, queryTodoTaskTool, loginTokenTool];
66
67
  const ctx = getCurrentSessionContext();
67
68
  const filtered = filterToolsByDevice(allTools, ctx?.deviceType);
68
69
  logger.log(`[DEVICE-FILTER] deviceType=${ctx?.deviceType ?? "(none)"}, tools: ${allTools.length} → ${filtered.length} (${filtered.map(t => t.name).join(", ")})`);
@@ -71,7 +71,6 @@ export async function monitorXYProvider(opts = {}) {
71
71
  const messageHandler = (message, sessionId, serverId) => {
72
72
  const messageKey = `${sessionId}::${message.id}`;
73
73
  log(`[MONITOR-HANDLER] ####### messageHandler triggered: sessionId=${sessionId}, messageId=${message.id} #######`);
74
- console.log(`[MONITOR-HANDLER] A2A message body: ${JSON.stringify(message)}`);
75
74
  // ✅ Report health: received a message
76
75
  trackEvent?.();
77
76
  // Check for duplicate message handling
@@ -9,6 +9,7 @@
9
9
  // models.providers.xiaoyiprovider.models = [...]
10
10
  import { createHash } from "crypto";
11
11
  import { getCurrentSessionContext } from "./tools/session-manager.js";
12
+ import { selfEvolutionManager } from "./utils/self-evolution-manager.js";
12
13
  // ── Retry config ──────────────────────────────────────────────
13
14
  const RETRY_DELAYS_MS = [10_000, 20_000, 40_000, 60_000];
14
15
  const MAX_RETRY_ATTEMPTS = 8;
@@ -21,6 +22,8 @@ function isRetryableProviderError(message) {
21
22
  return true;
22
23
  if (lower.includes("rate limit reached for requests"))
23
24
  return true;
25
+ if (lower.includes("现在访问有点拥挤,稍等一下再试会更顺畅哦~"))
26
+ return true;
24
27
  return false;
25
28
  }
26
29
  /** Check if the request is triggered by a cron job by inspecting the first user message. */
@@ -41,6 +44,25 @@ function isCronTriggered(messages) {
41
44
  }
42
45
  return /^\[cron:/i.test(text.trim());
43
46
  }
47
+ /** Extract cron title from first user message matching `[cron:<uuid> <title>]`. */
48
+ function extractCronTitle(messages) {
49
+ if (!messages)
50
+ return undefined;
51
+ const firstUser = messages.find(m => m.role === "user");
52
+ if (!firstUser)
53
+ return undefined;
54
+ let text = "";
55
+ if (typeof firstUser.content === "string") {
56
+ text = firstUser.content;
57
+ }
58
+ else if (Array.isArray(firstUser.content)) {
59
+ const block = firstUser.content.find(b => b.type === "text" && typeof b.text === "string");
60
+ if (block)
61
+ text = block.text;
62
+ }
63
+ const match = text.trim().match(/^\[cron:[^\s]+\s+(.+)\]$/);
64
+ return match ? match[1] : undefined;
65
+ }
44
66
  /** Compute retry delay in ms for the given 1-based attempt, with up to 10s jitter. */
45
67
  function getRetryDelayMs(attempt, isCron = false) {
46
68
  if (isCron) {
@@ -217,6 +239,39 @@ const HEADER_SESSION_ID = "x-session-id";
217
239
  const HEADER_INTERACTION_ID = "x-interaction-id";
218
240
  /** Internal key for passing fallback uid prefix from prepareExtraParams to wrapStreamFn. */
219
241
  const FALLBACK_PREFIX_KEY = "_xiaoyi_fallback_prefix";
242
+ const SELF_EVOLUTION_PROMPT_BEGIN = "<self_evolution_prompt>";
243
+ const SELF_EVOLUTION_PROMPT_END = "</self_evolution_prompt>";
244
+ const SELF_EVOLUTION_ENABLED_PROMPT_SECTION = `
245
+ ${SELF_EVOLUTION_PROMPT_BEGIN}
246
+ ## Self Evolution(CRITICAL)
247
+ 用户已启用自进化(不管之前是什么状态,不管之前的对话记录,当前视作已开启即可)。
248
+ 自进化含义:当你识别出某个稳定、可复用的经验、调试模式或工作流程,并且它很可能对未来完成此用户的同类任务有帮助时,你可以考虑将其保存为可复用的经验。
249
+
250
+ [触发条件]
251
+ 当满足以下任意一项时,必须触发自进化流程:
252
+ - 显式指令与关键词匹配(最高优先级):当用户消息中包含“进化”、“下次应该”、“以后必须”、“永远”等表达长期意图的词汇,或伴随明显的行为纠正、流程规范化或交付标准设定的指导动作等。
253
+ - 主动固化要求:用户主动要求将过去的交互经验(如交付件规格、skill选择、任务工作流等)进行沉淀/自进化。
254
+ - 隐式经验与避坑沉淀:当前任务执行过程中,你识别出了可以持久化为长期经验的标准工作流,或者在执行中遇到了工具调用错误、命令参数错误等明确的“踩坑点”,以后需要规避。
255
+
256
+ [触发后处理]
257
+ - 遵循 "self-evolution" 技能指导逐步进行技能评估。
258
+ - 只能在任务完成后,正式回复用户前读取该技能。
259
+ - 禁止直接修改文件。需要先创建提案,向用户发送进化请求,用户同意后才能进行更改。
260
+ ${SELF_EVOLUTION_PROMPT_END}
261
+ `.trim();
262
+ const SELF_EVOLUTION_DISABLED_PROMPT_SECTION = `
263
+ ${SELF_EVOLUTION_PROMPT_BEGIN}
264
+ ## Self Evolution
265
+ 用户当前已禁用自我进化。
266
+ 你不得执行自进化相关行为,并且应将此功能视为不可用(不管之前是什么状态,不管之前的对话记录,当前视作已关闭即可)。
267
+ ${SELF_EVOLUTION_PROMPT_END}
268
+ `.trim();
269
+ function stripSelfEvolutionPrompt(prompt) {
270
+ return prompt
271
+ .replace(/\n*<self_evolution_prompt>[\s\S]*?<\/self_evolution_prompt>\n*/gu, "\n\n")
272
+ .replace(/\n{3,}/gu, "\n\n")
273
+ .trim();
274
+ }
220
275
  /**
221
276
  * Encode uid via SHA-256 and take first 32 hex chars.
222
277
  */
@@ -293,15 +348,26 @@ export const xiaoyiProvider = {
293
348
  dynamicHeaders[HEADER_TRACE_ID] = isCron ? `cron_${fallbackValue}` : fallbackValue;
294
349
  dynamicHeaders[HEADER_SESSION_ID] = fallbackValue;
295
350
  dynamicHeaders[HEADER_INTERACTION_ID] = fallbackValue;
351
+ if (isCron) {
352
+ const cronTitle = extractCronTitle(context.messages);
353
+ if (cronTitle)
354
+ dynamicHeaders["x-cron-title"] = cronTitle;
355
+ }
296
356
  }
297
357
  else {
298
- // Session mode: use pre-resolved session headers
358
+ // Session mode: use pre-resolved session headers + fresh timestamp
299
359
  const traceId = ctx.extraParams[HEADER_TRACE_ID];
300
360
  const sessionId = ctx.extraParams[HEADER_SESSION_ID];
301
361
  const interactionId = ctx.extraParams[HEADER_INTERACTION_ID];
362
+ const ts = `_${Date.now()}`;
302
363
  if (typeof traceId === "string") {
303
364
  const isCron = isCronTriggered(context.messages);
304
- dynamicHeaders[HEADER_TRACE_ID] = isCron ? `cron_${traceId}` : traceId;
365
+ dynamicHeaders[HEADER_TRACE_ID] = isCron ? `cron_${traceId}${ts}` : `${traceId}${ts}`;
366
+ if (isCron) {
367
+ const cronTitle = extractCronTitle(context.messages);
368
+ if (cronTitle)
369
+ dynamicHeaders["x-cron-title"] = cronTitle;
370
+ }
305
371
  }
306
372
  if (typeof sessionId === "string")
307
373
  dynamicHeaders[HEADER_SESSION_ID] = sessionId;
@@ -314,6 +380,7 @@ export const xiaoyiProvider = {
314
380
  if (context.systemPrompt) {
315
381
  console.log(`[xiaoyiprovider] system prompt length: ${context.systemPrompt.length}`);
316
382
  }
383
+ const sessionCtx = getCurrentSessionContext();
317
384
  // 在发送给模型前,优化 systemPrompt 结构
318
385
  if (context.systemPrompt) {
319
386
  let sp = context.systemPrompt;
@@ -343,8 +410,17 @@ export const xiaoyiProvider = {
343
410
  console.log(`[xiaoyiprovider] system prompt optimized: ${beforeLen} -> ${sp.length}`);
344
411
  context.systemPrompt = sp;
345
412
  }
413
+ const selfEvolutionEnabled = await selfEvolutionManager.isEnabled();
414
+ const prompt = stripSelfEvolutionPrompt(context.systemPrompt ?? "");
415
+ context.systemPrompt = [
416
+ prompt,
417
+ selfEvolutionEnabled
418
+ ? SELF_EVOLUTION_ENABLED_PROMPT_SECTION
419
+ : SELF_EVOLUTION_DISABLED_PROMPT_SECTION,
420
+ ]
421
+ .filter(Boolean)
422
+ .join("\n\n");
346
423
  // Append device context to systemPrompt
347
- const sessionCtx = getCurrentSessionContext();
348
424
  if (sessionCtx?.deviceType) {
349
425
  const rawDevice = sessionCtx.deviceType;
350
426
  const displayDevice = (rawDevice === "2in1") ? "鸿蒙PC" : rawDevice;
@@ -1,3 +1,22 @@
1
- import type { PluginHookBeforePromptBuildEvent, PluginHookBeforePromptBuildResult, PluginHookAgentContext } from "openclaw/plugins/hooks.js";
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
+ }
2
20
  import type { ToolRetrieverConfig } from "./types.js";
3
21
  export declare function createBeforePromptBuildHandler(config: ToolRetrieverConfig): (event: PluginHookBeforePromptBuildEvent, ctx?: PluginHookAgentContext) => Promise<PluginHookBeforePromptBuildResult | undefined>;
22
+ export {};
@@ -3,18 +3,22 @@ const TOOL_RETRIEVER_HEADER = `## 用户查询相关skill列表如下:
3
3
 
4
4
  `;
5
5
  const TOOL_RETRIEVER_FOOTER = `
6
- 以上是检索到的、与当前查询相关但用户尚未安装的 skill,优先推荐强相关的 skill(通用兜底工具如 web-search、xiaoyi-gui-agent 仅在无强相关skill时调用)。
7
- 对于强相关skill,你需要主动提醒用户可以考虑安装,但禁止直接给用户安装。用户明确希望安装后,使用find-skill执行安装。
8
- 若用户已有的skill已能很好地完成当前任务,则无需提醒安装功能相似的skill。
6
+ 以上是检索到的、与当前查询相关但用户尚未安装的skill,请按照以下规则判断是否需要推荐:
7
+ 1.判断用户当前请求的意图类型:
8
+ -若用户请求为查询已有信息、查看状态、执行已有功能或进行常规操作(例如查看定时任务列表),且这些操作无需额外skill即可完成,则不推荐任何skill。
9
+ 2.仅在以下条件全部满足时,才考虑推荐skill:
10
+ -用户请求明确表示需要完成某个具体任务;
11
+ -现有能力(包括已安装的skill或系统自带功能)不足以满足该任务。此时,优先推荐与任务强相关的skill。
12
+ 3.对于强相关且用户尚未安装的skill:
13
+ -可主动提醒用户考虑安装,但禁止直接安装;
14
+ -用户明确同意后,使用find-skills执行安装。
15
+ 4.若用户已安装的skill已能很好地完成当前任务,即使存在功能相似的未安装skill,也无需提醒。
9
16
  ---以下是用户原始请求---
10
17
  `;
11
18
  const PLUGIN_LOG_PREFIX = "[skill-retriever]";
12
- const SKIP_KEYWORDS = ["安装", "装一下", "下载", "查询", "查找", "install", "卸载", "删除", "重载"];
19
+ const SKIP_KEYWORDS = ["安装", "装一下", "下载", "查询", "查找", "install", "卸载", "删除", "重载", "定时任务", "重装"];
13
20
  const SKIP_PATTERNS = [
14
- "/new",
15
- "/reset",
16
- "session was started",
17
- "a new session was started",
21
+ "/new", "/reset", "/compact", "/stop", "/think", "/model", "/fast", "/verbose", "/config", "/debug", "/status", "/tasks", "/whoami", "/context", "/skill", "/commands", "/tools"
18
22
  ];
19
23
  function shouldSkipSearch(prompt) {
20
24
  const trimmedPrompt = prompt.trim();
@@ -38,30 +42,22 @@ export function createBeforePromptBuildHandler(config) {
38
42
  return async (event, ctx) => {
39
43
  const userPrompt = event.prompt;
40
44
  if (ctx?.sessionKey?.includes(":subagent:")) {
41
- console.log(`${PLUGIN_LOG_PREFIX} [SKIP] Sub-agent detected, skipping search`);
42
45
  return undefined;
43
46
  }
44
47
  if (!config.enabled) {
45
- console.log(`${PLUGIN_LOG_PREFIX} [SKIP] Plugin disabled, original query: "${userPrompt}"`);
46
48
  return undefined;
47
49
  }
48
50
  if (!userPrompt || userPrompt.trim().length === 0) {
49
- console.log(`${PLUGIN_LOG_PREFIX} [SKIP] Empty query`);
50
51
  return undefined;
51
52
  }
52
- console.log(`${PLUGIN_LOG_PREFIX} [RECEIVED] Original user query (len=${userPrompt.length}): "${userPrompt}"`);
53
53
  const extractedQuery = extractUserQuery(userPrompt);
54
- console.log(`${PLUGIN_LOG_PREFIX} [EXTRACTED] Extracted user query: "${extractedQuery}"`);
55
54
  if (!extractedQuery || extractedQuery.length === 0) {
56
- console.log(`${PLUGIN_LOG_PREFIX} [SKIP] No valid user query after extraction, skipping search`);
57
55
  return undefined;
58
56
  }
59
57
  const skipReason = shouldSkipSearch(extractedQuery);
60
58
  if (skipReason) {
61
- console.log(`${PLUGIN_LOG_PREFIX} [SKIP] ${skipReason}, extracted query: "${extractedQuery}"`);
62
59
  return undefined;
63
60
  }
64
- console.log(`${PLUGIN_LOG_PREFIX} [PROCEED] Calling skill search API (timeout=${config.timeoutMs}ms) for query: "${extractedQuery}"`);
65
61
  try {
66
62
  const searchResult = await searchTools({
67
63
  query: extractedQuery,
@@ -74,16 +70,14 @@ export function createBeforePromptBuildHandler(config) {
74
70
  timeoutMs: config.timeoutMs,
75
71
  });
76
72
  if (!searchResult || searchResult.tools.length === 0) {
77
- console.log(`${PLUGIN_LOG_PREFIX} [RESULT] No skills found for query: "${extractedQuery}"`);
78
73
  return undefined;
79
74
  }
80
75
  console.log(`${PLUGIN_LOG_PREFIX} [RESULT] Found ${searchResult.tools.length} skills, building context...`);
81
76
  const toolsContext = formatToolsForContext(searchResult, config.includeUninstalledOnly);
82
77
  if (!toolsContext) {
83
- console.log(`${PLUGIN_LOG_PREFIX} [ERROR] Failed to format skills context for query: "${extractedQuery}"`);
78
+ console.log(`${PLUGIN_LOG_PREFIX} [ERROR] Failed to format skills context`);
84
79
  return undefined;
85
80
  }
86
- console.log(`${PLUGIN_LOG_PREFIX} [SUCCESS] Built context with ${searchResult.tools.length} skills for query: "${extractedQuery}"`);
87
81
  return {
88
82
  prependContext: TOOL_RETRIEVER_HEADER + toolsContext + TOOL_RETRIEVER_FOOTER,
89
83
  };
@@ -81,7 +81,6 @@ export async function searchTools(options) {
81
81
  const { query, maxTools = 5, includeUninstalledOnly = true, envFilePath = "~/.openclaw/.xiaoyienv", serviceUrl: configServiceUrl, apiKey: configApiKey, uid: configUid, timeoutMs = 1000, } = options;
82
82
  const envConfig = readEnvFile(envFilePath);
83
83
  const hasRequiredConfig = !!envConfig.SERVICE_URL && !!envConfig.PERSONAL_API_KEY && !!envConfig.PERSONAL_UID;
84
- console.log(`${PLUGIN_LOG_PREFIX} Env file loaded: ${hasRequiredConfig}, keys: ${Object.keys(envConfig).join(", ")}`);
85
84
  const serviceUrl = configServiceUrl ?? envConfig.SERVICE_URL;
86
85
  const apiKey = configApiKey ?? envConfig.PERSONAL_API_KEY;
87
86
  const uid = configUid ?? envConfig.PERSONAL_UID;
@@ -89,7 +88,6 @@ export async function searchTools(options) {
89
88
  console.warn(`${PLUGIN_LOG_PREFIX} Missing required configuration. serviceUrl: "${serviceUrl}", apiKey: "${apiKey ? '(set)' : '(missing)'} ", uid: "${uid ? '(set)' : '(missing)'}"`);
90
89
  return null;
91
90
  }
92
- console.log(`${PLUGIN_LOG_PREFIX} Configuration loaded - serviceUrl: ${serviceUrl}, uid: ${uid}`);
93
91
  const traceId = crypto.randomUUID();
94
92
  const apiUrl = `${serviceUrl}/celia-claw/v1/rest-api/skill/execute`;
95
93
  const headers = {
@@ -101,7 +99,6 @@ export async function searchTools(options) {
101
99
  "x-request-from": "openclaw",
102
100
  };
103
101
  const payload = { query };
104
- console.log(`${PLUGIN_LOG_PREFIX} [USER_REQUEST] origin-query:${query}, payload:${JSON.stringify(payload)}`);
105
102
  try {
106
103
  const response = await fetch(apiUrl, {
107
104
  method: "POST",
@@ -119,13 +116,9 @@ export async function searchTools(options) {
119
116
  responseData.content &&
120
117
  responseData.content.skills) {
121
118
  const rawSkills = responseData.content.skills;
122
- console.log(`${PLUGIN_LOG_PREFIX} [DEBUG] Raw skills from API: ${rawSkills.length}, ids: ${rawSkills.map((s) => s.skillId).join(", ")}`);
123
119
  const installedSkills = getInstalledSkills();
124
- console.log(`${PLUGIN_LOG_PREFIX} [DEBUG] Installed skills: ${installedSkills.length}, ids: ${installedSkills.join(", ")}`);
125
120
  const formattedData = formatSkillData(rawSkills, installedSkills);
126
- console.log(`${PLUGIN_LOG_PREFIX} [DEBUG] Formatted skills: ${formattedData.length}, statuses: ${formattedData.map((t) => `${t.skillId}:${t.status}`).join(", ")}`);
127
121
  const topTools = formattedData.slice(0, 2);
128
- console.log(`${PLUGIN_LOG_PREFIX} [DEBUG] Top 2 skills: ${topTools.length}, statuses: ${topTools.map((t) => `${t.skillId}:${t.status}`).join(", ")}`);
129
122
  const allInstalled = topTools.every((tool) => tool.status === "已安装");
130
123
  if (allInstalled) {
131
124
  console.log(`${PLUGIN_LOG_PREFIX} [DEBUG] All top 2 skills are installed, returning null`);
@@ -0,0 +1 @@
1
+ export declare const saveSelfEvolutionSkillTool: any;
@@ -0,0 +1,189 @@
1
+ import { createHash } from "node:crypto";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { getCurrentSessionContext } from "./session-manager.js";
5
+ import { selfEvolutionManager } from "../utils/self-evolution-manager.js";
6
+ const SELF_EVOLVED_SKILL_ROOT = "/home/sandbox/.openclaw/workspace/skills";
7
+ function slugifyTitle(title) {
8
+ return title
9
+ .trim()
10
+ .toLowerCase()
11
+ .replace(/[^a-z0-9]+/g, "-")
12
+ .replace(/^-+|-+$/g, "")
13
+ .slice(0, 80);
14
+ }
15
+ function normalizeStringArray(value) {
16
+ if (Array.isArray(value)) {
17
+ return value
18
+ .map((item) => (typeof item === "string" ? item.trim() : ""))
19
+ .filter(Boolean);
20
+ }
21
+ if (typeof value === "string" && value.trim()) {
22
+ return [value.trim()];
23
+ }
24
+ return [];
25
+ }
26
+ function containsSensitiveContent(text) {
27
+ const lower = text.toLowerCase();
28
+ const sensitivePatterns = [
29
+ /api[_ -]?key/u,
30
+ /access[_ -]?token/u,
31
+ /bearer\s+[a-z0-9._-]+/iu,
32
+ /password/u,
33
+ /secret/u,
34
+ /\/home\/sandbox\//u,
35
+ /\/tmp\//u,
36
+ /[a-z]:\\/iu,
37
+ ];
38
+ return sensitivePatterns.some((pattern) => pattern.test(lower));
39
+ }
40
+ function buildSkillMarkdown(params) {
41
+ const description = `${params.summary}\n\nWhen to use: ${params.whenToUse}`
42
+ .replace(/"/g, '\\"')
43
+ .replace(/\r?\n/g, "\\n");
44
+ const lines = [
45
+ "---",
46
+ `name: "${params.title.replace(/"/g, '\\"')}"`,
47
+ `description: "${description}"`,
48
+ "---",
49
+ "",
50
+ `# ${params.title}`,
51
+ "",
52
+ "## Rules",
53
+ ];
54
+ for (const rule of params.rules) {
55
+ lines.push(`- ${rule}`);
56
+ }
57
+ if (params.examples.length > 0) {
58
+ lines.push("", "## Examples");
59
+ for (const example of params.examples) {
60
+ lines.push(`- ${example}`);
61
+ }
62
+ }
63
+ if (params.tags.length > 0) {
64
+ lines.push("", "## Tags", params.tags.map((tag) => `- ${tag}`).join("\n"));
65
+ }
66
+ lines.push("");
67
+ return lines.join("\n");
68
+ }
69
+ export const saveSelfEvolutionSkillTool = {
70
+ name: "save_self_evolution_skill",
71
+ label: "Save Self Evolution Skill",
72
+ description: "将可复用的经验/脚本/教训等保存为skill技能,供下次执行类似任务时参考。仅用于通用、可复用的场景。",
73
+ parameters: {
74
+ type: "object",
75
+ properties: {
76
+ title: {
77
+ type: "string",
78
+ description: "所学技能的简短、可复用标题。",
79
+ },
80
+ summary: {
81
+ type: "string",
82
+ description: "技能的概括性总结,不要太长。",
83
+ },
84
+ when_to_use: {
85
+ type: "string",
86
+ description: "描述在未来任务中什么情况/哪些条件下使用此技能。",
87
+ },
88
+ rules: {
89
+ type: "array",
90
+ items: { type: "string" },
91
+ description: "具体、可复用的规则或checklist。",
92
+ },
93
+ examples: {
94
+ type: "array",
95
+ items: { type: "string" },
96
+ description: "陷阱示例以及正确模式示例,可选",
97
+ },
98
+ tags: {
99
+ type: "array",
100
+ items: { type: "string" },
101
+ description: "用于未来发现的标签,可选。",
102
+ },
103
+ },
104
+ required: ["title", "summary", "when_to_use", "rules"],
105
+ },
106
+ async execute(_toolCallId, params) {
107
+ if (!(await selfEvolutionManager.isEnabled())) {
108
+ throw new Error("Self-evolution is currently disabled by the user.");
109
+ }
110
+ const sessionContext = getCurrentSessionContext();
111
+ if (!sessionContext) {
112
+ throw new Error("No active XY session found. This tool can only run during an active conversation.");
113
+ }
114
+ const title = typeof params.title === "string" ? params.title.trim() : "";
115
+ const summary = typeof params.summary === "string" ? params.summary.trim() : "";
116
+ const whenToUse = typeof params.when_to_use === "string" ? params.when_to_use.trim() : "";
117
+ const rules = normalizeStringArray(params.rules);
118
+ const examples = normalizeStringArray(params.examples);
119
+ const tags = normalizeStringArray(params.tags);
120
+ if (!title || !summary || !whenToUse || rules.length === 0) {
121
+ throw new Error("Missing required fields. title, summary, when_to_use, and at least one rule are required.");
122
+ }
123
+ if (title.length < 6 || summary.length < 10 || whenToUse.length < 10) {
124
+ throw new Error("Skill content is too short. Provide a reusable title, summary, and usage guidance.");
125
+ }
126
+ const combinedText = [title, summary, whenToUse, ...rules, ...examples, ...tags].join("\n");
127
+ if (containsSensitiveContent(combinedText)) {
128
+ throw new Error("Skill content appears to contain sensitive or environment-specific data and was rejected.");
129
+ }
130
+ const slug = slugifyTitle(title);
131
+ if (!slug) {
132
+ throw new Error("Title could not be normalized into a valid skill name.");
133
+ }
134
+ const skillDir = path.join(SELF_EVOLVED_SKILL_ROOT, `evolving-${slug}`);
135
+ const skillFilePath = path.join(skillDir, "SKILL.md");
136
+ const nextContent = buildSkillMarkdown({
137
+ title,
138
+ summary,
139
+ whenToUse,
140
+ rules,
141
+ examples,
142
+ tags,
143
+ });
144
+ const nextHash = createHash("sha256").update(nextContent).digest("hex");
145
+ await fs.mkdir(skillDir, { recursive: true });
146
+ try {
147
+ const existingContent = await fs.readFile(skillFilePath, "utf-8");
148
+ const existingHash = createHash("sha256").update(existingContent).digest("hex");
149
+ if (existingHash === nextHash) {
150
+ return {
151
+ content: [
152
+ {
153
+ type: "text",
154
+ text: JSON.stringify({
155
+ success: true,
156
+ deduped: true,
157
+ skillName: slug,
158
+ path: skillFilePath,
159
+ message: "An identical self-evolved skill already exists.",
160
+ }),
161
+ },
162
+ ],
163
+ };
164
+ }
165
+ throw new Error(`A different skill with the same title already exists: ${skillFilePath}`);
166
+ }
167
+ catch (error) {
168
+ if (error?.code !== "ENOENT") {
169
+ throw error;
170
+ }
171
+ }
172
+ await fs.writeFile(skillFilePath, nextContent, "utf-8");
173
+ return {
174
+ content: [
175
+ {
176
+ type: "text",
177
+ text: JSON.stringify({
178
+ success: true,
179
+ deduped: false,
180
+ skillName: slug,
181
+ path: skillFilePath,
182
+ sessionId: sessionContext.sessionId,
183
+ message: "Self-evolved skill saved successfully.",
184
+ }),
185
+ },
186
+ ],
187
+ };
188
+ },
189
+ };
@@ -2,6 +2,7 @@
2
2
  // Stores active session contexts that tools can access
3
3
  import { AsyncLocalStorage } from "async_hooks";
4
4
  import { configManager } from "../utils/config-manager.js";
5
+ import { toolCallNudgeManager } from "../utils/tool-call-nudge-manager.js";
5
6
  import { getCurrentTaskId, getCurrentMessageId } from "../task-manager.js";
6
7
  // Map of sessionKey -> SessionContextWithRef
7
8
  const activeSessions = new Map();
@@ -40,6 +41,7 @@ export function unregisterSession(sessionKey) {
40
41
  if (existing.refCount <= 0) {
41
42
  activeSessions.delete(sessionKey);
42
43
  configManager.clearSession(existing.sessionId);
44
+ toolCallNudgeManager.clearSession(sessionKey);
43
45
  }
44
46
  }
45
47
  /**
@@ -0,0 +1,5 @@
1
+ declare class SelfEvolutionManager {
2
+ isEnabled(): Promise<boolean>;
3
+ }
4
+ export declare const selfEvolutionManager: SelfEvolutionManager;
5
+ export {};
@@ -0,0 +1,47 @@
1
+ import fs from "node:fs/promises";
2
+ const SELF_EVOLUTION_ENV_FILE = "/home/sandbox/.openclaw/.xiaoyienv";
3
+ const SELF_EVOLUTION_ENV_KEY = "selfEvolutionState";
4
+ function parseBooleanLike(value) {
5
+ const normalized = value.trim().toLowerCase();
6
+ if (normalized === "true") {
7
+ return true;
8
+ }
9
+ if (normalized === "false") {
10
+ return false;
11
+ }
12
+ return null;
13
+ }
14
+ class SelfEvolutionManager {
15
+ async isEnabled() {
16
+ try {
17
+ const envData = await fs.readFile(SELF_EVOLUTION_ENV_FILE, "utf-8");
18
+ for (const line of envData.split(/\r?\n/u)) {
19
+ const trimmed = line.trim();
20
+ if (!trimmed || trimmed.startsWith("#")) {
21
+ continue;
22
+ }
23
+ const eqIndex = trimmed.indexOf("=");
24
+ if (eqIndex === -1) {
25
+ continue;
26
+ }
27
+ const key = trimmed.slice(0, eqIndex).trim();
28
+ if (key !== SELF_EVOLUTION_ENV_KEY) {
29
+ continue;
30
+ }
31
+ const value = trimmed.slice(eqIndex + 1).trim();
32
+ const parsed = parseBooleanLike(value);
33
+ if (parsed !== null) {
34
+ return parsed;
35
+ }
36
+ }
37
+ return false;
38
+ }
39
+ catch (error) {
40
+ if (error?.code !== "ENOENT") {
41
+ console.error(`[SELF_EVOLUTION] Failed to read ${SELF_EVOLUTION_ENV_FILE}:`, error);
42
+ }
43
+ return false;
44
+ }
45
+ }
46
+ }
47
+ export const selfEvolutionManager = new SelfEvolutionManager();
@@ -0,0 +1,14 @@
1
+ type RecordToolCallResult = {
2
+ count: number;
3
+ shouldNudge: boolean;
4
+ };
5
+ declare class ToolCallNudgeManager {
6
+ private readonly threshold;
7
+ private readonly sessions;
8
+ constructor(threshold?: number);
9
+ recordToolCall(sessionKey: string): RecordToolCallResult;
10
+ clearSession(sessionKey: string): void;
11
+ }
12
+ export declare const TOOL_CALL_NUDGE_THRESHOLD = 5;
13
+ export declare const toolCallNudgeManager: ToolCallNudgeManager;
14
+ export {};
@@ -0,0 +1,35 @@
1
+ const DEFAULT_TOOL_CALL_NUDGE_THRESHOLD = 5;
2
+ class ToolCallNudgeManager {
3
+ threshold;
4
+ sessions = new Map();
5
+ constructor(threshold = DEFAULT_TOOL_CALL_NUDGE_THRESHOLD) {
6
+ this.threshold = threshold;
7
+ }
8
+ recordToolCall(sessionKey) {
9
+ let state = this.sessions.get(sessionKey);
10
+ if (!state) {
11
+ state = {
12
+ count: 0,
13
+ nudged: false,
14
+ };
15
+ this.sessions.set(sessionKey, state);
16
+ }
17
+ state.count += 1;
18
+ if (!state.nudged && state.count >= this.threshold) {
19
+ state.nudged = true;
20
+ return {
21
+ count: state.count,
22
+ shouldNudge: true,
23
+ };
24
+ }
25
+ return {
26
+ count: state.count,
27
+ shouldNudge: false,
28
+ };
29
+ }
30
+ clearSession(sessionKey) {
31
+ this.sessions.delete(sessionKey);
32
+ }
33
+ }
34
+ export const TOOL_CALL_NUDGE_THRESHOLD = DEFAULT_TOOL_CALL_NUDGE_THRESHOLD;
35
+ export const toolCallNudgeManager = new ToolCallNudgeManager();
@@ -354,12 +354,11 @@ export class XYWebSocketManager extends EventEmitter {
354
354
  if (parsed.jsonrpc === "2.0") {
355
355
  const a2aRequest = parsed;
356
356
  // Extract sessionId from params
357
- // const sessionId = a2aRequest.params?.sessionId;
358
- // if (!sessionId) {
359
- // console.error("[XY] Message missing sessionId");
360
- // return;
361
- // }
362
- const sessionId = "111111";
357
+ const sessionId = a2aRequest.params?.sessionId;
358
+ if (!sessionId) {
359
+ console.error("[XY] Message missing sessionId");
360
+ return;
361
+ }
363
362
  // Check if message contains only data parts (tool results)
364
363
  const dataParts = a2aRequest.params?.message?.parts?.filter((p) => p.kind === "data");
365
364
  const hasOnlyDataParts = dataParts && dataParts.length > 0 &&
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ynhcj/xiaoyi-channel",
3
- "version": "0.0.67-next",
3
+ "version": "0.0.68-next",
4
4
  "description": "OpenClaw Xiaoyi Channel plugin - Xiaoyi A2A protocol integration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",