@ynhcj/xiaoyi-channel 0.0.72-beta → 0.0.72-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 (99) hide show
  1. package/dist/index.d.ts +0 -5
  2. package/dist/index.js +51 -14
  3. package/dist/src/bot.js +27 -3
  4. package/dist/src/channel.js +15 -23
  5. package/dist/src/cspl/call-api.js +14 -11
  6. package/dist/src/cspl/config.js +3 -3
  7. package/dist/src/cspl/constants.d.ts +2 -0
  8. package/dist/src/cspl/constants.js +12 -0
  9. package/dist/src/cspl/utils.js +4 -2
  10. package/dist/src/file-download.js +3 -6
  11. package/dist/src/file-upload.js +52 -5
  12. package/dist/src/login-token-handler.d.ts +8 -0
  13. package/dist/src/login-token-handler.js +60 -0
  14. package/dist/src/message-queue.d.ts +17 -0
  15. package/dist/src/message-queue.js +51 -0
  16. package/dist/src/monitor.js +54 -3
  17. package/dist/src/outbound.js +2 -7
  18. package/dist/src/provider.d.ts +1 -0
  19. package/dist/src/provider.js +442 -43
  20. package/dist/src/reply-dispatcher.js +6 -0
  21. package/dist/src/self-evolution-handler.d.ts +7 -0
  22. package/dist/src/self-evolution-handler.js +141 -0
  23. package/dist/src/self-evolution-keyword.d.ts +9 -0
  24. package/dist/src/self-evolution-keyword.js +145 -0
  25. package/dist/src/skill-retriever/config.d.ts +4 -0
  26. package/dist/src/skill-retriever/config.js +23 -0
  27. package/dist/src/skill-retriever/hooks.d.ts +22 -0
  28. package/dist/src/skill-retriever/hooks.js +91 -0
  29. package/dist/src/skill-retriever/tool-search.d.ts +16 -0
  30. package/dist/src/skill-retriever/tool-search.js +159 -0
  31. package/dist/src/skill-retriever/types.d.ts +34 -0
  32. package/dist/src/skill-retriever/types.js +1 -0
  33. package/dist/src/task-manager.d.ts +4 -0
  34. package/dist/src/task-manager.js +6 -0
  35. package/dist/src/tools/call-device-tool.d.ts +5 -0
  36. package/dist/src/tools/call-device-tool.js +130 -0
  37. package/dist/src/tools/create-alarm-tool.js +5 -16
  38. package/dist/src/tools/delete-alarm-tool.js +1 -4
  39. package/dist/src/tools/device-tool-map.js +5 -1
  40. package/dist/src/tools/find-pc-devices-tool.d.ts +5 -0
  41. package/dist/src/tools/find-pc-devices-tool.js +98 -0
  42. package/dist/src/tools/get-alarm-tool-schema.d.ts +16 -0
  43. package/dist/src/tools/get-alarm-tool-schema.js +11 -0
  44. package/dist/src/tools/get-calendar-tool-schema.d.ts +16 -0
  45. package/dist/src/tools/get-calendar-tool-schema.js +9 -0
  46. package/dist/src/tools/get-collection-tool-schema.d.ts +16 -0
  47. package/dist/src/tools/get-collection-tool-schema.js +10 -0
  48. package/dist/src/tools/get-contact-tool-schema.d.ts +16 -0
  49. package/dist/src/tools/get-contact-tool-schema.js +11 -0
  50. package/dist/src/tools/get-device-file-tool-schema.d.ts +16 -0
  51. package/dist/src/tools/get-device-file-tool-schema.js +10 -0
  52. package/dist/src/tools/get-email-tool-schema.d.ts +16 -0
  53. package/dist/src/tools/get-email-tool-schema.js +9 -0
  54. package/dist/src/tools/get-note-tool-schema.d.ts +16 -0
  55. package/dist/src/tools/get-note-tool-schema.js +10 -0
  56. package/dist/src/tools/get-photo-tool-schema.d.ts +16 -0
  57. package/dist/src/tools/get-photo-tool-schema.js +10 -0
  58. package/dist/src/tools/image-reading-tool.js +4 -7
  59. package/dist/src/tools/login-token-tool.d.ts +5 -0
  60. package/dist/src/tools/login-token-tool.js +136 -0
  61. package/dist/src/tools/modify-alarm-tool.js +10 -23
  62. package/dist/src/tools/query-app-message-tool.d.ts +4 -0
  63. package/dist/src/tools/query-app-message-tool.js +138 -0
  64. package/dist/src/tools/query-memory-data-tool.d.ts +4 -0
  65. package/dist/src/tools/query-memory-data-tool.js +154 -0
  66. package/dist/src/tools/query-todo-task-tool.d.ts +4 -0
  67. package/dist/src/tools/query-todo-task-tool.js +133 -0
  68. package/dist/src/tools/save-file-to-phone-tool.d.ts +5 -0
  69. package/dist/src/tools/save-file-to-phone-tool.js +166 -0
  70. package/dist/src/tools/save-media-to-gallery-tool.js +3 -7
  71. package/dist/src/tools/save-self-evolution-skill-tool.d.ts +1 -0
  72. package/dist/src/tools/save-self-evolution-skill-tool.js +412 -0
  73. package/dist/src/tools/schema-tool-factory.d.ts +27 -0
  74. package/dist/src/tools/schema-tool-factory.js +32 -0
  75. package/dist/src/tools/search-alarm-tool.js +6 -13
  76. package/dist/src/tools/search-calendar-tool.js +2 -0
  77. package/dist/src/tools/search-email-tool.d.ts +5 -0
  78. package/dist/src/tools/search-email-tool.js +137 -0
  79. package/dist/src/tools/search-file-tool.js +4 -4
  80. package/dist/src/tools/search-message-tool.js +1 -0
  81. package/dist/src/tools/search-photo-gallery-tool.js +2 -2
  82. package/dist/src/tools/send-email-tool.d.ts +4 -0
  83. package/dist/src/tools/send-email-tool.js +134 -0
  84. package/dist/src/tools/send-file-to-user-tool.js +2 -4
  85. package/dist/src/tools/session-manager.js +2 -0
  86. package/dist/src/tools/upload-file-tool.js +4 -4
  87. package/dist/src/tools/upload-photo-tool.js +2 -2
  88. package/dist/src/tools/xiaoyi-add-collection-tool.js +35 -6
  89. package/dist/src/tools/xiaoyi-collection-tool.js +2 -1
  90. package/dist/src/tools/xiaoyi-delete-collection-tool.js +1 -1
  91. package/dist/src/utils/runtime-manager.js +24 -2
  92. package/dist/src/utils/self-evolution-manager.d.ts +5 -0
  93. package/dist/src/utils/self-evolution-manager.js +47 -0
  94. package/dist/src/utils/tool-call-nudge-manager.d.ts +16 -0
  95. package/dist/src/utils/tool-call-nudge-manager.js +47 -0
  96. package/dist/src/websocket.d.ts +3 -0
  97. package/dist/src/websocket.js +71 -0
  98. package/openclaw.plugin.json +1 -0
  99. package/package.json +2 -2
