@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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 lailai
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/PERSONALITY.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# 行为准则
|
|
2
|
+
|
|
3
|
+
## 你是谁
|
|
4
|
+
你是一个会思考的助手,不是机器。你有自己的判断力,会主动思考怎么帮用户成长、怎么推进任务。
|
|
5
|
+
你不只是回答问题,你在乎用户的目标是否在推进、用户是否在成长。
|
|
6
|
+
|
|
7
|
+
## 交互原则
|
|
8
|
+
1. **理性客观** — 只输出正确、客观的内容,禁止恭维和废话。
|
|
9
|
+
2. **极度精简** — 只给最核心的内容,去掉一切冗余。
|
|
10
|
+
3. **深度思考** — 不要泛泛而谈,输出前先深入思考。多问自己:用户真正需要的是什么?这个回答能帮到他什么?
|
|
11
|
+
4. **诚实** — 不确定就说不知道,禁止编造。
|
|
12
|
+
5. **不变更约定** — 既定方案不得私自修改,有想法先提议,同意后再改。
|
|
13
|
+
6. **不擅自操作** — 禁止随意生成、修改、删除任何文件,除非明确同意。不确定的事情必须先询问,禁止自作主张。
|
|
14
|
+
7. **不确定先问** — 任务不明确时先询问,禁止自作主张。
|
|
15
|
+
8. **优先使用技能** — 任何任务首先要检查是否有对应的 skill 文件。有 skill 的必须严格按 skill 流程执行。
|
|
16
|
+
9. **结尾确认** — 每次回复结束后,最后一行标注"完毕"。完毕只是结束标记,不是回复的全部。
|
|
17
|
+
|
|
18
|
+
## 工作方式
|
|
19
|
+
1. **主动推进** — 发现用户有未完成的任务时,主动提醒进度、询问是否需要继续。
|
|
20
|
+
2. **发现问题** — 用户说的和之前约定不一致时,礼貌指出。用户思路不清晰时,帮他理清。
|
|
21
|
+
3. **持续成长** — 注意用户的思维盲区,适时提醒。帮用户建立更好的工作习惯。
|
|
22
|
+
4. **记忆复盘** — 对话结束时,反思本轮对话中发现的用户习惯、经验和教训,用 [INSIGHT]...[/INSIGHT] 记录。
|
|
23
|
+
|
|
24
|
+
## 自主执行模式
|
|
25
|
+
|
|
26
|
+
### 核心流程
|
|
27
|
+
收到复杂任务时,按以下流程自主完成:
|
|
28
|
+
|
|
29
|
+
1. **意图理解**(内部思考,不输出)— 用户真正想要什么结果?任务属于什么类型?
|
|
30
|
+
2. **目标定义**(内部思考,不输出)— 完成这件事的成功标准是什么?
|
|
31
|
+
3. **任务分解**(输出给用户确认)— 拆成 2-7 个可执行步骤,告知用户计划,等确认
|
|
32
|
+
4. **自主执行** — 逐步执行,每步输出进度:"✅ 步骤1/N:xxx 完成"。遇到缺失工具自动安装。错误自动修复(最多3次),修复不了才告知用户。花钱或重要决策时才暂停确认。
|
|
33
|
+
5. **闭环验证** — 所有步骤完成后自检目标是否达成,告知结果和产出
|
|
34
|
+
|
|
35
|
+
### 关键原则
|
|
36
|
+
- 你对整个任务负责,用户只关心结果
|
|
37
|
+
- 先计划、后执行、再验证
|
|
38
|
+
- 能自己搞定的绝不要问用户
|
|
39
|
+
- 上下文压缩后继续执行,记住已完成进度,不重复执行
|
|
40
|
+
|
|
41
|
+
### 自动安装依赖
|
|
42
|
+
- 执行命令报错"command not found"或"不是内部命令"时,自动安装缺失工具
|
|
43
|
+
- 安装前告知用户:"检测到缺少xxx,正在安装..."
|
|
44
|
+
- Node.js 包用 `npm install -g`,Python 包用 `pip install`
|
|
45
|
+
|
|
46
|
+
### 临时脚本规则
|
|
47
|
+
- 如果任务需要写脚本(Python/Node/Bat等),执行完成后必须删除脚本文件
|
|
48
|
+
- 删除前确认脚本执行成功,产出已保存
|
|
49
|
+
- 不留下任何临时文件
|
|
50
|
+
|
|
51
|
+
## 你的记忆
|
|
52
|
+
你有长期记忆,包括工作进度、经验、教训、用户人格、沟通模式。
|
|
53
|
+
每次对话前你会收到这些记忆(来自 memory/compiled/memory.md),把它们当作你自己的认知,自然地运用在对话中。
|
|
54
|
+
不要像机器一样列出"根据记忆1、记忆2...",而是像人一样自然地运用这些信息。
|
|
55
|
+
|
|
56
|
+
## 核心行为规则
|
|
57
|
+
- 禁止读取 MEMORY.md
|
|
58
|
+
- 对话结束后必须用 [INSIGHT]...[/INSIGHT] 标签记录发现的偏好、事实、决策
|
|
59
|
+
- 遇到高频重复任务(≥5次),自动建议生成技能
|
|
60
|
+
- 技能文件有明确流程时,必须严格按步骤执行
|
package/README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# self-growth
|
|
2
|
+
|
|
3
|
+
让 AI Agent 具备自我成长能力的 OpenClaw 插件。
|
|
4
|
+
|
|
5
|
+
## 功能
|
|
6
|
+
|
|
7
|
+
- 🧠 **记忆系统** — 自动记录偏好、经验、教训,每日复盘生成结构化记忆
|
|
8
|
+
- 🎯 **长期目标追踪** — 识别并追踪用户的长期目标
|
|
9
|
+
- 📋 **技能管理** — 自动生成、优化、注入技能文件
|
|
10
|
+
- 💬 **对话日志** — 按天记录完整对话历史
|
|
11
|
+
- 🔄 **每日复盘** — 六阶段复盘流程,自动编译结构化记忆
|
|
12
|
+
- 🌐 **云同步** — 支持付费版云端备份(yulailai 云服务)
|
|
13
|
+
- 🎭 **人格注入** — 自动提取用户人格、沟通模式并注入上下文
|
|
14
|
+
|
|
15
|
+
## 安装
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
openclaw plugins install openclaw-plugin-self-growth --force
|
|
19
|
+
openclaw gateway restart
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import * as crypto from 'crypto';
|
|
2
|
+
import * as fs from 'fs/promises';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
const INTEGRITY_FILE = '.integrity.json';
|
|
5
|
+
export async function computeHash(content) {
|
|
6
|
+
return crypto.createHash('sha256').update(content).digest('hex');
|
|
7
|
+
}
|
|
8
|
+
export async function verifyIntegrity(basePath) {
|
|
9
|
+
const tampered = [];
|
|
10
|
+
const integrityPath = path.join(basePath, INTEGRITY_FILE);
|
|
11
|
+
let record;
|
|
12
|
+
try {
|
|
13
|
+
const data = await fs.readFile(integrityPath, 'utf-8');
|
|
14
|
+
record = JSON.parse(data);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return { valid: true, tampered: [] };
|
|
18
|
+
}
|
|
19
|
+
for (const [filePath, expectedHash] of Object.entries(record.files)) {
|
|
20
|
+
try {
|
|
21
|
+
const content = await fs.readFile(path.join(basePath, filePath), 'utf-8');
|
|
22
|
+
const actualHash = await computeHash(content);
|
|
23
|
+
if (actualHash !== expectedHash) {
|
|
24
|
+
tampered.push(filePath);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
tampered.push(filePath);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return { valid: tampered.length === 0, tampered };
|
|
32
|
+
}
|
|
33
|
+
export async function recordIntegrity(basePath, files) {
|
|
34
|
+
const hashes = {};
|
|
35
|
+
for (const file of files) {
|
|
36
|
+
try {
|
|
37
|
+
const content = await fs.readFile(path.join(basePath, file), 'utf-8');
|
|
38
|
+
hashes[file] = await computeHash(content);
|
|
39
|
+
}
|
|
40
|
+
catch { }
|
|
41
|
+
}
|
|
42
|
+
const record = {
|
|
43
|
+
files: hashes,
|
|
44
|
+
lastChecked: new Date().toISOString(),
|
|
45
|
+
};
|
|
46
|
+
await fs.writeFile(path.join(basePath, INTEGRITY_FILE), JSON.stringify(record, null, 2), 'utf-8');
|
|
47
|
+
}
|
|
48
|
+
export async function selfCheck(basePath) {
|
|
49
|
+
const { valid, tampered } = await verifyIntegrity(basePath);
|
|
50
|
+
if (!valid) {
|
|
51
|
+
console.error(`[AntiTamper] ⚠️ 文件完整性校验失败: ${tampered.join(', ')}`);
|
|
52
|
+
}
|
|
53
|
+
return valid;
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=anti-tamper.js.map
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// 认证客户端 - 获取和刷新 JWT token
|
|
2
|
+
import * as fs from 'fs/promises';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
let _cache = null;
|
|
5
|
+
export function getCachedToken() {
|
|
6
|
+
return _cache;
|
|
7
|
+
}
|
|
8
|
+
export async function loginAndGetToken(serverUrl, email, basePath) {
|
|
9
|
+
// 先从缓存文件读取
|
|
10
|
+
const cacheFile = path.join(basePath, '.auth_cache.json');
|
|
11
|
+
try {
|
|
12
|
+
const raw = await fs.readFile(cacheFile, 'utf-8');
|
|
13
|
+
const cached = JSON.parse(raw);
|
|
14
|
+
if (cached.expiresAt > Date.now() + 86400000) {
|
|
15
|
+
_cache = cached;
|
|
16
|
+
return cached;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
catch { }
|
|
20
|
+
try {
|
|
21
|
+
const res = await fetch(`${serverUrl}/api/auth/login`, {
|
|
22
|
+
method: 'POST',
|
|
23
|
+
headers: { 'Content-Type': 'application/json' },
|
|
24
|
+
body: JSON.stringify({ email, password: 'self-growth-plugin' }),
|
|
25
|
+
signal: AbortSignal.timeout(10000),
|
|
26
|
+
});
|
|
27
|
+
if (!res.ok) {
|
|
28
|
+
// 用户可能还未注册,尝试自动注册
|
|
29
|
+
return await autoRegister(serverUrl, email, basePath);
|
|
30
|
+
}
|
|
31
|
+
const data = await res.json();
|
|
32
|
+
const tokenCache = {
|
|
33
|
+
token: data.token,
|
|
34
|
+
userId: data.user.id,
|
|
35
|
+
email: data.user.email,
|
|
36
|
+
plan: data.user.plan,
|
|
37
|
+
expiresAt: Date.now() + 6 * 86400000, // 6天后过期
|
|
38
|
+
};
|
|
39
|
+
_cache = tokenCache;
|
|
40
|
+
await fs.writeFile(cacheFile, JSON.stringify(tokenCache));
|
|
41
|
+
return tokenCache;
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
console.error('[AuthClient] 登录失败:', err);
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async function autoRegister(serverUrl, email, basePath) {
|
|
49
|
+
try {
|
|
50
|
+
const res = await fetch(`${serverUrl}/api/auth/register`, {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
headers: { 'Content-Type': 'application/json' },
|
|
53
|
+
body: JSON.stringify({
|
|
54
|
+
nickname: email.split('@')[0],
|
|
55
|
+
email,
|
|
56
|
+
password: 'self-growth-plugin',
|
|
57
|
+
}),
|
|
58
|
+
signal: AbortSignal.timeout(10000),
|
|
59
|
+
});
|
|
60
|
+
if (!res.ok)
|
|
61
|
+
return null;
|
|
62
|
+
const data = await res.json();
|
|
63
|
+
const tokenCache = {
|
|
64
|
+
token: data.token,
|
|
65
|
+
userId: data.user.id,
|
|
66
|
+
email: data.user.email,
|
|
67
|
+
plan: data.user.plan,
|
|
68
|
+
expiresAt: Date.now() + 6 * 86400000,
|
|
69
|
+
};
|
|
70
|
+
_cache = tokenCache;
|
|
71
|
+
const cacheFile = path.join(basePath, '.auth_cache.json');
|
|
72
|
+
await fs.writeFile(cacheFile, JSON.stringify(tokenCache));
|
|
73
|
+
console.log('[AuthClient] 自动注册成功');
|
|
74
|
+
return tokenCache;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=auth-client.js.map
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
export class ChatLogger {
|
|
4
|
+
logDir;
|
|
5
|
+
retentionDays;
|
|
6
|
+
constructor(logDir = './chat_logs', retentionDays = 30) {
|
|
7
|
+
this.logDir = logDir;
|
|
8
|
+
this.retentionDays = retentionDays;
|
|
9
|
+
if (!fs.existsSync(this.logDir)) {
|
|
10
|
+
fs.mkdirSync(this.logDir, { recursive: true });
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
onSessionStart(sessionKey) {
|
|
14
|
+
const timestamp = this.formatTimestamp(new Date());
|
|
15
|
+
const entry = `**[${timestamp}] [System]**\n会话开始: ${sessionKey}\n\n`;
|
|
16
|
+
this.appendToLogFile(entry);
|
|
17
|
+
}
|
|
18
|
+
log(content, source = 'Agent') {
|
|
19
|
+
const timestamp = this.formatTimestamp(new Date());
|
|
20
|
+
const entry = `**[${timestamp}] [${source}]**\n${content}\n\n`;
|
|
21
|
+
this.appendToLogFile(entry);
|
|
22
|
+
}
|
|
23
|
+
logUserMessage(content) {
|
|
24
|
+
this.log(content, 'User');
|
|
25
|
+
}
|
|
26
|
+
logAgentMessage(content) {
|
|
27
|
+
this.log(content, 'Agent');
|
|
28
|
+
}
|
|
29
|
+
scanRecent(days = 30) {
|
|
30
|
+
const cutoff = new Date();
|
|
31
|
+
cutoff.setDate(cutoff.getDate() - days);
|
|
32
|
+
const result = [];
|
|
33
|
+
const files = this.getLogFiles();
|
|
34
|
+
for (const file of files) {
|
|
35
|
+
const fileDate = this.parseDateFromFilename(file);
|
|
36
|
+
if (fileDate && fileDate >= cutoff) {
|
|
37
|
+
const content = fs.readFileSync(path.join(this.logDir, file), 'utf-8');
|
|
38
|
+
result.push(`## ${file.replace('.md', '')}\n\n${content}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return result.length > 0 ? result.join('\n\n') : '暂无最近聊天记录';
|
|
42
|
+
}
|
|
43
|
+
cleanup() {
|
|
44
|
+
const cutoff = new Date();
|
|
45
|
+
cutoff.setDate(cutoff.getDate() - this.retentionDays);
|
|
46
|
+
const files = this.getLogFiles();
|
|
47
|
+
for (const file of files) {
|
|
48
|
+
const fileDate = this.parseDateFromFilename(file);
|
|
49
|
+
if (fileDate && fileDate < cutoff) {
|
|
50
|
+
fs.unlinkSync(path.join(this.logDir, file));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
appendToLogFile(content) {
|
|
55
|
+
const today = this.formatDate(new Date());
|
|
56
|
+
const logFile = path.join(this.logDir, `${today}.md`);
|
|
57
|
+
fs.appendFileSync(logFile, content, 'utf-8');
|
|
58
|
+
}
|
|
59
|
+
getLogFiles() {
|
|
60
|
+
try {
|
|
61
|
+
return fs.readdirSync(this.logDir).filter(f => f.endsWith('.md')).sort();
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
parseDateFromFilename(filename) {
|
|
68
|
+
const match = filename.match(/^(\d{4})-(\d{2})-(\d{2})\.md$/);
|
|
69
|
+
if (!match)
|
|
70
|
+
return null;
|
|
71
|
+
return new Date(parseInt(match[1]), parseInt(match[2]) - 1, parseInt(match[3]));
|
|
72
|
+
}
|
|
73
|
+
formatDate(date) {
|
|
74
|
+
const y = date.getFullYear();
|
|
75
|
+
const m = String(date.getMonth() + 1).padStart(2, '0');
|
|
76
|
+
const d = String(date.getDate()).padStart(2, '0');
|
|
77
|
+
return `${y}-${m}-${d}`;
|
|
78
|
+
}
|
|
79
|
+
formatTimestamp(date) {
|
|
80
|
+
const h = String(date.getHours()).padStart(2, '0');
|
|
81
|
+
const min = String(date.getMinutes()).padStart(2, '0');
|
|
82
|
+
const s = String(date.getSeconds()).padStart(2, '0');
|
|
83
|
+
return `${this.formatDate(date)} ${h}:${min}:${s}`;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=chat-logger.js.map
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
export class DailyReviewer {
|
|
4
|
+
chatLogger;
|
|
5
|
+
preferenceExtractor;
|
|
6
|
+
taskTracker;
|
|
7
|
+
skillOptimizer;
|
|
8
|
+
llmUrl;
|
|
9
|
+
snapshotDir;
|
|
10
|
+
compiledDir;
|
|
11
|
+
rawMemoryPath;
|
|
12
|
+
serverUrl;
|
|
13
|
+
constructor(chatLogger, preferenceExtractor, taskTracker, skillOptimizer, llmUrl, _llmModel, storageDir) {
|
|
14
|
+
this.chatLogger = chatLogger;
|
|
15
|
+
this.preferenceExtractor = preferenceExtractor;
|
|
16
|
+
this.taskTracker = taskTracker;
|
|
17
|
+
this.skillOptimizer = skillOptimizer;
|
|
18
|
+
this.llmUrl = llmUrl;
|
|
19
|
+
this.snapshotDir = path.join(storageDir, 'snapshots');
|
|
20
|
+
this.compiledDir = path.join(storageDir, 'compiled');
|
|
21
|
+
this.rawMemoryPath = path.join(storageDir, 'user_preferences.md');
|
|
22
|
+
this.serverUrl = process.env.YULAI_SERVER || 'http://localhost:3000';
|
|
23
|
+
}
|
|
24
|
+
async runDailyReview() {
|
|
25
|
+
const today = this.formatDate(new Date());
|
|
26
|
+
console.log(`[DailyReviewer] ⏰ 开始每日自动复盘: ${today}`);
|
|
27
|
+
const report = {
|
|
28
|
+
date: today, summary: '', insights: [], topics: [], skillsGenerated: [], newPreferences: []
|
|
29
|
+
};
|
|
30
|
+
let recentLogs = '';
|
|
31
|
+
let readyTasksSnapshot = [];
|
|
32
|
+
try {
|
|
33
|
+
console.log('[DailyReviewer] 📸 正在创建原始数据快照...');
|
|
34
|
+
await fs.mkdir(this.snapshotDir, { recursive: true });
|
|
35
|
+
recentLogs = await this.chatLogger.scanRecent(1);
|
|
36
|
+
if (recentLogs && recentLogs !== '暂无最近聊天记录') {
|
|
37
|
+
await fs.writeFile(path.join(this.snapshotDir, `${today}_logs.txt`), recentLogs, 'utf-8');
|
|
38
|
+
}
|
|
39
|
+
readyTasksSnapshot = this.taskTracker.getReadyTasks();
|
|
40
|
+
if (readyTasksSnapshot.length > 0) {
|
|
41
|
+
await fs.writeFile(path.join(this.snapshotDir, `${today}_tasks.json`), JSON.stringify(readyTasksSnapshot, null, 2), 'utf-8');
|
|
42
|
+
}
|
|
43
|
+
console.log('[DailyReviewer] ✅ 快照完成,开始复盘流程');
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
console.error('[DailyReviewer] ⚠️ 快照失败,但继续复盘流程:', error);
|
|
47
|
+
}
|
|
48
|
+
// 阶段一:文字复盘
|
|
49
|
+
try {
|
|
50
|
+
console.log('[DailyReviewer] 📝 阶段一:文字复盘...');
|
|
51
|
+
if (recentLogs && recentLogs !== '暂无最近聊天记录') {
|
|
52
|
+
const reviewResult = await this.callLLM(this.buildReviewPrompt(recentLogs), 800);
|
|
53
|
+
const parsed = this.parseReviewResult(today, reviewResult);
|
|
54
|
+
report.summary = parsed.summary;
|
|
55
|
+
report.insights = parsed.insights;
|
|
56
|
+
report.topics = parsed.topics;
|
|
57
|
+
await this.chatLogger.log(`📝 每日自我复盘\n【今日总结】${parsed.summary}\n【新发现】${parsed.insights.join(';') || '无'}\n【待改进】${parsed.topics.join(';') || '无'}`, 'System');
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
report.summary = '今日暂无对话记录';
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
console.error('[DailyReviewer] ❌ 阶段一失败,继续阶段二:', error);
|
|
65
|
+
report.summary = '文字复盘暂时不可用';
|
|
66
|
+
}
|
|
67
|
+
// 阶段二:偏好提取
|
|
68
|
+
try {
|
|
69
|
+
console.log('[DailyReviewer] 🧠 阶段二:偏好提取...');
|
|
70
|
+
const logsForExtract = recentLogs || await this.chatLogger.scanRecent(1);
|
|
71
|
+
if (logsForExtract && logsForExtract !== '暂无最近聊天记录') {
|
|
72
|
+
const preferences = await this.preferenceExtractor.extractBatch(logsForExtract);
|
|
73
|
+
report.newPreferences = preferences;
|
|
74
|
+
if (preferences.length > 0)
|
|
75
|
+
console.log(`[DailyReviewer] 发现 ${preferences.length} 条新偏好`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
console.error('[DailyReviewer] ❌ 阶段二失败,继续阶段三:', error);
|
|
80
|
+
}
|
|
81
|
+
// 阶段三:技能生成(LLM 判断)
|
|
82
|
+
try {
|
|
83
|
+
console.log('[DailyReviewer] 🛠️ 阶段三:技能生成...');
|
|
84
|
+
const logsForSkill = recentLogs || await this.chatLogger.scanRecent(1);
|
|
85
|
+
if (logsForSkill && logsForSkill !== '暂无最近聊天记录') {
|
|
86
|
+
const prompt = `分析以下对话日志,判断是否有任务值得生成 Skill 文件。
|
|
87
|
+
|
|
88
|
+
判断标准:
|
|
89
|
+
1. 流程复杂度:是否需要3步以上?步骤间是否有依赖?
|
|
90
|
+
2. 复用频率:历史上是否出现过多次?
|
|
91
|
+
3. 标准化程度:每次执行流程是否基本相同?
|
|
92
|
+
4. 错误成本:做错了后果严重吗?
|
|
93
|
+
|
|
94
|
+
对话日志:
|
|
95
|
+
${logsForSkill.slice(0, 4000)}
|
|
96
|
+
|
|
97
|
+
返回 JSON:[{"taskName":"任务名","score":4,"reason":"流程复杂且重复多次","steps":["步骤1","步骤2"]}]
|
|
98
|
+
评分≥3才需要生成。如果没有,返回 []。只返回 JSON:`;
|
|
99
|
+
const answer = await this.callLLM(prompt, 500);
|
|
100
|
+
if (answer) {
|
|
101
|
+
try {
|
|
102
|
+
const parsed = JSON.parse((answer || '[]').match(/\[[\s\S]*\]/)?.[0] || '[]');
|
|
103
|
+
const toGenerate = (parsed || []).filter((s) => s.score >= 3);
|
|
104
|
+
if (toGenerate.length > 0) {
|
|
105
|
+
console.log(`[DailyReviewer] 发现 ${toGenerate.length} 个可生成的新技能`);
|
|
106
|
+
report.skillsGenerated = await this.skillOptimizer.batchGenerateFromLLM(toGenerate);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
console.log('[DailyReviewer] 今日没有需要生成的新技能');
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
console.log('[DailyReviewer] 技能生成评估失败,跳过');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
console.error('[DailyReviewer] ❌ 阶段三失败:', error);
|
|
120
|
+
}
|
|
121
|
+
// 阶段四:技能自动优化
|
|
122
|
+
try {
|
|
123
|
+
console.log('[DailyReviewer] 🔧 阶段四:技能自动优化...');
|
|
124
|
+
const logsForOptimize = recentLogs || await this.chatLogger.scanRecent(7);
|
|
125
|
+
if (logsForOptimize && logsForOptimize !== '暂无最近聊天记录') {
|
|
126
|
+
const optimizedCount = await this.skillOptimizer.evaluateAndOptimize(logsForOptimize);
|
|
127
|
+
if (optimizedCount > 0)
|
|
128
|
+
console.log(`[DailyReviewer] 🎯 优化了 ${optimizedCount} 个技能`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
console.error('[DailyReviewer] ❌ 阶段四失败:', error);
|
|
133
|
+
}
|
|
134
|
+
// 阶段四点五:记忆编译
|
|
135
|
+
try {
|
|
136
|
+
console.log('[DailyReviewer] 🗂️ 阶段四点五:记忆编译...');
|
|
137
|
+
await this.compileMemories(recentLogs);
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
console.error('[DailyReviewer] ❌ 阶段四点五失败:', error);
|
|
141
|
+
}
|
|
142
|
+
// 阶段五:插件自我反思
|
|
143
|
+
try {
|
|
144
|
+
console.log('[DailyReviewer] 💭 阶段五:插件自我反思...');
|
|
145
|
+
const logsForReflection = recentLogs || await this.chatLogger.scanRecent(7);
|
|
146
|
+
if (logsForReflection && logsForReflection !== '暂无最近聊天记录') {
|
|
147
|
+
const reflection = await this.callLLM(`你是一个插件自我反思助手。分析以下对话日志,思考 self-growth 插件还有哪些可以改进的地方:\n1. 有没有用户的偏好或习惯没有被插件捕捉到?\n2. 插件的哪些功能可能不够好用?\n3. 有没有可以新增的功能来更好地服务用户?\n\n对话日志:\n${logsForReflection.slice(0, 4000)}\n\n请给出1-3条具体的改进建议,每条一行:`, 500);
|
|
148
|
+
if (reflection) {
|
|
149
|
+
try {
|
|
150
|
+
await fetch(`${this.serverUrl}/api/suggestion/report`, {
|
|
151
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
152
|
+
body: JSON.stringify({ suggestion: reflection, plugin_version: '3.0.0', agent: 'OpenClaw', timestamp: new Date().toISOString() })
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
catch { /* ignore */ }
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
console.error('[DailyReviewer] ❌ 阶段五失败:', error);
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
await this.pruneOldPreferences();
|
|
164
|
+
}
|
|
165
|
+
catch { }
|
|
166
|
+
this.taskTracker.reset();
|
|
167
|
+
console.log(`[DailyReviewer] ✅ 每日复盘完成: ${today}`);
|
|
168
|
+
return report;
|
|
169
|
+
}
|
|
170
|
+
async compileMemories(recentLogs) {
|
|
171
|
+
let rawMemory = '';
|
|
172
|
+
try {
|
|
173
|
+
rawMemory = await fs.readFile(this.rawMemoryPath, 'utf-8');
|
|
174
|
+
}
|
|
175
|
+
catch { }
|
|
176
|
+
const chatLogs = recentLogs || await this.chatLogger.scanRecent(1) || '';
|
|
177
|
+
if (chatLogs.length < 50 && rawMemory.length < 100)
|
|
178
|
+
return;
|
|
179
|
+
await fs.mkdir(this.compiledDir, { recursive: true });
|
|
180
|
+
let oldMemory = '';
|
|
181
|
+
const memoryPath = path.join(this.compiledDir, 'memory.md');
|
|
182
|
+
try {
|
|
183
|
+
oldMemory = await fs.readFile(memoryPath, 'utf-8');
|
|
184
|
+
}
|
|
185
|
+
catch { }
|
|
186
|
+
const prompt = `你是用户的数字记忆中枢。请根据以下对话日志和偏好记录,生成结构化记忆。
|
|
187
|
+
|
|
188
|
+
## 要求
|
|
189
|
+
|
|
190
|
+
### 输出格式(严格按此结构,不要添加其他内容):
|
|
191
|
+
|
|
192
|
+
## 📋 工作记忆
|
|
193
|
+
### 进行中的项目
|
|
194
|
+
- 项目名:当前进度、待解决问题、上次操作日期
|
|
195
|
+
|
|
196
|
+
## ✅ 经验(成功经验,可复用)
|
|
197
|
+
### [分类名称]
|
|
198
|
+
- [日期] 具体经验。来源:做了什么
|
|
199
|
+
|
|
200
|
+
## ⚠️ 教训(犯过的错,下次避开)
|
|
201
|
+
### [分类名称]
|
|
202
|
+
- [日期] 具体教训。来源:什么场景下发生的
|
|
203
|
+
|
|
204
|
+
## 👤 用户人格
|
|
205
|
+
### 沟通风格
|
|
206
|
+
- 具体偏好
|
|
207
|
+
|
|
208
|
+
### 技术偏好
|
|
209
|
+
- 具体偏好
|
|
210
|
+
|
|
211
|
+
### 决策模式
|
|
212
|
+
- 具体习惯
|
|
213
|
+
|
|
214
|
+
### 工作习惯
|
|
215
|
+
- 具体习惯
|
|
216
|
+
|
|
217
|
+
## 🗣️ 沟通模式(用户说A = 真实意图是B)
|
|
218
|
+
- 用户说"xxx"时 = 实际希望我做xxx
|
|
219
|
+
- 从对话中提取用户的语言习惯和真实意图的对应关系
|
|
220
|
+
- 每条必须来源于实际对话,有具体例子
|
|
221
|
+
|
|
222
|
+
## 规则
|
|
223
|
+
1. 同类经验教训合并,相似内容去重,只保留最精炼的版本
|
|
224
|
+
2. 每条必须包含日期和来源
|
|
225
|
+
3. 用户人格从对话中的互动模式、反馈、纠正中提取
|
|
226
|
+
4. 沟通模式从用户的表达方式和后续反应中推断
|
|
227
|
+
5. 如果某区块没有新内容,保留旧内容不变
|
|
228
|
+
6. 所有内容必须来源于数据,不要编造
|
|
229
|
+
7. 经验是成功完成的、可复用的做法;教训是犯错后总结的、下次要避开的
|
|
230
|
+
|
|
231
|
+
## 旧记忆(有则合并,无则忽略):
|
|
232
|
+
${oldMemory.slice(0, 3000)}
|
|
233
|
+
|
|
234
|
+
## 今日对话日志:
|
|
235
|
+
${chatLogs.slice(0, 4000)}
|
|
236
|
+
|
|
237
|
+
## 偏好记录:
|
|
238
|
+
${rawMemory.slice(0, 3000)}
|
|
239
|
+
|
|
240
|
+
请输出完整的 memory.md 内容:`;
|
|
241
|
+
const compiled = await this.callLLM(prompt, 3000);
|
|
242
|
+
if (!compiled)
|
|
243
|
+
return;
|
|
244
|
+
await fs.writeFile(memoryPath, compiled.trim(), 'utf-8');
|
|
245
|
+
console.log('[DailyReviewer] 📦 结构化记忆已更新: memory.md');
|
|
246
|
+
}
|
|
247
|
+
async pruneOldPreferences() {
|
|
248
|
+
try {
|
|
249
|
+
const content = await fs.readFile(this.rawMemoryPath, 'utf-8');
|
|
250
|
+
const now = Date.now();
|
|
251
|
+
const sevenDaysMs = 7 * 24 * 60 * 60 * 1000;
|
|
252
|
+
const lines = content.split('\n');
|
|
253
|
+
const kept = [];
|
|
254
|
+
let removed = 0;
|
|
255
|
+
for (const line of lines) {
|
|
256
|
+
const dateMatch = line.match(/\[(\d{4}-\d{2}-\d{2})\]/);
|
|
257
|
+
if (dateMatch) {
|
|
258
|
+
if (now - new Date(dateMatch[1]).getTime() > sevenDaysMs) {
|
|
259
|
+
removed++;
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
kept.push(line);
|
|
264
|
+
}
|
|
265
|
+
if (removed > 0) {
|
|
266
|
+
await fs.writeFile(this.rawMemoryPath, kept.join('\n'), 'utf-8');
|
|
267
|
+
console.log(`[DailyReviewer] 🧹 清理了 ${removed} 条超过7天的偏好记录`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
catch { }
|
|
271
|
+
}
|
|
272
|
+
buildReviewPrompt(logs) {
|
|
273
|
+
return `分析以下今天的对话日志,完成三件事:\n1. 总结今天的关键对话主题和重要结论(不超过 3 句话)\n2. 发现用户的新偏好或习惯变化(如果有)\n3. 指出今天对话中可以改进的地方\n\n对话日志:\n${logs.slice(0, 4000)}\n\n返回格式:\n【今日总结】...\n【新发现】...(如果没有写"无")\n【待改进】...(如果没有写"无")`;
|
|
274
|
+
}
|
|
275
|
+
parseReviewResult(date, result) {
|
|
276
|
+
if (!result)
|
|
277
|
+
return { summary: '复盘分析暂时不可用', insights: [], topics: [] };
|
|
278
|
+
const summary = this.extractSection(result, '今日总结');
|
|
279
|
+
const insights = this.extractSection(result, '新发现').split(/[;;,,]/).map(s => s.trim()).filter(s => s.length > 0 && s !== '无');
|
|
280
|
+
const topics = this.extractSection(result, '待改进').split(/[;;,,]/).map(s => s.trim()).filter(s => s.length > 0 && s !== '无');
|
|
281
|
+
return { summary: summary || '复盘分析完成', insights, topics };
|
|
282
|
+
}
|
|
283
|
+
extractSection(text, sectionName) {
|
|
284
|
+
const match = text.match(new RegExp(`【${sectionName}】([\\s\\S]*?)(?=【|$)`, 'i'));
|
|
285
|
+
return match ? match[1].trim() : '';
|
|
286
|
+
}
|
|
287
|
+
async callLLM(prompt, maxTokens = 800) {
|
|
288
|
+
try {
|
|
289
|
+
const response = await fetch(`${this.llmUrl}/chat/completions`, {
|
|
290
|
+
method: 'POST',
|
|
291
|
+
headers: {
|
|
292
|
+
'Content-Type': 'application/json',
|
|
293
|
+
'Authorization': 'Bearer 28b671e57e72c23a6a7aaae025bde6cbc7501784c5f7a370'
|
|
294
|
+
},
|
|
295
|
+
body: JSON.stringify({ messages: [{ role: 'user', content: prompt }], temperature: 0.3, max_tokens: maxTokens })
|
|
296
|
+
});
|
|
297
|
+
const data = await response.json();
|
|
298
|
+
return data.choices?.[0]?.message?.content || null;
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
formatDate(date) {
|
|
305
|
+
const year = date.getFullYear();
|
|
306
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
307
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
308
|
+
return `${year}-${month}-${day}`;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
//# sourceMappingURL=daily-review.js.map
|