@clawhub-amd5-dcf4642990
自动修复飞书-自动修复飞书群聊+自动修复会话 - 诊断 Gateway 连接、权限配置、消息投递问题
---
name: feishu-repair
description: 自动修复飞书-自动修复飞书群聊+自动修复会话 - 诊断 Gateway 连接、权限配置、消息投递问题
version: 2.0.0
author: c32
category: monitoring
tags:
- feishu
- repair
- group-chat
- session
requirements:
node: ">=18.0.0"
systemd: true
---
# Feishu Repair — 飞书群聊+会话修复技能
**版本**: 1.9.0
**创建日期**: 2026-04-14
**触发关键词**: `修复飞书`
---
## 📋 功能
自动诊断和修复 OpenClaw 飞书渠道的常见问题:
| 问题类型 | 诊断方式 | 修复方式 |
|---------|---------|---------|
| Gateway 未运行 | systemctl 检查 | 自动重启 Gateway |
| 飞书 WebSocket 断开 | journalctl 日志 | 自动重启 Gateway |
| 群聊权限丢失 | 检查 groupAllowFrom | 自动恢复配置 + 强制重启 + 验证 + 发送消息到所有群聊和用户 |
| 用户权限丢失 | 检查 allowFrom | 自动恢复配置 + 强制重启 + 验证 + 发送消息到所有群聊和用户 |
| 配置未生效 | 检查 config | 强制重启 Gateway + 验证 |
| 消息不回复 | 综合诊断 | 输出修复报告 + 发送验证消息 |
---
## 📂 文件结构
```
skills/feishu-repair/
├── SKILL.md
├── skill.json
├── _meta.json
└── scripts/
└── diagnose.js # 诊断脚本
```
---
## 🔧 修复流程
```
诊断 → 修复 → 强制重启 Gateway → 验证 → 发消息确认
```
| 步骤 | 功能 | 说明 |
|------|------|------|
| 1️⃣ 诊断 | 检查 Gateway、飞书配置、日志错误 | 始终执行 |
| 2️⃣ 修复 | 从配置恢复丢失的权限 | 检测到问题 |
| 3️⃣ **强制重启** | 重启 Gateway 使配置生效 | **有修复操作时强制重启** |
| 4️⃣ 验证 | 配置 + 日志双重检查 | 重启后自动执行 |
| 5️⃣ 消息确认 | 遍历所有群聊和会话发送当前时间 | 验证通过后自动发送 |
### 修复策略
| 策略 | 触发条件 | 动作 |
|------|---------|------|
| 配置恢复 | 权限丢失/配置异常 | 从 `openclaw.json` 或 `openclaw.json.bak*` 读取完整配置自动恢复 |
| Gateway 状态检查 | Gateway 未运行 | 自动重启 Gateway |
| WebSocket 重连 | WS 断开日志 | 自动重启 Gateway |
| 配置生效检查 | 配置变更未生效 | 自动重启 Gateway + 验证 |
### 配置读取优先级
1. **`~/.openclaw/openclaw.json`**(当前配置)
2. **`~/.openclaw/openclaw.json.bak`**(最新备份)
3. **`~/.openclaw/openclaw.json.bak.1`**(更早备份)
按顺序读取,找到第一个有飞书配置的文件即停止。从中提取 `allowFrom`、`groupAllowFrom`、`appId` 等完整列表。
---
## 📊 配置来源
技能内**不硬编码**任何用户 ID、群聊 ID、App ID。
全部从用户的 `openclaw.json` 及其备份文件中动态读取。
---
## ⚠️ 注意事项
- 检测到配置问题并修复后,**强制重启 Gateway**(不是提示手动)
- 重启完成后,自动验证修复结果(配置 + 日志双重检查)
- 验证通过后,自动在飞书所有群聊(groupAllowFrom)和会话(allowFrom)发送当前时间,确认消息功能已恢复
- 配置读取优先级:`openclaw.json` > `openclaw.json.bak` > `openclaw.json.bak.1`
- 诊断结果输出详细报告
FILE:_meta.json
{
"ownerId": "amd5",
"slug": "feishu-repair",
"version": "2.0.0",
"publishedAt": 0
}
FILE:scripts/diagnose.js
#!/usr/bin/env node
/**
* Feishu Repair — 飞书群聊+会话诊断与修复
*
* 功能:
* 1. 检查 Gateway 运行状态
* 2. 检查飞书 WebSocket 连接状态
* 3. 检查群聊权限配置(groupAllowFrom)
* 4. 检查用户权限配置(allowFrom)
* 5. 检查最近日志中的飞书相关错误
* 6. 自动修复丢失的权限配置
*
* 用法:
* node diagnose.js
* node diagnose.js --json # JSON 输出
*/
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
// ============================================================================
// 从 openclaw.json 及备份文件中读取配置
// 优先读取当前配置,其次读取最新备份
// ============================================================================
function loadConfig() {
const HOME = process.env.HOME || '/root';
const OPENCLAW_DIR = path.join(HOME, '.openclaw');
const CURRENT_FILE = path.join(OPENCLAW_DIR, 'openclaw.json');
// 尝试从多个来源读取配置(优先级:当前 → 备份)
const sources = [CURRENT_FILE];
// 添加备份文件
try {
const files = fs.readdirSync(OPENCLAW_DIR)
.filter(f => f.startsWith('openclaw.json.bak'))
.map(f => path.join(OPENCLAW_DIR, f));
files.sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs);
sources.push(...files);
} catch {}
for (const src of sources) {
try {
if (!fs.existsSync(src)) continue;
const content = fs.readFileSync(src, 'utf8');
const config = JSON.parse(content);
const feishu = config.channels?.feishu || {};
const accounts = feishu.accounts || {};
const mainAccount = accounts.main || {};
// 找第一个有飞书配置的账号
let account = mainAccount;
if (!account.appId) {
for (const [name, acc] of Object.entries(accounts)) {
if (acc.appId && acc.allowFrom?.length) {
account = acc;
break;
}
}
}
if (account.appId) {
return {
userId: (account.allowFrom || [])[0] || null,
groupId: (account.groupAllowFrom || [])[0] || null,
appId: account.appId || null,
appSecret: account.appSecret || null,
allowFrom: account.allowFrom || [],
groupAllowFrom: account.groupAllowFrom || [],
sourceFile: src,
sourceTime: fs.statSync(src).mtime,
accountName: Object.keys(accounts).find(k => accounts[k] === account) || 'main',
};
}
} catch {}
}
return null;
}
const CONFIG = loadConfig();
// ============================================================================
// 诊断函数
// ============================================================================
function checkGateway() {
try {
const output = execSync('systemctl --user is-active openclaw-gateway.service 2>/dev/null', { encoding: 'utf8' }).trim();
return {
running: output === 'active',
status: output,
};
} catch (e) {
return {
running: false,
status: 'unknown',
};
}
}
function checkFeishuConfig() {
try {
const output = execSync('openclaw config get channels.feishu 2>/dev/null', { encoding: 'utf8' });
const config = JSON.parse(output);
const mainAccount = config.accounts?.main || {};
const issues = [];
// 检查飞书是否启用
if (config.enabled !== true) {
issues.push('飞书渠道未启用(channels.feishu.enabled = false)');
}
// 检查 main 账号是否启用
if (mainAccount.enabled !== true) {
issues.push('main 账号未启用(channels.feishu.accounts.main.enabled = false)');
}
// 检查 allowFrom(用户权限)- 使用配置文件中的值
if (!mainAccount.allowFrom || mainAccount.allowFrom.length === 0) {
issues.push('用户权限为空(allowFrom 为空,DM 消息将被拒绝)');
} else if (CONFIG && !mainAccount.allowFrom.includes(CONFIG.userId)) {
issues.push(`配置文件中的用户 ID 不在 allowFrom 中(缺少 CONFIG.userId)`);
}
// 检查 groupAllowFrom(群聊权限)- 使用配置文件中的值
if (!mainAccount.groupAllowFrom || mainAccount.groupAllowFrom.length === 0) {
issues.push('群聊权限为空(groupAllowFrom 为空,群消息将被拒绝)');
} else if (CONFIG && !mainAccount.groupAllowFrom.includes(CONFIG.groupId)) {
issues.push(`配置文件中的群聊 ID 不在 groupAllowFrom 中(缺少 CONFIG.groupId)`);
}
return {
valid: issues.length === 0,
config: {
enabled: config.enabled,
mainEnabled: mainAccount.enabled,
allowFrom: mainAccount.allowFrom || [],
groupAllowFrom: mainAccount.groupAllowFrom || [],
connectionMode: mainAccount.connectionMode || config.connectionMode || 'unknown',
},
issues,
};
} catch (e) {
return {
valid: false,
config: null,
issues: ['无法读取飞书配置: ' + e.message],
};
}
}
function checkRecentLogs() {
try {
const output = execSync(
'journalctl --user -u openclaw-gateway.service --since "30 min ago" 2>/dev/null | grep -i "feishu" | tail -20',
{ encoding: 'utf8' }
);
const lines = output.trim().split('\n').filter(Boolean);
const errors = [];
const warnings = [];
for (const line of lines) {
if (/error|fail|reject|not in|disconnect|close/i.test(line)) {
errors.push(line.trim());
}
if (/warn|timeout|retry/i.test(line)) {
warnings.push(line.trim());
}
}
return { errors, warnings, totalLines: lines.length };
} catch {
return { errors: [], warnings: [], totalLines: 0 };
}
}
// ============================================================================
// 修复函数
// ============================================================================
function fixAllowFrom() {
const allowFrom = CONFIG?.allowFrom || [];
if (allowFrom.length === 0) {
return { success: false, message: '配置文件中无 allowFrom 值可恢复' };
}
try {
const json = JSON.stringify(allowFrom);
execSync(
`openclaw config set channels.feishu.allowFrom 'json' 2>/dev/null`,
{ encoding: 'utf8' }
);
return { success: true, message: `已从配置文件恢复 allowFrom: allowFrom.join(', ')` };
} catch (e) {
return { success: false, message: '恢复 allowFrom 失败: ' + e.message };
}
}
function fixGroupAllowFrom() {
const groupAllowFrom = CONFIG?.groupAllowFrom || [];
if (groupAllowFrom.length === 0) {
return { success: false, message: '配置文件中无 groupAllowFrom 值可恢复' };
}
try {
const json = JSON.stringify(groupAllowFrom);
execSync(
`openclaw config set channels.feishu.groupAllowFrom 'json' 2>/dev/null`,
{ encoding: 'utf8' }
);
return { success: true, message: `已从配置文件恢复 groupAllowFrom: groupAllowFrom.join(', ')` };
} catch (e) {
return { success: false, message: '恢复 groupAllowFrom 失败: ' + e.message };
}
}
// ============================================================================
// 自动重启 Gateway
// ============================================================================
function restartGateway() {
try {
console.log('🔄 正在重启 Gateway...');
execSync('systemctl --user restart openclaw-gateway.service', { timeout: 30000, encoding: 'utf8' });
// 等待服务启动
let retries = 0;
const maxRetries = 10;
while (retries < maxRetries) {
try {
const status = execSync('systemctl --user is-active openclaw-gateway.service 2>/dev/null', { encoding: 'utf8' }).trim();
if (status === 'active') {
return { success: true, message: 'Gateway 重启成功' };
}
} catch {}
execSync('sleep 2', { encoding: 'utf8' });
retries++;
}
return { success: false, message: 'Gateway 重启超时(等待超过 20 秒)' };
} catch (e) {
return { success: false, message: 'Gateway 重启失败: ' + e.message };
}
}
// ============================================================================
// 自动验证飞书修复情况
// ============================================================================
function getTenantAccessToken(appId, appSecret) {
try {
const output = execSync(
`curl -s -X POST "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal" \
-H "Content-Type: application/json" \
-d '{"app_id":"appId","app_secret":"appSecret"}'`,
{ encoding: 'utf8', timeout: 10000 }
);
const result = JSON.parse(output);
return result.tenant_access_token || null;
} catch (e) {
return null;
}
}
function sendFeishuMessage(targetId, text, receiveIdType = 'chat_id') {
if (!CONFIG?.appId || !CONFIG?.appSecret) {
return { success: false, message: '配置文件中无 appId/appSecret' };
}
try {
// 1. 获取 token
const token = getTenantAccessToken(CONFIG.appId, CONFIG.appSecret);
if (!token) {
return { success: false, message: '获取 tenant_access_token 失败' };
}
// 2. 发送消息
const now = new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' });
const content = JSON.stringify({ text: text.replace('{time}', now) });
const output = execSync(
`curl -s -X POST "https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=receiveIdType" \
-H "Authorization: Bearer token" \
-H "Content-Type: application/json" \
-d '{"receive_id":"targetId","msg_type":"text","content":JSON.stringify(content)}'`,
{ encoding: 'utf8', timeout: 10000 }
);
const result = JSON.parse(output);
if (result.code === 0) {
return { success: true, message: `✅ 已发送消息到飞书群聊(now)`, now };
} else {
return { success: false, message: `飞书 API 错误: result.msg || result.code` };
}
} catch (e) {
return { success: false, message: '发送飞书消息失败: ' + e.message };
}
}
function verifyFeishuFix() {
try {
// 1. 读取当前飞书配置
const output = execSync('openclaw config get channels.feishu 2>/dev/null', { encoding: 'utf8' });
const config = JSON.parse(output);
const mainAccount = config.accounts?.main || {};
const verificationResults = [];
let allPassed = true;
// 验证飞书启用
if (config.enabled === true) {
verificationResults.push({ check: '飞书渠道', status: 'pass', value: '已启用' });
} else {
verificationResults.push({ check: '飞书渠道', status: 'fail', value: '未启用' });
allPassed = false;
}
// 验证 main 账号
if (mainAccount.enabled === true) {
verificationResults.push({ check: 'main 账号', status: 'pass', value: '已启用' });
} else {
verificationResults.push({ check: 'main 账号', status: 'fail', value: '未启用' });
allPassed = false;
}
// 验证 allowFrom
if (CONFIG && mainAccount.allowFrom?.includes(CONFIG.userId)) {
verificationResults.push({ check: 'allowFrom', status: 'pass', value: `包含 CONFIG.userId` });
} else if (CONFIG) {
verificationResults.push({ check: 'allowFrom', status: 'fail', value: `缺少 CONFIG.userId` });
allPassed = false;
}
// 验证 groupAllowFrom
if (CONFIG && mainAccount.groupAllowFrom?.includes(CONFIG.groupId)) {
verificationResults.push({ check: 'groupAllowFrom', status: 'pass', value: `包含 CONFIG.groupId` });
} else if (CONFIG) {
verificationResults.push({ check: 'groupAllowFrom', status: 'fail', value: `缺少 CONFIG.groupId` });
allPassed = false;
}
// 2. 检查最近日志中的飞书错误
try {
const logs = execSync(
'journalctl --user -u openclaw-gateway.service --since "2 min ago" 2>/dev/null | grep -i "feishu" | tail -10',
{ encoding: 'utf8' }
);
const errorLines = logs.split('\n').filter(l => /error|fail|reject|not in/i.test(l));
if (errorLines.length > 0) {
verificationResults.push({ check: 'Gateway 日志', status: 'warn', value: `发现 errorLines.length 条错误` });
} else {
verificationResults.push({ check: 'Gateway 日志', status: 'pass', value: '无错误' });
}
} catch {
verificationResults.push({ check: 'Gateway 日志', status: 'skip', value: '无法读取' });
}
return {
success: allPassed,
checks: verificationResults,
summary: allPassed ? '✅ 飞书配置修复验证通过' : '❌ 飞书配置修复验证失败',
};
} catch (e) {
return {
success: false,
checks: [],
summary: '验证失败: ' + e.message,
};
}
}
// ============================================================================
// 发送飞书验证消息(遍历所有群聊和用户)
// ============================================================================
function sendFeishuVerification() {
const results = {
groups: [],
users: [],
};
const message = '🔧 飞书修复验证 - 当前时间:{time}\n✅ 如果收到此消息,说明飞书消息功能已恢复正常。';
// 发送到所有群聊
if (CONFIG?.groupAllowFrom?.length) {
for (const groupId of CONFIG.groupAllowFrom) {
const res = sendFeishuMessage(groupId, message, 'chat_id');
results.groups.push({ id: groupId, ...res });
}
}
// 发送到所有用户(DM)
if (CONFIG?.allowFrom?.length) {
for (const userId of CONFIG.allowFrom) {
const res = sendFeishuMessage(userId, message, 'open_id');
results.users.push({ id: userId, ...res });
}
}
// 汇总结果
const totalSent = results.groups.filter(r => r.success).length + results.users.filter(r => r.success).length;
const totalFailed = results.groups.filter(r => !r.success).length + results.users.filter(r => !r.success).length;
return {
success: totalSent > 0,
groups: results.groups,
users: results.users,
summary: `已发送 totalSent 条消息totalFailed > 0 ? `,${totalFailed 条失败` : ''}`,
};
}
// ============================================================================
// 主逻辑
// ============================================================================
function diagnose() {
const jsonMode = process.argv.includes('--json');
const results = {
timestamp: new Date().toISOString(),
gateway: checkGateway(),
feishuConfig: checkFeishuConfig(),
recentLogs: checkRecentLogs(),
fixes: [],
};
// 自动修复
if (results.feishuConfig.issues.some(i => i.includes('allowFrom'))) {
results.fixes.push(fixAllowFrom());
}
if (results.feishuConfig.issues.some(i => i.includes('groupAllowFrom'))) {
results.fixes.push(fixGroupAllowFrom());
}
// 如果有修复操作,强制重启 Gateway 并验证
const hasFixes = results.fixes.length > 0;
if (hasFixes) {
// 1. 强制重启 Gateway
console.log('\n🔄 配置已恢复,强制重启 Gateway...');
results.gatewayRestart = restartGateway();
// 2. 等待 Gateway 启动完成
if (results.gatewayRestart.success) {
console.log('⏳ 等待 Gateway 重启完成...');
execSync('sleep 5', { encoding: 'utf8' });
// 3. 自动验证修复结果(配置+日志)
results.verification = verifyFeishuFix();
// 4. 发送飞书验证消息(带当前时间)
results.feishuMessage = sendFeishuVerification();
}
}
// 输出
if (jsonMode) {
console.log(JSON.stringify(results, null, 2));
return;
}
// 文字报告
console.log('🔍 飞书群聊+会话诊断报告');
console.log('═'.repeat(50));
console.log(`时间: new Date().toLocaleString('zh-CN')`);
// 配置来源
if (CONFIG) {
console.log(`📂 配置来源: CONFIG.sourceFile (CONFIG.sourceTime.toLocaleString('zh-CN'))`);
console.log(` 账号: CONFIG.accountName | App ID: CONFIG.appId`);
} else {
console.log('❌ 未找到任何配置文件(openclaw.json 及备份)');
}
console.log('');
// Gateway 状态
const gwIcon = results.gateway.running ? '✅' : '❌';
console.log(`gwIcon Gateway: results.gateway.status`);
// 飞书配置
if (results.feishuConfig.valid) {
console.log('✅ 飞书配置: 正常');
} else {
console.log('❌ 飞书配置: 异常');
for (const issue of results.feishuConfig.issues) {
console.log(` ⚠️ issue`);
}
}
// 修复结果
if (results.fixes.length > 0) {
console.log('');
console.log('🔧 自动修复:');
for (const fix of results.fixes) {
const icon = fix.success ? '✅' : '❌';
console.log(` icon fix.message`);
}
// Gateway 重启结果
if (results.gatewayRestart) {
const gwIcon = results.gatewayRestart.success ? '✅' : '❌';
console.log(` gwIcon results.gatewayRestart.message`);
}
// 验证结果
if (results.verification) {
console.log('');
console.log('🔍 修复验证:');
for (const check of results.verification.checks) {
const icon = check.status === 'pass' ? '✅' : check.status === 'fail' ? '❌' : '⚠️';
console.log(` icon check.check: check.value`);
}
console.log(` results.verification.summary`);
}
// 飞书消息发送结果
if (results.feishuMessage) {
console.log('');
console.log('📤 飞书消息确认:');
for (const g of results.feishuMessage.groups) {
const icon = g.success ? '✅' : '❌';
console.log(` icon 群聊 g.id: g.message`);
}
for (const u of results.feishuMessage.users) {
const icon = u.success ? '✅' : '❌';
console.log(` icon 用户 u.id: u.message`);
}
console.log(` 📊 results.feishuMessage.summary`);
}
}
// 日志检查
if (results.recentLogs.errors.length > 0) {
console.log('');
console.log(`❌ 最近日志发现 results.recentLogs.errors.length 条错误:`);
for (const err of results.recentLogs.errors.slice(0, 5)) {
console.log(` err`);
}
}
// 总结
const hasCritical = !results.gateway.running || !results.feishuConfig.valid;
console.log('');
if (hasCritical) {
console.log('📋 建议操作:');
if (!results.gateway.running) {
console.log(' 1. 启动 Gateway: systemctl --user start openclaw-gateway.service');
}
if (results.fixes.length > 0) {
console.log(' 2. 重启 Gateway: systemctl --user restart openclaw-gateway.service');
}
} else {
console.log('✅ 飞书连接正常,无异常');
}
}
diagnose();
FILE:skill.json
{
"name": "Feishu Repair",
"description": "自动修复飞书-自动修复飞书群聊+自动修复会话 - 诊断 Gateway 连接、权限配置、消息投递问题",
"version": "2.0.0",
"identifier": "feishu-repair",
"author": "c32",
"category": "monitoring",
"tags": [
"feishu",
"repair",
"group-chat",
"session",
"diagnose"
],
"requirements": {
"node": ">=18.0.0",
"systemd": true
},
"scripts": {}
}
多实例记忆共享,多个 Agent 之间同步记忆
---
name: team-shared-memory
description: 多实例记忆共享,多个 Agent 之间同步记忆
version: 1.0.1
---
# Team Memory — 多实例记忆共享
**版本**: 1.0.1
**创建日期**: 2026-03-11
**更新日期**: 2026-04-14
**作者: c32
---
## 📋 功能
| 功能 | 说明 |
|------|------|
| 记忆同步 | 跨 Agent 同步记忆数据 |
| 冲突解决 | 自动检测和处理记忆冲突 |
| 安全扫描 | secret-scan 检测敏感信息泄露 |
| 增量同步 | 只同步变更部分 |
---
## 📂 文件结构
```
skills/team-memory/
├── SKILL.md
├── skill.json
└── scripts/
├── sync.js # 记忆同步主程序
└── secret-scan.js # 敏感信息扫描
```
---
## 📊 Cron 任务
| 任务名 | 频率 | Job ID |
|--------|------|--------|
| Team Memory 同步 | 每 12 小时 | `f47e2a9d` |
---
## ⚠️ 注意事项
- 同步间隔可配置
- 冲突时优先保留最新版本
- 安全扫描检测 API 密钥等敏感信息
FILE:scripts/secret-scan.js
#!/usr/bin/env node
/**
* 密钥扫描脚本
* 扫描文件中的敏感信息,防止同步时泄露
*
* 用法:
* node secret-scan.js <文件路径>
* node secret-scan.js --dir <目录路径>
*/
const fs = require('fs');
const path = require('path');
// 密钥模式
const SECRET_PATTERNS = [
{ name: 'AWS Access Key', regex: /AKIA[0-9A-Z]{16}/ },
{ name: '阿里云 AccessKey', regex: /LTAI[0-9A-Za-z]{12,20}/ },
{ name: '私钥', regex: /-----BEGIN (RSA |EC |DSA )?PRIVATE KEY-----/ },
{ name: 'Bearer Token', regex: /Bearer [a-zA-Z0-9\-_]{20,}/ },
{ name: 'API Key (通用)', regex: /(?:api[_-]?key|apikey)\s*[:=]\s*['"]?[a-zA-Z0-9\-_]{16,}/i },
{ name: '密码赋值', regex: /(?:password|passwd|pwd)\s*[:=]\s*['"][^'"]{4,}['"]/i },
{ name: '数据库连接串', regex: /(?:mysql|mongodb|redis|postgres):\/\/[^\s]+:[^\s]+@/i },
{ name: 'sk- 开头的 Key', regex: /sk-[a-zA-Z0-9\-_]{20,}/ },
{ name: 'ghp_ GitHub Token', regex: /ghp_[a-zA-Z0-9]{36}/ },
{ name: 'xoxb_ Slack Token', regex: /xoxb-[a-zA-Z0-9\-]+/ },
];
function scanContent(content, filepath) {
const found = [];
for (const pattern of SECRET_PATTERNS) {
const matches = content.match(pattern.regex);
if (matches) {
for (const match of matches) {
// 脱敏显示
const masked = match.length > 10 ? match.substring(0, 6) + '***' + match.substring(match.length - 4) : '***';
found.push({
type: pattern.name,
match: masked,
line: content.substring(0, content.indexOf(match)).split('\n').length
});
}
}
}
return found;
}
function scanFile(filepath) {
try {
const content = fs.readFileSync(filepath, 'utf8');
const secrets = scanContent(content, filepath);
return { filepath, secrets, safe: secrets.length === 0 };
} catch (e) {
return { filepath, error: e.message, safe: false };
}
}
function scanDir(dirPath) {
const results = [];
function walk(dir) {
if (!fs.existsSync(dir)) return;
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullpath = path.join(dir, entry.name);
if (entry.name.startsWith('.')) continue; // 跳过隐藏文件
if (entry.isDirectory()) {
walk(fullpath);
} else if (entry.isFile() && /\.(md|json|js|ts|yaml|yml|env|conf|cfg|ini|txt|sh|py)$/i.test(entry.name)) {
results.push(scanFile(fullpath));
}
}
}
walk(dirPath);
return results;
}
function main() {
const args = process.argv.slice(2);
if (args.length === 0) {
console.log('用法: node secret-scan.js <文件路径>');
console.log(' node secret-scan.js --dir <目录路径>');
process.exit(1);
}
if (args[0] === '--dir' && args[1]) {
console.log(`=== 扫描目录: args[1] ===\n`);
const results = scanDir(args[1]);
const unsafe = results.filter(r => !r.safe);
const safe = results.filter(r => r.safe);
console.log(`扫描完成: results.length 个文件`);
console.log(` 安全: safe.length`);
console.log(` ⚠️ 含密钥: unsafe.length\n`);
for (const result of unsafe) {
console.log(`❌ result.filepath`);
for (const secret of result.secrets) {
console.log(` secret.type: secret.match (第 secret.line 行)`);
}
}
process.exit(unsafe.length > 0 ? 1 : 0);
return;
}
// 扫描单个文件
const filepath = args[0];
console.log(`=== 扫描文件: filepath ===\n`);
const result = scanFile(filepath);
if (result.error) {
console.error(`错误: result.error`);
process.exit(1);
}
if (result.safe) {
console.log('✅ 未检测到密钥');
} else {
console.log(`⚠️ 检测到 result.secrets.length 个密钥:\n`);
for (const secret of result.secrets) {
console.log(` secret.type: secret.match (第 secret.line 行)`);
}
process.exit(1);
}
}
main();
FILE:scripts/sync.js
#!/usr/bin/env node
/**
* Team Memory 同步脚本
* 多工作区/实例间共享记忆
*
* 用法:
* node sync.js --local # 本地多工作区同步
* node sync.js --status # 查看同步状态
* node sync.js --scan <路径> # 密钥扫描
*/
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const WORKSPACE = process.env.HOME + '/.openclaw/workspace';
const MEMORY_DIR = WORKSPACE + '/memory';
const TEAM_DIR = MEMORY_DIR + '/team';
const AUTO_DIR = MEMORY_DIR + '/auto';
const STATE_FILE = TEAM_DIR + '/.sync-state.json';
const LOCK_FILE = TEAM_DIR + '/.sync-lock';
// 可共享的记忆类型
const SHARED_TYPES = ['project', 'reference'];
const PRIVATE_TYPES = ['user', 'feedback'];
// 确保目录存在
function ensureTeamDir() {
if (!fs.existsSync(TEAM_DIR)) {
fs.mkdirSync(TEAM_DIR, { recursive: true });
}
}
// 读取同步状态
function readState() {
try {
return JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'));
} catch (e) {
return { lastSyncAt: null, syncCount: 0, lastHash: {} };
}
}
// 保存同步状态
function saveState(state) {
fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
}
// 计算文件 hash
function fileHash(filepath) {
const content = fs.readFileSync(filepath);
return crypto.createHash('md5').update(content).digest('hex');
}
// 尝试获取锁
function tryLock() {
if (fs.existsSync(LOCK_FILE)) {
try {
const lock = JSON.parse(fs.readFileSync(LOCK_FILE, 'utf8'));
const lockAge = Date.now() - lock.startedAt;
if (lockAge < 5 * 60 * 1000) { // 5 分钟锁
console.log('[锁] 同步进行中,跳过');
return false;
}
} catch (e) {}
}
fs.writeFileSync(LOCK_FILE, JSON.stringify({
pid: process.pid,
startedAt: Date.now()
}));
return true;
}
function releaseLock() {
try { fs.unlinkSync(LOCK_FILE); } catch (e) {}
}
// 本地同步:将共享记忆复制到 team 目录,反之亦然
function localSync() {
ensureTeamDir();
const state = readState();
const syncReport = { timestamp: new Date().toISOString(), pushed: [], pulled: [], conflicts: [] };
// 推送:auto/{project,reference} → team/
for (const type of SHARED_TYPES) {
const srcDir = path.join(AUTO_DIR, type);
const destDir = path.join(TEAM_DIR, type);
if (!fs.existsSync(srcDir)) continue;
fs.mkdirSync(destDir, { recursive: true });
const files = fs.readdirSync(srcDir).filter(f => f.endsWith('.md') && !f.startsWith('.'));
for (const file of files) {
const srcFile = path.join(srcDir, file);
const destFile = path.join(destDir, file);
const hash = fileHash(srcFile);
// 检查是否变更
if (state.lastHash[file] === hash && fs.existsSync(destFile)) {
continue; // 未变更,跳过
}
// 目标存在且不同 → 冲突
if (fs.existsSync(destFile)) {
const destHash = fileHash(destFile);
if (hash !== destHash) {
// 以较新的为准
const srcMtime = fs.statSync(srcFile).mtimeMs;
const destMtime = fs.statSync(destFile).mtimeMs;
if (srcMtime > destMtime) {
fs.copyFileSync(srcFile, destFile);
syncReport.pushed.push(`type/file`);
} else {
fs.copyFileSync(destFile, srcFile);
syncReport.pulled.push(`type/file`);
}
syncReport.conflicts.push(`type/file`);
}
} else {
fs.copyFileSync(srcFile, destFile);
syncReport.pushed.push(`type/file`);
}
state.lastHash[file] = hash;
}
}
// 拉取:team/ → auto/{project,reference}(新增的文件)
for (const type of SHARED_TYPES) {
const srcDir = path.join(TEAM_DIR, type);
const destDir = path.join(AUTO_DIR, type);
if (!fs.existsSync(srcDir)) continue;
fs.mkdirSync(destDir, { recursive: true });
const files = fs.readdirSync(srcDir).filter(f => f.endsWith('.md') && !f.startsWith('.'));
for (const file of files) {
const srcFile = path.join(srcDir, file);
const destFile = path.join(destDir, file);
if (!fs.existsSync(destFile)) {
fs.copyFileSync(srcFile, destFile);
syncReport.pulled.push(`type/file`);
state.lastHash[file] = fileHash(srcFile);
}
}
}
// 保存状态
state.lastSyncAt = Date.now();
state.syncCount = (state.syncCount || 0) + 1;
saveState(state);
return syncReport;
}
// 主函数
function main() {
const args = process.argv.slice(2);
if (args.includes('--status')) {
const state = readState();
const hoursSince = state.lastSyncAt ? ((Date.now() - state.lastSyncAt) / (1000 * 60 * 60)).toFixed(1) : '从未';
console.log('=== 团队记忆同步状态 ===');
console.log(`上次同步: hoursSinceh 前`);
console.log(`总同步次数: state.syncCount || 0`);
console.log(`共享类型: SHARED_TYPES.join(', ')`);
console.log(`私有类型: PRIVATE_TYPES.join(', ') (不共享)`);
// 统计文件数
for (const type of [...SHARED_TYPES, ...PRIVATE_TYPES]) {
const dir = path.join(AUTO_DIR, type);
const count = fs.existsSync(dir) ? fs.readdirSync(dir).filter(f => f.endsWith('.md')).length : 0;
console.log(` type: count 条记忆`);
}
return;
}
// 执行同步
console.log('=== 开始本地同步 ===\n');
if (tryLock()) {
try {
const report = localSync();
console.log(`\n推送: report.pushed.length 条`);
console.log(`拉取: report.pulled.length 条`);
console.log(`冲突: report.conflicts.length 条`);
console.log('\n=== 同步完成 ===');
} finally {
releaseLock();
}
}
}
main();
FILE:skill.json
{
"name": "Team Shared Memory",
"description": "团队记忆同步服务,多工作区/实例间共享记忆",
"version": "1.0.1",
"identifier": "team-shared-memory",
"author": "c32",
"category": "memory",
"tags": [
"memory",
"team",
"sync",
"shared"
],
"requirements": {
"node": ">=18.0.0"
}
}AES 加密存储,用于安全保存 API 密钥等敏感信息
---
name: secure-storage
description: AES 加密存储,用于安全保存 API 密钥等敏感信息
version: 1.0.1
---
# Secure Storage — 加密存储
**版本**: 1.0.1
**创建日期**: 2026-04-13
**更新日期**: 2026-04-14
---
## 📋 功能
使用 AES 加密算法安全存储敏感信息:
| 功能 | 说明 |
|------|------|
| set | 加密存储键值对 |
| get | 解密读取值 |
| list | 列出所有已存储键名 |
| delete | 删除指定键 |
---
## 📂 文件结构
```
skills/secure-storage/
├── SKILL.md
├── skill.json
└── scripts/
└── secure-storage.js # 加密存储主程序
```
---
## 🔧 用法
```bash
# 存储
node skills/secure-storage/scripts/secure-storage.js set <key> <value>
# 读取
node skills/secure-storage/scripts/secure-storage.js get <key>
# 列表
node skills/secure-storage/scripts/secure-storage.js list
# 删除
node skills/secure-storage/scripts/secure-storage.js delete <key>
```
---
## 📊 触发方式
- **手动触发**(按需使用的安全工具,无需自动化)
---
## ⚠️ 注意事项
- 加密密钥从环境变量读取
- 存储文件:`.secure-storage.json`
- 不要提交加密文件到版本控制
FILE:scripts/secure-storage.js
#!/usr/bin/env node
/**
* Secure Storage — 安全存储
* 源自 utils/secureStorage/ (6 文件, 629 行)
*
* Linux 服务器降级为文件存储(权限 0600)
*/
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const STORAGE_FILE = process.env.HOME + '/.openclaw/workspace/memory/secure-storage.json';
// 简单加密(Base64 + 密钥混淆,非生产级)
// 生产环境应使用系统密钥链或 KMS
const SIMPLE_KEY = 'openclaw-secure-storage-v1';
function encrypt(text) {
const iv = crypto.randomBytes(16);
const key = crypto.scryptSync(SIMPLE_KEY, 'salt', 32);
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
return iv.toString('hex') + ':' + encrypted;
}
function decrypt(text) {
const parts = text.split(':');
const iv = Buffer.from(parts[0], 'hex');
const key = crypto.scryptSync(SIMPLE_KEY, 'salt', 32);
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
let decrypted = decipher.update(parts[1], 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
function loadStorage() {
try {
return JSON.parse(fs.readFileSync(STORAGE_FILE, 'utf-8'));
} catch { return {}; }
}
function saveStorage(data) {
const dir = path.dirname(STORAGE_FILE);
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(STORAGE_FILE, JSON.stringify(data, null, 2), { mode: 0o600 });
}
function setKey(key, value) {
const storage = loadStorage();
storage[key] = { encrypted: encrypt(value), createdAt: new Date().toISOString() };
saveStorage(storage);
console.log(`✅ 已存储: key`);
}
function getKey(key) {
const storage = loadStorage();
if (!storage[key]) { console.log(`❌ 未找到: key`); return; }
try {
const value = decrypt(storage[key].encrypted);
console.log(`key: value`);
return value;
} catch {
console.log('❌ 解密失败');
}
}
function deleteKey(key) {
const storage = loadStorage();
if (!storage[key]) { console.log(`❌ 未找到: key`); return; }
delete storage[key];
saveStorage(storage);
console.log(`✅ 已删除: key`);
}
function listKeys() {
const storage = loadStorage();
const keys = Object.keys(storage);
console.log('=== 安全存储 ===\n');
if (keys.length === 0) { console.log('无存储项'); return; }
console.log('密钥'.padEnd(30) + '创建时间');
console.log('-'.repeat(50));
keys.forEach(k => {
const time = new Date(storage[k].createdAt).toLocaleString('zh-CN');
console.log(k.padEnd(30) + time);
});
console.log(`\n共 keys.length 项`);
}
const [command, ...args] = process.argv.slice(2);
switch (command) {
case 'set':
if (args.length < 2) { console.log('用法: node secure-storage.js set <key> <value>'); process.exit(1); }
setKey(args[0], args.slice(1).join(' '));
break;
case 'get':
if (!args[0]) { console.log('用法: node secure-storage.js get <key>'); process.exit(1); }
getKey(args[0]);
break;
case 'delete':
if (!args[0]) { console.log('用法: node secure-storage.js delete <key>'); process.exit(1); }
deleteKey(args[0]);
break;
case 'list': listKeys(); break;
default:
console.log('用法: node secure-storage.js [set|get|delete|list]');
}
FILE:skill.json
{
"name": "Secure Storage",
"description": "AES 加密存储,用于安全保存 API 密钥等敏感信息",
"version": "1.0.1",
"identifier": "secure-storage",
"category": "security",
"tags": [
"encryption",
"storage",
"security",
"aes"
],
"author": "c32"
}内存快照分析(v8 heap snapshot)+ 性能分析(perf_hooks)
---
name: heap-dump
description: 内存快照分析(v8 heap snapshot)+ 性能分析(perf_hooks)
version: 1.0.1
---
# Heap Dump & Profiler — 内存与性能分析
**版本**: 1.0.1
**创建日期**: 2026-04-13
**更新日期**: 2026-04-14
---
## 📋 功能
| 功能 | 说明 |
|------|------|
| **heap-dump** | 生成 v8 堆快照,Chrome DevTools 分析 |
| **profiler** | perf_hooks 性能分析(start/checkpoint/end/report) |
---
## 📂 文件结构
```
skills/heap-dump/
├── SKILL.md
├── skill.json
└── scripts/
├── heap-dump.js # 内存快照生成
└── profiler.js # 性能分析工具
```
---
## 🔧 用法
```bash
# 内存快照
node skills/heap-dump/scripts/heap-dump.js snapshot
# 性能分析
node skills/heap-dump/scripts/profiler.js start "任务名" # 开始
node skills/heap-dump/scripts/profiler.js checkpoint "阶段" # 标记点
node skills/heap-dump/scripts/profiler.js end # 结束
node skills/heap-dump/scripts/profiler.js report # 生成报告
```
---
## 📊 触发方式
- **手动触发**(按需使用的诊断工具,无需自动化)
---
## ⚠️ 注意事项
- heap snapshot 文件较大,建议分析后删除
- profiler 输出写入 `memory/perf/` 目录
- 已合并 headless-profiler 功能
FILE:scripts/heap-dump.js
#!/usr/bin/env node
/**
* Heap Dump — 内存快照 + 诊断
* 基于 Claude Code heapDumpService.ts,适配 OpenClaw
*
* 用法:
* node heap-dump.js # 生成快照 + 诊断
* node heap-dump.js --snapshot # 仅快照
* node heap-dump.js --stats # 当前内存统计
*/
const v8 = require('v8');
const fs = require('fs');
const path = require('path');
const { pipeline } = require('stream/promises');
const SNAPSHOTS_DIR = process.env.HOME + '/.openclaw/heap-snapshots/';
const MAX_SNAPSHOTS = 10;
// 确保目录存在
fs.mkdirSync(SNAPSHOTS_DIR, { recursive: true });
/**
* 清理旧快照(保留最新的 MAX_SNAPSHOTS 个)
*/
function cleanupOldSnapshots() {
try {
const files = fs.readdirSync(SNAPSHOTS_DIR)
.filter(f => f.endsWith('.heapsnapshot') || f.endsWith('-diagnostics.json'))
.map(f => ({ name: f, mtime: fs.statSync(path.join(SNAPSHOTS_DIR, f)).mtimeMs }))
.sort((a, b) => b.mtime - a.mtime);
while (files.length > MAX_SNAPSHOTS * 2) {
const old = files.pop();
fs.unlinkSync(path.join(SNAPSHOTS_DIR, old.name));
}
} catch (e) {
console.log('[清理] 跳过:', e.message);
}
}
/**
* 获取内存诊断信息
*/
function getMemoryDiagnostics() {
const usage = process.memoryUsage();
const heapStats = v8.getHeapStatistics();
// 活跃句柄数(Node.js 内部 API)
let activeHandles = 0;
let activeRequests = 0;
try {
activeHandles = process._getActiveHandles().length;
activeRequests = process._getActiveRequests().length;
} catch (e) {
// 内部 API 可能在某些版本不可用
}
// 打开的文件描述符(仅 Linux)
let openFileDescriptors;
try {
openFileDescriptors = fs.readdirSync('/proc/self/fd').length;
} catch (e) {
// 非 Linux 或无权限
}
// 内存增长率
const uptimeSeconds = process.uptime();
const bytesPerSecond = uptimeSeconds > 0 ? usage.rss / uptimeSeconds : 0;
const mbPerHour = (bytesPerSecond * 3600) / (1024 * 1024);
// 潜在泄漏检测
const potentialLeaks = [];
if (heapStats.number_of_detached_contexts > 0) {
potentialLeaks.push(`heapStats.number_of_detached_contexts 个分离的上下文 — 可能的 iframe/上下文泄漏`);
}
if (activeHandles > 100) {
potentialLeaks.push(`activeHandles 个活跃句柄 — 可能的定时器/socket 泄漏`);
}
if (usage.rss - usage.heapUsed > usage.heapUsed) {
potentialLeaks.push('原生内存 > 堆内存 — 泄漏可能在原生插件中 (node-pty, sharp 等)');
}
if (mbPerHour > 100) {
potentialLeaks.push(`高内存增长率: mbPerHour.toFixed(1) MB/h`);
}
if (openFileDescriptors && openFileDescriptors > 500) {
potentialLeaks.push(`openFileDescriptors 个打开的文件描述符 — 可能的文件/socket 泄漏`);
}
// V8 堆空间统计
let heapSpaces;
try {
heapSpaces = v8.getHeapSpaceStatistics().map(space => ({
name: space.space_name,
size: space.space_size,
used: space.space_used_size,
available: space.space_available_size,
}));
} catch (e) {
// 某些运行时不支持
}
return {
timestamp: new Date().toISOString(),
uptime: formatUptime(uptimeSeconds),
nodeVersion: process.version,
memoryUsage: {
heapUsed: formatBytes(usage.heapUsed),
heapTotal: formatBytes(usage.heapTotal),
external: formatBytes(usage.external),
arrayBuffers: formatBytes(usage.arrayBuffers),
rss: formatBytes(usage.rss),
},
memoryGrowthRate: {
bytesPerSecond: Math.round(bytesPerSecond),
mbPerHour: mbPerHour.toFixed(1),
},
v8HeapStats: {
heapSizeLimit: formatBytes(heapStats.heap_size_limit),
mallocedMemory: formatBytes(heapStats.malloced_memory),
peakMallocedMemory: formatBytes(heapStats.peak_malloced_memory),
detachedContexts: heapStats.number_of_detached_contexts,
nativeContexts: heapStats.number_of_native_contexts,
},
v8HeapSpaces: heapSpaces,
activeHandles,
activeRequests,
openFileDescriptors,
potentialLeaks,
recommendation: potentialLeaks.length > 0
? `⚠️ 发现 potentialLeaks.length 个潜在泄漏指标。查看 potentialLeaks 数组。`
: '✅ 未发现明显泄漏指标。检查堆快照了解保留对象。',
};
}
/**
* 写入堆快照
*/
async function writeHeapSnapshot(filepath) {
const writeStream = fs.createWriteStream(filepath, { mode: 0o600 });
const heapSnapshotStream = v8.getHeapSnapshot();
await pipeline(heapSnapshotStream, writeStream);
}
/**
* 格式化字节
*/
function formatBytes(bytes) {
if (bytes >= 1024 * 1024 * 1024) return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
if (bytes >= 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
if (bytes >= 1024) return (bytes / 1024).toFixed(2) + ' KB';
return bytes + ' B';
}
/**
* 格式化运行时间
*/
function formatUptime(seconds) {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60);
return `hh mm ss`;
}
// 主函数
async function main() {
const args = process.argv.slice(2);
// 仅统计
if (args.includes('--stats')) {
const diag = getMemoryDiagnostics();
console.log('=== 内存统计 ===\n');
console.log(`运行时间: diag.uptime`);
console.log(`Node 版本: diag.nodeVersion\n`);
console.log('内存使用:');
console.log(` 堆使用: diag.memoryUsage.heapUsed`);
console.log(` 堆总计: diag.memoryUsage.heapTotal`);
console.log(` 外部: diag.memoryUsage.external`);
console.log(` 数组缓冲: diag.memoryUsage.arrayBuffers`);
console.log(` RSS: diag.memoryUsage.rss`);
console.log(`\n增长率: diag.memoryGrowthRate.mbPerHour MB/h`);
console.log(`\nV8 堆统计:`);
console.log(` 堆大小限制: diag.v8HeapStats.heapSizeLimit`);
console.log(` 分配内存: diag.v8HeapStats.mallocedMemory`);
console.log(` 峰值分配: diag.v8HeapStats.peakMallocedMemory`);
console.log(` 分离上下文: diag.v8HeapStats.detachedContexts`);
console.log(`\n句柄: diag.activeHandles`);
console.log(`请求: diag.activeRequests`);
if (diag.openFileDescriptors) console.log(`文件描述符: diag.openFileDescriptors`);
if (diag.potentialLeaks.length > 0) {
console.log(`\n⚠️ 潜在泄漏:`);
diag.potentialLeaks.forEach(l => console.log(` - l`));
}
console.log(`\ndiag.recommendation`);
return;
}
// 生成快照
console.log('📸 生成堆快照...\n');
const diag = getMemoryDiagnostics();
const timestamp = Date.now();
const diagPath = path.join(SNAPSHOTS_DIR, `timestamp-diagnostics.json`);
const snapshotPath = path.join(SNAPSHOTS_DIR, `timestamp.heapsnapshot`);
// 先写诊断(快照可能失败)
fs.writeFileSync(diagPath, JSON.stringify(diag, null, 2), { mode: 0o600 });
console.log(`[诊断] diagPath`);
console.log(` 堆使用: diag.memoryUsage.heapUsed`);
console.log(` RSS: diag.memoryUsage.rss\n`);
try {
await writeHeapSnapshot(snapshotPath);
console.log(`[快照] snapshotPath`);
console.log(`\n✅ 完成。在 Chrome DevTools > Memory 中加载 .heapsnapshot 文件分析。`);
} catch (err) {
console.log(`\n❌ 快照失败: err.message`);
console.log('诊断文件已保存,可单独查看。');
}
// 清理旧快照
cleanupOldSnapshots();
}
main().catch(err => {
console.error('错误:', err.message);
process.exit(1);
});
FILE:scripts/profiler.js
#!/usr/bin/env node
/**
* Headless Profiler — 性能分析
* 基于 Claude Code headlessProfiler.ts + profilerBase.ts,适配 OpenClaw
*
* 用于分析 Cron 任务和子代理的执行性能。
*
* 用法:
* node profiler.js start <阶段名> # 标记阶段开始
* node profiler.js checkpoint <阶段名> # 记录检查点
* node profiler.js report # 生成性能报告
* node profiler.js --status # 查看当前性能统计
*/
const { performance, PerformanceObserver } = require('perf_hooks');
const fs = require('fs');
const path = require('path');
const PROFILE_DIR = process.env.HOME + '/.openclaw/profiles/';
const PROFILE_FILE = path.join(PROFILE_DIR, 'current.json');
const HISTORY_FILE = path.join(PROFILE_DIR, 'history.jsonl');
fs.mkdirSync(PROFILE_DIR, { recursive: true });
/**
* 性能分析会话
*/
class ProfilerSession {
constructor() {
this.turns = [];
this.currentTurn = null;
this.checkpoints = new Map();
this.turnNumber = 0;
}
/**
* 开始新的轮次
*/
startTurn(name) {
if (this.currentTurn) {
this.endTurn();
}
this.turnNumber++;
this.currentTurn = {
number: this.turnNumber,
name: name || `turn-this.turnNumber`,
startTime: performance.now(),
processStart: process.hrtime.bigint(),
checkpoints: [],
markStart: performance.now(),
};
performance.mark(`turn_this.turnNumber_start`);
return this.currentTurn;
}
/**
* 记录检查点
*/
checkpoint(name) {
if (!this.currentTurn) return;
const now = performance.now();
const elapsed = now - this.currentTurn.startTime;
this.currentTurn.checkpoints.push({
name,
elapsed: Math.round(elapsed * 100) / 100, // ms, 2 位小数
timestamp: Date.now(),
});
performance.mark(`turn_this.currentTurn.number_name`);
}
/**
* 结束当前轮次
*/
endTurn() {
if (!this.currentTurn) return null;
const endTime = performance.now();
const duration = endTime - this.currentTurn.startTime;
const turn = {
...this.currentTurn,
endTime,
duration: Math.round(duration * 100) / 100,
};
this.turns.push(turn);
this.currentTurn = null;
// 保存到文件
this.save();
return turn;
}
/**
* 保存当前状态
*/
save() {
const state = {
turnNumber: this.turnNumber,
turns: this.turns,
currentTurn: this.currentTurn,
};
fs.writeFileSync(PROFILE_FILE, JSON.stringify(state, null, 2));
}
/**
* 追加到历史
*/
appendHistory() {
const line = JSON.stringify({
timestamp: Date.now(),
turnNumber: this.turnNumber,
turnsCount: this.turns.length,
});
fs.appendFileSync(HISTORY_FILE, line + '\n');
}
/**
* 加载状态
*/
load() {
try {
const state = JSON.parse(fs.readFileSync(PROFILE_FILE, 'utf8'));
this.turnNumber = state.turnNumber || 0;
this.turns = state.turns || [];
this.currentTurn = state.currentTurn || null;
} catch (e) {
// 无历史状态
}
}
/**
* 生成性能报告
*/
report() {
if (this.turns.length === 0) {
return '📊 无性能数据。先用 start 开始分析。';
}
const lines = ['📊 性能报告\n', `总轮次: this.turns.length\n`];
// 汇总统计
const durations = this.turns.map(t => t.duration);
const totalDuration = durations.reduce((a, b) => a + b, 0);
const avgDuration = totalDuration / durations.length;
const maxDuration = Math.max(...durations);
const minDuration = Math.min(...durations);
lines.push('=== 汇总统计 ===\n');
lines.push(`总耗时: formatMs(totalDuration)`);
lines.push(`平均耗时: formatMs(avgDuration)`);
lines.push(`最长轮次: formatMs(maxDuration)`);
lines.push(`最短轮次: formatMs(minDuration)`);
lines.push('');
// 每轮详情
lines.push('=== 每轮详情 ===\n');
for (const turn of this.turns) {
lines.push(`#turn.number turn.name: formatMs(turn.duration)`);
if (turn.checkpoints.length > 0) {
for (const cp of turn.checkpoints) {
lines.push(` ├─ cp.name: formatMs(cp.elapsed)`);
}
}
lines.push('');
}
return lines.join('\n');
}
}
function formatMs(ms) {
if (ms >= 1000) return (ms / 1000).toFixed(2) + 's';
if (ms >= 1) return Math.round(ms) + 'ms';
return Math.round(ms * 1000) + 'μs';
}
// 主函数
function main() {
const args = process.argv.slice(2);
const command = args[0];
const profiler = new ProfilerSession();
switch (command) {
case 'start':
profiler.load();
const turnName = args[1] || `turn-profiler.turnNumber + 1`;
profiler.startTurn(turnName);
profiler.save();
console.log(`▶️ 开始: turnName (轮次 #profiler.turnNumber)`);
break;
case 'checkpoint':
profiler.load();
if (!profiler.currentTurn) {
console.log('❌ 无活跃轮次。先用 start 开始。');
process.exit(1);
}
const cpName = args[1] || `checkpoint-profiler.currentTurn.checkpoints.length + 1`;
profiler.checkpoint(cpName);
profiler.save();
console.log(`📍 检查点: cpName (formatMs(performance.now() - profiler.currentTurn.startTime))`);
break;
case 'end':
profiler.load();
const turn = profiler.endTurn();
if (turn) {
console.log(`⏹️ 结束: turn.name (耗时 formatMs(turn.duration))`);
profiler.appendHistory();
} else {
console.log('❌ 无活跃轮次。');
}
break;
case 'report':
profiler.load();
console.log(profiler.report());
break;
case '--status':
profiler.load();
console.log(`轮次数: profiler.turnNumber`);
console.log(`活跃轮次: '无'`);
console.log(`已完成: profiler.turns.length`);
break;
default:
console.log('用法:');
console.log(' node profiler.js start [名称] # 开始新轮次');
console.log(' node profiler.js checkpoint [名称] # 记录检查点');
console.log(' node profiler.js end # 结束当前轮次');
console.log(' node profiler.js report # 生成报告');
console.log(' node profiler.js --status # 查看状态');
process.exit(1);
}
}
main();
FILE:skill.json
{
"name": "Heap Dump & Profiler",
"description": "内存快照分析(v8 heap snapshot)+ 性能分析(perf_hooks)",
"version": "1.0.1",
"identifier": "heap-dump",
"category": "diagnostics",
"tags": [
"memory",
"profiling",
"heap",
"performance"
],
"author": "c32"
}错误监控 - 扫描 JSON 日志、捕获 ERROR 级别错误、OpenClaw 系统级错误修复建议
---
name: error-monitor-fix
description: 错误监控 - 扫描 JSON 日志、捕获 ERROR 级别错误、OpenClaw 系统级错误修复建议
version: 3.0.0
---
# Error Monitor Fix — 错误监控技能
**版本**: 2.3.0 (所有修复策略改为提示手动,不再执行文件系统修改)
**创建日期**: 2026-03-23
**更新日期**: 2026-04-14
---
## 📋 功能
实时监控 OpenClaw 运行日志中的 error 类型错误,自动追加到 `error.md`,并尝试自动修复。
---
## 📂 文件结构
```
skills/error-monitor-fix/
├── SKILL.md
├── skill.json
├── _meta.json # ClawHub 元数据
└── scripts/
├── monitor-error.js # 错误监控(JSON 日志解析 + 去重)
└── auto-fix.js # 自动修复(5 种策略)
```
---
## 🔧 修复策略
| 策略 | 匹配条件 | 动作 |
|------|---------|------|
| Gateway 重启(提示手动) | gateway/ws/连接错误 | ⚠️ 提示用户手动重启 |
| 端口释放(只读检查) | EADDRINUSE | 检查 node 监听端口 |
| 会话清理(只读检查) | INVALID_REQUEST | dry-run 检查 |
> 缓存清理、权限修复属于 dev/test/rule 子代理操作范畴,不属于本技能。
---
## 📊 Cron 任务
| 任务名 | 频率 | Job ID |
|--------|------|--------|
| 错误监控修复 | 每 5 分钟 | `176ecc83` |
---
## ⚠️ 注意事项
- 去重窗口:1 小时(避免同一错误重复报告)
- 日志格式:JSON 行格式
- 输出:追加到 `memory/error.md`
FILE:_meta.json
{
"ownerId": "amd5",
"slug": "error-monitor-fix",
"version": "3.0.0",
"publishedAt": 1774235000000
}
FILE:scripts/auto-fix.js
#!/usr/bin/env node
/**
* Error Auto Fix — 错误自动修复
*
* 功能:
* 1. 分析 OpenClaw 日志中的 ERROR 级别错误
* 2. 识别错误类型
* 3. 提供修复建议(提示手动)
*
* 修复策略:
* - Gateway 连接错误 → 提示用户手动重启
* - 端口占用 → 只读检查
* - 会话异常 → 只读检查
*
* 注意:缓存清理、权限修复属于 dev/test/rule 子代理的操作范畴,
* 不属于系统错误监控技能的职责。
*
* 用法:
* node auto-fix.js
* node auto-fix.js --dry-run # 只报告不执行
* node auto-fix.js --json # JSON 输出
*/
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const WORKSPACE = process.env.HOME + '/.openclaw/workspace';
const ERROR_FILE = path.join(WORKSPACE, 'error.md');
const LOG_FILE = `/tmp/openclaw/openclaw-new Date().toISOString().slice(0, 10).log`;
const FIX_LOG = path.join(WORKSPACE, '.fix-log.json');
// ============================================================================
// 修复策略定义
// ============================================================================
const FIX_STRATEGIES = [
{
id: 'gateway_restart',
name: 'Gateway 重启(提示手动)',
match: (errors) => {
return errors.some(e =>
e.includes('gateway') ||
e.includes('ws') ||
e.includes('WebSocket') ||
e.includes('connect') ||
e.includes('fetch failed')
);
},
action: () => {
return '⚠️ 检测到 Gateway 连接错误,请手动执行: openclaw gateway restart(或 systemctl --user restart openclaw-gateway.service)';
},
},
{
id: 'port_release',
name: '端口释放(只读检查)',
match: (errors) => {
return errors.some(e =>
e.includes('EADDRINUSE') ||
e.includes('port') && e.includes('in use') ||
e.includes('address already in use')
);
},
action: () => {
try {
const output = execSync('ss -tlnp 2>/dev/null | grep node | awk \'{print $4}\' | grep -oP ":\\K\\d+"', { encoding: 'utf8' });
const ports = output.trim().split('\n').filter(Boolean);
if (ports.length > 0) {
return `发现 node 监听端口: ports.join(', ')(无需强制释放,属正常监听)`;
}
return '无端口占用问题';
} catch {
return '端口检查失败';
}
},
},
{
id: 'session_cleanup',
name: '会话清理(只读检查)',
match: (errors) => {
return errors.some(e =>
e.includes('session') ||
e.includes('INVALID_REQUEST') ||
e.includes('not found')
);
},
action: () => {
try {
execSync('openclaw sessions cleanup --dry-run 2>/dev/null', { timeout: 10000 });
return '会话清理已检查';
} catch {
return '会话清理检查失败';
}
},
},
];
// ============================================================================
// 工具函数
// ============================================================================
function loadFixLog() {
try {
return JSON.parse(fs.readFileSync(FIX_LOG, 'utf8'));
} catch {
return { fixes: [], lastRun: null };
}
}
function saveFixLog(log) {
log.lastRun = new Date().toISOString();
fs.writeFileSync(FIX_LOG, JSON.stringify(log, null, 2));
}
function getRecentErrors() {
if (!fs.existsSync(LOG_FILE)) return [];
const content = fs.readFileSync(LOG_FILE, 'utf8');
const lines = content.split('\n').filter(l => l.trim());
const now = Date.now();
const cutoff = now - 60 * 60 * 1000; // 最近 1 小时
const errors = [];
for (const line of lines) {
try {
const obj = JSON.parse(line);
if (obj._meta?.logLevelId !== 5) continue;
const time = new Date(obj.time).getTime();
if (time < cutoff) continue;
let message = '';
for (const key of Object.keys(obj)) {
if (key === '_meta' || key === 'time') continue;
message += typeof obj[key] === 'string' ? obj[key] + ' ' : JSON.stringify(obj[key]) + ' ';
}
errors.push(message.trim());
} catch {}
}
return errors;
}
// ============================================================================
// 核心逻辑
// ============================================================================
function autoFix() {
const dryRun = process.argv.includes('--dry-run');
const jsonMode = process.argv.includes('--json');
const errors = getRecentErrors();
if (errors.length === 0) {
if (jsonMode) {
console.log(JSON.stringify({ message: '无待修复错误', fixes: [] }));
} else {
console.log('✅ 无待修复错误');
console.log('NO_REPLY');
}
return;
}
const fixLog = loadFixLog();
const applied = [];
for (const strategy of FIX_STRATEGIES) {
if (strategy.match(errors)) {
const result = {
id: strategy.id,
name: strategy.name,
status: dryRun ? 'dry-run' : 'pending',
message: '',
};
if (dryRun) {
result.message = `[DRY RUN] 将执行: strategy.name`;
} else {
try {
result.message = strategy.action();
result.status = 'success';
} catch (e) {
result.status = 'failed';
result.message = e.message || '修复失败';
}
}
applied.push(result);
}
}
// 记录修复历史
fixLog.fixes.push({
timestamp: new Date().toISOString(),
errorCount: errors.length,
fixes: applied,
});
// 只保留最近 100 条
if (fixLog.fixes.length > 100) {
fixLog.fixes = fixLog.fixes.slice(-100);
}
saveFixLog(fixLog);
// 输出
if (jsonMode) {
console.log(JSON.stringify({
errors: errors.length,
fixes: applied,
}, null, 2));
return;
}
if (applied.length === 0) {
console.log('✅ 无匹配的自动修复策略');
console.log('NO_REPLY');
return;
}
console.log(`🔧 执行了 applied.length 个修复操作:`);
console.log('');
for (const fix of applied) {
const icon = fix.status === 'success' ? '✅' : fix.status === 'failed' ? '❌' : '🔵';
console.log(`icon [fix.name] fix.message`);
}
console.log('');
console.log('✅ 自动修复完成');
}
autoFix();
FILE:scripts/monitor-error.js
#!/usr/bin/env node
/**
* Error Monitor — OpenClaw 错误日志监控
*
* 功能:
* 1. 扫描 /tmp/openclaw/openclaw-YYYY-MM-DD.log JSON 日志
* 2. 提取 ERROR 级别日志(logLevelId: 5)
* 3. 去重:同一错误类型 1 小时内只记录一次
* 4. 追加到 ~/.openclaw/workspace/error.md
*
* 用法:
* node monitor-error.js # 检查最近 5 分钟
* node monitor-error.js --all # 检查今日全部
* node monitor-error.js --json # JSON 输出
*/
const fs = require('fs');
const path = require('path');
const WORKSPACE = process.env.HOME + '/.openclaw/workspace';
const ERROR_FILE = path.join(WORKSPACE, 'error.md');
const LOG_FILE = `/tmp/openclaw/openclaw-new Date().toISOString().slice(0, 10).log`;
const DEDUP_FILE = path.join(WORKSPACE, '.error-dedup.json');
const WINDOW_MS = 5 * 60 * 1000; // 5 分钟窗口
const DEDUP_MS = 60 * 60 * 1000; // 1 小时去重
// ============================================================================
// 工具函数
// ============================================================================
function loadDedup() {
try {
return JSON.parse(fs.readFileSync(DEDUP_FILE, 'utf8'));
} catch {
return {};
}
}
function saveDedup(dedup) {
// 清理过期的
const now = Date.now();
const cleaned = {};
for (const [key, time] of Object.entries(dedup)) {
if (now - time < DEDUP_MS) cleaned[key] = time;
}
fs.writeFileSync(DEDUP_FILE, JSON.stringify(cleaned, null, 2));
}
function hashError(err) {
// 生成错误指纹:子系统 + 错误类型
let subsystem = err.subsystem || err.channel || 'unknown';
let errorType = '';
if (err.message?.includes('fetch failed')) errorType = 'fetch_failed';
else if (err.message?.includes('timeout')) errorType = 'timeout';
else if (err.message?.includes('ECONNABORTED')) errorType = 'conn_aborted';
else if (err.message?.includes('ws') && err.message?.includes('fail')) errorType = 'ws_failed';
else if (err.message?.includes('EACCES') || err.message?.includes('permission')) errorType = 'permission';
else if (err.message?.includes('EADDRINUSE')) errorType = 'port_in_use';
else if (err.message?.includes('ENOENT')) errorType = 'file_not_found';
else if (err.message?.includes('memory') || err.message?.includes('OOM')) errorType = 'memory';
else if (err.message?.includes('DeprecationWarning')) errorType = 'deprecation';
else errorType = 'unknown';
return `subsystem:errorType`;
}
function parseLogLine(line) {
try {
const obj = JSON.parse(line);
const logLevelId = obj._meta?.logLevelId || 0;
const logLevelName = obj._meta?.logLevelName || 'INFO';
// 只收集 ERROR 级别 (logLevelId: 5)
if (logLevelId !== 5) return null;
// 合并消息
let message = '';
const parts = [];
for (const key of Object.keys(obj)) {
if (key === '_meta' || key === 'time') continue;
parts.push(typeof obj[key] === 'string' ? obj[key] : JSON.stringify(obj[key]));
}
message = parts.join(' | ');
// 提取子系统
let subsystem = '';
if (typeof obj[0] === 'string') {
const subMatch = obj[0].match(/gateway\/channels\/([^/]+)/);
if (subMatch) subsystem = subMatch[1];
else if (obj[0].includes('subsystem')) {
try {
const subObj = JSON.parse(obj[0]);
subsystem = subObj.subsystem || '';
} catch {}
}
}
return {
timestamp: obj.time || '',
subsystem,
message: message.slice(0, 500),
level: logLevelName,
hash: '',
};
} catch {
return null;
}
}
// ============================================================================
// 核心逻辑
// ============================================================================
function scanErrors() {
const now = Date.now();
const cutoff = now - WINDOW_MS;
const jsonMode = process.argv.includes('--json');
const allMode = process.argv.includes('--all');
// 检查日志文件
if (!fs.existsSync(LOG_FILE)) {
if (jsonMode) {
console.log(JSON.stringify({ errors: [], message: '今日日志不存在' }));
} else {
console.log(`📭 今日日志文件不存在:LOG_FILE`);
console.log('NO_REPLY');
}
return;
}
// 读取日志
const content = fs.readFileSync(LOG_FILE, 'utf8');
const lines = content.split('\n').filter(l => l.trim());
const errors = [];
const dedup = loadDedup();
for (const line of lines) {
const entry = parseLogLine(line);
if (!entry) continue;
// 时间过滤
if (!allMode) {
const entryTime = new Date(entry.timestamp).getTime();
if (entryTime < cutoff) continue;
}
// 去重
entry.hash = hashError(entry);
if (dedup[entry.hash] && now - dedup[entry.hash] < DEDUP_MS) {
continue; // 已记录过,跳过
}
errors.push(entry);
dedup[entry.hash] = now;
}
saveDedup(dedup);
// 输出
if (errors.length === 0) {
if (jsonMode) {
console.log(JSON.stringify({ errors: [], message: '无新错误' }));
} else {
console.log(`✅ 无新错误('最近 5 分钟')`);
console.log('NO_REPLY');
}
return;
}
if (jsonMode) {
console.log(JSON.stringify({ errors, count: errors.length }, null, 2));
return;
}
// 追加到 error.md
const lines_out = [];
lines_out.push(`## 'Asia/Shanghai')} 自动扫描`);
lines_out.push('');
lines_out.push(`**扫描时间**: new Date().toISOString()`);
lines_out.push(`**扫描范围**: '最近 5 分钟'`);
lines_out.push(`**发现错误**: errors.length 条`);
lines_out.push('');
lines_out.push('| 时间 | 子系统 | 错误类型 | 描述 |');
lines_out.push('|------|--------|----------|------|');
for (const err of errors) {
const time = err.timestamp.slice(11, 19);
const shortMsg = err.message.slice(0, 80).replace(/\|/g, '\\|');
const errorType = err.hash.split(':')[1] || 'unknown';
lines_out.push(`| time | err.subsystem || '-' | errorType | shortMsg |`);
}
lines_out.push('');
lines_out.push('---');
lines_out.push('');
fs.appendFileSync(ERROR_FILE, '\n' + lines_out.join('\n'));
// 输出报告
console.log(`⚠️ 发现 errors.length 条新错误:`);
console.log('');
for (const err of errors) {
console.log(` 🔴 [err.subsystem || 'system'] ')[1]`);
console.log(` err.message.slice(0, 100)`);
console.log('');
}
console.log('✅ 已追加到 error.md');
}
scanErrors();
FILE:skill.json
{
"name": "Error Monitor Fix",
"description": "错误监控 - OpenClaw 系统级错误扫描与修复建议(Gateway/端口/会话)",
"version": "3.0.0",
"identifier": "error-monitor-fix",
"author": "c32",
"category": "monitoring",
"tags": [
"error",
"monitor",
"auto-fix",
"log"
],
"requirements": {
"node": ">=18.0.0",
"systemd": true
},
"scripts": {
"postinstall": "bash scripts/install.sh"
}
}工作进度检查技能 - 定期检查待办事项 + 子代理超时/消失检测与自动恢复 + 全量会话监控
---
name: work-progress
description: 工作进度检查技能 - 定期检查待办事项 + 子代理超时/消失检测与自动恢复 + 全量会话监控
version: 4.0.6
author: c32
---
# Work Progress Skill - 工作进度检查技能
**版本**: 4.0.6
**创建日期**: 2026-03-11
**更新日期**: 2026-04-14
**作者**: c32
---
## 📋 技能描述
定期检查工作进度和待办事项完成情况,主动检测子代理超时/消失任务并自动恢复。
---
## 📂 文件结构
```
skills/work-progress/
├── SKILL.md # 本文件
├── skill.json # 技能元数据 (v4.0.2)
├── _meta.json # ClawHub 元数据
├── .clawhub/
│ └── origin.json # 来源信息
├── state.json # 任务状态持久化(自动维护)
└── scripts/
├── check-progress.js # 进度检查(Node.js)
├── auto-recover.js # 自动恢复(Node.js)
├── work-monitor.js # 全量会话监控(Node.js)
└── install.js # 安装脚本
```
---
## 🎯 功能
### check-progress.js — 进度检查
- 状态同步:发现/注册子代理任务
- 进度检查:超时检测
- 待办事项:检查 daily 文件
- 终态 GC:自动清理完成任务(5 分钟 grace period)
### auto-recover.js — 自动恢复
- 检查超时/消失/失败任务
- 记录到 error.md
- 建议恢复操作
### work-monitor.js — 全量会话监控
- 扫描所有 Agent 的活跃会话
- 检测超时/卡死/失败会话
- 输出结构化监控报告
---
## 📊 Cron 任务
| 任务 | 频率 | Job ID |
|------|------|--------|
| 工作进度检查 | 10m | `6a4bde16` |
| 全量工作监控 | 5m | `98f5a84a` |
---
## 🔄 状态机
```
pending → running → completed/failed/disappeared → notified → GC
```
---
*技能位置:`~/.openclaw/workspace/skills/work-progress/`*
FILE:_meta.json
{
"ownerId": "kn709ac4cea693g2hg2p4p3sgn82vk9z",
"slug": "work-progress",
"version": "4.0.2",
"publishedAt": 1774239093852
}
FILE:scripts/auto-recover.js
#!/usr/bin/env node
/**
* Auto Recover — 超时任务自动恢复
*
* 功能:
* 1. 检查 state.json 中超时/消失的任务
* 2. 记录到 error.md
* 3. 建议恢复操作
*
* 用法:
* node auto-recover.js
* node auto-recover.js --dry-run
*/
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const WORKSPACE = process.env.HOME + '/.openclaw/workspace';
const STATE_FILE = path.join(WORKSPACE, 'skills/work-progress/state.json');
const LOG_FILE = path.join(WORKSPACE, 'memory/error.md');
const NOW = new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' });
function loadState() {
try {
return JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'));
} catch {
return { tasks: {}, evicted: [], lastCheck: null, version: 1 };
}
}
function main() {
const dryRun = process.argv.includes('--dry-run');
const state = loadState();
const tasks = state.tasks || {};
const recoverable = [];
for (const [key, task] of Object.entries(tasks)) {
if (task.status === 'disappeared' || task.status === 'failed') {
recoverable.push({ key, label: task.label, status: task.status });
}
if (task.status === 'running' && (task.timeouts || 0) >= 2) {
recoverable.push({ key, label: task.label, status: 'timeout', timeouts: task.timeouts });
}
}
if (recoverable.length === 0) {
console.log('NO_REPLY');
return;
}
console.log(`🔄 发现 recoverable.length 个可恢复任务:`);
console.log('');
for (const r of recoverable) {
console.log(` • [r.status] r.label || r.key`);
if (!dryRun) {
// 记录到 error.md
const entry = `\n### [NOW] 任务异常 - r.label || r.key\n状态: r.status\n建议: 重新执行\n\n---\n`;
fs.appendFileSync(LOG_FILE, entry);
}
}
console.log('');
if (dryRun) {
console.log('[DRY RUN] 未执行恢复操作');
} else {
console.log('✅ 已记录到 error.md');
}
}
main();
FILE:scripts/check-progress.js
#!/usr/bin/env node
/**
* Work Progress Check — 工作进度检查
*
* 功能:
* 1. 状态同步 — 发现/注册子代理任务
* 2. 进度检查 — 增量输出追踪
* 3. 待办事项 — 日常检查
* 4. 终态 GC — 自动清理完成任务
*
* 用法:
* node check-progress.js # 全量检查
* node check-progress.js --json # JSON 输出
*/
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const WORKSPACE = process.env.HOME + '/.openclaw/workspace';
const STATE_FILE = path.join(WORKSPACE, 'skills/work-progress/state.json');
const TODAY = new Date().toISOString().slice(0, 10);
const NOW = new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' });
const DAILY_PATH = path.join(WORKSPACE, 'memory/daily', TODAY + '.md');
const LOG_FILE = path.join(WORKSPACE, 'memory/error.md');
// ============================================================================
// 工具函数
// ============================================================================
function loadState() {
try {
return JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'));
} catch {
return { tasks: {}, evicted: [], lastCheck: null, version: 1 };
}
}
function saveState(state) {
state.lastCheck = new Date().toISOString();
fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
}
function runOpenClaw(args) {
try {
return JSON.parse(execSync(`openclaw args --json 2>/dev/null`, {
encoding: 'utf8',
timeout: 15000,
}));
} catch {
return null;
}
}
// ============================================================================
// 1. 状态同步
// ============================================================================
function syncState() {
const data = runOpenClaw('sessions list --all-agents --active 30');
const state = loadState();
const sessions = data?.sessions || [];
const issues = [];
const currentKeys = new Set();
for (const s of sessions) {
if (!s.key) continue;
currentKeys.add(s.key);
if (!state.tasks[s.key]) {
state.tasks[s.key] = {
id: s.key,
status: s.status,
label: s.label || s.displayName,
firstSeen: new Date().toISOString(),
lastSeen: new Date().toISOString(),
timeouts: 0,
};
} else {
const prev = state.tasks[s.key].status;
state.tasks[s.key].status = s.status;
state.tasks[s.key].lastSeen = new Date().toISOString();
if (prev === 'running' && (s.status === 'failed' || s.status === 'done')) {
issues.push({
type: 'task_completed',
key: s.key,
label: s.label,
status: s.status,
});
}
}
}
// 标记消失的会话
for (const [key, task] of Object.entries(state.tasks)) {
if (!currentKeys.has(key) && task.status === 'running') {
task.status = 'disappeared';
issues.push({
type: 'session_disappeared',
key,
label: task.label,
});
}
}
saveState(state);
return { sessions: sessions.length, issues };
}
// ============================================================================
// 2. 进度检查
// ============================================================================
function checkProgress() {
const state = loadState();
const issues = [];
const now = Date.now();
for (const [key, task] of Object.entries(state.tasks)) {
if (task.status !== 'running') continue;
const started = new Date(task.firstSeen).getTime();
const elapsed = (now - started) / 1000 / 60; // 分钟
if (elapsed > 30) {
task.timeouts = (task.timeouts || 0) + 1;
issues.push({
type: 'timeout_warning',
key,
label: task.label,
elapsed: Math.floor(elapsed) + '分钟',
timeouts: task.timeouts,
});
}
}
saveState(state);
return issues;
}
// ============================================================================
// 3. 待办事项检查
// ============================================================================
function checkTodos() {
if (!fs.existsSync(DAILY_PATH)) {
return { found: false, count: 0, todos: [] };
}
const content = fs.readFileSync(DAILY_PATH, 'utf8');
const todos = content.match(/^- \[ \] .+/gm) || [];
const done = content.match(/^- \[x\] .+/gm) || [];
return {
found: true,
count: todos.length,
done: done.length,
todos: todos.map(t => t.replace(/^- \[ \] /, '')),
};
}
// ============================================================================
// 4. 终态 GC
// ============================================================================
function gcTerminal() {
const state = loadState();
const terminal = new Set(['completed', 'failed', 'killed', 'disappeared', 'done']);
const graceMs = 5 * 60 * 1000; // 5 分钟
const now = Date.now();
let cleaned = 0;
for (const [key, task] of Object.entries(state.tasks)) {
if (!terminal.has(task.status)) continue;
if (!task.notified) {
task.notified = true;
task.notifiedAt = new Date().toISOString();
} else if (task.notifiedAt) {
const notifiedTime = new Date(task.notifiedAt).getTime();
if (now - notifiedTime > graceMs) {
if (!state.evicted) state.evicted = [];
state.evicted.push({ id: key, evictedAt: new Date().toISOString() });
delete state.tasks[key];
cleaned++;
}
}
}
// 只保留最近 50 条 evicted
if (state.evicted && state.evicted.length > 50) {
state.evicted = state.evicted.slice(-50);
}
saveState(state);
return cleaned;
}
// ============================================================================
// 主流程
// ============================================================================
function main() {
const jsonMode = process.argv.includes('--json');
const sync = syncState();
const progress = checkProgress();
const todos = checkTodos();
const gc = gcTerminal();
const hasIssues = sync.issues.length > 0 || progress.length > 0 || todos.count > 0;
if (jsonMode) {
console.log(JSON.stringify({
timestamp: NOW,
sync,
progress,
todos,
gc,
}, null, 2));
return;
}
if (!hasIssues) {
console.log('NO_REPLY');
return;
}
const lines = [];
lines.push(`🔍 工作进度检查 (NOW)`);
lines.push('═'.repeat(40));
if (sync.issues.length > 0) {
lines.push(`\n⚠️ sync.issues.length 个状态变化:`);
for (const issue of sync.issues) {
lines.push(` • [issue.type] issue.label || issue.key`);
}
}
if (progress.length > 0) {
lines.push(`\n⏱️ progress.length 个超时任务:`);
for (const p of progress) {
lines.push(` • [p.label] 已运行 p.elapsed (超时 p.timeouts 次)`);
}
}
if (todos.count > 0) {
lines.push(`\n📋 todos.count 个待办未完成:`);
for (const t of todos.todos.slice(0, 5)) {
lines.push(` • t`);
}
}
if (gc > 0) {
lines.push(`\n🧹 已清理 gc 个终态任务`);
}
lines.push('');
console.log(lines.join('\n'));
}
main();
FILE:scripts/install.js
#!/usr/bin/env node
/**
* Work Progress Skill - 安装脚本
* 用法:node scripts/install.js
*
* 原为 install.sh,已转为纯 JS
*/
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const SKILL_DIR = path.join(__dirname, '..');
const WORKSPACE = path.join(process.env.HOME, '.openclaw', 'workspace');
console.log('🔧 安装 work-progress 技能...');
console.log('================================\n');
// 创建必要的目录
const dirs = [
path.join(WORKSPACE, 'memory', 'daily'),
path.join(WORKSPACE, 'memory', 'weekly'),
];
for (const dir of dirs) {
fs.mkdirSync(dir, { recursive: true });
console.log(`✅ path.relative(WORKSPACE, dir)/`);
}
console.log('\n🔧 安装完成!');
console.log('请确认 openclaw cron list 中包含 work-progress 相关定时任务');
FILE:scripts/work-monitor.js
#!/usr/bin/env node
/**
* Work Monitor — 全量工作监控脚本
*
* 功能:
* 1. 扫描所有 agent 的活跃会话
* 2. 检测超时/卡死/失败的会话
* 3. 检测子会话(childSessions)状态
* 4. 检测未完成的工作流
* 5. 输出监控报告
*
* 用法:
* node work-monitor.js # 全量检查
* node work-monitor.js --agents # 只看 agent 状态
* node work-monitor.js --cron # 只看 cron 任务
* node work-monitor.js --children # 只看子会话
* node work-monitor.js --json # JSON 输出
*
* 此脚本设计为被 OpenClaw agent 通过 exec 调用,
* agent 读取输出后决定是否需要人工介入或自动恢复。
*/
const { execSync } = require('child_process');
const path = require('path');
const fs = require('fs');
const WORKSPACE = process.env.HOME + '/.openclaw/workspace';
const STATE_FILE = path.join(WORKSPACE, 'skills/work-progress/state.json');
const TODAY = new Date().toISOString().slice(0, 10);
const NOW = new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' });
// ============================================================================
// 超时配置(毫秒)
// ============================================================================
const TIMEOUT = {
cron_normal: 60_000, // cron 任务正常上限:1 分钟
cron_warning: 120_000, // cron 任务警告阈值:2 分钟
cron_critical: 300_000, // cron 任务严重超时:5 分钟
session_idle: 600_000, // 会话空闲上限:10 分钟无活动
child_session: 1800_000, // 子会话运行上限:30 分钟
};
// ============================================================================
// 工具函数
// ============================================================================
function runOpenClaw(args) {
try {
return JSON.parse(execSync(`openclaw args --json 2>/dev/null`, {
encoding: 'utf8',
timeout: 15000,
}));
} catch (e) {
return null;
}
}
function fmtDuration(ms) {
const s = Math.floor(ms / 1000);
if (s < 60) return `ss`;
const m = Math.floor(s / 60);
if (m < 60) return `mms % 60s`;
const h = Math.floor(m / 60);
return `hhm % 60m`;
}
function isTimedOut(session, nowMs) {
const started = session.startedAt || 0;
const elapsed = nowMs - started;
const label = session.label || session.displayName || '';
const isCron = label.includes('Cron') || session.kind === 'other';
if (isCron && elapsed > TIMEOUT.cron_critical) return 'critical';
if (isCron && elapsed > TIMEOUT.cron_warning) return 'warning';
if (elapsed > TIMEOUT.child_session) return 'critical';
if (elapsed > TIMEOUT.session_idle && session.status === 'running') return 'warning';
return null;
}
function loadState() {
try {
return JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'));
} catch {
return { tasks: {}, evicted: [], lastCheck: null, version: 1 };
}
}
function saveState(state) {
state.lastCheck = new Date().toISOString();
fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
}
// ============================================================================
// 核心监控逻辑
// ============================================================================
function scanAllSessions() {
const data = runOpenClaw('sessions list --all-agents --active 60');
if (!data || !data.sessions) return [];
return data.sessions;
}
function analyzeSessions(sessions) {
const now = Date.now();
const state = loadState();
const report = {
timestamp: NOW,
summary: {
total: sessions.length,
running: 0,
done: 0,
failed: 0,
idle: 0,
timed_out: 0,
},
agents: {},
cron_tasks: [],
child_sessions: [],
issues: [],
recoverable: [],
};
for (const s of sessions) {
const agentId = s.key?.split(':')[1] || 'unknown';
const elapsed = now - (s.startedAt || now);
const timeoutLevel = isTimedOut(s, now);
// 按 agent 分组
if (!report.agents[agentId]) {
report.agents[agentId] = { sessions: 0, running: 0, issues: [] };
}
report.agents[agentId].sessions++;
// 分类统计
if (s.status === 'running') {
report.summary.running++;
report.agents[agentId].running++;
} else if (s.status === 'failed') {
report.summary.failed++;
} else if (s.status === 'done') {
report.summary.done++;
}
// Cron 任务分析
if (s.label?.includes('Cron')) {
const cronInfo = {
key: s.key,
label: s.label,
agent: agentId,
status: s.status,
elapsed: fmtDuration(elapsed),
elapsedMs: elapsed,
totalTokens: s.totalTokens || 0,
cost: s.estimatedCostUsd || 0,
};
if (timeoutLevel) {
cronInfo.timeout = timeoutLevel;
report.summary.timed_out++;
report.issues.push({
type: 'cron_timeout',
severity: timeoutLevel,
session: s.key,
label: s.label,
elapsed: cronInfo.elapsed,
});
report.recoverable.push({
type: 'restart_cron',
key: s.key,
label: s.label,
reason: `timeoutLevel 超时 (cronInfo.elapsed)`,
});
}
report.cron_tasks.push(cronInfo);
}
// 子会话分析
if (s.childSessions?.length > 0) {
for (const childKey of s.childSessions) {
report.child_sessions.push({
parent: s.key,
child: childKey,
status: s.status,
});
}
}
// 失败会话检测
if (s.status === 'failed') {
report.issues.push({
type: 'session_failed',
severity: 'critical',
session: s.key,
label: s.label || s.displayName,
elapsed: fmtDuration(elapsed),
});
report.recoverable.push({
type: 'investigate_failed',
key: s.key,
label: s.label || s.displayName,
reason: `会话失败,运行时长 fmtDuration(s.runtimeMs || elapsed)`,
});
}
// 空闲检测
if (s.status === 'running' && timeoutLevel === 'warning' && !timeoutLevel) {
report.summary.idle++;
}
}
// 更新状态机
for (const s of sessions) {
if (s.key && !state.tasks[s.key]) {
state.tasks[s.key] = {
id: s.key,
status: s.status,
label: s.label || s.displayName,
firstSeen: new Date().toISOString(),
lastSeen: new Date().toISOString(),
timeouts: 0,
};
} else if (state.tasks[s.key]) {
state.tasks[s.key].status = s.status;
state.tasks[s.key].lastSeen = new Date().toISOString();
}
}
// 标记消失的会话
const currentKeys = new Set(sessions.map(s => s.key));
for (const [key, task] of Object.entries(state.tasks)) {
if (!currentKeys.has(key) && task.status === 'running') {
task.status = 'disappeared';
report.issues.push({
type: 'session_disappeared',
severity: 'warning',
session: key,
label: task.label,
reason: '会话消失(可能崩溃或被杀死)',
});
}
}
saveState(state);
return report;
}
function formatReport(report) {
const lines = [];
lines.push(`🔍 全量工作监控报告 (report.timestamp)`);
lines.push('═'.repeat(50));
lines.push('');
// 摘要
const s = report.summary;
lines.push(`📊 会话摘要: 总计 s.total | 运行 s.running | 完成 s.done | 失败 s.failed | 超时 s.timed_out`);
lines.push('');
if (report.issues.length === 0) {
lines.push('✅ 一切正常,无异常会话');
lines.push('NO_REPLY');
return lines.join('\n');
}
// 问题详情
lines.push(`⚠️ 发现 report.issues.length 个问题:`);
lines.push('');
for (const issue of report.issues) {
const icon = issue.severity === 'critical' ? '🔴' : issue.severity === 'warning' ? '🟡' : '🔵';
lines.push(`icon [issue.severity.toUpperCase()] issue.type`);
lines.push(` 会话: issue.session`);
if (issue.label) lines.push(` 标签: issue.label`);
if (issue.elapsed) lines.push(` 时长: issue.elapsed`);
if (issue.reason) lines.push(` 原因: issue.reason`);
lines.push('');
}
// 可恢复项
if (report.recoverable.length > 0) {
lines.push('🔄 建议恢复操作:');
for (const r of report.recoverable) {
lines.push(` • r.type: r.label — r.reason`);
}
lines.push('');
}
// Agent 状态
lines.push('📋 Agent 状态:');
for (const [agent, info] of Object.entries(report.agents)) {
const status = info.running > 0 ? `🟢 info.running running` : '⚪ idle';
lines.push(` agent: info.sessions 会话, status`);
}
lines.push('');
lines.push('═'.repeat(50));
return lines.join('\n');
}
// ============================================================================
// 主入口
// ============================================================================
function main() {
const args = process.argv.slice(2);
const jsonMode = args.includes('--json');
const mode = args.find(a => a.startsWith('--')) || '--all';
const sessions = scanAllSessions();
const report = analyzeSessions(sessions);
if (jsonMode) {
console.log(JSON.stringify(report, null, 2));
return;
}
console.log(formatReport(report));
}
main();
FILE:skill.json
{
"name": "Work Progress",
"description": "工作进度检查技能 - 定期检查待办事项 + 子代理超时检测与自动恢复 + 全量会话监控(全JS)",
"version": "4.0.6",
"identifier": "work-progress",
"author": "c32",
"category": "productivity"
}
FILE:state.json
{
"tasks": {},
"evicted": [],
"lastCheck": "2026-04-14T07:56:26.367Z",
"version": 1
}记忆管理技能 - 三层时间架构 + 自动搜索/提取/会话笔记统一 Hook + 记忆巩固(整合 auto-dream)
---
name: memory-archiver
description: 记忆管理技能 - 三层时间架构 + 自动搜索/提取/会话笔记统一 Hook + 记忆巩固(整合 auto-dream)
version: 10.3.0
author: c32
---
# Memory Archiver Skill - 记忆归档技能
**版本**: 10.3.0 (安全修复:execFile 杜绝命令注入)
**创建日期**: 2026-03-11
**更新日期**: 2026-04-14
**作者**: c32
---
## 📋 技能描述
**二维记忆架构**:时间分层 × 类型标签
- **时间分层**: daily (每天) → weekly (每周) → long-term (长期/MEMORY.md)
- **类型标签**: [episodic] 事件 / [semantic] 知识 / [procedural] 流程
- **存储**: 每日记忆 + 每周记忆 + 长期精选记忆
- **WAL 协议**: Write-Ahead Log,写前日志防数据丢失
- **统一 Hook** (message:received): 自动搜索 + 自动提取 + 会话笔记追踪(整合原 auto-memory-extract + session-notes)
---
## 🎯 功能清单
### 时间分层任务
| 任务 | 频率 | 说明 |
|------|------|------|
| **记忆及时写入** | 10 分钟 | 检查并写入重要信息到 daily 文件 |
| **记忆归档 - Daily 层** | 每天 23:00 | 提炼当天内容到 daily 文件 |
| **记忆总结 - Weekly 层** | 每周日 22:00 | 提炼 weekly 到 MEMORY.md 长期记忆 |
### 记忆巩固(原 auto-dream)
| 任务 | 频率 | 说明 |
|------|------|------|
| **记忆巩固** | 每 6 小时 | 闸门检查(24h/5新会话) → 老化清理 → 数量限制 → 索引更新 → 去重 |
| **老化清理** | 随巩固触发 | 标记并清理超过 30 天的记忆文件 |
| **数量限制** | 随巩固触发 | 每类型最多 50 条,超出清理最旧的 |
| **索引更新** | 随巩固触发 | 重建 MEMORY.md 底部记忆索引 |
| **去重** | 随巩固触发 | MEMORY.md 段落级去重(清理重复/无效内容) |
### 自动搜索 Hook(多维度增强)
| 功能 | 说明 |
|------|------|
| **消息类型检测** | 疑问/修复/规范/特征/配置/命令/技术 |
| **关键词提取** | 自动提取中英文关键词 |
| **维度 1: 关键词搜索** | 在 SESSION-STATE.md 缓存中搜索 |
| **维度 2: 类型标签搜索** | 按 [episodic]/[semantic]/[procedural] 标签搜索 |
| **维度 3: 时间维度搜索** | 今日→昨日→长期记忆,优先最近 |
| **维度 4: 组合搜索** | 多关键词 OR 关系,扩大匹配范围 |
| **上下文注入** | 合并所有维度结果注入 prompt |
### 自动记忆提取
| 功能 | 说明 |
|------|------|
| **记忆分类** | 基于关键词和模式自动分类为 user/feedback/project/reference |
| **自动去重** | MD5 hash + 模糊匹配,防止重复写入 |
| **索引更新** | 自动更新 MEMORY.md 底部记忆索引 |
### 会话笔记追踪
| 功能 | 说明 |
|------|------|
| **自动初始化** | 新会话自动创建笔记 |n| **消息计数** | 每 10 条消息更新一次笔记 |
| **会话归档** | 会话结束自动生成摘要并归档 |
---
## 📂 文件结构
```
skills/memory-archiver/
├── SKILL.md # 本文件
├── skill.json # 技能元数据
├── _meta.json # ClawHub 元数据
├── .clawhub/ # ClawHub 同步目录
│ └── origin.json # 来源信息
├── scripts/
│ ├── install.js # 安装脚本(含 hook 自动注册)
│ ├── auto-memory-search.js # 自动记忆搜索(被 hook 调用)
│ ├── memory-loader.js # 加载记忆到缓存
│ ├── memory-search.js # 搜索记忆
│ ├── memory-refresh.js # 智能刷新缓存
│ ├── memory-dedup.js # MEMORY.md 段落级去重
│ ├── memory-extract.js # 从对话提取记忆
│ ├── memory-classify.js # 关键词分类器
│ ├── memory-dedup-extract.js # 提取去重(MD5 hash)
│ ├── memory-aging.js # 记忆老化与数量限制检查
│ ├── dream-consolidate.js # 记忆巩固主程序(闸门+索引+编排,原 auto-dream)
│ ├── dream-lock.js # 文件锁(防止并发巩固)
│ ├── session-tracker.js # 会话笔记追踪
│ └── README.md # 脚本说明文档
├── hooks/ # Hook 源文件(安装时复制到 workspace/hooks/)
│ ├── handler.js # Hook 处理器(事件:message:received)
│ ├── HOOK.md # Hook 元数据
│ └── bootstrap-loader/ # 启动加载 Hook
│ ├── handler.js # Hook 处理器(事件:agent:bootstrap)
│ └── HOOK.md # Hook 元数据
└── prompts/ # 提示词
└── consolidation.md # 记忆巩固提示词
```
### 安装后的工作区文件
```
~/.openclaw/workspace/
├── MEMORY.md # 长期精选记忆
├── hooks/
│ └── memory-archiver-hook/ # Hook(由 install.js 自动部署)
│ ├── handler.js
│ └── HOOK.md
└── memory/
├── daily/ # 每日记忆
├── weekly/ # 每周记忆
├── auto/ # 自动分类记忆 (user/feedback/project/reference)
├── .dream-state.json # 巩固状态(自动维护)
└── .dream.lock # 巩固文件锁
```
---
## 🔧 安装
### 方法 1: 通过 ClawHub 安装(推荐 ⭐)
```bash
clawhub install memory-archiver
```
安装后**自动执行**:
1. 创建 `memory/daily/` 和 `memory/weekly/` 目录
2. 部署 hook 到 `workspace/hooks/memory-archiver-hook/`
3. 执行 `openclaw hooks install --link` 注册 hook
4. 自动添加 3 个 cron 任务
5. 提示重启 gateway
### 方法 2: 本地技能目录(开发调试)
如果技能已在 `~/.openclaw/workspace/skills/memory-archiver/`:
```
node ~/.openclaw/workspace/skills/memory-archiver/scripts/install.js
```
### 验证安装
```
openclaw hooks list
# 应看到 memory-archiver-hook (✓ ready)
openclaw cron list
# 应看到 3 个记忆相关任务
```
---
## 📝 记忆写入规范
### 三类记忆标签
| 标签 | 说明 | 例子 |
|------|------|------|
| `[episodic]` | 事件/经历 | "用户今天完成了模板重设计" |
| `[semantic]` | 知识/事实 | "用户喜欢 Tailwind CSS" |
| `[procedural]` | 流程/方法 | "部署步骤:1. 构建 2. 上传 3. 重启" |
### 记录原则
**✅ 应该记录**:
- 关键决策和教训
- 新发现的有价值内容
- 技术栈使用经验
- 工作习惯调整
- 用户偏好
**❌ 不应该记录**:
- ❌ **重复的上下文** — 已有记录的内容不再重复
- ❌ **毫无意义的日常** — 无事发生就不记
- ❌ **重复的任务进度提示** — 避免刷屏
- ❌ **私密细节** — 保护隐私
- ❌ **短期易变想法** — 临时念头不持久
**核心判断**: 这条信息在未来回顾时是否有价值?
---
## 🔍 记忆搜索
### 自动加载(OpenClaw 启动时)
每次 OpenClaw 启动时,通过 `agent:bootstrap` Hook 自动加载记忆到缓存,无需手动触发。
加载内容:今日 + 昨日 + 最近 3 天 daily + MEMORY.md + 最近 weekly
### 交互式搜索
```
node ~/.openclaw/workspace/skills/memory-archiver/scripts/memory-search.js "搜索内容"
```
**在对话中使用**:
- 直接提问,Hook 会自动检测并搜索相关记忆
### 使用 grep 搜索
```bash
# 搜索所有记忆文件
grep -ri "CSS" ~/.openclaw/workspace/memory/
# 带上下文显示
grep -riC 3 "CSS" ~/.openclaw/workspace/memory/daily/*.md
```
---
## 📊 版本历史
| 版本 | 日期 | 变更 |
|------|------|------|
| **10.1** | 2026-04-14 | **全部 .sh 转为 .js**: install/memory-search/memory-loader/memory-refresh/memory-dedup/dream-lock 全部纯 JS |
| **10.0** | 2026-04-14 | **整合 auto-memory-extract + session-notes**: 统一 Hook (message:received) 包含搜索/提取/会话笔记三模块 |
| **9.0** | 2026-04-14 | **完全整合 auto-dream**: dream-consolidate.js(闸门+索引+编排)、dream-lock.js(文件锁)、prompts/consolidation.md 全部迁入,auto-dream 技能移除 |
| **8.0** | 2026-04-13 | **整合 auto-dream 去重功能**: memory-dedup.js 现在负责 MEMORY.md 段落去重,auto-dream 仅负责触发 |
| **7.0** | 2026-03-23 | **Hook 安装自动化**: `skill.json` 添加 `postinstall` 脚本,`clawhub install` 自动部署 hook + cron |
| 6.0 | 2026-03-20 | 整合 Auto Memory Search Hook: 将独立 Hook 合并到技能内 |
| 5.0 | 2026-03-20 | **三层精简架构**: 移除 monthly/yearly 层,保留 daily/weekly/long-term |
| 4.0 | 2026-03-20 | **精简版**: 移除向量搜索依赖,简化架构 |
| 3.0 | 2026-03-19 | 向量增强版:整合 Qdrant + Transformers.js |
| 2.0 | 2026-03-19 | 五层时间架构 (hourly/daily/weekly/monthly/yearly) |
| 1.0 | 2026-03-11 | 初始版本 |
---
## 🛠️ 维护命令
```bash
# 检查记忆文件总量
du -sh ~/.openclaw/workspace/memory/
# 查看每日记忆文件
ls -lh ~/.openclaw/workspace/memory/daily/
# 搜索记忆内容
grep -ri "CSS" ~/.openclaw/workspace/memory/
```
---
*文档最后更新:2026-04-14*
FILE:_meta.json
{
"ownerId": "kn709ac4cea693g2hg2p4p3sgn82vk9z",
"slug": "memory-archiver",
"version": "10.3.0",
"publishedAt": 1774238428997
}
FILE:hooks/HOOK.md
---
name: memory-archiver-hook
description: "统一记忆 Hook:自动搜索 + 自动提取 + 会话笔记追踪"
metadata:
{
"openclaw":
{
"emoji": "🧠",
"events": ["message:received"],
"install": [{ "id": "local", "kind": "local", "label": "Local workspace hook" }],
},
}
---
# Memory Archiver 统一 Hook
**整合功能**(原 3 个独立 Hook):
1. 自动记忆搜索(auto-memory-search)
2. 自动记忆提取(auto-memory-extract)
3. 会话笔记追踪(session-notes)
## 触发方式
- **事件**: `message:received`
- **优先级**: 单 Hook 统一处理,避免重复触发
## 模块 1: 自动记忆搜索
当用户消息包含以下类型时,自动搜索相关记忆并注入上下文:
| 类型 | 触发关键词 |
|------|-----------|
| 疑问 | 怎么, 如何, 为什么, what, how, why |
| 修复 | bug, 错误, 修复, fix, error |
| 规范 | 规范, 规则, 标准, spec, rule |
| 特征 | 特征, 特点, feature |
| 配置 | 配置, 设置, 安装, config, setup |
| 命令 | 命令, 脚本, 用法, command |
| 技术 | css, html, php, javascript, tailwind, vite |
## 模块 2: 自动记忆提取
从每条用户消息中自动提取持久记忆,分类存储到:
- `memory/auto/user/` — 用户偏好、角色、目标
- `memory/auto/feedback/` — 纠正、工作模式调整
- `memory/auto/project/` — 项目上下文、环境、工作流
- `memory/auto/reference/` — 参考资料、命令、解决方案
## 模块 3: 会话笔记追踪
- 自动初始化会话笔记
- 每 10 条消息更新一次
- 笔记写入 `memory/sessions/`
- 会话结束后可归档
## 文件结构
```
skills/memory-archiver/
├── hooks/
│ ├── handler.js # 统一 Hook 处理器
│ └── HOOK.md # 本文件
├── scripts/
│ ├── auto-memory-search.js # 记忆搜索脚本
│ ├── memory-extract.js # 记忆提取脚本
│ ├── memory-classify.js # 记忆分类脚本
│ ├── memory-dedup-extract.js # 记忆去重脚本
│ └── session-tracker.js # 会话追踪脚本
└── ...
```
## 禁用
```bash
openclaw hooks disable memory-archiver-hook
```
FILE:hooks/bootstrap-loader/HOOK.md
---
name: memory-bootstrap-load
description: "OpenClaw 启动时自动加载记忆到缓存"
metadata:
{
"openclaw":
{
"emoji": "🧠",
"events": ["agent:bootstrap"],
"install": [{ "id": "local", "kind": "local", "label": "Local workspace hook" }],
}
}
---
# Memory Bootstrap Load Hook
OpenClaw 每次启动时自动加载记忆到 `SESSION-STATE.md` 缓存。
## 功能
1. 监听 `agent:bootstrap` 事件(OpenClaw 启动时触发)
2. 运行 `memory-loader.js` 加载记忆
3. 加载内容:今日 + 昨日 + 最近 3 天 daily + MEMORY.md + 最近 weekly
## 触发时机
- OpenClaw Gateway 启动时自动触发
FILE:hooks/bootstrap-loader/handler.js
/**
* Memory Bootstrap Load Hook
*
* 事件:agent:bootstrap
* 功能:OpenClaw 启动时自动加载记忆到 SESSION-STATE.md 缓存
*/
import { exec } from 'child_process';
import { promisify } from 'util';
import * as path from 'path';
import * as fs from 'fs';
const execAsync = promisify(exec);
const handler = async (event) => {
console.log('[MemoryBootstrapLoad] Event received:', event.type, event.action);
// 只处理 bootstrap 事件
if (event.type !== 'agent' || event.action !== 'bootstrap') {
return;
}
const homeDir = process.env.HOME || '/root';
const workspaceDir = path.join(homeDir, '.openclaw', 'workspace');
const scriptPath = path.join(workspaceDir, 'skills', 'memory-archiver', 'scripts', 'memory-loader.js');
// 检查脚本是否存在
if (!fs.existsSync(scriptPath)) {
console.log('[MemoryBootstrapLoad] 脚本不存在:', scriptPath);
return;
}
try {
console.log('[MemoryBootstrapLoad] 开始加载记忆到缓存...');
const { stdout, stderr } = await execAsync(`bash "scriptPath"`);
console.log('[MemoryBootstrapLoad] 加载完成:', stdout.trim());
if (stderr) {
console.log('[MemoryBootstrapLoad] 警告:', stderr.trim());
}
} catch (error) {
console.log('[MemoryBootstrapLoad] 加载失败:', error.message);
}
};
export default handler;
FILE:hooks/handler.js
/**
* Memory Archiver - 统一 Hook Handler
*
* 事件:message:received
* 功能:
* 1. 自动记忆搜索 - 检测消息类型,搜索相关记忆并注入上下文
* 2. 自动记忆提取 - 从对话中提取持久记忆,分类存储
* 3. 会话笔记追踪 - 维护当前活跃会话笔记
*/
import { execFile, execFileSync, execSync } from 'child_process';
import { promisify } from 'util';
import * as path from 'path';
import * as fs from 'fs';
const execFileAsync = promisify(execFile);
const HOME_DIR = process.env.HOME || '/root';
const WORKSPACE = path.join(HOME_DIR, '.openclaw', 'workspace');
const SKILLS_DIR = path.join(WORKSPACE, 'skills', 'memory-archiver');
const MEMORY_DIR = path.join(WORKSPACE, 'memory');
// ========== 模块 1: 自动记忆搜索 ==========
/**
* 检测消息类型
*/
function detectMessageType(message) {
const lowerMsg = message.toLowerCase();
if (/怎么|如何|为什么|什么|哪里|何时|谁|哪个|whether|what|how|why|where|when|who/.test(lowerMsg)) {
return '疑问';
}
if (/修复|bug|错误|问题|故障|解决|repair|fix|error|issue|debug/.test(lowerMsg)) {
return '修复';
}
if (/规范|规则|标准|要求|必须|应该|spec|standard|rule|require/.test(lowerMsg)) {
return '规范';
}
if (/特征|特点|特性|特色|feature|characteristic/.test(lowerMsg)) {
return '特征';
}
if (/配置|设置|安装|部署|环境|config|setup|install|deploy|environment/.test(lowerMsg)) {
return '配置';
}
if (/命令|指令|脚本|用法|example|command|script|usage/.test(lowerMsg)) {
return '命令';
}
if (/\b(css|html|php|javascript|node|npm|tailwind|vite|thinkphp)\b/i.test(lowerMsg)) {
return '技术';
}
return null;
}
/**
* 提取关键词
*/
function extractKeywords(message) {
const enKeywords = message.match(/[A-Za-z0-9_]{2,}/g) || [];
const cnKeywords = message
.split(/[\s,,.。!?!?;;::]+/)
.filter(w => w.length >= 2 && /[\u4e00-\u9fa5]/.test(w));
const all = [...new Set([...enKeywords, ...cnKeywords])];
return all.slice(0, 5);
}
/**
* 搜索记忆
*
* 安全修复: 使用 execFile 传参数数组,不经过 shell,
* 彻底杜绝命令注入(backtick、$()、;、|、&& 等)
*/
async function searchMemory(message) {
const scriptPath = path.join(SKILLS_DIR, 'scripts', 'auto-memory-search.js');
if (!fs.existsSync(scriptPath)) {
console.log('[MemorySearch] 脚本不存在');
return null;
}
try {
const { stdout } = await execFileAsync('node', [scriptPath, message]);
return stdout.trim() || null;
} catch (error) {
console.log('[MemorySearch] 搜索失败:', error.message);
return null;
}
}
// ========== 模块 2: 自动记忆提取 ==========
/**
* 记忆分类(基于关键词规则)
*/
function classifyMemory(content) {
const rules = {
user: {
keywords: ['偏好', '喜欢', '习惯', '角色', '目标', '负责', '职位', '身份', '称呼', '语言', '时区'],
patterns: [/我是.+/, /我叫.+/, /我的.+是/, /我希望.+/, /我偏好.+/],
weight: 1
},
feedback: {
keywords: ['不对', '错误', '纠正', '不是', '应该', '不要', '禁止', '避免', '改进', '修复', '教训'],
patterns: [/不要.+/, /禁止.+/, /避免.+/, /应该.+/, /不应该.+/],
weight: 1.2
},
project: {
keywords: ['项目', '任务', '需求', '架构', '方案', '设计', '部署', '工作区', '路由', '数据库'],
patterns: [/我们.+做/, /这个.+是/, /需要.+/, /计划.+/, /决定.+/],
weight: 1
},
reference: {
keywords: ['命令', '脚本', '配置', '参数', '格式', '用法', 'api', '接口', '协议'],
patterns: [/用.+.实现/, /通过.+方式/, /使用.+方法/],
weight: 0.8
}
};
let bestType = 'reference';
let bestScore = 0;
for (const [type, rule] of Object.entries(rules)) {
let score = 0;
for (const kw of rule.keywords) {
if (content.includes(kw)) score += rule.weight;
}
for (const pattern of rule.patterns) {
if (pattern.test(content)) score += rule.weight * 1.5;
}
if (score > bestScore) {
bestScore = score;
bestType = type;
}
}
return bestScore > 0 ? bestType : 'reference';
}
/**
* 提取记忆(后台执行,不阻塞 hook)
*
* 安全修复: 使用 execFile 传参数数组,不经过 shell
*/
function extractMemoryAsync(message) {
const scriptPath = path.join(SKILLS_DIR, 'scripts', 'memory-extract.js');
if (!fs.existsSync(scriptPath)) return;
try {
execFile('node', [scriptPath, message], { stdio: 'ignore' });
} catch (e) {
console.log('[MemoryExtract] 提取失败:', e.message);
}
}
// ========== 模块 3: 会话笔记追踪 ==========
const SESSIONS_DIR = path.join(MEMORY_DIR, 'sessions');
const CURRENT_FILE = path.join(SESSIONS_DIR, '.current-session.json');
const ARCHIVE_DIR = path.join(SESSIONS_DIR, 'archive');
const COUNTER_FILE = path.join(SESSIONS_DIR, '.message-counter');
function getMessageCount() {
try {
return parseInt(fs.readFileSync(COUNTER_FILE, 'utf8').trim()) || 0;
} catch (e) {
return 0;
}
}
function incrementMessageCount() {
const count = getMessageCount() + 1;
fs.writeFileSync(COUNTER_FILE, count.toString());
return count;
}
function hasActiveSession() {
return fs.existsSync(CURRENT_FILE);
}
function generateSessionId() {
return Date.now().toString(36) + Math.random().toString(36).substring(2, 8);
}
function initSession(topic = '未命名会话') {
if (!fs.existsSync(SESSIONS_DIR)) {
fs.mkdirSync(SESSIONS_DIR, { recursive: true });
}
if (!fs.existsSync(ARCHIVE_DIR)) {
fs.mkdirSync(ARCHIVE_DIR, { recursive: true });
}
const sessionId = generateSessionId();
const now = new Date().toISOString();
const filepath = path.join(SESSIONS_DIR, `sessionId.md`);
const meta = {
sessionId,
topic,
startedAt: now,
endedAt: null,
filepath,
messageCount: 0,
lastUpdatedAt: now,
archived: false
};
const content = `---
session_id: sessionId
topic: topic
started: now
ended:
message_count: 0
archived: false
---
# 会话笔记: topic
## 关键决策
## 待办事项
## 重要发现
## 用户偏好
`;
fs.writeFileSync(filepath, content, 'utf8');
fs.writeFileSync(CURRENT_FILE, JSON.stringify(meta, null, 2));
console.log(`[SessionNotes] 初始化会话: sessionId (topic)`);
return meta;
}
function updateSessionNote(content) {
if (!hasActiveSession()) {
initSession('自动会话 ' + new Date().toLocaleString('zh-CN'));
return updateSessionNote(content);
}
const meta = JSON.parse(fs.readFileSync(CURRENT_FILE, 'utf8'));
if (!fs.existsSync(meta.filepath)) return;
let fileContent = fs.readFileSync(meta.filepath, 'utf8');
fileContent += `\ncontent\n`;
meta.messageCount = (meta.messageCount || 0) + 1;
meta.lastUpdatedAt = new Date().toISOString();
fs.writeFileSync(CURRENT_FILE, JSON.stringify(meta, null, 2));
fileContent = fileContent.replace(/message_count: \d+/, `message_count: meta.messageCount`);
fs.writeFileSync(meta.filepath, fileContent, 'utf8');
}
// ========== 统一主 Hook ==========
const handler = async (event) => {
console.log(`[MemoryArchiver] Hook triggered: type=event.type, action=event.action`);
if (event.type !== 'message' || event.action !== 'received') {
return;
}
const userMessage = event.context?.content || event.message?.text || '';
if (!userMessage) {
console.log('[MemoryArchiver] No message content');
return;
}
// 跳过系统消息/cron/心跳
if (userMessage.startsWith('System:') ||
userMessage.includes('记忆及时写入检查') ||
userMessage.includes('工作进度检查') ||
userMessage.includes('错误监控检查') ||
userMessage.includes('全量工作监控') ||
userMessage.includes('Token 使用自动记录') ||
userMessage.includes('HEARTBEAT')) {
console.log('[MemoryArchiver] Skipping system/cron/heartbeat');
return;
}
// === 模块 1: 自动记忆搜索 ===
const msgType = detectMessageType(userMessage);
if (msgType) {
console.log(`[MemorySearch] 检测到消息类型: msgType`);
const searchResults = await searchMemory(userMessage);
if (searchResults) {
event.messages.push(`📚 相关记忆:\nsearchResults`);
console.log('[MemorySearch] 记忆已注入');
}
}
// === 模块 2: 自动记忆提取(后台) ===
console.log('[MemoryExtract] 触发后台记忆提取');
extractMemoryAsync(userMessage);
// === 模块 3: 会话笔记追踪 ===
if (!hasActiveSession()) {
// 从消息内容猜测主题
const topic = userMessage.substring(0, 50).replace(/[\n\r]/g, ' ');
initSession(topic);
}
const count = incrementMessageCount();
if (count % 10 === 0) {
// 每 10 条消息更新一次笔记
updateSessionNote(`- [new Date().toLocaleTimeString('zh-CN')] 消息 #count`);
console.log(`[SessionNotes] 更新笔记 (第 count 条)`);
}
};
export default handler;
FILE:prompts/consolidation.md
# Dream: Memory Consolidation
你正在执行一次 dream — 对记忆文件的反思性整理。将近期所学综合为结构化、组织良好的记忆,让未来会话能快速定位。
记忆目录: `{{memoryRoot}}`
如果目录不存在,先创建它。
会话记录: `{{transcriptDir}}`(大型 JSONL 文件 — 用 grep 精确搜索,不要读整个文件)
---
## 阶段一 — 定向(Orient)
- `ls` 记忆目录,查看已有内容
- 阅读入口文件了解当前索引结构
- 浏览现有主题文件,确保改进而非创建副本
- 如果存在 `logs/` 或 `sessions/` 子目录,查看近期条目
## 阶段二 — 收集近期信号(Gather)
寻找值得保留的新信息。来源按优先级排序:
1. **日志文件**(`logs/YYYY/MM/YYYY-MM-DD.md`)— 如果存在,这些是只追加流
2. **漂移的记忆** — 与当前代码库矛盾的事实
3. **会话搜索** — 仅在需要特定上下文时精确搜索:
`grep -rn "关键词" {{transcriptDir}}/ --include="*.jsonl" | tail -50`
**不要**穷尽地读会话记录。只搜索你已经怀疑重要的内容。
## 阶段三 — 巩固(Consolidate)
对每个值得记忆的内容,在记忆目录顶层写入或更新文件。
重点关注:
- **合并新信号到现有文件** 而非创建近似副本
- **转换相对日期为绝对日期**("昨天"、"上周" → 具体日期)
- **删除矛盾事实** — 如果今天的调查推翻了旧记忆,直接在源头修正
## 阶段四 — 修剪与索引(Prune & Index)
更新入口索引文件,保持在合理行数以内(~200行)且 ~25KB 以内。
每条一行,~150字符:`- [标题](file.md) — 一句话说明`
- 移除指向过时/错误/被替代记忆的指针
- 降级冗长条目:如果索引行超过 ~200 字符,缩短它,把详情移到主题文件
- 添加新的重要记忆指针
- 解决矛盾 — 如果两个文件不一致,修正错误的那个
---
返回一份简要总结:你巩固、更新或修剪了什么。
如果记忆已经很紧凑、无需变更,直接说明。
## 额外约束
**工具限制**:Bash 仅限只读命令(`ls`、`find`、`grep`、`cat`、`stat`、`wc`、`head`、`tail` 及类似命令)。任何写入、重定向到文件或修改状态的操作都将被拒绝。
会话列表({{sessionCount}} 个):
{{sessionList}}
FILE:scripts/README.md
# 记忆管理脚本
## 📚 功能说明
三层记忆架构:**每天 → 每周 → 长期** + **记忆巩固**(原 auto-dream)
## 🔧 脚本列表
### 1. memory-loader.sh - 加载记忆
加载三层记忆到 `SESSION-STATE.md` 缓存文件。
```bash
bash scripts/memory-loader.sh
```
**加载内容**:
- 今日记忆 (memory/daily/YYYY-MM-DD.md)
- 昨日记忆
- 最近 3 天 daily 记忆(前 50 行)
- 长期记忆 (MEMORY.md, 前 150 行)
- 最近 weekly 记忆(前 80 行)
**输出**: `~/.openclaw/workspace/SESSION-STATE.md`
---
### 2. memory-search.sh - 搜索记忆
在加载的记忆缓存中搜索关键词。
```bash
bash scripts/memory-search.sh "搜索关键词"
```
**例子**:
```bash
# 搜索 CSS 相关内容
bash scripts/memory-search.sh "CSS"
# 搜索模板开发
bash scripts/memory-search.sh "模板"
# 搜索 Ollama 配置
bash scripts/memory-search.sh "Ollama"
```
**输出**: 带上下文的搜索结果(前后 3 行)
---
### 3. memory-refresh.sh - 智能刷新记忆缓存
**智能检查**:只在记忆文件最近 10 分钟内更新过时才刷新。
```bash
bash ~/.openclaw/workspace/skills/memory-archiver/scripts/memory-refresh.sh
```
**工作流程**:
1. 检查今日记忆文件是否存在
2. 检查文件最后修改时间
3. 如果最近 10 分钟内更新过 → 重新加载全部记忆
4. 否则 → 跳过刷新(避免无效刷新)
---
### 4. memory-dedup.sh - 长期记忆自动去重
检测并清理 MEMORY.md 中的**重复内容、无意义日常、重复任务进度**。
```bash
bash scripts/memory-dedup.sh
```
**工作流程**:
1. 备份 MEMORY.md 到 `MEMORY.md.backup.YYYYMMDD-HHMMSS`
2. 检测并删除:
- ❌ 重复的上下文
- ❌ 毫无意义的日常(无事发生)
- ❌ 重复的任务进度提示
3. 保留唯一有价值内容
---
### 5. memory-aging.js - 记忆老化与数量限制检查
检查并清理过期和超限的记忆文件。
```bash
node scripts/memory-aging.js # 执行清理
node scripts/memory-aging.js --dry-run # 只报告不删除
```
**功能**:
- 老化检查:标记超过 30 天的记忆文件
- 数量限制:每类型最多 50 条,超出清理最旧的
- 类型:user / feedback / project / reference
---
### 6. dream-consolidate.js - 记忆巩固主程序(原 auto-dream)
定期整理、合并、去重、老化记忆的总控脚本。
```bash
node scripts/dream-consolidate.js # 检查闸门后执行巩固
node scripts/dream-consolidate.js --force # 强制执行
node scripts/dream-consolidate.js --status # 查看巩固状态
```
**闸门条件**:
- 时间闸门:距离上次巩固 ≥ 24 小时
- 会话闸门:≥ 5 个新会话
**巩固流程**:
1. 老化检查 → 标记超过 30 天的记忆
2. 数量限制 → 每类型最多 50 条
3. 索引更新 → 重建 MEMORY.md 底部记忆索引
4. 去重 → 调用 memory-dedup.sh
**文件锁**: 使用 `dream-lock.sh` 防止并发运行
---
### 7. dream-lock.sh - 巩固文件锁
防止多个巩固任务同时运行。
```bash
bash scripts/dream-lock.sh /path/to/memory acquire # 获取锁
bash scripts/dream-lock.sh /path/to/memory release # 释放锁
bash scripts/dream-lock.sh /path/to/memory check # 检查锁状态
bash scripts/dream-lock.sh /path/to/memory force # 强制获取锁
```
**特性**:
- PID 检测:同一进程重复获取返回已有锁
- mtime 超时保护:30 分钟后自动释放过期锁
---
### 8. auto-memory-search.sh - 自动触发搜索
被 Hook 调用,自动检测用户消息并搜索记忆。
```bash
bash scripts/auto-memory-search.sh "用户消息"
```
---
## 💬 在对话中使用
### 加载记忆
```
加载记忆
```
→ 运行 `memory-loader.sh`
### 搜索记忆
```
搜索记忆:CSS 框架
```
→ 运行 `memory-search.sh "CSS 框架"`
### 查看巩固状态
```
巩固状态
```
→ 运行 `dream-consolidate.js --status`
---
## 📊 文件结构
```
~/.openclaw/workspace/
├── skills/memory-archiver/scripts/
│ ├── memory-loader.sh # 加载记忆
│ ├── memory-search.sh # 搜索记忆
│ ├── memory-refresh.sh # 智能刷新
│ ├── memory-dedup.sh # 自动去重
│ ├── memory-aging.js # 老化与数量限制
│ ├── dream-consolidate.js # 记忆巩固主程序
│ ├── dream-lock.sh # 文件锁
│ ├── auto-memory-search.sh # 自动触发搜索
│ └── README.md # 本文件
├── SESSION-STATE.md # 记忆缓存(自动生成)
├── MEMORY.md # 长期记忆
└── memory/
├── daily/ # 每日记忆
├── weekly/ # 每周记忆
├── auto/ # 自动分类 (user/feedback/project/reference)
├── .dream-state.json # 巩固状态
└── .dream.lock # 巩固文件锁
```
---
## 🔄 自动化
### Cron 任务
| 任务 | 频率 | 脚本 |
|------|------|------|
| 记忆巩固 | 每 6 小时 | dream-consolidate.js |
| 记忆及时写入 | 每 10 分钟 | (agent 执行) |
| 记忆归档-Daily | 每天 23:00 | (agent 执行) |
| 记忆总结-Weekly | 每周日 22:00 | (agent 执行) |
---
*最后更新:2026-04-14*
FILE:scripts/auto-memory-search.js
#!/usr/bin/env node
/**
* Auto Memory Search - 自动触发记忆搜索(多维度增强版)
* 用法:node auto-memory-search.js "用户消息"
* 功能:检测消息类型,多维度搜索相关记忆
*
* 原为 auto-memory-search.sh,已转为纯 JS
*/
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const WORKSPACE_DIR = path.join(process.env.HOME, '.openclaw', 'workspace');
const SESSION_STATE = path.join(WORKSPACE_DIR, 'SESSION-STATE.md');
const MEMORY_DIR = path.join(WORKSPACE_DIR, 'memory');
const MEMORY_MD = path.join(WORKSPACE_DIR, 'MEMORY.md');
// 检测消息类型
function detectType(msg) {
const lowerMsg = msg.toLowerCase();
if (/怎么|如何|为什么|什么|哪里|何时|谁|哪个|能否|可以吗|行不行|what|how|why|where|when|who/.test(lowerMsg)) return '疑问';
if (/修复|bug|错误|问题|故障|解决|repair|fix|error|issue|debug/.test(lowerMsg)) return '修复';
if (/规范|规则|标准|要求|必须|应该|spec|standard|rule|require/.test(lowerMsg)) return '规范';
if (/特征|特点|特性|特色|feature|characteristic/.test(lowerMsg)) return '特征';
if (/配置|设置|安装|部署|环境|config|setup|install|deploy|environment/.test(lowerMsg)) return '配置';
if (/命令|指令|脚本|用法|example|command|script|usage/.test(lowerMsg)) return '命令';
if (/\b(css|html|php|javascript|node|npm|tailwind|vite|thinkphp)\b/i.test(lowerMsg)) return '技术';
return '普通';
}
// 提取搜索关键词
function extractKeywords(msg) {
const enKeywords = (msg.match(/[A-Za-z0-9_]{2,}/g) || []).slice(0, 5);
const cnWords = msg
.split(/[\s,,.。!?!?;;::]+/)
.filter(w => w.length >= 2 && /[\u4e00-\u9fa5]/.test(w))
.slice(0, 5);
return [...new Set([...enKeywords, ...cnWords])];
}
// 关键词搜索(带上下文)
function searchByKeyword(keyword, file) {
try {
if (!fs.existsSync(file)) return '';
const content = fs.readFileSync(file, 'utf8');
const lines = content.split('\n');
const results = [];
for (let i = 0; i < lines.length; i++) {
if (lines[i].toLowerCase().includes(keyword.toLowerCase())) {
const start = Math.max(0, i - 2);
const end = Math.min(lines.length - 1, i + 2);
results.push(`📌 "keyword" (行 i + 1):\n---\nlines.slice(start, end + 1).join('\n')\n---`);
}
}
return results.join('\n\n');
} catch { return ''; }
}
// 类型标签搜索
function searchByType(msgType, file) {
try {
if (!fs.existsSync(file)) return '';
const content = fs.readFileSync(file, 'utf8');
const lines = content.split('\n');
let typeTag = 'episodic';
if (['修复', '问题', '错误'].includes(msgType)) typeTag = 'procedural';
else if (['配置', '命令', '技术'].includes(msgType)) typeTag = 'semantic';
const results = [];
for (let i = 0; i < lines.length; i++) {
if (lines[i].includes(`[typeTag]`)) {
const start = Math.max(0, i - 3);
const end = Math.min(lines.length - 1, i + 3);
results.push(`📌 类型标签 [typeTag] 相关:\n---\nlines.slice(start, end + 1).join('\n')\n---`);
}
}
return results.join('\n\n');
} catch { return ''; }
}
// 时间维度搜索
function searchByTime(keywords) {
let results = [];
// 今日记忆
const today = new Date().toISOString().slice(0, 10);
const todayFile = path.join(MEMORY_DIR, 'daily', `today.md`);
if (fs.existsSync(todayFile)) {
const content = fs.readFileSync(todayFile, 'utf8');
for (const kw of keywords) {
const lines = content.split('\n');
for (let i = 0; i < lines.length; i++) {
if (lines[i].toLowerCase().includes(kw.toLowerCase())) {
const start = Math.max(0, i - 2);
const end = Math.min(lines.length - 1, i + 2);
results.push(`📍 今日记忆 "kw":\n---\nlines.slice(start, end + 1).join('\n')\n---`);
break;
}
}
}
}
// 昨日记忆
const yesterday = new Date(Date.now() - 86400000).toISOString().slice(0, 10);
const yesterdayFile = path.join(MEMORY_DIR, 'daily', `yesterday.md`);
if (fs.existsSync(yesterdayFile)) {
const content = fs.readFileSync(yesterdayFile, 'utf8');
for (const kw of keywords) {
const lines = content.split('\n');
for (let i = 0; i < lines.length; i++) {
if (lines[i].toLowerCase().includes(kw.toLowerCase())) {
const start = Math.max(0, i - 2);
const end = Math.min(lines.length - 1, i + 2);
results.push(`📍 昨日记忆 "kw":\n---\nlines.slice(start, end + 1).join('\n')\n---`);
break;
}
}
}
}
// 长期记忆
if (fs.existsSync(MEMORY_MD)) {
const content = fs.readFileSync(MEMORY_MD, 'utf8');
for (const kw of keywords) {
const lines = content.split('\n');
for (let i = 0; i < lines.length; i++) {
if (lines[i].toLowerCase().includes(kw.toLowerCase())) {
const start = Math.max(0, i - 2);
const end = Math.min(lines.length - 1, i + 2);
results.push(`📍 长期记忆 "kw":\n---\nlines.slice(start, end + 1).join('\n')\n---`);
break;
}
}
}
}
return results.join('\n\n');
}
// 组合搜索
function searchCombined(keywords, file) {
try {
if (!fs.existsSync(file) || keywords.length === 0) return '';
const content = fs.readFileSync(file, 'utf8');
const lines = content.split('\n');
const pattern = new RegExp(keywords.map(k => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|'), 'i');
const results = [];
for (let i = 0; i < lines.length; i++) {
if (pattern.test(lines[i])) {
const start = Math.max(0, i - 3);
const end = Math.min(lines.length - 1, i + 3);
results.push(lines.slice(start, end + 1).join('\n'));
}
}
return results.length > 0 ? `📌 组合搜索结果:\n---\nresults.slice(0, 5).join('\n---\n')\n---` : '';
} catch { return ''; }
}
// 主逻辑
function main() {
const userMessage = process.argv.slice(2).join(' ');
if (!userMessage) {
console.log('用法:node auto-memory-search.js "用户消息"');
process.exit(1);
}
// 检查缓存是否存在,不存在则加载记忆
if (!fs.existsSync(SESSION_STATE)) {
const loaderPath = path.join(__dirname, 'memory-loader.js');
if (fs.existsSync(loaderPath)) {
try { execSync(`node "loaderPath"`, { stdio: 'pipe' }); } catch {}
}
}
const msgType = detectType(userMessage);
if (msgType === '普通') {
console.log('ℹ️ 普通消息,不自动搜索');
process.exit(0);
}
console.log(`📋 消息类型:msgType`);
const keywords = extractKeywords(userMessage);
if (keywords.length === 0) {
console.log('⚠️ 未提取到关键词');
process.exit(0);
}
console.log(`🔑 关键词:keywords.join(', ')`);
// 多维度搜索
let allResults = [];
// 维度 1: 关键词搜索
for (const kw of keywords) {
const result = searchByKeyword(kw, SESSION_STATE);
if (result) allResults.push(result);
}
// 维度 2: 类型标签搜索
const typeResult = searchByType(msgType, SESSION_STATE);
if (typeResult) allResults.push(typeResult);
// 维度 3: 时间维度搜索
const timeResult = searchByTime(keywords);
if (timeResult) allResults.push(timeResult);
// 维度 4: 组合搜索
const combinedResult = searchCombined(keywords, SESSION_STATE);
if (combinedResult) allResults.push(combinedResult);
// 输出结果
if (allResults.length === 0) {
console.log('📭 未找到相关记忆');
} else {
console.log('\n✅ 多维度记忆搜索完成\n');
console.log(allResults.join('\n\n'));
console.log('\n---\n以上记忆供参考,根据情况决定是否引用');
}
}
main();
FILE:scripts/dream-consolidate.js
#!/usr/bin/env node
/**
* Dream Consolidation - 记忆巩固脚本(原 auto-dream 合并)
* 定期整理、合并、去重、老化记忆
*
* 用法:
* node dream-consolidate.js # 检查闸门条件,通过后执行巩固
* node dream-consolidate.js --force # 强制执行巩固
* node dream-consolidate.js --status # 查看巩固状态
*/
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const WORKSPACE = process.env.HOME + '/.openclaw/workspace';
const MEMORY_DIR = WORKSPACE + '/memory';
const AUTO_DIR = MEMORY_DIR + '/auto';
const STATE_FILE = MEMORY_DIR + '/.dream-state.json';
const LOCK_FILE = MEMORY_DIR + '/.dream.lock';
const MEMORY_INDEX = WORKSPACE + '/MEMORY.md';
const MEMORY_DEDUP = path.join(__dirname, 'memory-dedup.js');
const MEMORY_AGING = path.join(__dirname, 'memory-aging.js');
const MEMORY_TYPES = ['user', 'feedback', 'project', 'reference'];
// 闸门配置
const CONFIG = {
minHours: 24, // 最小时间间隔(小时)
minSessions: 5, // 最小新会话数
maxAge: 30, // 记忆老化天数
maxPerType: 50, // 每类型最大记忆数
};
// 读取巩固状态
function readState() {
try {
return JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'));
} catch (e) {
return {
lastConsolidatedAt: null,
lastSessionCount: 0,
totalConsolidations: 0
};
}
}
// 保存巩固状态
function saveState(state) {
fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
}
// 获取会话数(通过会话文件数估算)
function getSessionCount() {
const sessionsDir = MEMORY_DIR + '/sessions';
if (!fs.existsSync(sessionsDir)) return 0;
return fs.readdirSync(sessionsDir).filter(f => f.endsWith('.md')).length;
}
// 使用 Shell 锁脚本(支持 PID 复用保护 + mtime 防并发)
const LOCK_SCRIPT = path.join(__dirname, 'dream-lock.js');
function tryLock() {
try {
const result = execSync(`bash "LOCK_SCRIPT" "MEMORY_DIR" acquire`, {
encoding: 'utf8',
timeout: 5000
}).trim();
console.log(`[锁] result`);
return result.startsWith('ACQUIRED') || result.startsWith('FORCED');
} catch (e) {
console.log(`[锁] 获取失败: e.stdout?.trim() || e.message`);
return false;
}
}
function releaseLock() {
try {
const result = execSync(`bash "LOCK_SCRIPT" "MEMORY_DIR" release`, {
encoding: 'utf8',
timeout: 5000
}).trim();
console.log(`[锁] result`);
} catch (e) {
console.log(`[锁] 释放异常: e.message`);
}
}
function checkLock() {
try {
const result = execSync(`bash "LOCK_SCRIPT" "MEMORY_DIR" check`, {
encoding: 'utf8',
timeout: 5000
}).trim();
return result.startsWith('FREE');
} catch (e) {
return false;
}
}
// 检查闸门条件
function checkGates() {
const state = readState();
const now = Date.now();
// 时间闸门
if (state.lastConsolidatedAt) {
const hoursSince = (now - state.lastConsolidatedAt) / (1000 * 60 * 60);
if (hoursSince < CONFIG.minHours) {
console.log(`[闸门] 时间未达:hoursSince.toFixed(1)h < CONFIG.minHoursh`);
return false;
}
console.log(`[闸门] 时间通过:hoursSince.toFixed(1)h ≥ CONFIG.minHoursh`);
} else {
console.log('[闸门] 时间通过:首次巩固');
}
// 会话闸门
const sessionCount = getSessionCount();
const newSessions = sessionCount - state.lastSessionCount;
if (newSessions < CONFIG.minSessions && state.lastConsolidatedAt) {
console.log(`[闸门] 会话未达:newSessions < CONFIG.minSessions`);
return false;
}
console.log(`[闸门] 会话通过:newSessions ≥ CONFIG.minSessions`);
return true;
}
// 老化检查
function ageCheck() {
const now = Date.now();
const agedFiles = [];
for (const type of MEMORY_TYPES) {
const dir = path.join(AUTO_DIR, type);
if (!fs.existsSync(dir)) continue;
const files = fs.readdirSync(dir).filter(f => f.endsWith('.md') && !f.startsWith('.'));
for (const file of files) {
const filepath = path.join(dir, file);
const stat = fs.statSync(filepath);
const ageDays = (now - stat.mtimeMs) / (1000 * 60 * 60 * 24);
if (ageDays > CONFIG.maxAge) {
agedFiles.push({ type, file, ageDays, filepath });
}
}
}
return agedFiles;
}
// 数量限制检查
function countCheck() {
const excessFiles = [];
for (const type of MEMORY_TYPES) {
const dir = path.join(AUTO_DIR, type);
if (!fs.existsSync(dir)) continue;
const files = fs.readdirSync(dir).filter(f => f.endsWith('.md') && !f.startsWith('.'));
if (files.length > CONFIG.maxPerType) {
// 按修改时间排序,删除最旧的
const sorted = files
.map(f => ({ file: f, mtime: fs.statSync(path.join(dir, f)).mtimeMs }))
.sort((a, b) => a.mtime - b.mtime);
for (const item of sorted.slice(0, files.length - CONFIG.maxPerType)) {
excessFiles.push({ type, file: item.file, filepath: path.join(dir, item.file) });
}
}
}
return excessFiles;
}
// 执行巩固
function consolidate() {
console.log('\n=== 开始记忆巩固 ===\n');
const report = {
timestamp: new Date().toISOString(),
aged: [],
excess: [],
cleaned: [],
indexUpdated: false
};
// 1. 老化检查
console.log('[1/3] 老化检查...');
const aged = ageCheck();
if (aged.length > 0) {
console.log(` 发现 aged.length 条过期记忆(>CONFIG.maxAge天)`);
for (const item of aged) {
console.log(` 标记: item.type/item.file (item.ageDays.toFixed(0)天)`);
}
report.aged = aged;
} else {
console.log(' 无过期记忆');
}
// 2. 数量限制
console.log('[2/3] 数量限制检查...');
const excess = countCheck();
if (excess.length > 0) {
console.log(` 发现 excess.length 条超出限制的记忆`);
for (const item of excess) {
console.log(` 清理: item.type/item.file`);
// 不直接删除,移动到 archive
const archiveDir = path.join(AUTO_DIR, '.archive', item.type);
fs.mkdirSync(archiveDir, { recursive: true });
fs.renameSync(item.filepath, path.join(archiveDir, item.file));
report.cleaned.push(item);
}
} else {
console.log(' 未超出限制');
}
report.excess = excess;
// 3. 更新索引
console.log('[3/3] 更新索引...');
updateIndex();
report.indexUpdated = true;
// 4. 调用 memory-archiver 去重(合并功能)
console.log('[4/4] 记忆去重...');
try {
if (fs.existsSync(MEMORY_DEDUP)) {
const dedupOutput = execSync(`bash "MEMORY_DEDUP"`, {
encoding: 'utf8',
timeout: 30000
}).trim();
console.log(` dedupOutput.split('\n').slice(-1).join('')`);
report.dedupDone = true;
} else {
console.log(' 跳过: memory-dedup.js 不存在');
report.dedupDone = false;
}
} catch (e) {
console.log(` 去重异常: e.message`);
report.dedupDone = false;
}
// 保存状态
const state = readState();
state.lastConsolidatedAt = Date.now();
state.lastSessionCount = getSessionCount();
state.totalConsolidations = (state.totalConsolidations || 0) + 1;
saveState(state);
console.log('\n=== 巩固完成 ===');
console.log(` 老化: aged.length 条`);
console.log(` 清理: report.cleaned.length 条`);
console.log(` 总巩固次数: state.totalConsolidations`);
return report;
}
// 更新索引(简化版)
function updateIndex() {
if (!fs.existsSync(MEMORY_INDEX)) {
fs.writeFileSync(MEMORY_INDEX, '# MEMORY.md - 长期记忆\n\n> 精选知识与模式\n\n---\n', 'utf8');
return;
}
// 读取现有内容
let content = fs.readFileSync(MEMORY_INDEX, 'utf8');
// 查找并替换索引部分
const marker = '## 记忆索引';
let indexStart = content.indexOf(marker);
if (indexStart === -1) {
content += '\n## 记忆索引\n\n_由 Auto-Memory 自动维护_\n\n';
indexStart = content.indexOf(marker);
}
// 重建索引
const entries = [];
for (const type of MEMORY_TYPES) {
const dir = path.join(AUTO_DIR, type);
if (!fs.existsSync(dir)) continue;
const files = fs.readdirSync(dir).filter(f => f.endsWith('.md') && !f.startsWith('.'));
if (files.length === 0) continue;
entries.push(`### type\n`);
for (const file of files.slice(0, 10)) {
const filepath = path.join(dir, file);
const fileContent = fs.readFileSync(filepath, 'utf8');
const titleMatch = fileContent.match(/^# (.+)$/m);
const title = titleMatch ? titleMatch[1] : file;
entries.push(`- title \`memory/auto/type/file\``);
}
entries.push('');
}
const newIndex = entries.join('\n');
const nextSection = content.indexOf('\n## ', indexStart + 1);
const beforeIndex = content.substring(0, indexStart);
const afterIndex = nextSection !== -1 ? content.substring(nextSection) : '';
let newContent = beforeIndex + '## 记忆索引\n\n_由 Auto-Memory 自动维护_\n\n' + newIndex + afterIndex;
// 大小限制
const lines = newContent.split('\n');
if (lines.length > 200) newContent = lines.slice(0, 200).join('\n');
if (Buffer.byteLength(newContent, 'utf8') > 25000) {
newContent = newContent.substring(0, 25000);
}
fs.writeFileSync(MEMORY_INDEX, newContent, 'utf8');
}
// 主函数
function main() {
const args = process.argv.slice(2);
if (args.includes('--status')) {
const state = readState();
const now = Date.now();
const hoursSince = state.lastConsolidatedAt ? ((now - state.lastConsolidatedAt) / (1000 * 60 * 60)).toFixed(1) : '从未';
const sessions = getSessionCount();
const newSessions = state.lastConsolidatedAt ? sessions - state.lastSessionCount : sessions;
console.log('=== 巩固状态 ===');
console.log(`上次巩固: hoursSinceh 前`);
console.log(`总会话数: sessions`);
console.log(`新会话数: newSessions (阈值: CONFIG.minSessions)`);
console.log(`总巩固次数: state.totalConsolidations || 0`);
console.log(`\n闸门状态:`);
console.log(` 时间: '❌ 未达'`);
console.log(` 会话: '❌ 未达'`);
return;
}
const force = args.includes('--force');
if (force) {
console.log('[强制] 跳过闸门检查');
if (tryLock()) {
try {
consolidate();
} finally {
releaseLock();
}
}
return;
}
// 正常模式:检查闸门
if (checkGates()) {
if (tryLock()) {
try {
consolidate();
} finally {
releaseLock();
}
}
} else {
console.log('\n[跳过] 闸门条件未满足');
}
}
main();
FILE:scripts/dream-lock.js
#!/usr/bin/env node
/**
* Dream Lock - 防止并发巩固的锁机制
* 用法:
* node dream-lock.js acquire
* node dream-lock.js release
* node dream-lock.js check
* node dream-lock.js force
*
* 原为 dream-lock.sh,已转为纯 JS
*/
const fs = require('fs');
const path = require('path');
const LOCK_DIR = process.argv[3] || path.join(process.env.HOME, '.openclaw', 'workspace', 'memory');
const LOCK_FILE = path.join(LOCK_DIR, '.dream.lock');
const HOLDER_STALE_MS = 30 * 60 * 1000; // 30 分钟
function nowMs() {
return Date.now();
}
function acquire() {
fs.mkdirSync(LOCK_DIR, { recursive: true });
if (fs.existsSync(LOCK_FILE)) {
const stat = fs.statSync(LOCK_FILE);
const ageMs = nowMs() - stat.mtimeMs;
if (ageMs < HOLDER_STALE_MS) {
try {
const holderPid = parseInt(fs.readFileSync(LOCK_FILE, 'utf8').trim());
if (holderPid && process.pid !== holderPid) {
// 检查进程是否存活
try { process.kill(holderPid, 0); } catch {
// 进程已退出,可以 reclaim
fs.writeFileSync(LOCK_FILE, process.pid.toString());
console.log(`ACQUIRED (stale PID holderPid)`);
return 0;
}
console.log(`LOCKED: PID holderPid 执行中 (ageMsms)`);
return 1;
}
} catch {}
} else {
console.log(`STALE: lock expired (ageMsms), reclaim`);
}
}
fs.writeFileSync(LOCK_FILE, process.pid.toString());
console.log('ACQUIRED');
return 0;
}
function release() {
if (!fs.existsSync(LOCK_FILE)) {
console.log('SKIP: no lock file');
return 0;
}
try {
const holderPid = parseInt(fs.readFileSync(LOCK_FILE, 'utf8').trim());
if (!holderPid || holderPid === process.pid) {
fs.unlinkSync(LOCK_FILE);
console.log('RELEASED');
return 0;
} else {
console.log(`SKIP: held by holderPid`);
return 1;
}
} catch {
console.log('SKIP: could not read lock file');
return 1;
}
}
function check() {
if (!fs.existsSync(LOCK_FILE)) {
console.log('FREE');
return 0;
}
const stat = fs.statSync(LOCK_FILE);
const ageMs = nowMs() - stat.mtimeMs;
try {
const holderPid = parseInt(fs.readFileSync(LOCK_FILE, 'utf8').trim());
if (holderPid && ageMs < HOLDER_STALE_MS) {
try { process.kill(holderPid, 0); } catch {
console.log(`FREE (stale: holderPid, ageMsms)`);
return 0;
}
console.log(`BUSY: PID holderPid (ageMsms)`);
return 1;
}
console.log(`FREE (stale: holderPid, ageMsms)`);
return 0;
} catch {
console.log('FREE');
return 0;
}
}
function force() {
fs.mkdirSync(LOCK_DIR, { recursive: true });
fs.writeFileSync(LOCK_FILE, process.pid.toString());
console.log('FORCED');
return 0;
}
// 主函数
const cmd = process.argv[2] || 'check';
const commands = { acquire, release, check, force };
if (!commands[cmd]) {
console.log(`用法: node dream-lock.js {acquire|release|check|force}`);
process.exit(1);
}
process.exit(commands[cmd]());
FILE:scripts/install.js
#!/usr/bin/env node
/**
* Memory Archiver Skill - 安装脚本
* 用法:node scripts/install.js
*
* 功能:
* 1. 创建记忆目录结构
* 2. 安装 memory-archiver-hook hook(自动注册到 OpenClaw)
* 3. 自动添加 cron 任务
*
* 原为 install.sh,已转为纯 JS
*/
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const WORKSPACE = path.join(process.env.HOME, '.openclaw', 'workspace');
const SKILL_DIR = path.join(WORKSPACE, 'skills', 'memory-archiver');
const HOOKS_DIR = path.join(WORKSPACE, 'hooks', 'memory-archiver-hook');
const MEMORY_DAILY = path.join(WORKSPACE, 'memory', 'daily');
const MEMORY_WEEKLY = path.join(WORKSPACE, 'memory', 'weekly');
console.log('🔧 开始安装 Memory Archiver Skill...\n');
// ===== Step 1: 创建记忆目录 =====
console.log('📁 Step 1: 创建记忆目录...');
fs.mkdirSync(MEMORY_DAILY, { recursive: true });
fs.mkdirSync(MEMORY_WEEKLY, { recursive: true });
console.log(' ✅ memory/daily/');
console.log(' ✅ memory/weekly/\n');
// ===== Step 2: 安装 hook =====
console.log('🔍 Step 2: 安装 memory-archiver-hook...');
const hookSource = path.join(SKILL_DIR, 'hooks');
const handlerJs = path.join(hookSource, 'handler.js');
const hookMd = path.join(hookSource, 'HOOK.md');
if (!fs.existsSync(handlerJs) || !fs.existsSync(hookMd)) {
console.log(` ❌ Hook 源文件不存在:hookSource/`);
process.exit(1);
}
fs.mkdirSync(HOOKS_DIR, { recursive: true });
fs.copyFileSync(handlerJs, path.join(HOOKS_DIR, 'handler.js'));
fs.copyFileSync(hookMd, path.join(HOOKS_DIR, 'HOOK.md'));
console.log(` ✅ Hook 文件已复制到 HOOKS_DIR/\n`);
// 尝试通过 openclaw hooks install 正式注册
try {
const listOutput = execSync('openclaw hooks list 2>/dev/null', { encoding: 'utf8' });
if (listOutput.includes('memory-archiver-hook')) {
console.log(' ✅ Hook 已注册(跳过重复安装)');
} else {
console.log(' 📦 正在注册 hook...');
const oldHookDir = path.join(process.env.HOME, '.openclaw', 'hooks', 'memory-archiver-hook');
try { fs.rmSync(oldHookDir, { recursive: true, force: true }); } catch {}
try {
execSync(`openclaw hooks install --link "HOOKS_DIR"`, { stdio: 'pipe' });
console.log(' ✅ Hook 已通过 openclaw hooks install 注册');
} catch {
console.log(' ⚠️ 自动注册失败,请手动执行:');
console.log(` openclaw hooks install --link HOOKS_DIR`);
}
}
console.log('\n 💡 重启 gateway 生效:');
console.log(' systemctl --user restart openclaw-gateway.service');
} catch {
console.log(' ⚠️ 未检测到 openclaw CLI,请手动注册 hook:');
console.log(` openclaw hooks install --link HOOKS_DIR`);
}
console.log('');
// ===== Step 3: 自动添加 Cron 任务 =====
console.log('⏰ Step 3: 添加 Cron 任务...\n');
try {
const cronList = execSync('openclaw cron list 2>/dev/null', { encoding: 'utf8' });
const memoryCronCount = (cronList.match(/记忆/g) || []).length;
if (memoryCronCount >= 3) {
console.log(' ✅ Cron 任务已存在(跳过添加)');
} else {
const cronJobs = [
{
name: '记忆及时写入',
schedule: '{"kind":"every","everyMs":600000}',
payload: '{"kind":"systemEvent","text":"📝 记忆及时写入检查(静默模式)"}'
},
{
name: '记忆归档 - Daily',
schedule: '{"kind":"cron","expr":"0 23 * * *","tz":"Asia/Shanghai"}',
payload: '{"kind":"systemEvent","text":"🌙 每日记忆归档时间(23:00)!"}'
},
{
name: '记忆总结 - Weekly',
schedule: '{"kind":"cron","expr":"0 22 * * 0","tz":"Asia/Shanghai"}',
payload: '{"kind":"systemEvent","text":"📅 每周记忆总结时间(周日 22:00)!"}'
}
];
for (const job of cronJobs) {
console.log(` 📅 正在添加 job.name...`);
try {
execSync(
`openclaw cron add --name "job.name" --schedule 'job.schedule' --payload 'job.payload' --session-target main --delivery '{"mode":"none"}'`,
{ stdio: 'pipe' }
);
console.log(' ✅ 已添加');
} catch {
console.log(' ⚠️ 添加失败(可能已存在)');
}
}
}
} catch {
console.log(' ⚠️ 无法检查 Cron 任务');
}
console.log('');
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.log('✅ Memory Archiver 安装完成!');
console.log('');
console.log(' 📂 记忆目录:~/.openclaw/workspace/memory/');
console.log(' 🔍 Hook: ~/.openclaw/workspace/hooks/memory-archiver-hook/');
console.log(' 📖 文档:skills/memory-archiver/SKILL.md');
console.log(' ⏰ Cron: 3 个定时任务已配置');
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
FILE:scripts/memory-aging.js
#!/usr/bin/env node
/**
* Memory Aging & Cleanup - 记忆老化检查与清理
*
* 功能(从 auto-dream 合并而来):
* 1. 老化检查:标记超过 30 天的记忆文件
* 2. 数量限制:每类型最多 50 条,超出清理最旧的
* 3. 报告生成:输出清理统计
*
* 用法:
* node memory-aging.js # 检查并清理
* node memory-aging.js --dry-run # 只报告不删除
*/
const fs = require('fs');
const path = require('path');
const WORKSPACE = process.env.HOME + '/.openclaw/workspace';
const AUTO_DIR = path.join(WORKSPACE, 'memory/auto');
const MEMORY_TYPES = ['user', 'feedback', 'project', 'reference'];
// 配置
const CONFIG = {
maxAge: 30, // 记忆老化天数
maxPerType: 50, // 每类型最大记忆数
};
// ============================================================================
// 老化检查
// ============================================================================
function ageCheck() {
const now = Date.now();
const agedFiles = [];
for (const type of MEMORY_TYPES) {
const dir = path.join(AUTO_DIR, type);
if (!fs.existsSync(dir)) continue;
const files = fs.readdirSync(dir).filter(f => f.endsWith('.md') && !f.startsWith('.'));
for (const file of files) {
const filepath = path.join(dir, file);
const stat = fs.statSync(filepath);
const ageDays = (now - stat.mtimeMs) / (1000 * 60 * 60 * 24);
if (ageDays > CONFIG.maxAge) {
agedFiles.push({ type, file, ageDays, filepath });
}
}
}
return agedFiles;
}
// ============================================================================
// 数量限制检查
// ============================================================================
function countCheck() {
const excessFiles = [];
for (const type of MEMORY_TYPES) {
const dir = path.join(AUTO_DIR, type);
if (!fs.existsSync(dir)) continue;
const files = fs.readdirSync(dir).filter(f => f.endsWith('.md') && !f.startsWith('.'));
if (files.length > CONFIG.maxPerType) {
// 按修改时间排序,清理最旧的
const sorted = files
.map(f => ({ file: f, mtime: fs.statSync(path.join(dir, f)).mtimeMs }))
.sort((a, b) => a.mtime - b.mtime);
for (const item of sorted.slice(0, files.length - CONFIG.maxPerType)) {
excessFiles.push({ type, file: item.file, filepath: path.join(dir, item.file) });
}
}
}
return excessFiles;
}
// ============================================================================
// 执行清理
// ============================================================================
function cleanup(dryRun) {
console.log('🔍 记忆老化与清理检查');
console.log('═'.repeat(40));
const aged = ageCheck();
const excess = countCheck();
if (aged.length === 0 && excess.length === 0) {
console.log('✅ 所有记忆正常,无需清理');
return { aged: 0, excess: 0, cleaned: 0 };
}
// 统计每类型数量
console.log('\n📊 各类型记忆数量:');
for (const type of MEMORY_TYPES) {
const dir = path.join(AUTO_DIR, type);
if (!fs.existsSync(dir)) {
console.log(` type: 0`);
continue;
}
const count = fs.readdirSync(dir).filter(f => f.endsWith('.md') && !f.startsWith('.')).length;
const status = count > CONFIG.maxPerType ? '⚠️ 超出' : '✅';
console.log(` type: count status`);
}
// 老化文件
if (aged.length > 0) {
console.log(`\n⏰ 老化文件 (aged.length 条,>CONFIG.maxAge天):`);
for (const item of aged) {
console.log(` • item.type/item.file (item.ageDays.toFixed(0)天)`);
}
}
// 超出文件
if (excess.length > 0) {
console.log(`\n📦 超出限制文件 (excess.length 条):`);
for (const item of excess) {
console.log(` • item.type/item.file`);
}
}
if (dryRun) {
console.log('\n[DRY RUN] 未执行删除');
return { aged: aged.length, excess: excess.length, cleaned: 0 };
}
// 执行删除
let cleaned = 0;
const allToDelete = [...aged, ...excess];
const uniquePaths = new Set(allToDelete.map(f => f.filepath));
for (const filepath of uniquePaths) {
try {
fs.unlinkSync(filepath);
cleaned++;
} catch (e) {
console.log(` ❌ 删除失败: filepath - e.message`);
}
}
console.log(`\n✅ 已清理 cleaned 个文件`);
return { aged: aged.length, excess: excess.length, cleaned };
}
// ============================================================================
// 主入口
// ============================================================================
function main() {
const dryRun = process.argv.includes('--dry-run');
const result = cleanup(dryRun);
// 静默模式:无问题时不输出
if (result.aged === 0 && result.excess === 0) {
console.log('NO_REPLY');
}
}
main();
FILE:scripts/memory-classify.js
#!/usr/bin/env node
/**
* 记忆分类脚本
* 根据内容自动判断记忆类型
*
* 用法: node classify.js "<内容>"
* 输出: user | feedback | project | reference
*/
// 分类关键词规则
const RULES = {
user: {
keywords: ['偏好', '喜欢', '习惯', '角色', '目标', '负责', '职位', '身份', '称呼', '语言', '时区'],
patterns: [/我是.+/, /我叫.+/, /我的.+是/, /我希望.+/, /我偏好.+/],
weight: 1
},
feedback: {
keywords: ['不对', '错误', '纠正', '不是', '应该', '不要', '禁止', '避免', '改进', '修复', '教训'],
patterns: [/不要.+/, /禁止.+/, /避免.+/, /应该.+/, /不应该.+/],
weight: 1.2
},
project: {
keywords: ['项目', '部署', '环境', '配置', '路径', '地址', '端口', '域名', '工作区', '目录', '版本'],
patterns: [/部署到.+/, /路径是.+/, /地址是.+/, /使用.+版本/],
weight: 1
},
reference: {
keywords: ['命令', '用法', '示例', '参考', '解决', '方案', '命令', '脚本', 'API', '接口', '模板'],
patterns: [/用法:?/, /示例:?/, /命令:?/, /步骤:?/, /\$\s*\w+/],
weight: 0.8
}
};
function classify(content) {
const scores = {};
for (const [type, rule] of Object.entries(RULES)) {
let score = 0;
// 关键词匹配
for (const keyword of rule.keywords) {
if (content.includes(keyword)) {
score += rule.weight;
}
}
// 正则匹配
for (const pattern of rule.patterns) {
if (pattern.test(content)) {
score += rule.weight * 1.5;
}
}
scores[type] = score;
}
// 返回得分最高的类型
let bestType = 'project';
let bestScore = 0;
for (const [type, score] of Object.entries(scores)) {
if (score > bestScore) {
bestScore = score;
bestType = type;
}
}
return { type: bestType, scores, confidence: bestScore > 0 ? Math.min(bestScore / 5, 1) : 0.3 };
}
function main() {
const content = process.argv.slice(2).join(' ');
if (!content) {
console.log('用法: node classify.js "<内容>"');
process.exit(1);
}
const result = classify(content);
console.log(JSON.stringify(result, null, 2));
}
main();
FILE:scripts/memory-dedup-extract.js
#!/usr/bin/env node
/**
* 记忆去重脚本
* 检查内容是否已存在,支持模糊匹配
*
* 用法: node dedup.js "<内容>" [类型]
* 输出: {"duplicate": true, "existing": "文件名"} | {"duplicate": false}
*/
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const WORKSPACE = process.env.HOME + '/.openclaw/workspace';
const MEMORY_DIR = WORKSPACE + '/memory/auto';
// 精确去重:MD5 hash
function exactHash(content) {
return crypto.createHash('md5').update(content.trim()).digest('hex');
}
// 模糊去重:提取关键词后比较
function fuzzyMatch(content, type) {
// 提取关键词(中文词组 + 英文单词)
const keywords = content.match(/[\u4e00-\u9fff]{2,}|[a-zA-Z]{3,}/g) || [];
if (keywords.length < 3) return null;
const dir = path.join(MEMORY_DIR, type);
if (!fs.existsSync(dir)) return null;
const files = fs.readdirSync(dir).filter(f => f.endsWith('.md') && !f.startsWith('.'));
for (const file of files) {
const filepath = path.join(dir, file);
const fileContent = fs.readFileSync(filepath, 'utf8');
// 计算关键词重叠率
const fileKeywords = fileContent.match(/[\u4e00-\u9fff]{2,}|[a-zA-Z]{3,}/g) || [];
const overlap = keywords.filter(k => fileKeywords.includes(k));
const overlapRate = overlap.length / Math.max(keywords.length, fileKeywords.length);
if (overlapRate > 0.7) {
// 进一步检查核心内容相似度
const titleMatch = fileContent.match(/^# (.+)$/m);
return { file, title: titleMatch ? titleMatch[1] : file, overlapRate };
}
}
return null;
}
function check(content, type = null) {
const hash = exactHash(content);
// 检查所有类型或指定类型
const types = type ? [type] : ['user', 'feedback', 'project', 'reference'];
for (const t of types) {
const dir = path.join(MEMORY_DIR, t);
const hashFile = path.join(dir, '.hashes.json');
if (fs.existsSync(hashFile)) {
try {
const hashes = JSON.parse(fs.readFileSync(hashFile, 'utf8'));
if (hashes[hash]) {
return { duplicate: true, type: t, file: hashes[hash], method: 'exact' };
}
} catch (e) {}
}
// 模糊匹配
const fuzzy = fuzzyMatch(content, t);
if (fuzzy) {
return { duplicate: true, type: t, file: fuzzy.file, title: fuzzy.title, method: 'fuzzy', overlapRate: fuzzy.overlapRate };
}
}
return { duplicate: false };
}
function main() {
const args = process.argv.slice(2);
if (args.length < 1) {
console.log('用法: node dedup.js "<内容>" [类型]');
process.exit(1);
}
const content = args[0];
const type = args[1] || null;
const result = check(content, type);
console.log(JSON.stringify(result, null, 2));
}
main();
FILE:scripts/memory-dedup.js
#!/usr/bin/env node
/**
* Memory Dedup - 长期记忆自动去重
* 用法:node memory-dedup.js
* 功能:检测并清理重复内容、无意义日常、重复任务进度
*
* 原为 memory-dedup.sh,已转为纯 JS
*/
const fs = require('fs');
const path = require('path');
const WORKSPACE_DIR = path.join(process.env.HOME, '.openclaw', 'workspace');
const MEMORY_FILE = path.join(WORKSPACE_DIR, 'MEMORY.md');
function main() {
console.log('🔍 检查长期记忆重复...');
if (!fs.existsSync(MEMORY_FILE)) {
console.log('⚠️ MEMORY.md 不存在,跳过');
process.exit(0);
}
const now = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
const backupFile = path.join(WORKSPACE_DIR, `MEMORY.md.backup.now`);
// 备份
fs.copyFileSync(MEMORY_FILE, backupFile);
console.log(`📦 已备份到: backupFile`);
let content = fs.readFileSync(MEMORY_FILE, 'utf8');
const lines = content.split('\n');
// 1. 去除完全重复的行
const uniqueLines = [];
const seen = new Set();
let dupCount = 0;
for (const line of lines) {
const trimmed = line.trim();
if (trimmed && !/^(#{1,6}|-{3,}|\*{3,}|___|\s*$)/.test(trimmed)) {
if (seen.has(trimmed)) {
dupCount++;
continue;
}
seen.add(trimmed);
}
uniqueLines.push(line);
}
// 2. 去除无意义的日常记录(过于简短的行)
const meaningfulLines = [];
let trivialCount = 0;
const trivialPatterns = [
/^[-*]\s*今日正常$/,
/^[-*]\s*无异常$/,
/^[-*]\s*一切正常$/,
/^[-*]\s*无待办$/,
/^[-*]\s*全部完成$/,
];
for (const line of uniqueLines) {
const trimmed = line.trim();
const isTrivial = trivialPatterns.some(p => p.test(trimmed));
if (isTrivial) {
trivialCount++;
continue;
}
meaningfulLines.push(line);
}
const newContent = meaningfulLines.join('\n');
if (dupCount > 0 || trivialCount > 0) {
fs.writeFileSync(MEMORY_FILE, newContent, 'utf8');
console.log(`✅ 去重完成:删除 dupCount 条重复行,trivialCount 条无意义行`);
console.log(` 文件大小: (content.length / 1024).toFixed(1)KB → (newContent.length / 1024).toFixed(1)KB`);
} else {
console.log('✅ 无重复内容,无需清理');
// 删除多余备份
fs.unlinkSync(backupFile);
}
}
main();
FILE:scripts/memory-extract.js
#!/usr/bin/env node
/**
* Auto-Memory 提取脚本
* 从对话内容中提取持久记忆,分类存储
*
* 用法:
* node extract.js "<内容>" [--type <type>] [--tags <tag1,tag2>]
* node extract.js --scan "<目录>" 扫描目录中的文件提取记忆
*/
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const WORKSPACE = process.env.HOME + '/.openclaw/workspace';
const MEMORY_DIR = WORKSPACE + '/memory/auto';
const MEMORY_INDEX = WORKSPACE + '/MEMORY.md';
const MAX_INDEX_LINES = 200;
const MAX_INDEX_BYTES = 25000;
const MEMORY_TYPES = ['user', 'feedback', 'project', 'reference'];
// 确保目录存在
function ensureDirs() {
for (const type of MEMORY_TYPES) {
const dir = path.join(MEMORY_DIR, type);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
}
// 生成记忆文件名
function generateFilename(type, title) {
const slug = title
.toLowerCase()
.replace(/[^a-z0-9\u4e00-\u9fff]+/g, '_')
.replace(/^_|_$/g, '')
.substring(0, 50);
const hash = crypto.randomBytes(4).toString('hex');
return `slug_hash.md`;
}
// 计算内容 hash(用于去重)
function contentHash(content) {
return crypto.createHash('md5').update(content.trim()).digest('hex');
}
// 去重检查
function isDuplicate(type, content) {
const dir = path.join(MEMORY_DIR, type);
if (!fs.existsSync(dir)) return false;
const hash = contentHash(content);
const hashFile = path.join(dir, '.hashes.json');
let hashes = {};
if (fs.existsSync(hashFile)) {
try {
hashes = JSON.parse(fs.readFileSync(hashFile, 'utf8'));
} catch (e) {
hashes = {};
}
}
if (hashes[hash]) {
console.log(`[去重] 发现重复内容,跳过: hash`);
return true;
}
return false;
}
// 记录 hash
function recordHash(type, content, filename) {
const dir = path.join(MEMORY_DIR, type);
const hashFile = path.join(dir, '.hashes.json');
let hashes = {};
if (fs.existsSync(hashFile)) {
try {
hashes = JSON.parse(fs.readFileSync(hashFile, 'utf8'));
} catch (e) {}
}
hashes[contentHash(content)] = filename;
fs.writeFileSync(hashFile, JSON.stringify(hashes, null, 2));
}
// 写入记忆文件
function writeMemory(type, title, content, tags = []) {
ensureDirs();
if (isDuplicate(type, content)) {
return { success: false, reason: 'duplicate' };
}
const filename = generateFilename(type, title);
const filepath = path.join(MEMORY_DIR, type, filename);
const now = new Date().toISOString().split('T')[0];
const memoryContent = `---
type: type
created: now
tags: [tags.join(', ')]
---
# title
content
`;
fs.writeFileSync(filepath, memoryContent, 'utf8');
recordHash(type, content, filename);
console.log(`[记忆写入] type/filename`);
return { success: true, type, filename, filepath };
}
// 更新 MEMORY.md 索引
function updateIndex() {
if (!fs.existsSync(MEMORY_INDEX)) {
fs.writeFileSync(MEMORY_INDEX, '# MEMORY.md - 长期记忆\n\n> 精选知识与模式\n\n---\n', 'utf8');
return;
}
let content = fs.readFileSync(MEMORY_INDEX, 'utf8');
// 查找索引部分
const indexMarker = '## 记忆索引';
let indexStart = content.indexOf(indexMarker);
if (indexStart === -1) {
// 添加索引标记
content += '\n## 记忆索引\n\n_由 Auto-Memory 自动维护_\n\n';
indexStart = content.indexOf('## 记忆索引');
}
// 重建索引
const entries = [];
for (const type of MEMORY_TYPES) {
const dir = path.join(MEMORY_DIR, type);
if (!fs.existsSync(dir)) continue;
const files = fs.readdirSync(dir).filter(f => f.endsWith('.md') && !f.startsWith('.'));
if (files.length === 0) continue;
entries.push(`### type\n`);
for (const file of files.slice(0, 10)) { // 每类型最多 10 条
const filepath = path.join(dir, file);
const fileContent = fs.readFileSync(filepath, 'utf8');
const titleMatch = fileContent.match(/^# (.+)$/m);
const tagsMatch = fileContent.match(/^tags: \[(.+?)\]$/m);
const title = titleMatch ? titleMatch[1] : file;
const tags = tagsMatch ? tagsMatch[1] : '';
entries.push(`- [title](memory/auto/type/file) ''`);
}
entries.push('');
}
const newIndex = entries.join('\n');
// 替换旧索引
const nextSection = content.indexOf('\n## ', indexStart + 1);
const beforeIndex = content.substring(0, indexStart);
const afterIndex = nextSection !== -1 ? content.substring(nextSection) : '';
content = beforeIndex + '## 记忆索引\n\n_由 Auto-Memory 自动维护_\n\n' + newIndex + afterIndex;
// 检查大小限制
const lines = content.split('\n');
if (lines.length > MAX_INDEX_LINES) {
content = lines.slice(0, MAX_INDEX_LINES).join('\n');
}
if (Buffer.byteLength(content, 'utf8') > MAX_INDEX_BYTES) {
content = content.substring(0, MAX_INDEX_BYTES);
}
fs.writeFileSync(MEMORY_INDEX, content, 'utf8');
console.log(`[索引更新] MEMORY.md 已更新`);
}
// 主函数
function main() {
const args = process.argv.slice(2);
if (args.length === 0) {
console.log('用法: node extract.js "<内容>" [--type <type>] [--tags <tag1,tag2>]');
console.log(' node extract.js --update-index');
process.exit(1);
}
if (args[0] === '--update-index') {
updateIndex();
return;
}
// 解析参数
let content = '';
let type = 'project'; // 默认类型
let tags = [];
let title = '';
for (let i = 0; i < args.length; i++) {
if (args[i] === '--type' && i + 1 < args.length) {
type = args[++i];
if (!MEMORY_TYPES.includes(type)) {
console.error(`错误: 未知类型 "type",可选: MEMORY_TYPES.join(', ')`);
process.exit(1);
}
} else if (args[i] === '--tags' && i + 1 < args.length) {
tags = args[++i].split(',').map(t => t.trim());
} else if (args[i] === '--title' && i + 1 < args.length) {
title = args[++i];
} else {
content += args[i] + ' ';
}
}
content = content.trim();
if (!content) {
console.error('错误: 内容为空');
process.exit(1);
}
if (!title) {
title = content.substring(0, 30) + (content.length > 30 ? '...' : '');
}
const result = writeMemory(type, title, content, tags);
if (result.success) {
updateIndex();
console.log(`✅ 记忆已保存: result.filepath`);
} else {
console.log(`⏭️ 跳过: result.reason`);
}
}
main();
FILE:scripts/memory-loader.js
#!/usr/bin/env node
/**
* Memory Loader - 加载三层记忆到 SESSION-STATE.md
* 用法:node memory-loader.js
*
* 原为 memory-loader.sh,已转为纯 JS
*/
const fs = require('fs');
const path = require('path');
const WORKSPACE_DIR = path.join(process.env.HOME, '.openclaw', 'workspace');
const MEMORY_DIR = path.join(WORKSPACE_DIR, 'memory');
const SESSION_STATE = path.join(WORKSPACE_DIR, 'SESSION-STATE.md');
function main() {
const sessionStateHeader = `# SESSION-STATE.md - 会话记忆缓存
> 自动加载的三层记忆内容(用于快速搜索)
> 生成时间: new Date().toISOString()
`;
let content = sessionStateHeader;
// 加载长期记忆
const memoryMd = path.join(WORKSPACE_DIR, 'MEMORY.md');
if (fs.existsSync(memoryMd)) {
const memContent = fs.readFileSync(memoryMd, 'utf8');
// 只加载 ## 之后的正文内容,跳过头部元数据
const bodyStart = memContent.indexOf('\n## ');
if (bodyStart !== -1) {
content += '## 长期记忆 (MEMORY.md)\n\n';
content += memContent.slice(bodyStart);
content += '\n\n';
}
}
// 加载今日记忆
const today = new Date().toISOString().slice(0, 10);
const todayFile = path.join(MEMORY_DIR, 'daily', `today.md`);
if (fs.existsSync(todayFile)) {
content += '## 今日记忆\n\n';
content += fs.readFileSync(todayFile, 'utf8');
content += '\n\n';
}
// 加载昨日记忆
const yesterday = new Date(Date.now() - 86400000).toISOString().slice(0, 10);
const yesterdayFile = path.join(MEMORY_DIR, 'daily', `yesterday.md`);
if (fs.existsSync(yesterdayFile)) {
content += '## 昨日记忆\n\n';
content += fs.readFileSync(yesterdayFile, 'utf8');
content += '\n\n';
}
// 加载最近 weekly 记忆
const weeklyDir = path.join(MEMORY_DIR, 'weekly');
if (fs.existsSync(weeklyDir)) {
const weeklyFiles = fs.readdirSync(weeklyDir)
.filter(f => f.endsWith('.md'))
.sort()
.reverse()
.slice(0, 2); // 最近 2 个 weekly
for (const file of weeklyFiles) {
content += `## 周记忆 (file.replace('.md', ''))\n\n`;
content += fs.readFileSync(path.join(weeklyDir, file), 'utf8');
content += '\n\n';
}
}
// 写入 SESSION-STATE.md
fs.writeFileSync(SESSION_STATE, content, 'utf8');
console.log(`📚 记忆已加载到 SESSION-STATE.md`);
}
main();
FILE:scripts/memory-refresh.js
#!/usr/bin/env node
/**
* Memory Refresh - 智能刷新记忆缓存(检查后刷新)
* 用法:node memory-refresh.js
*
* 原为 memory-refresh.sh,已转为纯 JS
*/
const fs = require('fs');
const path = require('path');
const WORKSPACE_DIR = path.join(process.env.HOME, '.openclaw', 'workspace');
const DAILY_FILE = path.join(WORKSPACE_DIR, 'memory', 'daily', `new Date().toISOString().slice(0, 10).md`);
const SESSION_STATE = path.join(WORKSPACE_DIR, 'SESSION-STATE.md');
function main() {
console.log('🔄 智能刷新记忆缓存...');
// 检查今日记忆文件是否存在
if (!fs.existsSync(DAILY_FILE)) {
console.log('⚠️ 今日记忆文件不存在,跳过刷新');
process.exit(0);
}
// 检查 SESSION-STATE.md 是否已包含今日记忆
if (fs.existsSync(SESSION_STATE)) {
const sessionContent = fs.readFileSync(SESSION_STATE, 'utf8');
const today = new Date().toISOString().slice(0, 10);
if (sessionContent.includes(`## 今日记忆`) && sessionContent.includes(today)) {
console.log('✅ 记忆缓存已是最新,无需刷新');
process.exit(0);
}
}
// 刷新记忆缓存
console.log('📝 刷新记忆缓存...');
const loaderPath = path.join(__dirname, 'memory-loader.js');
if (fs.existsSync(loaderPath)) {
try {
require('child_process').execSync(`node "loaderPath"`, { stdio: 'inherit' });
console.log('✅ 记忆缓存已刷新');
} catch (e) {
console.log('⚠️ 刷新失败:', e.message);
}
}
}
main();
FILE:scripts/memory-search.js
#!/usr/bin/env node
/**
* Memory Search - 在加载的记忆中搜索
* 用法:node memory-search.js "搜索关键词"
*
* 原为 memory-search.sh,已转为纯 JS
*/
const fs = require('fs');
const path = require('path');
const WORKSPACE_DIR = path.join(process.env.HOME, '.openclaw', 'workspace');
const SESSION_STATE = path.join(WORKSPACE_DIR, 'SESSION-STATE.md');
function searchInFile(query, file) {
if (!fs.existsSync(file)) return [];
const content = fs.readFileSync(file, 'utf8');
const lines = content.split('\n');
const results = [];
const lowerQuery = query.toLowerCase();
for (let i = 0; i < lines.length; i++) {
if (lines[i].toLowerCase().includes(lowerQuery)) {
const start = Math.max(0, i - 3);
const end = Math.min(lines.length - 1, i + 3);
results.push({
line: i + 1,
context: lines.slice(start, end + 1).join('\n')
});
}
}
return results;
}
function main() {
const args = process.argv.slice(2);
if (args.length === 0) {
console.log('用法:node memory-search.js "搜索关键词"');
console.log('例如:node memory-search.js "CSS 框架"');
process.exit(1);
}
const query = args.join(' ');
if (!fs.existsSync(SESSION_STATE)) {
console.log('📚 记忆缓存不存在,先加载记忆...');
const loaderPath = path.join(__dirname, 'memory-loader.js');
if (fs.existsSync(loaderPath)) {
try {
require('child_process').execSync(`node "loaderPath"`, { stdio: 'inherit' });
} catch (e) {
console.log('⚠️ 记忆加载失败');
process.exit(1);
}
}
}
console.log(`🔍 搜索: "query"`);
console.log('');
const results = searchInFile(query, SESSION_STATE);
if (results.length === 0) {
console.log('📭 未找到相关记忆');
} else {
console.log(`✅ 找到 results.length 处匹配:\n`);
results.forEach((r, i) => {
console.log(`--- 匹配 #i + 1 (行 r.line) ---`);
console.log(r.context);
console.log('');
});
}
}
main();
FILE:scripts/session-tracker.js
#!/usr/bin/env node
/**
* Session Memory 会话跟踪脚本
* 维护当前活跃会话笔记,会话结束后归档
*
* 用法:
* node tracker.js --current # 查看当前会话笔记
* node tracker.js --update "<内容>" # 更新会话笔记
* node tracker.js --archive # 归档当前会话
* node tracker.js --list # 列出所有会话
* node tracker.js --init "<主题>" # 初始化新会话
*/
const fs = require('fs');
const path = require('path');
const WORKSPACE = process.env.HOME + '/.openclaw/workspace';
const MEMORY_DIR = WORKSPACE + '/memory';
const SESSIONS_DIR = MEMORY_DIR + '/sessions';
const CURRENT_FILE = SESSIONS_DIR + '/.current-session.json';
const ARCHIVE_DIR = SESSIONS_DIR + '/archive';
// 确保目录存在
function ensureDirs() {
[SESSIONS_DIR, ARCHIVE_DIR].forEach(dir => {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
});
}
// 读取当前会话元数据
function readCurrentSession() {
try {
return JSON.parse(fs.readFileSync(CURRENT_FILE, 'utf8'));
} catch (e) {
return null;
}
}
// 保存当前会话元数据
function saveCurrentSession(meta) {
fs.writeFileSync(CURRENT_FILE, JSON.stringify(meta, null, 2));
}
// 生成会话 ID
function generateSessionId() {
return Date.now().toString(36) + Math.random().toString(36).substring(2, 8);
}
// 初始化新会话
function initSession(topic = '未命名会话') {
ensureDirs();
const sessionId = generateSessionId();
const now = new Date().toISOString();
const filepath = path.join(SESSIONS_DIR, `sessionId.md`);
const meta = {
sessionId,
topic,
startedAt: now,
endedAt: null,
filepath,
messageCount: 0,
lastUpdatedAt: now,
archived: false
};
const content = `---
session_id: sessionId
topic: topic
started: now
ended:
message_count: 0
archived: false
---
# 会话笔记: topic
## 关键决策
## 待办事项
## 重要发现
## 用户偏好
`;
fs.writeFileSync(filepath, content, 'utf8');
saveCurrentSession(meta);
console.log(`[会话初始化] ID: sessionId`);
console.log(` 主题: topic`);
console.log(` 文件: filepath`);
return meta;
}
// 更新会话笔记
function updateSession(content, section = null) {
const meta = readCurrentSession();
if (!meta) {
console.log('[提示] 无活跃会话,自动初始化...');
initSession('自动会话 ' + new Date().toLocaleString('zh-CN'));
return updateSession(content, section);
}
const filepath = meta.filepath;
if (!fs.existsSync(filepath)) {
console.log('[错误] 会话文件不存在');
return;
}
let fileContent = fs.readFileSync(filepath, 'utf8');
if (section) {
// 更新指定部分
const sectionRegex = new RegExp(`(## section\\n)([\\s\\S]*?)(?=\\n## |$)`);
if (sectionRegex.test(fileContent)) {
fileContent = fileContent.replace(sectionRegex, `$1content\n`);
} else {
fileContent += `\n## section\n\ncontent\n`;
}
} else {
// 追加到末尾
fileContent += `\ncontent\n`;
}
// 更新元数据
meta.messageCount = (meta.messageCount || 0) + 1;
meta.lastUpdatedAt = new Date().toISOString();
saveCurrentSession(meta);
// 更新文件中的 message_count
fileContent = fileContent.replace(/message_count: \d+/, `message_count: meta.messageCount`);
fs.writeFileSync(filepath, fileContent, 'utf8');
console.log(`[会话更新] +1 条笔记 (meta.messageCount 总计)`);
}
// 归档当前会话
function archiveSession() {
const meta = readCurrentSession();
if (!meta) {
console.log('[提示] 无活跃会话');
return;
}
const filepath = meta.filepath;
if (!fs.existsSync(filepath)) {
console.log('[错误] 会话文件不存在');
return;
}
// 更新文件
let content = fs.readFileSync(filepath, 'utf8');
content = content
.replace(/ended: \n/, `ended: new Date().toISOString()\n`)
.replace(/archived: false/, 'archived: true');
// 移动到归档目录
const archivePath = path.join(ARCHIVE_DIR, path.basename(filepath));
fs.writeFileSync(archivePath, content, 'utf8');
fs.unlinkSync(filepath);
// 更新元数据
meta.endedAt = new Date().toISOString();
meta.archived = true;
saveCurrentSession(meta);
// 清除当前会话
fs.unlinkSync(CURRENT_FILE);
console.log(`[会话归档] meta.sessionId`);
console.log(` 主题: meta.topic`);
console.log(` 笔记数: meta.messageCount`);
console.log(` 时长: ((Date.now() - new Date(meta.startedAt).getTime()) / 60000).toFixed(0) 分钟`);
console.log(` 归档: archivePath`);
}
// 列出所有会话
function listSessions() {
ensureDirs();
console.log('=== 活跃会话 ===');
const current = readCurrentSession();
if (current) {
const duration = ((Date.now() - new Date(current.startedAt).getTime()) / 60000).toFixed(0);
console.log(`📝 current.sessionId - current.topic`);
console.log(` 开始: new Date(current.startedAt).toLocaleString('zh-CN')`);
console.log(` 笔记: current.messageCount 条 | 时长: duration 分钟`);
} else {
console.log(' 无活跃会话');
}
console.log('\n=== 已归档会话 ===');
if (fs.existsSync(ARCHIVE_DIR)) {
const files = fs.readdirSync(ARCHIVE_DIR).filter(f => f.endsWith('.md'));
if (files.length === 0) {
console.log(' 无归档会话');
} else {
for (const file of files.slice(-10).reverse()) { // 最近 10 个
const filepath = path.join(ARCHIVE_DIR, file);
const content = fs.readFileSync(filepath, 'utf8');
const topicMatch = content.match(/^topic: (.+)$/m);
const startedMatch = content.match(/^started: (.+)$/m);
const countMatch = content.match(/^message_count: (\d+)$/m);
const topic = topicMatch ? topicMatch[1] : '未命名';
const started = startedMatch ? new Date(startedMatch[1]).toLocaleDateString('zh-CN') : '?';
const count = countMatch ? countMatch[1] : '?';
console.log(`📦 file - topic`);
console.log(` 日期: started | 笔记: count 条`);
}
}
}
}
// 查看当前会话笔记
function showCurrent() {
const meta = readCurrentSession();
if (!meta) {
console.log('无活跃会话');
return;
}
const filepath = meta.filepath;
if (!fs.existsSync(filepath)) {
console.log('会话文件不存在');
return;
}
console.log(fs.readFileSync(filepath, 'utf8'));
}
// 主函数
function main() {
const args = process.argv.slice(2);
if (args.length === 0) {
console.log('用法:');
console.log(' node tracker.js --init "<主题>" 初始化新会话');
console.log(' node tracker.js --current 查看当前笔记');
console.log(' node tracker.js --update "<内容>" 更新笔记');
console.log(' node tracker.js --archive 归档会话');
console.log(' node tracker.js --list 列出会话');
process.exit(1);
}
if (args[0] === '--init' && args[1]) {
initSession(args.slice(1).join(' '));
} else if (args[0] === '--current') {
showCurrent();
} else if (args[0] === '--update' && args[1]) {
const section = args.includes('--section') ? args[args.indexOf('--section') + 1] : null;
updateSession(args.slice(1, args.indexOf('--section') !== -1 ? args.indexOf('--section') : undefined).join(' '), section);
} else if (args[0] === '--archive') {
archiveSession();
} else if (args[0] === '--list') {
listSessions();
}
}
main();
FILE:skill.json
{
"name": "Memory Archiver",
"description": "记忆管理 - 三层架构+统一Hook+记忆巩固(全JS无.sh)",
"version": "10.3.0",
"identifier": "memory-archiver",
"author": "c32",
"category": "memory",
"tags": [
"memory",
"archive",
"daily-log",
"hook",
"consolidation",
"dream"
],
"requirements": {
"node": ">=18.0.0"
},
"scripts": {
"postinstall": "node scripts/install.js"
}
}