@@ -0,0 +1,166 @@
1
+ import { getXYWebSocketManager } from "../client.js";
2
+ import { sendCommand } from "../formatter.js";
3
+ import { getCurrentSessionContext } from "./session-manager.js";
4
+ import { XYFileUploadService } from "../file-upload.js";
5
+ /**
6
+ * Duck-typed ToolInputError: openclaw 按 .name 字段匹配,不用 instanceof。
7
+ * 抛出此错误会让 openclaw 返回 HTTP 400 而非 500,
8
+ * LLM 会将其识别为参数错误而非瞬时故障,不会触发重试。
9
+ */
10
+ class ToolInputError extends Error {
11
+ status = 400;
12
+ constructor(message) {
13
+ super(message);
14
+ this.name = "ToolInputError";
15
+ }
16
+ }
17
+ /**
18
+ * XY save file to phone tool - saves files to user's device file manager.
19
+ * Supports local file paths (auto-uploaded to get public URL) and public URLs.
20
+ */
21
+ export const saveFileToPhoneTool = {
22
+ name: "save_file_to_file_manager",
23
+ label: "Save File to Phone",
24
+ description: `将文件保存到手机文件管理器。
25
+
26
+ 注意:
27
+ a. 操作超时时间为60秒,请勿重复调用此工具
28
+ b. 如果遇到各类调用失败场景,不可以重试,直接返回错误。
29
+ c. 调用工具前需认真检查调用参数是否满足工具要求
30
+
31
+ 回复约束:如果工具返回没有授权或者其他报错,只需要完整描述没有授权或者其他报错内容即可,不需要主动给用户提供解决方案,例如告诉用户如何授权,如何解决报错等都是不需要的,请严格遵守。
32
+ `,
33
+ parameters: {
34
+ type: "object",
35
+ properties: {
36
+ fileName: {
37
+ type: "string",
38
+ description: "必填,文件名称。",
39
+ },
40
+ url: {
41
+ type: "string",
42
+ description: "必填,支持本地路径或者公网url路径。如果是本地路径会先上传获取公网url。",
43
+ },
44
+ suffix: {
45
+ type: "string",
46
+ description: "必填,文件后缀,例如 ppt、doc、pdf 等。",
47
+ },
48
+ },
49
+ required: ["fileName", "url", "suffix"],
50
+ },
51
+ async execute(toolCallId, params) {
52
+ // Validate parameters
53
+ const { fileName, url, suffix } = params;
54
+ if (!url || typeof url !== "string") {
55
+ throw new ToolInputError("缺少必填参数: url");
56
+ }
57
+ if (!fileName || typeof fileName !== "string") {
58
+ throw new ToolInputError("缺少必填参数: fileName");
59
+ }
60
+ if (!suffix || typeof suffix !== "string") {
61
+ throw new ToolInputError("缺少必填参数: suffix");
62
+ }
63
+ // Get session context
64
+ const sessionContext = getCurrentSessionContext();
65
+ if (!sessionContext) {
66
+ throw new Error("No active XY session found. SaveFileToFileManager tool can only be used during an active conversation.");
67
+ }
68
+ const { config, sessionId, taskId, messageId } = sessionContext;
69
+ // Get WebSocket manager
70
+ const wsManager = getXYWebSocketManager(config);
71
+ // Determine the URL: if it's a local path, upload first to get public URL
72
+ let publicUrl = url;
73
+ if (!url.startsWith("http://") && !url.startsWith("https://")) {
74
+ // Local file path - upload to get public URL
75
+ const uploadService = new XYFileUploadService(config.fileUploadUrl, config.apiKey, config.uid);
76
+ publicUrl = await uploadService.uploadFileAndGetUrl(url);
77
+ if (!publicUrl) {
78
+ throw new Error("本地文件上传失败,无法获取公网URL");
79
+ }
80
+ }
81
+ // Build intentParam
82
+ const intentParam = {
83
+ fileName: fileName,
84
+ url: publicUrl,
85
+ suffix: suffix,
86
+ };
87
+ // Build SaveFileToFileManager command
88
+ const command = {
89
+ header: {
90
+ namespace: "Common",
91
+ name: "Action",
92
+ },
93
+ payload: {
94
+ cardParam: {},
95
+ executeParam: {
96
+ executeMode: "background",
97
+ intentName: "SaveFileToFileManager",
98
+ bundleName: "com.huawei.hmos.vassistant",
99
+ dimension: "",
100
+ needUnlock: true,
101
+ actionResponse: true,
102
+ appType: "OHOS_APP",
103
+ timeOut: 5,
104
+ timeout: 55000,
105
+ intentParam,
106
+ permissionId: ["ohos.permission.WRITE_IMAGEVIDEO"],
107
+ achieveType: "INTENT",
108
+ },
109
+ responses: [
110
+ {
111
+ resultCode: "",
112
+ displayText: "",
113
+ ttsText: "",
114
+ },
115
+ ],
116
+ needUploadResult: true,
117
+ noHalfPage: false,
118
+ pageControlRelated: false,
119
+ },
120
+ };
121
+ // Send command and wait for response (60 second timeout)
122
+ return new Promise((resolve, reject) => {
123
+ const timeout = setTimeout(() => {
124
+ wsManager.off("data-event", handler);
125
+ reject(new Error("保存文件到手机超时(60秒)"));
126
+ }, 60000);
127
+ // Listen for data events from WebSocket
128
+ const handler = (event) => {
129
+ if (event.intentName === "SaveFileToFileManager") {
130
+ clearTimeout(timeout);
131
+ wsManager.off("data-event", handler);
132
+ if (event.status === "success" && event.outputs) {
133
+ resolve({
134
+ content: [
135
+ {
136
+ type: "text",
137
+ text: JSON.stringify(event.outputs),
138
+ }
139
+ ]
140
+ });
141
+ }
142
+ else {
143
+ reject(new Error(`保存文件到手机失败: ${event.status}`));
144
+ }
145
+ }
146
+ };
147
+ // Register event handler
148
+ wsManager.on("data-event", handler);
149
+ // Send the command
150
+ sendCommand({
151
+ config,
152
+ sessionId,
153
+ taskId,
154
+ messageId,
155
+ command,
156
+ })
157
+ .then(() => {
158
+ })
159
+ .catch((error) => {
160
+ clearTimeout(timeout);
161
+ wsManager.off("data-event", handler);
162
+ reject(error);
163
+ });
164
+ });
165
+ },
166
+ };
@@ -19,13 +19,9 @@ class ToolInputError extends Error {
19
19
  * Supports local file paths (auto-uploaded to get public URL) and public URLs.
20
20
  */
21
21
  export const saveMediaToGalleryTool = {
22
- name: "SaveMediaToGallery",
22
+ name: "save_media_to_gallery",
23
23
  label: "Save Media to Gallery",
24
- description: `将图片文件或者视频文件保存到手机图库。
25
- 工具参数说明:
26
- a. mediaType:非必填,string类型,不传端侧默认为pic。支持传 pic(图片) 或 video(视频)。
27
- b. fileName:非必填,string类型,文件名称,不传手机侧默认生成随机uuid。
28
- c. url:必填,string类型,支持本地路径或者公网url路径。如果是本地路径,会先上传获取公网url再保存到图库。
24
+ description: `将图片文件或者视频文件保存到设备图库。
29
25
 
30
26
  注意:
31
27
  a. 操作超时时间为60秒,请勿重复调用此工具
@@ -47,7 +43,7 @@ export const saveMediaToGalleryTool = {
47
43
  },
48
44
  url: {
49
45
  type: "string",
50
- description: "必填,支持本地路径或者公网url路径。如果是本地路径会先上传获取公网url",
46
+ description: "必填,支持本地路径或者公网url路径。如果是本地路径,会先上传获取公网url再保存到图库",
51
47
  },
52
48
  },
53
49
  required: ["url"],
@@ -0,0 +1 @@
1
+ export declare const saveSelfEvolutionSkillTool: any;
@@ -0,0 +1,412 @@
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/.agents/skills";
7
+ const ISO_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/u;
8
+ function slugifyTitle(title) {
9
+ return title
10
+ .trim()
11
+ .toLowerCase()
12
+ .replace(/[^a-z0-9]+/g, "-")
13
+ .replace(/^-+|-+$/g, "")
14
+ .slice(0, 80);
15
+ }
16
+ function normalizeStringArray(value) {
17
+ if (Array.isArray(value)) {
18
+ return value
19
+ .map((item) => (typeof item === "string" ? item.trim() : ""))
20
+ .filter(Boolean);
21
+ }
22
+ if (typeof value === "string" && value.trim()) {
23
+ return [value.trim()];
24
+ }
25
+ return [];
26
+ }
27
+ function normalizeWhitespace(text) {
28
+ return text.replace(/\s+/gu, " ").trim();
29
+ }
30
+ function normalizeForFingerprint(text) {
31
+ return normalizeWhitespace(text)
32
+ .toLowerCase()
33
+ .replace(/[`"'()[\]{}:;,.!?]/gu, "")
34
+ .replace(/\s+/gu, " ")
35
+ .trim();
36
+ }
37
+ function normalizeForComparison(items) {
38
+ return items
39
+ .map((item) => normalizeForFingerprint(item))
40
+ .filter(Boolean)
41
+ .sort();
42
+ }
43
+ function sanitizeLine(text) {
44
+ let value = text;
45
+ let changed = false;
46
+ const replacements = [
47
+ [/(bearer\s+)[a-z0-9._=-]{12,}/giu, "$1[REDACTED_TOKEN]"],
48
+ [/((?:api[_ -]?key|access[_ -]?token|refresh[_ -]?token|password|secret)\s*[:=]\s*)([^\s,;]+)/giu, "$1[REDACTED_SECRET]"],
49
+ [/(-----BEGIN [A-Z ]*PRIVATE KEY-----)[\s\S]*?(-----END [A-Z ]*PRIVATE KEY-----)/gu, "$1\n[REDACTED_PRIVATE_KEY]\n$2"],
50
+ [/\b(?:[a-zA-Z]:\\(?:[^\\\r\n]+\\)*[^\\\r\n\s]+|\/(?:home|Users|tmp|var|private|etc)\/[^\s"'`<>]+)/gu, "[REDACTED_PATH]"],
51
+ [/\b(sk-[a-zA-Z0-9]{16,}|AKIA[0-9A-Z]{16}|AIza[0-9A-Za-z\-_]{20,})\b/gu, "[REDACTED_SECRET]"],
52
+ ];
53
+ for (const [pattern, replacement] of replacements) {
54
+ const next = value.replace(pattern, replacement);
55
+ if (next !== value) {
56
+ value = next;
57
+ changed = true;
58
+ }
59
+ }
60
+ return { value, changed };
61
+ }
62
+ function sanitizeStringArray(values) {
63
+ let changed = false;
64
+ const sanitized = values.map((value) => {
65
+ const result = sanitizeLine(value);
66
+ changed = changed || result.changed;
67
+ return result.value;
68
+ });
69
+ return { values: sanitized, changed };
70
+ }
71
+ function sanitizeSkillContent(params) {
72
+ const titleResult = sanitizeLine(params.title);
73
+ const summaryResult = sanitizeLine(params.summary);
74
+ const whenToUseResult = sanitizeLine(params.whenToUse);
75
+ const supplementResult = sanitizeLine(params.supplement);
76
+ const rulesResult = sanitizeStringArray(params.rules);
77
+ const examplesResult = sanitizeStringArray(params.examples);
78
+ const tagsResult = sanitizeStringArray(params.tags);
79
+ return {
80
+ title: titleResult.value,
81
+ summary: summaryResult.value,
82
+ whenToUse: whenToUseResult.value,
83
+ supplement: supplementResult.value,
84
+ rules: rulesResult.values,
85
+ examples: examplesResult.values,
86
+ tags: tagsResult.values,
87
+ changed: titleResult.changed ||
88
+ summaryResult.changed ||
89
+ whenToUseResult.changed ||
90
+ supplementResult.changed ||
91
+ rulesResult.changed ||
92
+ examplesResult.changed ||
93
+ tagsResult.changed,
94
+ };
95
+ }
96
+ function containsHighlySensitiveContent(text) {
97
+ const highRiskPatterns = [
98
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----/u,
99
+ /bearer\s+[a-z0-9._=-]{12,}/iu,
100
+ /\b(?:sk-[a-zA-Z0-9]{16,}|AKIA[0-9A-Z]{16}|AIza[0-9A-Za-z\-_]{20,})\b/u,
101
+ /(?:api[_ -]?key|access[_ -]?token|refresh[_ -]?token|password|secret)\s*[:=]\s*[^\s,;]{8,}/iu,
102
+ ];
103
+ return highRiskPatterns.some((pattern) => pattern.test(text));
104
+ }
105
+ function buildSkillFingerprint(params) {
106
+ const normalized = {
107
+ title: normalizeForFingerprint(params.title),
108
+ summary: normalizeForFingerprint(params.summary),
109
+ whenToUse: normalizeForFingerprint(params.whenToUse),
110
+ supplement: normalizeForFingerprint(params.supplement),
111
+ rules: normalizeForComparison(params.rules),
112
+ examples: normalizeForComparison(params.examples),
113
+ tags: normalizeForComparison(params.tags),
114
+ };
115
+ return createHash("sha256").update(JSON.stringify(normalized)).digest("hex");
116
+ }
117
+ function parseFrontmatterValue(content, key) {
118
+ const match = content.match(new RegExp(`^${key}:\\s*"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"`, "m"));
119
+ if (match) {
120
+ return match[1].replace(/\\"/g, '"').replace(/\\n/g, "\n");
121
+ }
122
+ return null;
123
+ }
124
+ function parseTimestampFromExistingSkill(content, key) {
125
+ const value = parseFrontmatterValue(content, key);
126
+ if (!value) {
127
+ return null;
128
+ }
129
+ return ISO_DATE_PATTERN.test(value) ? value : null;
130
+ }
131
+ async function findDuplicateSkillByFingerprint(targetFingerprint) {
132
+ try {
133
+ const entries = await fs.readdir(SELF_EVOLVED_SKILL_ROOT, { withFileTypes: true });
134
+ for (const entry of entries) {
135
+ if (!entry.isDirectory() || !entry.name.startsWith("evolving-")) {
136
+ continue;
137
+ }
138
+ const skillFilePath = path.join(SELF_EVOLVED_SKILL_ROOT, entry.name, "SKILL.md");
139
+ try {
140
+ const existingContent = await fs.readFile(skillFilePath, "utf-8");
141
+ const fingerprint = parseFrontmatterValue(existingContent, "fingerprint");
142
+ if (fingerprint && fingerprint === targetFingerprint) {
143
+ return {
144
+ path: skillFilePath,
145
+ slug: entry.name.replace(/^evolving-/u, ""),
146
+ };
147
+ }
148
+ }
149
+ catch (error) {
150
+ if (error?.code !== "ENOENT") {
151
+ throw error;
152
+ }
153
+ }
154
+ }
155
+ }
156
+ catch (error) {
157
+ if (error?.code !== "ENOENT") {
158
+ throw error;
159
+ }
160
+ }
161
+ return null;
162
+ }
163
+ function buildSkillMarkdown(params) {
164
+ const description = `${params.summary}\n\nWhen to use: ${params.whenToUse}`
165
+ .replace(/"/g, '\\"')
166
+ .replace(/\r?\n/g, "\\n");
167
+ const lines = [
168
+ "---",
169
+ `name: "${params.title.replace(/"/g, '\\"')}"`,
170
+ `description: "${description}"`,
171
+ `fingerprint: "${params.fingerprint}"`,
172
+ `created_at: "${params.createdAt}"`,
173
+ ];
174
+ if (params.updatedAt) {
175
+ lines.push(`updated_at: "${params.updatedAt}"`);
176
+ }
177
+ lines.push("---", "", `# ${params.title}`, "", "## Metadata", `- Created At: ${params.createdAt}`);
178
+ if (params.updatedAt) {
179
+ lines.push(`- Updated At: ${params.updatedAt}`);
180
+ }
181
+ lines.push("", "## Rules");
182
+ for (const rule of params.rules) {
183
+ lines.push(`- ${rule}`);
184
+ }
185
+ if (params.examples.length > 0) {
186
+ lines.push("", "## Examples");
187
+ for (const example of params.examples) {
188
+ lines.push(`- ${example}`);
189
+ }
190
+ }
191
+ if (params.supplement) {
192
+ lines.push("", "## Supplement", params.supplement);
193
+ }
194
+ if (params.tags.length > 0) {
195
+ lines.push("", "## Tags", params.tags.map((tag) => `- ${tag}`).join("\n"));
196
+ }
197
+ lines.push("");
198
+ return lines.join("\n");
199
+ }
200
+ export const saveSelfEvolutionSkillTool = {
201
+ name: "save_self_evolution_skill",
202
+ label: "Save Self Evolution Skill",
203
+ description: "将可复用的经验/脚本/教训等保存为skill技能,供下次执行类似任务时参考。仅用于通用、可复用的场景。仅当自进化开启时可调用本工具。",
204
+ parameters: {
205
+ type: "object",
206
+ properties: {
207
+ title: {
208
+ type: "string",
209
+ description: "所学技能的简短标题。**必须为小写字母/数字/中划线。**",
210
+ },
211
+ summary: {
212
+ type: "string",
213
+ description: "技能的概括性总结,不要太长。",
214
+ },
215
+ when_to_use: {
216
+ type: "string",
217
+ description: "描述在未来任务中什么情况/哪些条件下使用此技能,描述尽量精准。",
218
+ },
219
+ rules: {
220
+ type: "array",
221
+ items: { type: "string" },
222
+ description: "具体、可复用的规则或checklist。",
223
+ },
224
+ examples: {
225
+ type: "array",
226
+ items: { type: "string" },
227
+ description: "陷阱示例或正确模式示例,可选",
228
+ },
229
+ tags: {
230
+ type: "array",
231
+ items: { type: "string" },
232
+ description: "用于未来发现的标签,可选。",
233
+ },
234
+ supplement: {
235
+ type: "string",
236
+ description: "补充说明。将其他想补充但不属于固定字段的内容放在这里。可选。",
237
+ },
238
+ },
239
+ required: ["title", "summary", "when_to_use", "rules"],
240
+ },
241
+ async execute(_toolCallId, params) {
242
+ if (!(await selfEvolutionManager.isEnabled())) {
243
+ throw new Error("Self-evolution is currently disabled by the user.");
244
+ }
245
+ const sessionContext = getCurrentSessionContext();
246
+ if (!sessionContext) {
247
+ throw new Error("No active XY session found. This tool can only run during an active conversation.");
248
+ }
249
+ const title = typeof params.title === "string" ? params.title.trim() : "";
250
+ const summary = typeof params.summary === "string" ? params.summary.trim() : "";
251
+ const whenToUse = typeof params.when_to_use === "string" ? params.when_to_use.trim() : "";
252
+ const supplement = typeof params.supplement === "string" ? params.supplement.trim() : "";
253
+ const rawRules = normalizeStringArray(params.rules);
254
+ const rawExamples = normalizeStringArray(params.examples);
255
+ const rawTags = normalizeStringArray(params.tags);
256
+ if (!title || !summary || !whenToUse || rawRules.length === 0) {
257
+ throw new Error("Missing required fields. title, summary, when_to_use, and at least one rule are required.");
258
+ }
259
+ if (title.length < 6 || summary.length < 10 || whenToUse.length < 10) {
260
+ throw new Error("Skill content is too short. Provide a reusable title, summary, and usage guidance.");
261
+ }
262
+ const sanitized = sanitizeSkillContent({
263
+ title,
264
+ summary,
265
+ whenToUse,
266
+ supplement,
267
+ rules: rawRules,
268
+ examples: rawExamples,
269
+ tags: rawTags,
270
+ });
271
+ const combinedText = [
272
+ sanitized.title,
273
+ sanitized.summary,
274
+ sanitized.whenToUse,
275
+ sanitized.supplement,
276
+ ...sanitized.rules,
277
+ ...sanitized.examples,
278
+ ...sanitized.tags,
279
+ ].join("\n");
280
+ if (containsHighlySensitiveContent(combinedText)) {
281
+ throw new Error("Skill content appears to contain sensitive or environment-specific data and was rejected.");
282
+ }
283
+ const slug = slugifyTitle(sanitized.title);
284
+ if (!slug) {
285
+ throw new Error("Title could not be normalized into a valid skill name.");
286
+ }
287
+ const skillDir = path.join(SELF_EVOLVED_SKILL_ROOT, `evolving-${slug}`);
288
+ const skillFilePath = path.join(skillDir, "SKILL.md");
289
+ const fingerprint = buildSkillFingerprint({
290
+ title: sanitized.title,
291
+ summary: sanitized.summary,
292
+ whenToUse: sanitized.whenToUse,
293
+ supplement: sanitized.supplement,
294
+ rules: sanitized.rules,
295
+ examples: sanitized.examples,
296
+ tags: sanitized.tags,
297
+ });
298
+ const duplicateSkill = await findDuplicateSkillByFingerprint(fingerprint);
299
+ if (duplicateSkill && duplicateSkill.path !== skillFilePath) {
300
+ return {
301
+ content: [
302
+ {
303
+ type: "text",
304
+ text: JSON.stringify({
305
+ success: true,
306
+ deduped: true,
307
+ sanitized: sanitized.changed,
308
+ skillName: duplicateSkill.slug,
309
+ path: duplicateSkill.path,
310
+ message: "A semantically identical self-evolved skill already exists.",
311
+ }),
312
+ },
313
+ ],
314
+ };
315
+ }
316
+ const nowIso = new Date().toISOString();
317
+ let createdAt = nowIso;
318
+ let updatedAt;
319
+ try {
320
+ const existingContent = await fs.readFile(skillFilePath, "utf-8");
321
+ const existingFingerprint = parseFrontmatterValue(existingContent, "fingerprint");
322
+ const existingCreatedAt = parseTimestampFromExistingSkill(existingContent, "created_at");
323
+ createdAt = existingCreatedAt ?? nowIso;
324
+ if (existingFingerprint === fingerprint) {
325
+ return {
326
+ content: [
327
+ {
328
+ type: "text",
329
+ text: JSON.stringify({
330
+ success: true,
331
+ deduped: true,
332
+ sanitized: sanitized.changed,
333
+ skillName: slug,
334
+ path: skillFilePath,
335
+ createdAt,
336
+ message: "An identical self-evolved skill already exists.",
337
+ }),
338
+ },
339
+ ],
340
+ };
341
+ }
342
+ updatedAt = nowIso;
343
+ }
344
+ catch (error) {
345
+ if (error?.code !== "ENOENT") {
346
+ throw error;
347
+ }
348
+ }
349
+ const nextContent = buildSkillMarkdown({
350
+ title: sanitized.title,
351
+ summary: sanitized.summary,
352
+ whenToUse: sanitized.whenToUse,
353
+ supplement: sanitized.supplement,
354
+ rules: sanitized.rules,
355
+ examples: sanitized.examples,
356
+ tags: sanitized.tags,
357
+ fingerprint,
358
+ createdAt,
359
+ updatedAt,
360
+ });
361
+ const nextHash = createHash("sha256").update(nextContent).digest("hex");
362
+ await fs.mkdir(skillDir, { recursive: true });
363
+ try {
364
+ const existingContent = await fs.readFile(skillFilePath, "utf-8");
365
+ const existingHash = createHash("sha256").update(existingContent).digest("hex");
366
+ if (existingHash === nextHash) {
367
+ return {
368
+ content: [
369
+ {
370
+ type: "text",
371
+ text: JSON.stringify({
372
+ success: true,
373
+ deduped: true,
374
+ sanitized: sanitized.changed,
375
+ skillName: slug,
376
+ path: skillFilePath,
377
+ createdAt,
378
+ message: "An identical self-evolved skill already exists.",
379
+ }),
380
+ },
381
+ ],
382
+ };
383
+ }
384
+ }
385
+ catch (error) {
386
+ if (error?.code !== "ENOENT") {
387
+ throw error;
388
+ }
389
+ }
390
+ await fs.writeFile(skillFilePath, nextContent, "utf-8");
391
+ return {
392
+ content: [
393
+ {
394
+ type: "text",
395
+ text: JSON.stringify({
396
+ success: true,
397
+ deduped: false,
398
+ sanitized: sanitized.changed,
399
+ skillName: slug,
400
+ path: skillFilePath,
401
+ sessionId: sessionContext.sessionId,
402
+ createdAt,
403
+ updatedAt,
404
+ message: updatedAt
405
+ ? "Self-evolved skill updated successfully."
406
+ : "Self-evolved skill saved successfully.",
407
+ }),
408
+ },
409
+ ],
410
+ };
411
+ },
412
+ };
@@ -0,0 +1,27 @@
1
+ /**
2
+ * schema-tool-factory: 创建 get_xxx_tool_schema 工具。
3
+ * 每个工具在被 LLM 调用时,返回其所包含的子工具的 name / description / parameters schema。
4
+ */
5
+ interface SchemaToolOptions {
6
+ name: string;
7
+ label: string;
8
+ description: string;
9
+ tools: any[];
10
+ }
11
+ export declare function createSchemaTool(options: SchemaToolOptions): {
12
+ name: string;
13
+ label: string;
14
+ description: string;
15
+ parameters: {
16
+ type: "object";
17
+ properties: {};
18
+ required: string[];
19
+ };
20
+ execute(_toolCallId: string, _params: any): Promise<{
21
+ content: {
22
+ type: "text";
23
+ text: string;
24
+ }[];
25
+ }>;
26
+ };
27
+ export {};
@@ -0,0 +1,32 @@
1
+ /**
2
+ * schema-tool-factory: 创建 get_xxx_tool_schema 工具。
3
+ * 每个工具在被 LLM 调用时,返回其所包含的子工具的 name / description / parameters schema。
4
+ */
5
+ export function createSchemaTool(options) {
6
+ const { name, label, description, tools } = options;
7
+ return {
8
+ name,
9
+ label,
10
+ description,
11
+ parameters: {
12
+ type: "object",
13
+ properties: {},
14
+ required: [],
15
+ },
16
+ async execute(_toolCallId, _params) {
17
+ const schemas = tools.map((t) => ({
18
+ name: t.name,
19
+ description: t.description,
20
+ parameters: t.parameters,
21
+ }));
22
+ return {
23
+ content: [
24
+ {
25
+ type: "text",
26
+ text: JSON.stringify(schemas, null, 2),
27
+ },
28
+ ],
29
+ };
30
+ },
31
+ };
32
+ }