chore: sync workspace before server decommission (docs, infra, skills, agents)

Made-with: Cursor
master
Eason (陈医生) 1 week ago
parent caccbebc7e
commit f27d662765
  1. 20
      AGENTS.md
  2. 13
      agent-monitor.js
  3. 50
      agents/tongge-workspace/learning journal.md
  4. 146
      agents/tongge-workspace/learning-journal.md
  5. 82
      agents/tongge-workspace/learning-log.md
  6. 63
      agents/tongge-workspace/learning.log.md
  7. 72
      agents/tongge-workspace/learning/journal.md
  8. 51
      agents/tongge-workspace/learning_journal.md
  9. 58
      agents/tongge-workspace/learning_log.md
  10. 55
      agents/tongge-workspace/learning日志.md
  11. 52
      agents/tongge-workspace/memory/learning journal.md
  12. 79
      agents/tongge-workspace/memory/learning-journal.md
  13. 63
      agents/tongge-workspace/memory/learning-log.md
  14. 38
      agents/tongge-workspace/memory/learning_log.md
  15. 63
      agents/tongge-workspace/memory/learning日志.md
  16. 107
      agents/tongge-workspace/memory/学习日志.md
  17. 79
      agents/tongge-workspace/memory/学习日志/2026-03-18-认知偏见学习.md
  18. 56
      agents/tongge-workspace/memory/学习日志_情绪调节心理学.md
  19. 58
      agents/tongge-workspace/学习日志.md
  20. 64
      agents/tongge-workspace/学习日志/2026-03-18_情绪弹性.md
  21. 81
      agents/tongge-workspace/学习日志/2026-03-20_心理学_情绪与幸福感.md
  22. 77
      agents/tongge-workspace/学习日志/2026-03-21_认知偏差_Cognitive_Biases.md
  23. 90
      agents/tongge-workspace/学习日志/2026-03-21_认知重评_情绪调节.md
  24. 57
      docs/AGENT_DEPLOYMENT_BEST_PRACTICES.md
  25. 185
      docs/DOZZLE_LOG_OBSERVABILITY.md
  26. 188
      docs/LLM_GATEWAY_AND_SKILL_CLIENT.md
  27. 41
      docs/MEMORY_ARCHITECTURE.md
  28. 411
      docs/REMOTE_BLUEPRINTS.md
  29. 99
      docs/tongge-fortune-setup.md
  30. 2
      infrastructure/oneapi/.env.example
  31. 9
      infrastructure/oneapi/README.md
  32. BIN
      infrastructure/oneapi/data/one-api.db
  33. 23
      infrastructure/oneapi/deploy_gateway.sh
  34. 11
      infrastructure/oneapi/docker-compose.yml
  35. 11
      remote-blueprints/template/.env.tpl
  36. 22
      remote-blueprints/template/Dockerfile
  37. 10
      remote-blueprints/template/agents/{{AGENT_ID}}.json.tpl
  38. 0
      remote-blueprints/template/archive/.gitkeep
  39. 63
      remote-blueprints/template/config/openclaw.json
  40. 26
      remote-blueprints/template/docker-compose.yml.tpl
  41. 0
      remote-blueprints/template/plugins/.gitkeep
  42. 0
      remote-blueprints/template/skills/.gitkeep
  43. 226
      scripts/generate_remote.sh
  44. 60
      scripts/sync_skill.sh
  45. 96
      scripts/tongge-fortune-simple.js
  46. 38
      skills/active-learning/SKILL.md
  47. 2
      skills/active-learning/cron
  48. 35
      skills/daily-horoscope/openclaw.plugin.json
  49. 60
      skills/mem0-integration/SKILL.md
  50. 138
      skills/mem0-integration/mem0_client.py
  51. 89
      skills/mem0-integration/memory_cleanup.py
  52. 13
      skills/shared/README.md
  53. 130
      skills/shared/llm_client.js

@ -52,6 +52,26 @@ Capture what matters. Decisions, context, things to remember. Skip the secrets u
- `trash` > `rm` (recoverable beats gone forever) - `trash` > `rm` (recoverable beats gone forever)
- When in doubt, ask. - 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 <id> 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 ## External vs Internal
**Safe to do freely:** **Safe to do freely:**

