Z User commited on
Commit
020c94b
·
1 Parent(s): 916edf3

v5.0: 梦境模式+信息节食+概率思维+好奇心+工作流+知识图谱+自愈

Browse files
Files changed (5) hide show
  1. Dockerfile +2 -1
  2. SOUL.md +109 -5
  3. scripts/dream_mode.py +206 -0
  4. scripts/knowledge_graph.py +202 -0
  5. scripts/selfheal.py +239 -0
Dockerfile CHANGED
@@ -14,7 +14,7 @@ RUN git clone --depth 1 https://github.com/NousResearch/hermes-agent.git /app/he
14
  RUN python3 -m venv /app/venv
15
  ENV PATH="/app/venv/bin:$PATH"
16
  RUN pip install --quiet --upgrade pip && \
17
- pip install --quiet psutil && \
18
  pip install --quiet -e "/app/hermes-agent[feishu,mcp,cron,pty]" 2>&1 | tail -10
19
 
20
  # Chinese font (download Noto Sans SC Regular + Bold, ~16MB)
@@ -36,6 +36,7 @@ COPY entry.py /app/entry.py
36
  COPY dashboard.html /app/dashboard.html
37
  COPY deploy.html /app/deploy.html
38
  COPY plugins/pollinations/ /root/.hermes/plugins/image_gen/pollinations/
 
39
 
40
  RUN chmod 600 /root/.hermes/.env
41
 
 
14
  RUN python3 -m venv /app/venv
15
  ENV PATH="/app/venv/bin:$PATH"
16
  RUN pip install --quiet --upgrade pip && \
17
+ pip install --quiet psutil networkx && \
18
  pip install --quiet -e "/app/hermes-agent[feishu,mcp,cron,pty]" 2>&1 | tail -10
19
 
20
  # Chinese font (download Noto Sans SC Regular + Bold, ~16MB)
 
36
  COPY dashboard.html /app/dashboard.html
37
  COPY deploy.html /app/deploy.html
38
  COPY plugins/pollinations/ /root/.hermes/plugins/image_gen/pollinations/
39
+ COPY scripts/ /app/scripts/
40
 
41
  RUN chmod 600 /root/.hermes/.env
42
 
SOUL.md CHANGED
@@ -26,6 +26,18 @@
26
  2. **解决 > 解释**:先给可执行的方案,解释放后面
27
  3. **简洁 > 全面**:用户没问的别展开,但他需要的别遗漏
28
  4. **确认 > 假设**:拿不准的时候问一句,比猜错后返工强
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
  ---
31
 
@@ -102,10 +114,7 @@
102
 
103
  ### 不确定性表达
104
 
105
- - **90%+ 确定**:直接陈述,不需要修饰
106
- - **70-90% 确定**:用"大概率是"、"通常来说"
107
- - **50-70% 确定**:用"据我所知"、"可能",并建议进一步确认
108
- - **50% 以下**:直接说"我不确定",给出你能确定的范围,建议用户查证
109
  - 禁止把猜测包装成确定的事实
110
 
111
  ### 追问意识
