@yulailai/openclaw-plugin-self-growth 3.1.6

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.
@@ -0,0 +1,393 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as path from 'path';
3
+ export class SkillOptimizer {
4
+ skillsDir;
5
+ llmUrl;
6
+ llmModel;
7
+ scores;
8
+ pendingUpgradesPath;
9
+ constructor(storageDir = './memory', llmUrl = 'http://127.0.0.1:1234/v1', llmModel = 'qwen/qwen3.5-9b') {
10
+ this.skillsDir = path.join(storageDir, 'skills');
11
+ this.llmUrl = llmUrl;
12
+ this.llmModel = llmModel;
13
+ this.scores = new Map();
14
+ this.pendingUpgradesPath = path.join(this.skillsDir, 'pending_upgrades.json');
15
+ }
16
+ async init() {
17
+ await fs.mkdir(this.skillsDir, { recursive: true });
18
+ }
19
+ async generateSkill(taskRecord) {
20
+ const skillName = taskRecord.name;
21
+ const skillDir = path.join(this.skillsDir, skillName);
22
+ const skillFilePath = path.join(skillDir, 'SKILL.md');
23
+ await fs.mkdir(skillDir, { recursive: true });
24
+ const content = this.buildSkillMarkdown(skillName, taskRecord);
25
+ try {
26
+ await fs.writeFile(skillFilePath, content, 'utf-8');
27
+ console.log(`[SkillOptimizer] ✨ 成功生成 SKILL.md: ${skillFilePath}`);
28
+ return skillFilePath;
29
+ }
30
+ catch (error) {
31
+ console.error(`[SkillOptimizer] ❌ 生成 SKILL.md 失败 [${skillName}]:`, error);
32
+ throw error;
33
+ }
34
+ }
35
+ async batchGenerate(readyTasks) {
36
+ const generatedFiles = [];
37
+ for (const task of readyTasks) {
38
+ try {
39
+ const filePath = await this.generateSkill(task);
40
+ generatedFiles.push(filePath);
41
+ }
42
+ catch (error) {
43
+ console.warn(`[SkillOptimizer] ⚠️ 跳过任务 [${task.name}]:`, error);
44
+ }
45
+ }
46
+ return generatedFiles;
47
+ }
48
+ async batchGenerateFromLLM(tasks) {
49
+ const generatedFiles = [];
50
+ for (const task of tasks) {
51
+ try {
52
+ const taskRecord = {
53
+ name: task.taskName,
54
+ category: "自动生成",
55
+ count: 1,
56
+ steps: task.steps,
57
+ history: [{ time: new Date().toISOString(), steps: task.steps }],
58
+ readyForSkill: true,
59
+ runIds: []
60
+ };
61
+ const filePath = await this.generateSkill(taskRecord);
62
+ generatedFiles.push(filePath);
63
+ }
64
+ catch (error) {
65
+ console.warn(`[SkillOptimizer] ⚠️ 跳过任务 [${task.taskName}]:`, error);
66
+ }
67
+ }
68
+ return generatedFiles;
69
+ }
70
+ async listGenerated() {
71
+ try {
72
+ const entries = await fs.readdir(this.skillsDir, { withFileTypes: true });
73
+ return entries
74
+ .filter(e => e.isDirectory())
75
+ .filter(e => {
76
+ const skillFile = path.join(this.skillsDir, e.name, 'SKILL.md');
77
+ return fs.access(skillFile).then(() => true).catch(() => false);
78
+ })
79
+ .map(e => e.name);
80
+ }
81
+ catch {
82
+ return [];
83
+ }
84
+ }
85
+ async getExistingSkill(skillName) {
86
+ const skillFilePath = path.join(this.skillsDir, skillName, 'SKILL.md');
87
+ try {
88
+ return await fs.readFile(skillFilePath, 'utf-8');
89
+ }
90
+ catch {
91
+ return null;
92
+ }
93
+ }
94
+ recordExecution(skillName, success, durationMs, hadManualFix = false) {
95
+ if (!this.scores.has(skillName)) {
96
+ this.scores.set(skillName, { recentScores: [], optimizeCount: 0, usageCount: 0, lastUsed: Date.now() });
97
+ }
98
+ const record = this.scores.get(skillName);
99
+ record.usageCount += 1;
100
+ record.lastUsed = Date.now();
101
+ let score = 100;
102
+ if (!success)
103
+ score -= 30;
104
+ if (durationMs > 60000)
105
+ score -= 15;
106
+ if (hadManualFix)
107
+ score -= 25;
108
+ record.recentScores.push(score);
109
+ if (record.recentScores.length > 10) {
110
+ record.recentScores.shift();
111
+ }
112
+ const recent = record.recentScores;
113
+ if (recent.length >= 3 && recent.slice(-3).every(s => s < 80)) {
114
+ return true;
115
+ }
116
+ return false;
117
+ }
118
+ async calculateComprehensiveScore(skillName) {
119
+ const record = this.scores.get(skillName) || { recentScores: [], optimizeCount: 0, usageCount: 0, lastUsed: 0 };
120
+ const content = await this.getExistingSkill(skillName);
121
+ let reuse = 0;
122
+ if (record.usageCount >= 10)
123
+ reuse += 20;
124
+ else if (record.usageCount >= 5)
125
+ reuse += 15;
126
+ else if (record.usageCount >= 1)
127
+ reuse += 10;
128
+ const daysSinceUsed = (Date.now() - record.lastUsed) / 86400000;
129
+ if (daysSinceUsed < 7)
130
+ reuse += 10;
131
+ else if (daysSinceUsed < 30)
132
+ reuse += 5;
133
+ if (daysSinceUsed < 1)
134
+ reuse += 10;
135
+ let quality = 0;
136
+ const recentScores = record.recentScores.slice(-10);
137
+ if (recentScores.length > 0) {
138
+ const avgScore = recentScores.reduce((a, b) => a + b, 0) / recentScores.length;
139
+ quality = Math.round(avgScore * 0.4);
140
+ }
141
+ else {
142
+ quality = 20;
143
+ }
144
+ let structure = 0;
145
+ if (content) {
146
+ if (/## 🔄 核心工作流/.test(content))
147
+ structure += 5;
148
+ if (/## ✅ 完成标准/.test(content))
149
+ structure += 5;
150
+ if (/## ⚠️ 注意事项|## 🚫 不适用/.test(content))
151
+ structure += 5;
152
+ if (content.split('\n').filter(l => /^\d+\./.test(l.trim())).length >= 3)
153
+ structure += 5;
154
+ }
155
+ return { total: reuse + quality + structure, reuse, quality, structure };
156
+ }
157
+ async evaluateAndCleanup() {
158
+ const skillNames = await this.listGenerated();
159
+ const suggestions = [];
160
+ const now = Date.now();
161
+ for (const skillName of skillNames) {
162
+ const score = await this.calculateComprehensiveScore(skillName);
163
+ const record = this.scores.get(skillName);
164
+ if (score.total < 50) {
165
+ const skillDir = path.join(this.skillsDir, skillName);
166
+ try {
167
+ await fs.rm(skillDir, { recursive: true });
168
+ }
169
+ catch { }
170
+ console.log(`[SkillOptimizer] 🗑️ 技能评分过低(${score.total}分),已删除: ${skillName}`);
171
+ continue;
172
+ }
173
+ if (score.total < 80) {
174
+ const content = await this.getExistingSkill(skillName);
175
+ if (content && this.parseStatus(content) === 'watch') {
176
+ const skillDir = path.join(this.skillsDir, skillName);
177
+ try {
178
+ await fs.rm(skillDir, { recursive: true });
179
+ }
180
+ catch { }
181
+ console.log(`[SkillOptimizer] 🗑️ 观察期技能评分不足(${score.total}分),已删除: ${skillName}`);
182
+ continue;
183
+ }
184
+ }
185
+ const daysSinceUsed = (now - (record?.lastUsed || 0)) / 86400000;
186
+ if (record && record.usageCount >= 3 && daysSinceUsed <= 7 && score.total >= 50 && score.total < 80) {
187
+ const suggestion = await this.generateUpgradeSuggestion(skillName, score.total);
188
+ suggestions.push(suggestion);
189
+ console.log(`[SkillOptimizer] 💡 生成升级建议: ${skillName}(评分:${score.total} 使用:${record.usageCount}次)`);
190
+ }
191
+ }
192
+ if (suggestions.length > 0) {
193
+ await fs.writeFile(this.pendingUpgradesPath, JSON.stringify(suggestions, null, 2), 'utf-8');
194
+ }
195
+ else {
196
+ try {
197
+ await fs.unlink(this.pendingUpgradesPath);
198
+ }
199
+ catch { }
200
+ }
201
+ return suggestions;
202
+ }
203
+ async generateUpgradeSuggestion(skillName, currentScore) {
204
+ const content = await this.getExistingSkill(skillName);
205
+ let suggestion = `建议优化"${skillName}"技能(当前评分${currentScore}),可考虑:简化冗余步骤、补充完成标准、添加不适用场景说明。`;
206
+ if (content) {
207
+ try {
208
+ const prompt = `分析以下技能文件,给出1-2条具体的优化建议(每条约20字):
209
+ ${content.substring(0, 1500)}
210
+
211
+ 评分较低的原因可能有:步骤冗余、缺少完成标准、缺少边界说明。请直接输出建议:`;
212
+ const llmSuggestion = await this.callLLM(prompt, 200);
213
+ if (llmSuggestion)
214
+ suggestion = llmSuggestion;
215
+ }
216
+ catch { }
217
+ }
218
+ return { skillName, currentScore, suggestion, generatedAt: new Date().toISOString(), status: 'pending' };
219
+ }
220
+ async getPendingUpgrades() {
221
+ try {
222
+ const data = await fs.readFile(this.pendingUpgradesPath, 'utf-8');
223
+ return JSON.parse(data);
224
+ }
225
+ catch {
226
+ return [];
227
+ }
228
+ }
229
+ async updateUpgradeStatus(skillName, status) {
230
+ const upgrades = await this.getPendingUpgrades();
231
+ const updated = upgrades.filter(u => u.skillName !== skillName);
232
+ if (updated.length === 0) {
233
+ try {
234
+ await fs.unlink(this.pendingUpgradesPath);
235
+ }
236
+ catch { }
237
+ }
238
+ else {
239
+ await fs.writeFile(this.pendingUpgradesPath, JSON.stringify(updated, null, 2), 'utf-8');
240
+ }
241
+ }
242
+ getAverageScore(skillName) {
243
+ const record = this.scores.get(skillName);
244
+ if (!record || record.recentScores.length === 0)
245
+ return 100;
246
+ const recent = record.recentScores.slice(-5);
247
+ return recent.reduce((sum, s) => sum + s, 0) / recent.length;
248
+ }
249
+ resetScores(skillName) {
250
+ const record = this.scores.get(skillName);
251
+ if (record) {
252
+ record.recentScores = [];
253
+ record.optimizeCount += 1;
254
+ }
255
+ }
256
+ async detectChanges(skillName, recentLogs) {
257
+ const prompt = `分析以下对话日志,判断"${skillName}"技能的使用模式是否发生了变化。如果有变化,简要说明;如果没有,回复"无变化"。\n\n对话日志:\n${recentLogs.slice(0, 3000)}`;
258
+ const response = await this.callLLM(prompt, 300);
259
+ return response && response !== '无变化' ? response : null;
260
+ }
261
+ async diagnoseAndPatch(skillName, skillContent, recentLogs) {
262
+ const prompt = `分析以下日志,诊断"${skillName}"技能的缺陷:\n1. 哪些步骤经常失败?原因是什么?\n2. 用户在对话中如何进行手动修正?\n3. 提供精确的修改方案,只修改有问题的部分。\n\n旧版 SKILL.md:\n${skillContent.slice(0, 3000)}\n\n最近对话日志:\n${recentLogs.slice(0, 3000)}\n\n返回 JSON:{"problemsFound": true/false, "diagnosis": "...", "patchInstruction": "..."}`;
263
+ const response = await this.callLLM(prompt, 1000);
264
+ try {
265
+ return JSON.parse(response || '{}');
266
+ }
267
+ catch {
268
+ return { problemsFound: false, diagnosis: '', patchInstruction: '' };
269
+ }
270
+ }
271
+ async applyPatch(skillPath, patchInstruction) {
272
+ if (!patchInstruction)
273
+ return false;
274
+ try {
275
+ const oldContent = await fs.readFile(skillPath, 'utf-8');
276
+ const prompt = `根据以下指令修改 SKILL.md 文件。保留 YAML frontmatter 不变,只修改 Markdown 正文。\n原始文件:\n${oldContent}\n\n修改指令:\n${patchInstruction}\n\n返回完整的、可直接保存的 SKILL.md 内容。`;
277
+ let newContent = await this.callLLM(prompt, 3000);
278
+ if (newContent && newContent.length > 200) {
279
+ const frontmatterEnd = oldContent.indexOf('---', 4);
280
+ if (frontmatterEnd !== -1) {
281
+ const oldFrontmatter = oldContent.substring(0, frontmatterEnd + 3);
282
+ const newFrontmatterEnd = newContent.indexOf('---', 4);
283
+ if (newFrontmatterEnd !== -1) {
284
+ const newFrontmatter = newContent.substring(0, newFrontmatterEnd + 3);
285
+ newContent = oldFrontmatter + newContent.substring(newFrontmatter.length);
286
+ }
287
+ }
288
+ await fs.writeFile(skillPath, newContent, 'utf-8');
289
+ return true;
290
+ }
291
+ }
292
+ catch (error) {
293
+ console.error('[SkillOptimizer] ❌ 修复失败:', error);
294
+ }
295
+ return false;
296
+ }
297
+ async evaluateAndOptimize(recentLogs) {
298
+ const skillNames = await this.listGenerated();
299
+ let optimizedCount = 0;
300
+ for (const skillName of skillNames) {
301
+ const avgScore = this.getAverageScore(skillName);
302
+ if (avgScore >= 80)
303
+ continue;
304
+ const skillContent = await this.getExistingSkill(skillName);
305
+ if (!skillContent)
306
+ continue;
307
+ try {
308
+ const diagnosis = await this.diagnoseAndPatch(skillName, skillContent, recentLogs);
309
+ if (!diagnosis.problemsFound)
310
+ continue;
311
+ const skillPath = path.join(this.skillsDir, skillName, 'SKILL.md');
312
+ const patched = await this.applyPatch(skillPath, diagnosis.patchInstruction);
313
+ if (patched) {
314
+ this.resetScores(skillName);
315
+ optimizedCount++;
316
+ }
317
+ }
318
+ catch (err) {
319
+ console.error(`[SkillOptimizer] ❌ 技能 "${skillName}" 优化失败:`, err);
320
+ }
321
+ }
322
+ console.log(`[SkillOptimizer] 📊 本轮共优化 ${optimizedCount} 个技能`);
323
+ return optimizedCount;
324
+ }
325
+ parseStatus(content) {
326
+ const match = content.match(/status: (.+)/);
327
+ return match ? match[1].trim() : 'active';
328
+ }
329
+ buildSkillMarkdown(name, record) {
330
+ const stepsMarkdown = this.optimizeSteps(record.steps)
331
+ .map((step, index) => `${index + 1}. ${step}`)
332
+ .join('\n');
333
+ return `---
334
+ name: ${name}
335
+ description: >
336
+ 自动生成的技能。触发条件:用户提到'执行${name}'或相关描述时,自动调用此技能。
337
+ 此技能由 self-growth 插件自动生成,基于用户 ${record.count} 次执行此任务的流程。
338
+ category: ${record.category}
339
+ user-invocable: true
340
+ disable-model-invocation: false
341
+ ---
342
+
343
+ # ${name}
344
+
345
+ ## 🎯 触发条件
346
+ 当用户提到执行"${name}"任务时,自动调用此技能。
347
+
348
+ ## 🔄 核心工作流
349
+ ${stepsMarkdown}
350
+
351
+ ## ✅ 完成标准
352
+ - 所有步骤已按顺序执行完毕
353
+ - 每步结果已向用户汇报
354
+
355
+ ## 📤 输出与交付
356
+ - 每步执行完成后,简要汇报结果
357
+ - 全部完成后,输出任务总结
358
+
359
+ ## 📝 技能信息
360
+ - **生成时间**: ${new Date().toISOString()}
361
+ - **任务类别**: ${record.category}
362
+ - **步骤数量**: ${record.steps.length}
363
+ - **执行次数**: ${record.count}
364
+ - **生成插件**: self-growth
365
+ `;
366
+ }
367
+ optimizeSteps(steps) {
368
+ if (!steps || steps.length === 0)
369
+ return [];
370
+ let optimized = steps.map(step => step.trim()).filter(step => step.length > 0);
371
+ return Array.from(new Set(optimized));
372
+ }
373
+ async callLLM(prompt, maxTokens = 800) {
374
+ try {
375
+ const response = await fetch(`${this.llmUrl}/chat/completions`, {
376
+ method: 'POST',
377
+ headers: { 'Content-Type': 'application/json' },
378
+ body: JSON.stringify({
379
+ messages: [{ role: 'user', content: prompt }],
380
+ temperature: 0.3,
381
+ max_tokens: maxTokens
382
+ })
383
+ });
384
+ const data = await response.json();
385
+ return data.choices?.[0]?.message?.content || null;
386
+ }
387
+ catch (error) {
388
+ console.error('[SkillOptimizer] LLM 调用失败:', error);
389
+ return null;
390
+ }
391
+ }
392
+ }
393
+ //# sourceMappingURL=skill-optimizer.js.map
@@ -0,0 +1,220 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as path from 'path';
3
+ import { loadActivation } from './payment';
4
+ import { getCachedToken, loginAndGetToken } from './auth-client';
5
+ export class SyncClient {
6
+ config;
7
+ timer = null;
8
+ constructor(config) {
9
+ this.config = config;
10
+ }
11
+ async start(basePath) {
12
+ const activation = await loadActivation(basePath);
13
+ if (activation.plan === 'free') {
14
+ console.log('[SyncClient] 免费版,手动同步模式');
15
+ return;
16
+ }
17
+ this.timer = setInterval(() => this.sync(basePath), this.config.interval);
18
+ console.log(`[SyncClient] 自动同步已启动 (间隔: ${this.config.interval / 1000}s)`);
19
+ }
20
+ stop() {
21
+ if (this.timer) {
22
+ clearInterval(this.timer);
23
+ this.timer = null;
24
+ }
25
+ }
26
+ async sync(basePath) {
27
+ const activation = await loadActivation(basePath);
28
+ if (activation.plan === 'free')
29
+ return;
30
+ const email = activation?.email || '';
31
+ if (!email)
32
+ return;
33
+ // 获取 JWT token
34
+ const auth = getCachedToken() || await loginAndGetToken(this.config.serverUrl, email, basePath);
35
+ if (!auth) {
36
+ console.error('[SyncClient] 无法获取认证 token');
37
+ return;
38
+ }
39
+ // 同步记忆数据
40
+ await this.syncPreferences(basePath, auth.token);
41
+ // 同步 chat_logs 和 skills 作为备份文件
42
+ await this.syncFiles(basePath, auth.token);
43
+ }
44
+ async syncPreferences(basePath, token) {
45
+ try {
46
+ const prefsPath = path.join(basePath, 'memory', 'user_preferences.md');
47
+ const content = await fs.readFile(prefsPath, 'utf-8').catch(() => '');
48
+ if (!content.trim())
49
+ return;
50
+ // 解析本地偏好文件,提取条目
51
+ const items = this.parsePreferences(content);
52
+ if (items.length === 0)
53
+ return;
54
+ // 按类型分组上传
55
+ const typeGroups = {};
56
+ for (const item of items) {
57
+ if (!typeGroups[item.type])
58
+ typeGroups[item.type] = [];
59
+ typeGroups[item.type].push(item);
60
+ }
61
+ for (const [type, typeItems] of Object.entries(typeGroups)) {
62
+ await fetch(`${this.config.serverUrl}/api/sync/push`, {
63
+ method: 'POST',
64
+ headers: {
65
+ 'Content-Type': 'application/json',
66
+ 'Authorization': `Bearer ${token}`,
67
+ },
68
+ body: JSON.stringify({
69
+ type,
70
+ items: typeItems.map(item => ({
71
+ content: JSON.stringify({ text: item.text, source: item.source }),
72
+ confidence: item.confidence,
73
+ })),
74
+ }),
75
+ signal: AbortSignal.timeout(15000),
76
+ });
77
+ }
78
+ console.log(`[SyncClient] 偏好同步完成: ${items.length} 条`);
79
+ }
80
+ catch (err) {
81
+ console.error('[SyncClient] 偏好同步失败:', err);
82
+ }
83
+ }
84
+ async syncFiles(basePath, token) {
85
+ // 同步编译后的记忆文件到 memory API
86
+ try {
87
+ const compiledDir = path.join(basePath, 'memory', 'compiled');
88
+ const files = ['memory.md', 'preferences.md', 'lessons.md'];
89
+ for (const file of files) {
90
+ const content = await fs.readFile(path.join(compiledDir, file), 'utf-8').catch(() => '');
91
+ if (!content.trim())
92
+ continue;
93
+ const type = file.replace('.md', '') === 'memory' ? 'preference' : file.replace('.md', '');
94
+ await fetch(`${this.config.serverUrl}/api/memory/${type}`, {
95
+ method: 'POST',
96
+ headers: {
97
+ 'Content-Type': 'application/json',
98
+ 'Authorization': `Bearer ${token}`,
99
+ },
100
+ body: JSON.stringify({ content, confidence: 3 }),
101
+ signal: AbortSignal.timeout(15000),
102
+ });
103
+ }
104
+ console.log('[SyncClient] 记忆文件同步完成');
105
+ }
106
+ catch (err) {
107
+ console.error('[SyncClient] 文件同步失败:', err);
108
+ }
109
+ }
110
+ parsePreferences(markdown) {
111
+ const items = [];
112
+ const lines = markdown.split('\n');
113
+ let currentType = 'preference';
114
+ const typeMap = {
115
+ '偏好': 'preference',
116
+ '习惯': 'habit',
117
+ '事实': 'fact',
118
+ '决定': 'decision',
119
+ '教训': 'lesson',
120
+ '技能': 'skill',
121
+ };
122
+ for (const line of lines) {
123
+ // 检测类型标题
124
+ const headingMatch = line.match(/^##\s+(?:[^\s]+\s+)?(.+)$/);
125
+ if (headingMatch) {
126
+ const headingText = headingMatch[1].trim();
127
+ for (const [label, type] of Object.entries(typeMap)) {
128
+ if (headingText.includes(label)) {
129
+ currentType = type;
130
+ break;
131
+ }
132
+ }
133
+ continue;
134
+ }
135
+ // 跳过统计行
136
+ if (line.startsWith('## 📊') || line.startsWith('---') || line.startsWith('>')) {
137
+ continue;
138
+ }
139
+ // 解析条目
140
+ if (line.trim().startsWith('- ')) {
141
+ const text = line.trim().substring(2).trim();
142
+ // 提取星级
143
+ const starMatch = text.match(/\[([★☆]+)\]/);
144
+ const confidence = starMatch
145
+ ? (starMatch[1].match(/★/g) || []).length
146
+ : 1;
147
+ const cleanText = text.replace(/\[[★☆]+\]\s*/, '');
148
+ if (cleanText && !cleanText.includes('*暂无') && !cleanText.startsWith('**')) {
149
+ items.push({
150
+ type: currentType,
151
+ text: cleanText,
152
+ confidence,
153
+ source: 'user_preferences.md',
154
+ });
155
+ }
156
+ }
157
+ }
158
+ return items;
159
+ }
160
+ async restore(basePath) {
161
+ const activation = await loadActivation(basePath);
162
+ if (activation.plan === 'free')
163
+ return;
164
+ const email = activation?.email || '';
165
+ if (!email)
166
+ return;
167
+ const auth = getCachedToken() || await loginAndGetToken(this.config.serverUrl, email, basePath);
168
+ if (!auth)
169
+ return;
170
+ // 拉取所有类型的数据
171
+ const types = ['preference', 'habit', 'fact', 'decision', 'lesson', 'skill'];
172
+ const allItems = [];
173
+ for (const type of types) {
174
+ try {
175
+ const res = await fetch(`${this.config.serverUrl}/api/sync/pull?type=${type}`, {
176
+ headers: { 'Authorization': `Bearer ${auth.token}` },
177
+ signal: AbortSignal.timeout(10000),
178
+ });
179
+ if (res.ok) {
180
+ const data = await res.json();
181
+ if (data.items) {
182
+ allItems.push(...data.items.map((item) => ({ ...item, type })));
183
+ }
184
+ }
185
+ }
186
+ catch { }
187
+ }
188
+ // 重建本地偏好文件
189
+ const typeLabels = {
190
+ preference: '偏好',
191
+ habit: '习惯',
192
+ fact: '事实',
193
+ decision: '决定',
194
+ lesson: '教训',
195
+ skill: '技能',
196
+ };
197
+ let markdown = `# 用户偏好\n\n> 自动同步于 ${new Date().toISOString()}\n\n`;
198
+ const groups = {};
199
+ for (const item of allItems) {
200
+ if (!groups[item.type])
201
+ groups[item.type] = [];
202
+ groups[item.type].push(item);
203
+ }
204
+ for (const [type, items] of Object.entries(groups)) {
205
+ markdown += `## ${typeLabels[type] || type}\n\n`;
206
+ for (const item of items) {
207
+ const content = typeof item.content === 'string'
208
+ ? JSON.parse(item.content).text
209
+ : item.content?.text || '';
210
+ const stars = '★'.repeat(item.confidence || 1);
211
+ markdown += `- [${stars}] ${content}\n`;
212
+ }
213
+ markdown += '\n';
214
+ }
215
+ await fs.mkdir(path.dirname(path.join(basePath, 'memory', 'user_preferences.md')), { recursive: true });
216
+ await fs.writeFile(path.join(basePath, 'memory', 'user_preferences.md'), markdown);
217
+ console.log(`[SyncClient] 数据恢复完成: ${allItems.length} 条`);
218
+ }
219
+ }
220
+ //# sourceMappingURL=sync-client.js.map