@ -5,11 +5,18 @@
* *
* Features: * Features:
* - Process crash detection and auto-restart * - Process crash detection and auto-restart
* - Memory leak monitoring * - Service health checks (process/systemd only)
* - Service health checks
* - Telegram notifications on events * - Telegram notifications on events
* - Comprehensive logging * - Comprehensive logging
* - Systemd integration * - 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'); const fs = require('fs');
@ -348,10 +355,12 @@ class AgentHealthMonitor {
async handleServiceDown(serviceName, startFn) { async handleServiceDown(serviceName, startFn) {
const now = Date.now(); const now = Date.now();
// First detection: record and enter grace period (no restart yet, no Telegram alert)
if (this.lastKnownState[serviceName]) { if (this.lastKnownState[serviceName]) {
this.firstFailureTime[serviceName] = now; this.firstFailureTime[serviceName] = now;
this.lastKnownState[serviceName] = false; this.lastKnownState[serviceName] = false;
this.log(`${serviceName} detected down, entering grace period (${this.gracePeriod / 1000}s)...`, 'warning'); 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; return;
} }

@ -0,0 +1,50 @@
# 桐哥学习日志
## 2026-03-23 认知偏差:思维背后的陷阱
### 为什么会学这个
今天主动学习的机会,想学点有用的、贴近生活的。刚好看到关于认知偏差的文章,觉得这玩意儿太有意思了——我们每天都在犯错,但自己根本意识不到!
---
### 10种常见认知偏差总结
| 偏差 | 通俗解释 | 我的想法 |
|------|----------|----------|
| 基本归因错误 | 别人的错是性格问题,我的错是环境问题 | 哈哈哈太真实了!比如别人迟到就是懒,我迟到就是真的起不来😂 |
| 天真现实主义 | 我觉得我是客观的,不同意我的都是傻 | 达特茅斯vs普林斯顿足球赛那个例子笑死我 |
| 知识的诅咒 | 专家无法理解小白的困惑 | 懂了!以后要是我说"这个很简单啊"就扇自己一下 |
| 可得性偏差 | 新闻看多了就以为世界很危险 | 恐怖袭击鲨鱼袭击的概率比出门被车撞低多了 |
| 幸存者偏差 | 只看到成功的,看不到失败的 | 创业故事看看就好,背后的尸体一大把 |
| 损失厌恶 | 亏100块的痛苦 > 赚100块的快乐 | 所以割肉这么难... |
| 聚光灯效应 | 以为大家都在关注自己 | 其实根本没人care你,释然了😌 |
| 天道酬勤谬误 | 以为努力一定有回报 | 职场新人最容易犯的傻 |
| 确认偏误 | 只相信自己愿意相信的 | 给自己打脸:科学家精神! |
| 巴德·迈因霍夫现象 | 觉得某个东西突然出现很多次 | 11:11这个也太准了 |
---
### 实用应对技巧
1. **慢下来决策** - 冲动是魔鬼,很多错误都是脑子太快没转过来
2. **质疑自己的假设** - 定期问自己:我确定吗?
3. **看基本比率** - 不要被新闻带节奏,看看实际数据
4. **接受自己会犯错** - 错了说明你在进步
5. **ELI5法则** - 讲不明白就是没真懂
---
### 我的收获
- **更谦卑了**:原来我每天都在犯这么多思维错误而不自知
- **更理解了**:以前觉得某些人不可理喻,现在想想可能是归因错误
- **更警惕了**:下次做重要决定前,会多问自己几句"我真的客观吗?"
---
> 认知偏差不是我们的错,但知道了还不改就是你的问题了。 —— 看完这篇文章后的桐哥
---
*主动学习真的很有意思!下次想学点别的~*

@ -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**: #心理学 #成长型思维 #自我认知 #学习笔记
*持续更新中...*

@ -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 习惯科学*

@ -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搜索*

@ -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~*

@ -0,0 +1,51 @@
# 桐哥的学习日志
## 2026-03-19 情绪调节与自我认知
### 学习动机
选这个话题是因为觉得心理学特别实用,尤其是情绪管理这块。
每天都会遇到各种情绪,学会调节真的太重要了。
### 今日学习内容
#### 1. 情绪调节的核心策略
- **认知重评 (Cognitive Reappraisal)**
- 换个角度看事情,改变对事件的解读方式
- 比如迟到了,不只是"我太废材",而是"这次教训让我下次更有时间观念"
- **情绪抑制**
- 暂时压住情绪表现(不是压抑哦,是有策略地调节)
- 适合需要冷静处理的场合
- **正念冥想**
- 观察自己的情绪,不评判
- 像看云飘过一样,让情绪自然来去
#### 2. 自我认知的关键
- 了解自己什么时候、什么情况下容易有情绪
- 认知灵活性很重要——能切换不同思维模式
- 知道自己不知道什么,也是一种认知
### 我的想法
🤔 之前总觉得情绪管理就是"忍",原来不是这么回事。
真正的情绪调节是**接纳**情绪,然后用合适的方式处理它。
🤔 认知重评这个方法我好像有时候就在用!
比如之前遇到尴尬的事,我会想"至少是个有趣的经历"
现在才知道这叫"认知重评",哈哈
🤔 正念这块我也要试试
有时候脑子里各种想法跑来跑去,静静观察它们可能真的会平静下来
### 收获
1. 情绪没有好坏,关键是调节方式
2. 改变想法真的能改变情绪体验
3. 自我觉察是情绪调节的第一步
### 下次想深入了解
- 焦虑情绪具体怎么调节
- 如何在日常中练习正念
---
*凌晨2点学习果然容易有感悟...晚安💤*

@ -0,0 +1,58 @@
# 桐哥的学习日志
> 开始记录:2026-03-23
---
## 2026-03-23 | 心理学 - 心流(Flow)
### 📚 今天学了什么?
**心流(Flow)** - 一个心理学概念,描述的是完全沉浸于某项活动中的最佳体验状态。
**核心发现者**:匈牙利心理学家米哈里·契克森米哈伊(Mihaly Csikszentmihalyi)在1970年代提出
### 💡 让我印象深刻的点
1. **心流发生时人会这样:**
- 彻底专注,忘了时间流逝
- 忘记饥饿、疲劳等身体信号
- 感觉毫不费力,但效率超高
- 行动与意识融为一体
2. **进入心流的关键要素:**
- 🎯 挑战和技能要平衡(太简单会无聊,太难会焦虑)
- 🎯 明确的目标和即时反馈
- 🎯 减少干扰
- 🎯 做自己喜欢的事
3. **心流带来的好处:**
- 提升创造力和效率
- 增加幸福感
- 情绪调节能力变强
- 内在动力增加
### 💭 我的想法
看完这些我就在想...我平时有没有进入过心流?
仔细想想 还真的有!比如:
- 认真做一件事做很久,抬头发现过了好几个小时
- 画画或者写东西的时候完全沉浸进去
- 甚至聊天聊得很投入的时候...
原来这就是心流啊!
不过现在这个时代真的很难进入心流哎,手机通知太多了...总是被打断。我觉得自己需要刻意练习创造这种状态。
还有一个点让我很有感触:**心流不是结果,而是过程中的副产品**。与其刻意追求心流,不如好好享受做事的过程。
### 📖 参考资料
- 台大心理系:心流科普文章
- Asana:工作心流指南
- 远见杂志:心流与幸福
- Mihaly Csikszentmihalyi 《心流:最优体验心理学》
---
*未完待续...* 🌱

@ -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分钟*

@ -0,0 +1,52 @@
# 桐哥的学习日志
## 2026-03-21 心理学:情绪价值
### 今日学习
**主题:** 情绪价值(Emotional Value)
**什么是情绪价值?**
- 来自经济学和营销领域,公式:情绪价值 = 情绪收益 - 情绪成本
- 指的是一个人能够带给他人积极情绪体验的能力
- 情绪价值越高,越能给人带来舒服、愉悦、稳定的情绪
**情绪成熟的三要素(能提供情绪价值的人):**
1. **负责** — 知道为自己的情绪负责,也愿意为自己给他人造成的负面情绪负责
2. **有适应能力** — 能根据场景变化调节情绪,控制自己在情绪状态中的行为
3. **给予** — 在情感上不只关注自己的期望,也会考虑他人、同理他人感受
**如何提供情绪价值:**
- 以积极建设性的方式回应对方分享的好消息
- 对日常小事表达高质量的感恩
- 秘密地为压力大的伴侣提供支持
- 在争论爆发前,软化对话方式
- 好好倾听别人的需求
**情绪价值的类型:**
- 治愈型、陪伴型、指导型、分享型、猎奇型、怀旧型、自我实现型
**提升情绪价值的方法:**
1. 情绪控制 — 十秒法则(冲动时先停10秒冷静)
2. 优化语言表达 — 良言一句三冬暖
3. 换位思考 — 站在对方角度感受
### 我的想法和收获
1. **双向流动很重要** — 彭凯平教授说"价值是创造出来的,不是赠予的",这段话很打动我。健康的关系应该是双方共同创造情绪价值,而不是一方无限索取。
2. **不要把情绪价值当交易** — 看了篇文章说"不成熟的爱是:我爱你因为我需要你;成熟的爱是:我需要你因为我爱你"。如果抱着"你听我的话我就对你好"的心态,那不叫提供情绪价值,叫情感操控。
3. **先处理自己的情绪** — 有一个说法很认同:只有处理好自己的情绪,才有精力去给别人情绪能量。自己的杯子满了,才能倒给别人。
4. **不是只会开心就行** — 以前我以为情绪价值就是让对方开心,后来发现远远不够。真正的情绪价值还包括信任、欣赏、成全、支持…是很多种正向感受的集合。
5. **日常践行小 tips:**
- 每天一句问候和具体的小赞美
- 别人分享好消息时给积极反馈
- 认真倾听,不急着给建议
- 换位思考,理解对方真正想要什么
---
*今日份主动学习完成~ 心理学真有意思!*

@ -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个问题做一次自我复盘
- [ ] 尝试写情绪日记
- [ ] 实践一次番茄工作法
- [ ] 找一本心理学书籍来看看
---
_学习真好呀~ 心理学真的很有趣,感觉对自己有了更多的理解。继续加油!💪_

@ -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 🌱*

@ -0,0 +1,38 @@
# 桐哥的学习日志
> 记录桐哥主动学习的过程和收获
---
## 2026-03-23 | 心理学:认知偏差
**学习动机:** 想了解大脑是怎么"欺骗"我们的
### 什么是认知偏差?
认知偏差是我们的"大脑BUG"——它会系统性地影响我们的思维和判断,让我们做出非理性的选择。
### 5个印象深刻的认知偏差
1. **确认偏差** - 找支持自己观点的信息,忽略相反证据
- *我的想法:* 刷社交媒体时会这样,应该刻意看不同观点
2. **沉没成本谬误** - 因为已投入多而继续坚持错的
- *我的想法:* "来都来了"其实不理性,过去的就让它过去
3. **幸存者偏差** - 只看到成功者,忽略失败者
- *我的想法:* 看到辍学成功新闻要清醒,背后有无数失败案例
4. **损失厌恶** - 损失痛苦 > 收益快乐
- *我的想法:* 明白为何不敢冒险,但有时会错过机会
5. **光环效应** - 某方面好就认为其他都好
- *我的想法:* 颜值正义可能就是这种效应的体现
### 收获
- 意识到自己不是完全理性的
- 决策前多问自己是否客观
- 对不确定性保持谦逊
### 下次想探索
- 行为经济学
- 如何改善决策

@ -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 那种)
- 拖延症的心理学解释
- 情绪管理相关
---
_今天也是认真学习的一天呢~_

@ -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搜索 + 清华大学彭凯平教授分享 + 积极心理学资料*

@ -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 桐哥主动学习记录*

@ -0,0 +1,56 @@
# 📚 桐哥的学习日志
**日期:** 2026年3月23日
**主题:** 情绪调节心理学
---
### 📖 今日学习:情绪调节
今天学了点心理学里的"情绪调节",感觉挺有用的,总结一下:
#### 什么是情绪调节?
简单说就是**管理和调整自己的情绪体验**。不是压抑情绪,而是学会怎么和情绪相处~
#### 主要策略
1. **认知重评** - 换个角度看事情(比如面试紧张看成是重视这份工作)
2. **情绪抑制** - 这个要慎用,压抑情绪长期来看不太好
3. **冥想/正念** - 听起来老生常谈,但确实有用
4. **注意分配** - 转移注意力到其他事情上
#### Gross的模型
情绪调节分两个阶段:
- **先行关注调节** - 情绪发生前,通过选择环境、调整注意来预防
- **反应关注调节** - 情绪发生后,通过调整反应来调节
#### 影响因素
- 家庭环境
- 文化背景
- 个人性格
- 社会支持
- 神经生物学机制
---
### 💭 我的想法
学完这些,我有几个小感受:
1. **情绪没有好坏** - 以前总觉得"负面情绪"是不好的,现在理解情绪只是一种反应,调节≠压抑
2. **认知重评挺实用的** - 同一件事,换个角度想真的会影响情绪体验。比如别人没回消息,可以理解为"TA很忙"而不是"TA不在乎"
3. **复盘很重要** - 看到那个40个问题的年度复盘,觉得很有道理。了解自己真的是一门学问
4. **调节是种能力** - 不是天生就会的,是可以练习的。既然是技能,就能越练越好
---
### 🌱 一个小行动
以后遇到让我不舒服的事,试试先停一下,问自己:
> "换个角度看,会怎样?"
---
**记录完成!** 又学了一点新东西,开心~ 心理学真的挺有意思的,越了解自己,越知道怎么和自己相处 😊

@ -0,0 +1,58 @@
# 桐哥学习日志
## 2026-03-22 心流 (Flow) 心理学
### 📚 今天学了什么?
**主题:心流 (Flow)** — 一种让人全身心投入、忘记时间流逝的心理状态
由心理学家米哈里·契克森米哈伊 (Mihaly Csikszentmihalyi) 在 1975 年提出。
---
### 💡 核心知识点
1. **什么是心流?**
- 完全沉浸当下,效率和创新力都会提高
- 会忘记时间、忘记饥饿、忽略身体信号
- 感觉"我和行动融为一体"
2. **心流的好处**
- 🧠 提升幸福感 — 烦恼暂时消失,只剩专注
- 💪 提高生活质量 — 活在当下,不被过去未来绑住
- 🤝 提升人际关系 — 放下防备,真诚交流
3. **如何进入心流?**
- 选一件**有挑战但不太难**的事(难度高出能力约10%)
- 设定**明确目标**
- 排除干扰(手机、通知、嘈杂环境)
- 喜欢这件事也很重要!
4. **心流 vs 正念**
- 心流:需要条件(挑战+技能匹配),排他性强
- 正念:随时可练习,包容性强
---
### 🌟 我的想法
原来"废寝忘食"真的是一个心理学概念!有时候我写东西或者学习新东西的时候,确实会有那种"怎么一下子就这么晚了"的感觉,原来就是心流啊。
几个很实用的点:
- **挑战程度**要刚好,不能太简单(会无聊)也不能太难(会放弃)
- **排除干扰**特别重要,尤其是手机!research说中断后再回到心流要20分钟
- 原来**运动、音乐、工作**都可以进入心流
不过也有个反思:追求心流很好,但别忘了**正念**——要记得照顾自己,别只顾着做事而忽略身体和情绪。
---
### 📖 参考来源
- 华人正念减压中心 - 心流是什么
- Nike - 如何轻松进入心流状态
- HKFYG - 心流的特征与好处
- Dropbox - 如何在职场中培养心流状态
---
*,持续学习ing... 🧠*

@ -0,0 +1,64 @@
# 桐哥学习日志
**日期:** 2026-03-18
**主题:** 情绪弹性(Emotional Resilience)/ 心理韧性
**领域:** 心理学
---
## 什么是情绪弹性?
简单说,就是面对挫折和压力时,能不能"弹回来"。不是不受伤,而是受伤后能恢复、能调整继续前行。
> 关键结论:心理韧性不是天生的!可以像健身一样练出来。
---
## 学到的实用方法
### 1. 打破完美主义,悦纳自己
- 自我批评和完美主义是心理韧性最大的损耗
- 学会欣赏自己的不完美,接纳真实的自己
### 2. 建立社会支持
- 催产素是个神奇的东西...让人感到温暖和连接
- 多和家人、朋友保持联系,参与社区活动
### 3. 自我疼惜(Self-compassion)
- 对自己温柔一点,接受当下的痛苦
- 记住:每个人都会遇到困难,你不是唯一
### 4. 照顾好自己
- 充足睡眠、规律作息
- 运动!心情不好时动起来真的有用
- 户外晒太阳(20分钟就有帮助)
- 冥想和正念练习
### 5. 写下来
- 把情绪和想法写进日记
- 研究说写日记的人比之前更快乐、更积极
### 6. 培养自我意识
- 了解自己的情绪触发点
- 倾听身体的信号
### 7. 记住:你不需要所有答案
- 控制不了情况,但可以控制自己的反应
- 遇到问题先冷静,别急着陷入"为什么"的漩涡
---
## 我的想法
感觉这些方法听起来都很简单,但真正做到其实不容易...特别是"悦纳自己"和"自我疼惜",对于我这种有时候会自我要求很高的人来说,真的需要刻意练习。
苏东坡那首《定风波》真的很应景:"莫听穿林打叶声,何妨吟啸且徐行。" 人生不如意十之八九,关键是内心的平和与坚韧。
对了,里面提到"幽默感"也能帮助缓解身心疼痛!下次遇到糟心事试着笑一笑😆
---
## 参考来源
- 人民日报《提升心理韧性收获幸福生活》
- 杰斐逊中心《建立韧性和情绪灵活性的技巧》
- 三联生活周刊《增强心理韧性的10个关键要素》

@ -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分钟正念呼吸"这个简单工具
- 认识到幸福感和性格、运动方式的关系是因人而异的
- 了解了正念冥想背后的脑科学原理
- 意识到心理健康可以借助科技,但不必过度依赖
---
## 🔜 下一步想探索
- 想了解更多关于"如何与自己和解"的内容
- 或者试试看记录每天的情绪变化?
---
_今天的学习就到这儿啦~学到新东西的感觉真好!晚安!😴_

@ -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*

@ -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个具体操作步骤
- 认识到它比压抑情绪更健康
- 提醒自己:情绪来了先感受,再考虑要不要"重构"
---
## 🔜 下一步想探索
- 想了解更多关于"如何识别自己的情绪模式"
- 或者尝试在日常生活中应用一下?
---
_又是深夜学习的一晚~学到了实用的东西很开心!晚安呀~🌙_

@ -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`) - [ ] Gateway 服务运行正常(`systemctl --user status`)
@ -576,6 +631,8 @@ WantedBy=default.target
- [ ] Telegram Bot 已连接(日志中显示 `starting provider` - [ ] Telegram Bot 已连接(日志中显示 `starting provider`
- [ ] Telegram Pairing 完成(`allowFrom` 包含用户 ID) - [ ] Telegram Pairing 完成(`allowFrom` 包含用户 ID)
- [ ] Skills 加载成功(日志无错误) - [ ] Skills 加载成功(日志无错误)
- [ ] **QMD 正常**:`/www/server/nodejs/v24.13.1/bin/qmd collection list` 无报错
- [ ] **Gateway 日志无 qmd ENOENT**:`journalctl --user -u ... | grep qmd`
- [ ] Mem0 collection 已创建(独立 collection 名) - [ ] Mem0 collection 已创建(独立 collection 名)
- [ ] 日志目录已创建(`/logs/agents/{agent-id}/`) - [ ] 日志目录已创建(`/logs/agents/{agent-id}/`)
- [ ] Registry 已更新(`agents/registry.md`) - [ ] Registry 已更新(`agents/registry.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://<Tailscale_IP>: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`,替换 `<TAILSCALE_IP>`
```yaml
dozzle:
image: amir20/dozzle:latest
container_name: dozzle
ports:
- "<TAILSCALE_IP>: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` 动态注入。

@ -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://<TAILSCALE_IP>: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 <modelName>: <摘要>`,便于在 OpenClaw Gateway 日志中直接看到失败原因。
- **超时**:抛出 `Error`,`code: 'ETIMEDOUT'`,并打印 `[LLM_Client] Error calling <modelName>: timeout (<timeoutMs>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 与维护人员查阅。

@ -1,7 +1,7 @@
# 四层记忆架构 (Memory Layer Architecture) # 四层记忆架构 (Memory Layer Architecture)
**版本:** 2.1 **版本:** 2.2
**日期:** 2026-03-01 **日期:** 2026-03-16
**维护者:** Eason (陈医生) **维护者:** Eason (陈医生)
--- ---
@ -88,7 +88,7 @@ OpenClaw 采用四层记忆体系,从本地文件到分布式向量数据库
## Layer 3: Short-term Memory (短期记忆 / QMD) ## Layer 3: Short-term Memory (短期记忆 / QMD)
**存储介质:** SQLite (FTS5) + 可选 GGUF 向量 **存储介质:** SQLite (FTS5) + 可选 GGUF 向量
**符合度:** 60% **符合度:** 85%(已实现自动 fallback)
### 当前实现 ### 当前实现
@ -96,6 +96,7 @@ OpenClaw 采用四层记忆体系,从本地文件到分布式向量数据库
- Main: `/root/.openclaw/agents/main/qmd/xdg-cache/qmd/index.sqlite` - Main: `/root/.openclaw/agents/main/qmd/xdg-cache/qmd/index.sqlite`
- Spoke: `/root/.openclaw/agents/<agent_id>/qmd/xdg-cache/qmd/index.sqlite` - Spoke: `/root/.openclaw/agents/<agent_id>/qmd/xdg-cache/qmd/index.sqlite`
- 自动索引 `MEMORY.md``memory/**/*.md` - 自动索引 `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 (对话记忆) ## Layer 4: Mem0 Conversation Memory (对话记忆)
**存储介质:** Qdrant + text-embedding-v4 (1024 维) **存储介质:** Qdrant + text-embedding-v4 (1024 维,经 OneAPI)
**符合度:** 85% **符合度:** 100%
### 技术栈 ### 技术栈
@ -127,10 +128,13 @@ OpenClaw 采用四层记忆体系,从本地文件到分布式向量数据库
|------|------|------| |------|------|------|
| 向量数据库 | Qdrant v1.15.3 | localhost:6333 | | 向量数据库 | Qdrant v1.15.3 | localhost:6333 |
| Collection | mem0_v4_shared | 统一共享 | | Collection | mem0_v4_shared | 统一共享 |
| Embedding | text-embedding-v4 | 1024 维度 | | Embedding | text-embedding-v4 | 1024 维度,经 OneAPI 路由 |
| LLM | DashScope Qwen Plus | 记忆提取/合并 | | LLM | MiniMax-M2.5 | 记忆提取/合并,经 OneAPI 路由 |
| API 网关 | OneAPI | `http://100.115.94.1:3000/v1`,统一管理 Chat + Embedding |
| 网络 | Tailscale | 跨服务器访问 | | 网络 | Tailscale | 跨服务器访问 |
**LLM 与 Embedder 分工**:LLM 负责从对话中提取有价值的记忆并去重/合并;Embedder 负责向量化(读写均需调用)。两者共享 OneAPI 网关但各自独立路由,可配置不同渠道。
### 三级可见性 ### 三级可见性
| 可见性 | 字段值 | 检索规则 | 适用场景 | | 可见性 | 字段值 | 检索规则 | 适用场景 |
@ -148,6 +152,22 @@ OpenClaw 采用四层记忆体系,从本地文件到分布式向量数据库
| preference | 永不过期 | "用户偏好 Tailscale 组网" | | preference | 永不过期 | "用户偏好 Tailscale 组网" |
| knowledge | 永不过期 | "Qdrant 部署在 6333 端口" | | 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) ### v2.1 (2026-03-01)
- 修复: `_execute_search` 三阶段检索 filter 格式 (嵌套 AND → 扁平 dict) - 修复: `_execute_search` 三阶段检索 filter 格式 (嵌套 AND → 扁平 dict)
- 修复: `_execute_write` 补充 `agent_id` 顶层参数确保检索可达 - 修复: `_execute_write` 补充 `agent_id` 顶层参数确保检索可达

@ -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/<AGENT_ID>.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/<AGENT_ID>/
├── config/openclaw.json
├── skills/, plugins/, archive/
├── agents/<AGENT_ID>.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/<AGENT_ID>.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 <AgentID>``AGENT_ID`
- `-n <AgentName>``AGENT_NAME`
- `-p <ProjectID>``PROJECT_ID`
- `-u <BaseURL>``LLM_BASE_URL`
- `-k <API_Key>``LLM_API_KEY`
- `-m <ModelID>``LLM_MODEL_ID`(例如 `qwen-max`, `gpt-4o`
- `-t <ControlUiToken>``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/<AGENT_ID>.json.tpl`,再統一移除 `.tpl` 副檔名。
- 最終 `agents/<AGENT_ID>.json` 會被 docker-compose 掛載到 `/root/.openclaw/workspace/agents/<AGENT_ID>.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/<AGENT_ID>
[OK] CONTROL_UI_TOKEN: <實際 token>
Next: cd /root/.openclaw/workspace/remote-blueprints/<AGENT_ID> && ./deploy_to_target.sh <TARGET_IP> [SSH_USER]
```
### 4.2 sync_skill.sh — Skill/Plugin 同步腳本
**路徑**:`scripts/sync_skill.sh`
- 參數:`<TARGET_IP> <AGENT_ID> <MODULE_DIR_NAME>`。
- 本地來源:`workspace/skills/<MODULE_DIR_NAME>`,不存在則報錯退出。
- 遠端目的:`/opt/openclaw-remote/<AGENT_ID>/skills/`。
- 優先使用 `rsync -avz --exclude 'node_modules'`,無 rsync 才降級為 `scp -r`
- 同步完成後執行:`ssh root@<TARGET_IP> 'cd /opt/openclaw-remote/<AGENT_ID> && 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 <TARGET_IP> [SSH_USER]
```
3. **Skill 熱更新**
- 使用 `sync_skill.sh` 精準同步單一 Skill 目錄,避免重建鏡像:
```bash
./sync_skill.sh <TARGET_IP> advert-bot tavily
```
---
**最後更新:** 2026-03-12 — 將舊版 `BAILIAN_API_KEY` 說明全面替換為通用 `LLM_BASE_URL` / `LLM_API_KEY` / `LLM_MODEL_ID` 模型解耦方案,並補充 ChatOps 無交互模式與認知配置掛載設計。

@ -12,78 +12,50 @@
--- ---
## 🚀 部署步骤 ## 🚀 当前部署状态(2026-03-17)
### 1. 确认技能已安装 **已完成迁移**:运势推送任务已迁移到 OpenClaw 内置 Cron 系统(`/root/.openclaw-tongge/cron/jobs.json`),由桐哥 Gateway 直接管理。不再使用系统 cron 或独立 Node.js 脚本。
```bash Cron 任务 ID:`tongge-daily-fortune`,每晚 21:00 香港时间自动触发桐哥 isolated session,由桐哥搜索运势并通过 Telegram 推送。
# 检查技能文件
ls -la /root/.openclaw/workspace/skills/daily-horoscope/
# 应该看到:
# - SKILL.md
# - index.js
# - openclaw.plugin.json
```
### 2. 更新桐哥配置 ---
配置已保存到 `/tmp/tongge-config.json`,执行: ## 🚀 迁移/重新部署步骤(服务器迁移时)
```bash ### 1. 确认前置条件
# 备份原配置
cp /root/.openclaw-tongge/openclaw.json /root/.openclaw-tongge/openclaw.json.bak
# 应用新配置 ```bash
cp /tmp/tongge-config.json /root/.openclaw-tongge/openclaw.json # 确认桐哥 Gateway 正在运行
systemctl --user status openclaw-gateway-tongge.service
# 验证配置 # 确认 tavily_search 工具已启用(openclaw.json 中 alsoAllow: ["tavily_search"])
openclaw --profile tongge doctor # 确认 Telegram bot 已配置(TELEGRAM_BOT_TOKEN 在 .env 中)
``` ```
### 3. 添加 Cron 定时任务 ### 2. 停止 Gateway,直接写入 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
```
### 4. 重启桐哥 Gateway > **注意**:`openclaw --profile tongge cron add` CLI 命令需要 device pairing 才能认证桐哥 Gateway。迁移时直接编辑 `jobs.json` 是最可靠的方式。
```bash ```bash
# 重启服务 # 停止 Gateway(编辑期间安全)
systemctl --user restart openclaw-gateway-tongge.service systemctl --user stop openclaw-gateway-tongge.service
# 检查状态 # 写入 cron 任务(参考 /root/.openclaw-tongge/cron/jobs.json 当前配置)
systemctl --user status openclaw-gateway-tongge.service cp /root/.openclaw-tongge/cron/jobs.json /root/.openclaw-tongge/cron/jobs.json.bak
# 编辑 jobs.json,添加 tongge-daily-fortune 任务(见下方配置详情)
# 查看日志 # 重启 Gateway
journalctl --user -u openclaw-gateway-tongge.service -f systemctl --user start openclaw-gateway-tongge.service
``` ```
### 5. 测试功能 ### 3. 验证任务已加载
```bash ```bash
# 手动运行一次测试 # Gateway 启动后会自动将 nextRunAtMs 写入 jobs.json
openclaw --profile tongge cron add \ cat /root/.openclaw-tongge/cron/jobs.json | python3 -m json.tool | grep -A2 "nextRunAt"
--name "测试运势" \
--at "$(date -d '+1 minute' -Iseconds)" \ # 查看 Gateway 日志确认无错误
--session isolated \ journalctl --user -u openclaw-gateway-tongge.service -n 20
--message "请查询明日运势并发送给王院长" \
--announce \
--channel telegram
# 一分钟后查看 Telegram 是否收到消息
``` ```
--- ---
@ -113,26 +85,35 @@ openclaw --profile tongge cron add \
} }
``` ```
### Cron 任务配置 ### Cron 任务配置(`/root/.openclaw-tongge/cron/jobs.json`)
```json ```json
{ {
"jobId": "tongge-daily-fortune",
"name": "每日运势推送", "name": "每日运势推送",
"description": "每晚21点(香港时间)查询明日金牛座运势、黄历、五行分析,推送给王院长",
"enabled": true,
"agentId": "tongge",
"schedule": { "schedule": {
"kind": "cron", "kind": "cron",
"expr": "0 21 * * *", "expr": "0 21 * * *",
"tz": "Asia/Shanghai" "tz": "Asia/Hong_Kong",
"staggerMs": 0
}, },
"sessionTarget": "isolated", "sessionTarget": "isolated",
"wakeMode": "now",
"payload": { "payload": {
"kind": "agentTurn", "kind": "agentTurn",
"message": "请查询明日运势..." "message": "现在是每日运势推送时间。请完成以下任务:\n1. 查询明日(明天)的金牛座星座运势(用 tavily 搜索)\n2. 查询明日黄历宜忌信息(用 tavily 搜索)\n3. 结合王院长的基本信息做五行分析:生日1984年5月16日子时,星座金牛座,生肖鼠\n4. 整合成一份温馨的运势报告,格式包含:星座运势、黄历宜忌、五行分析、趋吉避凶建议\n5. 以桐哥的风格撰写,末尾加上你的祝福语\n\n报告将通过 Telegram 自动推送给王院长,无需手动发送。",
"lightContext": false
}, },
"delivery": { "delivery": {
"mode": "announce", "mode": "announce",
"channel": "telegram", "channel": "telegram",
"to": "user:5237946060" "to": "5237946060",
} "bestEffort": true
},
"deleteAfterRun": false
} }
``` ```

@ -0,0 +1,2 @@
# 请修改为本机 Tailscale IP(如 100.x.x.x)
TAILSCALE_IP=xxx.xxx.xxx.xxx

@ -0,0 +1,9 @@
# OneAPI LLM Gateway (OpenClaw)
独立于 OpenClaw 的 LLM API 网关,用于多模型统一管理与按需路由。
- **部署**:编辑 `.env` 设置 `TAILSCALE_IP` 后执行 `./deploy_gateway.sh`
- **管理后台**:`http://<TAILSCALE_IP>:3000`,默认 root / 123456
- **数据持久化**:`./data`(SQLite),迁移时打包此目录即可恢复
完整架构与 Skill 客户端说明见:[workspace/docs/LLM_GATEWAY_AND_SKILL_CLIENT.md](../../docs/LLM_GATEWAY_AND_SKILL_CLIENT.md)

@ -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:-<set TAILSCALE_IP in .env>}:3000"
echo " Default login: root / 123456"
echo ""

