@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.
- package/LICENSE +21 -0
- package/PERSONALITY.md +60 -0
- package/README.md +19 -0
- package/dist/anti-tamper.js +55 -0
- package/dist/auth-client.js +80 -0
- package/dist/chat-logger.js +86 -0
- package/dist/daily-review.js +311 -0
- package/dist/health-check.js +65 -0
- package/dist/index.js +721 -0
- package/dist/memory-manager.js +374 -0
- package/dist/payment.js +89 -0
- package/dist/preference-extractor.js +190 -0
- package/dist/setup-wizard.js +130 -0
- package/dist/skill-generator.js +259 -0
- package/dist/skill-optimizer.js +393 -0
- package/dist/sync-client.js +220 -0
- package/dist/task-tracker.js +215 -0
- package/dist/utils/logger.js +24 -0
- package/openclaw.plugin.json +85 -0
- package/package.json +376 -0
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { TaskTracker } from './task-tracker';
|
|
4
|
+
import { SkillOptimizer } from './skill-optimizer';
|
|
5
|
+
import { DailyReviewer } from './daily-review';
|
|
6
|
+
import { ChatLogger } from './chat-logger';
|
|
7
|
+
import { PreferenceExtractor } from './preference-extractor';
|
|
8
|
+
export class MemoryManager {
|
|
9
|
+
storageDir;
|
|
10
|
+
llmUrl;
|
|
11
|
+
llmModel;
|
|
12
|
+
chatLogger;
|
|
13
|
+
preferenceExtractor;
|
|
14
|
+
taskTracker;
|
|
15
|
+
skillOptimizer;
|
|
16
|
+
dailyReviewer;
|
|
17
|
+
preferences = [];
|
|
18
|
+
preferenceFilePath;
|
|
19
|
+
fileHeader = `# 用户偏好记忆
|
|
20
|
+
|
|
21
|
+
> 📝 本文件由 self-growth 插件自动维护
|
|
22
|
+
> ⚠️ 你可以手动编辑,但请注意格式
|
|
23
|
+
> 📅 最后更新:{{lastUpdated}}
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
`;
|
|
28
|
+
typeLabels = {
|
|
29
|
+
preference: '偏好',
|
|
30
|
+
habit: '习惯',
|
|
31
|
+
fact: '事实',
|
|
32
|
+
decision: '决定'
|
|
33
|
+
};
|
|
34
|
+
constructor(storageDir = './memory', llmUrl = 'http://127.0.0.1:1234/v1', llmModel = 'qwen/qwen3.5-9b') {
|
|
35
|
+
this.storageDir = storageDir;
|
|
36
|
+
this.llmUrl = llmUrl;
|
|
37
|
+
this.llmModel = llmModel;
|
|
38
|
+
this.preferenceFilePath = path.join(this.storageDir, 'user_preferences.md');
|
|
39
|
+
this.chatLogger = new ChatLogger(this.storageDir);
|
|
40
|
+
this.preferenceExtractor = new PreferenceExtractor(this.llmUrl);
|
|
41
|
+
this.taskTracker = new TaskTracker(this.storageDir);
|
|
42
|
+
this.skillOptimizer = new SkillOptimizer(this.storageDir, this.llmUrl, this.llmModel);
|
|
43
|
+
this.dailyReviewer = new DailyReviewer(this.chatLogger, this.preferenceExtractor, this.taskTracker, this.skillOptimizer, this.llmUrl, this.llmModel, this.storageDir);
|
|
44
|
+
}
|
|
45
|
+
async boot() {
|
|
46
|
+
console.log('[MemoryManager] 🚀 正在启动 self-growth 记忆中枢...');
|
|
47
|
+
try {
|
|
48
|
+
await fs.mkdir(this.storageDir, { recursive: true });
|
|
49
|
+
console.log(`[MemoryManager] 📂 存储目录已就绪: ${this.storageDir}`);
|
|
50
|
+
await Promise.all([
|
|
51
|
+
this.taskTracker.init(),
|
|
52
|
+
this.skillOptimizer.init(),
|
|
53
|
+
this.loadPreferences()
|
|
54
|
+
]);
|
|
55
|
+
console.log('[MemoryManager] ✅ 记忆中枢启动完成,自我进化系统已激活!\n');
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
console.error('[MemoryManager] ❌ 启动过程中发生严重错误:', error);
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async shutdown() {
|
|
63
|
+
console.log('[MemoryManager] 🛑 正在关闭记忆中枢...');
|
|
64
|
+
await this.writePreferences();
|
|
65
|
+
console.log('[MemoryManager] ✅ 记忆中枢已安全关闭。');
|
|
66
|
+
}
|
|
67
|
+
getActiveTasks() {
|
|
68
|
+
return this.taskTracker.getActiveTasks();
|
|
69
|
+
}
|
|
70
|
+
async triggerDailyReview() {
|
|
71
|
+
console.log('[MemoryManager] ⏳ 收到手动触发复盘指令...');
|
|
72
|
+
return await this.dailyReviewer.runDailyReview();
|
|
73
|
+
}
|
|
74
|
+
async addPreference(preference) {
|
|
75
|
+
const now = Date.now();
|
|
76
|
+
const existingIndex = this.preferences.findIndex((existing) => {
|
|
77
|
+
const a = existing.text.trim().replace(/\s+/g, '');
|
|
78
|
+
const b = preference.text.trim().replace(/\s+/g, '');
|
|
79
|
+
return a === b || (a.length > 2 && b.length > 2 &&
|
|
80
|
+
(a.includes(b) || b.includes(a)) &&
|
|
81
|
+
Math.abs(a.length - b.length) / Math.max(a.length, b.length) < 0.3);
|
|
82
|
+
});
|
|
83
|
+
if (existingIndex !== -1) {
|
|
84
|
+
const existing = this.preferences[existingIndex];
|
|
85
|
+
existing.occurrences += 1;
|
|
86
|
+
existing.lastMentioned = now;
|
|
87
|
+
if (existing.confidence < 5) {
|
|
88
|
+
existing.confidence = Math.min(5, Math.floor(existing.occurrences / 2) + 1);
|
|
89
|
+
}
|
|
90
|
+
console.log(`[MemoryManager] ⬆️ 偏好升级: [${existing.confidence}★] ${existing.text}`);
|
|
91
|
+
await this.writePreferences();
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
if (this.preferences.length >= 100) {
|
|
95
|
+
const removed = this.preferences.shift();
|
|
96
|
+
console.log(`[MemoryManager] 🧹 已满100条,移除最早记录: ${removed?.text}`);
|
|
97
|
+
}
|
|
98
|
+
if (this.preferences.length >= 50) {
|
|
99
|
+
console.warn(`[MemoryManager] ⚠️ 偏好已达 ${this.preferences.length} 条,建议清理旧偏好`);
|
|
100
|
+
}
|
|
101
|
+
const scored = {
|
|
102
|
+
...preference,
|
|
103
|
+
confidence: 1,
|
|
104
|
+
occurrences: 1,
|
|
105
|
+
lastMentioned: now
|
|
106
|
+
};
|
|
107
|
+
this.preferences.push(scored);
|
|
108
|
+
await this.writePreferences();
|
|
109
|
+
console.log(`[MemoryManager] ✅ 新增记忆 [${scored.confidence}★]: [${this.typeLabels[preference.type] || preference.type}] ${preference.text}`);
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
autoDegrade() {
|
|
113
|
+
const now = Date.now();
|
|
114
|
+
let degraded = 0;
|
|
115
|
+
for (const pref of this.preferences) {
|
|
116
|
+
const daysSinceMention = (now - pref.lastMentioned) / 86400000;
|
|
117
|
+
if (daysSinceMention > 30 && pref.confidence > 1) {
|
|
118
|
+
pref.confidence -= 1;
|
|
119
|
+
pref.lastMentioned = now;
|
|
120
|
+
degraded++;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (degraded > 0) {
|
|
124
|
+
console.log(`[MemoryManager] 📉 ${degraded} 条偏好因长期未提及被降星`);
|
|
125
|
+
this.writePreferences();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
async addPreferences(preferences) {
|
|
129
|
+
let addedCount = 0;
|
|
130
|
+
for (const pref of preferences) {
|
|
131
|
+
const added = await this.addPreference(pref);
|
|
132
|
+
if (added)
|
|
133
|
+
addedCount++;
|
|
134
|
+
}
|
|
135
|
+
if (addedCount > 0) {
|
|
136
|
+
console.log(`[MemoryManager] 📊 批量添加完成: ${addedCount}/${preferences.length} 条`);
|
|
137
|
+
}
|
|
138
|
+
return addedCount;
|
|
139
|
+
}
|
|
140
|
+
getPreferences(type) {
|
|
141
|
+
if (type) {
|
|
142
|
+
return this.preferences.filter((p) => p.type === type);
|
|
143
|
+
}
|
|
144
|
+
return [...this.preferences];
|
|
145
|
+
}
|
|
146
|
+
searchPreferences(query) {
|
|
147
|
+
const lowerQuery = query.toLowerCase();
|
|
148
|
+
return this.preferences.filter((p) => p.text.toLowerCase().includes(lowerQuery));
|
|
149
|
+
}
|
|
150
|
+
async removePreference(text) {
|
|
151
|
+
const index = this.preferences.findIndex((p) => p.text.trim() === text.trim());
|
|
152
|
+
if (index === -1)
|
|
153
|
+
return false;
|
|
154
|
+
const removed = this.preferences.splice(index, 1)[0];
|
|
155
|
+
await this.writePreferences();
|
|
156
|
+
console.log(`[MemoryManager] 🗑️ 已删除记忆: ${removed.text}`);
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
async clearPreferences() {
|
|
160
|
+
this.preferences = [];
|
|
161
|
+
await this.writePreferences();
|
|
162
|
+
console.log('[MemoryManager] 🧹 所有偏好记忆已清空');
|
|
163
|
+
}
|
|
164
|
+
getPreferenceStats() {
|
|
165
|
+
const byType = {};
|
|
166
|
+
for (const pref of this.preferences) {
|
|
167
|
+
byType[pref.type] = (byType[pref.type] || 0) + 1;
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
total: this.preferences.length,
|
|
171
|
+
byType,
|
|
172
|
+
lastUpdated: new Date().toISOString()
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
getPreferencesForRetrieval() {
|
|
176
|
+
return this.preferences.map(p => ({
|
|
177
|
+
text: p.text,
|
|
178
|
+
confidence: p.confidence,
|
|
179
|
+
type: p.type
|
|
180
|
+
}));
|
|
181
|
+
}
|
|
182
|
+
formatPreferencesForContext(maxItems) {
|
|
183
|
+
if (this.preferences.length === 0) {
|
|
184
|
+
return '(暂无用户偏好记录)';
|
|
185
|
+
}
|
|
186
|
+
const sorted = [...this.preferences].sort((a, b) => b.confidence - a.confidence);
|
|
187
|
+
let items = sorted;
|
|
188
|
+
if (maxItems && maxItems > 0) {
|
|
189
|
+
items = sorted.slice(0, maxItems);
|
|
190
|
+
}
|
|
191
|
+
const lines = items.map((p) => {
|
|
192
|
+
const stars = '★'.repeat(p.confidence) + '☆'.repeat(5 - p.confidence);
|
|
193
|
+
const label = this.typeLabels[p.type] || p.type;
|
|
194
|
+
return `- [${label}] [${stars}] ${p.text}`;
|
|
195
|
+
});
|
|
196
|
+
return `## 用户偏好记忆\n\n${lines.join('\n')}`;
|
|
197
|
+
}
|
|
198
|
+
async getRelevantMemories(context, limit = 5) {
|
|
199
|
+
if (this.preferences.length === 0) {
|
|
200
|
+
return "(暂无用户偏好记录)";
|
|
201
|
+
}
|
|
202
|
+
const highConf = this.preferences.filter(p => p.confidence >= 4);
|
|
203
|
+
const lowConf = this.preferences.filter(p => p.confidence < 4);
|
|
204
|
+
if (this.preferences.length <= 10) {
|
|
205
|
+
return this.formatSorted(highConf, lowConf, limit);
|
|
206
|
+
}
|
|
207
|
+
try {
|
|
208
|
+
const allSorted = [...this.preferences].sort((a, b) => b.confidence - a.confidence);
|
|
209
|
+
const memoryList = allSorted
|
|
210
|
+
.map((p, i) => `${i + 1}. [${p.confidence}★] [${this.typeLabels[p.type] || p.type}] ${p.text}`)
|
|
211
|
+
.join('\n');
|
|
212
|
+
const prompt = `从以下记忆列表中选出与用户问题最相关的 ${limit} 条记忆,只返回编号(如:2,5,7)。
|
|
213
|
+
高星级表示该记忆被多次提及,应优先选择。
|
|
214
|
+
|
|
215
|
+
用户问题:${context}
|
|
216
|
+
|
|
217
|
+
记忆列表:
|
|
218
|
+
${memoryList}
|
|
219
|
+
|
|
220
|
+
最相关记忆编号:`;
|
|
221
|
+
const res = await fetch(`${this.llmUrl}/chat/completions`, {
|
|
222
|
+
method: 'POST',
|
|
223
|
+
headers: { 'Content-Type': 'application/json' },
|
|
224
|
+
body: JSON.stringify({
|
|
225
|
+
model: this.llmModel,
|
|
226
|
+
messages: [{ role: 'user', content: prompt }],
|
|
227
|
+
temperature: 0.1,
|
|
228
|
+
max_tokens: 50
|
|
229
|
+
})
|
|
230
|
+
});
|
|
231
|
+
const data = await res.json();
|
|
232
|
+
const answer = data?.choices?.[0]?.message?.content?.trim() || '';
|
|
233
|
+
const indices = (answer.match(/\d+/g) || []).map(Number);
|
|
234
|
+
const selected = indices
|
|
235
|
+
.filter((i) => i >= 1 && i <= allSorted.length)
|
|
236
|
+
.map((i) => allSorted[i - 1]);
|
|
237
|
+
if (selected.length === 0) {
|
|
238
|
+
return this.formatSorted(highConf, lowConf, limit);
|
|
239
|
+
}
|
|
240
|
+
return this.formatResult(selected);
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
return this.formatSorted(highConf, lowConf, limit);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
formatSorted(high, low, limit) {
|
|
247
|
+
const combined = [...high, ...low].slice(0, limit);
|
|
248
|
+
return this.formatResult(combined);
|
|
249
|
+
}
|
|
250
|
+
formatResult(items) {
|
|
251
|
+
const lessons = items.filter(p => p.text.startsWith('[教训]'));
|
|
252
|
+
const normalPrefs = items.filter(p => !p.text.startsWith('[教训]'));
|
|
253
|
+
let result = '';
|
|
254
|
+
if (lessons.length > 0) {
|
|
255
|
+
result += `## ⚠️ 错误教训(务必避免重复)\n\n${lessons.map(p => `- ${p.text}`).join('\n')}\n\n`;
|
|
256
|
+
}
|
|
257
|
+
if (normalPrefs.length > 0) {
|
|
258
|
+
result += `## 👤 用户偏好(与当前话题相关)\n\n${normalPrefs.map(p => `- [${p.confidence}★] [${this.typeLabels[p.type] || p.type}] ${p.text}`).join('\n')}`;
|
|
259
|
+
}
|
|
260
|
+
return result.trim();
|
|
261
|
+
}
|
|
262
|
+
async loadPreferences() {
|
|
263
|
+
try {
|
|
264
|
+
await fs.access(this.preferenceFilePath);
|
|
265
|
+
const content = await fs.readFile(this.preferenceFilePath, 'utf-8');
|
|
266
|
+
this.preferences = this.parsePreferencesFromMarkdown(content);
|
|
267
|
+
console.log(`[MemoryManager] 📂 已加载 ${this.preferences.length} 条偏好记忆`);
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
console.log('[MemoryManager] 📝 user_preferences.md 不存在,将创建新文件');
|
|
271
|
+
this.preferences = [];
|
|
272
|
+
await this.writePreferences();
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
async writePreferences() {
|
|
276
|
+
const now = new Date().toISOString().replace('T', ' ').substring(0, 19);
|
|
277
|
+
const header = this.fileHeader.replace('{{lastUpdated}}', now);
|
|
278
|
+
const body = this.formatPreferencesToMarkdown();
|
|
279
|
+
const content = header + body;
|
|
280
|
+
const tmpPath = this.preferenceFilePath + '.tmp';
|
|
281
|
+
try {
|
|
282
|
+
await fs.writeFile(tmpPath, content, 'utf-8');
|
|
283
|
+
await fs.rename(tmpPath, this.preferenceFilePath);
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
console.error('[MemoryManager] ❌ 写入偏好文件失败:', error);
|
|
287
|
+
try {
|
|
288
|
+
await fs.unlink(tmpPath);
|
|
289
|
+
}
|
|
290
|
+
catch { /* ignore */ }
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
formatPreferencesToMarkdown() {
|
|
294
|
+
if (this.preferences.length === 0) {
|
|
295
|
+
return `*暂无记录的用户偏好。当你在对话中表达偏好、习惯或决定时,我会自动记录在这里。*\n`;
|
|
296
|
+
}
|
|
297
|
+
const groups = {};
|
|
298
|
+
for (const pref of this.preferences) {
|
|
299
|
+
if (!groups[pref.type])
|
|
300
|
+
groups[pref.type] = [];
|
|
301
|
+
groups[pref.type].push(pref);
|
|
302
|
+
}
|
|
303
|
+
const typeOrder = ['preference', 'habit', 'fact', 'decision'];
|
|
304
|
+
const sortedTypes = Object.keys(groups).sort((a, b) => typeOrder.indexOf(a) - typeOrder.indexOf(b));
|
|
305
|
+
let markdown = '';
|
|
306
|
+
markdown += `## 📊 统计\n\n`;
|
|
307
|
+
markdown += `- **总计**:${this.preferences.length} 条记忆\n`;
|
|
308
|
+
for (const type of sortedTypes) {
|
|
309
|
+
const label = this.typeLabels[type] || type;
|
|
310
|
+
markdown += `- **${label}**:${groups[type].length} 条\n`;
|
|
311
|
+
}
|
|
312
|
+
markdown += '\n---\n\n';
|
|
313
|
+
for (const type of sortedTypes) {
|
|
314
|
+
const label = this.typeLabels[type] || type;
|
|
315
|
+
const emoji = this.getTypeEmoji(type);
|
|
316
|
+
markdown += `## ${emoji} ${label}\n\n`;
|
|
317
|
+
for (const pref of groups[type]) {
|
|
318
|
+
const stars = '★'.repeat(pref.confidence) + '☆'.repeat(5 - pref.confidence);
|
|
319
|
+
markdown += `- [${stars}] ${pref.text}\n`;
|
|
320
|
+
}
|
|
321
|
+
markdown += '\n';
|
|
322
|
+
}
|
|
323
|
+
return markdown;
|
|
324
|
+
}
|
|
325
|
+
parsePreferencesFromMarkdown(content) {
|
|
326
|
+
const preferences = [];
|
|
327
|
+
const lines = content.split('\n');
|
|
328
|
+
let currentType = null;
|
|
329
|
+
for (const line of lines) {
|
|
330
|
+
const typeMatch = line.match(/^##\s+(?:[^\s]+\s+)?(.+)$/);
|
|
331
|
+
if (typeMatch) {
|
|
332
|
+
const headingText = typeMatch[1].trim();
|
|
333
|
+
for (const [key, label] of Object.entries(this.typeLabels)) {
|
|
334
|
+
if (headingText === label) {
|
|
335
|
+
currentType = key;
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
if (line.startsWith('## 📊') || line.startsWith('---') || line.startsWith('>')) {
|
|
342
|
+
currentType = null;
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
if (currentType && line.trim().startsWith('- ')) {
|
|
346
|
+
const text = line.trim().substring(2).trim();
|
|
347
|
+
if (text && !text.includes('*暂无') && !text.includes('**总计**') && !text.startsWith('**')) {
|
|
348
|
+
const starMatch = text.match(/\[(★+)(☆+)?\]/);
|
|
349
|
+
const confidence = starMatch ? starMatch[1].length : 1;
|
|
350
|
+
const cleanText = text.replace(/\[[★☆]+\]\s*/, '');
|
|
351
|
+
preferences.push({
|
|
352
|
+
text: cleanText,
|
|
353
|
+
type: currentType,
|
|
354
|
+
source: 'user_preferences.md',
|
|
355
|
+
confidence,
|
|
356
|
+
occurrences: confidence,
|
|
357
|
+
lastMentioned: Date.now()
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return preferences;
|
|
363
|
+
}
|
|
364
|
+
getTypeEmoji(type) {
|
|
365
|
+
const emojis = {
|
|
366
|
+
preference: '🎯',
|
|
367
|
+
habit: '🔄',
|
|
368
|
+
fact: '📋',
|
|
369
|
+
decision: '✅'
|
|
370
|
+
};
|
|
371
|
+
return emojis[type] || '📌';
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
//# sourceMappingURL=memory-manager.js.map
|
package/dist/payment.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
const ACTIVATION_FILE = 'payment.json';
|
|
4
|
+
export function getActivationPath(basePath) {
|
|
5
|
+
return path.join(basePath, ACTIVATION_FILE);
|
|
6
|
+
}
|
|
7
|
+
export async function loadActivation(basePath) {
|
|
8
|
+
const filePath = getActivationPath(basePath);
|
|
9
|
+
try {
|
|
10
|
+
const data = await fs.readFile(filePath, 'utf-8');
|
|
11
|
+
const parsed = JSON.parse(data);
|
|
12
|
+
if (!parsed.deviceId) {
|
|
13
|
+
parsed.deviceId = generateDeviceId();
|
|
14
|
+
await fs.writeFile(filePath, JSON.stringify(parsed, null, 2), 'utf-8');
|
|
15
|
+
}
|
|
16
|
+
return parsed;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
const state = {
|
|
20
|
+
plan: 'free',
|
|
21
|
+
license: null,
|
|
22
|
+
activatedAt: null,
|
|
23
|
+
expiresAt: null,
|
|
24
|
+
deviceId: generateDeviceId(),
|
|
25
|
+
email: null,
|
|
26
|
+
};
|
|
27
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
28
|
+
await fs.writeFile(filePath, JSON.stringify(state, null, 2), 'utf-8');
|
|
29
|
+
return state;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export async function saveActivation(basePath, state) {
|
|
33
|
+
const filePath = getActivationPath(basePath);
|
|
34
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
35
|
+
await fs.writeFile(filePath, JSON.stringify(state, null, 2), 'utf-8');
|
|
36
|
+
}
|
|
37
|
+
export async function activateLicense(basePath, license, serverUrl) {
|
|
38
|
+
const res = await fetch(`${serverUrl}/api/license/verify`, {
|
|
39
|
+
method: 'POST',
|
|
40
|
+
headers: { 'Content-Type': 'application/json' },
|
|
41
|
+
body: JSON.stringify({ license }),
|
|
42
|
+
});
|
|
43
|
+
if (!res.ok)
|
|
44
|
+
throw new Error('许可证无效或已过期');
|
|
45
|
+
const data = await res.json();
|
|
46
|
+
const state = {
|
|
47
|
+
plan: data.plan,
|
|
48
|
+
license,
|
|
49
|
+
activatedAt: new Date().toISOString(),
|
|
50
|
+
expiresAt: data.expiresAt,
|
|
51
|
+
deviceId: generateDeviceId(),
|
|
52
|
+
email: data.email || null,
|
|
53
|
+
};
|
|
54
|
+
await saveActivation(basePath, state);
|
|
55
|
+
return state;
|
|
56
|
+
}
|
|
57
|
+
export async function createPaymentOrder(plan, serverUrl) {
|
|
58
|
+
const state = await loadActivation('');
|
|
59
|
+
const res = await fetch(`${serverUrl}/api/payment/create-order`, {
|
|
60
|
+
method: 'POST',
|
|
61
|
+
headers: {
|
|
62
|
+
'Content-Type': 'application/json',
|
|
63
|
+
'Authorization': `Bearer ${state.license}`,
|
|
64
|
+
},
|
|
65
|
+
body: JSON.stringify({ plan }),
|
|
66
|
+
});
|
|
67
|
+
if (!res.ok)
|
|
68
|
+
throw new Error('创建订单失败');
|
|
69
|
+
return res.json();
|
|
70
|
+
}
|
|
71
|
+
export async function checkPaymentStatus(orderId, serverUrl) {
|
|
72
|
+
const state = await loadActivation('');
|
|
73
|
+
const res = await fetch(`${serverUrl}/api/payment/order/${orderId}`, {
|
|
74
|
+
headers: { 'Authorization': `Bearer ${state.license}` },
|
|
75
|
+
});
|
|
76
|
+
if (!res.ok)
|
|
77
|
+
return 'unknown';
|
|
78
|
+
const data = await res.json();
|
|
79
|
+
return data.status;
|
|
80
|
+
}
|
|
81
|
+
function generateDeviceId() {
|
|
82
|
+
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
|
83
|
+
let id = '';
|
|
84
|
+
for (let i = 0; i < 16; i++) {
|
|
85
|
+
id += chars[Math.floor(Math.random() * chars.length)];
|
|
86
|
+
}
|
|
87
|
+
return id;
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=payment.js.map
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
/**
|
|
5
|
+
* 用户偏好提取器
|
|
6
|
+
* 职责:只负责从对话文本中提取结构化的偏好数据。
|
|
7
|
+
* 不负责存储——存储逻辑交给 MemoryManager 处理。
|
|
8
|
+
* 模型自动识别:环境变量 > openclaw.json > 自动探测 LLM 服务端 > 兜底。
|
|
9
|
+
*/
|
|
10
|
+
export class PreferenceExtractor {
|
|
11
|
+
llmModel = '';
|
|
12
|
+
llmUrl;
|
|
13
|
+
// 触发偏好提取的关键词
|
|
14
|
+
triggerKeywords = [
|
|
15
|
+
'喜欢', '讨厌', '习惯', '以后', '记住', '不要',
|
|
16
|
+
'总是', '用这个', '改成', '偏好', '我不喜欢',
|
|
17
|
+
'下次', '每次都', '一直用', '改一下'
|
|
18
|
+
];
|
|
19
|
+
constructor(llmUrl = 'http://127.0.0.1:1234/v1') {
|
|
20
|
+
this.llmUrl = llmUrl;
|
|
21
|
+
// 异步初始化模型名称,不阻塞构造函数
|
|
22
|
+
this.resolveModel().then(model => {
|
|
23
|
+
this.llmModel = model;
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* 从对话中提取用户偏好
|
|
28
|
+
* @param userMessage 用户的最新消息
|
|
29
|
+
* @param agentMessage Agent 对应的回复
|
|
30
|
+
* @returns 提取到的偏好列表,如果没有偏好则返回空数组
|
|
31
|
+
*/
|
|
32
|
+
async extract(userMessage, agentMessage) {
|
|
33
|
+
// 1. 关键词预过滤:避免无意义的 LLM 调用
|
|
34
|
+
const hasTrigger = this.triggerKeywords.some(keyword => userMessage.includes(keyword));
|
|
35
|
+
if (!hasTrigger)
|
|
36
|
+
return [];
|
|
37
|
+
// 2. 确保模型已经初始化
|
|
38
|
+
if (!this.llmModel) {
|
|
39
|
+
await this.resolveModel();
|
|
40
|
+
}
|
|
41
|
+
console.log('[PreferenceExtractor] 🔍 检测到潜在的偏好表达,正在分析...');
|
|
42
|
+
// 3. 构造提取 Prompt
|
|
43
|
+
const prompt = this.buildExtractionPrompt(userMessage, agentMessage);
|
|
44
|
+
try {
|
|
45
|
+
const response = await this.callLLM(prompt);
|
|
46
|
+
const preferences = this.parseResponse(response);
|
|
47
|
+
return preferences;
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
console.error('[PreferenceExtractor] ❌ 偏好提取失败:', error);
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* 批量提取:从一整天的对话日志中提取所有偏好
|
|
56
|
+
* @param conversationText 一整天的对话文本
|
|
57
|
+
* @returns 提取到的偏好列表
|
|
58
|
+
*/
|
|
59
|
+
async extractBatch(conversationText) {
|
|
60
|
+
if (!this.llmModel) {
|
|
61
|
+
await this.resolveModel();
|
|
62
|
+
}
|
|
63
|
+
const prompt = `分析以下对话,从中提取所有重要的用户偏好、习惯和事实。
|
|
64
|
+
以 JSON 列表格式返回,每条记忆包含三个字段:
|
|
65
|
+
- text: 记忆内容(一句话)
|
|
66
|
+
- type: 类型,可选值:preference(偏好)、habit(习惯)、fact(事实)、decision(决定)
|
|
67
|
+
- source: 来源(对话摘要)
|
|
68
|
+
|
|
69
|
+
对话内容:
|
|
70
|
+
${conversationText}
|
|
71
|
+
|
|
72
|
+
只返回 JSON 列表,不要其他内容:`;
|
|
73
|
+
try {
|
|
74
|
+
const response = await this.callLLM(prompt);
|
|
75
|
+
return this.parseResponse(response);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// ========== 私有方法 ==========
|
|
82
|
+
/**
|
|
83
|
+
* 智能获取可用的模型名称
|
|
84
|
+
* 优先级:环境变量 > openclaw.json > 自动探测 LLM 服务端 > 兜底
|
|
85
|
+
*/
|
|
86
|
+
async resolveModel() {
|
|
87
|
+
// 1. 环境变量(最高优先级,用户手动指定)
|
|
88
|
+
if (process.env.SELF_GROWTH_LLM_MODEL) {
|
|
89
|
+
this.llmModel = process.env.SELF_GROWTH_LLM_MODEL;
|
|
90
|
+
return this.llmModel;
|
|
91
|
+
}
|
|
92
|
+
// 2. 读取 openclaw.json 中的 primary 模型
|
|
93
|
+
try {
|
|
94
|
+
const configPath = path.join(os.homedir(), '.openclaw', 'openclaw.json');
|
|
95
|
+
const configContent = await fs.readFile(configPath, 'utf-8');
|
|
96
|
+
const config = JSON.parse(configContent);
|
|
97
|
+
const primaryModel = config?.agents?.defaults?.model?.primary;
|
|
98
|
+
if (primaryModel) {
|
|
99
|
+
this.llmModel = primaryModel;
|
|
100
|
+
return this.llmModel;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// 配置文件不存在或读取失败,静默跳过
|
|
105
|
+
}
|
|
106
|
+
// 3. 探测 llmUrl 的 /models 端点
|
|
107
|
+
// 兼容任何 OpenAI 兼容服务:LM Studio、Ollama、vLLM、text-generation-webui 等
|
|
108
|
+
try {
|
|
109
|
+
const response = await fetch(`${this.llmUrl}/models`);
|
|
110
|
+
const data = await response.json();
|
|
111
|
+
// 不同服务返回的字段名可能不同,逐个尝试
|
|
112
|
+
const firstModel = data?.data?.[0]?.id // LM Studio / Ollama 格式
|
|
113
|
+
|| data?.models?.[0]?.name // vLLM 格式
|
|
114
|
+
|| data?.models?.[0]?.id // 其他格式
|
|
115
|
+
|| data?.data?.[0]?.name;
|
|
116
|
+
if (firstModel) {
|
|
117
|
+
this.llmModel = firstModel;
|
|
118
|
+
return this.llmModel;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
// 服务端不在线或 /models 端点不可用,跳过
|
|
123
|
+
}
|
|
124
|
+
// 4. 最终兜底
|
|
125
|
+
this.llmModel = 'default-model';
|
|
126
|
+
return this.llmModel;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* 构建发送给 LLM 的分析 Prompt
|
|
130
|
+
*/
|
|
131
|
+
buildExtractionPrompt(userMessage, agentMessage) {
|
|
132
|
+
return `你是一个专业的用户偏好分析助手。请分析以下对话,判断用户是否表达了需要长期记忆的"个人偏好"或"习惯"。
|
|
133
|
+
|
|
134
|
+
【对话内容】
|
|
135
|
+
用户: ${userMessage}
|
|
136
|
+
Agent: ${agentMessage}
|
|
137
|
+
|
|
138
|
+
【判断标准】
|
|
139
|
+
1. 必须是针对未来交互的通用要求(例如:"以后写代码都用中文变量名"、"我不喜欢看长篇大论")。
|
|
140
|
+
2. 如果是针对当前任务的临时指令(例如:"把这句话翻译成英文"),则忽略。
|
|
141
|
+
3. 如果确认是长期偏好,请用 JSON 格式返回,包含 text(一句话精炼总结)、type(preference/habit/fact/decision)、source("对话记录")。
|
|
142
|
+
4. 如果不是,请返回空列表 []。
|
|
143
|
+
|
|
144
|
+
【你的结论】`;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* 调用 LLM API(兼容任何 OpenAI 兼容服务端)
|
|
148
|
+
*/
|
|
149
|
+
async callLLM(prompt) {
|
|
150
|
+
try {
|
|
151
|
+
const response = await fetch(`${this.llmUrl}/chat/completions`, {
|
|
152
|
+
method: 'POST',
|
|
153
|
+
headers: { 'Content-Type': 'application/json' },
|
|
154
|
+
body: JSON.stringify({
|
|
155
|
+
messages: [{ role: 'user', content: prompt }],
|
|
156
|
+
temperature: 0.1,
|
|
157
|
+
max_tokens: 2000
|
|
158
|
+
})
|
|
159
|
+
});
|
|
160
|
+
const data = await response.json();
|
|
161
|
+
return data.choices?.[0]?.message?.content || '[]';
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
console.error('[PreferenceExtractor] LLM 调用失败:', error);
|
|
165
|
+
return '[]';
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* 解析 LLM 返回的 JSON 响应
|
|
170
|
+
* 能处理纯 JSON、JSON 数组、以及嵌在文本中的 JSON
|
|
171
|
+
*/
|
|
172
|
+
parseResponse(response) {
|
|
173
|
+
try {
|
|
174
|
+
return JSON.parse(response);
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
const match = response.match(/\[[\s\S]*\]/);
|
|
178
|
+
if (match) {
|
|
179
|
+
try {
|
|
180
|
+
return JSON.parse(match[0]);
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
// 解析失败,返回空
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return [];
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
//# sourceMappingURL=preference-extractor.js.map
|