auto-snapshot 2026-09-02 03:00:29

This commit is contained in:
小唯 A06 2026-09-02 03:00:29 +08:00
parent 5d3b64de0c
commit 165c75c2fc
6 changed files with 275 additions and 26 deletions

View File

@ -38,7 +38,7 @@ check_mount() {
log "⚠️ 服务器未挂载,尝试连接..."
# -n: non-interactivecron 环境无密码输入机会,立即失败不卡住
# || true: 失败是预期路径(服务器不在线),由下方 mountpoint 判断处理,避免 set -e 提前退出
sudo -n mount -t cifs //192.168.123.11/beifen /mnt/server-backup \
sudo -n mount -t cifs //192.168.188.11/beifen /mnt/server-backup \
-o username=administrator,password=xue.2538,uid=1000,gid=1000,iocharset=utf8,file_mode=0755,dir_mode=0755 2>/dev/null || true
sleep 2
if ! mountpoint -q /mnt/server-backup; then

129
scripts/newapi-observe.py Executable file
View File

@ -0,0 +1,129 @@
#!/usr/bin/env python3
"""
NewAPI 观测脚本 持续采集主网关健康/延迟/路由数据
6h cron 触发no_agent 模式
- 正常时静默数据追加到 JSONL 观测日志
- 异常时输出报警cron 会自动推送
数据用途监测 NewAPI + 9 NIM key 池健康度
替代 omniroute-observe.pyOmniRoute 已于 2026-09-02 关停
"""
import json
import os
import time
import requests
from datetime import datetime, timezone
API = "http://127.0.0.1:3000/v1"
# api-test-token 是 NewAPI 唯一启用 token (user_id=1)
KEY = "0ExNiLblJvIWBDpkS50fwOBw4MmqLyKdHJK5iQtlw9dOMWBP"
STATE_DIR = os.path.expanduser("~/.hermes/newapi-observe")
LOG = os.path.join(STATE_DIR, "observations.jsonl")
STATE_FILE = os.path.join(STATE_DIR, "state.json")
# 测试用的模型组合 — 覆盖 NewAPI 真实 channel
# - sensenova-free: 主 channel (sensenova deepseek-v4-flash)
# - minimaxai/minimax-m3: NIM-k1 (NVIDIA 集成, 9 个 key 池)
# - google/gemma-4-31b-it: NIM 池可用模型
TEST_MODELS = [
"deepseek-v4-flash", # sensenova 通道
"minimaxai/minimax-m3", # NIM-k1 (已知可用)
"google/gemma-4-31b-it", # NIM 池备选
]
def api_call(model, max_tokens=20):
"""发一次真实请求,返回 (ok, latency_ms, model, content_len)"""
t0 = time.time()
try:
r = requests.post(f"{API}/chat/completions",
json={"model": model,
"messages": [{"role": "user", "content": "ping"}],
"max_tokens": max_tokens},
headers={"Authorization": f"Bearer {KEY}"},
timeout=30, stream=False)
elapsed = round((time.time() - t0) * 1000)
if r.status_code == 200:
try:
data = r.json()
m = data.get("model", model)
choices = data.get("choices", [])
content_len = len(choices[0].get("message", {}).get("content", "")) if choices else 0
return True, elapsed, m, content_len
except Exception:
return True, elapsed, model, 0
else:
return False, elapsed, f"HTTP {r.status_code}", 0
except Exception as e:
return False, round((time.time() - t0) * 1000), f"EXC: {str(e)[:50]}", 0
def check_service():
"""检查 NewAPI 服务 + models API 是否健康"""
try:
r = requests.get(f"{API}/models",
headers={"Authorization": f"Bearer {KEY}"},
timeout=10)
if r.status_code == 200:
models = r.json().get("data", [])
return True, len(models)
return False, 0
except Exception:
return False, 0
def main():
# 确保 state 目录存在
os.makedirs(STATE_DIR, exist_ok=True)
# 1. 服务健康
svc_ok, model_count = check_service()
if not svc_ok:
print(f"🚨 NewAPI 服务不可达或 models API 失败")
return
# 2. 测试每个模型
results = []
for m in TEST_MODELS:
ok, lat, routed, content_len = api_call(m)
results.append({
"model": m,
"ok": ok,
"latency_ms": lat,
"routed_to": routed,
"content_len": content_len
})
# 3. 判断是否有异常
fails = [r for r in results if not r["ok"]]
slow = [r for r in results if r["ok"] and r["latency_ms"] > 8000]
# 4. 追加 JSONL 观测记录
obs = {
"ts": datetime.now(timezone.utc).isoformat(),
"service": "ok" if svc_ok else "fail",
"model_count": model_count,
"tests": results
}
with open(LOG, "a") as f:
f.write(json.dumps(obs) + "\n")
# 5. 写 state.json供其他脚本查询
with open(STATE_FILE, "w") as f:
json.dump(obs, f, indent=2)
# 6. 报警逻辑
if fails:
msgs = [f"{r['model']}: {r['routed_to']} ({r['latency_ms']}ms)" for r in fails]
print(f"🚨 NewAPI 异常 ({len(fails)}/{len(results)} 失败):")
for m in msgs:
print(m)
elif slow:
msgs = [f"⚠️ {r['model']}: {r['latency_ms']}ms" for r in slow]
print(f"⚠️ NewAPI 慢响应 ({len(slow)}/{len(results)} > 8s):")
for m in msgs:
print(m)
# else: 静默(健康)
if __name__ == "__main__":
main()

92
scripts/newapi-weekly-report.py Executable file
View File

@ -0,0 +1,92 @@
#!/usr/bin/env python3
"""
NewAPI 周度评估报告 汇总观测数据输出评估结论
每周日 18:00 cron 触发输出可读报告非静默始终输出
评估维度可用性 / 延迟 / 路由一致性
替代 omniroute-weekly-report.pyOmniRoute 已于 2026-09-02 关停
"""
import json
import os
import statistics
from datetime import datetime, timezone, timedelta
STATE_DIR = os.path.expanduser("~/.hermes/newapi-observe")
LOG = os.path.join(STATE_DIR, "observations.jsonl")
# 评估阈值
LATENCY_WARN_MS = 8000 # 平均延迟超 8s 警告
SUCCESS_RATE_MIN = 0.90 # 成功率低于 90% 警告
def load_observations(days=7):
if not os.path.exists(LOG):
return []
cutoff = datetime.now(timezone.utc) - timedelta(days=days)
obs = []
with open(LOG) as f:
for line in f:
try:
d = json.loads(line)
ts = datetime.fromisoformat(d["ts"])
if ts >= cutoff:
obs.append(d)
except Exception:
continue
return obs
def main():
obs = load_observations(7)
n = len(obs)
print(f"=== NewAPI 周度评估(最近 7 天)===")
print(f"观测次数: {n}")
print(f"生成时间: {datetime.now(timezone.utc).isoformat()}")
print()
if n == 0:
print("⚠️ 无观测数据cron 可能在观测脚本切换期间未运行)")
return
# 1. 服务可用性
svc_ok = sum(1 for o in obs if o.get("service") == "ok")
svc_rate = svc_ok / n
print(f"【服务可用性】 {svc_ok}/{n} = {svc_rate*100:.1f}%")
# 2. 按模型统计
by_model = {}
for o in obs:
for t in o.get("tests", []):
m = t["model"]
if m not in by_model:
by_model[m] = {"ok": 0, "total": 0, "latencies": []}
by_model[m]["total"] += 1
if t["ok"]:
by_model[m]["ok"] += 1
by_model[m]["latencies"].append(t["latency_ms"])
print(f"\n【按模型表现】")
for m, d in sorted(by_model.items()):
rate = d["ok"] / d["total"] * 100 if d["total"] else 0
if d["latencies"]:
avg = statistics.mean(d["latencies"])
p95 = sorted(d["latencies"])[int(len(d["latencies"])*0.95)] if len(d["latencies"]) > 1 else d["latencies"][0]
else:
avg = p95 = 0
status = "" if rate >= SUCCESS_RATE_MIN*100 and avg < LATENCY_WARN_MS else "⚠️"
print(f" {status} {m}: {d['ok']}/{d['total']} = {rate:.0f}%, 平均 {avg:.0f}ms, P95 {p95:.0f}ms")
# 3. 总体评估
total_tests = sum(d["total"] for d in by_model.values())
total_ok = sum(d["ok"] for d in by_model.values())
overall_rate = total_ok / total_tests * 100 if total_tests else 0
print(f"\n【总体】 {total_ok}/{total_tests} = {overall_rate:.1f}%")
if overall_rate < SUCCESS_RATE_MIN*100:
print("⚠️ 建议: 检查 NewAPI 上游 channel 健康度")
else:
print("✅ 评估: NewAPI 表现健康,可作为本地主网关")
if __name__ == "__main__":
main()

File diff suppressed because one or more lines are too long

View File

@ -346,14 +346,14 @@
"created_by": "agent",
"last_patched_at": "2026-09-01T13:24:30.353399+00:00",
"last_reused_patch_generation": 2,
"last_used_at": "2026-09-01T14:46:17.329481+00:00",
"last_viewed_at": "2026-09-01T14:46:17.312828+00:00",
"last_used_at": "2026-09-01T17:40:43.660373+00:00",
"last_viewed_at": "2026-09-01T17:40:43.644850+00:00",
"patch_count": 4,
"patch_generation": 2,
"pinned": false,
"state": "active",
"use_count": 16,
"view_count": 16
"use_count": 17,
"view_count": 17
},
"blocked-page-recovery": {
"archived_at": null,
@ -839,14 +839,14 @@
"created_by": "agent",
"last_patched_at": "2026-08-26T05:42:43.675272+00:00",
"last_reused_patch_generation": 2,
"last_used_at": "2026-09-01T12:49:40.492187+00:00",
"last_viewed_at": "2026-09-01T12:49:40.486311+00:00",
"last_used_at": "2026-09-01T15:52:18.292077+00:00",
"last_viewed_at": "2026-09-01T15:52:18.282846+00:00",
"patch_count": 98,
"patch_generation": 2,
"pinned": false,
"state": "active",
"use_count": 88,
"view_count": 88
"use_count": 89,
"view_count": 89
},
"devops/bge-embed-crash-loop-fix": {
"archived_at": null,
@ -1655,14 +1655,14 @@
"created_by": null,
"last_patched_at": "2026-08-12T05:13:32.138839+00:00",
"last_reused_patch_generation": 1,
"last_used_at": "2026-09-01T13:17:29.163415+00:00",
"last_viewed_at": "2026-09-01T13:17:29.139591+00:00",
"last_used_at": "2026-09-01T17:41:09.065587+00:00",
"last_viewed_at": "2026-09-01T17:41:09.051579+00:00",
"patch_count": 81,
"patch_generation": 1,
"pinned": false,
"state": "active",
"use_count": 139,
"view_count": 128
"use_count": 140,
"view_count": 129
},
"hermes-venv-dependency-safety": {
"archived_at": null,
@ -1813,6 +1813,21 @@
"use_count": 5,
"view_count": 5
},
"kanban-router": {
"archived_at": null,
"created_at": "2026-09-01T15:56:37.956717+00:00",
"created_by": null,
"last_patched_at": null,
"last_reused_patch_generation": 0,
"last_used_at": "2026-09-01T15:56:37.961319+00:00",
"last_viewed_at": "2026-09-01T15:56:37.956727+00:00",
"patch_count": 0,
"patch_generation": 0,
"pinned": false,
"state": "active",
"use_count": 1,
"view_count": 1
},
"kanban-routing": {
"archived_at": null,
"created_at": "2026-09-01T13:21:22.328206+00:00",
@ -1948,16 +1963,16 @@
"archived_at": null,
"created_at": "2026-08-01T13:47:26.532313+00:00",
"created_by": "agent",
"last_patched_at": "2026-08-20T18:26:57.337252+00:00",
"last_reused_patch_generation": 29,
"last_used_at": "2026-08-29T10:58:53.508614+00:00",
"last_viewed_at": "2026-08-29T10:58:53.501617+00:00",
"patch_count": 35,
"patch_generation": 29,
"last_patched_at": "2026-09-01T18:34:24.564573+00:00",
"last_reused_patch_generation": 30,
"last_used_at": "2026-09-01T18:33:03.244527+00:00",
"last_viewed_at": "2026-09-01T18:33:03.239992+00:00",
"patch_count": 40,
"patch_generation": 34,
"pinned": false,
"state": "active",
"use_count": 39,
"view_count": 39
"use_count": 45,
"view_count": 45
},
"llm-wiki": {
"archived_at": null,
@ -2861,14 +2876,14 @@
"created_by": "agent",
"last_patched_at": "2026-09-01T13:48:27.218650+00:00",
"last_reused_patch_generation": 27,
"last_used_at": "2026-09-01T14:46:17.334034+00:00",
"last_viewed_at": "2026-09-01T14:46:17.325238+00:00",
"last_used_at": "2026-09-01T18:32:51.753345+00:00",
"last_viewed_at": "2026-09-01T18:32:51.742812+00:00",
"patch_count": 230,
"patch_generation": 27,
"pinned": false,
"state": "active",
"use_count": 191,
"view_count": 191
"use_count": 200,
"view_count": 200
},
"self-hosted-tunneling": {
"archived_at": null,

View File

@ -7,10 +7,12 @@ description: LLM 网关运维 — OmniRoute/NewAPI 类 AI 网关的评估、部
## 触发场景
- 用户分享新 AI 网关/路由项目(如 OmniRoute、OneAPI 类)→ 评估是否替代现有 NewAPI
- **用户问"X 网关是否有必要保留/关停"→ 拉真实调用量做决策**(不只是看进程在不在)→ 决策后看 `references/gateway-shutdown-procedure-20260902.md` 走 6 步标准流程
- 部署/试跑网关、做 systemd 常驻、接入看门狗
- 写脚本调网关 APIchat/completions遇超时/挂起
- 把 cron 任务从 NewAPI 切换到其他网关 provider
- 需要持续观测网关稳定性、为"是否全面切换"提供数据
- 周期性自检网关使用率cron `0 9 1 * *`
## 核心方法论:评估是否替代现有网关
@ -19,6 +21,10 @@ description: LLM 网关运维 — OmniRoute/NewAPI 类 AI 网关的评估、部
3. **生产链路绝不动**:付费主模型(如 deepseek-v4-flash 走官方 API不切
4. **观测 2-4 周**再决定是否迁移——必须有量化数据支撑,不拍脑袋
5. 评估维度:稳定性/免费模型供给/路由能力/成本控制/国内网络兼容/生态成熟度
6. **logs 表 0 调用 ≠ 服务僵尸**2026-09-02 教训):`logs` 是写日志表,不代表服务可用。判断"僵尸"必跑 3 步——L1 进程活 + L2 直连 200 + L3 `provider_models_cache.json` 列了模型。三步全过才认活(详细见 `references/gateway-real-usage-evaluation-20260901.md` 的"修正版"章节)
7. **进程内存要算子进程总和**2026-09-02 教训OmniRoute 主进程 46MB + 子进程 394MB = **441MB 总占用**,只看主进程会少算 90%
8. **HTTP 410/404 ≠ key 失效**2026-09-02 教训NVIDIA 端下架特定模型410 "has retired")≠ API key 废了——**同一个 key 通常能跑多个模型**,重测 `minimaxai/minimax-m3``google/gemma-4-31b-it` 等常用模型验证。`9 个 NIM key 8 个模型下架` 的判断是错的——9/9 全部活跃,只是测错了模型。
9. **评估完成后如何安全关停**2026-09-02 OmniRoute 实战6 步标准流程——①备份 4 件套(脚本/服务/配置/状态)②先写替代观测脚本(不能只关停就跑,否则 cron 一直报错)③改 cron 任务no_agent script 字段必须相对路径④systemctl stop + disable ⑤验证替代品 + 内存释放 ⑥归档不删原可执行文件。详见 `references/gateway-shutdown-procedure-20260902.md`。**patch 工具拒绝改 config.yaml**——provider 段留着不动通常没事(端口已死不会触发)。
## OmniRoute 部署npm 全局)
@ -410,4 +416,6 @@ NewAPI 添加渠道有两种方式API会 panic版本 bug和 Web UI
- `references/newapi-channel-management-20260820.md` — NewAPI 渠道管理坑SQLite 直改完整性、API panic bug、Web UI 替代方案
- `references/newapi-sensenova-channel-20260821.md` — Sensenova Token Plan 免费接入5 模型 + abilities 联动 + reserved word 坑
- `references/openclaw-config-validation-pitfalls-20260821.md` — OpenClaw openclaw.json 配置验证坑defaultModel/defaultProvider 非法、output 字段不合法、models 数组必填
- 微信(iLink)/QQ 平台接入 Hermes gateway 的 .env 环境变量铁律见 **hermes-debug 第 6 节**WEIXIN_TOKEN/QQ_APP_ID 只认 .env 不读 config.yaml 段v0.20 需 WEIXIN_ALLOW_ALL_USERS=trueQQ 用 `hermes pairing approve qqbot <CODE>` 授权)
- `references/gateway-real-usage-evaluation-20260901.md` — 评估方法论3 步框架L1 进程/L2 配置/L3 调用)+ 决策树 + 410 ≠ key 失效陷阱
- `references/gateway-shutdown-procedure-20260902.md`**关停标准流程**6 步(备份→写替代脚本→改 cron→systemctl stop+disable→验证→归档+ 4 个 cron/patch 陷阱
- `references/2026-08-04-omniroute-cron-failure.md` — OmniRoute cron 504 失败案例auto/chat 路由慢模型)