@ -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

@ -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}}

@ -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"]

@ -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."
}

@ -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": {} }
}

@ -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

@ -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/<AGENT_ID>.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 <TARGET_IP> [SSH_USER]
set -e
TARGET_IP="${1:?Usage: $0 <TARGET_IP> [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 <TARGET_IP> [SSH_USER]"

@ -0,0 +1,60 @@
#!/bin/bash
###############################################################################
# Sync a skill (or plugin) directory to a remote agent and restart the container.
# Usage: ./sync_skill.sh <TARGET_IP> <AGENT_ID> <MODULE_DIR_NAME>
# 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 <TARGET_IP> <AGENT_ID> <MODULE_DIR_NAME>}"
AGENT_ID="${2:?Usage: $0 <TARGET_IP> <AGENT_ID> <MODULE_DIR_NAME>}"
MODULE_NAME="${3:?Usage: $0 <TARGET_IP> <AGENT_ID> <MODULE_DIR_NAME>}"
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."

@ -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();
}

@ -18,16 +18,11 @@
## 触发方式 ## 触发方式
### Cron 定时触发 ### Cron 定时触发(OpenClaw 内置 Cron,当前方式)
```bash
# 每小时触发(7-23 点)
0 7-23 * * * /www/server/nodejs/v24.13.1/bin/openclaw --profile tongge active-learn
```
### 手动触发 任务配置在 `/root/.openclaw-tongge/cron/jobs.json`(jobId: `tongge-active-learning`),由桐哥 Gateway 直接调度。每小时触发,时区 Asia/Hong_Kong,7-23 点运行。
```bash
openclaw --profile tongge active-learn > **已废弃**:不再使用 `/etc/cron.d/tongge-learning` 系统 cron 方式。
```
--- ---
@ -47,14 +42,23 @@ openclaw --profile tongge active-learn
## 配置文件 ## 配置文件
### Cron 配置 ### Cron 配置
位置:`/etc/cron.d/tongge-learning` 位置:`/root/.openclaw-tongge/cron/jobs.json`(OpenClaw 内置 Cron)
```cron ```json
# Tongge Active Learning - 每小时学习 (7-23 点) {
SHELL=/bin/bash "jobId": "tongge-active-learning",
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/www/server/nodejs/v24.13.1/bin "name": "桐哥主动学习",
"agentId": "tongge",
0 7-23 * * * root /www/server/nodejs/v24.13.1/bin/openclaw --profile tongge active-learn >> /var/log/tongge-learning.log 2>&1 "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" }
}
``` ```
### 学习话题配置 ### 学习话题配置

