@ynhcj/xiaoyi-channel 1.1.19 → 1.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +0 -5
- package/dist/index.js +107 -10
- 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 +14 -0
- package/dist/src/provider.js +319 -27
- package/dist/src/self-evolution-handler.d.ts +1 -0
- package/dist/src/self-evolution-handler.js +47 -0
- 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 +91 -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/call-device-tool.js +4 -0
- package/dist/src/tools/get-email-tool-schema.d.ts +16 -0
- package/dist/src/tools/get-email-tool-schema.js +9 -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/tools/query-app-message-tool.d.ts +4 -0
- package/dist/src/tools/query-app-message-tool.js +138 -0
- package/dist/src/tools/query-memory-data-tool.d.ts +4 -0
- package/dist/src/tools/query-memory-data-tool.js +154 -0
- package/dist/src/tools/query-todo-task-tool.d.ts +4 -0
- package/dist/src/tools/query-todo-task-tool.js +133 -0
- package/dist/src/tools/save-self-evolution-skill-tool.d.ts +1 -0
- package/dist/src/tools/save-self-evolution-skill-tool.js +412 -0
- package/dist/src/tools/session-manager.js +2 -0
- package/dist/src/utils/runtime-manager.js +24 -2
- package/dist/src/utils/self-evolution-manager.d.ts +5 -0
- package/dist/src/utils/self-evolution-manager.js +47 -0
- package/dist/src/utils/tool-call-nudge-manager.d.ts +16 -0
- package/dist/src/utils/tool-call-nudge-manager.js +47 -0
- package/dist/src/websocket.js +18 -0
- package/package.json +2 -2
|
@@ -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/workspace/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
|
+
};
|
|
@@ -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
|
/**
|
|
@@ -28,8 +28,30 @@ export async function saveRuntimeInfo(webSocketSessionId, conversationId, taskId
|
|
|
28
28
|
}
|
|
29
29
|
try {
|
|
30
30
|
await ensureDirectoryExists(RUNTIME_FILE);
|
|
31
|
-
const
|
|
32
|
-
|
|
31
|
+
const updates = {
|
|
32
|
+
SESSION_ID: webSocketSessionId,
|
|
33
|
+
CONVERSATION_ID: conversationId,
|
|
34
|
+
TASK_ID: taskId,
|
|
35
|
+
};
|
|
36
|
+
let lines = [];
|
|
37
|
+
try {
|
|
38
|
+
const content = await fs.readFile(RUNTIME_FILE, "utf-8");
|
|
39
|
+
lines = content.split("\n");
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// File doesn't exist yet
|
|
43
|
+
}
|
|
44
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
45
|
+
const index = lines.findIndex((line) => line.startsWith(`${key}=`));
|
|
46
|
+
if (index !== -1) {
|
|
47
|
+
lines[index] = `${key}=${value}`;
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
lines.push(`${key}=${value}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const result = lines.filter((line) => line.trim() !== "").join("\n") + "\n";
|
|
54
|
+
await fs.writeFile(RUNTIME_FILE, result, "utf-8");
|
|
33
55
|
logger.log(`[RuntimeManager] ✅ Saved runtime info to .xiaoyiruntime`);
|
|
34
56
|
logger.log(`[RuntimeManager] - SESSION_ID: ${webSocketSessionId}`);
|
|
35
57
|
logger.log(`[RuntimeManager] - CONVERSATION_ID: ${conversationId}`);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
const SELF_EVOLUTION_ENV_FILE = "/home/sandbox/.openclaw/.xiaoyiruntime";
|
|
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,16 @@
|
|
|
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
|
+
private getSessionState;
|
|
10
|
+
recordToolCall(sessionKey: string): RecordToolCallResult;
|
|
11
|
+
tryMarkKeywordNudge(sessionKey: string): boolean;
|
|
12
|
+
clearSession(sessionKey: string): void;
|
|
13
|
+
}
|
|
14
|
+
export declare const TOOL_CALL_NUDGE_THRESHOLD = 6;
|
|
15
|
+
export declare const toolCallNudgeManager: ToolCallNudgeManager;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const DEFAULT_TOOL_CALL_NUDGE_THRESHOLD = 6;
|
|
2
|
+
class ToolCallNudgeManager {
|
|
3
|
+
threshold;
|
|
4
|
+
sessions = new Map();
|
|
5
|
+
constructor(threshold = DEFAULT_TOOL_CALL_NUDGE_THRESHOLD) {
|
|
6
|
+
this.threshold = threshold;
|
|
7
|
+
}
|
|
8
|
+
getSessionState(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
|
+
return state;
|
|
18
|
+
}
|
|
19
|
+
recordToolCall(sessionKey) {
|
|
20
|
+
const state = this.getSessionState(sessionKey);
|
|
21
|
+
state.count += 1;
|
|
22
|
+
if (!state.nudged && state.count >= this.threshold) {
|
|
23
|
+
state.nudged = true;
|
|
24
|
+
return {
|
|
25
|
+
count: state.count,
|
|
26
|
+
shouldNudge: true,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
count: state.count,
|
|
31
|
+
shouldNudge: false,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
tryMarkKeywordNudge(sessionKey) {
|
|
35
|
+
const state = this.getSessionState(sessionKey);
|
|
36
|
+
if (state.nudged) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
state.nudged = true;
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
clearSession(sessionKey) {
|
|
43
|
+
this.sessions.delete(sessionKey);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export const TOOL_CALL_NUDGE_THRESHOLD = DEFAULT_TOOL_CALL_NUDGE_THRESHOLD;
|
|
47
|
+
export const toolCallNudgeManager = new ToolCallNudgeManager();
|
package/dist/src/websocket.js
CHANGED
|
@@ -394,6 +394,18 @@ export class XYWebSocketManager extends EventEmitter {
|
|
|
394
394
|
taskId: a2aRequest.params?.id, // 新的 taskId(点击推送时生成)
|
|
395
395
|
});
|
|
396
396
|
}
|
|
397
|
+
else if (item.header?.namespace === "AgentEvent" && item.header?.name === "ClawSelfEvolutionState") {
|
|
398
|
+
console.log("[XY] ClawSelfEvolutionState event detected, emitting self-evolution-event");
|
|
399
|
+
this.emit("self-evolution-event", {
|
|
400
|
+
event: item,
|
|
401
|
+
});
|
|
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
|
+
}
|
|
397
409
|
}
|
|
398
410
|
}
|
|
399
411
|
return;
|
|
@@ -448,6 +460,12 @@ export class XYWebSocketManager extends EventEmitter {
|
|
|
448
460
|
taskId: inboundMsg.taskId || a2aRequest.params?.id,
|
|
449
461
|
});
|
|
450
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
|
+
}
|
|
451
469
|
}
|
|
452
470
|
}
|
|
453
471
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ynhcj/xiaoyi-channel",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.21",
|
|
4
4
|
"description": "OpenClaw Xiaoyi Channel plugin - Xiaoyi A2A protocol integration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"openclaw.plugin.json"
|
|
17
17
|
],
|
|
18
18
|
"scripts": {
|
|
19
|
-
"build": "tsc"
|
|
19
|
+
"build": "node ./node_modules/typescript/bin/tsc"
|
|
20
20
|
},
|
|
21
21
|
"keywords": [
|
|
22
22
|
"openclaw",
|