Initial: multi-agent XMPP communication system with dashboard
- Platform-based architecture (Windows/Linux/Mac) - Agent instance registry (agents.yaml) - Management dashboard with cross-platform monitoring - xmpp_bot with HTTP bridge + health endpoints - wechat_agent with WeChat-Hermes bridging - Platform services: ProcessGuardian, HealthProbe, APIRouter, ChannelBridge - Deployment: systemd (Linux) + PowerShell (Windows) - Monitoring: SSH+ejabberdctl for cross-platform presence
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
# xxm(小小莫)稳定性与观察者模式
|
||||
|
||||
> 最后更新:2026-06-03
|
||||
|
||||
---
|
||||
|
||||
## 架构概要
|
||||
|
||||
xxm(`xxm@yoin.fun`)是一个 XMPP bot,通过 slixmpp 连接 ejabberd(`xmpp.yoin.fun:3021`),作为"小小莫"在群聊 `coregroup@conference.yoin.fun` 中观察和响应消息。
|
||||
|
||||
```
|
||||
XMPP 消息
|
||||
↓
|
||||
xmpp_bot.py ← on_message / on_group_message
|
||||
↓
|
||||
session_router.py → route() → 构建 prompt
|
||||
↓
|
||||
chat_bridge.py → send_raw() → HTTP API 调用
|
||||
↓
|
||||
volcengine API (deepseek-v4-flash)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2026-06-03 修复记录
|
||||
|
||||
### 一、HTTP 请求死锁(最严重的稳定性问题)
|
||||
|
||||
**问题**:`urllib.request.urlopen(req, timeout=timeout)` 在 Windows 特定网络条件下,timeout 参数不触发,请求永远挂在 socket 上。桥接线程卡死 → bot 不回消息。
|
||||
|
||||
**修复**:弃用 `urllib.request`,改用 `requests` 库。
|
||||
|
||||
```python
|
||||
# 旧(会挂死)
|
||||
req = urllib.request.Request(url, data=data, headers=headers)
|
||||
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
||||
body = json.loads(resp.read())
|
||||
|
||||
# 新(Connect=10s, Read=60s,分别起效)
|
||||
resp = requests.post(url, json=payload, timeout=(10, timeout))
|
||||
resp.raise_for_status()
|
||||
body = resp.json()
|
||||
```
|
||||
|
||||
**效果**:
|
||||
- Connect timeout 10s:DNS / 连接层挂死不再发生
|
||||
- Read timeout 60s:API 响应慢也能兜底
|
||||
- 异常精确分类:`Timeout` / `HTTPError` / `RequestException` 分别打不同日志
|
||||
|
||||
**涉及文件**:`chat_bridge.py`
|
||||
|
||||
---
|
||||
|
||||
### 二、工具循环截断
|
||||
|
||||
**问题**:`_MAX_TOOL_LOOPS = 5`,LLM 做多步排查(SSH 到服务器逐条查 nginx 配置)时很容易耗尽,耗尽后 `return None` → bot 告诉用户"模型无响应"。
|
||||
|
||||
**修复**:
|
||||
1. `_MAX_TOOL_LOOPS = 5 → 50`(足够做深度排查)
|
||||
2. 50 轮耗尽后,再调一次**不带 tools 参数**的 API,强制模型用文字总结
|
||||
|
||||
```python
|
||||
# 耗尽后的兜底
|
||||
final_resp = session.post(final_url, json={"model": model, "messages": messages}, ...)
|
||||
final_msg = final_resp.json()["choices"][0]["message"]["content"]
|
||||
if final_msg.strip():
|
||||
return final_msg.strip()
|
||||
```
|
||||
|
||||
**涉及文件**:`chat_bridge.py`
|
||||
|
||||
---
|
||||
|
||||
### 三、API Key 冷却与 retry-cache 绕过
|
||||
|
||||
**问题**:opencode-go / opencode-go-new 两个 key 都在 403 冷却期(`code 1010`)。同时 opencode 的 retry-cache 机制(`#25803/#24462`)按 `(provider, model)` 缓存 429 错误,导致切换到另一个 provider 后仍然被旧 cache 截断。
|
||||
|
||||
**修复**:
|
||||
1. 创建 `api_proxy.py` 本地 HTTP 代理(`:8787`)
|
||||
2. 模型名重映射:`deepseek-v4-flash-safe` → `deepseek-v4-flash`(不同的模型名绕开 retry-cache key 碰撞)
|
||||
3. 代理吞掉 429/5xx 错误码,自动重试最多 3 次(指数退避 1s/2s/4s)
|
||||
4. 代理配置为独立 provider:`volcengine-proxy/deepseek-v4-flash-safe`
|
||||
|
||||
```json
|
||||
{
|
||||
"volcengine-proxy": {
|
||||
"type": "openai",
|
||||
"options": {
|
||||
"baseURL": "http://localhost:8787",
|
||||
"apiKey": "...",
|
||||
"model": "deepseek-v4-flash-safe"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**涉及文件**:`api_proxy.py`、`start_proxy.bat`、`~/.config/opencode/config.json`
|
||||
|
||||
---
|
||||
|
||||
### 四、观察者模式与 `__SILENT__` 协议
|
||||
|
||||
**问题**:xxm 在群里应该只回应 @自己的消息,其他消息保持沉默。但 LLM 会说"我应该保持沉默"然后把这句话本身发出去(没有用 `__SILENT__` 前缀)。
|
||||
|
||||
**修复**(两层防护):
|
||||
|
||||
**第一层 — System Prompt 明确教学**(`chat_bridge.py`):
|
||||
```
|
||||
=== 群聊沉默协议 ===
|
||||
保持沉默的方法:在回复的最开头写 __SILENT__
|
||||
系统检测到 __SILENT__ 就不会把消息发出去。
|
||||
注意:不要直接把"我应该保持沉默"当回复发出去。
|
||||
```
|
||||
|
||||
**第二层 — 自然语言兜底**(`xmpp_bot.py`):
|
||||
```python
|
||||
_SILENCE_PATTERNS = [
|
||||
"保持沉默", "不应[该]?回复", "没有.*@.*我",
|
||||
"不是对[我我说]", "跟我无关", "我不用回复",
|
||||
]
|
||||
```
|
||||
如果回复第一行匹配任何静默模式(即便没有 `__SILENT__` 前缀),直接 suppress。只检查第一行,避免误杀多行正常回复。
|
||||
|
||||
**涉及文件**:`chat_bridge.py`、`xmpp_bot.py`
|
||||
|
||||
---
|
||||
|
||||
### 五、群消息合并与串行化
|
||||
|
||||
**问题**:同个群多条消息靠近到达时,每条都触发独立的 LLM 调用和工具循环,导致:
|
||||
- 并发工具调用互相干扰(同时 SSH 到同一台服务器)
|
||||
- 多条重复排查(浪费额度)
|
||||
- log 混乱
|
||||
|
||||
**修复**:3 秒 debounce + 房间级串行化
|
||||
|
||||
**三个状态 / 两个路径**:
|
||||
|
||||
```
|
||||
消息到达
|
||||
├─ @xxm → 立即处理(绕过 batch)
|
||||
└─ 其他 → 进入 batch 系统
|
||||
│
|
||||
[BATCHING] 3s 窗口内可合并
|
||||
│ Timer 到期
|
||||
▼
|
||||
[PROCESSING] LLM 调用中
|
||||
│ 新消息 → 进入 pending 队列
|
||||
│ LLM 结束
|
||||
▼
|
||||
[_batch_done] 检查 pending
|
||||
├─ 有 pending → 立即发起下一批
|
||||
└─ 无 pending → IDLE
|
||||
```
|
||||
|
||||
**保证**:同一房间同一时刻最多一个 LLM 调用在处理。
|
||||
|
||||
**涉及文件**:`xmpp_bot.py`
|
||||
|
||||
---
|
||||
|
||||
### 六、context log 保存(self-message)
|
||||
|
||||
**问题**:LLM 看不到自己在群里说过什么,因为 bot 的 self-message 被直接丢弃了。每次都是"失忆"状态。
|
||||
|
||||
**修复**:self-message 不走 LLM,但写入 bridge context log(`_append_to_log("assistant", body)`),这样下次 LLM 调用时通过 `_read_recent_context()` 能看到自己说过的话。
|
||||
|
||||
**涉及文件**:`xmpp_bot.py`(`on_group_message` 中 `nickname == bot_nick` 分支)
|
||||
|
||||
---
|
||||
|
||||
### 七、`part_` → `prt_` 前缀修复(2026-06-11)
|
||||
|
||||
**问题**:`chat_bridge.py` 的 `_append_to_session()` 生成 part ID 时用了 `part_` 前缀:
|
||||
```python
|
||||
part_id = "part_" + _uuid.uuid4().hex[:24] # BUG!
|
||||
```
|
||||
但 OpenCode 1.17+ 要求 part ID 必须用 `prt_` 前缀。这导致:
|
||||
- 每次 xxm 写入消息到 session → 产生 `part_` 数据
|
||||
- compaction 扫描到旧前缀 → schema 校验失败 → 死循环
|
||||
- session 崩掉 → xxm 也卡住
|
||||
|
||||
**修复**:`part_` → `prt_`(`chat_bridge.py:335`)
|
||||
|
||||
**连带发现**:`ses_xxm_xmpp` session 在代码里配了但 DB 里不存在,已手动创建。
|
||||
|
||||
### 八、`session_search` 工具说明
|
||||
|
||||
**用途**:让 xxm 能搜索其他 session 的历史对话。已内置为 function calling tool。
|
||||
|
||||
**定义位置**:`chat_bridge.py` 的 `_TOOLS` 列表
|
||||
|
||||
| 参数 | 默认值 | 说明 |
|
||||
|------|--------|------|
|
||||
| `session_id` | 当前 session | 指定要搜索的 session ID,空字符串则查自己 |
|
||||
| `limit` | 20(最大100) | 返回最近多少条消息 |
|
||||
|
||||
**调用方式**:LLM 通过 function calling 调用 `session_search`,不需要 xxm 写代码。
|
||||
|
||||
**注意**:archived session 仍可搜索(`extract_session_context` 直接从 message 表读,不依赖 session 状态)。
|
||||
|
||||
---
|
||||
|
||||
## 关键配置
|
||||
|
||||
| 参数 | 值 | 说明 |
|
||||
|------|-----|------|
|
||||
| `_MAX_TOOL_LOOPS` | 30 | 工具循环上限(超限后 clean final force,不泄漏 XML) |
|
||||
| `DEFAULT_TIMEOUT` | 60s | 每次 API 调用 read timeout |
|
||||
| `LOCK_DURATION` | 300s | 锁定成功 provider(避免频繁切换) |
|
||||
| `FAILED_BACKOFF` | 1800s | 失败 provider 冷却 |
|
||||
| `_BATCH_WINDOW` | 3.0s | 群消息合并窗口 |
|
||||
| api_proxy 重试 | 3 次 | 指数退避 1s/2s/4s |
|
||||
|
||||
## Provider 优先级
|
||||
|
||||
```
|
||||
volcengine (deepseek-v4-flash) # 主用,火山免费额度
|
||||
→ opencode-go-new (deepseek-v4-flash) # 备用,有订阅但冷却中
|
||||
→ opencode-go (deepseek-v4-flash) # 备用,冷却中
|
||||
```
|
||||
|
||||
## 日志
|
||||
|
||||
| 日志文件 | 路径 | 用途 |
|
||||
|---------|------|------|
|
||||
| `logs/xmpp_bot.log` | `projects/.../logs/` | XMPP 连接、消息收发、batch 状态 |
|
||||
| `logs/bridge.log` | `projects/.../logs/` | LLM API 调用、耗时、工具调用 |
|
||||
| `logs/api_proxy.log` | `projects/.../logs/` | 代理请求、错误吞没、重试 |
|
||||
Reference in New Issue
Block a user