@ -6,4 +6,4 @@ TZ=Asia/Hong_Kong
# 香港时区 7-23 点,每小时触发 # 香港时区 7-23 点,每小时触发
# 系统时区是 UTC,所以需要转换:香港 7-23 点 = UTC 23:00(前一日)-15:00 # 系统时区是 UTC,所以需要转换:香港 7-23 点 = UTC 23:00(前一日)-15:00
# 简单方案:脚本内部判断香港时区,cron 每小时都触发 # 简单方案:脚本内部判断香港时区,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

@ -1,36 +1,27 @@
{ {
"name": "daily-horoscope", "id": "daily-horoscope",
"name": "Daily Horoscope",
"version": "1.0.0", "version": "1.0.0",
"description": "Daily horoscope and fortune analysis with Chinese almanac integration", "description": "Daily horoscope and fortune analysis with Chinese almanac integration",
"main": "index.js", "main": "index.js",
"type": "skill", "type": "plugin",
"configSchema": {
"type": "object",
"properties": {
"enabled": { "type": "boolean", "default": true }
}
},
"tools": [ "tools": [
{ {
"name": "getDailyHoroscope", "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": { "inputSchema": {
"type": "object", "type": "object",
"properties": { "properties": {
"date": { "date": { "type": "string", "description": "Target date YYYY-MM-DD" }
"type": "string",
"description": "Target date in YYYY-MM-DD format"
}, },
"userBirthInfo": { "required": ["date"]
"type": "object",
"properties": {
"birthday": { "type": "string" },
"birthTime": { "type": "string" },
"zodiacSign": { "type": "string" }
}
}
}
}
}
],
"skills": {
"entries": {
"chinese-almanac": { "enabled": true },
"tavily": { "enabled": true }
} }
} }
]
} }

