🔄 卡若AI 同步 2026-02-25 12:07 | 更新:Cursor规则、水桥平台对接、运营中枢、运营中枢参考资料、运营中枢工作台 | 排除 >20MB: 13 个
This commit is contained in:
29
.cursor/rules/api-failover-stability.mdc
Normal file
29
.cursor/rules/api-failover-stability.mdc
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
description: 卡若AI API 接口排队与故障切换稳定性规则
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# 卡若AI API 稳定性规则
|
||||
|
||||
## 目标
|
||||
- 所有 AI 请求优先走可用接口,单接口超时/报错时自动切换下一接口。
|
||||
- 全部接口都失败时,必须触发邮件告警并返回可读降级回复。
|
||||
|
||||
## 接口排队规则(按顺序)
|
||||
- 使用 `OPENAI_API_BASES` 配置接口队列(逗号分隔)。
|
||||
- 对应密钥用 `OPENAI_API_KEYS`(可选);若缺省则回退 `OPENAI_API_KEY`。
|
||||
- 对应模型用 `OPENAI_MODELS`(可选);若缺省则回退 `OPENAI_MODEL`。
|
||||
|
||||
## 故障切换规则
|
||||
- 单接口失败条件:超时、网络错误、HTTP 非 200、响应体不可解析。
|
||||
- 每次请求必须按队列顺序逐个尝试,直到成功或队列耗尽。
|
||||
- 队列耗尽后:发送告警邮件到 `ALERT_EMAIL_TO`(默认 `zhiqun@qq.com`),并返回降级回复,不得直接空响应。
|
||||
|
||||
## 告警规则
|
||||
- SMTP 默认:`SMTP_HOST=smtp.qq.com`、`SMTP_PORT=465`。
|
||||
- 必填:`SMTP_USER`、`SMTP_PASS`、`ALERT_EMAIL_TO`(可不填则默认收件箱)。
|
||||
- 为防刷屏,告警需带冷却时间(默认 300 秒)。
|
||||
|
||||
## 安全规则
|
||||
- 不在代码与规则里写死明文密钥。
|
||||
- 仅记录必要错误摘要,不在日志输出完整 token。
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"access_token": "u-4Tr54dmqV8lE_qtfG76A2Il5mMMBk1irW8aaVBM00wO2",
|
||||
"refresh_token": "ur-4iCTU0PcheAVQUi_Z43c9El5koO5k1MpV8aaIQw00wCn",
|
||||
"access_token": "u-6IgXw46WF09ryzyx_zjXmEl5kqo5k1WrNUaaEAM00xO6",
|
||||
"refresh_token": "ur-7A8zuMj6t7gaO8JsTXVY8zl5mqU5k1OrhUaaUxQ00BCi",
|
||||
"name": "飞书用户",
|
||||
"auth_time": "2026-02-25T09:19:23.848992"
|
||||
"auth_time": "2026-02-25T12:00:51.797153"
|
||||
}
|
||||
@@ -15,9 +15,36 @@ uvicorn main:app --host 0.0.0.0 --port 8000
|
||||
- `OPENAI_API_KEY`:OpenAI 或兼容 API 的密钥,配置后使用真实 LLM 生成回复。
|
||||
- `OPENAI_API_BASE`:兼容接口地址,默认 `https://api.openai.com/v1`。
|
||||
- `OPENAI_MODEL`:模型名,默认 `gpt-4o-mini`。
|
||||
- `OPENAI_API_BASES`:接口队列(逗号分隔),例如 `https://a.example.com/v1,https://b.example.com/v1`。
|
||||
- `OPENAI_API_KEYS`:队列密钥(逗号分隔,可选)。若未配置,回退 `OPENAI_API_KEY`。
|
||||
- `OPENAI_MODELS`:队列模型(逗号分隔,可选)。若未配置,回退 `OPENAI_MODEL`。
|
||||
- `ALERT_EMAIL_TO`:全部接口失败时的告警收件人(默认 `zhiqun@qq.com`)。
|
||||
- `SMTP_HOST` / `SMTP_PORT` / `SMTP_USER` / `SMTP_PASS`:SMTP 告警配置(QQ 邮箱默认 `smtp.qq.com:465`)。
|
||||
- `KARUO_GATEWAY_CONFIG`:网关配置路径(默认 `config/gateway.yaml`)。
|
||||
- `KARUO_GATEWAY_SALT`:部门 Key 的 salt(用于 sha256 校验;不写入仓库)。
|
||||
|
||||
### 接口排队与自动切换(稳定性)
|
||||
|
||||
网关会按顺序尝试接口队列:
|
||||
|
||||
1. 优先使用 `OPENAI_API_BASES`(可配多个)
|
||||
2. 任一接口超时/异常/非 200 时,自动切换下一接口
|
||||
3. 全部失败时:发送告警邮件并返回降级回复(不中断对话)
|
||||
|
||||
示例:
|
||||
|
||||
```bash
|
||||
export OPENAI_API_BASES="https://api.openai.com/v1,https://openrouter.ai/api/v1"
|
||||
export OPENAI_API_KEYS="sk-xxx,sk-yyy"
|
||||
export OPENAI_MODELS="gpt-4o-mini,openai/gpt-4o-mini"
|
||||
|
||||
export ALERT_EMAIL_TO="zhiqun@qq.com"
|
||||
export SMTP_HOST="smtp.qq.com"
|
||||
export SMTP_PORT="465"
|
||||
export SMTP_USER="zhiqun@qq.com"
|
||||
export SMTP_PASS="你的QQ邮箱授权码"
|
||||
```
|
||||
|
||||
## 部门/科室鉴权与白名单(推荐启用)
|
||||
|
||||
网关支持“每部门一个 Key + 技能白名单”,用于:
|
||||
|
||||
@@ -30,6 +30,17 @@ llm:
|
||||
api_key_env: OPENAI_API_KEY
|
||||
api_base_env: OPENAI_API_BASE
|
||||
model_env: OPENAI_MODEL
|
||||
# 队列模式(可选):逗号分隔多个上游接口,按顺序自动切换
|
||||
api_bases_env: OPENAI_API_BASES
|
||||
api_keys_env: OPENAI_API_KEYS
|
||||
models_env: OPENAI_MODELS
|
||||
# 全部接口失败时邮件告警(可选)
|
||||
alert_email_to_env: ALERT_EMAIL_TO
|
||||
smtp_host_env: SMTP_HOST
|
||||
smtp_port_env: SMTP_PORT
|
||||
smtp_user_env: SMTP_USER
|
||||
smtp_pass_env: SMTP_PASS
|
||||
alert_cooldown_seconds: 300
|
||||
timeout_seconds: 60
|
||||
max_tokens: 2000
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ import time
|
||||
import json
|
||||
import hashlib
|
||||
import hmac
|
||||
import smtplib
|
||||
from email.message import EmailMessage
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import yaml
|
||||
@@ -96,13 +98,13 @@ def _get_api_key_from_request(request: Request, cfg: Dict[str, Any]) -> str:
|
||||
- X-Karuo-Api-Key: <key>(原生网关方式)
|
||||
- Authorization: Bearer <key>(OpenAI 兼容客户端常用)
|
||||
"""
|
||||
auth = request.headers.get("authorization", "").strip()
|
||||
if auth.lower().startswith("bearer "):
|
||||
return auth[7:].strip()
|
||||
header_name = _auth_header_name(cfg)
|
||||
api_key = request.headers.get(header_name, "").strip()
|
||||
if api_key:
|
||||
return api_key
|
||||
auth = request.headers.get("authorization", "").strip()
|
||||
if auth.lower().startswith("bearer "):
|
||||
return auth[7:].strip()
|
||||
return ""
|
||||
|
||||
|
||||
@@ -209,6 +211,102 @@ def _llm_settings(cfg: Dict[str, Any]) -> Dict[str, Any]:
|
||||
return (cfg or {}).get("llm") or {}
|
||||
|
||||
|
||||
def _split_csv_env(value: str) -> List[str]:
|
||||
if not value:
|
||||
return []
|
||||
s = value.replace("\n", ",").replace(";", ",")
|
||||
return [x.strip() for x in s.split(",") if x.strip()]
|
||||
|
||||
|
||||
def _build_provider_queue(llm_cfg: Dict[str, Any]) -> List[Dict[str, str]]:
|
||||
"""
|
||||
构建 LLM 接口队列:
|
||||
1) 优先读取 OPENAI_API_BASES / OPENAI_API_KEYS / OPENAI_MODELS(逗号分隔)
|
||||
2) 若未配置队列,则回退到单接口 OPENAI_API_BASE / OPENAI_API_KEY / OPENAI_MODEL
|
||||
"""
|
||||
base_env = llm_cfg.get("api_base_env", "OPENAI_API_BASE")
|
||||
key_env = llm_cfg.get("api_key_env", "OPENAI_API_KEY")
|
||||
model_env = llm_cfg.get("model_env", "OPENAI_MODEL")
|
||||
bases_env = llm_cfg.get("api_bases_env", "OPENAI_API_BASES")
|
||||
keys_env = llm_cfg.get("api_keys_env", "OPENAI_API_KEYS")
|
||||
models_env = llm_cfg.get("models_env", "OPENAI_MODELS")
|
||||
|
||||
single_base = os.environ.get(base_env, "https://api.openai.com/v1").strip()
|
||||
single_key = os.environ.get(key_env, "").strip()
|
||||
single_model = os.environ.get(model_env, "gpt-4o-mini").strip() or "gpt-4o-mini"
|
||||
|
||||
bases = _split_csv_env(os.environ.get(bases_env, ""))
|
||||
keys = _split_csv_env(os.environ.get(keys_env, ""))
|
||||
models = _split_csv_env(os.environ.get(models_env, ""))
|
||||
|
||||
providers: List[Dict[str, str]] = []
|
||||
if bases:
|
||||
for i, b in enumerate(bases):
|
||||
key = keys[i] if i < len(keys) and keys[i] else single_key
|
||||
model = models[i] if i < len(models) and models[i] else single_model
|
||||
if not b or not key:
|
||||
continue
|
||||
providers.append({"base_url": b.rstrip("/"), "api_key": key, "model": model})
|
||||
elif single_key:
|
||||
providers.append({"base_url": single_base.rstrip("/"), "api_key": single_key, "model": single_model})
|
||||
return providers
|
||||
|
||||
|
||||
def _send_provider_alert(cfg: Dict[str, Any], errors: List[str], prompt: str, matched_skill: str, skill_path: str) -> None:
|
||||
"""
|
||||
当所有 LLM 接口都失败时,发邮件告警(支持 QQ SMTP)。
|
||||
环境变量:
|
||||
- ALERT_EMAIL_TO(默认 zhiqun@qq.com)
|
||||
- SMTP_HOST(默认 smtp.qq.com)
|
||||
- SMTP_PORT(默认 465)
|
||||
- SMTP_USER / SMTP_PASS
|
||||
"""
|
||||
llm_cfg = _llm_settings(cfg)
|
||||
to_env = llm_cfg.get("alert_email_to_env", "ALERT_EMAIL_TO")
|
||||
smtp_host_env = llm_cfg.get("smtp_host_env", "SMTP_HOST")
|
||||
smtp_port_env = llm_cfg.get("smtp_port_env", "SMTP_PORT")
|
||||
smtp_user_env = llm_cfg.get("smtp_user_env", "SMTP_USER")
|
||||
smtp_pass_env = llm_cfg.get("smtp_pass_env", "SMTP_PASS")
|
||||
|
||||
to_addr = os.environ.get(to_env, "zhiqun@qq.com").strip()
|
||||
smtp_host = os.environ.get(smtp_host_env, "smtp.qq.com").strip() or "smtp.qq.com"
|
||||
smtp_port = int(str(os.environ.get(smtp_port_env, "465") or "465").strip())
|
||||
smtp_user = os.environ.get(smtp_user_env, "").strip()
|
||||
smtp_pass = os.environ.get(smtp_pass_env, "").strip()
|
||||
|
||||
# 避免因邮件配置不完整影响主流程
|
||||
if not (to_addr and smtp_user and smtp_pass):
|
||||
return
|
||||
|
||||
cooldown = int(llm_cfg.get("alert_cooldown_seconds", 300) or 300)
|
||||
now = int(time.time())
|
||||
last_ts = int(getattr(app.state, "_last_provider_alert_ts", 0) or 0)
|
||||
if last_ts and now - last_ts < cooldown:
|
||||
return
|
||||
|
||||
app.state._last_provider_alert_ts = now
|
||||
|
||||
msg = EmailMessage()
|
||||
msg["Subject"] = "【卡若AI网关告警】全部LLM接口不可用"
|
||||
msg["From"] = smtp_user
|
||||
msg["To"] = to_addr
|
||||
safe_prompt = (prompt or "").strip()
|
||||
if len(safe_prompt) > 200:
|
||||
safe_prompt = safe_prompt[:200] + "..."
|
||||
body = (
|
||||
"卡若AI 网关检测到:本次请求所有上游接口都失败。\n\n"
|
||||
f"时间戳: {now}\n"
|
||||
f"匹配技能: {matched_skill} ({skill_path})\n"
|
||||
f"用户问题片段: {safe_prompt}\n\n"
|
||||
"错误列表:\n- " + "\n- ".join(errors[:10])
|
||||
)
|
||||
msg.set_content(body)
|
||||
|
||||
with smtplib.SMTP_SSL(smtp_host, smtp_port, timeout=15) as s:
|
||||
s.login(smtp_user, smtp_pass)
|
||||
s.send_message(msg)
|
||||
|
||||
|
||||
def build_reply_with_llm(prompt: str, cfg: Dict[str, Any], matched_skill: str, skill_path: str) -> str:
|
||||
"""调用 LLM 生成回复(OpenAI 兼容)。未配置则返回模板回复。"""
|
||||
bootstrap = load_bootstrap()
|
||||
@@ -218,26 +316,37 @@ def build_reply_with_llm(prompt: str, cfg: Dict[str, Any], matched_skill: str, s
|
||||
"先简短思考并输出,再给执行要点,最后必须带「[卡若复盘]」块(含目标·结果·达成率、过程 1 2 3、反思、总结、下一步)。"
|
||||
)
|
||||
llm_cfg = _llm_settings(cfg)
|
||||
api_key = os.environ.get(llm_cfg.get("api_key_env", "OPENAI_API_KEY"))
|
||||
base_url = os.environ.get(llm_cfg.get("api_base_env", "OPENAI_API_BASE"), "https://api.openai.com/v1")
|
||||
if api_key:
|
||||
providers = _build_provider_queue(llm_cfg)
|
||||
if providers:
|
||||
errors: List[str] = []
|
||||
for idx, p in enumerate(providers, start=1):
|
||||
try:
|
||||
import httpx
|
||||
|
||||
r = httpx.post(
|
||||
f"{p['base_url']}/chat/completions",
|
||||
headers={"Authorization": f"Bearer {p['api_key']}", "Content-Type": "application/json"},
|
||||
json={
|
||||
"model": p["model"],
|
||||
"messages": [{"role": "system", "content": system}, {"role": "user", "content": prompt}],
|
||||
"max_tokens": int(llm_cfg.get("max_tokens", 2000)),
|
||||
},
|
||||
timeout=float(llm_cfg.get("timeout_seconds", 60)),
|
||||
)
|
||||
if r.status_code == 200:
|
||||
data = r.json()
|
||||
return data["choices"][0]["message"]["content"]
|
||||
errors.append(f"provider#{idx} status={r.status_code} body={r.text[:120]}")
|
||||
except Exception as e:
|
||||
errors.append(f"provider#{idx} exception={type(e).__name__}: {str(e)[:160]}")
|
||||
|
||||
# 所有接口失败:邮件告警 + 降级回复
|
||||
try:
|
||||
import httpx
|
||||
r = httpx.post(
|
||||
f"{base_url.rstrip('/')}/chat/completions",
|
||||
headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
|
||||
json={
|
||||
"model": os.environ.get(llm_cfg.get("model_env", "OPENAI_MODEL"), "gpt-4o-mini"),
|
||||
"messages": [{"role": "system", "content": system}, {"role": "user", "content": prompt}],
|
||||
"max_tokens": int(llm_cfg.get("max_tokens", 2000)),
|
||||
},
|
||||
timeout=float(llm_cfg.get("timeout_seconds", 60)),
|
||||
)
|
||||
if r.status_code == 200:
|
||||
data = r.json()
|
||||
return data["choices"][0]["message"]["content"]
|
||||
except Exception as e:
|
||||
return _template_reply(prompt, matched_skill, skill_path, error=str(e))
|
||||
_send_provider_alert(cfg, errors, prompt, matched_skill, skill_path)
|
||||
except Exception:
|
||||
# 告警失败不影响主流程,继续降级
|
||||
pass
|
||||
return _template_reply(prompt, matched_skill, skill_path, error=" | ".join(errors[:3]))
|
||||
return _template_reply(prompt, matched_skill, skill_path)
|
||||
|
||||
|
||||
|
||||
69
运营中枢/参考资料/卡若AI_API接口排队与故障切换规则.md
Normal file
69
运营中枢/参考资料/卡若AI_API接口排队与故障切换规则.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# 卡若AI API 接口排队与故障切换规则
|
||||
|
||||
## 1. 本机已识别的 AI 接口配置入口
|
||||
|
||||
- 网关代码入口:`运营中枢/scripts/karuo_ai_gateway/main.py`
|
||||
- 网关说明文档:`运营中枢/scripts/karuo_ai_gateway/README.md`
|
||||
- 网关配置样例:`运营中枢/scripts/karuo_ai_gateway/config/gateway.example.yaml`
|
||||
|
||||
当前支持的接口变量(不含明文密钥):
|
||||
|
||||
- 单接口:`OPENAI_API_BASE` / `OPENAI_API_KEY` / `OPENAI_MODEL`
|
||||
- 队列接口:`OPENAI_API_BASES` / `OPENAI_API_KEYS` / `OPENAI_MODELS`
|
||||
- 告警邮箱:`ALERT_EMAIL_TO` / `SMTP_HOST` / `SMTP_PORT` / `SMTP_USER` / `SMTP_PASS`
|
||||
|
||||
---
|
||||
|
||||
## 2. 规则目标
|
||||
|
||||
1. 任一接口超时或异常,自动切换到下一个接口。
|
||||
2. 只要队列中有一个接口可用,必须返回正常回复。
|
||||
3. 全部接口不可用时,自动发邮件到 `zhiqun@qq.com`,并返回降级回复,不能空响应。
|
||||
|
||||
---
|
||||
|
||||
## 3. 可直接使用的配置模板
|
||||
|
||||
```bash
|
||||
# 1) 接口队列(按顺序)
|
||||
export OPENAI_API_BASES="https://api.openai.com/v1,https://openrouter.ai/api/v1,https://your-backup-api/v1"
|
||||
|
||||
# 2) 对应密钥(顺序与上面一致;可先只填一个,会回退到 OPENAI_API_KEY)
|
||||
export OPENAI_API_KEYS="sk-main,sk-backup,sk-third"
|
||||
|
||||
# 3) 对应模型(可选,不填则回退 OPENAI_MODEL)
|
||||
export OPENAI_MODELS="gpt-4o-mini,openai/gpt-4o-mini,gpt-4o-mini"
|
||||
|
||||
# 4) 单接口兜底(建议保留)
|
||||
export OPENAI_API_BASE="https://api.openai.com/v1"
|
||||
export OPENAI_API_KEY="sk-main"
|
||||
export OPENAI_MODEL="gpt-4o-mini"
|
||||
|
||||
# 5) 全挂告警邮件
|
||||
export ALERT_EMAIL_TO="zhiqun@qq.com"
|
||||
export SMTP_HOST="smtp.qq.com"
|
||||
export SMTP_PORT="465"
|
||||
export SMTP_USER="zhiqun@qq.com"
|
||||
export SMTP_PASS="你的QQ邮箱授权码"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 执行逻辑(网关内置)
|
||||
|
||||
1. 读取 `OPENAI_API_BASES` 队列。
|
||||
2. 按顺序逐个请求上游接口。
|
||||
3. 某个接口成功(HTTP 200)即返回结果,不再继续重试后续接口。
|
||||
4. 失败(超时/异常/非 200)则自动切到下一接口。
|
||||
5. 若全部失败:
|
||||
- 发送告警邮件(默认带 300 秒冷却,避免刷屏);
|
||||
- 返回可读降级回复,保证前端有响应。
|
||||
|
||||
---
|
||||
|
||||
## 5. 验证清单
|
||||
|
||||
1. 停掉第一个接口或改错第一个 key,确认仍能正常回复(证明切换生效)。
|
||||
2. 同时让全部接口不可用,确认收到 `zhiqun@qq.com` 告警。
|
||||
3. 查看网关响应:不应出现空白回复或长时间卡死。
|
||||
|
||||
@@ -139,3 +139,4 @@
|
||||
| 2026-02-25 10:23:07 | 🔄 卡若AI 同步 2026-02-25 10:22 | 更新:水桥平台对接、运营中枢工作台 | 排除 >20MB: 13 个 |
|
||||
| 2026-02-25 10:26:04 | 🔄 卡若AI 同步 2026-02-25 10:26 | 更新:水桥平台对接、水溪整理归档 | 排除 >20MB: 13 个 |
|
||||
| 2026-02-25 11:03:16 | 🔄 卡若AI 同步 2026-02-25 11:03 | 更新:水桥平台对接、运营中枢工作台 | 排除 >20MB: 13 个 |
|
||||
| 2026-02-25 11:52:39 | 🔄 卡若AI 同步 2026-02-25 11:52 | 更新:水溪整理归档、运营中枢、运营中枢工作台 | 排除 >20MB: 13 个 |
|
||||
|
||||
@@ -142,3 +142,4 @@
|
||||
| 2026-02-25 10:23:07 | 成功 | 成功 | 🔄 卡若AI 同步 2026-02-25 10:22 | 更新:水桥平台对接、运营中枢工作台 | 排除 >20MB: 13 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
|
||||
| 2026-02-25 10:26:04 | 成功 | 成功 | 🔄 卡若AI 同步 2026-02-25 10:26 | 更新:水桥平台对接、水溪整理归档 | 排除 >20MB: 13 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
|
||||
| 2026-02-25 11:03:16 | 成功 | 成功 | 🔄 卡若AI 同步 2026-02-25 11:03 | 更新:水桥平台对接、运营中枢工作台 | 排除 >20MB: 13 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
|
||||
| 2026-02-25 11:52:39 | 成功 | 成功 | 🔄 卡若AI 同步 2026-02-25 11:52 | 更新:水溪整理归档、运营中枢、运营中枢工作台 | 排除 >20MB: 13 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
|
||||
|
||||
Reference in New Issue
Block a user