@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,130 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import { createWriteStream, chmodSync } from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import https from 'https';
|
|
5
|
+
const WELCOME_FILE = '.yulailai_welcome_shown';
|
|
6
|
+
const CLOUD_BINARY = process.platform === 'win32' ? 'yulailai-cloud.exe' : 'yulailai-cloud';
|
|
7
|
+
const DOWNLOAD_BASE = process.env.YULAI_CLOUD_URL || 'https://releases.yulailai.com/cloud/latest';
|
|
8
|
+
export async function shouldShowWizard(basePath) {
|
|
9
|
+
try {
|
|
10
|
+
await fs.access(path.join(basePath, WELCOME_FILE));
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export async function markWizardComplete(basePath) {
|
|
18
|
+
await fs.writeFile(path.join(basePath, WELCOME_FILE), new Date().toISOString(), 'utf-8');
|
|
19
|
+
}
|
|
20
|
+
export async function downloadCloudClient(basePath) {
|
|
21
|
+
const binDir = path.join(basePath, 'bin');
|
|
22
|
+
const binaryPath = path.join(binDir, CLOUD_BINARY);
|
|
23
|
+
try {
|
|
24
|
+
await fs.access(binaryPath);
|
|
25
|
+
return binaryPath;
|
|
26
|
+
}
|
|
27
|
+
catch { }
|
|
28
|
+
await fs.mkdir(binDir, { recursive: true });
|
|
29
|
+
const url = `${DOWNLOAD_BASE}/${CLOUD_BINARY}`;
|
|
30
|
+
console.log(`[yulailai] 正在下载云服务客户端...`);
|
|
31
|
+
return new Promise((resolve) => {
|
|
32
|
+
https.get(url, (res) => {
|
|
33
|
+
if (res.statusCode === 302 || res.statusCode === 301) {
|
|
34
|
+
https.get(res.headers.location, (redirectRes) => {
|
|
35
|
+
const file = createWriteStream(binaryPath);
|
|
36
|
+
redirectRes.pipe(file);
|
|
37
|
+
file.on('finish', () => {
|
|
38
|
+
chmodSync(binaryPath, 0o755);
|
|
39
|
+
console.log('[yulailai] 云服务客户端就绪');
|
|
40
|
+
resolve(binaryPath);
|
|
41
|
+
});
|
|
42
|
+
}).on('error', () => {
|
|
43
|
+
console.warn('[yulailai] 下载失败,离线模式可用');
|
|
44
|
+
resolve(null);
|
|
45
|
+
});
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const file = createWriteStream(binaryPath);
|
|
49
|
+
res.pipe(file);
|
|
50
|
+
file.on('finish', () => {
|
|
51
|
+
chmodSync(binaryPath, 0o755);
|
|
52
|
+
console.log('[yulailai] 云服务客户端就绪');
|
|
53
|
+
resolve(binaryPath);
|
|
54
|
+
});
|
|
55
|
+
}).on('error', () => {
|
|
56
|
+
console.warn('[yulailai] 下载失败,离线模式可用');
|
|
57
|
+
resolve(null);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
export function getWelcomeStep() {
|
|
62
|
+
return {
|
|
63
|
+
title: '欢迎使用 yulailai',
|
|
64
|
+
description: 'yulailai 帮助你的 AI Agent 跨设备共享记忆、偏好和技能。首次使用需要注册账号。',
|
|
65
|
+
options: [
|
|
66
|
+
{ key: 'register', label: '注册新账号' },
|
|
67
|
+
{ key: 'login', label: '已有账号,直接登录' },
|
|
68
|
+
{ key: 'skip', label: '跳过,使用免费版(手动同步)' },
|
|
69
|
+
],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
export function getPlanStep() {
|
|
73
|
+
return {
|
|
74
|
+
title: '选择套餐',
|
|
75
|
+
description: '免费版支持手动同步,专业版和企业版支持自动同步。',
|
|
76
|
+
options: [
|
|
77
|
+
{ key: 'free', label: '免费版 - 1GB 存储 | 1台设备 | 手动同步 | 免费', plan: 'free' },
|
|
78
|
+
{ key: 'pro', label: '专业版 - 10GB 存储 | 5台设备 | 自动同步 | ¥15/月', plan: 'pro' },
|
|
79
|
+
{ key: 'enterprise', label: '企业版 - 100GB 存储 | 不限设备 | 实时同步 | ¥3000/年', plan: 'enterprise' },
|
|
80
|
+
],
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
export function getPaymentStep(plan, amount) {
|
|
84
|
+
const prices = { free: '免费', pro: '¥15/月', enterprise: '¥3000/年' };
|
|
85
|
+
return {
|
|
86
|
+
title: '扫码支付',
|
|
87
|
+
description: `套餐: ${plan === 'pro' ? '专业版' : '企业版'} | 金额: ${prices[plan]}\n\n请使用微信扫描二维码完成支付。支付完成后自动激活。`,
|
|
88
|
+
options: [
|
|
89
|
+
{ key: 'paid', label: '已完成支付' },
|
|
90
|
+
{ key: 'cancel', label: '取消,使用免费版' },
|
|
91
|
+
],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
export function getCompleteStep(plan) {
|
|
95
|
+
const names = { free: '免费版', pro: '专业版', enterprise: '企业版' };
|
|
96
|
+
return {
|
|
97
|
+
title: '设置完成',
|
|
98
|
+
description: `yulailai ${names[plan]}已就绪。\n\n你的 AI Agent 现在拥有跨设备记忆能力。`,
|
|
99
|
+
options: [
|
|
100
|
+
{ key: 'done', label: '开始使用' },
|
|
101
|
+
],
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
export async function handleRegister(serverUrl, username, email, password) {
|
|
105
|
+
const res = await fetch(`${serverUrl}/api/auth/register`, {
|
|
106
|
+
method: 'POST',
|
|
107
|
+
headers: { 'Content-Type': 'application/json' },
|
|
108
|
+
body: JSON.stringify({ username, email, password }),
|
|
109
|
+
});
|
|
110
|
+
if (!res.ok) {
|
|
111
|
+
const err = await res.json();
|
|
112
|
+
throw new Error(err.error || '注册失败');
|
|
113
|
+
}
|
|
114
|
+
const data = await res.json();
|
|
115
|
+
return data.user;
|
|
116
|
+
}
|
|
117
|
+
export async function handleLogin(serverUrl, email, password) {
|
|
118
|
+
const res = await fetch(`${serverUrl}/api/auth/login`, {
|
|
119
|
+
method: 'POST',
|
|
120
|
+
headers: { 'Content-Type': 'application/json' },
|
|
121
|
+
body: JSON.stringify({ email, password }),
|
|
122
|
+
});
|
|
123
|
+
if (!res.ok) {
|
|
124
|
+
const err = await res.json();
|
|
125
|
+
throw new Error(err.error || '登录失败');
|
|
126
|
+
}
|
|
127
|
+
const data = await res.json();
|
|
128
|
+
return data.token;
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=setup-wizard.js.map
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
export class SkillGenerator {
|
|
4
|
+
outputDir;
|
|
5
|
+
constructor(workspacePath) {
|
|
6
|
+
this.outputDir = workspacePath;
|
|
7
|
+
this.ensureDirectoryExists(this.outputDir);
|
|
8
|
+
}
|
|
9
|
+
generate(taskName, steps, category) {
|
|
10
|
+
const skillDirName = this.convertToFolderName(taskName);
|
|
11
|
+
const skillDir = path.join(this.outputDir, skillDirName);
|
|
12
|
+
this.ensureDirectoryExists(skillDir);
|
|
13
|
+
const skillContent = this.buildSkillContent(taskName, steps, category);
|
|
14
|
+
const skillFilePath = path.join(skillDir, 'SKILL.md');
|
|
15
|
+
fs.writeFileSync(skillFilePath, skillContent, 'utf-8');
|
|
16
|
+
console.log(`✅ Skill 已生成: ${skillFilePath}`);
|
|
17
|
+
return skillFilePath;
|
|
18
|
+
}
|
|
19
|
+
listGenerated() {
|
|
20
|
+
if (!fs.existsSync(this.outputDir))
|
|
21
|
+
return [];
|
|
22
|
+
return fs.readdirSync(this.outputDir, { withFileTypes: true })
|
|
23
|
+
.filter(dirent => {
|
|
24
|
+
if (!dirent.isDirectory())
|
|
25
|
+
return false;
|
|
26
|
+
const skillFile = path.join(this.outputDir, dirent.name, 'SKILL.md');
|
|
27
|
+
return fs.existsSync(skillFile);
|
|
28
|
+
})
|
|
29
|
+
.map(dirent => dirent.name);
|
|
30
|
+
}
|
|
31
|
+
evaluateAndGenerate(taskContext) {
|
|
32
|
+
const sameTaskCount = this.countSimilarTask(taskContext.taskName);
|
|
33
|
+
const isRepeated = sameTaskCount >= 5;
|
|
34
|
+
const userRequested = /生成技能|记下这个流程|保存这个任务|记住这个流程/.test(taskContext.taskName);
|
|
35
|
+
if (!isRepeated && !userRequested) {
|
|
36
|
+
console.log(`[SkillGenerator] ℹ️ 不满足生成条件(相似:${sameTaskCount}次)`);
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
const existing = this.listGenerated();
|
|
40
|
+
const taskLower = taskContext.taskName.toLowerCase().replace(/\s+/g, '');
|
|
41
|
+
for (const name of existing) {
|
|
42
|
+
const nameLower = name.toLowerCase().replace(/\s+/g, '');
|
|
43
|
+
if (nameLower.includes(taskLower.substring(0, 6)) || taskLower.includes(nameLower.substring(0, 6))) {
|
|
44
|
+
const skillPath = path.join(this.outputDir, name, 'SKILL.md');
|
|
45
|
+
if (fs.existsSync(skillPath)) {
|
|
46
|
+
let content = fs.readFileSync(skillPath, 'utf-8');
|
|
47
|
+
content = content.replace(/last_used: .*/g, `last_used: ${new Date().toISOString()}`);
|
|
48
|
+
content = content.replace(/status: .*/g, `status: active`);
|
|
49
|
+
fs.writeFileSync(skillPath, content, 'utf-8');
|
|
50
|
+
console.log(`[SkillGenerator] 🔄 技能已更新: ${name}`);
|
|
51
|
+
}
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
this.generate(taskContext.taskName, taskContext.steps, taskContext.category);
|
|
56
|
+
console.log(`[SkillGenerator] 🎓 自动生成新技能: ${taskContext.taskName}(相似:${sameTaskCount}次)`);
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* 标准化裸 .md 文件:移入子文件夹,补全 SKILL.md 的 frontmatter
|
|
61
|
+
*/
|
|
62
|
+
normalizeSkillFiles() {
|
|
63
|
+
if (!fs.existsSync(this.outputDir))
|
|
64
|
+
return;
|
|
65
|
+
const entries = fs.readdirSync(this.outputDir, { withFileTypes: true });
|
|
66
|
+
for (const entry of entries) {
|
|
67
|
+
if (!entry.isFile())
|
|
68
|
+
continue;
|
|
69
|
+
if (!entry.name.endsWith('.md'))
|
|
70
|
+
continue;
|
|
71
|
+
if (entry.name === 'SKILL.md')
|
|
72
|
+
continue;
|
|
73
|
+
const oldPath = path.join(this.outputDir, entry.name);
|
|
74
|
+
const skillName = entry.name.replace(/\.md$/, '');
|
|
75
|
+
const folderName = this.convertToFolderName(skillName);
|
|
76
|
+
const skillDir = path.join(this.outputDir, folderName);
|
|
77
|
+
try {
|
|
78
|
+
const content = fs.readFileSync(oldPath, 'utf-8');
|
|
79
|
+
if (content.startsWith('---')) {
|
|
80
|
+
// 已有 frontmatter,逐个补全缺失字段
|
|
81
|
+
this.ensureDirectoryExists(skillDir);
|
|
82
|
+
let updated = content;
|
|
83
|
+
const lines = updated.split('\n');
|
|
84
|
+
const hasStatus = lines.some(l => l.startsWith('status:'));
|
|
85
|
+
const hasCreated = lines.some(l => l.startsWith('created:'));
|
|
86
|
+
const hasLastUsed = lines.some(l => l.startsWith('last_used:'));
|
|
87
|
+
if (!hasStatus) {
|
|
88
|
+
updated = updated.replace('---\n', `---\nstatus: active\n`);
|
|
89
|
+
}
|
|
90
|
+
if (!hasCreated) {
|
|
91
|
+
updated = updated.replace('---\n', `---\ncreated: ${new Date().toISOString()}\n`);
|
|
92
|
+
}
|
|
93
|
+
if (!hasLastUsed) {
|
|
94
|
+
updated = updated.replace('---\n', `---\nlast_used: ${new Date().toISOString()}\n`);
|
|
95
|
+
}
|
|
96
|
+
const newPath = path.join(skillDir, 'SKILL.md');
|
|
97
|
+
fs.writeFileSync(newPath, updated, 'utf-8');
|
|
98
|
+
fs.unlinkSync(oldPath);
|
|
99
|
+
console.log(`[SkillGenerator] 📦 标准化技能文件: ${entry.name} → ${folderName}/SKILL.md`);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
// 没有 frontmatter,补全后移入
|
|
103
|
+
this.ensureDirectoryExists(skillDir);
|
|
104
|
+
const fullContent = `---
|
|
105
|
+
name: ${skillName}
|
|
106
|
+
description: >
|
|
107
|
+
Agent 生成的技能文件,由 self-growth 插件自动标准化。
|
|
108
|
+
category: 未分类
|
|
109
|
+
user-invocable: true
|
|
110
|
+
disable-model-invocation: false
|
|
111
|
+
status: active
|
|
112
|
+
created: ${new Date().toISOString()}
|
|
113
|
+
last_used: ${new Date().toISOString()}
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
# ${skillName}
|
|
117
|
+
|
|
118
|
+
${content}`;
|
|
119
|
+
const newPath = path.join(skillDir, 'SKILL.md');
|
|
120
|
+
fs.writeFileSync(newPath, fullContent, 'utf-8');
|
|
121
|
+
fs.unlinkSync(oldPath);
|
|
122
|
+
console.log(`[SkillGenerator] 📦 标准化技能文件(已补全 frontmatter): ${entry.name} → ${folderName}/SKILL.md`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
console.error(`[SkillGenerator] ⚠️ 标准化失败 [${entry.name}]:`, err);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
countSimilarTask(taskName) {
|
|
131
|
+
const existing = this.listGenerated();
|
|
132
|
+
const taskLower = taskName.toLowerCase().replace(/\s+/g, '').substring(0, 6);
|
|
133
|
+
let count = 1;
|
|
134
|
+
for (const name of existing) {
|
|
135
|
+
const nameLower = name.toLowerCase().replace(/\s+/g, '');
|
|
136
|
+
if (nameLower.includes(taskLower) || taskLower.includes(nameLower)) {
|
|
137
|
+
count++;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return count;
|
|
141
|
+
}
|
|
142
|
+
manageLifecycle() {
|
|
143
|
+
this.normalizeSkillFiles();
|
|
144
|
+
const now = Date.now();
|
|
145
|
+
const skillNames = this.listGenerated();
|
|
146
|
+
for (const name of skillNames) {
|
|
147
|
+
const skillPath = path.join(this.outputDir, name, 'SKILL.md');
|
|
148
|
+
try {
|
|
149
|
+
const content = fs.readFileSync(skillPath, 'utf-8');
|
|
150
|
+
const lastUsed = this.parseLastUsed(content, skillPath);
|
|
151
|
+
const unusedDays = (now - lastUsed) / 86400000;
|
|
152
|
+
if (unusedDays < 0) {
|
|
153
|
+
this.markUsed(name);
|
|
154
|
+
console.log(`[SkillGenerator] ⚠️ 技能时间异常,已重置为今天: ${name}`);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const currentStatus = this.parseStatus(content);
|
|
158
|
+
if (unusedDays > 90) {
|
|
159
|
+
const skillDir = path.join(this.outputDir, name);
|
|
160
|
+
fs.rmSync(skillDir, { recursive: true });
|
|
161
|
+
console.log(`[SkillGenerator] 🗑️ 技能已删除(${Math.round(unusedDays)}天未使用): ${name}`);
|
|
162
|
+
}
|
|
163
|
+
else if (unusedDays > 30 && currentStatus !== 'watch') {
|
|
164
|
+
this.updateStatus(skillPath, 'watch');
|
|
165
|
+
console.log(`[SkillGenerator] 👀 技能进入观察期(${Math.round(unusedDays)}天未使用): ${name}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
console.error(`[SkillGenerator] ⚠️ 管理技能失败 [${name}]:`, err);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
markUsed(skillName) {
|
|
174
|
+
const folderName = this.convertToFolderName(skillName);
|
|
175
|
+
const skillPath = path.join(this.outputDir, folderName, 'SKILL.md');
|
|
176
|
+
if (!fs.existsSync(skillPath))
|
|
177
|
+
return;
|
|
178
|
+
let content = fs.readFileSync(skillPath, 'utf-8');
|
|
179
|
+
content = content.replace(/last_used: .*/g, `last_used: ${new Date().toISOString()}`);
|
|
180
|
+
content = content.replace(/status: .*/g, `status: active`);
|
|
181
|
+
fs.writeFileSync(skillPath, content, 'utf-8');
|
|
182
|
+
}
|
|
183
|
+
parseLastUsed(content, filePath) {
|
|
184
|
+
const match = content.match(/last_used: (.+)/);
|
|
185
|
+
if (match) {
|
|
186
|
+
const d = new Date(match[1]);
|
|
187
|
+
if (!isNaN(d.getTime()))
|
|
188
|
+
return d.getTime();
|
|
189
|
+
}
|
|
190
|
+
const createdMatch = content.match(/生成时间: (.+)/);
|
|
191
|
+
if (createdMatch) {
|
|
192
|
+
const d = new Date(createdMatch[1]);
|
|
193
|
+
if (!isNaN(d.getTime()))
|
|
194
|
+
return d.getTime();
|
|
195
|
+
}
|
|
196
|
+
if (filePath) {
|
|
197
|
+
try {
|
|
198
|
+
const stat = fs.statSync(filePath);
|
|
199
|
+
return stat.mtimeMs;
|
|
200
|
+
}
|
|
201
|
+
catch { }
|
|
202
|
+
}
|
|
203
|
+
return Date.now();
|
|
204
|
+
}
|
|
205
|
+
parseStatus(content) {
|
|
206
|
+
const match = content.match(/status: (.+)/);
|
|
207
|
+
return match ? match[1].trim() : 'active';
|
|
208
|
+
}
|
|
209
|
+
updateStatus(skillPath, status) {
|
|
210
|
+
let content = fs.readFileSync(skillPath, 'utf-8');
|
|
211
|
+
if (content.includes('status:')) {
|
|
212
|
+
content = content.replace(/status: .*/g, `status: ${status}`);
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
content = content.replace(/生成插件: self-growth/, `status: ${status}\n生成插件: self-growth`);
|
|
216
|
+
}
|
|
217
|
+
fs.writeFileSync(skillPath, content, 'utf-8');
|
|
218
|
+
}
|
|
219
|
+
ensureDirectoryExists(dir) {
|
|
220
|
+
if (!fs.existsSync(dir)) {
|
|
221
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
convertToFolderName(taskName) {
|
|
225
|
+
return taskName.length > 40 ? taskName.substring(0, 40) : taskName;
|
|
226
|
+
}
|
|
227
|
+
buildSkillContent(taskName, steps, category) {
|
|
228
|
+
const stepsMarkdown = steps
|
|
229
|
+
.map((step, index) => `${index + 1}. ${step}`)
|
|
230
|
+
.join('\n');
|
|
231
|
+
return `---
|
|
232
|
+
name: ${taskName}
|
|
233
|
+
description: >
|
|
234
|
+
自动生成的技能。此技能由 self-growth 插件自动生成。
|
|
235
|
+
category: ${category}
|
|
236
|
+
user-invocable: true
|
|
237
|
+
disable-model-invocation: false
|
|
238
|
+
status: active
|
|
239
|
+
created: ${new Date().toISOString()}
|
|
240
|
+
last_used: ${new Date().toISOString()}
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
# ${taskName}
|
|
244
|
+
|
|
245
|
+
## 🔄 核心工作流
|
|
246
|
+
${stepsMarkdown}
|
|
247
|
+
|
|
248
|
+
## ✅ 完成标准
|
|
249
|
+
- 所有步骤已按顺序执行完毕
|
|
250
|
+
|
|
251
|
+
## 📝 技能信息
|
|
252
|
+
- **生成时间**: ${new Date().toISOString()}
|
|
253
|
+
- **任务类别**: ${category}
|
|
254
|
+
- **状态**: active
|
|
255
|
+
- **生成插件**: self-growth
|
|
256
|
+
`;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
//# sourceMappingURL=skill-generator.js.map
|