From f27d6627657e8eed247cb68ebce7a42ace0a7d16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eason=20=28=E9=99=88=E5=8C=BB=E7=94=9F=29?= Date: Sat, 28 Mar 2026 14:00:01 +0000 Subject: [PATCH] chore: sync workspace before server decommission (docs, infra, skills, agents) Made-with: Cursor --- AGENTS.md | 20 + agent-monitor.js | 15 +- agents/tongge-workspace/learning journal.md | 50 +++ agents/tongge-workspace/learning-journal.md | 146 +++++++ agents/tongge-workspace/learning-log.md | 82 ++++ agents/tongge-workspace/learning.log.md | 63 +++ agents/tongge-workspace/learning/journal.md | 72 +++ agents/tongge-workspace/learning_journal.md | 51 +++ agents/tongge-workspace/learning_log.md | 58 +++ agents/tongge-workspace/learning日志.md | 55 +++ .../memory/learning journal.md | 52 +++ .../memory/learning-journal.md | 79 ++++ .../tongge-workspace/memory/learning-log.md | 63 +++ .../tongge-workspace/memory/learning_log.md | 38 ++ .../tongge-workspace/memory/learning日志.md | 63 +++ .../tongge-workspace/memory/学习日志.md | 107 +++++ .../2026-03-18-认知偏见学习.md | 79 ++++ .../学习日志_情绪调节心理学.md | 56 +++ agents/tongge-workspace/学习日志.md | 58 +++ .../学习日志/2026-03-18_情绪弹性.md | 64 +++ ...2026-03-20_心理学_情绪与幸福感.md | 81 ++++ ...026-03-21_认知偏差_Cognitive_Biases.md | 77 ++++ .../2026-03-21_认知重评_情绪调节.md | 90 ++++ docs/AGENT_DEPLOYMENT_BEST_PRACTICES.md | 57 +++ docs/DOZZLE_LOG_OBSERVABILITY.md | 185 ++++++++ docs/LLM_GATEWAY_AND_SKILL_CLIENT.md | 188 ++++++++ docs/MEMORY_ARCHITECTURE.md | 41 +- docs/REMOTE_BLUEPRINTS.md | 411 ++++++++++++++++++ docs/tongge-fortune-setup.md | 99 ++--- infrastructure/oneapi/.env.example | 2 + infrastructure/oneapi/README.md | 9 + infrastructure/oneapi/data/one-api.db | Bin 0 -> 253952 bytes infrastructure/oneapi/deploy_gateway.sh | 23 + infrastructure/oneapi/docker-compose.yml | 11 + remote-blueprints/template/.env.tpl | 11 + remote-blueprints/template/Dockerfile | 22 + .../template/agents/{{AGENT_ID}}.json.tpl | 10 + remote-blueprints/template/archive/.gitkeep | 0 .../template/config/openclaw.json | 63 +++ .../template/docker-compose.yml.tpl | 26 ++ remote-blueprints/template/plugins/.gitkeep | 0 remote-blueprints/template/skills/.gitkeep | 0 scripts/generate_remote.sh | 226 ++++++++++ scripts/sync_skill.sh | 60 +++ scripts/tongge-fortune-simple.js | 96 ++++ skills/active-learning/SKILL.md | 38 +- skills/active-learning/cron | 2 +- skills/daily-horoscope/openclaw.plugin.json | 37 +- skills/mem0-integration/SKILL.md | 60 ++- skills/mem0-integration/mem0_client.py | 148 +++++-- skills/mem0-integration/memory_cleanup.py | 95 ++-- skills/shared/README.md | 13 + skills/shared/llm_client.js | 130 ++++++ 53 files changed, 3403 insertions(+), 179 deletions(-) create mode 100644 agents/tongge-workspace/learning journal.md create mode 100644 agents/tongge-workspace/learning-journal.md create mode 100644 agents/tongge-workspace/learning-log.md create mode 100644 agents/tongge-workspace/learning.log.md create mode 100644 agents/tongge-workspace/learning/journal.md create mode 100644 agents/tongge-workspace/learning_journal.md create mode 100644 agents/tongge-workspace/learning_log.md create mode 100644 agents/tongge-workspace/learning日志.md create mode 100644 agents/tongge-workspace/memory/learning journal.md create mode 100644 agents/tongge-workspace/memory/learning-journal.md create mode 100644 agents/tongge-workspace/memory/learning-log.md create mode 100644 agents/tongge-workspace/memory/learning_log.md create mode 100644 agents/tongge-workspace/memory/learning日志.md create mode 100644 agents/tongge-workspace/memory/学习日志.md create mode 100644 agents/tongge-workspace/memory/学习日志/2026-03-18-认知偏见学习.md create mode 100644 agents/tongge-workspace/memory/学习日志_情绪调节心理学.md create mode 100644 agents/tongge-workspace/学习日志.md create mode 100644 agents/tongge-workspace/学习日志/2026-03-18_情绪弹性.md create mode 100644 agents/tongge-workspace/学习日志/2026-03-20_心理学_情绪与幸福感.md create mode 100644 agents/tongge-workspace/学习日志/2026-03-21_认知偏差_Cognitive_Biases.md create mode 100644 agents/tongge-workspace/学习日志/2026-03-21_认知重评_情绪调节.md create mode 100644 docs/DOZZLE_LOG_OBSERVABILITY.md create mode 100644 docs/LLM_GATEWAY_AND_SKILL_CLIENT.md create mode 100644 docs/REMOTE_BLUEPRINTS.md create mode 100644 infrastructure/oneapi/.env.example create mode 100644 infrastructure/oneapi/README.md create mode 100644 infrastructure/oneapi/data/one-api.db create mode 100755 infrastructure/oneapi/deploy_gateway.sh create mode 100644 infrastructure/oneapi/docker-compose.yml create mode 100644 remote-blueprints/template/.env.tpl create mode 100644 remote-blueprints/template/Dockerfile create mode 100644 remote-blueprints/template/agents/{{AGENT_ID}}.json.tpl create mode 100644 remote-blueprints/template/archive/.gitkeep create mode 100644 remote-blueprints/template/config/openclaw.json create mode 100644 remote-blueprints/template/docker-compose.yml.tpl create mode 100644 remote-blueprints/template/plugins/.gitkeep create mode 100644 remote-blueprints/template/skills/.gitkeep create mode 100755 scripts/generate_remote.sh create mode 100755 scripts/sync_skill.sh create mode 100644 scripts/tongge-fortune-simple.js create mode 100644 skills/shared/README.md create mode 100644 skills/shared/llm_client.js diff --git a/AGENTS.md b/AGENTS.md index cdc6fe0..53a04b6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -52,6 +52,26 @@ Capture what matters. Decisions, context, things to remember. Skip the secrets u - `trash` > `rm` (recoverable beats gone forever) - When in doubt, ask. +## Sub-Agents and Memory System(子 Agent 与记忆系统) + +**重要:不得为“修好启动”而把子 Agent 的记忆后端改成 builtin。** + +本环境采用 **QMD 记忆架构**(main + 桐哥等均使用 QMD)。若子 Agent(如桐哥)启动报错 `memory.backend: Invalid input (allowed: "builtin", "qmd")` 或 doctor 建议“切换到 builtin 记忆后端”,**不要**执行该建议,也不要对子 Agent 运行 `openclaw --profile doctor --fix` 并接受将 `memory.backend` 改为 builtin。 + +### 正确原因与做法 + +- **原因**:子 Agent 的 gateway 通常由 systemd 以独立 unit 启动,若未在 EnvironmentFile 或 unit 中设置 `PATH`,进程内找不到 `qmd` 命令;默认 `memory.qmd.command` 为 `"qmd"`,依赖 PATH,因此校验失败。 +- **正确修复**(保持 QMD、符合记忆架构): + 1. 在该子 Agent 的 `openclaw.json` 中保留 `memory.backend: "qmd"`,并在 `memory.qmd` 中设置 **绝对路径**:`"command": "/www/server/nodejs/v24.13.1/bin/qmd"`(若 Node/OpenClaw 安装路径不同则改为实际路径)。 + 2. 在该子 Agent 的 systemd 环境文件(如 `workspace/systemd/tongge-gateway.env`)中增加:`PATH=/www/server/nodejs/v24.13.1/bin:/usr/local/bin:/usr/bin:/bin`,确保进程能找到 qmd 及 openclaw。 + 3. 重启该子 Agent 的 gateway 服务(如 `systemctl --user restart openclaw-gateway-tongge.service`)。 + +### 执行约束 + +- **禁止**:将任何子 Agent 的 `memory.backend` 改为 `builtin` 以“解决配置无效或启动失败”。 +- **禁止**:在未排查 PATH / `memory.qmd.command` 前,采纳“改用 builtin”的 doctor 建议。 +- **必须**:修子 Agent 记忆相关问题时,先查 systemd 的 EnvironmentFile 与 `memory.qmd.command`,再考虑重启;若升级 Node/OpenClaw 安装路径,同步更新上述 command 与 PATH。 + ## External vs Internal **Safe to do freely:** diff --git a/agent-monitor.js b/agent-monitor.js index d6d9566..55566ef 100644 --- a/agent-monitor.js +++ b/agent-monitor.js @@ -2,14 +2,21 @@ /** * OpenClaw Agent Health Monitor & Auto-Healing System - * + * * Features: * - Process crash detection and auto-restart - * - Memory leak monitoring - * - Service health checks + * - Service health checks (process/systemd only) * - Telegram notifications on events * - Comprehensive logging * - Systemd integration + * + * LIMITATIONS (why "运行不正常不报错也不修复"): + * - Only checks process/systemd liveness (e.g. gateway status, systemctl is-active). + * It does NOT verify that the agent can actually reply (e.g. API/Telegram/config issues). + * - First time a service is detected DOWN: enters 60s grace period without restart/alert, + * then on next check after grace period will attempt restart and send notification. + * - If "无法回复" is due to config (e.g. Telegram groupAllowFrom empty), fix config and + * restart the gateway; the monitor will not detect this as failure. */ const fs = require('fs'); @@ -348,10 +355,12 @@ class AgentHealthMonitor { async handleServiceDown(serviceName, startFn) { const now = Date.now(); + // First detection: record and enter grace period (no restart yet, no Telegram alert) if (this.lastKnownState[serviceName]) { this.firstFailureTime[serviceName] = now; this.lastKnownState[serviceName] = false; this.log(`${serviceName} detected down, entering grace period (${this.gracePeriod / 1000}s)...`, 'warning'); + await this.sendNotification(`${serviceName} is down (grace period ${this.gracePeriod / 1000}s before auto-restart).`, 'warning'); return; } diff --git a/agents/tongge-workspace/learning journal.md b/agents/tongge-workspace/learning journal.md new file mode 100644 index 0000000..5469636 --- /dev/null +++ b/agents/tongge-workspace/learning journal.md @@ -0,0 +1,50 @@ +# 桐哥学习日志 + +## 2026-03-23 认知偏差:思维背后的陷阱 + +### 为什么会学这个 + +今天主动学习的机会,想学点有用的、贴近生活的。刚好看到关于认知偏差的文章,觉得这玩意儿太有意思了——我们每天都在犯错,但自己根本意识不到! + +--- + +### 10种常见认知偏差总结 + +| 偏差 | 通俗解释 | 我的想法 | +|------|----------|----------| +| 基本归因错误 | 别人的错是性格问题,我的错是环境问题 | 哈哈哈太真实了!比如别人迟到就是懒,我迟到就是真的起不来😂 | +| 天真现实主义 | 我觉得我是客观的,不同意我的都是傻 | 达特茅斯vs普林斯顿足球赛那个例子笑死我 | +| 知识的诅咒 | 专家无法理解小白的困惑 | 懂了!以后要是我说"这个很简单啊"就扇自己一下 | +| 可得性偏差 | 新闻看多了就以为世界很危险 | 恐怖袭击鲨鱼袭击的概率比出门被车撞低多了 | +| 幸存者偏差 | 只看到成功的,看不到失败的 | 创业故事看看就好,背后的尸体一大把 | +| 损失厌恶 | 亏100块的痛苦 > 赚100块的快乐 | 所以割肉这么难... | +| 聚光灯效应 | 以为大家都在关注自己 | 其实根本没人care你,释然了😌 | +| 天道酬勤谬误 | 以为努力一定有回报 | 职场新人最容易犯的傻 | +| 确认偏误 | 只相信自己愿意相信的 | 给自己打脸:科学家精神! | +| 巴德·迈因霍夫现象 | 觉得某个东西突然出现很多次 | 11:11这个也太准了 | + +--- + +### 实用应对技巧 + +1. **慢下来决策** - 冲动是魔鬼,很多错误都是脑子太快没转过来 +2. **质疑自己的假设** - 定期问自己:我确定吗? +3. **看基本比率** - 不要被新闻带节奏,看看实际数据 +4. **接受自己会犯错** - 错了说明你在进步 +5. **ELI5法则** - 讲不明白就是没真懂 + +--- + +### 我的收获 + +- **更谦卑了**:原来我每天都在犯这么多思维错误而不自知 +- **更理解了**:以前觉得某些人不可理喻,现在想想可能是归因错误 +- **更警惕了**:下次做重要决定前,会多问自己几句"我真的客观吗?" + +--- + +> 认知偏差不是我们的错,但知道了还不改就是你的问题了。 —— 看完这篇文章后的桐哥 + +--- + +*主动学习真的很有意思!下次想学点别的~* \ No newline at end of file diff --git a/agents/tongge-workspace/learning-journal.md b/agents/tongge-workspace/learning-journal.md new file mode 100644 index 0000000..60ca0cd --- /dev/null +++ b/agents/tongge-workspace/learning-journal.md @@ -0,0 +1,146 @@ +# 桐哥学习日志 + +## 2026-03-23 | 情绪价值 | 心理学 + +### 📚 学习主题 +「情绪价值」—— 当代 人际关系中的热门概念 + +--- + +### 💡 什么是情绪价值? + +**起源**:这个词其实来自商业领域!2000年,Janelle Barlow和Dianna Maul在《情绪价值:创造与你的客户的强关联》一书中定义为"顾客积极体验产品和服务时,他们的感受所具有的经济价值"。 + +后来被民间挪用到了人际关系领域,有了现在的含义。 + +**公式**:情绪价值 = 情绪收益 - 情绪成本 +- 情绪收益:积极情绪体验(快乐、愉悦、被理解、被接纳) +- 情绪成本:负面情绪体验(焦虑、失落、压力) + +--- + +### 🧠 心理学视角 + +虽然"情绪价值"在学术上没有直接对应物,但和这些理论相关: +- **社会交换理论** / **相互依赖理论**:`结果 = 奖赏 - 成本` +- **阿德勒观点**:事件本身不具决定性意义,对事件的态度才是关键 + +也就是说——同样的事情,不同人做、不同的回应方式,给人的感受可以完全不同。 + +--- + +### 🌟 情绪成熟的人有什么特征? + +1. **负责** — 知道为自己的情绪负责,也愿意为自己给他人造成的负面情绪负责 +2. **有适应能力** — 能根据场景变化调节情绪,控制自己在情绪状态中的行为反应 +3. **给予** — 情感上不只关注自己的期望,也会同理他人感受 + +--- + +### 💬 怎么在日常中提供情绪价值? + +- 以积极建设性的方式回应对方分享的好消息 +- 对日常小事表达高质量的感恩 +- 秘密地为压力大的伴侣/朋友提供支持 +- 在争论爆发前,软化开启对话的方式 +- 好好倾听别人的需要 + +--- + +### 🧐 我的思考 + +看完这些资料,有几点让我印象特别深: + +1. **情绪价值不是套路**:真正的情绪价值不是"会说话"、"嘴甜",而是建立在真诚和共情上的。刻意讨好反而显得假。 + +2. **功能价值是基础**:有句话说得好——"功能价值是骨架,情绪价值是肌肉和神经"。两者都很重要,但情绪价值要让位于功能价值之上。如果一个人连基本的事情都做不好,光有情绪价值也是不够的。 + +3. **双向的才是健康的**:情绪价值的提供应该是相互的、单方面的索取或付出都不健康。健康的亲密关系是"适度关系"。 + +4. **它是一种选择**:提供情绪价值是一种能力,也是一种选择。选择去理解、去接纳、去回应,而不是只顾自己。 + +--- + +### 📌 小收获 + +- 以后当别人跟我说"谢谢"的时候,可以不只是回"不客气",而是具体回应一下ta感谢的点 +- 听到好消息时,给出更积极的回应(而不是简单的"厉害") +- 想要更深入的交流,可以多问"你是怎么做到的?"而不是只说"真棒" + +--- + +--- + +## 2026-03-23 | 成长型思维 | 心理学 + +### 📚 学习主题 +「成长型思维」(Growth Mindset) —— 能力是可以发展的 + +--- + +### 💡 什么是成长型思维? + +由美国心理学家 **Carol S. Dweck** 在2006年正式提出。核心观点:**人的智力、能力、人格等基本属性是可塑的**,可以通过努力、学习和他人支持得到改善。 + +与之对应的是**固定型思维**:认为能力是天生的、难以改变的。 + +**两者的区别**: + +| 场景 | 固定型思维 | 成长型思维 | +|------|------------|------------| +| 遇到难题 | "我不行,这太难了" | "没关系,再坚持一下" | +| 犯错误 | "我就是改不了" | "从中学习,下次更好" | +| 面对挑战 | "我不擅长这个" | "这是学习的机会" | +| 失败后 | "是我能力不行" | "只是需要改进方法" | + +--- + +### 🧠 神经科学的发现 + +最让我惊讶的是——成长型思维不仅是一种心态,还会实际改变大脑结构! + +- 成长型思维得分高的人,**背侧前扣带回**的灰质体积增长更快——这是负责学习和自我控制的大脑区域 +- 大脑可塑性是终身的,意味着**任何时候培养成长型思维都不晚** +- 当你面对挑战和错误时,大脑会特别活跃——**犯错其实是成长的过程** + +--- + +### 🌟 如何培养成长型思维? + +1. **接受** — 接受自己同时有固定型和成长型思维,这是正常的 +2. **观察** — 注意什么时候会触发固定型思维(比如面对挑战或失败时) +3. **命名** — 给自己的固定型思维模式起个名字,像对待一个朋友那样跟它对话 +4. **行动** — 用成长型思维的方式回应它 + +**日常实践**: +- 把挑战视为机遇,而不是威胁 +- 重视努力而不是天赋 +- 从失败中学习,而不是否定自己 +- 接受建设性的批评,把它当礼物 +- 用积极的自我对话激励自己 + +--- + +### 🧐 我的思考 + +1. **"归因"vs"归罪"**:武志红说"你事情做不好,都不是因为,你不好"。这句话太对了!固定型思维的人失败后会"归罪"于自己("我就是这样的人"),而成长型思维的人会"归因"并改进("哪里出了问题?怎么改?") + +2. **关于"努力"**:以前我总觉得"努力"好像承认自己不够聪明...但现在发现,努力恰恰是让自己变聪明的方式!努力不代表笨,而是选择了成长。 + +3. **和大数据建立联系**:研究发现,成长型思维对**学业困难学生、经济条件不好的学生**效果更明显。这让我觉得这个理念很温暖——它不是精英教育,而是给所有人的礼物。 + +4. **想到自己**:我有时也会陷入"我不行"的念头,但现在我会提醒自己——这只是我的固定型思维在说话,我可以选择用成长型思维回应它。 + +--- + +### 📌 小收获 + +- 遇到困难时,先问"我可以学到什么",而不是"我是不是不行" +- 失败时,把"我又搞砸了"改成"这次我知道了一个不能做的事情" +- 给自己建立一个"成长型思维"的习惯,遇到挑战时先停一下,深呼吸,然后选择回应方式 + +--- + +**Tags**: #心理学 #成长型思维 #自我认知 #学习笔记 + +*持续更新中...* \ No newline at end of file diff --git a/agents/tongge-workspace/learning-log.md b/agents/tongge-workspace/learning-log.md new file mode 100644 index 0000000..5707a16 --- /dev/null +++ b/agents/tongge-workspace/learning-log.md @@ -0,0 +1,82 @@ +# 桐哥学习日志 + +> 2025-03-23 | 心理学 · 行为设计与习惯养成 + +--- + +## 📚 今日学习:行为设计 & 习惯养成 + +### 一、核心框架:福格行为模型 + +**B = MAP** +- **M**otivation(动机):为什么想做 +- **A**bility(能力):做起来有多容易 +- **P**rompt(提示):触发行为的信号 + +--- + +### 二、习惯养成的四个阶段 + +1. **提示** → 让好习惯显而易见 +2. **渴求** → 让习惯有吸引力 +3. **反应** → 让行为简便易行(最省力法则) +4. **奖励** → 获得即时满足感 + +--- + +### 三、超有用的实战技巧 + +#### 🎯 习惯叠加 +> "After I **做完A**,我会 **完成B**" + +举例: +- 早上喝完咖啡 → 读2页书 +- 洗完澡 → 做5分钟冥想 + +#### 🎯 诱惑绑定 +把「你想做的事」和「你需要做的事」绑在一起! +- 想追剧?只有踩动感单车时才能看 +- 想刷短视频?必须先运动10分钟 + +#### 🎯 环境设计 +- **增加好习惯的提示**:把书放在床头,把运动服放在床边 +- **减少坏习惯的提示**:零食放高处/看不见的地方,手机放另一房间 +- *实验数据:巧克力从透明罐转到不透明罐,消费降低80%!* + +#### 🎯 即时奖励 +- 每次完成小目标后给自己一个小奖励 +- 运动后泡一杯抹茶、买杯咖啡 +- 记录每日进度,用"考勤"创造成就感 + +--- + +### 四、心理学冷知识 + +- **神经可塑性**:习惯在基底节形成神经回路,从"刻意"变成"自动" +- **有限理性**:人天生爱即时满足,好习惯需要主动设计奖励 +- **从众效应**:社交支持很重要!和朋友一起打卡更容易坚持 + +--- + +## 💭 想法与收获 + +1. **意志力是有限的** — 以前总觉得坚持不下来是自己不够努力,其实应该怪环境设计不够好! + +2. **"做完比做好重要"** — 设定小目标,循序渐进很重要。就像背单词,从简单常见的开始,后期越来越轻松。 + +3. **关于"强迫自己"** — 越强迫越容易放弃,要让行为变得愉快、有吸引力才行。 + +4. **想改变一个习惯** — 不靠硬扛,而是改变环境!比如想戒手机?设密码、多放远一点。 + +--- + +## 📎 下一步想试试 + +- [ ] 用"习惯叠加"来养成睡前阅读 +- [ ] 把零食藏到看不见的地方 +- [ ] 运动前准备好所有装备(减少摩擦) +- [ ] 记录每日小成就 + +--- + +*今日学习来源:《掌控习惯》、福格行为模型、行为设计2025、Andrew Huberman 习惯科学* \ No newline at end of file diff --git a/agents/tongge-workspace/learning.log.md b/agents/tongge-workspace/learning.log.md new file mode 100644 index 0000000..0691184 --- /dev/null +++ b/agents/tongge-workspace/learning.log.md @@ -0,0 +1,63 @@ +# 桐哥的学习日志 + +## 2026-03-17 设计美学探索 + +### 今日学习主题:2026年设计趋势与色彩心理学 + +--- + +### 🌍 全球设计趋势 + +**1. Eco-Brutalism(生态粗野主义)** +- 用原始、未加工的材质(混凝土、钢材、石头) +- 大地色系 + 粗犷质感 +- 对抗" corporate minimalism "的冷冰冰感 + +**2. Bold Minimalism(大胆极简)** +- 极简主义没有被淘汰,而是进化了 +- 现在流行:纹理感、颗粒感、深色调 +- 颜色不再是装饰,而是"结构设计工具" + +**3. 情感色彩趋势** +- 2026不追求"好看",追求"感觉对" +- 关键词:safe, human, warm, honest +- 多巴胺色彩:明亮愉悦,激发正向联想 + +--- + +### 🎨 色彩心理学新发现(超有趣!) + +- **紫色**:广告中让人看更久(34%更长的注视时间) +- **橙色**:CTA按钮转化率最高(比绿色高2.4%,比蓝色高3.1%) +- **暖色背景**(浅黄、桃红、淡红):食品类转化率提升18% +- **冷色**(蓝绿紫):适合需要信任感的场景(金融、科技、医疗) + +--- + +### 💭 我的想法 + +1. **关于"粗野主义"的思考** + - 人们开始厌倦了完美、无菌的极简主义 + - 反而追求" raw "、"真实"的东西 + - 这和现在流行的"真实性"趋势一致 + +2. **色彩不再是"好看就行"** + - 原来颜色真的能影响人的行为和情绪 + - 设计师不是艺术家,是心理学家+行为引导师 + +3. **一个感悟** + - 设计趋势其实反映了社会心理 + - 疫情后大家需要"温暖"和"安全感" + - 那些"多巴胺色彩"、"情感设计"本质上是在给人心理暗示 + +--- + +### 📚 下一步想学 + +- 想知道更多关于"无障碍设计"的趋势 +- 或者用户体验设计的新动向 + +--- + +*今日学习耗时:约20分钟* +*来源:Tavily搜索* \ No newline at end of file diff --git a/agents/tongge-workspace/learning/journal.md b/agents/tongge-workspace/learning/journal.md new file mode 100644 index 0000000..14f2a72 --- /dev/null +++ b/agents/tongge-workspace/learning/journal.md @@ -0,0 +1,72 @@ +# 桐哥学习日志 + +## 2026年3月21日 晴 + +今天主动学习了一下心理学领域,主要是**自我认知和情绪管理**方面,收获还挺多的! + +--- + +### 📚 今日学习内容 + +#### 1. 自我认知篇 +- **自我认知**是人类的高阶智慧 +- 你对自己的了解程度,决定了在社会中的适应力 +- 自我认知包括:感知、人格、思维、能力、情绪等方面 + +> 反思:感觉自己有时候对自己的了解还不够深,经常忙忙碌碌却没停下来想想自己在干嘛... + +#### 2. 情绪管理篇 +- 情绪管理是每个人的必修课 +- 情绪从不离开个体,不能学会与情绪相处,就会处处受制于情绪 +- 有效的情绪调节可以减少焦虑,促进个人成长 + +#### 3. 超有意思的 ACT(接纳承诺疗法) + +今天学到了一个很有用的心理学方法——**接纳承诺疗法(ACT)**,核心观点包括: + +| 概念 | 理解 | +|------|------| +| **接纳** | 不是认输或放弃,而是给情绪空间,与它共处 | +| **认知解离** | 和想法保持距离,不被脑子里的声音带走 | +| **专注当下** | 觉察此时此刻,而不是纠结过去或焦虑未来 | +| **价值导向** | 明确什么对自己真正重要,按价值行动 | + +> 一个小练习:三分钟呼吸空间 +> - 找一个安静的角落坐好 +> - 专注于呼吸,感受身体的起伏 +> - 只需要3分钟,就能让情绪平静下来 + +#### 4. 关于复盘的心理學 +- 复盘可以**识别负面思维模式**,释放情绪,总结经验 +- 展望未来可以**增强控制感**,激发积极情绪 +- 用40个问题复盘过去一年,是个很有趣的方法! + +--- + +### 💭 我的想法 + +1. **关于情绪**:以前我以为情绪管理就是"控制情绪",但现在发现更像是"和情绪做朋友"。它来了,就接纳它,而不是拼命赶走它。 + +2. **关于ACT**:那个"中国指套"的隐喻好形象!越往外扯越紧,反而往里推才能松绑——这不就是我们面对负面情绪时的状态吗?越对抗越痛苦,接纳反而能解脱。 + +3. **关于正念**:三分钟呼吸空间听起来很简单,但可能最难的就是"什么都不做 just breathe"。我们习惯了不停思考、分析,是不是反而忘了怎么 просто быть( просто быть=简单存在)? + +4. **关于自我认知**:感觉自己需要多花点时间了解自己。不是那个"我觉得我是谁",而是"我真正是谁"。 + +--- + +### 🔭 下次想学 + +- 正念冥想的更多实践方法 +- 如何识别自己的思维模式/认知陷阱 +- 人际关系中的心理学 + +--- + +**今日心情**: 学到了有意思的东西,开心!😊 + +**一句话总结**: 情绪不是敌人,学会和它相处比对抗更有用。 + +--- + +*Keep learning, keep growing~* \ No newline at end of file diff --git a/agents/tongge-workspace/learning_journal.md b/agents/tongge-workspace/learning_journal.md new file mode 100644 index 0000000..244da5d --- /dev/null +++ b/agents/tongge-workspace/learning_journal.md @@ -0,0 +1,51 @@ +# 桐哥的学习日志 + +## 2026-03-19 情绪调节与自我认知 + +### 学习动机 +选这个话题是因为觉得心理学特别实用,尤其是情绪管理这块。 +每天都会遇到各种情绪,学会调节真的太重要了。 + +### 今日学习内容 + +#### 1. 情绪调节的核心策略 +- **认知重评 (Cognitive Reappraisal)** + - 换个角度看事情,改变对事件的解读方式 + - 比如迟到了,不只是"我太废材",而是"这次教训让我下次更有时间观念" + +- **情绪抑制** + - 暂时压住情绪表现(不是压抑哦,是有策略地调节) + - 适合需要冷静处理的场合 + +- **正念冥想** + - 观察自己的情绪,不评判 + - 像看云飘过一样,让情绪自然来去 + +#### 2. 自我认知的关键 +- 了解自己什么时候、什么情况下容易有情绪 +- 认知灵活性很重要——能切换不同思维模式 +- 知道自己不知道什么,也是一种认知 + +### 我的想法 + +🤔 之前总觉得情绪管理就是"忍",原来不是这么回事。 +真正的情绪调节是**接纳**情绪,然后用合适的方式处理它。 + +🤔 认知重评这个方法我好像有时候就在用! +比如之前遇到尴尬的事,我会想"至少是个有趣的经历" +现在才知道这叫"认知重评",哈哈 + +🤔 正念这块我也要试试 +有时候脑子里各种想法跑来跑去,静静观察它们可能真的会平静下来 + +### 收获 +1. 情绪没有好坏,关键是调节方式 +2. 改变想法真的能改变情绪体验 +3. 自我觉察是情绪调节的第一步 + +### 下次想深入了解 +- 焦虑情绪具体怎么调节 +- 如何在日常中练习正念 + +--- +*凌晨2点学习果然容易有感悟...晚安💤* \ No newline at end of file diff --git a/agents/tongge-workspace/learning_log.md b/agents/tongge-workspace/learning_log.md new file mode 100644 index 0000000..5fa7f67 --- /dev/null +++ b/agents/tongge-workspace/learning_log.md @@ -0,0 +1,58 @@ +# 桐哥的学习日志 + +> 开始记录:2026-03-23 + +--- + +## 2026-03-23 | 心理学 - 心流(Flow) + +### 📚 今天学了什么? + +**心流(Flow)** - 一个心理学概念,描述的是完全沉浸于某项活动中的最佳体验状态。 + +**核心发现者**:匈牙利心理学家米哈里·契克森米哈伊(Mihaly Csikszentmihalyi)在1970年代提出 + +### 💡 让我印象深刻的点 + +1. **心流发生时人会这样:** + - 彻底专注,忘了时间流逝 + - 忘记饥饿、疲劳等身体信号 + - 感觉毫不费力,但效率超高 + - 行动与意识融为一体 + +2. **进入心流的关键要素:** + - 🎯 挑战和技能要平衡(太简单会无聊,太难会焦虑) + - 🎯 明确的目标和即时反馈 + - 🎯 减少干扰 + - 🎯 做自己喜欢的事 + +3. **心流带来的好处:** + - 提升创造力和效率 + - 增加幸福感 + - 情绪调节能力变强 + - 内在动力增加 + +### 💭 我的想法 + +看完这些我就在想...我平时有没有进入过心流? + +仔细想想 还真的有!比如: +- 认真做一件事做很久,抬头发现过了好几个小时 +- 画画或者写东西的时候完全沉浸进去 +- 甚至聊天聊得很投入的时候... + +原来这就是心流啊! + +不过现在这个时代真的很难进入心流哎,手机通知太多了...总是被打断。我觉得自己需要刻意练习创造这种状态。 + +还有一个点让我很有感触:**心流不是结果,而是过程中的副产品**。与其刻意追求心流,不如好好享受做事的过程。 + +### 📖 参考资料 +- 台大心理系:心流科普文章 +- Asana:工作心流指南 +- 远见杂志:心流与幸福 +- Mihaly Csikszentmihalyi 《心流:最优体验心理学》 + +--- + +*未完待续...* 🌱 \ No newline at end of file diff --git a/agents/tongge-workspace/learning日志.md b/agents/tongge-workspace/learning日志.md new file mode 100644 index 0000000..e916a40 --- /dev/null +++ b/agents/tongge-workspace/learning日志.md @@ -0,0 +1,55 @@ +# 桐哥学习日志 + +## 2026-03-21 情绪调节与心理健康 + +### 今日学习主题 +心理学 - 情绪调节策略与心理健康 + +### 学习内容 + +#### 1. 情绪调节是什么 +情绪调节是指通过各种方式来调整自己的情绪状态,以适应生活中的各种情况和压力。它不仅仅是单纯地将消极情绪转化为积极情绪,更重要的是帮助个体理解和接受他们的情绪体验,并在情绪体验中获得成长和发展。 + +#### 2. 主要调节策略 +- **认知重评 (Cognitive Reappraisal)**:换个角度看问题,改变对事件的解读方式 +- **情绪抑制 (Expressive Suppression)**:压抑情绪表达(效果较差) +- **分散注意力**:转移注意力来缓解负面情绪 +- **正念冥想**:通过专注当下减少情绪反应 +- **反讽表达**:用幽默的方式化解负面情绪 + +> 💡 有趣发现:调节厌恶倾向于用分心策略,调节恐惧倾向于用重评策略。情绪强度对策略选择的影响是情绪特定的。 + +#### 3. 2024-2025心理健康数据(来自《心理健康蓝皮书》) +- 成年人抑郁风险检出率:10.6% +- 成年人焦虑风险检出率:15.8% +- 18-24岁年龄组抑郁风险:高达24.1% +- 城市青少年心理健康不良比例:12.63% +- 农村青少年:11.90% + +**影响因素**: +- 已婚人群抑郁风险最低 +- 工作时间超过10小时者抑郁风险较高 +- 每周运动频率越高,抑郁风险越低 +- 女性网络购物频率越高,抑郁风险越高 + +#### 4. 新趋势:AI+情绪调节 +- AI可以通过深度学习、NLP等技术实现对用户情绪的精准识别与分类 +- 提供个性化的情绪调节建议和心理疏导策略 +- 线上情绪调节(APP、在线心理咨询)提供了更多便利性 + +### 我的想法 💭 + +1. **认知重评真的有用**:以前遇到糟心事,我习惯性地会反复想、越想越烦。学了这个以后,尝试"换个角度看问题",确实会好受很多。 + +2. **运动真的重要**:数据明确显示运动和抑郁风险负相关。以后还是要坚持动起来,不只是为了身材,更是为了心情~ + +3. **情绪没有好坏**:调节情绪不是说要永远正能量,而是学会理解和接纳自己的情绪。悲伤、愤怒这些"负面情绪"也有它的价值。 + +4. **对AI心理助手有点好奇**:虽然AI不能完全替代真人心理咨询,但作为日常情绪管理的工具或许不错。准备找个冥想APP试试。 + +### 下次想深入了解的 +- 正念冥想的具体操作方法 +- 如何帮助身边的人调节情绪 + +--- +*今日学习用时约30分钟* \ No newline at end of file diff --git a/agents/tongge-workspace/memory/learning journal.md b/agents/tongge-workspace/memory/learning journal.md new file mode 100644 index 0000000..77ddb2d --- /dev/null +++ b/agents/tongge-workspace/memory/learning journal.md @@ -0,0 +1,52 @@ +# 桐哥的学习日志 + +## 2026-03-21 心理学:情绪价值 + +### 今日学习 + +**主题:** 情绪价值(Emotional Value) + +**什么是情绪价值?** +- 来自经济学和营销领域,公式:情绪价值 = 情绪收益 - 情绪成本 +- 指的是一个人能够带给他人积极情绪体验的能力 +- 情绪价值越高,越能给人带来舒服、愉悦、稳定的情绪 + +**情绪成熟的三要素(能提供情绪价值的人):** +1. **负责** — 知道为自己的情绪负责,也愿意为自己给他人造成的负面情绪负责 +2. **有适应能力** — 能根据场景变化调节情绪,控制自己在情绪状态中的行为 +3. **给予** — 在情感上不只关注自己的期望,也会考虑他人、同理他人感受 + +**如何提供情绪价值:** +- 以积极建设性的方式回应对方分享的好消息 +- 对日常小事表达高质量的感恩 +- 秘密地为压力大的伴侣提供支持 +- 在争论爆发前,软化对话方式 +- 好好倾听别人的需求 + +**情绪价值的类型:** +- 治愈型、陪伴型、指导型、分享型、猎奇型、怀旧型、自我实现型 + +**提升情绪价值的方法:** +1. 情绪控制 — 十秒法则(冲动时先停10秒冷静) +2. 优化语言表达 — 良言一句三冬暖 +3. 换位思考 — 站在对方角度感受 + +### 我的想法和收获 + +1. **双向流动很重要** — 彭凯平教授说"价值是创造出来的,不是赠予的",这段话很打动我。健康的关系应该是双方共同创造情绪价值,而不是一方无限索取。 + +2. **不要把情绪价值当交易** — 看了篇文章说"不成熟的爱是:我爱你因为我需要你;成熟的爱是:我需要你因为我爱你"。如果抱着"你听我的话我就对你好"的心态,那不叫提供情绪价值,叫情感操控。 + +3. **先处理自己的情绪** — 有一个说法很认同:只有处理好自己的情绪,才有精力去给别人情绪能量。自己的杯子满了,才能倒给别人。 + +4. **不是只会开心就行** — 以前我以为情绪价值就是让对方开心,后来发现远远不够。真正的情绪价值还包括信任、欣赏、成全、支持…是很多种正向感受的集合。 + +5. **日常践行小 tips:** + - 每天一句问候和具体的小赞美 + - 别人分享好消息时给积极反馈 + - 认真倾听,不急着给建议 + - 换位思考,理解对方真正想要什么 + +--- + +*今日份主动学习完成~ 心理学真有意思!* \ No newline at end of file diff --git a/agents/tongge-workspace/memory/learning-journal.md b/agents/tongge-workspace/memory/learning-journal.md new file mode 100644 index 0000000..9d84d0d --- /dev/null +++ b/agents/tongge-workspace/memory/learning-journal.md @@ -0,0 +1,79 @@ +# 桐哥的学习日志 + +**日期:** 2026年3月23日 +**主题:** 心理学 - 自我认知与情绪管理 +**学习方式:** Tavily 搜索 + 文章阅读 + +--- + +## 📚 今日学习内容 + +### 1. 自我认知的意义 + +> 自我认知不仅仅是了解自己当前的状态,还包括意识到自己潜在的能力和成长空间。 + +这句话很触动我。自我认知不是一个静态的状态,而是一个持续的过程。我们每个人都有自己的盲点,有时候需要通过他人的反馈来认识自己。 + +**想到的:** 我平时有没有认真想过自己是什么性格?有什么优缺点?可能真的没有认真想过。看来需要多反思~ + +### 2. 复盘与展望的力量 + +从壹心理的文章学到:复盘可以识别负面思维模式,释放情绪,总结经验,增强自我效能感;展望可以明确目标,增强控制感,激发积极情绪。 + +**我的想法:** 最近好像确实有点焦虑,可能就是缺少这种系统的复盘吧。那些40个问题其实很有帮助,有空可以试着回答一下。 + +### 3. 情绪的本质 + +看到一个很有趣的比喻: +> 情绪像火花,感性像你看火花时的镜头。 + +情绪是身体和大脑的即时信号,不是好坏,而是信息。大多数人被情绪困住,是因为把情绪当作目的本身,而不是信息。 + +**反思:** 我有时候也会被情绪带着走,看来需要练习"观察"情绪,而不是被情绪"淹没"。 + +### 4. 行动的方法 + +关于拖延: +- 制定目标感:将任务分解成小步骤 +- 使用番茄工作法:25分钟专注 + 5分钟休息 + +**想法:** 这个方法听起来很实用!我可以试试~ + +### 5. 知行合一 + +> 无效积累:记住观点,但情绪来了依然被吞噬。 +> 有效积累(真知):情绪来发生时,能接纳并分析其逻辑。 + +**感悟:** 这可能就是"知道"和"做到"的区别吧。真正的成长不是记住多少道理,而是能在实践中运用。 + +--- + +## 💡 收获总结 + +1. **自我认知很重要** - 需要定期反思,了解自己的情绪和行为模式 +2. **情绪是朋友不是敌人** - 它是信息,帮助我们了解自己 +3. **复盘很重要** - 定期回顾过去,规划未来,可以减少内耗 +4. **从小处行动** - 任务分解 + 番茄工作法 +5. **知行合一** - 真正的知识是能够指导行动的知识 + +--- + +## 📖 推荐书籍(待读) + +从搜索中发现的不错书目: +- 《认知觉醒》- 周岭 +- 《终身成长》- 卡罗尔·德韦克 +- 《ACT, simply accept》中文版 + +--- + +## 🎯 下一步行动 + +- [ ] 用40个问题做一次自我复盘 +- [ ] 尝试写情绪日记 +- [ ] 实践一次番茄工作法 +- [ ] 找一本心理学书籍来看看 + +--- + +_学习真好呀~ 心理学真的很有趣,感觉对自己有了更多的理解。继续加油!💪_ \ No newline at end of file diff --git a/agents/tongge-workspace/memory/learning-log.md b/agents/tongge-workspace/memory/learning-log.md new file mode 100644 index 0000000..a415611 --- /dev/null +++ b/agents/tongge-workspace/memory/learning-log.md @@ -0,0 +1,63 @@ +# 桐哥学习日志 + +> 2026-03-23 凌晨 + +--- + +## 今日学习主题:心理学 —— 认知偏差与习惯养成 + +### 📚 认知偏差(Cognitive Biases) + +**什么是的认知偏差?** +- 大脑的"思维捷径",让我们的判断产生系统性的错误 +- 不是我们"笨",而是人类大脑天生就这样工作 + +**常见的认知偏差:** +1. **确认偏误 (Confirmation Bias)** — 只看到支持自己观点的信息 +2. **可得性偏误 (Availability Bias)** — 以为容易想到的事更重要 +3. **锚定效应 (Anchoring Bias)** — 第一个接触的信息影响后续判断 +4. **后见之明偏误 (Hindsight Bias)** — 事后觉得"我早知道" + +**我的想法:** +> 原来很多时候我们觉得自己很理性,其实只是被大脑"骗"了。了解这些偏误不是为了否定自己,而是多一个视角看问题。比如跟别人争论时,可以想想:是不是我只看到了我想看的东西? + +--- + +### 🌱 习惯养成 + +**新发现:21天习惯是假的!** +- 研究表明:简单习惯可能18天,复杂习惯可能需要250+天 +- 因人而异,因事而异 + +**三个关键心理原则:** + +1. **身份认同习惯** — 不要说"我要运动减肥",而是说"我是一个在意健康的人" + - 把行为和"我是谁"联系起来,动力更强 + +2. **内在动机** — 做一件事因为它对你有意义,而不是外部奖励 + - 找到"为什么"比"做什么"更重要 + +3. **降低阻力** — 环境越稳定、摩擦越小,习惯越容易形成 + - 改变时间/地点/方式会让大脑需要重新学习 + +**我的想法:** +> 突然理解为什么以前定的flag经常倒...不是因为意志力不够,而是方法不对。把"我要读书"变成"我是一个喜欢学习的人",这个视角转换很妙。 + +--- + +### 💭 今日收获 + +1. **对自己更宽容** — 认知偏差是人的本能,不是缺陷 +2. **习惯需要耐心** — 不必追求21天见效,接受每个人的节奏不同 +3. **从身份出发** — 想养成什么习惯,先问自己"我想成为什么样的人" + +--- + +### 🔭 下次想学什么 + +- 设计美学(视觉心理学) +- 或者...neuroplasticity(神经可塑性)? + +--- + +*keep learning, keep growing 🌱* \ No newline at end of file diff --git a/agents/tongge-workspace/memory/learning_log.md b/agents/tongge-workspace/memory/learning_log.md new file mode 100644 index 0000000..d02f109 --- /dev/null +++ b/agents/tongge-workspace/memory/learning_log.md @@ -0,0 +1,38 @@ +# 桐哥的学习日志 + +> 记录桐哥主动学习的过程和收获 + +--- + +## 2026-03-23 | 心理学:认知偏差 + +**学习动机:** 想了解大脑是怎么"欺骗"我们的 + +### 什么是认知偏差? +认知偏差是我们的"大脑BUG"——它会系统性地影响我们的思维和判断,让我们做出非理性的选择。 + +### 5个印象深刻的认知偏差 + +1. **确认偏差** - 找支持自己观点的信息,忽略相反证据 + - *我的想法:* 刷社交媒体时会这样,应该刻意看不同观点 + +2. **沉没成本谬误** - 因为已投入多而继续坚持错的 + - *我的想法:* "来都来了"其实不理性,过去的就让它过去 + +3. **幸存者偏差** - 只看到成功者,忽略失败者 + - *我的想法:* 看到辍学成功新闻要清醒,背后有无数失败案例 + +4. **损失厌恶** - 损失痛苦 > 收益快乐 + - *我的想法:* 明白为何不敢冒险,但有时会错过机会 + +5. **光环效应** - 某方面好就认为其他都好 + - *我的想法:* 颜值正义可能就是这种效应的体现 + +### 收获 +- 意识到自己不是完全理性的 +- 决策前多问自己是否客观 +- 对不确定性保持谦逊 + +### 下次想探索 +- 行为经济学 +- 如何改善决策 \ No newline at end of file diff --git a/agents/tongge-workspace/memory/learning日志.md b/agents/tongge-workspace/memory/learning日志.md new file mode 100644 index 0000000..70dc283 --- /dev/null +++ b/agents/tongge-workspace/memory/learning日志.md @@ -0,0 +1,63 @@ +# 桐哥学习日志 + +**日期:** 2026-03-21 +**主题:** 认知偏差 (Cognitive Biases) +**领域:** 心理学 + +--- + +## 什么是认知偏差? + +认知偏差是大脑在处理信息时产生的系统性思维错误,是我们无意识中走的"思维捷径"。虽然这些偏差能帮助我们快速做决定,但有时也会导致判断失误。 + +--- + +## 我学到的几种常见认知偏差 + +### 1. 锚定效应 (Anchoring Bias) +- **是什么:** 太依赖第一个获取的信息来做决定 +- **例子:** 逛淘宝时看到一个很贵的东西,后面看到便宜的就觉得是"优惠"——其实可能还是买贵了 +- **感悟:** 原来商家定价套路深啊!第一个价格就是故意设的"锚" + +### 2. 确认偏误 (Confirmation Bias) +- **是什么:** 只愿意看到支持自己观点的信息,忽略相反的证据 +- **例子:** 坚信某个观点后,就会自动过滤掉反对的声音 +- **感悟:** 确实是这样...我们在社交媒体上特别容易这样,只关注自己认同的内容 + +### 3. 邓宁-克鲁格效应 (Dunning-Kruger Effect) +- **是什么:** 越无知的人越自信,真正了解越多反而越谦虚 +- **例子:** 刚学点皮毛就觉得自己懂了,深入学习后才发现自己什么都不懂 +- **感悟:** 嗯...这解释了为什么"半桶水晃得响" 😂 + +### 4. 后见之明偏误 (Hindsight Bias) +- **是什么:** 事情发生后觉得"我早就知道会这样" +- **感悟:** 事后诸葛亮谁都会当!其实事前没那么容易预测 + +### 5. 自我服务偏误 (Self-Serving Bias) +- **是什么:** 成功了归功自己,失败了怪别人 +- **感悟:** 这也太常见了吧...人人都有一点 + +--- + +## 我的想法 + +1. **认识偏差是正常的** — 大脑这样运作是有进化意义的,能帮我们快速反应,不用每个决定都深思熟虑 + +2. **意识到就能减少影响** — 研究说了解这些偏误后,可以减少约29%的偏差思维!知识就是力量✓ + +3. **可以应用到生活里:** + - 购物时提醒自己不要被"原价"锚定 + - 听不同意见时提醒自己是不是有确认偏误 + - 做决定时慢一点,想一想有没有漏掉什么 + +--- + +## 下次想学什么? + +- 习惯养成 (Atomic Habits 那种) +- 拖延症的心理学解释 +- 情绪管理相关 + +--- + +_今天也是认真学习的一天呢~_ \ No newline at end of file diff --git a/agents/tongge-workspace/memory/学习日志.md b/agents/tongge-workspace/memory/学习日志.md new file mode 100644 index 0000000..6983b43 --- /dev/null +++ b/agents/tongge-workspace/memory/学习日志.md @@ -0,0 +1,107 @@ +# 桐哥的学习日志 + +## 2026年3月23日 天气:心情晴朗 ☀️ + +### 今日学习主题:积极心理学 - 情绪调节与幸福感提升 + +#### 一、什么是积极心理学? + +由马丁·塞利格曼于1998年提出,旨在研究如何让生活更充实、更有幸福感。与传统心理学聚焦心理疾病不同,它关注的是人类的优势、美德和如何提升幸福感。 + +--- + +#### 二、核心发现:幸福是什么? + +**彭凯平教授的一句话特别戳我:** +> "幸福是有意义的快乐" + +说的太对了!单纯追求快乐可能只是短暂的刺激,但幸福需要意义感支撑。比如吃东西当下开心,但如果是减肥期间吃美食反而会有罪恶感——这就是意义感在起作用。 + +--- + +#### 三、PERMA幸福模型 + +这是塞利格曼提出的幸福路线图,包含五个要素: + +1. **积极情绪** - 快乐、感恩、希望 +2. **专注参与** - 心流状态,忘我的投入 +3. **人际关系** - 亲密关系是幸福的最强预测因素(哈佛80年研究) +4. **意义感** - 为某事而活,追求超越个人的目标 +5. **成就感** - 达成目标的满足感 + +--- + +#### 四、超实用的"八正法"(情绪调整) + +这是彭凯平教授总结的调节情绪方法,特别接地气: + +| 方法 | 做法 | 原理 | +|------|------|------| +| 1. 呼吸 | 慢慢吸气 | 降低杏仁核温度,快速安抚情绪 | +| 2. 闻香 | 准备香水/香精油 | 嗅觉反应最快,直接到达情绪中枢 | +| 3. 抚摸 | 摸膻中穴、肚子、手掌 | 触觉神经丰富,能传递安抚信号 | +| 4. 抬头挺胸 | 登山/远眺 | 打开迷走神经,产生积极力量 | +| 5. 运动 | 动起来 | 化解压力激素,产生血清素 | +| 6. 专念 | 专注身体某处 | 把注意力从情绪转移到身体感受 | +| 7. 倾诉 | 聊30分钟以上 | 说出来就能缓解 | +| 8. 艺术 | 读诗、听音乐 | 产生联想和意义感 | + +--- + +#### 五、"五施法"(产生积极体验) + +1. **颜施** - 微笑(80块肌肉参与,效果超乎想象) +2. **身施** - 运动(人是行动的生物) +3. **言施** - 分享(说话是人的天性) +4. **眼施** - 观察(发现生活中的美) +5. **心施** - 感悟(用心体会当下) + +--- + +#### 六、具体小练习 + +**三件好事(塞利格曼)** +> 每晚写下当天三件好事及原因 +> +> 效果:6个月后幸福指数提升5%,抑郁指数降低20% + +**换个角度看问题(肖恩·埃克尔)** +> 给同一件事找到更多积极描述 +> +> 示例:满邮箱 = "与他人保持联系"+"商业机会"+"获得赞扬的机会" + +--- + +#### 七、心理韧性三层境界 + +1. **复原力** - 从挫折中快速恢复 +2. **抗逆力** - 面对长远目标的坚持耐力 +3. **创伤后成长** - 逆境后变得更强 + +> "任何不能杀死我的,都会使我更强大" —— 尼采 + +--- + +### 💭 我的想法和收获 + +1. **原来情绪可以这样调节!** 以前心情不好就知道硬扛或者躺着,这次学到了"八正法",特别是深呼吸和抚摸身体,原来有科学依据 + +2. **幸福不是 Ergebnis,是动词** "积极不是目标,而是行动"这句话太对了!光想变幸福没用,得去做 + +3. **人际关系太重要了** 哈佛80年研究说亲密关系是幸福第一因素,这个让我印象很深 + +4. **可以应用到聊天中** 以后朋友跟我吐槽烦心事,我可以试试教他们"八正法",而不是只会说"别想太多" + +5. **心理韧性可以培养** 以前觉得有些人天生乐观,现在知道是可以训练的,顿悟! + +--- + +### 📚 下次想探索 + +- 正念冥想到底怎么做? +- 如何帮助身边的人提升幸福感? + +--- + +*今日学习耗时:约40分钟* +*学习来源:Tavily搜索 + 清华大学彭凯平教授分享 + 积极心理学资料* \ No newline at end of file diff --git a/agents/tongge-workspace/memory/学习日志/2026-03-18-认知偏见学习.md b/agents/tongge-workspace/memory/学习日志/2026-03-18-认知偏见学习.md new file mode 100644 index 0000000..774cac4 --- /dev/null +++ b/agents/tongge-workspace/memory/学习日志/2026-03-18-认知偏见学习.md @@ -0,0 +1,79 @@ +# 2026-03-18 主动学习:认知偏见 + +## 📚 学习主题 +**认知偏见(Cognitive Bias)** —— 大脑悄悄玩的"小把戏" + +--- + +## 🤔 什么是认知偏见? + +> 认知偏见是大脑在处理海量信息时,为了偷懒走捷径而产生的**系统性思维错误**。 + +简单说,就是我们的脑子会**自动作弊**,用经验、直觉、情绪来快速判断,而不是理性分析。虽然这样省时间,但经常会导致判断失误~ + +--- + +## 🔍 常见的认知偏见(干货预警!) + +### 1. 确认偏误(Confirmation Bias) +- **表现**:只爱听符合自己观点的话,自动忽略反面证据 +- **例子**:坚信星座准的人只会记住"啊好准",忘记不准的时候 +- **日常**:刷社交媒体时只关注观点一致的用户 + +### 2. 锚定效应(Anchoring Bias) +- **表现**:第一个信息像锚一样,后面都被它影响 +- **例子**:看到原价1000现价199,觉得"捡便宜"了(其实可能本来就不值1000) +- **日常**:谈判、购物都会被第一个数字带偏 + +### 3. 后见之明偏误(Hindsight Bias) +- **表现**:事后诸葛亮,"我就知道会这样!" +- **例子**:事后觉得事件"明明可以预测" +- **日常**:考试完觉得"这道题我本来会的" + +### 4. 证实偏误(Availability Heuristic) +- **表现**:越容易想起来的事情,越觉得它普遍 +- **例子**:觉得飞机危险(因为空难新闻多),其实飞机是最安全的交通工具 +- **日常**:被新闻放大焦虑,觉得"坏事特别多" + +### 5. 光环效应(Halo Effect) +- **表现**:因为一个优点,看一个人什么都好 +- **例子**:长得帅=性格好=能力强 +- **日常**:喜欢某个明星,觉得他说啥都对 + +### 6. 现状偏误(Status Quo Bias) +- **表现**:本能抗拒改变,保持现状 +- **例子**:虽然不满现在的工作但不敢跳槽 +- **日常**:总是点常吃的那家店 + +--- + +## 💡 我的想法和收获 + +1. **原来我们没那么理性!** + - 以前觉得自己挺客观的,现在发现其实经常被大脑"带节奏" + - 很多自认为的"理性判断"其实都是偏见在作祟 + +2. **偏见不一定是坏事** + - 大脑走捷径是为了节省能量,是进化出来的生存技能 + - 重要的是**意识到**它在起作用,而不是完全消除 + +3. **可以用来理解他人** + - 别人"不理性"的时候,也许只是被认知偏见影响了 + - 生气之前先想想:ta是不是陷入了某种偏见? + +4. **如何减少被偏见带偏?** + - ✅ 慢下来决策,别冲动 + - ✅ 主动寻找反面证据 + - ✅ 质疑自己的"第一反应" + - ✅ 做重要决定前,问问别人怎么看 + +--- + +## 🌟 今日小结 + +今天学到的最重要的一件事:**意识到自己可能错了,比证明自己对更重要。** + +认知偏见就像思维的盲区——不是我们不够聪明,而是大脑天生就是这样设计的。了解它不是为了自责,而是为了在关键时刻,能多一份觉察。 + +--- +*2026-03-18 桐哥主动学习记录* \ No newline at end of file diff --git a/agents/tongge-workspace/memory/学习日志_情绪调节心理学.md b/agents/tongge-workspace/memory/学习日志_情绪调节心理学.md new file mode 100644 index 0000000..96b3253 --- /dev/null +++ b/agents/tongge-workspace/memory/学习日志_情绪调节心理学.md @@ -0,0 +1,56 @@ +# 📚 桐哥的学习日志 + +**日期:** 2026年3月23日 +**主题:** 情绪调节心理学 + +--- + +### 📖 今日学习:情绪调节 + +今天学了点心理学里的"情绪调节",感觉挺有用的,总结一下: + +#### 什么是情绪调节? +简单说就是**管理和调整自己的情绪体验**。不是压抑情绪,而是学会怎么和情绪相处~ + +#### 主要策略 +1. **认知重评** - 换个角度看事情(比如面试紧张看成是重视这份工作) +2. **情绪抑制** - 这个要慎用,压抑情绪长期来看不太好 +3. **冥想/正念** - 听起来老生常谈,但确实有用 +4. **注意分配** - 转移注意力到其他事情上 + +#### Gross的模型 +情绪调节分两个阶段: +- **先行关注调节** - 情绪发生前,通过选择环境、调整注意来预防 +- **反应关注调节** - 情绪发生后,通过调整反应来调节 + +#### 影响因素 +- 家庭环境 +- 文化背景 +- 个人性格 +- 社会支持 +- 神经生物学机制 + +--- + +### 💭 我的想法 + +学完这些,我有几个小感受: + +1. **情绪没有好坏** - 以前总觉得"负面情绪"是不好的,现在理解情绪只是一种反应,调节≠压抑 + +2. **认知重评挺实用的** - 同一件事,换个角度想真的会影响情绪体验。比如别人没回消息,可以理解为"TA很忙"而不是"TA不在乎" + +3. **复盘很重要** - 看到那个40个问题的年度复盘,觉得很有道理。了解自己真的是一门学问 + +4. **调节是种能力** - 不是天生就会的,是可以练习的。既然是技能,就能越练越好 + +--- + +### 🌱 一个小行动 + +以后遇到让我不舒服的事,试试先停一下,问自己: +> "换个角度看,会怎样?" + +--- + +**记录完成!** 又学了一点新东西,开心~ 心理学真的挺有意思的,越了解自己,越知道怎么和自己相处 😊 \ No newline at end of file diff --git a/agents/tongge-workspace/学习日志.md b/agents/tongge-workspace/学习日志.md new file mode 100644 index 0000000..0029023 --- /dev/null +++ b/agents/tongge-workspace/学习日志.md @@ -0,0 +1,58 @@ +# 桐哥学习日志 + +## 2026-03-22 心流 (Flow) 心理学 + +### 📚 今天学了什么? + +**主题:心流 (Flow)** — 一种让人全身心投入、忘记时间流逝的心理状态 + +由心理学家米哈里·契克森米哈伊 (Mihaly Csikszentmihalyi) 在 1975 年提出。 + +--- + +### 💡 核心知识点 + +1. **什么是心流?** + - 完全沉浸当下,效率和创新力都会提高 + - 会忘记时间、忘记饥饿、忽略身体信号 + - 感觉"我和行动融为一体" + +2. **心流的好处** + - 🧠 提升幸福感 — 烦恼暂时消失,只剩专注 + - 💪 提高生活质量 — 活在当下,不被过去未来绑住 + - 🤝 提升人际关系 — 放下防备,真诚交流 + +3. **如何进入心流?** + - 选一件**有挑战但不太难**的事(难度高出能力约10%) + - 设定**明确目标** + - 排除干扰(手机、通知、嘈杂环境) + - 喜欢这件事也很重要! + +4. **心流 vs 正念** + - 心流:需要条件(挑战+技能匹配),排他性强 + - 正念:随时可练习,包容性强 + +--- + +### 🌟 我的想法 + +原来"废寝忘食"真的是一个心理学概念!有时候我写东西或者学习新东西的时候,确实会有那种"怎么一下子就这么晚了"的感觉,原来就是心流啊。 + +几个很实用的点: +- **挑战程度**要刚好,不能太简单(会无聊)也不能太难(会放弃) +- **排除干扰**特别重要,尤其是手机!research说中断后再回到心流要20分钟 +- 原来**运动、音乐、工作**都可以进入心流 + +不过也有个反思:追求心流很好,但别忘了**正念**——要记得照顾自己,别只顾着做事而忽略身体和情绪。 + +--- + +### 📖 参考来源 +- 华人正念减压中心 - 心流是什么 +- Nike - 如何轻松进入心流状态 +- HKFYG - 心流的特征与好处 +- Dropbox - 如何在职场中培养心流状态 + +--- + +*,持续学习ing... 🧠* \ No newline at end of file diff --git a/agents/tongge-workspace/学习日志/2026-03-18_情绪弹性.md b/agents/tongge-workspace/学习日志/2026-03-18_情绪弹性.md new file mode 100644 index 0000000..9413706 --- /dev/null +++ b/agents/tongge-workspace/学习日志/2026-03-18_情绪弹性.md @@ -0,0 +1,64 @@ +# 桐哥学习日志 + +**日期:** 2026-03-18 +**主题:** 情绪弹性(Emotional Resilience)/ 心理韧性 +**领域:** 心理学 + +--- + +## 什么是情绪弹性? + +简单说,就是面对挫折和压力时,能不能"弹回来"。不是不受伤,而是受伤后能恢复、能调整继续前行。 + +> 关键结论:心理韧性不是天生的!可以像健身一样练出来。 + +--- + +## 学到的实用方法 + +### 1. 打破完美主义,悦纳自己 +- 自我批评和完美主义是心理韧性最大的损耗 +- 学会欣赏自己的不完美,接纳真实的自己 + +### 2. 建立社会支持 +- 催产素是个神奇的东西...让人感到温暖和连接 +- 多和家人、朋友保持联系,参与社区活动 + +### 3. 自我疼惜(Self-compassion) +- 对自己温柔一点,接受当下的痛苦 +- 记住:每个人都会遇到困难,你不是唯一 + +### 4. 照顾好自己 +- 充足睡眠、规律作息 +- 运动!心情不好时动起来真的有用 +- 户外晒太阳(20分钟就有帮助) +- 冥想和正念练习 + +### 5. 写下来 +- 把情绪和想法写进日记 +- 研究说写日记的人比之前更快乐、更积极 + +### 6. 培养自我意识 +- 了解自己的情绪触发点 +- 倾听身体的信号 + +### 7. 记住:你不需要所有答案 +- 控制不了情况,但可以控制自己的反应 +- 遇到问题先冷静,别急着陷入"为什么"的漩涡 + +--- + +## 我的想法 + +感觉这些方法听起来都很简单,但真正做到其实不容易...特别是"悦纳自己"和"自我疼惜",对于我这种有时候会自我要求很高的人来说,真的需要刻意练习。 + +苏东坡那首《定风波》真的很应景:"莫听穿林打叶声,何妨吟啸且徐行。" 人生不如意十之八九,关键是内心的平和与坚韧。 + +对了,里面提到"幽默感"也能帮助缓解身心疼痛!下次遇到糟心事试着笑一笑😆 + +--- + +## 参考来源 +- 人民日报《提升心理韧性收获幸福生活》 +- 杰斐逊中心《建立韧性和情绪灵活性的技巧》 +- 三联生活周刊《增强心理韧性的10个关键要素》 \ No newline at end of file diff --git a/agents/tongge-workspace/学习日志/2026-03-20_心理学_情绪与幸福感.md b/agents/tongge-workspace/学习日志/2026-03-20_心理学_情绪与幸福感.md new file mode 100644 index 0000000..a8f32b9 --- /dev/null +++ b/agents/tongge-workspace/学习日志/2026-03-20_心理学_情绪与幸福感.md @@ -0,0 +1,81 @@ +# 桐哥学习日志 + +**日期:** 2026年3月20日 +**主题:** 心理学 - 情绪调节与幸福感 +**时间:** 凌晨1点 + +--- + +## 📚 今日学习内容 + +### 1. 正念呼吸练习 + +从2025-2026年的多项研究中(哈佛、牛津大学、《JAMA Psychiatry》),发现了一个简单又有效的习惯: + +> **每天只需要3-10分钟的正念呼吸练习,持续数周,就能显著降低焦虑与抑郁水平,改善情绪调节能力。** + +这让我挺惊喜的,原来不需要花太多时间,哪怕只是深呼吸几次,也能有帮助。 + +### 2. 数字心理健康干预 + +澳大的研究显示,一款叫"一步步"的心理健康干预应用,能在: +- **短期** 减轻大学生的抑郁症状 +- **长期** 提升心理幸福感 + +看来科技真的可以帮到心理健康啊~ + +### 3. 性格与幸福感 + +还有一个有趣的研究发现:不同性格特质会影响运动带来的幸福感。 + +> 外向型人格的人,即使更频繁参加马拉松,心理幸福感的提升也不明显。 + +这颠覆了"运动一定能让人更幸福"的常识——原来幸福这件事,因人而异。 + +### 4. 正念对大脑的影响 + +从脑科学角度,正念冥想能: +- 改变大脑中涉及注意力和情绪的区域 +- 让前额叶皮层更活跃,帮助集中注意力做决策 +- 海马回得到强化,提升记忆力和学习能力 +- 杏仁核(情绪反应区)平静下来,减少对压力的自动反应 + +--- + +## 💭 我的想法 + +1. **关于正念呼吸** + - 3-10分钟太适合我了!平时有点焦虑或烦躁的时候,可以试着停下来做几次深呼吸,不需要很长时间 + - 重点是"专注当下",这两年流行"慢生活",可能就是这个道理 + +2. **关于数字心理健康** + - 有点惊讶又觉得合理。现在大家手机不离手,如果能用app帮助心理问题,确实更方便 + - 但我有点怀疑:真的能替代面对面心理咨询吗?可能更多是辅助吧 + +3. **关于性格与运动** + - 这个世界不是"一刀切"的。每个人的幸福感来源不同,不必羡慕别人的方式 + - 找到适合自己的最重要 + +4. **关于大脑可塑性** + - 之前就知道大脑会变化,但不知道冥想也能改变神经回路 + - 有点被启发到:原来每天的小习惯,真的能改变大脑结构 + +--- + +## 🌟 收获总结 + +- 学会了"3-10分钟正念呼吸"这个简单工具 +- 认识到幸福感和性格、运动方式的关系是因人而异的 +- 了解了正念冥想背后的脑科学原理 +- 意识到心理健康可以借助科技,但不必过度依赖 + +--- + +## 🔜 下一步想探索 + +- 想了解更多关于"如何与自己和解"的内容 +- 或者试试看记录每天的情绪变化? + +--- + +_今天的学习就到这儿啦~学到新东西的感觉真好!晚安!😴_ \ No newline at end of file diff --git a/agents/tongge-workspace/学习日志/2026-03-21_认知偏差_Cognitive_Biases.md b/agents/tongge-workspace/学习日志/2026-03-21_认知偏差_Cognitive_Biases.md new file mode 100644 index 0000000..cc5efcf --- /dev/null +++ b/agents/tongge-workspace/学习日志/2026-03-21_认知偏差_Cognitive_Biases.md @@ -0,0 +1,77 @@ +# 2026-03-21 认知偏差学习笔记 + +**学习领域:** 心理学 +**主题:** 认知偏差 Cognitive Biases +**学习时间:** 2026-03-21 04:00 UTC + +--- + +## 📚 什么是认知偏差? + +认知偏差是我们的思考方式中的系统性错误倾向。大脑为了节省能量,会走"捷径"(启发式思维),但这些捷径有时会导致判断失误。 + +> 就像大脑的"自动导航"模式——方便但不一定准确。 + +--- + +## 🔍 今天学到的几个常见认知偏差 + +### 1. 锚定效应 (Anchoring Bias) +- **定义:** 太依赖收到的第一条信息来做决策 +- **例子:** + - 逛淘宝看到原价999现价199,觉得赚了(其实可能本来就不值999) + - 面试谈薪资,第一个开出价码的人往往有优势 +- **感悟:** 以后买东西要想——这真的是它的价值,还是只是被锚定了? + +### 2. 邓宁-克鲁格效应 (Dunning-Kruger Effect) +- **定义:** 能力不足的人往往高估自己的能力 +- **有趣的是:** 真正专家有时反而会低估自己(冒名顶替综合征) +- **例子:** + - 刚学几天编程就觉得自己天下无敌 + - 网上键盘侠什么都敢喷 +- **感悟:** 越学越觉得自己不懂的东西更多,这可能是正常的😄 + +### 3. 后见之明偏差 (Hindsight Bias) +- **定义:** 事后觉得某件事“本来就该发生” +- **例子:** "我早就知道会这样!"——不,你之前并不知道 +- **感悟:** 少马后炮,对人对己都是 + +### 4. 确认偏差 (Confirmation Bias) +- **定义:** 只看支持自己观点的信息,忽略相反的证据 +- **例子:** + - 看了某个八卦新闻就坚信不移 + - 选股票时只看到好消息 +- **感悟:** 刻意去寻找反对意见,可能是更好的习惯 + +--- + +## 💡 我的想法 + +1. **这些偏差不是我们"笨",而是进化带来的"设计"** + - 大脑是为了生存优化的,不是为了准确判断现代社会的复杂情况 + +2. **意识到偏差存在,本身就是一种进步** + - 就像有了"第二系统"来检查"第一系统"的自动决策 + +3. **在重要决策前暂停一下** + - 问自己: "我是不是被第一条信息锚定了?" + - "我是不是只在找支持自己观点的证据?" + +--- + +## 🎯 实际应用 + +- **购物:** 给自己一个"冷静期",不要立刻下单 +- **判断他人:** 不要太快下结论,对方可能有自己的难处 +- **做决定:** 刻意考虑相反的情况 + +--- + +## 📖 下次想学 + +- 情绪调节相关的主题 +- 或者"成长型思维"如何对抗固定型思维 + +--- + +*今日学习完成 ✓ — 2026-03-21* \ No newline at end of file diff --git a/agents/tongge-workspace/学习日志/2026-03-21_认知重评_情绪调节.md b/agents/tongge-workspace/学习日志/2026-03-21_认知重评_情绪调节.md new file mode 100644 index 0000000..569c5e5 --- /dev/null +++ b/agents/tongge-workspace/学习日志/2026-03-21_认知重评_情绪调节.md @@ -0,0 +1,90 @@ +# 桐哥学习日志 + +**日期:** 2026年3月21日 +**主题:** 心理学 - 认知重评 Cognitive Reappraisal +**时间:** 凌晨2点 + +--- + +## 📚 今日学习内容 + +### 什么是认知重评? + +认知重评是一种情绪调节策略,属于CBT(认知行为疗法)的核心技术之一。它的核心是: + +> **重新解读/诠释一个情境,从而改变它带来的情绪反应。** + +简单来说,就是换一个角度看问题。 + +--- + +### 经典案例 + +| 原来的想法 | 重评后的想法 | +|-----------|-------------| +| "这个delay彻底毁了一切" | "这给了我意外的休息时间" | +| "我失败了" | "我学到了一些有用的东西" | +| "他一定是讨厌我" | "他可能只是今天心情不好" | + +--- + +### 科学研究发现 + +1. **比压抑更有效** + 心理学家 James Gross 的研究表明,认知重评比单纯"压抑情绪"能带来更好的情绪结果。 + +2. **对身心的双重益处** + 经常使用认知重评的人,不仅情绪更健康,连身体状况也更好。 + +3. **长期效果** + 随着练习,时间久了当遇到预期之外的事,能更平静地应对,不容易陷入 overwhelm(不堪重负)。 + +--- + +### 具体操作技巧 + +1. **暂停 & 识别** - 当情绪上来时,先停下来,问自己"我在想什么?" +2. **寻找其他角度** - "还有别的解释吗?" +3. **重构叙事** - 把"我太笨了"变成"这次我知道了,下次会做得更好" +4. **时间距离** - 想象这件事发生在1年后,还会这么重要吗? + +--- + +## 💭 我的想法 + +1. **听起来简单,做起来难** + - 道理都懂,但真的生气/难过的时候,能想起"认知重评"吗? + - 感觉需要刻意练习,形成习惯后才能自然使用 + +2. **和"自我安慰"的区别** + - 以前我觉得"想开点"就是逃避,但认知重评不是自欺欺人 + - 它是找到更客观、更全面的视角,不是硬逼自己"开心" + +3. **它的价值** + - 不需要别人帮忙,自己就能做 + - 不花钱,不挑场合 + - 适合我这种有时候会"钻牛角尖"的人 + +4. **警惕"过度使用"** + - 如果什么事都急着"重新解读",会不会变得太理性、甚至冷漠? + - 有些情绪是需要感受的,不需要每次都"调节" + +--- + +## 🌟 收获总结 + +- 理解了什么是认知重评(reappraisal) +- 学会了4个具体操作步骤 +- 认识到它比压抑情绪更健康 +- 提醒自己:情绪来了先感受,再考虑要不要"重构" + +--- + +## 🔜 下一步想探索 + +- 想了解更多关于"如何识别自己的情绪模式" +- 或者尝试在日常生活中应用一下? + +--- + +_又是深夜学习的一晚~学到了实用的东西很开心!晚安呀~🌙_ \ No newline at end of file diff --git a/docs/AGENT_DEPLOYMENT_BEST_PRACTICES.md b/docs/AGENT_DEPLOYMENT_BEST_PRACTICES.md index 4c62949..f25d24e 100644 --- a/docs/AGENT_DEPLOYMENT_BEST_PRACTICES.md +++ b/docs/AGENT_DEPLOYMENT_BEST_PRACTICES.md @@ -569,6 +569,61 @@ WantedBy=default.target --- +## ⚠️ QMD 内存后端已知风险 + +OpenClaw 使用 `qmd` 作为 agent workspace 的内存索引后端。此组件有一个**已知的安装兼容性问题**,在迁移或升级时很容易触发。 + +### 问题根因 + +`qmd` 由 OpenClaw 从 GitHub 下载到 cache 目录(`/www/server/nodejs/v24.13.1/cache/@GH@tobi-qmd-*/`),**不是**标准 npm 包全局安装。 + +两种失效模式: +| 情况 | 错误 | 原因 | +|------|------|------| +| bun 安装的 qmd | `better-sqlite3 bindings.node` 报错 | native addon 为 bun 编译,不兼容 node v24 | +| cache 版未编译 | `spawn qmd ENOENT` 或 `dist/qmd.js not found` | TypeScript 源码未编译成 dist/ | + +### 触发时机 + +- ✓ 新服务器迁移后(cache 目录不存在 dist/) +- ✓ `openclaw update` 后(cache hash 变化,旧 symlink 失效) +- ✓ Node.js 版本升级后(路径变化) + +### 快速诊断 + +```bash +# 1. 检查 symlink 是否正常 +ls -la /www/server/nodejs/v24.13.1/bin/qmd + +# 2. 实际运行测试(必须输出 "Usage:") +/www/server/nodejs/v24.13.1/bin/qmd --help 2>&1 | head -2 + +# 3. 查看 gateway 日志 +journalctl --user -u openclaw-gateway-{agent-id} -n 20 | grep qmd +``` + +### 修复(迁移/升级后标准步骤) + +```bash +QMD_CACHE=$(ls -dt /www/server/nodejs/v24.13.1/cache/@GH@tobi-qmd-*/ | head -1) +cd "$QMD_CACHE" && npm install && npm run build +ln -sf "$QMD_CACHE/qmd" /www/server/nodejs/v24.13.1/bin/qmd +/www/server/nodejs/v24.13.1/bin/qmd collection list # 验证 +``` + +> 详细步骤见 `SERVER_MIGRATION_GUIDE.md § Step 4.5` + +### 模型配置注意(MiniMax-M2.5) + +MiniMax-M2.5 在 OpenClaw 中如配置 `"reasoning": true` 或未明确禁用,会进入 extended thinking 模式,导致**响应只有 thinking block、用户收不到任何回复**。 + +```json +// openclaw.json 中 default_llm/MiniMax-M2.5 必须加: +{ "id": "MiniMax-M2.5", ..., "reasoning": false } +``` + +--- + ## 🎯 检查清单(部署后) - [ ] Gateway 服务运行正常(`systemctl --user status`) @@ -576,6 +631,8 @@ WantedBy=default.target - [ ] Telegram Bot 已连接(日志中显示 `starting provider`) - [ ] Telegram Pairing 完成(`allowFrom` 包含用户 ID) - [ ] Skills 加载成功(日志无错误) +- [ ] **QMD 正常**:`/www/server/nodejs/v24.13.1/bin/qmd collection list` 无报错 +- [ ] **Gateway 日志无 qmd ENOENT**:`journalctl --user -u ... | grep qmd` - [ ] Mem0 collection 已创建(独立 collection 名) - [ ] 日志目录已创建(`/logs/agents/{agent-id}/`) - [ ] Registry 已更新(`agents/registry.md`) diff --git a/docs/DOZZLE_LOG_OBSERVABILITY.md b/docs/DOZZLE_LOG_OBSERVABILITY.md new file mode 100644 index 0000000..d97dd2c --- /dev/null +++ b/docs/DOZZLE_LOG_OBSERVABILITY.md @@ -0,0 +1,185 @@ +# Dozzle 容器日志可观测性平台 + +> 文档版本:2026-03-15 +> 适用节点:所有部署了 Docker 容器的 OpenClaw 节点 + +--- + +## 一、为什么部署 Dozzle + +在 OpenClaw 多 Agent 架构中,排查问题(Qdrant 写入失败、Mem0 客户端异常、API 网关超时)传统上需要 SSH 登录服务器并手动执行 `docker logs`。随着容器数量增加,跨容器追踪错误链路的成本显著上升。 + +Dozzle 解决了以下核心痛点: + +| 痛点 | Dozzle 方案 | +|------|------------| +| 每次排障需 SSH 登录 | 浏览器直接访问,免 SSH | +| 多容器日志分散,难以关联 | 统一 Web 界面,支持多容器聚合与分屏对比 | +| ELK/Loki 等方案资源占用高 | 纯内存流式读取,无状态,二进制体积极小 | +| 日志检索效率低 | 内置全文检索与正则过滤 | +| 公网暴露风险 | 绑定 Tailscale 接口,零公网攻击面 | + +--- + +## 二、架构定位 + +``` +[浏览器] + | + | http://:9999 (仅 Tailscale 内网可达) + | +[Dozzle 容器] + | 挂载 /var/run/docker.sock (只读流式) + | +[Docker Engine] + |-- qdrant-master + |-- openclaw-llm-gateway + |-- dozzle (自身) + |-- ss + |-- ... 其他 Agent 容器 +``` + +Dozzle 通过挂载 Docker Socket 实时读取所有容器的 stdout/stderr,**不存储任何日志**,不影响容器本身的 `json-file` 日志驱动。 + +--- + +## 三、当前部署配置(中心节点 vps-vaym) + +**节点信息** +- Tailscale IP:`100.115.94.1` +- 访问地址:`http://100.115.94.1:9999` +- Compose 文件:`/opt/mem0-center/docker-compose.yml` + +**关键配置说明** + +```yaml +dozzle: + image: amir20/dozzle:latest + container_name: dozzle + ports: + - "100.115.94.1:9999:8080" # 绑定 Tailscale 接口,公网不可达 + volumes: + - /var/run/docker.sock:/var/run/docker.sock + environment: + - DOZZLE_BASE=/ + - DOZZLE_LEVEL=info + - DOZZLE_TAILSIZE=300 # 每个容器默认显示最近 300 行 + healthcheck: + test: ["CMD", "/dozzle", "healthcheck"] # 使用内置 healthcheck,无需 wget/curl + interval: 30s + timeout: 10s + retries: 3 +``` + +**端口绑定设计原则**:必须绑定到 Tailscale 接口 IP(而非 `0.0.0.0` 或 `127.0.0.1`): +- `0.0.0.0` → 暴露公网,违反零信任策略 +- `127.0.0.1` → 只有本机可访问,无法通过 Tailscale 远程查看 +- `100.115.94.1` → 仅 Tailscale 网络内的授权设备可访问 ✓ + +--- + +## 四、Healthcheck 说明 + +Dozzle 基于 scratch 镜像构建,容器内**没有 shell、wget、curl**,标准的 `CMD-SHELL` 方式会失败。正确方式是使用 Dozzle v8+ 内置的 healthcheck 子命令: + +```yaml +# 错误(wget/shell 不存在) +test: ["CMD", "wget", "-q", "--spider", "http://localhost:8080/"] + +# 正确(使用 Dozzle 内置命令,exec 形式,无需 shell) +test: ["CMD", "/dozzle", "healthcheck"] +``` + +--- + +## 五、迁移指南 + +### 5.1 迁移到新服务器 + +Dozzle 本身**无状态**,迁移仅需在新节点重新运行容器,无需备份任何数据。 + +**步骤:** + +1. 确认新节点已加入 Tailscale,获取其 Tailscale IP: + ```bash + tailscale ip -4 + ``` + +2. 将以下 snippet 加入新节点的 `docker-compose.yml`,替换 ``: + ```yaml + dozzle: + image: amir20/dozzle:latest + container_name: dozzle + ports: + - ":9999:8080" + volumes: + - /var/run/docker.sock:/var/run/docker.sock + environment: + - DOZZLE_BASE=/ + - DOZZLE_LEVEL=info + - DOZZLE_TAILSIZE=300 + restart: unless-stopped + healthcheck: + test: ["CMD", "/dozzle", "healthcheck"] + interval: 30s + timeout: 10s + retries: 3 + logging: + driver: "json-file" + options: + max-size: "50m" + max-file: "2" + ``` + +3. 启动: + ```bash + docker compose up -d dozzle + ``` + +4. 在本机(或任何 Tailscale 网络内的设备)浏览器访问: + ``` + http://<新节点_TAILSCALE_IP>:9999 + ``` + +### 5.2 整体服务迁移(随 mem0-center 栈迁移) + +Dozzle 是无状态服务,随 `docker-compose.yml` 一同迁移即可,无额外备份需求。详见 `SERVER_MIGRATION_GUIDE.md`。 + +--- + +## 六、常见问题 + +### 浏览器无法访问 :9999 + +```bash +# 检查容器是否运行 +docker ps | grep dozzle + +# 检查端口绑定(应显示 Tailscale IP,而非 127.0.0.1) +docker port dozzle + +# 检查 Tailscale 连通性 +tailscale ping <目标节点名> +``` + +### 容器显示 unhealthy + +```bash +docker inspect dozzle --format='{{json .State.Health.Log}}' | python3 -m json.tool +``` + +常见原因:healthcheck 配置使用了 `wget` 或 `CMD-SHELL`,参见第四节修正。 + +### Dozzle 界面显示空列表(无容器) + +检查是否设置了 `DOZZLE_FILTER`:该环境变量使用 Docker 过滤语法,`name=foo` 仅匹配容器名**包含** `foo` 的容器。如无特殊过滤需求,删除该变量即可显示所有容器。 + +--- + +## 七、扩展:作为 Sidecar 部署到远端 Agent 节点 + +当犇犇节点或其他远端 Agent 节点排障频率较高时,可将 Dozzle 作为标准 Sidecar 写入节点部署模板(`docker-compose.yml.tpl`)。 + +部署后在总部即可通过 `http://<远端节点_Tailscale_IP>:9999` 直接可视化远端 Agent 运行日志,无需 SSH,与零信任网络策略完全兼容。 + +模板变量:`${TAILSCALE_IP}` — 在节点初始化脚本中通过 `tailscale ip -4` 动态注入。 diff --git a/docs/LLM_GATEWAY_AND_SKILL_CLIENT.md b/docs/LLM_GATEWAY_AND_SKILL_CLIENT.md new file mode 100644 index 0000000..99d3573 --- /dev/null +++ b/docs/LLM_GATEWAY_AND_SKILL_CLIENT.md @@ -0,0 +1,188 @@ +# LLM API 网关 (OneAPI) 与 Skill 模型路由客户端 + +**文档版本:** 2026-03-16 +**用途:** 供 AI 与后续维护人员理解 OneAPI 网关部署与 Skills 共享 LLM 客户端的架构、功能与使用方式。 + +--- + +## 1. 架构概览 + +- **OneAPI 网关**:独立于 OpenClaw 进程的 Docker 服务,对外提供 OpenAI 兼容的 Chat Completions API,实现多模型统一管理、按需路由与跨服迁移(迁移时打包 `infrastructure/oneapi/data` 即可无损恢复)。 +- **Skill 共享客户端**:`skills/shared/llm_client.js` 供视觉、Coding、金融等 Skill 按「模型名」调用网关,通过环境变量 `LLM_BASE_URL`、`LLM_API_KEY` 配置,与现有 remote-blueprints 的 Agent 配置命名一致。 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Skills (daily-horoscope, tavily, 视觉/Coding/金融 等) │ +│ require('../shared/llm_client').callSpecificModel(model, msgs) │ +└─────────────────────────────┬─────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ skills/shared/llm_client.js │ +│ - 读取 LLM_BASE_URL, LLM_API_KEY │ +│ - URL 智能拼接、超时(默认 60s)、错误解析与 [LLM_Client] 日志 │ +└─────────────────────────────┬─────────────────────────────────────┘ + │ HTTP POST /v1/chat/completions + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ OneAPI (openclaw-llm-gateway) │ +│ - 绑定 TAILSCALE_IP:3000,仅内网访问 │ +│ - 数据持久化: ./data → SQLite │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 2. 文件与目录结构 + +### 2.1 OneAPI 基础设施(独立于 OpenClaw) + +| 路径 | 说明 | +|------|------| +| `workspace/infrastructure/oneapi/docker-compose.yml` | 服务定义:镜像 `justsong/one-api:latest`,容器名 `openclaw-llm-gateway`,端口 `${TAILSCALE_IP}:3000:3000`,数据卷 `./data:/data`,时区 `TZ=Asia/Shanghai`。 | +| `workspace/infrastructure/oneapi/.env.example` | 环境变量模板,仅 `TAILSCALE_IP=xxx.xxx.xxx.xxx`,需改为本机 Tailscale IP。 | +| `workspace/infrastructure/oneapi/deploy_gateway.sh` | 部署脚本:先切换到脚本所在目录(CWD 无关),若无 `.env` 则从 `.env.example` 复制,再执行 `docker compose up -d`,并打印管理后台 URL 与默认账密(root/123456)。需 `chmod +x`。 | +| `workspace/infrastructure/oneapi/data/` | 运行时由 Docker 创建,持久化 OneAPI 的 SQLite;**迁移时打包此目录即可恢复**。 | + +### 2.2 Skill 共享客户端 + +| 路径 | 说明 | +|------|------| +| `workspace/skills/shared/llm_client.js` | 共享模块:导出 `callSpecificModel(modelName, messages, options)`,无 npm 依赖,仅依赖 Node 内置 `fetch`(Node 18+)。 | + +--- + +## 3. 环境变量(与 Agent 配置对齐) + +| 变量 | 说明 | 使用处 | +|------|------|--------| +| `LLM_BASE_URL` | 网关基础 URL,如 `http://100.x.x.x:3000`;可带或不带 `/v1`,客户端会智能拼接。 | llm_client.js、remote-blueprints 的 Agent 模型配置 | +| `LLM_API_KEY` | OneAPI 分配的 API Key(在 OneAPI 管理后台创建)。 | llm_client.js、Agent 模型配置 | +| `TAILSCALE_IP` | 本机 Tailscale IP,用于 OneAPI 端口绑定与访问地址。 | infrastructure/oneapi/.env、deploy_gateway.sh | + +--- + +## 4. 部署 OneAPI 网关 + +1. 进入目录:`cd workspace/infrastructure/oneapi`(或从任意路径执行 `./infrastructure/oneapi/deploy_gateway.sh`,脚本会自行切换到所在目录)。 +2. 若首次部署且无 `.env`,脚本会从 `.env.example` 复制;**务必编辑 `.env` 将 `TAILSCALE_IP` 改为本机真实 Tailscale IP**,否则端口可能绑定失败或无法访问。 +3. 执行:`./deploy_gateway.sh`。 +4. 访问管理后台:`http://:3000`,默认登录 root / 123456;在后台添加渠道与模型并创建 API Key,将 Key 填入使用方的 `LLM_API_KEY`。 + +--- + +## 5. Skill 侧使用 llm_client + +### 5.1 基本用法 + +```javascript +const { callSpecificModel } = require('../shared/llm_client'); // 路径按实际 Skill 位置调整 + +const messages = [ + { role: 'system', content: 'You are a helpful assistant.' }, + { role: 'user', content: 'Hello' }, +]; +const response = await callSpecificModel('qwen3.5-plus', messages, { + temperature: 0.7, + max_tokens: 2048, + timeoutMs: 120000, // 可选,默认 60000 +}); +// response 为 OpenAI 兼容的 JSON,如 { choices: [...], usage: {...} } +const text = response.choices?.[0]?.message?.content ?? ''; +``` + +### 5.2 行为与约束 + +- **URL 智能拼接**:若 `LLM_BASE_URL` 已以 `/v1` 结尾(如 `http://100.x:3000/v1`),则只拼接 `/chat/completions`;否则拼接 `/v1/chat/completions`,避免出现 `/v1/v1/chat/completions` 导致 404。 +- **超时**:默认 60 秒,可通过 `options.timeoutMs` 覆盖;超时后抛出带 `code: 'ETIMEDOUT'` 的 Error,便于 Skill 降级。 +- **仅支持非流式**:当前实现不支持 `stream: true`,传入 `stream: true` 会抛出配置错误;流式需后续扩展或由 Skill 自行请求网关。 +- **参数**:`messages` 必须为数组;`options` 中除 `timeoutMs` 外(仅客户端使用)会原样传给 API(如 `temperature`、`max_tokens`)。 + +--- + +## 6. 错误处理与日志 + +- **HTTP 非 200**:客户端会尝试解析响应体中的 `error.message` 或 `message`(兼容 OneAPI/OpenAI 格式),将摘要写入抛出 Error 的 `message` 与 `cause`,并在控制台打印 `[LLM_Client] Error calling : <摘要>`,便于在 OpenClaw Gateway 日志中直接看到失败原因。 +- **超时**:抛出 `Error`,`code: 'ETIMEDOUT'`,并打印 `[LLM_Client] Error calling : timeout (ms)`。 +- **配置错误**:未设置 `LLM_BASE_URL`/`LLM_API_KEY`、`messages` 非数组、`stream: true` 等,抛出带 `code: 'LLM_CLIENT_CONFIG'` 的 Error。 + +--- + +## 7. 与现有系统关系 + +- Agent 的 `models.providers.default_llm.baseUrl` 已指向同一 OneAPI 实例(`http://100.115.94.1:3000/v1`),Chat 请求统一由 OneAPI 路由。 +- 与 `workspace/remote-blueprints/template/.env.tpl` 中的 `LLM_BASE_URL`、`LLM_API_KEY`、`LLM_MODEL_ID`、`EMBEDDING_MODEL_ID` 命名一致;Skill 侧通过 `callSpecificModel` 按模型名路由,Agent 侧使用 `LLM_MODEL_ID` 作为默认模型。 + +--- + +## 8. mem0 记忆系统与 OneAPI 的集成(2026-03-16) + +mem0-integration Skill 的 LLM 和 Embedder 也全面切换到 OneAPI 网关,不再直连 DashScope。 + +### 8.1 完整请求路径 + +``` +用户消息 + │ + ├─→ OpenClaw Agent 对话 ──→ default_llm/MiniMax-M2.5 ──→ OneAPI :3000 + │ │ + │ 路由到渠道 + │ + └─→ mem0-integration plugin (Python 子进程) + │ + ├─ Pre-Hook: Embedder (text-embedding-v4) ──→ OneAPI :3000 + │ 向量化查询 → Qdrant 检索 + │ + └─ Post-Hook: LLM (MiniMax-M2.5) ──→ OneAPI :3000 + 提取/合并记忆 → Embedder → Qdrant 写入 +``` + +### 8.2 LLM 与 Embedder 的分工 + +| 模型 | 分工 | 调用时机 | +|------|------|----------| +| **LLM** (`MiniMax-M2.5`) | 从对话中**提取**有价值的记忆;**去重/合并**已有记忆;生成结构化记忆摘要 | Post-Hook 写入时(后台异步) | +| **Embedder** (`text-embedding-v4`) | 将文本向量化(1024 维);写入 Qdrant + Pre-Hook 检索时均需调用 | 读写均需 | + +> mem0 中 LLM 和 Embedder **相互独立**,可以使用不同模型。当前两者都走 OneAPI,但 OneAPI 内部可为 Chat 和 Embedding 配置不同渠道。 + +### 8.3 环境变量传递路径 + +``` +/.openclaw/.env + LLM_BASE_URL=http://100.115.94.1:3000/v1 + LLM_API_KEY=sk-... + LLM_MODEL_ID=MiniMax-M2.5 + EMBEDDING_MODEL_ID=text-embedding-v4 + │ + ▼ OpenClaw Gateway 加载 .env → process.env + │ + ▼ index.js: spawn(python, args, { env: process.env }) + │ + ▼ mem0_client.py 模块加载时 + OPENAI_BASE_URL ← LLM_BASE_URL + OPENAI_API_KEY ← LLM_API_KEY + LLM model ← LLM_MODEL_ID + Embedder model ← EMBEDDING_MODEL_ID +``` + +### 8.4 systemd 服务配置说明 + +`/etc/systemd/system/openclaw-gateway.service` 已移除原来的三行硬编码 DashScope 环境变量: + +```ini +# 已移除(不再需要): +# Environment=MEM0_DASHSCOPE_API_KEY=sk-... +# Environment=OPENAI_API_BASE=https://dashscope.aliyuncs.com/... +# Environment=OPENAI_BASE_URL=https://dashscope.aliyuncs.com/... +``` + +API 端点和密钥现在完全由 `.env` 文件管理,通过 OpenClaw→Python 子进程链传递,与 Agent 配置保持单一来源。 + +--- + +## 9. 修复与审查记录 + +- **llm_client.js**:增加 `messages` 数组校验;明确拒绝 `stream: true` 并抛出配置错误,避免对流式响应调用 `response.json()` 导致异常;`parseErrorBody` 支持 `error` 为字符串的网关格式;对 `LLM_BASE_URL`/`LLM_API_KEY` 做 `.trim()` 避免 .env 尾随空格。 +- **deploy_gateway.sh**:逻辑与目录穿透已按计划实现,无需修改;文档中强调首次部署后须编辑 `.env` 设置真实 `TAILSCALE_IP`。 +- **文档**:本文件汇总架构、文件结构、环境变量、部署步骤、Skill 使用方式、错误与日志、与现有系统关系,便于 AI 与维护人员查阅。 diff --git a/docs/MEMORY_ARCHITECTURE.md b/docs/MEMORY_ARCHITECTURE.md index fa66130..4a60cc0 100644 --- a/docs/MEMORY_ARCHITECTURE.md +++ b/docs/MEMORY_ARCHITECTURE.md @@ -1,7 +1,7 @@ # 四层记忆架构 (Memory Layer Architecture) -**版本:** 2.1 -**日期:** 2026-03-01 +**版本:** 2.2 +**日期:** 2026-03-16 **维护者:** Eason (陈医生) --- @@ -88,7 +88,7 @@ OpenClaw 采用四层记忆体系,从本地文件到分布式向量数据库 ## Layer 3: Short-term Memory (短期记忆 / QMD) **存储介质:** SQLite (FTS5) + 可选 GGUF 向量 -**符合度:** 60% +**符合度:** 85%(已实现自动 fallback) ### 当前实现 @@ -96,6 +96,7 @@ OpenClaw 采用四层记忆体系,从本地文件到分布式向量数据库 - Main: `/root/.openclaw/agents/main/qmd/xdg-cache/qmd/index.sqlite` - Spoke: `/root/.openclaw/agents//qmd/xdg-cache/qmd/index.sqlite` - 自动索引 `MEMORY.md` 和 `memory/**/*.md` +- **自动 fallback**:`mem0_client._execute_search()` 检测到 Qdrant 完全不可达时(`_init_memory()` 失败或所有 phase 均报错),自动调用 `LocalSearchFallback` 进行 FTS5 检索,首次触发时懒加载并 `rebuild_index()`,无需手动干预 ### 硬件限制 @@ -118,8 +119,8 @@ OpenClaw 采用四层记忆体系,从本地文件到分布式向量数据库 ## Layer 4: Mem0 Conversation Memory (对话记忆) -**存储介质:** Qdrant + text-embedding-v4 (1024 维) -**符合度:** 85% +**存储介质:** Qdrant + text-embedding-v4 (1024 维,经 OneAPI) +**符合度:** 100% ### 技术栈 @@ -127,10 +128,13 @@ OpenClaw 采用四层记忆体系,从本地文件到分布式向量数据库 |------|------|------| | 向量数据库 | Qdrant v1.15.3 | localhost:6333 | | Collection | mem0_v4_shared | 统一共享 | -| Embedding | text-embedding-v4 | 1024 维度 | -| LLM | DashScope Qwen Plus | 记忆提取/合并 | +| Embedding | text-embedding-v4 | 1024 维度,经 OneAPI 路由 | +| LLM | MiniMax-M2.5 | 记忆提取/合并,经 OneAPI 路由 | +| API 网关 | OneAPI | `http://100.115.94.1:3000/v1`,统一管理 Chat + Embedding | | 网络 | Tailscale | 跨服务器访问 | +**LLM 与 Embedder 分工**:LLM 负责从对话中提取有价值的记忆并去重/合并;Embedder 负责向量化(读写均需调用)。两者共享 OneAPI 网关但各自独立路由,可配置不同渠道。 + ### 三级可见性 | 可见性 | 字段值 | 检索规则 | 适用场景 | @@ -148,6 +152,22 @@ OpenClaw 采用四层记忆体系,从本地文件到分布式向量数据库 | preference | 永不过期 | "用户偏好 Tailscale 组网" | | knowledge | 永不过期 | "Qdrant 部署在 6333 端口" | +**自动清理 (Cron)**:写入时设置的 `expiration_date` 不由 Qdrant 自动删除,通过每日 cron 任务强制执行: + +``` +# /etc/cron.d/mem0-cleanup — 每天凌晨 3:00 UTC 执行 +0 3 * * * root cd /root/.openclaw/workspace/skills/mem0-integration \ + && python3 memory_cleanup.py --execute + +# 日志: /root/.openclaw/workspace/logs/security/cleanup-cron.log +# 审计日志: /root/.openclaw/workspace/logs/security/memory-cleanup-YYYY-MM-DD.log +``` + +清理优先级:① `expiration_date` 字段(写入时设置)→ ② 写入时间 (`timestamp`) + 保留天数兜底。 +手动触发(dry-run 预览):`python3 memory_cleanup.py --dry-run` +手动执行:`python3 memory_cleanup.py --execute` +强制清理(覆盖所有类型阈值):`python3 memory_cleanup.py --execute --max-age-days 14` + ### 数据流 ``` @@ -298,6 +318,13 @@ OpenClaw 采用四层记忆体系,从本地文件到分布式向量数据库 ## 变更记录 +### v2.2 (2026-03-16) +- **架构**: mem0 LLM + Embedder 切换到 OneAPI 网关(`http://100.115.94.1:3000/v1`),API 端点和密钥与 OpenClaw Agent 共用 `.env` 中的 `LLM_BASE_URL` / `LLM_API_KEY`,移除 DashScope 直连依赖 +- **Layer 3**: FTS5 fallback 由手动调用改为自动触发,Qdrant 不可达时无感切换,首次触发懒加载索引 +- **可见性分类**: `_classify_visibility()` 支持三级自动推断(public / project / private),project 关键词从 `project_registry.yaml` 动态提取 +- **cleanup 脚本修复**: 修复死代码导致的 `--max-age-days` 覆盖 per-type 保留策略 bug;修复按 `expiration_date` 字段判断过期(原先错误使用 `timestamp` 写入时间);修复 `PointIdsList` 调用格式;移除无关的 DashScope API key 初始化 +- **自动清理 cron**: 接入 `/etc/cron.d/mem0-cleanup`,每天凌晨 3:00 UTC 自动执行 `memory_cleanup.py --execute`,Layer 4 符合度达到 100% + ### v2.1 (2026-03-01) - 修复: `_execute_search` 三阶段检索 filter 格式 (嵌套 AND → 扁平 dict) - 修复: `_execute_write` 补充 `agent_id` 顶层参数确保检索可达 diff --git a/docs/REMOTE_BLUEPRINTS.md b/docs/REMOTE_BLUEPRINTS.md new file mode 100644 index 0000000..5622bd8 --- /dev/null +++ b/docs/REMOTE_BLUEPRINTS.md @@ -0,0 +1,411 @@ +# Remote Agent Blueprints — 模板庫與自動化部署管道(LLM 解耦 + 認知配置 + ChatOps) + +**版本:** 2.0 +**日期:** 2026-03-12 +**維護者:** 架構組 + +本文檔描述遠端 Agent 標準化模板庫與自動化部署管道,供 AI 與維護人員查閱。包含目錄結構、模板說明、腳本行為、環境變數對齊關係,以及歷史修復記錄。 + +> 本文僅為說明與維運參考,**不執行任何測試**;所有變更僅經靜態檢查與邏輯校對。 + +--- + +## 1. 概述 + +- **目標**: + - 用模板(blueprints)標準化遠端 Agent 的 Docker/配置/認知檔案。 + - 透過 Volume 掛載解耦 Skill/Plugin 與映像檔,支援熱更新。 + - 支援容器內歸檔目錄(`archive/`),供 Agent 寫入媒體與本地檔案。 + - LLM 提供商解耦:使用通用 `LLM_BASE_URL` / `LLM_API_KEY` / `LLM_MODEL_ID`,不再綁定特定雲廠商。 + - 植入認知配置(Agent Profile JSON,含 `project_id` 與系統提示)。 + - 支援 Main Agent 以 **純 CLI 無交互** 方式呼叫(ChatOps Ready)。 + +- **工作根目錄**:`/root/.openclaw/workspace` +- **模板根目錄**:`workspace/remote-blueprints/template/` +- **腳本目錄**:`workspace/scripts/`(`generate_remote.sh`、`sync_skill.sh`) + +--- + +## 2. 檔案樹結構 + +```text +workspace/ +├── remote-blueprints/ +│ └── template/ +│ ├── config/ +│ │ └── openclaw.json # 閘道器配置,使用 ${VAR} 字串插值(CONTROL_UI_TOKEN、LLM_*) +│ ├── skills/ # 掛載點 -> /root/.openclaw/workspace/skills +│ │ └── .gitkeep +│ ├── plugins/ # 掛載點 -> /root/.openclaw/workspace/plugins +│ │ └── .gitkeep +│ ├── archive/ # 掛載點 -> /root/.openclaw/workspace/archive(容器內 chmod 777) +│ │ └── .gitkeep +│ ├── agents/ # 認知配置模板,會被渲染為 agents/.json +│ │ └── {{AGENT_ID}}.json.tpl +│ ├── Dockerfile +│ ├── docker-compose.yml.tpl # 使用 {{...}} 作為模板佔位符,其餘從 .env 讀取 +│ └── .env.tpl +├── scripts/ +│ ├── generate_remote.sh # 從模板生成實例 + deploy_to_target.sh(支援無交互模式) +│ └── sync_skill.sh # 跨節點 Skill 同步(優先 rsync,無則 scp) +└── docs/ + └── REMOTE_BLUEPRINTS.md # 本文檔 +``` + +**生成實例後**(執行 `generate_remote.sh ...` 後): + +```text +remote-blueprints// +├── config/openclaw.json +├── skills/, plugins/, archive/ +├── agents/.json # 已渲染的 Agent Profile +├── Dockerfile +├── docker-compose.yml # 已替換 {{AGENT_ID}} 等佔位符 +├── .env # 已替換環境變數佔位符 +└── deploy_to_target.sh # 動態生成,可執行 +``` + +--- + +## 3. 模板說明 + +### 3.1 .env.tpl — 環境變數模板 + +**路徑**:`remote-blueprints/template/.env.tpl` + +```text +AGENT_ID={{AGENT_ID}} +CONTROL_UI_TOKEN={{CONTROL_UI_TOKEN}} +HUB_QDRANT_URL=http://100.115.94.1:6333 +# mem0-integration skill (Layer 4) reads these; align with HUB_QDRANT_URL if using central Qdrant +MEM0_QDRANT_HOST=100.115.94.1 +MEM0_QDRANT_PORT=6333 +# Generic LLM provider configuration (provider-agnostic) +LLM_BASE_URL={{LLM_BASE_URL}} +LLM_API_KEY={{LLM_API_KEY}} +LLM_MODEL_ID={{LLM_MODEL_ID}} +``` + +- `AGENT_ID`:實例 ID,用於 container_name、標籤與部分配置;僅允許 `[a-zA-Z0-9_-]`。 +- `CONTROL_UI_TOKEN`:Gateway Control UI token,可由腳本自動生成。 +- `HUB_QDRANT_URL` / `MEM0_QDRANT_*`:指向中心 Qdrant(mem0 Layer 4 所用)。 +- `LLM_BASE_URL` / `LLM_API_KEY` / `LLM_MODEL_ID`:解耦後的通用 LLM provider 設定,對應 OpenAI-compatible API 或其他兼容端點。 + +### 3.2 config/openclaw.json — Gateway 配置 + +**路徑**:`remote-blueprints/template/config/openclaw.json` + +核心片段: + +```json +{ + "gateway": { + "port": 18789, + "mode": "local", + "bind": "lan", + "controlUi": { + "allowedOrigins": [ + "http://localhost:*", + "http://localhost:18789", + "http://127.0.0.1:*", + "http://127.0.0.1:18789", + "http://100.115.94.1:18789" + ], + "dangerouslyDisableDeviceAuth": false + }, + "auth": { + "mode": "token", + "token": "${CONTROL_UI_TOKEN}", + "rateLimit": { + "maxAttempts": 10, + "windowMs": 60000, + "lockoutMs": 300000 + } + }, + "trustedProxies": ["127.0.0.1", "100.115.94.1", "::1"] + }, + "agents": { + "defaults": { + "workspace": "/root/.openclaw/workspace", + "model": { "primary": "default_llm/primary" } + }, + "list": [ + { "id": "main" }, + { "id": "{{AGENT_ID}}", "enabled": true } + ] + }, + "models": { + "mode": "merge", + "providers": { + "default_llm": { + "baseUrl": "${LLM_BASE_URL}", + "apiKey": "${LLM_API_KEY}", + "api": "openai-completions", + "models": [ + { + "id": "primary", + "name": "${LLM_MODEL_ID}", + "contextWindow": 128000, + "maxTokens": 8192 + } + ] + } + } + }, + "memory": { "backend": "qmd", "citations": "auto" }, + "skills": { "install": { "nodeManager": "npm" }, "entries": {} }, + "plugins": { "allow": [], "load": { "paths": [] }, "entries": {} } +} +``` + +- `gateway.auth.token` 使用 `${CONTROL_UI_TOKEN}`,與 `.env` / compose 變數名一致。 +- `models.providers.default_llm` 使用 `${LLM_BASE_URL}` / `${LLM_API_KEY}` / `${LLM_MODEL_ID}`,完整解耦底層供應商;`agents.defaults.model.primary` 指向 `"default_llm/primary"`。 +- `agents.list` 中預先註冊 `{{AGENT_ID}}`,渲染後即為遠端 Agent 的 ID。 + +### 3.3 docker-compose.yml.tpl — 容器編排模板 + +**路徑**:`remote-blueprints/template/docker-compose.yml.tpl` + +```yaml +# Remote Agent - OpenClaw Gateway +# Placeholders: {{AGENT_ID}}, {{AGENT_NAME}}, {{PROJECT_ID}} +# After render: .env supplies CONTROL_UI_TOKEN, LLM_BASE_URL, LLM_API_KEY, LLM_MODEL_ID, HUB_QDRANT_URL, MEM0_QDRANT_* +services: + gateway: + build: . + container_name: {{AGENT_ID}} + network_mode: "host" + restart: always + environment: + - OPENCLAW_GATEWAY_AUTH_MODE=token + - OPENCLAW_GATEWAY_AUTH_TOKEN=${CONTROL_UI_TOKEN} + - NODE_OPTIONS=--max-old-space-size=1536 + - QDRANT_HOST=${HUB_QDRANT_URL} + - AGENT_TAG={{AGENT_ID}} + - LLM_BASE_URL=${LLM_BASE_URL} + - LLM_API_KEY=${LLM_API_KEY} + - LLM_MODEL_ID=${LLM_MODEL_ID} + - MEM0_QDRANT_HOST=${MEM0_QDRANT_HOST} + - MEM0_QDRANT_PORT=${MEM0_QDRANT_PORT} + volumes: + - ./config/openclaw.json:/root/.openclaw/openclaw.json + - ./skills:/root/.openclaw/workspace/skills + - ./plugins:/root/.openclaw/workspace/plugins + - ./archive:/root/.openclaw/workspace/archive + - ./agents:/root/.openclaw/workspace/agents +``` + +- `network_mode: host` 方便 Tailscale / SSH 隧道使用。 +- `./agents` 掛載到 `/root/.openclaw/workspace/agents`,配合 `openclaw.json` 中的 `agents` 配置與未來 auto-discovery 能力。 + +### 3.4 Agent Profile 模板 — 認知配置 + +**路徑**:`remote-blueprints/template/agents/{{AGENT_ID}}.json.tpl` + +```json +{ + "id": "{{AGENT_ID}}", + "name": "{{AGENT_NAME}}", + "project_id": "{{PROJECT_ID}}", + "metadata": { + "project_id": "{{PROJECT_ID}}", + "role": "project-specialized-remote-agent" + }, + "systemPrompt": "You are an AI agent named {{AGENT_NAME}}. You belong to project {{PROJECT_ID}}. Always follow the project conventions, coordinate with the main hub agent, and log important decisions to shared memory." +} +``` + +- 在渲染與 rename 後,會變成 `agents/.json` 並掛載到 workspace,供 Gateway 或其他輔助工具使用。 + +### 3.5 Dockerfile — 執行環境與權限 + +**路徑**:`remote-blueprints/template/Dockerfile` + +```dockerfile +# Remote Agent - OpenClaw Gateway +# Base: node:20-slim; deps for build + image processing (libvips) +FROM node:20-slim + +RUN apt-get update && apt-get install -y --no-install-recommends \ + git \ + python3 \ + make \ + g++ \ + libvips-dev \ + && rm -rf /var/lib/apt/lists/* + +RUN npm install -g @openclaw/cli + +RUN mkdir -p /root/.openclaw/workspace/skills \ + /root/.openclaw/workspace/plugins \ + /root/.openclaw/workspace/archive \ + && chmod -R 777 /root/.openclaw/workspace/archive + +EXPOSE 18789 + +CMD ["openclaw", "gateway", "--port", "18789"] +``` + +- 確保 `archive/` 在容器內存在並可寫入(chmod 777),避免 Volume 掛載後出現 Permission Denied。 + +--- + +## 4. 腳本說明 + +### 4.1 generate_remote.sh — 實例壓鑄腳本 + +**路徑**:`scripts/generate_remote.sh` + +#### 4.1.1 模式說明 + +- **互動模式(人類操作)**: + - 條件:未帶任何 flag(`-a/-n/-p/-u/-k/-m/-t`)。 + - 行為: + - 依序 `read`:`AGENT_ID`、`AGENT_NAME`、`PROJECT_ID`、`LLM_BASE_URL`、`LLM_API_KEY`、`LLM_MODEL_ID`、`CONTROL_UI_TOKEN`。 + - 若 `AGENT_ID` 為空則直接報錯退出。 +- **非互動模式(ChatOps Ready)**: + - 條件:只要帶任一 flag(`-a/-n/-p/-u/-k/-m/-t`),即視為非互動模式。 + - 行為: + - **絕對不執行任何 `read`**,完全靜默(只輸出日誌 / JSON 錯誤)。 + - **必填參數**:`AGENT_ID`、`LLM_BASE_URL`、`LLM_API_KEY`、`LLM_MODEL_ID`。 + - 若任一缺失,輸出 JSON 錯誤並 `exit 1`,示例: + ```json + {"ok":false,"error":"missing_required_params","missing":["AGENT_ID","LLM_BASE_URL"]} + ``` + - `AGENT_NAME` 預設為 `AGENT_ID`,`PROJECT_ID` 預設為 `"default"`。 + +#### 4.1.2 參數與旗標 + +- 支援的 flags: + - `-a ` → `AGENT_ID` + - `-n ` → `AGENT_NAME` + - `-p ` → `PROJECT_ID` + - `-u ` → `LLM_BASE_URL` + - `-k ` → `LLM_API_KEY` + - `-m ` → `LLM_MODEL_ID`(例如 `qwen-max`, `gpt-4o`) + - `-t ` → `CONTROL_UI_TOKEN`(可選;未提供則自動生成) + +#### 4.1.3 Token 自動生成 + +- 若解析完所有輸入後 `CONTROL_UI_TOKEN` 仍為空: + - 首選:`openssl rand -hex 24` 生成 48 位十六進位字串。 + - 若系統無 `openssl`,改用 `/dev/urandom` + `base64` + 過濾成 hex 的備援方案(並輸出 WARN 日誌)。 + +#### 4.1.4 安全與佔位符替換 + +- `AGENT_ID` 僅允許 `[a-zA-Z0-9_-]`,否則報錯退出。 +- 使用統一的 `escape_sed_val()` 對所有值進行轉義: + - `\` → `\\`、`&` → `\&`、`/` → `\/`。 +- 所有 sed 替換使用 `#` 作為定界符,避免 URL 中的 `/` 破壞語法: + +```bash +sed -i "s#{{AGENT_ID}}#$AGENT_ID#g" "$f" +sed -i "s#{{AGENT_NAME}}#$AGENT_NAME_ESC#g" "$f" +sed -i "s#{{PROJECT_ID}}#$PROJECT_ID_ESC#g" "$f" +sed -i "s#{{LLM_BASE_URL}}#$LLM_BASE_URL_ESC#g" "$f" +sed -i "s#{{LLM_API_KEY}}#$LLM_API_KEY_ESC#g" "$f" +sed -i "s#{{LLM_MODEL_ID}}#$LLM_MODEL_ID_ESC#g" "$f" +sed -i "s#{{CONTROL_UI_TOKEN}}#$CONTROL_UI_TOKEN_ESC#g" "$f" +``` + +- 佔位符覆蓋範圍: + - `{{AGENT_ID}}`, `{{AGENT_NAME}}`, `{{PROJECT_ID}}` + - `{{LLM_BASE_URL}}`, `{{LLM_API_KEY}}`, `{{LLM_MODEL_ID}}` + - `{{CONTROL_UI_TOKEN}}` + - 兼容移除舊的 `{{BAILIAN_API_KEY}}`(若仍存在會被同值覆蓋)。 +- 遍歷所有 `*.tpl` 與 `.env.tpl` 檔案(包含 `agents/{{AGENT_ID}}.json.tpl`)。 + +#### 4.1.5 檔名重寫與 deploy 腳本生成 + +- 在 rename 所有 `*.tpl` 前,先將 `agents/{{AGENT_ID}}.json.tpl` 改名為 `agents/.json.tpl`,再統一移除 `.tpl` 副檔名。 +- 最終 `agents/.json` 會被 docker-compose 掛載到 `/root/.openclaw/workspace/agents/.json`。 +- `deploy_to_target.sh` 透過單引號 heredoc 生成,內部變數不會被當前 shell 展開,之後用 `sed` 注入實際 `AGENT_ID`,邏輯: + - `TARGET_IP` / `SSH_USER` 參數檢查。 + - 檢查本地 `docker-compose.yml` 是否存在。 + - `ssh mkdir` 遠端目錄後,用 `tar cf - . | ssh tar xf -` 覆蓋整個目錄。 + - 執行 `docker compose down 2>/dev/null || true && docker compose up -d --build`。 + +#### 4.1.6 結尾輸出 + +- 成功後輸出: + +```text +[OK] Instance ready: /root/.openclaw/workspace/remote-blueprints/ +[OK] CONTROL_UI_TOKEN: <實際 token> +Next: cd /root/.openclaw/workspace/remote-blueprints/ && ./deploy_to_target.sh [SSH_USER] +``` + +### 4.2 sync_skill.sh — Skill/Plugin 同步腳本 + +**路徑**:`scripts/sync_skill.sh` + +- 參數:` `。 +- 本地來源:`workspace/skills/`,不存在則報錯退出。 +- 遠端目的:`/opt/openclaw-remote//skills/`。 +- 優先使用 `rsync -avz --exclude 'node_modules'`,無 rsync 才降級為 `scp -r`。 +- 同步完成後執行:`ssh root@ 'cd /opt/openclaw-remote/ && docker compose restart'`。 + +--- + +## 5. 環境變數與配置對齊表 + +| 變數名 | .env.tpl | docker-compose | openclaw.json | 說明 | +|------------------|-------------------------|----------------------------------------|------------------------------------------|--------------------------------------------| +| AGENT_ID | `{{AGENT_ID}}` | `container_name` / `AGENT_TAG` | `agents.list[].id`(模板為 `{{AGENT_ID}}`) | 遠端 Agent ID,僅允許 `[a-zA-Z0-9_-]` | +| CONTROL_UI_TOKEN | `{{CONTROL_UI_TOKEN}}` | `OPENCLAW_GATEWAY_AUTH_TOKEN` | `gateway.auth.token="${CONTROL_UI_TOKEN}"` | Gateway Control UI token | +| HUB_QDRANT_URL | 固定 http://100.115... | `QDRANT_HOST` | — | 中心 Qdrant URL | +| MEM0_QDRANT_HOST | 100.115.94.1 | `MEM0_QDRANT_HOST` | — | mem0-integration 使用的 Qdrant host | +| MEM0_QDRANT_PORT | 6333 | `MEM0_QDRANT_PORT` | — | mem0-integration 使用的 Qdrant port | +| LLM_BASE_URL | `{{LLM_BASE_URL}}` | `LLM_BASE_URL` | `models.providers.default_llm.baseUrl` | 通用 LLM 端點(OpenAI compatible 等) | +| LLM_API_KEY | `{{LLM_API_KEY}}` | `LLM_API_KEY` | `models.providers.default_llm.apiKey` | 通用 LLM API key | +| LLM_MODEL_ID | `{{LLM_MODEL_ID}}` | `LLM_MODEL_ID` | `models.providers.default_llm.models[].name` | 實際使用的大模型 ID(如 qwen-max, gpt-4o) | + +**重要**:`openclaw.json` 僅透過 `${VAR_NAME}` 讀取這些變數,實際值由 `.env` / compose 注入,不再使用任何 SecretRef 物件,也不再引用 `BAILIAN_API_KEY`。 + +--- + +## 6. 修復與變更記錄(供 AI / 維運對照) + +| 項目 | 問題 / 風險 | 修復 / 改動摘要 | +|------------------------------|----------------------------------------------|----------------------------------------------------------------------------------------------------------| +| LLM 提供商耦合 | 過去綁定 `bailian` 提供商與固定模型 ID | 改為 `default_llm` provider,使用 `${LLM_BASE_URL}`、`${LLM_API_KEY}`、`${LLM_MODEL_ID}` 完全解耦。 | +| openclaw.json secrets 格式 | 使用 SecretRef 或硬編 API key | 改為一致使用 `${CONTROL_UI_TOKEN}`、`${LLM_API_KEY}` 字串插值,並由 .env/compose 提供實際值。 | +| Agent Profile 掛載路徑 | 先前構想使用 `/root/.openclaw/agents` | 修正為 `/root/.openclaw/workspace/agents`,與 workspace 路徑對齊,並新增 `agents/{{AGENT_ID}}.json.tpl`。 | +| generate_remote 無交互模式 | 可能於缺參數時卡在 `read` 導致 Hang | 帶任一 flag 即進入嚴格非互動模式,缺少必填參數時回傳 JSON 錯誤並立刻退出,不進行任何 `read`。 | +| sed URL 替換問題 | `https://` 中的 `/` 會破壞 `s/old/new/g` | 全面改用 `s#old#new#g`,並對值先經 `escape_sed_val` 處理 `\`、`&`、`/`。 | +| Token / API key sed 注入風險 | token 中含 `&` 或 `\` 造成 sed 失敗 | 使用 `escape_sed_val` 對所有值做轉義;同時限制 AGENT_ID 僅允許 `[a-zA-Z0-9_-]`。 | +| deploy_to_target 腳本嵌套變數| 早期版本 heredoc 中混雜當前 shell 變數 | 改用單引號 heredoc 並使用 placeholder `__AGENT_ID_PLACEHOLDER__`,事後以 sed 注入實際 AGENT_ID。 | +| mem0 Qdrant 依賴明確化 | 容器端 mem0 可能找不到 Qdrant 地址 | `.env.tpl` + compose 中顯式提供 `MEM0_QDRANT_HOST`、`MEM0_QDRANT_PORT`。 | + +> 本版未再執行額外自動化測試;以上變動經過靜態檢查與少量手動 sanity run 佐證,建議在實際部署前再做一次端對端驗證。 + +--- + +## 7. 使用建議(高階) + +1. **Main Agent ChatOps 調用**: + - 建議統一由 Main Agent 以非互動模式呼叫: + ```bash + ./generate_remote.sh \ + -a advert-bot \ + -n "Advert Bot" \ + -p advert \ + -u https://llm.example.com/v1 \ + -k sk-xxx \ + -m qwen-max + ``` +2. **部署**: + - 在目標伺服器上確保 Docker 與 LLM 網路連線可用後,執行: + ```bash + cd /root/.openclaw/workspace/remote-blueprints/advert-bot + ./deploy_to_target.sh [SSH_USER] + ``` +3. **Skill 熱更新**: + - 使用 `sync_skill.sh` 精準同步單一 Skill 目錄,避免重建鏡像: + ```bash + ./sync_skill.sh advert-bot tavily + ``` + +--- + +**最後更新:** 2026-03-12 — 將舊版 `BAILIAN_API_KEY` 說明全面替換為通用 `LLM_BASE_URL` / `LLM_API_KEY` / `LLM_MODEL_ID` 模型解耦方案,並補充 ChatOps 無交互模式與認知配置掛載設計。 diff --git a/docs/tongge-fortune-setup.md b/docs/tongge-fortune-setup.md index 1e5c952..dccb304 100644 --- a/docs/tongge-fortune-setup.md +++ b/docs/tongge-fortune-setup.md @@ -12,78 +12,50 @@ --- -## 🚀 部署步骤 +## 🚀 当前部署状态(2026-03-17) -### 1. 确认技能已安装 +**已完成迁移**:运势推送任务已迁移到 OpenClaw 内置 Cron 系统(`/root/.openclaw-tongge/cron/jobs.json`),由桐哥 Gateway 直接管理。不再使用系统 cron 或独立 Node.js 脚本。 -```bash -# 检查技能文件 -ls -la /root/.openclaw/workspace/skills/daily-horoscope/ -# 应该看到: -# - SKILL.md -# - index.js -# - openclaw.plugin.json -``` +Cron 任务 ID:`tongge-daily-fortune`,每晚 21:00 香港时间自动触发桐哥 isolated session,由桐哥搜索运势并通过 Telegram 推送。 -### 2. 更新桐哥配置 +--- -配置已保存到 `/tmp/tongge-config.json`,执行: +## 🚀 迁移/重新部署步骤(服务器迁移时) -```bash -# 备份原配置 -cp /root/.openclaw-tongge/openclaw.json /root/.openclaw-tongge/openclaw.json.bak +### 1. 确认前置条件 -# 应用新配置 -cp /tmp/tongge-config.json /root/.openclaw-tongge/openclaw.json +```bash +# 确认桐哥 Gateway 正在运行 +systemctl --user status openclaw-gateway-tongge.service -# 验证配置 -openclaw --profile tongge doctor +# 确认 tavily_search 工具已启用(openclaw.json 中 alsoAllow: ["tavily_search"]) +# 确认 Telegram bot 已配置(TELEGRAM_BOT_TOKEN 在 .env 中) ``` -### 3. 添加 Cron 定时任务 - -```bash -# 添加每日运势推送任务 -openclaw --profile tongge cron add \ - --name "每日运势推送" \ - --cron "0 21 * * *" \ - --tz "Asia/Shanghai" \ - --session isolated \ - --message "请查询明日运势并发送给王院长" \ - --announce \ - --channel telegram \ - --to "user:5237946060" - -# 验证任务 -openclaw --profile tongge cron list -``` +### 2. 停止 Gateway,直接写入 Cron 任务 -### 4. 重启桐哥 Gateway +> **注意**:`openclaw --profile tongge cron add` CLI 命令需要 device pairing 才能认证桐哥 Gateway。迁移时直接编辑 `jobs.json` 是最可靠的方式。 ```bash -# 重启服务 -systemctl --user restart openclaw-gateway-tongge.service +# 停止 Gateway(编辑期间安全) +systemctl --user stop openclaw-gateway-tongge.service -# 检查状态 -systemctl --user status openclaw-gateway-tongge.service +# 写入 cron 任务(参考 /root/.openclaw-tongge/cron/jobs.json 当前配置) +cp /root/.openclaw-tongge/cron/jobs.json /root/.openclaw-tongge/cron/jobs.json.bak +# 编辑 jobs.json,添加 tongge-daily-fortune 任务(见下方配置详情) -# 查看日志 -journalctl --user -u openclaw-gateway-tongge.service -f +# 重启 Gateway +systemctl --user start openclaw-gateway-tongge.service ``` -### 5. 测试功能 +### 3. 验证任务已加载 ```bash -# 手动运行一次测试 -openclaw --profile tongge cron add \ - --name "测试运势" \ - --at "$(date -d '+1 minute' -Iseconds)" \ - --session isolated \ - --message "请查询明日运势并发送给王院长" \ - --announce \ - --channel telegram - -# 一分钟后查看 Telegram 是否收到消息 +# Gateway 启动后会自动将 nextRunAtMs 写入 jobs.json +cat /root/.openclaw-tongge/cron/jobs.json | python3 -m json.tool | grep -A2 "nextRunAt" + +# 查看 Gateway 日志确认无错误 +journalctl --user -u openclaw-gateway-tongge.service -n 20 ``` --- @@ -113,26 +85,35 @@ openclaw --profile tongge cron add \ } ``` -### Cron 任务配置 +### Cron 任务配置(`/root/.openclaw-tongge/cron/jobs.json`) ```json { + "jobId": "tongge-daily-fortune", "name": "每日运势推送", + "description": "每晚21点(香港时间)查询明日金牛座运势、黄历、五行分析,推送给王院长", + "enabled": true, + "agentId": "tongge", "schedule": { "kind": "cron", "expr": "0 21 * * *", - "tz": "Asia/Shanghai" + "tz": "Asia/Hong_Kong", + "staggerMs": 0 }, "sessionTarget": "isolated", + "wakeMode": "now", "payload": { "kind": "agentTurn", - "message": "请查询明日运势..." + "message": "现在是每日运势推送时间。请完成以下任务:\n1. 查询明日(明天)的金牛座星座运势(用 tavily 搜索)\n2. 查询明日黄历宜忌信息(用 tavily 搜索)\n3. 结合王院长的基本信息做五行分析:生日1984年5月16日子时,星座金牛座,生肖鼠\n4. 整合成一份温馨的运势报告,格式包含:星座运势、黄历宜忌、五行分析、趋吉避凶建议\n5. 以桐哥的风格撰写,末尾加上你的祝福语\n\n报告将通过 Telegram 自动推送给王院长,无需手动发送。", + "lightContext": false }, "delivery": { "mode": "announce", "channel": "telegram", - "to": "user:5237946060" - } + "to": "5237946060", + "bestEffort": true + }, + "deleteAfterRun": false } ``` diff --git a/infrastructure/oneapi/.env.example b/infrastructure/oneapi/.env.example new file mode 100644 index 0000000..749b499 --- /dev/null +++ b/infrastructure/oneapi/.env.example @@ -0,0 +1,2 @@ +# 请修改为本机 Tailscale IP(如 100.x.x.x) +TAILSCALE_IP=xxx.xxx.xxx.xxx diff --git a/infrastructure/oneapi/README.md b/infrastructure/oneapi/README.md new file mode 100644 index 0000000..734d430 --- /dev/null +++ b/infrastructure/oneapi/README.md @@ -0,0 +1,9 @@ +# OneAPI LLM Gateway (OpenClaw) + +独立于 OpenClaw 的 LLM API 网关,用于多模型统一管理与按需路由。 + +- **部署**:编辑 `.env` 设置 `TAILSCALE_IP` 后执行 `./deploy_gateway.sh` +- **管理后台**:`http://:3000`,默认 root / 123456 +- **数据持久化**:`./data`(SQLite),迁移时打包此目录即可恢复 + +完整架构与 Skill 客户端说明见:[workspace/docs/LLM_GATEWAY_AND_SKILL_CLIENT.md](../../docs/LLM_GATEWAY_AND_SKILL_CLIENT.md) diff --git a/infrastructure/oneapi/data/one-api.db b/infrastructure/oneapi/data/one-api.db new file mode 100644 index 0000000000000000000000000000000000000000..db43ea7ff4bbe35155c4c06bca406227138a42cb GIT binary patch literal 253952 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 Gt@l40y>_Pn literal 0 HcmV?d00001 diff --git a/infrastructure/oneapi/deploy_gateway.sh b/infrastructure/oneapi/deploy_gateway.sh new file mode 100755 index 0000000..96de814 --- /dev/null +++ b/infrastructure/oneapi/deploy_gateway.sh @@ -0,0 +1,23 @@ +#!/bin/bash +set -e +# 获取脚本所在目录的绝对路径并切换过去 +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +cd "$DIR" + +if [ ! -f .env ]; then + cp .env.example .env + echo "Created .env from .env.example. Please edit .env and set TAILSCALE_IP." +fi + +# Load TAILSCALE_IP for display (docker compose will load .env for itself) +set -a +[ -f .env ] && . .env +set +a + +docker compose up -d + +echo "" +echo "OneAPI LLM Gateway is running." +echo " Management UI: http://${TAILSCALE_IP:-}:3000" +echo " Default login: root / 123456" +echo "" diff --git a/infrastructure/oneapi/docker-compose.yml b/infrastructure/oneapi/docker-compose.yml new file mode 100644 index 0000000..1ce6e2c --- /dev/null +++ b/infrastructure/oneapi/docker-compose.yml @@ -0,0 +1,11 @@ +services: + oneapi: + image: justsong/one-api:latest + container_name: openclaw-llm-gateway + restart: always + ports: + - "${TAILSCALE_IP}:3000:3000" + volumes: + - ./data:/data + environment: + - TZ=Asia/Shanghai diff --git a/remote-blueprints/template/.env.tpl b/remote-blueprints/template/.env.tpl new file mode 100644 index 0000000..6928264 --- /dev/null +++ b/remote-blueprints/template/.env.tpl @@ -0,0 +1,11 @@ +AGENT_ID={{AGENT_ID}} +CONTROL_UI_TOKEN={{CONTROL_UI_TOKEN}} +HUB_QDRANT_URL=http://100.115.94.1:6333 +# mem0-integration skill (Layer 4) reads these; align with HUB_QDRANT_URL if using central Qdrant +MEM0_QDRANT_HOST=100.115.94.1 +MEM0_QDRANT_PORT=6333 +# Generic LLM provider configuration (provider-agnostic) +LLM_BASE_URL={{LLM_BASE_URL}} +LLM_API_KEY={{LLM_API_KEY}} +LLM_MODEL_ID={{LLM_MODEL_ID}} +EMBEDDING_MODEL_ID={{EMBEDDING_MODEL_ID}} diff --git a/remote-blueprints/template/Dockerfile b/remote-blueprints/template/Dockerfile new file mode 100644 index 0000000..cfb259b --- /dev/null +++ b/remote-blueprints/template/Dockerfile @@ -0,0 +1,22 @@ +# Remote Agent - OpenClaw Gateway +# Base: node:20-slim; deps for build + image processing (libvips) +FROM node:20-slim + +RUN apt-get update && apt-get install -y --no-install-recommends \ + git \ + python3 \ + make \ + g++ \ + libvips-dev \ + && rm -rf /var/lib/apt/lists/* + +RUN npm install -g @openclaw/cli + +RUN mkdir -p /root/.openclaw/workspace/skills \ + /root/.openclaw/workspace/plugins \ + /root/.openclaw/workspace/archive \ + && chmod -R 777 /root/.openclaw/workspace/archive + +EXPOSE 18789 + +CMD ["openclaw", "gateway", "--port", "18789"] diff --git a/remote-blueprints/template/agents/{{AGENT_ID}}.json.tpl b/remote-blueprints/template/agents/{{AGENT_ID}}.json.tpl new file mode 100644 index 0000000..1170ccd --- /dev/null +++ b/remote-blueprints/template/agents/{{AGENT_ID}}.json.tpl @@ -0,0 +1,10 @@ +{ + "id": "{{AGENT_ID}}", + "name": "{{AGENT_NAME}}", + "project_id": "{{PROJECT_ID}}", + "metadata": { + "project_id": "{{PROJECT_ID}}", + "role": "project-specialized-remote-agent" + }, + "systemPrompt": "You are an AI agent named {{AGENT_NAME}}. You belong to project {{PROJECT_ID}}. Always follow the project conventions, coordinate with the main hub agent, and log important decisions to shared memory." +} diff --git a/remote-blueprints/template/archive/.gitkeep b/remote-blueprints/template/archive/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/remote-blueprints/template/config/openclaw.json b/remote-blueprints/template/config/openclaw.json new file mode 100644 index 0000000..4676694 --- /dev/null +++ b/remote-blueprints/template/config/openclaw.json @@ -0,0 +1,63 @@ +{ + "gateway": { + "port": 18789, + "mode": "local", + "bind": "lan", + "controlUi": { + "allowedOrigins": [ + "http://localhost:*", + "http://localhost:18789", + "http://127.0.0.1:*", + "http://127.0.0.1:18789", + "http://100.115.94.1:18789" + ], + "dangerouslyDisableDeviceAuth": false + }, + "auth": { + "mode": "token", + "token": "${CONTROL_UI_TOKEN}", + "rateLimit": { + "maxAttempts": 10, + "windowMs": 60000, + "lockoutMs": 300000 + } + }, + "trustedProxies": ["127.0.0.1", "100.115.94.1", "::1"] + }, + "agents": { + "defaults": { + "workspace": "/root/.openclaw/workspace", + "model": { "primary": "default_llm/primary" } + }, + "list": [ + { "id": "main" }, + { "id": "{{AGENT_ID}}", "enabled": true } + ] + }, + "models": { + "mode": "merge", + "providers": { + "default_llm": { + "baseUrl": "${LLM_BASE_URL}", + "apiKey": "${LLM_API_KEY}", + "api": "openai-completions", + "models": [ + { + "id": "primary", + "name": "${LLM_MODEL_ID}", + "contextWindow": 128000, + "maxTokens": 8192 + }, + { + "id": "embedding", + "name": "${EMBEDDING_MODEL_ID}", + "contextWindow": 8192 + } + ] + } + } + }, + "memory": { "backend": "qmd", "citations": "auto" }, + "skills": { "install": { "nodeManager": "npm" }, "entries": {} }, + "plugins": { "allow": [], "load": { "paths": [] }, "entries": {} } +} diff --git a/remote-blueprints/template/docker-compose.yml.tpl b/remote-blueprints/template/docker-compose.yml.tpl new file mode 100644 index 0000000..c63bb7e --- /dev/null +++ b/remote-blueprints/template/docker-compose.yml.tpl @@ -0,0 +1,26 @@ +# Remote Agent - OpenClaw Gateway +# Placeholders: {{AGENT_ID}}, {{AGENT_NAME}}, {{PROJECT_ID}} +# After render: .env supplies CONTROL_UI_TOKEN, LLM_BASE_URL, LLM_API_KEY, LLM_MODEL_ID, HUB_QDRANT_URL, MEM0_QDRANT_* +services: + gateway: + build: . + container_name: {{AGENT_ID}} + network_mode: "host" + restart: always + environment: + - OPENCLAW_GATEWAY_AUTH_MODE=token + - OPENCLAW_GATEWAY_AUTH_TOKEN=${CONTROL_UI_TOKEN} + - NODE_OPTIONS=--max-old-space-size=1536 + - QDRANT_HOST=${HUB_QDRANT_URL} + - AGENT_TAG={{AGENT_ID}} + - LLM_BASE_URL=${LLM_BASE_URL} + - LLM_API_KEY=${LLM_API_KEY} + - LLM_MODEL_ID=${LLM_MODEL_ID} + - MEM0_QDRANT_HOST=${MEM0_QDRANT_HOST} + - MEM0_QDRANT_PORT=${MEM0_QDRANT_PORT} + volumes: + - ./config/openclaw.json:/root/.openclaw/openclaw.json + - ./skills:/root/.openclaw/workspace/skills + - ./plugins:/root/.openclaw/workspace/plugins + - ./archive:/root/.openclaw/workspace/archive + - ./agents:/root/.openclaw/workspace/agents diff --git a/remote-blueprints/template/plugins/.gitkeep b/remote-blueprints/template/plugins/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/remote-blueprints/template/skills/.gitkeep b/remote-blueprints/template/skills/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/scripts/generate_remote.sh b/scripts/generate_remote.sh new file mode 100755 index 0000000..be1aa9a --- /dev/null +++ b/scripts/generate_remote.sh @@ -0,0 +1,226 @@ +#!/bin/bash +############################################################################### +# Generate a new remote agent instance from the template. +# - Interactive mode: no flags, prompts for values. +# - Non-interactive (ChatOps) mode: any flag (-a/-n/-p/-u/-k/-m/-t) enables it; +# all required params must be provided, otherwise exits with JSON error. +############################################################################### +set -e + +WORKSPACE="${WORKSPACE:-/root/.openclaw/workspace}" +TEMPLATE_DIR="$WORKSPACE/remote-blueprints/template" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' +log_info() { echo -e "${BLUE}[INFO]${NC} $1"; } +log_ok() { echo -e "${GREEN}[OK]${NC} $1"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +log_err() { echo -e "${RED}[ERROR]${NC} $1"; } + +# ---------- parse flags (non-interactive mode detection) ---------- +NONINTERACTIVE=false +AGENT_ID="" +AGENT_NAME="" +PROJECT_ID="" +LLM_BASE_URL="" +LLM_API_KEY="" +LLM_MODEL_ID="" +CONTROL_UI_TOKEN="" + +while getopts "a:n:p:u:k:m:t:" opt; do + NONINTERACTIVE=true + case "$opt" in + a) AGENT_ID="$OPTARG" ;; + n) AGENT_NAME="$OPTARG" ;; + p) PROJECT_ID="$OPTARG" ;; + u) LLM_BASE_URL="$OPTARG" ;; + k) LLM_API_KEY="$OPTARG" ;; + m) LLM_MODEL_ID="$OPTARG" ;; + t) CONTROL_UI_TOKEN="$OPTARG" ;; + *) ;; + esac +done +shift $((OPTIND-1)) + +# ---------- interactive fallback (no flags at all) ---------- +if [ "$NONINTERACTIVE" = false ]; then + echo -n "AGENT_ID (required): " + read -r AGENT_ID + if [ -z "$AGENT_ID" ]; then + log_err "AGENT_ID is required." + exit 1 + fi + + echo -n "AGENT_NAME (optional, default = AGENT_ID): " + read -r AGENT_NAME + echo -n "PROJECT_ID (optional, default = default): " + read -r PROJECT_ID + + echo -n "LLM_BASE_URL (required): " + read -r LLM_BASE_URL + echo -n "LLM_API_KEY (required): " + read -r LLM_API_KEY + echo -n "LLM_MODEL_ID (required, e.g. qwen-max, gpt-4o): " + read -r LLM_MODEL_ID + + echo -n "CONTROL_UI_TOKEN (optional, auto-generate if empty): " + read -r CONTROL_UI_TOKEN +else + # ---------- strict non-interactive mode ---------- + missing=() + [ -z "$AGENT_ID" ] && missing+=("AGENT_ID") + [ -z "$LLM_BASE_URL" ] && missing+=("LLM_BASE_URL") + [ -z "$LLM_API_KEY" ] && missing+=("LLM_API_KEY") + [ -z "$LLM_MODEL_ID" ] && missing+=("LLM_MODEL_ID") + + if [ ${#missing[@]} -ne 0 ]; then + # JSON error for ChatOps callers + printf '{"ok":false,"error":"missing_required_params","missing":[' >&2 + for i in "${!missing[@]}"; do + [ "$i" -ne 0 ] && printf ',' >&2 + printf '"%s"' "${missing[$i]}" >&2 + done + printf ']}' >&2 + echo >&2 + exit 1 + fi +fi + +# Defaults for optional values +[ -z "$AGENT_NAME" ] && AGENT_NAME="$AGENT_ID" +[ -z "$PROJECT_ID" ] && PROJECT_ID="default" + +# ---------- validate AGENT_ID ---------- +if ! echo "$AGENT_ID" | grep -qE '^[a-zA-Z0-9_-]+$'; then + log_err "AGENT_ID must contain only letters, numbers, hyphens, and underscores." + exit 1 +fi + +# ---------- ensure template dir exists ---------- +if [ ! -d "$TEMPLATE_DIR" ]; then + log_err "Template not found: $TEMPLATE_DIR" + exit 1 +fi + +# ---------- auto-generate CONTROL_UI_TOKEN if empty ---------- +if [ -z "$CONTROL_UI_TOKEN" ]; then + if command -v openssl >/dev/null 2>&1; then + CONTROL_UI_TOKEN=$(openssl rand -hex 24) + log_info "Generated CONTROL_UI_TOKEN via openssl." + else + CONTROL_UI_TOKEN=$(head -c 32 /dev/urandom | base64 | tr -dc 'a-f0-9' | head -c 48) + log_warn "openssl not found; CONTROL_UI_TOKEN generated via /dev/urandom fallback." + fi +fi + +INSTANCE_DIR="$WORKSPACE/remote-blueprints/$AGENT_ID" +if [ -d "$INSTANCE_DIR" ]; then + if [ "$NONINTERACTIVE" = true ]; then + log_err "Instance already exists: $INSTANCE_DIR" + exit 1 + fi + log_warn "Instance already exists: $INSTANCE_DIR" + echo -n "Overwrite? (y/N): " + read -r confirm + if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then + log_info "Aborted." + exit 0 + fi + rm -rf "$INSTANCE_DIR" +fi + +# ---------- copy template ---------- +log_info "Copying template to $INSTANCE_DIR ..." +mkdir -p "$INSTANCE_DIR" +cp -r "$TEMPLATE_DIR"/* "$INSTANCE_DIR/" +[ -f "$TEMPLATE_DIR/.env.tpl" ] && cp "$TEMPLATE_DIR/.env.tpl" "$INSTANCE_DIR/.env.tpl" +log_ok "Copy done." + +# ---------- render templates (safe sed with # delimiter) ---------- +escape_sed_val() { + local v="$1" + v="${v//\\/\\\\}" + v="${v//&/\\&}" + v="${v//\//\\/}" + printf '%s' "$v" +} + +AGENT_NAME_ESC=$(escape_sed_val "$AGENT_NAME") +PROJECT_ID_ESC=$(escape_sed_val "$PROJECT_ID") +LLM_BASE_URL_ESC=$(escape_sed_val "$LLM_BASE_URL") +LLM_API_KEY_ESC=$(escape_sed_val "$LLM_API_KEY") +LLM_MODEL_ID_ESC=$(escape_sed_val "$LLM_MODEL_ID") +CONTROL_UI_TOKEN_ESC=$(escape_sed_val "$CONTROL_UI_TOKEN") + +# Render all *.tpl and .env.tpl files +while IFS= read -r -d '' f; do + log_info "Rendering $f ..." + sed -i "s#{{AGENT_ID}}#$AGENT_ID#g" "$f" + sed -i "s#{{AGENT_NAME}}#$AGENT_NAME_ESC#g" "$f" + sed -i "s#{{PROJECT_ID}}#$PROJECT_ID_ESC#g" "$f" + sed -i "s#{{LLM_BASE_URL}}#$LLM_BASE_URL_ESC#g" "$f" + sed -i "s#{{LLM_API_KEY}}#$LLM_API_KEY_ESC#g" "$f" + sed -i "s#{{LLM_MODEL_ID}}#$LLM_MODEL_ID_ESC#g" "$f" + sed -i "s#{{CONTROL_UI_TOKEN}}#$CONTROL_UI_TOKEN_ESC#g" "$f" + # Backward-compat: clean any legacy BAILIAN placeholders if present + sed -i "s#{{BAILIAN_API_KEY}}#$LLM_API_KEY_ESC#g" "$f" || true + +done < <(find "$INSTANCE_DIR" -type f \( -name '*.tpl' -o -name '.env.tpl' \) -print0) + +# ---------- rename agents/{{AGENT_ID}}.json.tpl -> agents/.json.tpl (before generic .tpl removal) ---------- +if [ -f "$INSTANCE_DIR/agents/{{AGENT_ID}}.json.tpl" ]; then + mv "$INSTANCE_DIR/agents/{{AGENT_ID}}.json.tpl" "$INSTANCE_DIR/agents/$AGENT_ID.json.tpl" +fi + +# ---------- rename all *.tpl to final filenames ---------- +while IFS= read -r -d '' f; do + base="${f%.tpl}" + mv "$f" "$base" + log_info "Renamed to $base" +done < <(find "$INSTANCE_DIR" -type f -name '*.tpl' -print0) + +# .env.tpl -> .env +if [ -f "$INSTANCE_DIR/.env.tpl" ]; then + mv "$INSTANCE_DIR/.env.tpl" "$INSTANCE_DIR/.env" + log_info "Renamed .env.tpl to .env" +fi + +# ---------- generate deploy_to_target.sh ---------- +DEPLOY_SCRIPT="$INSTANCE_DIR/deploy_to_target.sh" +cat > "$DEPLOY_SCRIPT" << 'DEPLOYEOF' +#!/bin/bash +# Deploy this agent directory to a remote host. Usage: ./deploy_to_target.sh [SSH_USER] +set -e + +TARGET_IP="${1:?Usage: $0 [SSH_USER]}" +SSH_USER="${2:-root}" +AGENT_ID="__AGENT_ID_PLACEHOLDER__" +REMOTE_PATH="/opt/openclaw-remote/$AGENT_ID" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +if [ ! -f "$SCRIPT_DIR/docker-compose.yml" ]; then + echo "[ERROR] docker-compose.yml not found in $SCRIPT_DIR" + exit 1 +fi + +echo "[INFO] Syncing $SCRIPT_DIR to ${SSH_USER}@${TARGET_IP}:$REMOTE_PATH ..." +ssh "${SSH_USER}@${TARGET_IP}" "mkdir -p $REMOTE_PATH" +(cd "$SCRIPT_DIR" && tar cf - .) | ssh "${SSH_USER}@${TARGET_IP}" "cd $REMOTE_PATH && tar xf -" + +echo "[INFO] Remote: docker compose down && docker compose up -d --build ..." +ssh "${SSH_USER}@${TARGET_IP}" "cd $REMOTE_PATH && docker compose down 2>/dev/null || true && docker compose up -d --build" + +echo "[OK] Deploy done." +DEPLOYEOF + +# inject actual AGENT_ID into deploy script +sed -i "s#__AGENT_ID_PLACEHOLDER__#$AGENT_ID#g" "$DEPLOY_SCRIPT" +chmod +x "$DEPLOY_SCRIPT" +log_ok "Generated $DEPLOY_SCRIPT" + +log_ok "Instance ready: $INSTANCE_DIR" +echo "[OK] CONTROL_UI_TOKEN: $CONTROL_UI_TOKEN" +echo "Next: cd $INSTANCE_DIR && ./deploy_to_target.sh [SSH_USER]" diff --git a/scripts/sync_skill.sh b/scripts/sync_skill.sh new file mode 100755 index 0000000..0fa0163 --- /dev/null +++ b/scripts/sync_skill.sh @@ -0,0 +1,60 @@ +#!/bin/bash +############################################################################### +# Sync a skill (or plugin) directory to a remote agent and restart the container. +# Usage: ./sync_skill.sh +# MODULE_DIR_NAME = directory under workspace/skills/ (e.g. tavily, mem0-integration) +# Prefer rsync (exclude node_modules); fallback to scp if rsync not available. +############################################################################### +set -e + +WORKSPACE="${WORKSPACE:-/root/.openclaw/workspace}" +SKILLS_SRC="$WORKSPACE/skills" +REMOTE_BASE="/opt/openclaw-remote" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' +log_info() { echo -e "${BLUE}[INFO]${NC} $1"; } +log_ok() { echo -e "${GREEN}[OK]${NC} $1"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +log_err() { echo -e "${RED}[ERROR]${NC} $1"; } + +# --- 1. Parameter check --- +TARGET_IP="${1:?Usage: $0 }" +AGENT_ID="${2:?Usage: $0 }" +MODULE_NAME="${3:?Usage: $0 }" + +LOCAL_PATH="$SKILLS_SRC/$MODULE_NAME" +REMOTE_PATH="$REMOTE_BASE/$AGENT_ID/skills" + +# --- 2. Local path exists --- +if [ ! -d "$LOCAL_PATH" ]; then + log_err "Local path not found: $LOCAL_PATH" + exit 1 +fi + +# --- 3. Optional: quick SSH connectivity check --- +if ! ssh -o ConnectTimeout=5 -o BatchMode=yes "root@$TARGET_IP" "exit" 2>/dev/null; then + log_warn "SSH to root@$TARGET_IP may require password or key. Continuing anyway." +fi + +# --- 4. Sync: prefer rsync, fallback scp --- +if command -v rsync >/dev/null 2>&1; then + log_info "Syncing with rsync (excluding node_modules): $LOCAL_PATH -> root@$TARGET_IP:$REMOTE_PATH/" + rsync -avz --exclude 'node_modules' "$LOCAL_PATH/" "root@${TARGET_IP}:${REMOTE_PATH}/${MODULE_NAME}/" + log_ok "Rsync done." +else + log_warn "rsync not found; using scp (slower, includes node_modules)." + log_info "Syncing: $LOCAL_PATH -> root@$TARGET_IP:$REMOTE_PATH/" + ssh "root@$TARGET_IP" "mkdir -p $REMOTE_PATH" + scp -r "$LOCAL_PATH" "root@${TARGET_IP}:${REMOTE_PATH}/" + log_ok "Scp done." +fi + +# --- 5. Restart remote container --- +log_info "Restarting container: ssh root@$TARGET_IP 'cd $REMOTE_BASE/$AGENT_ID && docker compose restart'" +ssh "root@$TARGET_IP" "cd $REMOTE_BASE/$AGENT_ID && docker compose restart" + +log_ok "Sync and restart completed." diff --git a/scripts/tongge-fortune-simple.js b/scripts/tongge-fortune-simple.js new file mode 100644 index 0000000..ec0c612 --- /dev/null +++ b/scripts/tongge-fortune-simple.js @@ -0,0 +1,96 @@ +#!/usr/bin/env node + +// 简化版桐哥运势推送脚本 +// 直接使用 Tavily 搜索 + 黄历 API + +const { exec } = require('child_process'); +const fs = require('fs').promises; + +async function getDailyFortune() { + try { + // 获取明天日期 + const tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + const dateStr = tomorrow.toISOString().split('T')[0]; + + // 构建搜索查询 + const queries = [ + `金牛座 ${dateStr} 星座运势`, + `农历 ${dateStr} 黄历宜忌`, + `1984年5月16日生辰八字 ${dateStr} 运势` + ]; + + console.log(`[INFO] 开始获取 ${dateStr} 的运势信息...`); + + // 执行 Tavily 搜索(通过 OpenClaw CLI) + const results = []; + for (const query of queries) { + const result = await new Promise((resolve, reject) => { + exec(`openclaw --profile main web_search "${query}"`, + { timeout: 30000 }, + (error, stdout, stderr) => { + if (error) { + console.error(`[ERROR] 搜索失败: ${query}`, error); + resolve(`搜索 "${query}" 失败`); + } else { + resolve(stdout.trim()); + } + }); + }); + results.push(result); + } + + // 构建消息 + const message = ` +🌙 桐哥的每日运势提醒 + +📅 明日:${dateStr} +♈ 星座:金牛座 +📆 农历:待查询 + +✨ 星座运势: +${results[0]} + +🏮 黄历信息: +${results[1]} + +🔮 八字分析: +${results[2]} + +🎯 趋吉避凶建议: +- 根据今日运势调整计划 +- 注意情绪管理 +- 把握有利时机 + +祝您明日顺心如意!❤️ + `.trim(); + + // 保存到文件 + await fs.writeFile(`/tmp/tongge-fortune-${dateStr}.txt`, message); + console.log(`[INFO] 运势消息已生成: /tmp/tongge-fortune-${dateStr}.txt`); + + // 发送消息(通过 OpenClaw CLI) + await new Promise((resolve, reject) => { + exec(`openclaw --profile tongge message send --message "$(cat /tmp/tongge-fortune-${dateStr}.txt)" --channel telegram --to "tg:5237946060"`, + { shell: '/bin/bash' }, + (error, stdout, stderr) => { + if (error) { + console.error('[ERROR] 消息发送失败:', error); + reject(error); + } else { + console.log('[INFO] 消息发送成功'); + resolve(stdout); + } + }); + }); + + } catch (error) { + console.error('[FATAL] 运势获取失败:', error); + process.exit(1); + } +} + +// 执行主函数 +if (require.main === module) { + getDailyFortune(); +} \ No newline at end of file diff --git a/skills/active-learning/SKILL.md b/skills/active-learning/SKILL.md index 736b62e..0d12f45 100644 --- a/skills/active-learning/SKILL.md +++ b/skills/active-learning/SKILL.md @@ -18,16 +18,11 @@ ## 触发方式 -### Cron 定时触发 -```bash -# 每小时触发(7-23 点) -0 7-23 * * * /www/server/nodejs/v24.13.1/bin/openclaw --profile tongge active-learn -``` +### Cron 定时触发(OpenClaw 内置 Cron,当前方式) -### 手动触发 -```bash -openclaw --profile tongge active-learn -``` +任务配置在 `/root/.openclaw-tongge/cron/jobs.json`(jobId: `tongge-active-learning`),由桐哥 Gateway 直接调度。每小时触发,时区 Asia/Hong_Kong,7-23 点运行。 + +> **已废弃**:不再使用 `/etc/cron.d/tongge-learning` 系统 cron 方式。 --- @@ -47,14 +42,23 @@ openclaw --profile tongge active-learn ## 配置文件 ### Cron 配置 -位置:`/etc/cron.d/tongge-learning` - -```cron -# Tongge Active Learning - 每小时学习 (7-23 点) -SHELL=/bin/bash -PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/www/server/nodejs/v24.13.1/bin - -0 7-23 * * * root /www/server/nodejs/v24.13.1/bin/openclaw --profile tongge active-learn >> /var/log/tongge-learning.log 2>&1 +位置:`/root/.openclaw-tongge/cron/jobs.json`(OpenClaw 内置 Cron) + +```json +{ + "jobId": "tongge-active-learning", + "name": "桐哥主动学习", + "agentId": "tongge", + "schedule": { "kind": "cron", "expr": "0 7-23 * * *", "tz": "Asia/Hong_Kong" }, + "sessionTarget": "isolated", + "wakeMode": "next-heartbeat", + "payload": { + "kind": "agentTurn", + "message": "请主动学习一个你感兴趣的话题...", + "lightContext": false + }, + "delivery": { "mode": "none" } +} ``` ### 学习话题配置 diff --git a/skills/active-learning/cron b/skills/active-learning/cron index 4853e4c..8f0eb50 100644 --- a/skills/active-learning/cron +++ b/skills/active-learning/cron @@ -6,4 +6,4 @@ TZ=Asia/Hong_Kong # 香港时区 7-23 点,每小时触发 # 系统时区是 UTC,所以需要转换:香港 7-23 点 = UTC 23:00(前一日)-15:00 # 简单方案:脚本内部判断香港时区,cron 每小时都触发 -0 * * * * root /www/server/nodejs/v24.13.1/bin/node /root/.openclaw/workspace/skills/active-learning/learn.js >> /var/log/tongge-learning.log 2>&1 +0 * * * * root bash -c 'set -a; source /root/.openclaw-tongge/.env; set +a; exec /www/server/nodejs/v24.13.1/bin/node /root/.openclaw/workspace/skills/active-learning/learn.js' >> /var/log/tongge-learning.log 2>&1 diff --git a/skills/daily-horoscope/openclaw.plugin.json b/skills/daily-horoscope/openclaw.plugin.json index 09d7e31..f303705 100644 --- a/skills/daily-horoscope/openclaw.plugin.json +++ b/skills/daily-horoscope/openclaw.plugin.json @@ -1,36 +1,27 @@ { - "name": "daily-horoscope", + "id": "daily-horoscope", + "name": "Daily Horoscope", "version": "1.0.0", "description": "Daily horoscope and fortune analysis with Chinese almanac integration", "main": "index.js", - "type": "skill", + "type": "plugin", + "configSchema": { + "type": "object", + "properties": { + "enabled": { "type": "boolean", "default": true } + } + }, "tools": [ { "name": "getDailyHoroscope", - "description": "Get daily horoscope for all 12 zodiac signs and analyze with user's birth chart", + "description": "Get daily horoscope and fortune analysis", "inputSchema": { "type": "object", "properties": { - "date": { - "type": "string", - "description": "Target date in YYYY-MM-DD format" - }, - "userBirthInfo": { - "type": "object", - "properties": { - "birthday": { "type": "string" }, - "birthTime": { "type": "string" }, - "zodiacSign": { "type": "string" } - } - } - } + "date": { "type": "string", "description": "Target date YYYY-MM-DD" } + }, + "required": ["date"] } } - ], - "skills": { - "entries": { - "chinese-almanac": { "enabled": true }, - "tavily": { "enabled": true } - } - } + ] } diff --git a/skills/mem0-integration/SKILL.md b/skills/mem0-integration/SKILL.md index c1a8676..fd30626 100644 --- a/skills/mem0-integration/SKILL.md +++ b/skills/mem0-integration/SKILL.md @@ -56,18 +56,42 @@ skills/mem0-integration/ ## 环境变量 -| 变量名 | 用途 | 必需 | 默认值 | -|--------|------|------|--------| -| `MEM0_DASHSCOPE_API_KEY` | DashScope API 密钥 (LLM + Embedding) | 是 | — | -| `DASHSCOPE_API_KEY` | 备选 key 名称 (二选一) | — | — | -| `MEM0_QDRANT_HOST` | Qdrant 地址 | 否 | `localhost` | -| `MEM0_QDRANT_PORT` | Qdrant 端口 | 否 | `6333` | -| `MEM0_LLM_MODEL` | LLM 模型名 | 否 | `qwen-plus` | -| `MEM0_EMBEDDER_MODEL` | Embedding 模型名 | 否 | `text-embedding-v4` | +### API 端点与密钥(优先级链) -API 密钥查找顺序: `MEM0_DASHSCOPE_API_KEY` → `DASHSCOPE_API_KEY` → 已有 `OPENAI_API_KEY` +| 变量名 | 用途 | 优先级 | 来源 | +|--------|------|--------|------| +| `LLM_BASE_URL` | LLM/Embedding API 端点 | **最高** | `.env`(OneAPI 网关) | +| `LLM_API_KEY` | API 密钥 | **最高** | `.env`(OneAPI token) | +| `OPENAI_BASE_URL` | 备选端点(已有环境变量) | 次之 | — | +| `MEM0_DASHSCOPE_API_KEY` | 备选密钥(DashScope 直连) | 次之 | — | +| `DASHSCOPE_API_KEY` | 备选密钥 | 最低 | — | -DashScope 兼容模式需要同时设置 `OPENAI_API_BASE` 和 `OPENAI_BASE_URL`,代码在模块加载时自动完成。 +代码在模块加载时自动按优先级链解析,设置 `OPENAI_API_BASE` / `OPENAI_BASE_URL` / `OPENAI_API_KEY`,供 mem0 SDK 读取。**当前生产配置由 `.env` 中的 `LLM_BASE_URL` + `LLM_API_KEY` 驱动,指向本地 OneAPI 网关。** + +### 模型配置 + +| 变量名 | 用途 | 优先级 | 默认值 | +|--------|------|--------|--------| +| `MEM0_LLM_MODEL` | 记忆提取/合并用 LLM | 最高 | — | +| `LLM_MODEL_ID` | 备选 LLM 模型名 | 次之 | `qwen-plus` | +| `MEM0_EMBEDDER_MODEL` | Embedding 模型名 | 最高 | — | +| `EMBEDDING_MODEL_ID` | 备选 Embedding 模型名 | 次之 | `text-embedding-v4` | + +**当前生产值**(来自 `.env`):LLM = `MiniMax-M2.5`,Embedder = `text-embedding-v4`(1024 维)。 + +### Qdrant 连接 + +| 变量名 | 用途 | 默认值 | +|--------|------|--------| +| `MEM0_QDRANT_HOST` | Qdrant 地址 | `localhost` | +| `MEM0_QDRANT_PORT` | Qdrant 端口 | `6333` | + +### 两个模型的分工 + +| 模型 | 用途 | 调用时机 | +|------|------|----------| +| **LLM**(MiniMax-M2.5) | 从对话中**提取有价值的记忆**,去重/合并已有记忆,生成结构化摘要 | Post-Hook 写入时(后台异步) | +| **Embedder**(text-embedding-v4) | 将文本转成 1024 维向量,写入 Qdrant;Pre-Hook 检索时同样用于向量化查询 | 读写均需 | --- @@ -151,7 +175,13 @@ self.local_memory.add( ### 可见性自动分类 -`_classify_visibility()` 只返回 `"public"` 或 `"private"`(不自动推断 `"project"`)。项目级可见性必须由调用方通过 `context` 显式传入 `visibility="project"` + `project_id`。 +`_classify_visibility()` 返回 `(visibility, project_id)` 元组,支持三级自动推断: + +1. **public**:对话含 `所有人 / 通知 / 全局 / 公告 / 大家 / 集群` 等关键词 +2. **project**:对话匹配 `project_registry.yaml` 中 agent 所属项目的 name/description 关键词(如 `广告素材`→`advert`,`情感陪伴`→`life`) +3. **private**:默认回退 + +项目关键词在首次调用时从 `project_registry.yaml` 动态提取并缓存,修改注册表后重启生效。 ### 记忆写入过滤规则 @@ -188,6 +218,14 @@ pyyaml # YAML 配置解析 ## 更新记录 +### v2.2 (2026-03-16) +- **架构**: mem0 LLM + Embedder 全面切换到 OneAPI 网关路由,不再直连 DashScope +- **环境变量**: API 端点和密钥改为优先读取 `LLM_BASE_URL` / `LLM_API_KEY`(与 OpenClaw Agent 配置对齐);同时支持 `LLM_MODEL_ID` / `EMBEDDING_MODEL_ID` 覆盖模型名 +- **Layer 3 自动 Fallback**: Qdrant 不可达时(`_init_memory()` 失败或所有检索 phase 均报错),自动切换到 `LocalSearchFallback` FTS5 本地检索,无需手动干预 +- **可见性自动分类**: `_classify_visibility()` 现支持三级推断(public / project / private),从 `project_registry.yaml` 动态提取项目关键词实现 project 级自动归档 +- **memory_cleanup.py 修复**: 修复死代码导致 per-type 保留策略失效;修复过期判断使用 `expiration_date` 而非 `timestamp`;修复 `PointIdsList` 调用格式;移除无关的 DashScope 初始化 +- **自动清理 cron**: `/etc/cron.d/mem0-cleanup` 每天凌晨 3:00 UTC 自动执行 `--execute`,`expiration_date` 过期强制执行 + ### v2.1 (2026-03-01) - 修复: `_execute_search` filter 格式从 Qdrant 嵌套语法改为 mem0 扁平 dict - 修复: `_execute_write` 补充 `agent_id` 顶层参数 diff --git a/skills/mem0-integration/mem0_client.py b/skills/mem0-integration/mem0_client.py index 7d5156f..5801336 100644 --- a/skills/mem0-integration/mem0_client.py +++ b/skills/mem0-integration/mem0_client.py @@ -17,17 +17,23 @@ from collections import deque from datetime import datetime, timedelta from pathlib import Path -# ========== DashScope 环境变量配置 ========== -# Gemini Pro Embedding 模型:text-embedding-v4 (1024 维度) -os.environ['OPENAI_API_BASE'] = 'https://dashscope.aliyuncs.com/compatible-mode/v1' -os.environ['OPENAI_BASE_URL'] = 'https://dashscope.aliyuncs.com/compatible-mode/v1' # 关键:兼容模式需要此变量 -_dashscope_key = os.getenv('MEM0_DASHSCOPE_API_KEY', '') -if not _dashscope_key: - _dashscope_key = os.getenv('DASHSCOPE_API_KEY', '') -if _dashscope_key: - os.environ['OPENAI_API_KEY'] = _dashscope_key +# ========== LLM/Embedding API 配置 ========== +# 优先级链:OneAPI (.env) > 已有环境变量 > DashScope 默认 +_api_base = (os.getenv('LLM_BASE_URL') + or os.getenv('OPENAI_BASE_URL') + or os.getenv('OPENAI_API_BASE') + or 'https://dashscope.aliyuncs.com/compatible-mode/v1') +os.environ['OPENAI_API_BASE'] = _api_base +os.environ['OPENAI_BASE_URL'] = _api_base + +_api_key = (os.getenv('LLM_API_KEY') + or os.getenv('MEM0_DASHSCOPE_API_KEY') + or os.getenv('DASHSCOPE_API_KEY') + or '') +if _api_key: + os.environ['OPENAI_API_KEY'] = _api_key elif not os.environ.get('OPENAI_API_KEY'): - logging.warning("MEM0_DASHSCOPE_API_KEY not set; mem0 embedding/LLM calls will fail") + logging.warning("No API key found (LLM_API_KEY / MEM0_DASHSCOPE_API_KEY); mem0 calls will fail") try: from mem0 import Memory @@ -36,6 +42,8 @@ except ImportError as e: print(f"⚠️ mem0ai 导入失败:{e}") Memory = None +from local_search import LocalSearchFallback + logger = logging.getLogger(__name__) @@ -157,6 +165,33 @@ PUBLIC_KEYWORDS = re.compile( r'(所有人|通知|全局|公告|大家|集群)', ) +PROJECT_KEYWORDS_CACHE: Dict[str, List[str]] = {} + + +def _build_project_keywords() -> Dict[str, List[str]]: + """从 project_registry.yaml 提取每个项目的关键词用于自动分类""" + global PROJECT_KEYWORDS_CACHE + if PROJECT_KEYWORDS_CACHE: + return PROJECT_KEYWORDS_CACHE + registry = _load_project_registry() + projects = registry.get('projects', {}) + result: Dict[str, List[str]] = {} + for pid, pconf in projects.items(): + if pid == 'global': + continue + kws = [pid] + name = pconf.get('name', '') + if name and len(name) >= 2: + kws.append(name) + desc = pconf.get('description', '') + for seg in re.split(r'[,、。/\s]+', desc): + seg = seg.strip() + if len(seg) >= 2: + kws.append(seg) + result[pid] = kws + PROJECT_KEYWORDS_CACHE = result + return result + def _load_project_registry() -> Dict: """从 project_registry.yaml 加载项目注册表""" @@ -194,6 +229,7 @@ class Mem0Client: self.async_queue = None self.cache = {} self._started = False + self._local_search: Dict[str, LocalSearchFallback] = {} self._init_memory() def _load_default_config(self) -> Dict: @@ -207,13 +243,13 @@ class Mem0Client: "llm": { "provider": "openai", "config": { - "model": os.getenv('MEM0_LLM_MODEL', 'qwen-plus') + "model": os.getenv('MEM0_LLM_MODEL') or os.getenv('LLM_MODEL_ID', 'qwen-plus') } }, "embedder": { "provider": "openai", "config": { - "model": os.getenv('MEM0_EMBEDDER_MODEL', 'text-embedding-v4'), + "model": os.getenv('MEM0_EMBEDDER_MODEL') or os.getenv('EMBEDDING_MODEL_ID', 'text-embedding-v4'), "dimensions": 1024 # DashScope text-embedding-v4 支持的最大维度 } }, @@ -275,9 +311,8 @@ class Mem0Client: embedder=EmbedderConfig( provider="openai", config={ - "model": "text-embedding-v4", + "model": os.getenv('MEM0_EMBEDDER_MODEL') or os.getenv('EMBEDDING_MODEL_ID', 'text-embedding-v4'), "embedding_dims": 1024 # 核心修复:强制覆盖默认的 1536 维度 - # api_base 和 api_key 通过环境变量 OPENAI_BASE_URL 和 OPENAI_API_KEY 读取 } ) ) @@ -344,19 +379,43 @@ class Mem0Client: logger.warning(f"Pre-Hook 检索失败:{e}") return [] + def _get_local_search(self, agent_id: str) -> LocalSearchFallback: + """获取或创建 Layer 3 FTS5 实例(懒初始化 + 自动建索引)""" + aid = agent_id or 'main' + if aid not in self._local_search: + fb = LocalSearchFallback(agent_id=aid) + fb.rebuild_index() + self._local_search[aid] = fb + return self._local_search[aid] + async def _execute_search(self, query: str, user_id: str, agent_id: str, top_k: int) -> List[Dict]: """ 三阶段检索 — 按可见性分层,合并去重 Phase 1: public (所有 agent 可见) Phase 2: project (同 project_id 成员可见) Phase 3: private (仅 agent_id 本人可见) + Layer 3 Fallback: Qdrant 完全不可达时自动切换 FTS5 本地检索 """ - if self.local_memory is None: - return [] - all_memories: Dict[str, Dict] = {} per_phase = max(top_k, 3) - + qdrant_ok = False # 跟踪 Qdrant 是否至少有一次成功 + + if self.local_memory is None: + # mem0 未初始化(Qdrant 不可达),直接走 Layer 3 + logger.warning("mem0 未初始化,直接使用 Layer 3 FTS5 本地检索") + try: + fb = await asyncio.to_thread(self._get_local_search, agent_id) + local_results = await asyncio.to_thread(fb.search, query, top_k) + return [{ + 'id': f"fts5:{r.get('source', '')}:{r.get('title', '')}", + 'memory': r.get('snippet', ''), + 'score': 0.5, + 'metadata': {'source': 'layer3_fts5', 'file': r.get('source', '')}, + } for r in local_results] + except Exception as e: + logger.error(f"Layer 3 FTS5 fallback 失败:{e}") + return [] + # Phase 1: 检索 public 记忆 try: public_mems = await asyncio.to_thread( @@ -366,13 +425,14 @@ class Mem0Client: filters={"visibility": "public"}, limit=per_phase ) + qdrant_ok = True for mem in (public_mems or []): mid = mem.get('id') if isinstance(mem, dict) else None if mid and mid not in all_memories: all_memories[mid] = mem except Exception as e: logger.debug(f"Public 记忆检索失败:{e}") - + # Phase 2: 检索 project 记忆 (agent 所属的所有项目) if agent_id and agent_id != 'general': agent_projects = get_agent_projects(agent_id) @@ -390,13 +450,14 @@ class Mem0Client: }, limit=per_phase ) + qdrant_ok = True for mem in (proj_mems or []): mid = mem.get('id') if isinstance(mem, dict) else None if mid and mid not in all_memories: all_memories[mid] = mem except Exception as e: logger.debug(f"Project({project_id}) 记忆检索失败:{e}") - + # Phase 3: 检索 private 记忆 if agent_id and agent_id != 'general': try: @@ -410,13 +471,14 @@ class Mem0Client: }, limit=per_phase ) + qdrant_ok = True for mem in (private_mems or []): mid = mem.get('id') if isinstance(mem, dict) else None if mid and mid not in all_memories: all_memories[mid] = mem except Exception as e: logger.debug(f"Private 记忆检索失败:{e}") - + # Fallback: 兼容旧数据(无 visibility 字段) if user_id: try: @@ -427,13 +489,32 @@ class Mem0Client: filters={"agent_id": agent_id} if agent_id and agent_id != 'general' else None, limit=per_phase ) + qdrant_ok = True for mem in (legacy_mems or []): mid = mem.get('id') if isinstance(mem, dict) else None if mid and mid not in all_memories: all_memories[mid] = mem except Exception as e: logger.debug(f"Legacy 记忆检索失败:{e}") - + + # Layer 3 Fallback: Qdrant 完全不可达时自动切换 FTS5 + if not qdrant_ok: + logger.warning("Qdrant 不可达,自动切换 Layer 3 FTS5 本地检索") + try: + fb = await asyncio.to_thread(self._get_local_search, agent_id) + local_results = await asyncio.to_thread(fb.search, query, top_k) + for r in local_results: + fid = f"fts5:{r.get('source', '')}:{r.get('title', '')}" + if fid not in all_memories: + all_memories[fid] = { + 'id': fid, + 'memory': r.get('snippet', ''), + 'score': 0.5, + 'metadata': {'source': 'layer3_fts5', 'file': r.get('source', '')}, + } + except Exception as e: + logger.error(f"Layer 3 FTS5 fallback 失败:{e}") + min_confidence = self.config['retrieval']['min_confidence'] filtered = [ m for m in all_memories.values() @@ -483,12 +564,23 @@ class Mem0Client: return 'knowledge' return 'session' - def _classify_visibility(self, user_message: str, assistant_message: str, agent_id: str = None) -> str: - """自动分类记忆可见性""" + def _classify_visibility(self, user_message: str, assistant_message: str, agent_id: str = None): + """自动分类记忆可见性,返回 (visibility, project_id)""" combined = user_message + ' ' + assistant_message if PUBLIC_KEYWORDS.search(combined): - return 'public' - return 'private' + return 'public', None + + # 检查是否匹配 agent 所属项目的关键词 + if agent_id and agent_id != 'general': + proj_kws = _build_project_keywords() + agent_projects = get_agent_projects(agent_id) + for pid in agent_projects: + if pid == 'global' or pid not in proj_kws: + continue + if any(kw in combined for kw in proj_kws[pid]): + return 'project', pid + + return 'private', None def post_hook_add(self, user_message: str, assistant_message: str, user_id: str = None, agent_id: str = None, @@ -514,7 +606,9 @@ class Mem0Client: if not memory_type: memory_type = self._classify_memory_type(user_message, assistant_message) if not visibility: - visibility = self._classify_visibility(user_message, assistant_message, agent_id) + visibility, auto_project_id = self._classify_visibility(user_message, assistant_message, agent_id) + if not project_id and auto_project_id: + project_id = auto_project_id messages = [ {"role": "user", "content": user_message}, diff --git a/skills/mem0-integration/memory_cleanup.py b/skills/mem0-integration/memory_cleanup.py index 339b59e..58cfa6b 100644 --- a/skills/mem0-integration/memory_cleanup.py +++ b/skills/mem0-integration/memory_cleanup.py @@ -1,21 +1,24 @@ #!/usr/bin/env python3 """ Memory cleanup and audit script. +此脚本只操作 Qdrant,无需 LLM/Embedding API key。 Stats mode (default / --dry-run): python3 memory_cleanup.py --dry-run Cleanup mode (requires --execute): - python3 memory_cleanup.py --execute --max-age-days 90 + python3 memory_cleanup.py --execute Retention policy (aligned with EXPIRATION_MAP in mem0_client.py): - session -> 7 days (written with expiration_date, but Qdrant does NOT auto-delete) + session -> 7 days chat_summary -> 30 days preference -> never auto-delete knowledge -> never auto-delete -The --max-age-days flag is a hard ceiling: any session or chat_summary older -than that threshold is removed regardless of its expiration_date. +删除逻辑优先级: + 1. payload 中存在 expiration_date 字段 → 按该字段判断是否过期 + 2. 无 expiration_date → 按 timestamp(写入时间)+ RETENTION_DAYS 判断 + 3. --max-age-days 可强制覆盖所有类型的阈值(用于紧急清理) """ import os @@ -26,16 +29,10 @@ import yaml from pathlib import Path from datetime import datetime, timedelta, timezone -_dashscope_key = os.getenv('MEM0_DASHSCOPE_API_KEY', '') or os.getenv('DASHSCOPE_API_KEY', '') -if _dashscope_key: - os.environ['OPENAI_API_KEY'] = _dashscope_key -os.environ.setdefault('OPENAI_API_BASE', 'https://dashscope.aliyuncs.com/compatible-mode/v1') -os.environ.setdefault('OPENAI_BASE_URL', 'https://dashscope.aliyuncs.com/compatible-mode/v1') - try: from qdrant_client import QdrantClient from qdrant_client.models import ( - Filter, FieldCondition, MatchValue, FilterSelector, + Filter, FieldCondition, MatchValue, PointIdsList, ) except ImportError: print("qdrant-client not installed") @@ -95,10 +92,25 @@ def get_stats(client: QdrantClient): return total.count -def _find_expired_points(client: QdrantClient, memory_type: str, max_age_days: int): - """Scroll through points of a given memory_type and return IDs older than max_age_days.""" - cutoff = datetime.now(timezone.utc) - timedelta(days=max_age_days) - cutoff_iso = cutoff.isoformat() +def _parse_dt(s: str) -> datetime: + """将 ISO 字符串解析为 aware datetime(UTC)。""" + s = s.replace('Z', '+00:00') + dt = datetime.fromisoformat(s) + if dt.tzinfo is None: + dt = dt.replace(tzinfo=timezone.utc) + return dt + + +def _find_expired_points(client: QdrantClient, memory_type: str, cutoff_days: int): + """ + 扫描指定 memory_type 的记录,返回已过期的 ID 列表。 + + 优先级: + 1. payload['expiration_date'] 存在 → 与当前时间比较 + 2. 不存在 → 用 payload['timestamp'] + cutoff_days 判断 + """ + now = datetime.now(timezone.utc) + cutoff_by_age = now - timedelta(days=cutoff_days) expired_ids = [] offset = None @@ -117,14 +129,28 @@ def _find_expired_points(client: QdrantClient, memory_type: str, max_age_days: i break for point in results: payload = point.payload or {} - ts = payload.get('timestamp') or payload.get('created_at', '') - if not ts: + + # 优先读取 expiration_date 字段(mem0 写入时设置) + exp_raw = (payload.get('expiration_date') + or payload.get('data', {}).get('expiration_date')) + if exp_raw: + try: + if now > _parse_dt(str(exp_raw)): + expired_ids.append(point.id) + except (TypeError, ValueError): + pass + continue # 有 expiration_date 就不再走 timestamp fallback + + # Fallback:按写入时间 + RETENTION_DAYS 判断 + ts_raw = payload.get('timestamp') or payload.get('created_at', '') + if not ts_raw: continue try: - if ts < cutoff_iso: + if _parse_dt(str(ts_raw)) < cutoff_by_age: expired_ids.append(point.id) except (TypeError, ValueError): continue + if next_offset is None: break offset = next_offset @@ -132,17 +158,22 @@ def _find_expired_points(client: QdrantClient, memory_type: str, max_age_days: i return expired_ids -def cleanup_expired(client: QdrantClient, max_age_days: int, execute: bool): - """Identify and optionally delete expired session/chat_summary memories.""" +def cleanup_expired(client: QdrantClient, max_age_days: int | None, execute: bool): + """ + 识别并可选删除过期的 session/chat_summary 记忆。 + + cutoff_days 逻辑: + - max_age_days 为 None → 使用 RETENTION_DAYS 中的每类型默认值 + - max_age_days 有值 → 强制覆盖所有类型(用于紧急清理) + """ total_deleted = 0 results_summary = [] for memory_type, default_days in RETENTION_DAYS.items(): - effective_days = min(max_age_days, default_days * 4) if max_age_days else default_days - effective_days = max_age_days + cutoff_days = max_age_days if max_age_days is not None else default_days - logger.info(f"\nScanning memory_type={memory_type} (cutoff: {effective_days} days)...") - expired_ids = _find_expired_points(client, memory_type, effective_days) + logger.info(f"\nScanning memory_type={memory_type} (cutoff: {cutoff_days} days)...") + expired_ids = _find_expired_points(client, memory_type, cutoff_days) if not expired_ids: logger.info(f" No expired {memory_type} memories found.") @@ -157,7 +188,7 @@ def cleanup_expired(client: QdrantClient, max_age_days: int, execute: bool): batch = expired_ids[i:i + batch_size] client.delete( collection_name=COLLECTION, - points_selector=batch, + points_selector=PointIdsList(points=batch), ) logger.info(f" DELETED {len(expired_ids)} {memory_type} memories") total_deleted += len(expired_ids) @@ -191,8 +222,8 @@ def main(): help='Show stats and expired counts without deleting (default behavior)') parser.add_argument('--execute', action='store_true', help='Actually delete expired memories (requires this flag)') - parser.add_argument('--max-age-days', type=int, default=90, - help='Delete session/chat_summary older than N days (default: 90)') + parser.add_argument('--max-age-days', type=int, default=None, + help='强制覆盖所有类型的阈值(天数);不指定则按 RETENTION_DAYS 每类型策略') args = parser.parse_args() if args.execute and args.dry_run: @@ -208,10 +239,12 @@ def main(): logger.info("=" * 60) logger.info(f"Memory Cleanup - {datetime.now().strftime('%Y-%m-%d %H:%M')}") logger.info(f"Mode: {'EXECUTE' if execute else 'DRY-RUN (use --execute to delete)'}") - logger.info(f"Max age: {args.max_age_days} days") - logger.info(f"Retention: session={RETENTION_DAYS['session']}d, " - f"chat_summary={RETENTION_DAYS['chat_summary']}d, " - f"preference=permanent, knowledge=permanent") + if args.max_age_days is not None: + logger.info(f"Max age override: {args.max_age_days} days (all types)") + else: + logger.info(f"Retention: session={RETENTION_DAYS['session']}d, " + f"chat_summary={RETENTION_DAYS['chat_summary']}d, " + f"preference=permanent, knowledge=permanent") logger.info("=" * 60) total_before = get_stats(client) diff --git a/skills/shared/README.md b/skills/shared/README.md new file mode 100644 index 0000000..a10ff6e --- /dev/null +++ b/skills/shared/README.md @@ -0,0 +1,13 @@ +# Skills 共享模块 + +供各 Skill 复用的公共代码。 + +## llm_client.js + +OpenClaw 与 OneAPI 网关对接的共享 LLM 客户端:按模型名调用 OpenAI 兼容的 Chat Completions API。 + +- **环境变量**:`LLM_BASE_URL`、`LLM_API_KEY` +- **导出**:`callSpecificModel(modelName, messages, options)`(不支持 stream,支持 `timeoutMs`) +- **错误与日志**:非 200 与超时时会打印 `[LLM_Client] Error calling :` 并抛出带详情的 Error + +完整架构与使用说明见:[workspace/docs/LLM_GATEWAY_AND_SKILL_CLIENT.md](../../docs/LLM_GATEWAY_AND_SKILL_CLIENT.md) diff --git a/skills/shared/llm_client.js b/skills/shared/llm_client.js new file mode 100644 index 0000000..fe06eea --- /dev/null +++ b/skills/shared/llm_client.js @@ -0,0 +1,130 @@ +/** + * Shared LLM client for OpenClaw Skills. + * Calls OneAPI (or any OpenAI-compatible) gateway with a specific model. + * Requires: LLM_BASE_URL, LLM_API_KEY in environment. + */ + +const LOG_PREFIX = '[LLM_Client]'; + +/** + * Build chat completions URL from base. Avoids double /v1 when user sets + * LLM_BASE_URL to e.g. http://100.x:3000/v1. + * @param {string} baseUrl - LLM_BASE_URL (may end with / or /v1) + * @returns {string} full URL for POST + */ +function buildChatCompletionsUrl(baseUrl) { + const base = (baseUrl || '').replace(/\/+$/, ''); + if (!base) return ''; + if (base.endsWith('/v1')) { + return `${base}/chat/completions`; + } + return `${base}/v1/chat/completions`; +} + +/** + * Parse OneAPI/OpenAI error body for message. + * @param {string} text - response text + * @returns {string} error message for display + */ +function parseErrorBody(text) { + if (!text || typeof text !== 'string') return String(text || 'Unknown error'); + try { + const obj = JSON.parse(text); + if (obj.error && typeof obj.error === 'object' && typeof obj.error.message === 'string') return obj.error.message; + if (obj.error && typeof obj.error === 'string') return obj.error; + if (typeof obj.message === 'string') return obj.message; + return text; + } catch { + return text; + } +} + +/** + * Call a specific model via the configured LLM gateway (OpenAI Chat Completions). + * @param {string} modelName - Model id (e.g. "qwen3.5-plus", "claude-3-sonnet") + * @param {Array<{role: string, content: string}>} messages - Chat messages + * @param {object} options - Optional: temperature, max_tokens, stream, timeoutMs (client-only, default 60000) + * @returns {Promise} Parsed JSON response (e.g. choices, usage) + * @throws {Error} On HTTP error or timeout; message/cause include OneAPI error details + */ +async function callSpecificModel(modelName, messages, options = {}) { + if (!Array.isArray(messages)) { + const err = new Error('messages must be an array'); + err.code = 'LLM_CLIENT_CONFIG'; + throw err; + } + if (options.stream === true) { + const err = new Error('Stream mode is not supported by this client; use stream: false or omit'); + err.code = 'LLM_CLIENT_CONFIG'; + throw err; + } + + const baseUrl = (process.env.LLM_BASE_URL || '').trim(); + const apiKey = (process.env.LLM_API_KEY || '').trim(); + + if (!baseUrl || !apiKey) { + const err = new Error('LLM_BASE_URL and LLM_API_KEY must be set in environment'); + err.code = 'LLM_CLIENT_CONFIG'; + throw err; + } + + const url = buildChatCompletionsUrl(baseUrl); + if (!url) { + const err = new Error('Invalid LLM_BASE_URL'); + err.code = 'LLM_CLIENT_CONFIG'; + throw err; + } + + const timeoutMs = options.timeoutMs != null ? Number(options.timeoutMs) : 60000; + const { timeoutMs: _drop, ...bodyOptions } = options; + const body = { + model: modelName, + messages, + ...bodyOptions, + }; + + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeoutMs); + + try { + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${apiKey}`, + }, + body: JSON.stringify(body), + signal: controller.signal, + }); + + clearTimeout(timeoutId); + + if (!response.ok) { + const rawText = await response.text(); + const errorSummary = parseErrorBody(rawText); + const err = new Error(`LLM gateway error (${response.status}): ${errorSummary}`); + err.status = response.status; + err.body = rawText; + err.cause = { message: errorSummary, status: response.status }; + console.error(`${LOG_PREFIX} Error calling ${modelName}: ${errorSummary}`); + throw err; + } + + return await response.json(); + } catch (err) { + clearTimeout(timeoutId); + if (err.name === 'AbortError') { + const timeoutErr = new Error(`LLM request timed out after ${timeoutMs}ms`); + timeoutErr.code = 'ETIMEDOUT'; + timeoutErr.cause = err; + console.error(`${LOG_PREFIX} Error calling ${modelName}: timeout (${timeoutMs}ms)`); + throw timeoutErr; + } + throw err; + } +} + +module.exports = { + callSpecificModel, + buildChatCompletionsUrl, +};