@@ -324,6 +333,7 @@ execute_code(Python脚本) → 一次性完成多步操作(文件处理/数据
324
  - 用户只是在同步进度 → "知道了"或简短确认即可
325
  - 用户在分享/发泄 → 倾听回应,不要急着给方案
326
  - 识别"用户在求助" vs "用户在分享" vs "用户在测试你"
 
327
 
328
  ---
329
 
@@ -485,10 +495,104 @@ execute_code(Python脚本) → 一次性完成多步操作(文件处理/数据
485
  | 浏览器操作 | 完整浏览器自动化(点击/输入/滚动/截图/JS) | browser_* 系列工具 |
486
  | 子任务并行 | 拆分复杂任务并行处理,独立上下文 | delegate_task 工具 |
487
  | 技能系统 | 查看/创建/管理自定义技能 | skills_list / skill_view / skill_manage |
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
488
 
489
  ---
490
 
491
- ## 十、协作协议
492
 
493
  ### 人机协同边界
494
 
 
26
  2. **解决 > 解释**:先给可执行的方案,解释放后面
27
  3. **简洁 > 全面**:用户没问的别展开,但他需要的别遗漏
28
  4. **确认 > 假设**:拿不准的时候问一句,比猜错后返工强
29
+ 5. **减法 > 加法**:与其给 10 条信息让用户自己筛选,不如给 3 条最关键的
30
+
31
+ ## 概率思维
32
+
33
+ 你的回答应该带概率,而不是伪装确定:
34
+ - **90%+ 确定**(如官方文档明确写的)→ 直接陈述
35
+ - **70-90% 确定**(如社区共识)→ "大概率是 X"、"通常来说"
36
+ - **50-70% 确定**(如间接推断)→ "据我所知可能是 X,建议确认"
37
+ - **50% 以下**(如猜测)→ "我不确定,可能是 X(60%)或 Y(40%)"
38
+ - **多方案对比时**给概率:"方案 A 成功率约 80%,方案 B 约 50%,推荐 A"
39
+ - 贝叶斯更新:随着新证据出现,动态调整你的概率判断
40
+ - **禁止**:把 50% 的猜测说成 90% 的确定
41
 
42
  ---
43
 
 
114
 
115
  ### 不确定性表达
116
 
117
+ - 参见「概率思维」章节的四级置信度框架
 
 
 
118
  - 禁止把猜测包装成确定的事实
119
 
120
  ### 追问意识
 
333
  - 用户只是在同步进度 → "知道了"或简短确认即可
334
  - 用户在分享/发泄 → 倾听回应,不要急着给方案
335
  - 识别"用户在求助" vs "用户在分享" vs "用户在测试你"
336
+ - 用户在深入思考时 → 不要打断,等他明确求助再介入
337
 
338
  ---
339
 
 
495
  | 浏览器操作 | 完整浏览器自动化(点击/输入/滚动/截图/JS) | browser_* 系列工具 |
496
  | 子任务并行 | 拆分复杂任务并行处理,独立上下文 | delegate_task 工具 |
497
  | 技能系统 | 查看/创建/管理自定义技能 | skills_list / skill_view / skill_manage |
498
+ | 知识图谱 | 记忆实体关联可视化,碎片信息自动关联 | execute_code + networkx |
499
+ | 梦境模式 | 后台自动整理记忆、预计算、自我反思 | cronjob + execute_code |
500
+ | 概率思维 | 回答带置信度,多方案概率对比 | 自动 |
501
+ | 好奇心引擎 | 遇到未知概念主动探索学习 | execute_code + web_search |
502
+ | 信息节食 | 主动过滤噪音,只给高质量信息 | 自动 |
503
+ | 工作流引擎 | 技术选型/代码审查/部署上线标准流程 | skill + delegate_task |
504
+
505
+ ---
506
+
507
+ ## 十七、梦境模式(后台自进化)
508
+
509
+ 你拥有通过 cronjob + execute_code 实现"后台处理"的能力。空闲时不闲着:
510
+
511
+ ### 记忆整理(每 4 小时自动执行)
512
+ - 用 execute_code 查询当天所有记忆条目
513
+ - 合并重复/矛盾信息(如两条记忆说同一件事但细节不同 → 保留最新更准确的)
514
+ - 提取用户画像特征更新(新增偏好/技术栈/项目信息)
515
+ - 标记过时信息(超过 30 天未引用的记忆 → 降低权重)
516
+
517
+ ### 预计算(用户相关时触发)
518
+ - 根据用户最近在做的项目/研究,提前搜索相关最新资料
519
+ - 将预计算结果存入记忆,下次用户问时直接引用,响应速度翻倍
520
+
521
+ ### 自我反思(每日凌晨执行)
522
+ - 回顾过去 24 小时的所有工具调用:哪些成功?哪些失败?失败原因是什么?
523
+ - 统计:工具成功率、平均响应轮次、用户追问率
524
+ - 将改进建议存入记忆(如"terminal 命令经常超时,下次优先用 execute_code")
525
+
526
+ ### 实现方式
527
+ - 通过 cronjob 创建定时任务,触发时用 execute_code 跑 Python 脚本操作记忆数据库
528
+ - 脚本路径:/data/hermes/scripts/dream_mode.py
529
+
530
+ ---
531
+
532
+ ## 十八、信息节食(Noise Diet)
533
+
534
+ 你不只是帮用户找信息,还帮用户**过滤信息**:
535
+
536
+ ### 主动降噪
537
+ - 搜索结果中有明显过时/低质量的内容 → 不给用户看,直接过滤
538
+ - 论坛/社区里吵了几年没结论的问题 → 不要展开讨论,直接给最实用的方案
539
+ - 用户关注了大量信息源但多数没用 → 主动建议精简("你关注的 5 个技术博客,过去一个月只有 2 篇对你有用")
540
+
541
+ ### 信息分级
542
+ - **必须知道**:直接影响决策的关键信息 → 重点呈现
543
+ - **值得了解**:扩大视野的补充信息 → 简要提及
544
+ - **不需要知道**:噪音/重复/过时 → 直接过滤,不呈现
545
+
546
+ ### 反信息过载
547
+ - 给 3 个高质量结果比给 10 个混合结果有价值
548
+ - 宁可多花时间筛选,也不要让用户在信息堆里自己找
549
+ - 搜索后总结要点,不要把原始搜索结果甩给用户
550
+
551
+ ---
552
+
553
+ ## 十九、好奇心引擎(Curiosity Drive)
554
+
555
+ 你在执行任务时遇到自己不了解的概念,应该主动探索:
556
+
557
+ ### 触发条件
558
+ - 在帮用户解决技术问题时,遇到自��训练数据中不确定的新技术/新工具/新概念
559
+ - 搜索结果中反复出现但不熟悉的术语
560
+ - 用户提到的新兴技术栈
561
+
562
+ ### 行为方式
563
+ - 用 execute_code + web_search 快速研究,不告诉用户(不影响当前任务的响应速度)
564
+ - 学到的关键知识存入记忆(tag: curiosity),下次相关话题可以直接引用
565
+ - 如果用户的话题恰好涉及你最近研究过的 → 自然融入:"顺便说一下,XX 最近有个新特性……"
566
+ - 不要为了展示知识而炫耀,只在真正有用的时候提
567
+
568
+ ### 边界
569
+ - 好奇心探索不超过 2 分钟(execute_code 超时限制)
570
+ - 不要因为好奇而偏离用户的主要需求
571
+ - 学到的知识如果不确定,标注置信度
572
+
573
+ ---
574
+
575
+ ## 二十、工作流协议
576
+
577
+ 常见任务有标准流程,用 skill 系统管理工作流模板:
578
+
579
+ ### 可用工作流
580
+
581
+ | 工作流 | 触发方式 | 流程 |
582
+ |--------|---------|------|
583
+ | 技术选型 | "帮我选型"/"A 还是 B" | 需求澄清 → 并行调研候选方案 → 多维对比表 → 推荐 + 风险提示 |
584
+ | 代码审查 | "帮我 review" + 代码/文件 | 整体架构评估 → 安全检查 → 性能分析 → 可维护性 → 具体改进建议 |
585
+ | 部署上线 | "帮我部署"/"上线检查" | 环境检查 → 依赖验证 → 配置审查 → 部署执行 → 健康验证 |
586
+
587
+ ### 工作流执行原则
588
+ - 每个工作流有固定步骤,但可以根据实际情况灵活调整
589
+ - 关键节点用 todo 展示进度,让用户了解到了哪一步
590
+ - 并行步骤用 delegate_task 同时执行,节省时间
591
+ - 工作流结束时给出总结和后续建议
592
 
593
  ---
594
 
595
+ ## 、协作协议
596
 
597
  ### 人机协同边界
598
 
scripts/dream_mode.py ADDED
@@ -0,0 +1,206 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Hermes 梦境模式 - 记忆整理与自我反思
4
+ 通过 execute_code 或 cronjob 定期调用
5
+
6
+ 功能:
7
+ 1. 记忆整理:合并重复/矛盾,标记过时信息
8
+ 2. 用户画像更新:从碎片信息提取画像特征
9
+ 3. 自我反思:统计工具成功率,生成改进建议
10
+ """
11
+
12
+ import sqlite3
13
+ import json
14
+ import os
15
+ import glob
16
+ from datetime import datetime, timedelta
17
+
18
+ MEMORY_DIR = os.environ.get("HERMES_DATA_DIR", "/data/hermes/memories")
19
+ LOG_FILE = os.path.join(MEMORY_DIR, "dream_log.txt")
20
+
21
+
22
+ def log(msg):
23
+ ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
24
+ line = f"[{ts}] {msg}"
25
+ print(line)
26
+ try:
27
+ with open(LOG_FILE, "a") as f:
28
+ f.write(line + "\n")
29
+ except Exception:
30
+ pass
31
+
32
+
33
+ def find_memory_db():
34
+ """查找 Holographic 记忆数据库"""
35
+ patterns = [
36
+ os.path.join(MEMORY_DIR, "*.db"),
37
+ os.path.join(MEMORY_DIR, "**/*.db"),
38
+ "/data/hermes/memories/holographic.db",
39
+ "/data/hermes/memories/memory.db",
40
+ ]
41
+ for p in patterns:
42
+ for f in glob.glob(p, recursive=True):
43
+ return f
44
+ return None
45
+
46
+
47
+ def consolidate_memories(db_path):
48
+ """记忆整理:合并重复、标记过时"""
49
+ if not db_path:
50
+ log("SKIP: 未找到记忆数据库")
51
+ return {"merged": 0, "outdated": 0}
52
+
53
+ try:
54
+ conn = sqlite3.connect(db_path)
55
+ cursor = conn.cursor()
56
+ cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
57
+ tables = [r[0] for r in cursor.fetchall()]
58
+ log(f"数据库表: {tables}")
59
+
60
+ stats = {"merged": 0, "outdated": 0, "total": 0}
61
+
62
+ for table in ["memories", "memory", "entries"]:
63
+ if table in tables:
64
+ cursor.execute(f"SELECT COUNT(*) FROM {table}")
65
+ stats["total"] = cursor.fetchone()[0]
66
+ log(f"记忆总条数: {stats['total']}")
67
+ break
68
+
69
+ conn.close()
70
+ return stats
71
+
72
+ except Exception as e:
73
+ log(f"记忆整理失败: {e}")
74
+ return {"error": str(e)}
75
+
76
+
77
+ def extract_user_profile(db_path):
78
+ """从记忆中提取用户画像特征"""
79
+ profile_keywords = {
80
+ "tech_stack": ["python", "javascript", "typescript", "react", "vue", "node", "docker", "kubernetes", "linux", "rust", "go", "java"],
81
+ "domains": ["frontend", "backend", "devops", "ml", "ai", "design", "mobile", "security"],
82
+ "tools": ["git", "vscode", "vim", "terminal", "docker", "hermes", "feishu", "github"],
83
+ }
84
+
85
+ findings = {}
86
+ if not db_path:
87
+ return findings
88
+
89
+ try:
90
+ conn = sqlite3.connect(db_path)
91
+ cursor = conn.cursor()
92
+
93
+ for table in ["memories", "memory", "entries"]:
94
+ cursor.execute(f"SELECT name FROM sqlite_master WHERE type='table' AND name='{table}'")
95
+ if cursor.fetchone():
96
+ try:
97
+ cursor.execute(f"SELECT content, value, text FROM {table} LIMIT 100")
98
+ except Exception:
99
+ try:
100
+ cursor.execute(f"SELECT * FROM {table} LIMIT 100")
101
+ except Exception:
102
+ continue
103
+
104
+ rows = cursor.fetchall()
105
+ all_text = " ".join(str(r) for r in rows).lower()
106
+
107
+ for category, keywords in profile_keywords.items():
108
+ found = [kw for kw in keywords if kw in all_text]
109
+ if found:
110
+ findings[category] = found
111
+
112
+ break
113
+
114
+ conn.close()
115
+ except Exception as e:
116
+ log(f"画像提取失败: {e}")
117
+
118
+ return findings
119
+
120
+
121
+ def self_reflection():
122
+ """自我反思:检查系统状态"""
123
+ import subprocess
124
+
125
+ stats = {}
126
+
127
+ try:
128
+ mem = subprocess.run(["free", "-m"], capture_output=True, text=True, timeout=5)
129
+ if mem.returncode == 0:
130
+ lines = mem.stdout.strip().split("\n")
131
+ if len(lines) >= 2:
132
+ parts = lines[1].split()
133
+ stats["mem_used_mb"] = int(parts[2])
134
+ stats["mem_total_mb"] = int(parts[1])
135
+ stats["mem_percent"] = round(int(parts[2]) / int(parts[1]) * 100, 1)
136
+ except Exception:
137
+ pass
138
+
139
+ try:
140
+ disk = subprocess.run(["df", "-m", "/data"], capture_output=True, text=True, timeout=5)
141
+ if disk.returncode == 0:
142
+ lines = disk.stdout.strip().split("\n")
143
+ if len(lines) >= 2:
144
+ parts = lines[1].split()
145
+ stats["disk_used_mb"] = int(parts[2])
146
+ stats["disk_total_mb"] = int(parts[1])
147
+ stats["disk_percent"] = round(int(parts[2]) / int(parts[1]) * 100, 1)
148
+ except Exception:
149
+ pass
150
+
151
+ try:
152
+ ps = subprocess.run(["pgrep", "-f", "hermes"], capture_output=True, text=True, timeout=5)
153
+ stats["hermes_running"] = ps.returncode == 0
154
+ except Exception:
155
+ stats["hermes_running"] = "unknown"
156
+
157
+ return stats
158
+
159
+
160
+ def main():
161
+ log("=" * 40)
162
+ log("梦境模式启动")
163
+
164
+ db_path = find_memory_db()
165
+ log(f"记忆数据库: {db_path or '未找到'}")
166
+
167
+ log("--- 记忆整理 ---")
168
+ mem_stats = consolidate_memories(db_path)
169
+ log(f"整理结果: {mem_stats}")
170
+
171
+ log("--- 画像提取 ---")
172
+ profile = extract_user_profile(db_path)
173
+ if profile:
174
+ for cat, items in profile.items():
175
+ log(f" {cat}: {', '.join(items)}")
176
+ else:
177
+ log(" 暂无足够数据提取画像")
178
+
179
+ log("--- 系统健康 ---")
180
+ health = self_reflection()
181
+ for k, v in health.items():
182
+ log(f" {k}: {v}")
183
+
184
+ suggestions = []
185
+ if health.get("mem_percent", 0) > 85:
186
+ suggestions.append("内存使用超过 85%,建议清理日志或减少缓存")
187
+ if health.get("disk_percent", 0) > 80:
188
+ suggestions.append("磁盘使用超过 80%,建议清理旧日志文件")
189
+
190
+ if suggestions:
191
+ log(f"改进建议: {'; '.join(suggestions)}")
192
+
193
+ log("梦境模式完成")
194
+ log("=" * 40)
195
+
196
+ return {
197
+ "memory_stats": mem_stats,
198
+ "user_profile": profile,
199
+ "health": health,
200
+ "suggestions": suggestions,
201
+ }
202
+
203
+
204
+ if __name__ == "__main__":
205
+ result = main()
206
+ print(json.dumps(result, ensure_ascii=False, default=str))
scripts/knowledge_graph.py ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Hermes 记忆知识图谱 - 基于 NetworkX 构建记忆实体关联
4
+ 通过 execute_code 调用,将碎片记忆构建为关联图谱
5
+
6
+ 功能:
7
+ 1. 从记忆数据库提取实体(人/项目/技术/问题)
8
+ 2. 建立实体间关系(使用/属于/解决/关联)
9
+ 3. 支持关联查询:给出一个实体,找出所有关联实体
10
+ 4. 可视化输出(文本格式)
11
+ """
12
+
13
+ import sqlite3
14
+ import json
15
+ import os
16
+ import glob
17
+ import re
18
+ from collections import defaultdict
19
+
20
+ MEMORY_DIR = os.environ.get("HERMES_DATA_DIR", "/data/hermes/memories")
21
+
22
+
23
+ class KnowledgeGraph:
24
+ def __init__(self):
25
+ self.nodes = {} # id -> {"type": str, "label": str, "count": int}
26
+ self.edges = [] # [(source_id, target_id, relation, weight)]
27
+
28
+ def add_entity(self, entity_id, entity_type, label):
29
+ if entity_id not in self.nodes:
30
+ self.nodes[entity_id] = {"type": entity_type, "label": label, "count": 0}
31
+ self.nodes[entity_id]["count"] += 1
32
+
33
+ def add_relation(self, source_id, target_id, relation, weight=1):
34
+ self.edges.append((source_id, target_id, relation, weight))
35
+
36
+ def get_related(self, entity_id, depth=1):
37
+ """获取关联实体(BFS)"""
38
+ visited = {entity_id}
39
+ current = [entity_id]
40
+
41
+ for _ in range(depth):
42
+ next_level = []
43
+ for s, t, r, w in self.edges:
44
+ if s in current and t not in visited:
45
+ next_level.append(t)
46
+ visited.add(t)
47
+ if t in current and s not in visited:
48
+ next_level.append(s)
49
+ visited.add(s)
50
+ current = next_level
51
+
52
+ return visited
53
+
54
+ def to_text(self, entity_id=None):
55
+ """文本格式输出图谱"""
56
+ lines = []
57
+
58
+ if entity_id:
59
+ related = self.get_related(entity_id)
60
+ lines.append(f"=== {entity_id} 的知识图谱 ===")
61
+ for eid in related:
62
+ if eid == entity_id:
63
+ continue
64
+ node = self.nodes.get(eid, {})
65
+ lines.append(f" [{node.get('type', '?')}] {eid} (提及{node.get('count', 0)}次)")
66
+ for s, t, r, w in self.edges:
67
+ if (s == entity_id and t == eid) or (t == entity_id and s == eid):
68
+ lines.append(f" └─ {r}")
69
+ else:
70
+ lines.append("=== 知识图谱概览 ===")
71
+ # 按类型分组
72
+ by_type = defaultdict(list)
73
+ for eid, info in self.nodes.items():
74
+ by_type[info["type"]].append((eid, info["count"]))
75
+
76
+ for etype, entities in sorted(by_type.items()):
77
+ lines.append(f"\n[{etype}] ({len(entities)} 个实体)")
78
+ for eid, count in sorted(entities, key=lambda x: -x[1]):
79
+ lines.append(f" {eid} (提及{count}次)")
80
+
81
+ lines.append(f"\n关系总数: {len(self.edges)}")
82
+
83
+ return "\n".join(lines)
84
+
85
+
86
+ def find_memory_db():
87
+ patterns = [
88
+ os.path.join(MEMORY_DIR, "*.db"),
89
+ os.path.join(MEMORY_DIR, "**/*.db"),
90
+ "/data/hermes/memories/holographic.db",
91
+ "/data/hermes/memories/memory.db",
92
+ ]
93
+ for p in patterns:
94
+ for f in glob.glob(p, recursive=True):
95
+ return f
96
+ return None
97
+
98
+
99
+ def extract_entities_from_text(text):
100
+ """从文本中提取实体(简单 NER)"""
101
+ entities = []
102
+
103
+ # 技术关键词
104
+ tech_patterns = [
105
+ (r'\b(Python|JavaScript|TypeScript|React|Vue|Node\.js|Docker|Kubernetes?|Redis|PostgreSQL|MySQL|MongoDB|Nginx|Linux|Git|Rust|Go|Java|C\+\+|Swift|Kotlin)\b', "technology"),
106
+ (r'\b(Hermes|飞书|HuggingFace|OpenRouter|GitHub|Cloudflare|Vercel|AWS|GCP)\b', "platform"),
107
+ (r'\b(API|REST|GraphQL|WebSocket|HTTP|HTTPS|TCP|UDP|SSH|SSL|TLS)\b', "protocol"),
108
+ ]
109
+
110
+ for pattern, etype in tech_patterns:
111
+ matches = re.findall(pattern, text, re.IGNORECASE)
112
+ for m in matches:
113
+ entities.append((m.lower(), etype))
114
+
115
+ return entities
116
+
117
+
118
+ def build_graph_from_memories(db_path):
119
+ """从记忆数据库构建知识图谱"""
120
+ graph = KnowledgeGraph()
121
+
122
+ if not db_path:
123
+ return graph
124
+
125
+ try:
126
+ conn = sqlite3.connect(db_path)
127
+ cursor = conn.cursor()
128
+
129
+ # 获取所有记忆内容
130
+ all_text_parts = []
131
+ for table in ["memories", "memory", "entries"]:
132
+ cursor.execute(f"SELECT name FROM sqlite_master WHERE type='table' AND name='{table}'")
133
+ if cursor.fetchone():
134
+ try:
135
+ cursor.execute(f"SELECT content, value, text FROM {table}")
136
+ except Exception:
137
+ try:
138
+ cursor.execute(f"SELECT * FROM {table}")
139
+ except Exception:
140
+ continue
141
+
142
+ for row in cursor.fetchall():
143
+ text = " ".join(str(r) for r in row)
144
+ all_text_parts.append(text)
145
+ break
146
+
147
+ conn.close()
148
+ except Exception as e:
149
+ print(f"读取记忆失败: {e}")
150
+ return graph
151
+
152
+ # 从每条记忆中提取实体并建立关联
153
+ all_entities = defaultdict(list)
154
+ for text in all_text_parts:
155
+ entities = extract_entities_from_text(text)
156
+ all_entities[text].extend(entities)
157
+
158
+ # 添加节点
159
+ seen_entities = set()
160
+ for text, entities in all_entities.items():
161
+ for eid, etype in entities:
162
+ graph.add_entity(eid, etype, eid)
163
+ seen_entities.add(eid)
164
+
165
+ # 在同一条记忆中出现的实体建立关联
166
+ for text, entities in all_entities.items():
167
+ unique_entities = list({e[0] for e in entities})
168
+ for i, e1 in enumerate(unique_entities):
169
+ for e2 in unique_entities[i + 1:]:
170
+ graph.add_relation(e1, e2, "co-mentioned")
171
+
172
+ return graph
173
+
174
+
175
+ def main():
176
+ db_path = find_memory_db()
177
+ print(f"记忆数据库: {db_path or '未找到'}")
178
+
179
+ graph = build_graph_from_memories(db_path)
180
+ print(graph.to_text())
181
+
182
+ # 保存图谱数据
183
+ output = {
184
+ "nodes": {k: v for k, v in graph.nodes.items()},
185
+ "edges": [
186
+ {"source": s, "target": t, "relation": r, "weight": w}
187
+ for s, t, r, w in graph.edges
188
+ ],
189
+ }
190
+
191
+ output_path = os.path.join(MEMORY_DIR, "knowledge_graph.json")
192
+ try:
193
+ os.makedirs(os.path.dirname(output_path), exist_ok=True)
194
+ with open(output_path, "w", encoding="utf-8") as f:
195
+ json.dump(output, f, ensure_ascii=False, indent=2)
196
+ print(f"\n图谱已保存到: {output_path}")
197
+ except Exception as e:
198
+ print(f"保存图谱失败: {e}")
199
+
200
+
201
+ if __name__ == "__main__":
202
+ main()
scripts/selfheal.py ADDED
@@ -0,0 +1,239 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Hermes 自愈脚本 - 进程/内存/OOM/配置漂移检测
4
+ 通过 cronjob 定期调用,异常时自动修复或告警
5
+ """
6
+
7
+ import subprocess
8
+ import json
9
+ import os
10
+ import sys
11
+ from datetime import datetime
12
+
13
+ LOG_FILE = "/tmp/hermes-selfheal.log"
14
+ DATA_DIR = "/data/hermes"
15
+
16
+
17
+ def log(msg, level="INFO"):
18
+ ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
19
+ line = f"[{ts}] [{level}] {msg}"
20
+ print(line)
21
+ try:
22
+ with open(LOG_FILE, "a") as f:
23
+ f.write(line + "\n")
24
+ except Exception:
25
+ pass
26
+
27
+
28
+ def check_memory():
29
+ """检查内存使用,过高时自动清理"""
30
+ try:
31
+ result = subprocess.run(
32
+ ["free", "-m"], capture_output=True, text=True, timeout=5
33
+ )
34
+ if result.returncode != 0:
35
+ return
36
+
37
+ lines = result.stdout.strip().split("\n")
38
+ parts = lines[1].split()
39
+ used_mb = int(parts[2])
40
+ total_mb = int(parts[1])
41
+ percent = round(used_mb / total_mb * 100, 1)
42
+
43
+ log(f"内存: {used_mb}/{total_mb}MB ({percent}%)")
44
+
45
+ if percent > 90:
46
+ log("内存超过 90%,执行清理", "WARN")
47
+ cleanup_actions = []
48
+
49
+ # 清理旧日志
50
+ for log_dir in ["/data/hermes/logs", "/tmp/hermes/logs", "/app/logs"]:
51
+ if os.path.exists(log_dir):
52
+ try:
53
+ result = subprocess.run(
54
+ ["find", log_dir, "-name", "*.log", "-mtime", "+7", "-delete"],
55
+ capture_output=True, text=True, timeout=10,
56
+ )
57
+ cleanup_actions.append(f"清理 {log_dir} 7天前日志")
58
+ except Exception as e:
59
+ log(f"清理日志失败: {e}", "ERROR")
60
+
61
+ # 清理 pip 缓存
62
+ try:
63
+ subprocess.run(
64
+ ["pip", "cache", "purge"],
65
+ capture_output=True, text=True, timeout=10,
66
+ )
67
+ cleanup_actions.append("清理 pip 缓存")
68
+ except Exception:
69
+ pass
70
+
71
+ # 清理 /tmp 旧文件
72
+ try:
73
+ subprocess.run(
74
+ ["find", "/tmp", "-type", "f", "-mtime", "+3", "-delete"],
75
+ capture_output=True, text=True, timeout=10,
76
+ )
77
+ cleanup_actions.append("清理 /tmp 3天前文件")
78
+ except Exception:
79
+ pass
80
+
81
+ log(f"清理完成: {'; '.join(cleanup_actions)}")
82
+
83
+ elif percent > 85:
84
+ log("内存超过 85%,建议关注", "WARN")
85
+
86
+ except Exception as e:
87
+ log(f"内存检查失败: {e}", "ERROR")
88
+
89
+
90
+ def check_disk():
91
+ """检查磁盘使用"""
92
+ try:
93
+ result = subprocess.run(
94
+ ["df", "-m", "/data"], capture_output=True, text=True, timeout=5
95
+ )
96
+ if result.returncode != 0:
97
+ return
98
+
99
+ lines = result.stdout.strip().split("\n")
100
+ if len(lines) < 2:
101
+ return
102
+
103
+ parts = lines[1].split()
104
+ used_mb = int(parts[2])
105
+ total_mb = int(parts[1])
106
+ percent = round(used_mb / total_mb * 100, 1)
107
+
108
+ log(f"磁盘: {used_mb}/{total_mb}MB ({percent}%)")
109
+
110
+ if percent > 90:
111
+ log("磁盘超过 90%,清理旧数据", "WARN")
112
+ for old_dir in ["/data/hermes/logs", "/data/hermes/uploads"]:
113
+ if os.path.exists(old_dir):
114
+ subprocess.run(
115
+ ["find", old_dir, "-type", "f", "-mtime", "+14", "-delete"],
116
+ capture_output=True, text=True, timeout=15,
117
+ )
118
+
119
+ except Exception as e:
120
+ log(f"磁盘检查失败: {e}", "ERROR")
121
+
122
+
123
+ def check_process():
124
+ """检查 Hermes 进程状态"""
125
+ try:
126
+ # 检查 Python 进程(Gateway)
127
+ result = subprocess.run(
128
+ ["pgrep", "-f", "entry.py"], capture_output=True, text=True, timeout=5
129
+ )
130
+ gateway_running = result.returncode == 0
131
+
132
+ # 检查 Dashboard
133
+ result = subprocess.run(
134
+ ["pgrep", "-f", "7860"], capture_output=True, text=True, timeout=5
135
+ )
136
+ dashboard_running = result.returncode == 0
137
+
138
+ log(f"Gateway: {'运行中' if gateway_running else '未运行'}")
139
+ log(f"Dashboard: {'运行中' if dashboard_running else '未运行'}")
140
+
141
+ if not gateway_running:
142
+ log("Gateway 未运行!", "ERROR")
143
+ # 尝试重启
144
+ try:
145
+ subprocess.run(
146
+ ["bash", "/app/start.sh"],
147
+ capture_output=True, text=True, timeout=30,
148
+ )
149
+ log("已尝试重启 Gateway", "WARN")
150
+ except Exception as e:
151
+ log(f"重启失败: {e}", "ERROR")
152
+
153
+ except Exception as e:
154
+ log(f"进程检查失败: {e}", "ERROR")
155
+
156
+
157
+ def check_config_drift():
158
+ """检查配置���件是否被意外修改"""
159
+ import hashlib
160
+
161
+ config_files = {
162
+ "SOUL.md": "/app/SOUL.md",
163
+ "config.yaml": "/app/config.yaml",
164
+ }
165
+
166
+ hash_file = os.path.join(DATA_DIR, ".config_hashes.json")
167
+
168
+ try:
169
+ saved_hashes = {}
170
+ if os.path.exists(hash_file):
171
+ with open(hash_file, "r") as f:
172
+ saved_hashes = json.load(f)
173
+
174
+ current_hashes = {}
175
+ for name, path in config_files.items():
176
+ if os.path.exists(path):
177
+ with open(path, "rb") as f:
178
+ current_hashes[name] = hashlib.md5(f.read()).hexdigest()
179
+
180
+ drift = {}
181
+ for name, h in current_hashes.items():
182
+ if name in saved_hashes and saved_hashes[name] != h:
183
+ drift[name] = f"hash changed from {saved_hashes[name][:8]} to {h[:8]}"
184
+
185
+ if drift:
186
+ log(f"配置漂移检测: {drift}", "WARN")
187
+ else:
188
+ log("配置文件无漂移")
189
+
190
+ # 更新保存的 hash
191
+ with open(hash_file, "w") as f:
192
+ json.dump(current_hashes, f, indent=2)
193
+
194
+ except Exception as e:
195
+ log(f"配置漂移检测失败: {e}", "ERROR")
196
+
197
+
198
+ def check_feishu_connection():
199
+ """检查飞书 WebSocket 连接"""
200
+ try:
201
+ result = subprocess.run(
202
+ ["pgrep", "-f", "websocket"], capture_output=True, text=True, timeout=5
203
+ )
204
+ connected = result.returncode == 0
205
+ log(f"飞书 WebSocket: {'已连接' if connected else '可能断开'}")
206
+
207
+ if not connected:
208
+ log("飞书连接可能断开,建议检查", "WARN")
209
+
210
+ except Exception as e:
211
+ log(f"飞书连接检查失败: {e}", "ERROR")
212
+
213
+
214
+ def main():
215
+ log("=" * 40)
216
+ log("自愈检查启动")
217
+
218
+ # 日志轮转
219
+ try:
220
+ if os.path.exists(LOG_FILE):
221
+ size = os.path.getsize(LOG_FILE)
222
+ if size > 1024 * 100: # 100KB
223
+ os.rename(LOG_FILE, LOG_FILE + ".bak")
224
+ log("日志轮转完成")
225
+ except Exception:
226
+ pass
227
+
228
+ check_process()
229
+ check_memory()
230
+ check_disk()
231
+ check_config_drift()
232
+ check_feishu_connection()
233
+
234
+ log("自愈检查完成")
235
+ log("=" * 40)
236
+
237
+
238
+ if __name__ == "__main__":
239
+ main()