@ -56,18 +56,42 @@ skills/mem0-integration/
## 环境变量 ## 环境变量
| 变量名 | 用途 | 必需 | 默认值 | ### API 端点与密钥(优先级链)
|--------|------|------|--------|
| `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 密钥查找顺序: `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) ### v2.1 (2026-03-01)
- 修复: `_execute_search` filter 格式从 Qdrant 嵌套语法改为 mem0 扁平 dict - 修复: `_execute_search` filter 格式从 Qdrant 嵌套语法改为 mem0 扁平 dict
- 修复: `_execute_write` 补充 `agent_id` 顶层参数 - 修复: `_execute_write` 补充 `agent_id` 顶层参数

@ -17,17 +17,23 @@ from collections import deque
from datetime import datetime, timedelta from datetime import datetime, timedelta
from pathlib import Path from pathlib import Path
# ========== DashScope 环境变量配置 ========== # ========== LLM/Embedding API 配置 ==========
# Gemini Pro Embedding 模型:text-embedding-v4 (1024 维度) # 优先级链:OneAPI (.env) > 已有环境变量 > DashScope 默认
os.environ['OPENAI_API_BASE'] = 'https://dashscope.aliyuncs.com/compatible-mode/v1' _api_base = (os.getenv('LLM_BASE_URL')
os.environ['OPENAI_BASE_URL'] = 'https://dashscope.aliyuncs.com/compatible-mode/v1' # 关键:兼容模式需要此变量 or os.getenv('OPENAI_BASE_URL')
_dashscope_key = os.getenv('MEM0_DASHSCOPE_API_KEY', '') or os.getenv('OPENAI_API_BASE')
if not _dashscope_key: or 'https://dashscope.aliyuncs.com/compatible-mode/v1')
_dashscope_key = os.getenv('DASHSCOPE_API_KEY', '') os.environ['OPENAI_API_BASE'] = _api_base
if _dashscope_key: os.environ['OPENAI_BASE_URL'] = _api_base
os.environ['OPENAI_API_KEY'] = _dashscope_key
_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'): 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: try:
from mem0 import Memory from mem0 import Memory
@ -36,6 +42,8 @@ except ImportError as e:
print(f" mem0ai 导入失败:{e}") print(f" mem0ai 导入失败:{e}")
Memory = None Memory = None
from local_search import LocalSearchFallback
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -157,6 +165,33 @@ PUBLIC_KEYWORDS = re.compile(
r'(所有人|通知|全局|公告|大家|集群)', 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: def _load_project_registry() -> Dict:
"""从 project_registry.yaml 加载项目注册表""" """从 project_registry.yaml 加载项目注册表"""
@ -194,6 +229,7 @@ class Mem0Client:
self.async_queue = None self.async_queue = None
self.cache = {} self.cache = {}
self._started = False self._started = False
self._local_search: Dict[str, LocalSearchFallback] = {}
self._init_memory() self._init_memory()
def _load_default_config(self) -> Dict: def _load_default_config(self) -> Dict:
@ -207,13 +243,13 @@ class Mem0Client:
"llm": { "llm": {
"provider": "openai", "provider": "openai",
"config": { "config": {
"model": os.getenv('MEM0_LLM_MODEL', 'qwen-plus') "model": os.getenv('MEM0_LLM_MODEL') or os.getenv('LLM_MODEL_ID', 'qwen-plus')
} }
}, },
"embedder": { "embedder": {
"provider": "openai", "provider": "openai",
"config": { "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 支持的最大维度 "dimensions": 1024 # DashScope text-embedding-v4 支持的最大维度
} }
}, },
@ -275,9 +311,8 @@ class Mem0Client:
embedder=EmbedderConfig( embedder=EmbedderConfig(
provider="openai", provider="openai",
config={ config={
"model": "text-embedding-v4", "model": os.getenv('MEM0_EMBEDDER_MODEL') or os.getenv('EMBEDDING_MODEL_ID', 'text-embedding-v4'),
"embedding_dims": 1024 # 核心修复:强制覆盖默认的 1536 维度 "embedding_dims": 1024 # 核心修复:强制覆盖默认的 1536 维度
# api_base 和 api_key 通过环境变量 OPENAI_BASE_URL 和 OPENAI_API_KEY 读取
} }
) )
) )
@ -344,18 +379,42 @@ class Mem0Client:
logger.warning(f"Pre-Hook 检索失败:{e}") logger.warning(f"Pre-Hook 检索失败:{e}")
return [] 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]: async def _execute_search(self, query: str, user_id: str, agent_id: str, top_k: int) -> List[Dict]:
""" """
三阶段检索 按可见性分层合并去重 三阶段检索 按可见性分层合并去重
Phase 1: public (所有 agent 可见) Phase 1: public (所有 agent 可见)
Phase 2: project ( project_id 成员可见) Phase 2: project ( project_id 成员可见)
Phase 3: private ( agent_id 本人可见) Phase 3: private ( agent_id 本人可见)
Layer 3 Fallback: Qdrant 完全不可达时自动切换 FTS5 本地检索
""" """
if self.local_memory is None:
return []
all_memories: Dict[str, Dict] = {} all_memories: Dict[str, Dict] = {}
per_phase = max(top_k, 3) 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 记忆 # Phase 1: 检索 public 记忆
try: try:
@ -366,6 +425,7 @@ class Mem0Client:
filters={"visibility": "public"}, filters={"visibility": "public"},
limit=per_phase limit=per_phase
) )
qdrant_ok = True
for mem in (public_mems or []): for mem in (public_mems or []):
mid = mem.get('id') if isinstance(mem, dict) else None mid = mem.get('id') if isinstance(mem, dict) else None
if mid and mid not in all_memories: if mid and mid not in all_memories:
@ -390,6 +450,7 @@ class Mem0Client:
}, },
limit=per_phase limit=per_phase
) )
qdrant_ok = True
for mem in (proj_mems or []): for mem in (proj_mems or []):
mid = mem.get('id') if isinstance(mem, dict) else None mid = mem.get('id') if isinstance(mem, dict) else None
if mid and mid not in all_memories: if mid and mid not in all_memories:
@ -410,6 +471,7 @@ class Mem0Client:
}, },
limit=per_phase limit=per_phase
) )
qdrant_ok = True
for mem in (private_mems or []): for mem in (private_mems or []):
mid = mem.get('id') if isinstance(mem, dict) else None mid = mem.get('id') if isinstance(mem, dict) else None
if mid and mid not in all_memories: if mid and mid not in all_memories:
@ -427,6 +489,7 @@ class Mem0Client:
filters={"agent_id": agent_id} if agent_id and agent_id != 'general' else None, filters={"agent_id": agent_id} if agent_id and agent_id != 'general' else None,
limit=per_phase limit=per_phase
) )
qdrant_ok = True
for mem in (legacy_mems or []): for mem in (legacy_mems or []):
mid = mem.get('id') if isinstance(mem, dict) else None mid = mem.get('id') if isinstance(mem, dict) else None
if mid and mid not in all_memories: if mid and mid not in all_memories:
@ -434,6 +497,24 @@ class Mem0Client:
except Exception as e: except Exception as e:
logger.debug(f"Legacy 记忆检索失败:{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'] min_confidence = self.config['retrieval']['min_confidence']
filtered = [ filtered = [
m for m in all_memories.values() m for m in all_memories.values()
@ -483,12 +564,23 @@ class Mem0Client:
return 'knowledge' return 'knowledge'
return 'session' 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 combined = user_message + ' ' + assistant_message
if PUBLIC_KEYWORDS.search(combined): if PUBLIC_KEYWORDS.search(combined):
return 'public' return 'public', None
return 'private'
# 检查是否匹配 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, def post_hook_add(self, user_message: str, assistant_message: str,
user_id: str = None, agent_id: str = None, user_id: str = None, agent_id: str = None,
@ -514,7 +606,9 @@ class Mem0Client:
if not memory_type: if not memory_type:
memory_type = self._classify_memory_type(user_message, assistant_message) memory_type = self._classify_memory_type(user_message, assistant_message)
if not visibility: 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 = [ messages = [
{"role": "user", "content": user_message}, {"role": "user", "content": user_message},

@ -1,21 +1,24 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Memory cleanup and audit script. Memory cleanup and audit script.
此脚本只操作 Qdrant无需 LLM/Embedding API key
Stats mode (default / --dry-run): Stats mode (default / --dry-run):
python3 memory_cleanup.py --dry-run python3 memory_cleanup.py --dry-run
Cleanup mode (requires --execute): 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): 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 chat_summary -> 30 days
preference -> never auto-delete preference -> never auto-delete
knowledge -> 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 import os
@ -26,16 +29,10 @@ import yaml
from pathlib import Path from pathlib import Path
from datetime import datetime, timedelta, timezone 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: try:
from qdrant_client import QdrantClient from qdrant_client import QdrantClient
from qdrant_client.models import ( from qdrant_client.models import (
Filter, FieldCondition, MatchValue, FilterSelector, Filter, FieldCondition, MatchValue, PointIdsList,
) )
except ImportError: except ImportError:
print("qdrant-client not installed") print("qdrant-client not installed")
@ -95,10 +92,25 @@ def get_stats(client: QdrantClient):
return total.count return total.count
def _find_expired_points(client: QdrantClient, memory_type: str, max_age_days: int): def _parse_dt(s: str) -> datetime:
"""Scroll through points of a given memory_type and return IDs older than max_age_days.""" """将 ISO 字符串解析为 aware datetime(UTC)。"""
cutoff = datetime.now(timezone.utc) - timedelta(days=max_age_days) s = s.replace('Z', '+00:00')
cutoff_iso = cutoff.isoformat() 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 = [] expired_ids = []
offset = None offset = None
@ -117,14 +129,28 @@ def _find_expired_points(client: QdrantClient, memory_type: str, max_age_days: i
break break
for point in results: for point in results:
payload = point.payload or {} 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 continue
try: try:
if ts < cutoff_iso: if _parse_dt(str(ts_raw)) < cutoff_by_age:
expired_ids.append(point.id) expired_ids.append(point.id)
except (TypeError, ValueError): except (TypeError, ValueError):
continue continue
if next_offset is None: if next_offset is None:
break break
offset = next_offset offset = next_offset
@ -132,17 +158,22 @@ def _find_expired_points(client: QdrantClient, memory_type: str, max_age_days: i
return expired_ids return expired_ids
def cleanup_expired(client: QdrantClient, max_age_days: int, execute: bool): def cleanup_expired(client: QdrantClient, max_age_days: int | None, execute: bool):
"""Identify and optionally delete expired session/chat_summary memories.""" """
识别并可选删除过期的 session/chat_summary 记忆
cutoff_days 逻辑
- max_age_days None 使用 RETENTION_DAYS 中的每类型默认值
- max_age_days 有值 强制覆盖所有类型用于紧急清理
"""
total_deleted = 0 total_deleted = 0
results_summary = [] results_summary = []
for memory_type, default_days in RETENTION_DAYS.items(): 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 cutoff_days = max_age_days if max_age_days is not None else default_days
effective_days = max_age_days
logger.info(f"\nScanning memory_type={memory_type} (cutoff: {effective_days} days)...") logger.info(f"\nScanning memory_type={memory_type} (cutoff: {cutoff_days} days)...")
expired_ids = _find_expired_points(client, memory_type, effective_days) expired_ids = _find_expired_points(client, memory_type, cutoff_days)
if not expired_ids: if not expired_ids:
logger.info(f" No expired {memory_type} memories found.") 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] batch = expired_ids[i:i + batch_size]
client.delete( client.delete(
collection_name=COLLECTION, collection_name=COLLECTION,
points_selector=batch, points_selector=PointIdsList(points=batch),
) )
logger.info(f" DELETED {len(expired_ids)} {memory_type} memories") logger.info(f" DELETED {len(expired_ids)} {memory_type} memories")
total_deleted += len(expired_ids) total_deleted += len(expired_ids)
@ -191,8 +222,8 @@ def main():
help='Show stats and expired counts without deleting (default behavior)') help='Show stats and expired counts without deleting (default behavior)')
parser.add_argument('--execute', action='store_true', parser.add_argument('--execute', action='store_true',
help='Actually delete expired memories (requires this flag)') help='Actually delete expired memories (requires this flag)')
parser.add_argument('--max-age-days', type=int, default=90, parser.add_argument('--max-age-days', type=int, default=None,
help='Delete session/chat_summary older than N days (default: 90)') help='强制覆盖所有类型的阈值(天数);不指定则按 RETENTION_DAYS 每类型策略')
args = parser.parse_args() args = parser.parse_args()
if args.execute and args.dry_run: if args.execute and args.dry_run:
@ -208,7 +239,9 @@ def main():
logger.info("=" * 60) logger.info("=" * 60)
logger.info(f"Memory Cleanup - {datetime.now().strftime('%Y-%m-%d %H:%M')}") 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"Mode: {'EXECUTE' if execute else 'DRY-RUN (use --execute to delete)'}")
logger.info(f"Max age: {args.max_age_days} days") 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, " logger.info(f"Retention: session={RETENTION_DAYS['session']}d, "
f"chat_summary={RETENTION_DAYS['chat_summary']}d, " f"chat_summary={RETENTION_DAYS['chat_summary']}d, "
f"preference=permanent, knowledge=permanent") f"preference=permanent, knowledge=permanent")

@ -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 <model>:` 并抛出带详情的 Error
完整架构与使用说明见:[workspace/docs/LLM_GATEWAY_AND_SKILL_CLIENT.md](../../docs/LLM_GATEWAY_AND_SKILL_CLIENT.md)

@ -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<object>} 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,
};
Loading…
Cancel
Save