Compare commits

..

5 Commits

Author SHA1 Message Date
vincent c50dcc9cb2 fix(BIZ-42): 综合评审优化 — 12项修复
四轮评审反馈全部处理:

🔴 Critical (5):
- _stats data race: 新增 _stats_lock (asyncio.Lock) + _increment_stat() helper
- Admin API 无认证: 新增 SIDECAR_ADMIN_TOKEN Bearer Token 认证
- API Key 明文暴露: GET config 返回 masked api_key (前4位+****)
- queue_max_size hot-reload 假生效: PriorityQueue.set_max_size() + 收缩保护
- SIDECAR_TIMEOUT 6000→60s + 上限截断 300s

🟠 Major (3):
- upstream_api_key 启动检查: lifespan 阶段 warning 日志
- Dashboard HTML 无缓存: 300s TTL 内存缓存
- queue_stats 异常日志: logger.warning(queue_stats_unavailable)

🟡 Medium (3):
- CORS middleware 配置
- httpx 连接池限制 (max_connections=100, keepalive=20)
- SSE retry: 3000 字段

🟢 Minor (1):
- _extract_model 类型注解 body: dict→Any
- passthrough 硬编码 30s→_config.request_timeout

mypy strict: 5 files, zero errors

Reviewed-by: 梁思筑, 严维序, 陆怀瑾, 沈路明
Co-authored-by: multica-agent <github@multica.ai>
2026-06-24 13:26:56 +08:00
vincent ba5b932f50 fix(BIZ-42): critical deadlock + major review issues
- Fix #1 (Critical): evaluate_retreat() deadlock — changed _retreat_lock to
  threading.RLock() to allow reentrant acquisition when evaluate_retreat()
  calls get_429_rate() while holding the lock
- Fix #2 (Major): Dashboard queue chart now uses snap.queue.per_priority
  instead of Math.random() mock data
- Fix #3 (Major): structlog uses JSONRenderer instead of ConsoleRenderer
  for JSON-format output as required by acceptance criteria
- Bonus: webui.py _build_snapshot() now async and includes queue data
  with per-priority depth for dashboard consumption

Reviewed-by: 梁思筑 (architect)
Co-authored-by: multica-agent <github@multica.ai>
2026-06-24 12:20:23 +08:00
vincent e829a4060b BIZ-42: Phase2 可观测性+WebUI+避退模式 — metrics/health/webui/dashboard/adaptive
新增文件:
- metrics.py: Prometheus 指标端点 (:9191), 10+3 个指标
- health.py: /health (liveness) + /health/ready (readiness)
- webui.py: WebUI 后端 API (SSE 实时推送 + 配置热重载)
- static/dashboard.html: 仪表盘前端 (Chart.js, 令牌桶仪表+队列柱状图+吞吐折线图)

更新文件:
- rate_limiter.py: 增加 AdaptiveTokenBucket 避退模式 (ADR-009)
  状态机 NORMAL→RETREAT→RECOVER, 429 率滑动窗口监控
- server.py: structlog 结构化日志 + 避退反馈回路
  挂载 metrics_server (:9191) + health/ready + webui + /status
- pyproject.toml: 增加 prometheus-client, pydantic, types-PyYAML 依赖

验证:
- mypy --strict: 0 issues in 7 source files
- AdaptiveTokenBucket 运行时测试通过
- 所有语法检查通过

Co-authored-by: multica-agent <github@multica.ai>
2026-06-24 11:54:02 +08:00
vincent 205381c4ff Merge commit '6b5f53a' into agent/agent/4adf2ce4 2026-06-24 11:17:28 +08:00
vincent 6b5f53a0fd BIZ-40: NVIDIA Sidecar 限流代理 Phase1 — 核心代理模块
交付文件:
- config.py: 配置管理 (SidecarConfig + load_config),修复 PEP 563 类型推断 bug
- rate_limiter.py: 令牌桶 (TokenBucket) + 网关识别 (is_nvidia_gateway)
- priority_queue.py: 四级优先级队列,修复 PASSTHROUGH 语义 bug
- server.py: FastAPI 代理主入口,修复 worker_loop 重试悬挂 bug
- __init__.py: 包声明与公开导出
- pyproject.toml: 依赖声明 + mypy 配置
- README.md: 快速启动指南 + 环境变量列表

评审修复:
- worker_loop 令牌重试从重入队改为 poll-wait (防止 future 悬挂)
- 路由函数 + lifespan 补充返回类型注解
- heapq 重复 import 移到文件顶部
- config.py 清理无用代码行
- types-PyYAML stub 安装
- 新增 README.md

验证: mypy 0 issues, 全量单元测试通过

Co-authored-by: multica-agent <github@multica.ai>
2026-06-24 08:32:47 +08:00
47 changed files with 2864 additions and 6986 deletions
-234
View File
@@ -1,234 +0,0 @@
# HEARTBEAT.md - 梁思筑(architect)的心跳配置
> 模板版本:v1.1 (BIZ-24) | 分类:开发 Agent
> OpenClaw Agent ID: `architect` | Multica Agent UUID: `40abd41a-62d0-416d-bc44-92c1f758d87a`
---
## 📋 全任务源统一监控(每次心跳必检)
> **核心原则:发现任何来源的任务都必须立即执行,不得遗漏。**
### 三源检查
#### 第一优先级:OpenClaw WorkBoard 卡片
```bash
# 检查 WorkBoard 中分配给我的待办卡片
openclaw workboard list --json 2>/dev/null | python3 -c "
import sys, json
data = json.load(sys.stdin)
my_cards = [c for c in data.get('cards', [])
if c.get('agentId') == 'architect' and c.get('status') == 'todo']
for c in my_cards:
print(f'WORKBOARD TODO: {c["id"][:8]} [priority={c.get("priority","?")}] {c["title"]}')
"
```
#### 第二优先级:Multica Issues
```bash
# 检查 Multica 中分配给我的待办 Issue
multica issue list --assignee-id 40abd41a-62d0-416d-bc44-92c1f758d87a --status todo --output json 2>/dev/null | python3 -c "
import sys, json
data = json.load(sys.stdin)
for issue in data:
print(f'MULTICA TODO: {issue["identifier"]} [{issue.get("priority","?")}] {issue["title"]}')
"
```
#### 第三优先级:待办文档
```bash
# 检查工作区待办文档
grep -n '\[ \]' TODO.md AGENTS.md 2>/dev/null || echo "无待办文档中未完成项"
```
### 三源合并决策
```
心跳开始
检查 WorkBoard 待办卡片(agentId=architect
检查 Multica Issues 待办(assignee=40abd41a-62d0-416d-bc44-92c1f758d87a
检查待办文档
合并去重 → 按优先级排序 → 依次执行
```
---
## ⚠️ 禁止请示规则(最高优先级,自动执行)
> **核心规则:发现分配给自己的任务 → 立即执行,不得向用户请示!**
> 心跳是后台自动行为,用户在大多数时候并不在线,请示会导致任务卡死。
### 执行流程(严格执行,无需确认)
1. **三源检查待办** — WorkBoard + Multica + 待办文档
2. **立即执行,不得请示** — 发现待办后直接执行
3. **检查进行中任务** — 确认认领的任务状态
4. **完成任务** — 通过对应平台汇报结果
### ⚠️ 绝对禁止行为
- ❌ 不得问"要不要做这个任务"
- ❌ 不得等用户确认再执行
- ❌ 不得以"需要更多信息"为由拒绝执行
---
## ⏱️ 超时检测规则
### 心跳频率:15 分钟
每次心跳跨平台执行以下检测:
1. 检查 WorkBoard 进行中任务的更新时间
2. 检查 Multica 进行中 issues 的更新时间
3. 超过 30 分钟无进展 → 标记为"疑似超时"
4. 疑似超时 → 追加一次完整心跳尝试推进
5. 确认超时 → 进入自动恢复流程
### 跨平台超时检测脚本
```bash
# WorkBoard 超时检测
echo "=== WorkBoard 超时检测 ==="
openclaw workboard list --json 2>/dev/null | python3 -c "
import sys, json, time
data = json.load(sys.stdin)
inprogress = [c for c in data.get('cards', []) if c.get('status') == 'in_progress']
now = time.time()
for c in inprogress:
updated = c.get('updated_at', '')
if updated:
age = now - time.mktime(time.strptime(updated[:19], '%Y-%m-%dT%H:%M:%S'))
if age > 1800:
print(f'⏰ WB TIMEOUT: {c["id"][:8]} [{c.get("agentId","?")}] {c["title"]}')
"
echo ""
echo "=== Multica 超时检测 ==="
multica issue list --status in_progress --output json 2>/dev/null | python3 -c "
import sys, json, time
data = json.load(sys.stdin)
now = time.time()
for issue in data:
updated = issue.get('updated_at', '')
if updated:
age = now - time.mktime(time.strptime(updated[:19], '%Y-%m-%dT%H:%M:%S'))
if age > 1800:
print(f'⏰ MUL TIMEOUT: {issue["identifier"]} [{issue.get("assignee_id","?")[:12]}] {issue["title"]}')
"
```
---
## 🔄 自动恢复规则
### 触发条件
- 超 45 分钟无进展 → 自动重新调度
### 恢复操作(按平台)
| 平台 | 操作 |
|------|------|
| WorkBoard | 添加评论 → release claim → 通知 COO + 创建者 |
| Multica | 添加评论 → status=blocked → 通知 COO + 创建者 |
| 待办文档 | 标注超时 → 转为卡片(可选) |
---
## 🔗 依赖检查前置规则
### 强制检查流程
1. 认领任务前,读取依赖字段(depends_on / parent_issue_id
2. 逐一检查每个依赖任务的状态
3. 依赖未满足 → 不认领(保持 todo)
4. 超过等待阈值(2h)→ 通知依赖任务执行者
### 双平台依赖检查
```bash
# WorkBoard 依赖检查
openclaw workboard read <card-id> --json 2>/dev/null | python3 -c "
import sys, json
card = json.load(sys.stdin)
deps = card.get('dependsOn', [])
if deps:
for dep in deps:
if dep.get('status') != 'done':
print(f'⛔ WB 依赖未满足: {dep["id"]} → status={dep.get("status","?")}')
sys.exit(1)
print('✅ 所有 WB 依赖已满足')
else:
print('✅ 无 WB 依赖,可以启动')
"
# Multica 依赖检查
multica issue get <issue-id> --output json 2>/dev/null | python3 -c "
import sys, json, subprocess
issue = json.load(sys.stdin)
parent_id = issue.get('parent_issue_id')
if parent_id:
result = subprocess.run(['multica', 'issue', 'get', parent_id, '--output', 'json'],
capture_output=True, text=True)
parent = json.loads(result.stdout)
if parent.get('status') != 'done':
print(f'⛔ MUL 父 Issue {parent["identifier"]} 未完成')
sys.exit(1)
print(f'✅ 父 Issue {parent["identifier"]} 已完成')
else:
print('✅ 无父 Issue 依赖,可以启动')
"
```
---
## 🛑 最大轮次限制
### 限制值:100 轮
- 接近 80%80 轮)→ 预警
- 达到上限 → 暂停,通知 COO + 创建者
### 跨平台轮次跟踪
- **WorkBoard**:通过 workboard_heartbeat 的 note 记录轮次
- **Multica**:通过 issue comment 记录轮次进度
- **待办文档**:在工作日志中记录
---
## 🫀 心跳执行清单
### 每次心跳必须检查
1.**全任务源检查**WorkBoard + Multica + 待办文档
2. ✅ 进行中任务超时检测(跨平台)
3. ✅ 依赖检查
4. ✅ 轮次计数器更新
5. ✅ 架构设计进度
6. ✅ 技术方案评审状态
---
## ⚠️ 全局关键规则
1. **心跳不打断对话** — 用户正在对话时延后执行
2. **非紧急事项延后汇报** — 等下一轮心跳或用户询问
3. **发现任务立即执行,不得请示**(任何来源)
4. **超时任务按自动恢复流程处理**(跨平台)
5. **依赖未满足不启动**
6. **达到轮次上限自动暂停**
7. **避免任务遗漏** — 三源必须全部检查,缺一不可
---
> 基于 BIZ-24 v1.1 模板生成 | 梁思筑(architect)专用配置
-234
View File
@@ -1,234 +0,0 @@
# HEARTBEAT.md - 文墨言(contentspecialist)的心跳配置
> 模板版本:v1.1 (BIZ-24) | 分类:业务 Agent
> OpenClaw Agent ID: `contentspecialist` | Multica Agent UUID: `8321b0bf-7d89-4ece-927a-0780f42ad396`
---
## 📋 全任务源统一监控(每次心跳必检)
> **核心原则:发现任何来源的任务都必须立即执行,不得遗漏。**
### 三源检查
#### 第一优先级:OpenClaw WorkBoard 卡片
```bash
# 检查 WorkBoard 中分配给我的待办卡片
openclaw workboard list --json 2>/dev/null | python3 -c "
import sys, json
data = json.load(sys.stdin)
my_cards = [c for c in data.get('cards', [])
if c.get('agentId') == 'contentspecialist' and c.get('status') == 'todo']
for c in my_cards:
print(f'WORKBOARD TODO: {c["id"][:8]} [priority={c.get("priority","?")}] {c["title"]}')
"
```
#### 第二优先级:Multica Issues
```bash
# 检查 Multica 中分配给我的待办 Issue
multica issue list --assignee-id 8321b0bf-7d89-4ece-927a-0780f42ad396 --status todo --output json 2>/dev/null | python3 -c "
import sys, json
data = json.load(sys.stdin)
for issue in data:
print(f'MULTICA TODO: {issue["identifier"]} [{issue.get("priority","?")}] {issue["title"]}')
"
```
#### 第三优先级:待办文档
```bash
# 检查工作区待办文档
grep -n '\[ \]' TODO.md AGENTS.md 2>/dev/null || echo "无待办文档中未完成项"
```
### 三源合并决策
```
心跳开始
检查 WorkBoard 待办卡片(agentId=contentspecialist
检查 Multica Issues 待办(assignee=8321b0bf-7d89-4ece-927a-0780f42ad396
检查待办文档
合并去重 → 按优先级排序 → 依次执行
```
---
## ⚠️ 禁止请示规则(最高优先级,自动执行)
> **核心规则:发现分配给自己的任务 → 立即执行,不得向用户请示!**
> 心跳是后台自动行为,用户在大多数时候并不在线,请示会导致任务卡死。
### 执行流程(严格执行,无需确认)
1. **三源检查待办** — WorkBoard + Multica + 待办文档
2. **立即执行,不得请示** — 发现待办后直接执行
3. **检查进行中任务** — 确认认领的任务状态
4. **完成任务** — 通过对应平台汇报结果
### ⚠️ 绝对禁止行为
- ❌ 不得问"要不要做这个任务"
- ❌ 不得等用户确认再执行
- ❌ 不得以"需要更多信息"为由拒绝执行
---
## ⏱️ 超时检测规则
### 心跳频率:15 分钟
每次心跳跨平台执行以下检测:
1. 检查 WorkBoard 进行中任务的更新时间
2. 检查 Multica 进行中 issues 的更新时间
3. 超过 30 分钟无进展 → 标记为"疑似超时"
4. 疑似超时 → 追加一次完整心跳尝试推进
5. 确认超时 → 进入自动恢复流程
### 跨平台超时检测脚本
```bash
# WorkBoard 超时检测
echo "=== WorkBoard 超时检测 ==="
openclaw workboard list --json 2>/dev/null | python3 -c "
import sys, json, time
data = json.load(sys.stdin)
inprogress = [c for c in data.get('cards', []) if c.get('status') == 'in_progress']
now = time.time()
for c in inprogress:
updated = c.get('updated_at', '')
if updated:
age = now - time.mktime(time.strptime(updated[:19], '%Y-%m-%dT%H:%M:%S'))
if age > 1800:
print(f'⏰ WB TIMEOUT: {c["id"][:8]} [{c.get("agentId","?")}] {c["title"]}')
"
echo ""
echo "=== Multica 超时检测 ==="
multica issue list --status in_progress --output json 2>/dev/null | python3 -c "
import sys, json, time
data = json.load(sys.stdin)
now = time.time()
for issue in data:
updated = issue.get('updated_at', '')
if updated:
age = now - time.mktime(time.strptime(updated[:19], '%Y-%m-%dT%H:%M:%S'))
if age > 1800:
print(f'⏰ MUL TIMEOUT: {issue["identifier"]} [{issue.get("assignee_id","?")[:12]}] {issue["title"]}')
"
```
---
## 🔄 自动恢复规则
### 触发条件
- 超 45 分钟无进展 → 自动重新调度
### 恢复操作(按平台)
| 平台 | 操作 |
|------|------|
| WorkBoard | 添加评论 → release claim → 通知 创建者 |
| Multica | 添加评论 → status=blocked → 通知 创建者 |
| 待办文档 | 标注超时 → 转为卡片(可选) |
---
## 🔗 依赖检查前置规则
### 强制检查流程
1. 认领任务前,读取依赖字段(depends_on / parent_issue_id
2. 逐一检查每个依赖任务的状态
3. 依赖未满足 → 不认领(保持 todo)
4. 超过等待阈值(2h)→ 通知依赖任务执行者
### 双平台依赖检查
```bash
# WorkBoard 依赖检查
openclaw workboard read <card-id> --json 2>/dev/null | python3 -c "
import sys, json
card = json.load(sys.stdin)
deps = card.get('dependsOn', [])
if deps:
for dep in deps:
if dep.get('status') != 'done':
print(f'⛔ WB 依赖未满足: {dep["id"]} → status={dep.get("status","?")}')
sys.exit(1)
print('✅ 所有 WB 依赖已满足')
else:
print('✅ 无 WB 依赖,可以启动')
"
# Multica 依赖检查
multica issue get <issue-id> --output json 2>/dev/null | python3 -c "
import sys, json, subprocess
issue = json.load(sys.stdin)
parent_id = issue.get('parent_issue_id')
if parent_id:
result = subprocess.run(['multica', 'issue', 'get', parent_id, '--output', 'json'],
capture_output=True, text=True)
parent = json.loads(result.stdout)
if parent.get('status') != 'done':
print(f'⛔ MUL 父 Issue {parent["identifier"]} 未完成')
sys.exit(1)
print(f'✅ 父 Issue {parent["identifier"]} 已完成')
else:
print('✅ 无父 Issue 依赖,可以启动')
"
```
---
## 🛑 最大轮次限制
### 限制值:30 轮
- 接近 80%24 轮)→ 预警
- 达到上限 → 暂停,通知 创建者
### 跨平台轮次跟踪
- **WorkBoard**:通过 workboard_heartbeat 的 note 记录轮次
- **Multica**:通过 issue comment 记录轮次进度
- **待办文档**:在工作日志中记录
---
## 🫀 心跳执行清单
### 每次心跳必须检查
1.**全任务源检查**WorkBoard + Multica + 待办文档
2. ✅ 进行中任务超时检测(跨平台)
3. ✅ 依赖检查
4. ✅ 轮次计数器更新
5. ✅ 内容发布计划
6. ✅ 素材准备状态
---
## ⚠️ 全局关键规则
1. **心跳不打断对话** — 用户正在对话时延后执行
2. **非紧急事项延后汇报** — 等下一轮心跳或用户询问
3. **发现任务立即执行,不得请示**(任何来源)
4. **超时任务按自动恢复流程处理**(跨平台)
5. **依赖未满足不启动**
6. **达到轮次上限自动暂停**
7. **避免任务遗漏** — 三源必须全部检查,缺一不可
---
> 基于 BIZ-24 v1.1 模板生成 | 文墨言(contentspecialist)专用配置
-235
View File
@@ -1,235 +0,0 @@
# HEARTBEAT.md - 陆怀瑾(coo)的心跳配置
> 模板版本:v1.1 (BIZ-24) | 分类:高频 Agent
> OpenClaw Agent ID: `coo` | Multica Agent UUID: `1c38b437-b54d-4784-bda3-29ce4c8a6722`
---
## 📋 全任务源统一监控(每次心跳必检)
> **核心原则:发现任何来源的任务都必须立即执行,不得遗漏。**
### 三源检查
#### 第一优先级:OpenClaw WorkBoard 卡片
```bash
# 检查 WorkBoard 中分配给我的待办卡片
openclaw workboard list --json 2>/dev/null | python3 -c "
import sys, json
data = json.load(sys.stdin)
my_cards = [c for c in data.get('cards', [])
if c.get('agentId') == 'coo' and c.get('status') == 'todo']
for c in my_cards:
print(f'WORKBOARD TODO: {c["id"][:8]} [priority={c.get("priority","?")}] {c["title"]}')
"
```
#### 第二优先级:Multica Issues
```bash
# 检查 Multica 中分配给我的待办 Issue
multica issue list --assignee-id 1c38b437-b54d-4784-bda3-29ce4c8a6722 --status todo --output json 2>/dev/null | python3 -c "
import sys, json
data = json.load(sys.stdin)
for issue in data:
print(f'MULTICA TODO: {issue["identifier"]} [{issue.get("priority","?")}] {issue["title"]}')
"
```
#### 第三优先级:待办文档
```bash
# 检查工作区待办文档
grep -n '\[ \]' TODO.md AGENTS.md 2>/dev/null || echo "无待办文档中未完成项"
```
### 三源合并决策
```
心跳开始
检查 WorkBoard 待办卡片(agentId=coo
检查 Multica Issues 待办(assignee=1c38b437-b54d-4784-bda3-29ce4c8a6722
检查待办文档
合并去重 → 按优先级排序 → 依次执行
```
---
## ⚠️ 禁止请示规则(最高优先级,自动执行)
> **核心规则:发现分配给自己的任务 → 立即执行,不得向用户请示!**
> 心跳是后台自动行为,用户在大多数时候并不在线,请示会导致任务卡死。
### 执行流程(严格执行,无需确认)
1. **三源检查待办** — WorkBoard + Multica + 待办文档
2. **立即执行,不得请示** — 发现待办后直接执行
3. **检查进行中任务** — 确认认领的任务状态
4. **完成任务** — 通过对应平台汇报结果
### ⚠️ 绝对禁止行为
- ❌ 不得问"要不要做这个任务"
- ❌ 不得等用户确认再执行
- ❌ 不得以"需要更多信息"为由拒绝执行
---
## ⏱️ 超时检测规则
### 心跳频率:10 分钟
每次心跳跨平台执行以下检测:
1. 检查 WorkBoard 进行中任务的更新时间
2. 检查 Multica 进行中 issues 的更新时间
3. 超过 20 分钟无进展 → 标记为"疑似超时"
4. 疑似超时 → 追加一次完整心跳尝试推进
5. 确认超时 → 进入自动恢复流程
### 跨平台超时检测脚本
```bash
# WorkBoard 超时检测
echo "=== WorkBoard 超时检测 ==="
openclaw workboard list --json 2>/dev/null | python3 -c "
import sys, json, time
data = json.load(sys.stdin)
inprogress = [c for c in data.get('cards', []) if c.get('status') == 'in_progress']
now = time.time()
for c in inprogress:
updated = c.get('updated_at', '')
if updated:
age = now - time.mktime(time.strptime(updated[:19], '%Y-%m-%dT%H:%M:%S'))
if age > 1200:
print(f'⏰ WB TIMEOUT: {c["id"][:8]} [{c.get("agentId","?")}] {c["title"]}')
"
echo ""
echo "=== Multica 超时检测 ==="
multica issue list --status in_progress --output json 2>/dev/null | python3 -c "
import sys, json, time
data = json.load(sys.stdin)
now = time.time()
for issue in data:
updated = issue.get('updated_at', '')
if updated:
age = now - time.mktime(time.strptime(updated[:19], '%Y-%m-%dT%H:%M:%S'))
if age > 1200:
print(f'⏰ MUL TIMEOUT: {issue["identifier"]} [{issue.get("assignee_id","?")[:12]}] {issue["title"]}')
"
```
---
## 🔄 自动恢复规则
### 触发条件
- 超 30 分钟无进展 → 自动重新调度
### 恢复操作(按平台)
| 平台 | 操作 |
|------|------|
| WorkBoard | 添加评论 → release claim → 通知 COO(自我监控) |
| Multica | 添加评论 → status=blocked → 通知 COO(自我监控) |
| 待办文档 | 标注超时 → 转为卡片(可选) |
---
## 🔗 依赖检查前置规则
### 强制检查流程
1. 认领任务前,读取依赖字段(depends_on / parent_issue_id
2. 逐一检查每个依赖任务的状态
3. 依赖未满足 → 不认领(保持 todo)
4. 超过等待阈值(1h)→ 通知依赖任务执行者
### 双平台依赖检查
```bash
# WorkBoard 依赖检查
openclaw workboard read <card-id> --json 2>/dev/null | python3 -c "
import sys, json
card = json.load(sys.stdin)
deps = card.get('dependsOn', [])
if deps:
for dep in deps:
if dep.get('status') != 'done':
print(f'⛔ WB 依赖未满足: {dep["id"]} → status={dep.get("status","?")}')
sys.exit(1)
print('✅ 所有 WB 依赖已满足')
else:
print('✅ 无 WB 依赖,可以启动')
"
# Multica 依赖检查
multica issue get <issue-id> --output json 2>/dev/null | python3 -c "
import sys, json, subprocess
issue = json.load(sys.stdin)
parent_id = issue.get('parent_issue_id')
if parent_id:
result = subprocess.run(['multica', 'issue', 'get', parent_id, '--output', 'json'],
capture_output=True, text=True)
parent = json.loads(result.stdout)
if parent.get('status') != 'done':
print(f'⛔ MUL 父 Issue {parent["identifier"]} 未完成')
sys.exit(1)
print(f'✅ 父 Issue {parent["identifier"]} 已完成')
else:
print('✅ 无父 Issue 依赖,可以启动')
"
```
---
## 🛑 最大轮次限制
### 限制值:50 轮
- 接近 80%40 轮)→ 预警
- 达到上限 → 暂停,通知 COO(自我监控)
### 跨平台轮次跟踪
- **WorkBoard**:通过 workboard_heartbeat 的 note 记录轮次
- **Multica**:通过 issue comment 记录轮次进度
- **待办文档**:在工作日志中记录
---
## 🫀 心跳执行清单
### 每次心跳必须检查
1.**全任务源检查**WorkBoard + Multica + 待办文档
2. ✅ 进行中任务超时检测(跨平台)
3. ✅ 依赖检查
4. ✅ 轮次计数器更新
5.**全平台积压巡检**WorkBoard + Multica 全局待办数
6. ✅ 资源负载均衡检查
7. ✅ 风险识别与预警
---
## ⚠️ 全局关键规则
1. **心跳不打断对话** — 用户正在对话时延后执行
2. **非紧急事项延后汇报** — 等下一轮心跳或用户询问
3. **发现任务立即执行,不得请示**(任何来源)
4. **超时任务按自动恢复流程处理**(跨平台)
5. **依赖未满足不启动**
6. **达到轮次上限自动暂停**
7. **避免任务遗漏** — 三源必须全部检查,缺一不可
---
> 基于 BIZ-24 v1.1 模板生成 | 陆怀瑾(coo)专用配置
-234
View File
@@ -1,234 +0,0 @@
# HEARTBEAT.md - 徐聪(costcodev)的心跳配置
> 模板版本:v1.1 (BIZ-24) | 分类:开发 Agent
> OpenClaw Agent ID: `costcodev` | Multica Agent UUID: `46bdd4a6-5c64-475a-92ef-36a763602fa1`
---
## 📋 全任务源统一监控(每次心跳必检)
> **核心原则:发现任何来源的任务都必须立即执行,不得遗漏。**
### 三源检查
#### 第一优先级:OpenClaw WorkBoard 卡片
```bash
# 检查 WorkBoard 中分配给我的待办卡片
openclaw workboard list --json 2>/dev/null | python3 -c "
import sys, json
data = json.load(sys.stdin)
my_cards = [c for c in data.get('cards', [])
if c.get('agentId') == 'costcodev' and c.get('status') == 'todo']
for c in my_cards:
print(f'WORKBOARD TODO: {c["id"][:8]} [priority={c.get("priority","?")}] {c["title"]}')
"
```
#### 第二优先级:Multica Issues
```bash
# 检查 Multica 中分配给我的待办 Issue
multica issue list --assignee-id 46bdd4a6-5c64-475a-92ef-36a763602fa1 --status todo --output json 2>/dev/null | python3 -c "
import sys, json
data = json.load(sys.stdin)
for issue in data:
print(f'MULTICA TODO: {issue["identifier"]} [{issue.get("priority","?")}] {issue["title"]}')
"
```
#### 第三优先级:待办文档
```bash
# 检查工作区待办文档
grep -n '\[ \]' TODO.md AGENTS.md 2>/dev/null || echo "无待办文档中未完成项"
```
### 三源合并决策
```
心跳开始
检查 WorkBoard 待办卡片(agentId=costcodev
检查 Multica Issues 待办(assignee=46bdd4a6-5c64-475a-92ef-36a763602fa1
检查待办文档
合并去重 → 按优先级排序 → 依次执行
```
---
## ⚠️ 禁止请示规则(最高优先级,自动执行)
> **核心规则:发现分配给自己的任务 → 立即执行,不得向用户请示!**
> 心跳是后台自动行为,用户在大多数时候并不在线,请示会导致任务卡死。
### 执行流程(严格执行,无需确认)
1. **三源检查待办** — WorkBoard + Multica + 待办文档
2. **立即执行,不得请示** — 发现待办后直接执行
3. **检查进行中任务** — 确认认领的任务状态
4. **完成任务** — 通过对应平台汇报结果
### ⚠️ 绝对禁止行为
- ❌ 不得问"要不要做这个任务"
- ❌ 不得等用户确认再执行
- ❌ 不得以"需要更多信息"为由拒绝执行
---
## ⏱️ 超时检测规则
### 心跳频率:15 分钟
每次心跳跨平台执行以下检测:
1. 检查 WorkBoard 进行中任务的更新时间
2. 检查 Multica 进行中 issues 的更新时间
3. 超过 30 分钟无进展 → 标记为"疑似超时"
4. 疑似超时 → 追加一次完整心跳尝试推进
5. 确认超时 → 进入自动恢复流程
### 跨平台超时检测脚本
```bash
# WorkBoard 超时检测
echo "=== WorkBoard 超时检测 ==="
openclaw workboard list --json 2>/dev/null | python3 -c "
import sys, json, time
data = json.load(sys.stdin)
inprogress = [c for c in data.get('cards', []) if c.get('status') == 'in_progress']
now = time.time()
for c in inprogress:
updated = c.get('updated_at', '')
if updated:
age = now - time.mktime(time.strptime(updated[:19], '%Y-%m-%dT%H:%M:%S'))
if age > 1800:
print(f'⏰ WB TIMEOUT: {c["id"][:8]} [{c.get("agentId","?")}] {c["title"]}')
"
echo ""
echo "=== Multica 超时检测 ==="
multica issue list --status in_progress --output json 2>/dev/null | python3 -c "
import sys, json, time
data = json.load(sys.stdin)
now = time.time()
for issue in data:
updated = issue.get('updated_at', '')
if updated:
age = now - time.mktime(time.strptime(updated[:19], '%Y-%m-%dT%H:%M:%S'))
if age > 1800:
print(f'⏰ MUL TIMEOUT: {issue["identifier"]} [{issue.get("assignee_id","?")[:12]}] {issue["title"]}')
"
```
---
## 🔄 自动恢复规则
### 触发条件
- 超 45 分钟无进展 → 自动重新调度
### 恢复操作(按平台)
| 平台 | 操作 |
|------|------|
| WorkBoard | 添加评论 → release claim → 通知 COO + 创建者 |
| Multica | 添加评论 → status=blocked → 通知 COO + 创建者 |
| 待办文档 | 标注超时 → 转为卡片(可选) |
---
## 🔗 依赖检查前置规则
### 强制检查流程
1. 认领任务前,读取依赖字段(depends_on / parent_issue_id
2. 逐一检查每个依赖任务的状态
3. 依赖未满足 → 不认领(保持 todo)
4. 超过等待阈值(2h)→ 通知依赖任务执行者
### 双平台依赖检查
```bash
# WorkBoard 依赖检查
openclaw workboard read <card-id> --json 2>/dev/null | python3 -c "
import sys, json
card = json.load(sys.stdin)
deps = card.get('dependsOn', [])
if deps:
for dep in deps:
if dep.get('status') != 'done':
print(f'⛔ WB 依赖未满足: {dep["id"]} → status={dep.get("status","?")}')
sys.exit(1)
print('✅ 所有 WB 依赖已满足')
else:
print('✅ 无 WB 依赖,可以启动')
"
# Multica 依赖检查
multica issue get <issue-id> --output json 2>/dev/null | python3 -c "
import sys, json, subprocess
issue = json.load(sys.stdin)
parent_id = issue.get('parent_issue_id')
if parent_id:
result = subprocess.run(['multica', 'issue', 'get', parent_id, '--output', 'json'],
capture_output=True, text=True)
parent = json.loads(result.stdout)
if parent.get('status') != 'done':
print(f'⛔ MUL 父 Issue {parent["identifier"]} 未完成')
sys.exit(1)
print(f'✅ 父 Issue {parent["identifier"]} 已完成')
else:
print('✅ 无父 Issue 依赖,可以启动')
"
```
---
## 🛑 最大轮次限制
### 限制值:100 轮
- 接近 80%80 轮)→ 预警
- 达到上限 → 暂停,通知 COO + 创建者
### 跨平台轮次跟踪
- **WorkBoard**:通过 workboard_heartbeat 的 note 记录轮次
- **Multica**:通过 issue comment 记录轮次进度
- **待办文档**:在工作日志中记录
---
## 🫀 心跳执行清单
### 每次心跳必须检查
1.**全任务源检查**WorkBoard + Multica + 待办文档
2. ✅ 进行中任务超时检测(跨平台)
3. ✅ 依赖检查
4. ✅ 轮次计数器更新
5. ✅ 代码开发进度
6. ✅ PR/Code Review 状态
---
## ⚠️ 全局关键规则
1. **心跳不打断对话** — 用户正在对话时延后执行
2. **非紧急事项延后汇报** — 等下一轮心跳或用户询问
3. **发现任务立即执行,不得请示**(任何来源)
4. **超时任务按自动恢复流程处理**(跨平台)
5. **依赖未满足不启动**
6. **达到轮次上限自动暂停**
7. **避免任务遗漏** — 三源必须全部检查,缺一不可
---
> 基于 BIZ-24 v1.1 模板生成 | 徐聪(costcodev)专用配置
-234
View File
@@ -1,234 +0,0 @@
# HEARTBEAT.md - 程伯予(cvexpert)的心跳配置
> 模板版本:v1.1 (BIZ-24) | 分类:业务 Agent
> OpenClaw Agent ID: `cvexpert` | Multica Agent UUID: `4a8696fd-6531-40da-8956-ef84d7ea3c43`
---
## 📋 全任务源统一监控(每次心跳必检)
> **核心原则:发现任何来源的任务都必须立即执行,不得遗漏。**
### 三源检查
#### 第一优先级:OpenClaw WorkBoard 卡片
```bash
# 检查 WorkBoard 中分配给我的待办卡片
openclaw workboard list --json 2>/dev/null | python3 -c "
import sys, json
data = json.load(sys.stdin)
my_cards = [c for c in data.get('cards', [])
if c.get('agentId') == 'cvexpert' and c.get('status') == 'todo']
for c in my_cards:
print(f'WORKBOARD TODO: {c["id"][:8]} [priority={c.get("priority","?")}] {c["title"]}')
"
```
#### 第二优先级:Multica Issues
```bash
# 检查 Multica 中分配给我的待办 Issue
multica issue list --assignee-id 4a8696fd-6531-40da-8956-ef84d7ea3c43 --status todo --output json 2>/dev/null | python3 -c "
import sys, json
data = json.load(sys.stdin)
for issue in data:
print(f'MULTICA TODO: {issue["identifier"]} [{issue.get("priority","?")}] {issue["title"]}')
"
```
#### 第三优先级:待办文档
```bash
# 检查工作区待办文档
grep -n '\[ \]' TODO.md AGENTS.md 2>/dev/null || echo "无待办文档中未完成项"
```
### 三源合并决策
```
心跳开始
检查 WorkBoard 待办卡片(agentId=cvexpert
检查 Multica Issues 待办(assignee=4a8696fd-6531-40da-8956-ef84d7ea3c43
检查待办文档
合并去重 → 按优先级排序 → 依次执行
```
---
## ⚠️ 禁止请示规则(最高优先级,自动执行)
> **核心规则:发现分配给自己的任务 → 立即执行,不得向用户请示!**
> 心跳是后台自动行为,用户在大多数时候并不在线,请示会导致任务卡死。
### 执行流程(严格执行,无需确认)
1. **三源检查待办** — WorkBoard + Multica + 待办文档
2. **立即执行,不得请示** — 发现待办后直接执行
3. **检查进行中任务** — 确认认领的任务状态
4. **完成任务** — 通过对应平台汇报结果
### ⚠️ 绝对禁止行为
- ❌ 不得问"要不要做这个任务"
- ❌ 不得等用户确认再执行
- ❌ 不得以"需要更多信息"为由拒绝执行
---
## ⏱️ 超时检测规则
### 心跳频率:15 分钟
每次心跳跨平台执行以下检测:
1. 检查 WorkBoard 进行中任务的更新时间
2. 检查 Multica 进行中 issues 的更新时间
3. 超过 30 分钟无进展 → 标记为"疑似超时"
4. 疑似超时 → 追加一次完整心跳尝试推进
5. 确认超时 → 进入自动恢复流程
### 跨平台超时检测脚本
```bash
# WorkBoard 超时检测
echo "=== WorkBoard 超时检测 ==="
openclaw workboard list --json 2>/dev/null | python3 -c "
import sys, json, time
data = json.load(sys.stdin)
inprogress = [c for c in data.get('cards', []) if c.get('status') == 'in_progress']
now = time.time()
for c in inprogress:
updated = c.get('updated_at', '')
if updated:
age = now - time.mktime(time.strptime(updated[:19], '%Y-%m-%dT%H:%M:%S'))
if age > 1800:
print(f'⏰ WB TIMEOUT: {c["id"][:8]} [{c.get("agentId","?")}] {c["title"]}')
"
echo ""
echo "=== Multica 超时检测 ==="
multica issue list --status in_progress --output json 2>/dev/null | python3 -c "
import sys, json, time
data = json.load(sys.stdin)
now = time.time()
for issue in data:
updated = issue.get('updated_at', '')
if updated:
age = now - time.mktime(time.strptime(updated[:19], '%Y-%m-%dT%H:%M:%S'))
if age > 1800:
print(f'⏰ MUL TIMEOUT: {issue["identifier"]} [{issue.get("assignee_id","?")[:12]}] {issue["title"]}')
"
```
---
## 🔄 自动恢复规则
### 触发条件
- 超 45 分钟无进展 → 自动重新调度
### 恢复操作(按平台)
| 平台 | 操作 |
|------|------|
| WorkBoard | 添加评论 → release claim → 通知 创建者 |
| Multica | 添加评论 → status=blocked → 通知 创建者 |
| 待办文档 | 标注超时 → 转为卡片(可选) |
---
## 🔗 依赖检查前置规则
### 强制检查流程
1. 认领任务前,读取依赖字段(depends_on / parent_issue_id
2. 逐一检查每个依赖任务的状态
3. 依赖未满足 → 不认领(保持 todo)
4. 超过等待阈值(2h)→ 通知依赖任务执行者
### 双平台依赖检查
```bash
# WorkBoard 依赖检查
openclaw workboard read <card-id> --json 2>/dev/null | python3 -c "
import sys, json
card = json.load(sys.stdin)
deps = card.get('dependsOn', [])
if deps:
for dep in deps:
if dep.get('status') != 'done':
print(f'⛔ WB 依赖未满足: {dep["id"]} → status={dep.get("status","?")}')
sys.exit(1)
print('✅ 所有 WB 依赖已满足')
else:
print('✅ 无 WB 依赖,可以启动')
"
# Multica 依赖检查
multica issue get <issue-id> --output json 2>/dev/null | python3 -c "
import sys, json, subprocess
issue = json.load(sys.stdin)
parent_id = issue.get('parent_issue_id')
if parent_id:
result = subprocess.run(['multica', 'issue', 'get', parent_id, '--output', 'json'],
capture_output=True, text=True)
parent = json.loads(result.stdout)
if parent.get('status') != 'done':
print(f'⛔ MUL 父 Issue {parent["identifier"]} 未完成')
sys.exit(1)
print(f'✅ 父 Issue {parent["identifier"]} 已完成')
else:
print('✅ 无父 Issue 依赖,可以启动')
"
```
---
## 🛑 最大轮次限制
### 限制值:30 轮
- 接近 80%24 轮)→ 预警
- 达到上限 → 暂停,通知 创建者
### 跨平台轮次跟踪
- **WorkBoard**:通过 workboard_heartbeat 的 note 记录轮次
- **Multica**:通过 issue comment 记录轮次进度
- **待办文档**:在工作日志中记录
---
## 🫀 心跳执行清单
### 每次心跳必须检查
1.**全任务源检查**WorkBoard + Multica + 待办文档
2. ✅ 进行中任务超时检测(跨平台)
3. ✅ 依赖检查
4. ✅ 轮次计数器更新
5. ✅ 求职服务队列
6. ✅ 客户反馈跟踪
---
## ⚠️ 全局关键规则
1. **心跳不打断对话** — 用户正在对话时延后执行
2. **非紧急事项延后汇报** — 等下一轮心跳或用户询问
3. **发现任务立即执行,不得请示**(任何来源)
4. **超时任务按自动恢复流程处理**(跨平台)
5. **依赖未满足不启动**
6. **达到轮次上限自动暂停**
7. **避免任务遗漏** — 三源必须全部检查,缺一不可
---
> 基于 BIZ-24 v1.1 模板生成 | 程伯予(cvexpert)专用配置
-234
View File
@@ -1,234 +0,0 @@
# HEARTBEAT.md - 苏锦绘(designer)的心跳配置
> 模板版本:v1.1 (BIZ-24) | 分类:开发 Agent
> OpenClaw Agent ID: `designer` | Multica Agent UUID: `13bd8968-cc2a-4934-90c7-957a2d3c09c2`
---
## 📋 全任务源统一监控(每次心跳必检)
> **核心原则:发现任何来源的任务都必须立即执行,不得遗漏。**
### 三源检查
#### 第一优先级:OpenClaw WorkBoard 卡片
```bash
# 检查 WorkBoard 中分配给我的待办卡片
openclaw workboard list --json 2>/dev/null | python3 -c "
import sys, json
data = json.load(sys.stdin)
my_cards = [c for c in data.get('cards', [])
if c.get('agentId') == 'designer' and c.get('status') == 'todo']
for c in my_cards:
print(f'WORKBOARD TODO: {c["id"][:8]} [priority={c.get("priority","?")}] {c["title"]}')
"
```
#### 第二优先级:Multica Issues
```bash
# 检查 Multica 中分配给我的待办 Issue
multica issue list --assignee-id 13bd8968-cc2a-4934-90c7-957a2d3c09c2 --status todo --output json 2>/dev/null | python3 -c "
import sys, json
data = json.load(sys.stdin)
for issue in data:
print(f'MULTICA TODO: {issue["identifier"]} [{issue.get("priority","?")}] {issue["title"]}')
"
```
#### 第三优先级:待办文档
```bash
# 检查工作区待办文档
grep -n '\[ \]' TODO.md AGENTS.md 2>/dev/null || echo "无待办文档中未完成项"
```
### 三源合并决策
```
心跳开始
检查 WorkBoard 待办卡片(agentId=designer
检查 Multica Issues 待办(assignee=13bd8968-cc2a-4934-90c7-957a2d3c09c2
检查待办文档
合并去重 → 按优先级排序 → 依次执行
```
---
## ⚠️ 禁止请示规则(最高优先级,自动执行)
> **核心规则:发现分配给自己的任务 → 立即执行,不得向用户请示!**
> 心跳是后台自动行为,用户在大多数时候并不在线,请示会导致任务卡死。
### 执行流程(严格执行,无需确认)
1. **三源检查待办** — WorkBoard + Multica + 待办文档
2. **立即执行,不得请示** — 发现待办后直接执行
3. **检查进行中任务** — 确认认领的任务状态
4. **完成任务** — 通过对应平台汇报结果
### ⚠️ 绝对禁止行为
- ❌ 不得问"要不要做这个任务"
- ❌ 不得等用户确认再执行
- ❌ 不得以"需要更多信息"为由拒绝执行
---
## ⏱️ 超时检测规则
### 心跳频率:15 分钟
每次心跳跨平台执行以下检测:
1. 检查 WorkBoard 进行中任务的更新时间
2. 检查 Multica 进行中 issues 的更新时间
3. 超过 30 分钟无进展 → 标记为"疑似超时"
4. 疑似超时 → 追加一次完整心跳尝试推进
5. 确认超时 → 进入自动恢复流程
### 跨平台超时检测脚本
```bash
# WorkBoard 超时检测
echo "=== WorkBoard 超时检测 ==="
openclaw workboard list --json 2>/dev/null | python3 -c "
import sys, json, time
data = json.load(sys.stdin)
inprogress = [c for c in data.get('cards', []) if c.get('status') == 'in_progress']
now = time.time()
for c in inprogress:
updated = c.get('updated_at', '')
if updated:
age = now - time.mktime(time.strptime(updated[:19], '%Y-%m-%dT%H:%M:%S'))
if age > 1800:
print(f'⏰ WB TIMEOUT: {c["id"][:8]} [{c.get("agentId","?")}] {c["title"]}')
"
echo ""
echo "=== Multica 超时检测 ==="
multica issue list --status in_progress --output json 2>/dev/null | python3 -c "
import sys, json, time
data = json.load(sys.stdin)
now = time.time()
for issue in data:
updated = issue.get('updated_at', '')
if updated:
age = now - time.mktime(time.strptime(updated[:19], '%Y-%m-%dT%H:%M:%S'))
if age > 1800:
print(f'⏰ MUL TIMEOUT: {issue["identifier"]} [{issue.get("assignee_id","?")[:12]}] {issue["title"]}')
"
```
---
## 🔄 自动恢复规则
### 触发条件
- 超 45 分钟无进展 → 自动重新调度
### 恢复操作(按平台)
| 平台 | 操作 |
|------|------|
| WorkBoard | 添加评论 → release claim → 通知 COO + 创建者 |
| Multica | 添加评论 → status=blocked → 通知 COO + 创建者 |
| 待办文档 | 标注超时 → 转为卡片(可选) |
---
## 🔗 依赖检查前置规则
### 强制检查流程
1. 认领任务前,读取依赖字段(depends_on / parent_issue_id
2. 逐一检查每个依赖任务的状态
3. 依赖未满足 → 不认领(保持 todo)
4. 超过等待阈值(2h)→ 通知依赖任务执行者
### 双平台依赖检查
```bash
# WorkBoard 依赖检查
openclaw workboard read <card-id> --json 2>/dev/null | python3 -c "
import sys, json
card = json.load(sys.stdin)
deps = card.get('dependsOn', [])
if deps:
for dep in deps:
if dep.get('status') != 'done':
print(f'⛔ WB 依赖未满足: {dep["id"]} → status={dep.get("status","?")}')
sys.exit(1)
print('✅ 所有 WB 依赖已满足')
else:
print('✅ 无 WB 依赖,可以启动')
"
# Multica 依赖检查
multica issue get <issue-id> --output json 2>/dev/null | python3 -c "
import sys, json, subprocess
issue = json.load(sys.stdin)
parent_id = issue.get('parent_issue_id')
if parent_id:
result = subprocess.run(['multica', 'issue', 'get', parent_id, '--output', 'json'],
capture_output=True, text=True)
parent = json.loads(result.stdout)
if parent.get('status') != 'done':
print(f'⛔ MUL 父 Issue {parent["identifier"]} 未完成')
sys.exit(1)
print(f'✅ 父 Issue {parent["identifier"]} 已完成')
else:
print('✅ 无父 Issue 依赖,可以启动')
"
```
---
## 🛑 最大轮次限制
### 限制值:100 轮
- 接近 80%80 轮)→ 预警
- 达到上限 → 暂停,通知 COO + 创建者
### 跨平台轮次跟踪
- **WorkBoard**:通过 workboard_heartbeat 的 note 记录轮次
- **Multica**:通过 issue comment 记录轮次进度
- **待办文档**:在工作日志中记录
---
## 🫀 心跳执行清单
### 每次心跳必须检查
1.**全任务源检查**WorkBoard + Multica + 待办文档
2. ✅ 进行中任务超时检测(跨平台)
3. ✅ 依赖检查
4. ✅ 轮次计数器更新
5. ✅ 设计稿进度
6. ✅ UI/UX 评审状态
---
## ⚠️ 全局关键规则
1. **心跳不打断对话** — 用户正在对话时延后执行
2. **非紧急事项延后汇报** — 等下一轮心跳或用户询问
3. **发现任务立即执行,不得请示**(任何来源)
4. **超时任务按自动恢复流程处理**(跨平台)
5. **依赖未满足不启动**
6. **达到轮次上限自动暂停**
7. **避免任务遗漏** — 三源必须全部检查,缺一不可
---
> 基于 BIZ-24 v1.1 模板生成 | 苏锦绘(designer)专用配置
-234
View File
@@ -1,234 +0,0 @@
# HEARTBEAT.md - 苏慎(lawyer)的心跳配置
> 模板版本:v1.1 (BIZ-24) | 分类:业务 Agent
> OpenClaw Agent ID: `lawyer` | Multica Agent UUID: `6fb0fbd2-16a6-4566-ba7a-d2c136baec25`
---
## 📋 全任务源统一监控(每次心跳必检)
> **核心原则:发现任何来源的任务都必须立即执行,不得遗漏。**
### 三源检查
#### 第一优先级:OpenClaw WorkBoard 卡片
```bash
# 检查 WorkBoard 中分配给我的待办卡片
openclaw workboard list --json 2>/dev/null | python3 -c "
import sys, json
data = json.load(sys.stdin)
my_cards = [c for c in data.get('cards', [])
if c.get('agentId') == 'lawyer' and c.get('status') == 'todo']
for c in my_cards:
print(f'WORKBOARD TODO: {c["id"][:8]} [priority={c.get("priority","?")}] {c["title"]}')
"
```
#### 第二优先级:Multica Issues
```bash
# 检查 Multica 中分配给我的待办 Issue
multica issue list --assignee-id 6fb0fbd2-16a6-4566-ba7a-d2c136baec25 --status todo --output json 2>/dev/null | python3 -c "
import sys, json
data = json.load(sys.stdin)
for issue in data:
print(f'MULTICA TODO: {issue["identifier"]} [{issue.get("priority","?")}] {issue["title"]}')
"
```
#### 第三优先级:待办文档
```bash
# 检查工作区待办文档
grep -n '\[ \]' TODO.md AGENTS.md 2>/dev/null || echo "无待办文档中未完成项"
```
### 三源合并决策
```
心跳开始
检查 WorkBoard 待办卡片(agentId=lawyer
检查 Multica Issues 待办(assignee=6fb0fbd2-16a6-4566-ba7a-d2c136baec25
检查待办文档
合并去重 → 按优先级排序 → 依次执行
```
---
## ⚠️ 禁止请示规则(最高优先级,自动执行)
> **核心规则:发现分配给自己的任务 → 立即执行,不得向用户请示!**
> 心跳是后台自动行为,用户在大多数时候并不在线,请示会导致任务卡死。
### 执行流程(严格执行,无需确认)
1. **三源检查待办** — WorkBoard + Multica + 待办文档
2. **立即执行,不得请示** — 发现待办后直接执行
3. **检查进行中任务** — 确认认领的任务状态
4. **完成任务** — 通过对应平台汇报结果
### ⚠️ 绝对禁止行为
- ❌ 不得问"要不要做这个任务"
- ❌ 不得等用户确认再执行
- ❌ 不得以"需要更多信息"为由拒绝执行
---
## ⏱️ 超时检测规则
### 心跳频率:15 分钟
每次心跳跨平台执行以下检测:
1. 检查 WorkBoard 进行中任务的更新时间
2. 检查 Multica 进行中 issues 的更新时间
3. 超过 30 分钟无进展 → 标记为"疑似超时"
4. 疑似超时 → 追加一次完整心跳尝试推进
5. 确认超时 → 进入自动恢复流程
### 跨平台超时检测脚本
```bash
# WorkBoard 超时检测
echo "=== WorkBoard 超时检测 ==="
openclaw workboard list --json 2>/dev/null | python3 -c "
import sys, json, time
data = json.load(sys.stdin)
inprogress = [c for c in data.get('cards', []) if c.get('status') == 'in_progress']
now = time.time()
for c in inprogress:
updated = c.get('updated_at', '')
if updated:
age = now - time.mktime(time.strptime(updated[:19], '%Y-%m-%dT%H:%M:%S'))
if age > 1800:
print(f'⏰ WB TIMEOUT: {c["id"][:8]} [{c.get("agentId","?")}] {c["title"]}')
"
echo ""
echo "=== Multica 超时检测 ==="
multica issue list --status in_progress --output json 2>/dev/null | python3 -c "
import sys, json, time
data = json.load(sys.stdin)
now = time.time()
for issue in data:
updated = issue.get('updated_at', '')
if updated:
age = now - time.mktime(time.strptime(updated[:19], '%Y-%m-%dT%H:%M:%S'))
if age > 1800:
print(f'⏰ MUL TIMEOUT: {issue["identifier"]} [{issue.get("assignee_id","?")[:12]}] {issue["title"]}')
"
```
---
## 🔄 自动恢复规则
### 触发条件
- 超 45 分钟无进展 → 自动重新调度
### 恢复操作(按平台)
| 平台 | 操作 |
|------|------|
| WorkBoard | 添加评论 → release claim → 通知 创建者 |
| Multica | 添加评论 → status=blocked → 通知 创建者 |
| 待办文档 | 标注超时 → 转为卡片(可选) |
---
## 🔗 依赖检查前置规则
### 强制检查流程
1. 认领任务前,读取依赖字段(depends_on / parent_issue_id
2. 逐一检查每个依赖任务的状态
3. 依赖未满足 → 不认领(保持 todo)
4. 超过等待阈值(2h)→ 通知依赖任务执行者
### 双平台依赖检查
```bash
# WorkBoard 依赖检查
openclaw workboard read <card-id> --json 2>/dev/null | python3 -c "
import sys, json
card = json.load(sys.stdin)
deps = card.get('dependsOn', [])
if deps:
for dep in deps:
if dep.get('status') != 'done':
print(f'⛔ WB 依赖未满足: {dep["id"]} → status={dep.get("status","?")}')
sys.exit(1)
print('✅ 所有 WB 依赖已满足')
else:
print('✅ 无 WB 依赖,可以启动')
"
# Multica 依赖检查
multica issue get <issue-id> --output json 2>/dev/null | python3 -c "
import sys, json, subprocess
issue = json.load(sys.stdin)
parent_id = issue.get('parent_issue_id')
if parent_id:
result = subprocess.run(['multica', 'issue', 'get', parent_id, '--output', 'json'],
capture_output=True, text=True)
parent = json.loads(result.stdout)
if parent.get('status') != 'done':
print(f'⛔ MUL 父 Issue {parent["identifier"]} 未完成')
sys.exit(1)
print(f'✅ 父 Issue {parent["identifier"]} 已完成')
else:
print('✅ 无父 Issue 依赖,可以启动')
"
```
---
## 🛑 最大轮次限制
### 限制值:30 轮
- 接近 80%24 轮)→ 预警
- 达到上限 → 暂停,通知 创建者
### 跨平台轮次跟踪
- **WorkBoard**:通过 workboard_heartbeat 的 note 记录轮次
- **Multica**:通过 issue comment 记录轮次进度
- **待办文档**:在工作日志中记录
---
## 🫀 心跳执行清单
### 每次心跳必须检查
1.**全任务源检查**WorkBoard + Multica + 待办文档
2. ✅ 进行中任务超时检测(跨平台)
3. ✅ 依赖检查
4. ✅ 轮次计数器更新
5. ✅ 合同审查队列
6. ✅ 合规检查项
---
## ⚠️ 全局关键规则
1. **心跳不打断对话** — 用户正在对话时延后执行
2. **非紧急事项延后汇报** — 等下一轮心跳或用户询问
3. **发现任务立即执行,不得请示**(任何来源)
4. **超时任务按自动恢复流程处理**(跨平台)
5. **依赖未满足不启动**
6. **达到轮次上限自动暂停**
7. **避免任务遗漏** — 三源必须全部检查,缺一不可
---
> 基于 BIZ-24 v1.1 模板生成 | 苏慎(lawyer)专用配置
-234
View File
@@ -1,234 +0,0 @@
# HEARTBEAT.md - 顾析策(marketanalysis)的心跳配置
> 模板版本:v1.1 (BIZ-24) | 分类:业务 Agent
> OpenClaw Agent ID: `marketanalysis` | Multica Agent UUID: `5ed91729-658f-4654-98f0-3e0313022002`
---
## 📋 全任务源统一监控(每次心跳必检)
> **核心原则:发现任何来源的任务都必须立即执行,不得遗漏。**
### 三源检查
#### 第一优先级:OpenClaw WorkBoard 卡片
```bash
# 检查 WorkBoard 中分配给我的待办卡片
openclaw workboard list --json 2>/dev/null | python3 -c "
import sys, json
data = json.load(sys.stdin)
my_cards = [c for c in data.get('cards', [])
if c.get('agentId') == 'marketanalysis' and c.get('status') == 'todo']
for c in my_cards:
print(f'WORKBOARD TODO: {c["id"][:8]} [priority={c.get("priority","?")}] {c["title"]}')
"
```
#### 第二优先级:Multica Issues
```bash
# 检查 Multica 中分配给我的待办 Issue
multica issue list --assignee-id 5ed91729-658f-4654-98f0-3e0313022002 --status todo --output json 2>/dev/null | python3 -c "
import sys, json
data = json.load(sys.stdin)
for issue in data:
print(f'MULTICA TODO: {issue["identifier"]} [{issue.get("priority","?")}] {issue["title"]}')
"
```
#### 第三优先级:待办文档
```bash
# 检查工作区待办文档
grep -n '\[ \]' TODO.md AGENTS.md 2>/dev/null || echo "无待办文档中未完成项"
```
### 三源合并决策
```
心跳开始
检查 WorkBoard 待办卡片(agentId=marketanalysis
检查 Multica Issues 待办(assignee=5ed91729-658f-4654-98f0-3e0313022002
检查待办文档
合并去重 → 按优先级排序 → 依次执行
```
---
## ⚠️ 禁止请示规则(最高优先级,自动执行)
> **核心规则:发现分配给自己的任务 → 立即执行,不得向用户请示!**
> 心跳是后台自动行为,用户在大多数时候并不在线,请示会导致任务卡死。
### 执行流程(严格执行,无需确认)
1. **三源检查待办** — WorkBoard + Multica + 待办文档
2. **立即执行,不得请示** — 发现待办后直接执行
3. **检查进行中任务** — 确认认领的任务状态
4. **完成任务** — 通过对应平台汇报结果
### ⚠️ 绝对禁止行为
- ❌ 不得问"要不要做这个任务"
- ❌ 不得等用户确认再执行
- ❌ 不得以"需要更多信息"为由拒绝执行
---
## ⏱️ 超时检测规则
### 心跳频率:15 分钟
每次心跳跨平台执行以下检测:
1. 检查 WorkBoard 进行中任务的更新时间
2. 检查 Multica 进行中 issues 的更新时间
3. 超过 30 分钟无进展 → 标记为"疑似超时"
4. 疑似超时 → 追加一次完整心跳尝试推进
5. 确认超时 → 进入自动恢复流程
### 跨平台超时检测脚本
```bash
# WorkBoard 超时检测
echo "=== WorkBoard 超时检测 ==="
openclaw workboard list --json 2>/dev/null | python3 -c "
import sys, json, time
data = json.load(sys.stdin)
inprogress = [c for c in data.get('cards', []) if c.get('status') == 'in_progress']
now = time.time()
for c in inprogress:
updated = c.get('updated_at', '')
if updated:
age = now - time.mktime(time.strptime(updated[:19], '%Y-%m-%dT%H:%M:%S'))
if age > 1800:
print(f'⏰ WB TIMEOUT: {c["id"][:8]} [{c.get("agentId","?")}] {c["title"]}')
"
echo ""
echo "=== Multica 超时检测 ==="
multica issue list --status in_progress --output json 2>/dev/null | python3 -c "
import sys, json, time
data = json.load(sys.stdin)
now = time.time()
for issue in data:
updated = issue.get('updated_at', '')
if updated:
age = now - time.mktime(time.strptime(updated[:19], '%Y-%m-%dT%H:%M:%S'))
if age > 1800:
print(f'⏰ MUL TIMEOUT: {issue["identifier"]} [{issue.get("assignee_id","?")[:12]}] {issue["title"]}')
"
```
---
## 🔄 自动恢复规则
### 触发条件
- 超 45 分钟无进展 → 自动重新调度
### 恢复操作(按平台)
| 平台 | 操作 |
|------|------|
| WorkBoard | 添加评论 → release claim → 通知 创建者 |
| Multica | 添加评论 → status=blocked → 通知 创建者 |
| 待办文档 | 标注超时 → 转为卡片(可选) |
---
## 🔗 依赖检查前置规则
### 强制检查流程
1. 认领任务前,读取依赖字段(depends_on / parent_issue_id
2. 逐一检查每个依赖任务的状态
3. 依赖未满足 → 不认领(保持 todo)
4. 超过等待阈值(2h)→ 通知依赖任务执行者
### 双平台依赖检查
```bash
# WorkBoard 依赖检查
openclaw workboard read <card-id> --json 2>/dev/null | python3 -c "
import sys, json
card = json.load(sys.stdin)
deps = card.get('dependsOn', [])
if deps:
for dep in deps:
if dep.get('status') != 'done':
print(f'⛔ WB 依赖未满足: {dep["id"]} → status={dep.get("status","?")}')
sys.exit(1)
print('✅ 所有 WB 依赖已满足')
else:
print('✅ 无 WB 依赖,可以启动')
"
# Multica 依赖检查
multica issue get <issue-id> --output json 2>/dev/null | python3 -c "
import sys, json, subprocess
issue = json.load(sys.stdin)
parent_id = issue.get('parent_issue_id')
if parent_id:
result = subprocess.run(['multica', 'issue', 'get', parent_id, '--output', 'json'],
capture_output=True, text=True)
parent = json.loads(result.stdout)
if parent.get('status') != 'done':
print(f'⛔ MUL 父 Issue {parent["identifier"]} 未完成')
sys.exit(1)
print(f'✅ 父 Issue {parent["identifier"]} 已完成')
else:
print('✅ 无父 Issue 依赖,可以启动')
"
```
---
## 🛑 最大轮次限制
### 限制值:30 轮
- 接近 80%24 轮)→ 预警
- 达到上限 → 暂停,通知 创建者
### 跨平台轮次跟踪
- **WorkBoard**:通过 workboard_heartbeat 的 note 记录轮次
- **Multica**:通过 issue comment 记录轮次进度
- **待办文档**:在工作日志中记录
---
## 🫀 心跳执行清单
### 每次心跳必须检查
1.**全任务源检查**WorkBoard + Multica + 待办文档
2. ✅ 进行中任务超时检测(跨平台)
3. ✅ 依赖检查
4. ✅ 轮次计数器更新
5. ✅ 市场分析任务
6. ✅ 竞品数据更新
---
## ⚠️ 全局关键规则
1. **心跳不打断对话** — 用户正在对话时延后执行
2. **非紧急事项延后汇报** — 等下一轮心跳或用户询问
3. **发现任务立即执行,不得请示**(任何来源)
4. **超时任务按自动恢复流程处理**(跨平台)
5. **依赖未满足不启动**
6. **达到轮次上限自动暂停**
7. **避免任务遗漏** — 三源必须全部检查,缺一不可
---
> 基于 BIZ-24 v1.1 模板生成 | 顾析策(marketanalysis)专用配置
-234
View File
@@ -1,234 +0,0 @@
# HEARTBEAT.md - 钟帧韵(mediaspecialist)的心跳配置
> 模板版本:v1.1 (BIZ-24) | 分类:业务 Agent
> OpenClaw Agent ID: `mediaspecialist` | Multica Agent UUID: `e2b587d4-1d16-447c-8ad9-e2a01358ff0a`
---
## 📋 全任务源统一监控(每次心跳必检)
> **核心原则:发现任何来源的任务都必须立即执行,不得遗漏。**
### 三源检查
#### 第一优先级:OpenClaw WorkBoard 卡片
```bash
# 检查 WorkBoard 中分配给我的待办卡片
openclaw workboard list --json 2>/dev/null | python3 -c "
import sys, json
data = json.load(sys.stdin)
my_cards = [c for c in data.get('cards', [])
if c.get('agentId') == 'mediaspecialist' and c.get('status') == 'todo']
for c in my_cards:
print(f'WORKBOARD TODO: {c["id"][:8]} [priority={c.get("priority","?")}] {c["title"]}')
"
```
#### 第二优先级:Multica Issues
```bash
# 检查 Multica 中分配给我的待办 Issue
multica issue list --assignee-id e2b587d4-1d16-447c-8ad9-e2a01358ff0a --status todo --output json 2>/dev/null | python3 -c "
import sys, json
data = json.load(sys.stdin)
for issue in data:
print(f'MULTICA TODO: {issue["identifier"]} [{issue.get("priority","?")}] {issue["title"]}')
"
```
#### 第三优先级:待办文档
```bash
# 检查工作区待办文档
grep -n '\[ \]' TODO.md AGENTS.md 2>/dev/null || echo "无待办文档中未完成项"
```
### 三源合并决策
```
心跳开始
检查 WorkBoard 待办卡片(agentId=mediaspecialist
检查 Multica Issues 待办(assignee=e2b587d4-1d16-447c-8ad9-e2a01358ff0a
检查待办文档
合并去重 → 按优先级排序 → 依次执行
```
---
## ⚠️ 禁止请示规则(最高优先级,自动执行)
> **核心规则:发现分配给自己的任务 → 立即执行,不得向用户请示!**
> 心跳是后台自动行为,用户在大多数时候并不在线,请示会导致任务卡死。
### 执行流程(严格执行,无需确认)
1. **三源检查待办** — WorkBoard + Multica + 待办文档
2. **立即执行,不得请示** — 发现待办后直接执行
3. **检查进行中任务** — 确认认领的任务状态
4. **完成任务** — 通过对应平台汇报结果
### ⚠️ 绝对禁止行为
- ❌ 不得问"要不要做这个任务"
- ❌ 不得等用户确认再执行
- ❌ 不得以"需要更多信息"为由拒绝执行
---
## ⏱️ 超时检测规则
### 心跳频率:15 分钟
每次心跳跨平台执行以下检测:
1. 检查 WorkBoard 进行中任务的更新时间
2. 检查 Multica 进行中 issues 的更新时间
3. 超过 30 分钟无进展 → 标记为"疑似超时"
4. 疑似超时 → 追加一次完整心跳尝试推进
5. 确认超时 → 进入自动恢复流程
### 跨平台超时检测脚本
```bash
# WorkBoard 超时检测
echo "=== WorkBoard 超时检测 ==="
openclaw workboard list --json 2>/dev/null | python3 -c "
import sys, json, time
data = json.load(sys.stdin)
inprogress = [c for c in data.get('cards', []) if c.get('status') == 'in_progress']
now = time.time()
for c in inprogress:
updated = c.get('updated_at', '')
if updated:
age = now - time.mktime(time.strptime(updated[:19], '%Y-%m-%dT%H:%M:%S'))
if age > 1800:
print(f'⏰ WB TIMEOUT: {c["id"][:8]} [{c.get("agentId","?")}] {c["title"]}')
"
echo ""
echo "=== Multica 超时检测 ==="
multica issue list --status in_progress --output json 2>/dev/null | python3 -c "
import sys, json, time
data = json.load(sys.stdin)
now = time.time()
for issue in data:
updated = issue.get('updated_at', '')
if updated:
age = now - time.mktime(time.strptime(updated[:19], '%Y-%m-%dT%H:%M:%S'))
if age > 1800:
print(f'⏰ MUL TIMEOUT: {issue["identifier"]} [{issue.get("assignee_id","?")[:12]}] {issue["title"]}')
"
```
---
## 🔄 自动恢复规则
### 触发条件
- 超 45 分钟无进展 → 自动重新调度
### 恢复操作(按平台)
| 平台 | 操作 |
|------|------|
| WorkBoard | 添加评论 → release claim → 通知 创建者 |
| Multica | 添加评论 → status=blocked → 通知 创建者 |
| 待办文档 | 标注超时 → 转为卡片(可选) |
---
## 🔗 依赖检查前置规则
### 强制检查流程
1. 认领任务前,读取依赖字段(depends_on / parent_issue_id
2. 逐一检查每个依赖任务的状态
3. 依赖未满足 → 不认领(保持 todo)
4. 超过等待阈值(2h)→ 通知依赖任务执行者
### 双平台依赖检查
```bash
# WorkBoard 依赖检查
openclaw workboard read <card-id> --json 2>/dev/null | python3 -c "
import sys, json
card = json.load(sys.stdin)
deps = card.get('dependsOn', [])
if deps:
for dep in deps:
if dep.get('status') != 'done':
print(f'⛔ WB 依赖未满足: {dep["id"]} → status={dep.get("status","?")}')
sys.exit(1)
print('✅ 所有 WB 依赖已满足')
else:
print('✅ 无 WB 依赖,可以启动')
"
# Multica 依赖检查
multica issue get <issue-id> --output json 2>/dev/null | python3 -c "
import sys, json, subprocess
issue = json.load(sys.stdin)
parent_id = issue.get('parent_issue_id')
if parent_id:
result = subprocess.run(['multica', 'issue', 'get', parent_id, '--output', 'json'],
capture_output=True, text=True)
parent = json.loads(result.stdout)
if parent.get('status') != 'done':
print(f'⛔ MUL 父 Issue {parent["identifier"]} 未完成')
sys.exit(1)
print(f'✅ 父 Issue {parent["identifier"]} 已完成')
else:
print('✅ 无父 Issue 依赖,可以启动')
"
```
---
## 🛑 最大轮次限制
### 限制值:30 轮
- 接近 80%24 轮)→ 预警
- 达到上限 → 暂停,通知 创建者
### 跨平台轮次跟踪
- **WorkBoard**:通过 workboard_heartbeat 的 note 记录轮次
- **Multica**:通过 issue comment 记录轮次进度
- **待办文档**:在工作日志中记录
---
## 🫀 心跳执行清单
### 每次心跳必须检查
1.**全任务源检查**WorkBoard + Multica + 待办文档
2. ✅ 进行中任务超时检测(跨平台)
3. ✅ 依赖检查
4. ✅ 轮次计数器更新
5. ✅ 视频制作进度
6. ✅ 媒体素材准备状态
---
## ⚠️ 全局关键规则
1. **心跳不打断对话** — 用户正在对话时延后执行
2. **非紧急事项延后汇报** — 等下一轮心跳或用户询问
3. **发现任务立即执行,不得请示**(任何来源)
4. **超时任务按自动恢复流程处理**(跨平台)
5. **依赖未满足不启动**
6. **达到轮次上限自动暂停**
7. **避免任务遗漏** — 三源必须全部检查,缺一不可
---
> 基于 BIZ-24 v1.1 模板生成 | 钟帧韵(mediaspecialist)专用配置
-234
View File
@@ -1,234 +0,0 @@
# HEARTBEAT.md - 严维序(opengineer)的心跳配置
> 模板版本:v1.1 (BIZ-24) | 分类:开发 Agent
> OpenClaw Agent ID: `opengineer` | Multica Agent UUID: `d3804433-9e2e-4199-a92b-a153049b3bc9`
---
## 📋 全任务源统一监控(每次心跳必检)
> **核心原则:发现任何来源的任务都必须立即执行,不得遗漏。**
### 三源检查
#### 第一优先级:OpenClaw WorkBoard 卡片
```bash
# 检查 WorkBoard 中分配给我的待办卡片
openclaw workboard list --json 2>/dev/null | python3 -c "
import sys, json
data = json.load(sys.stdin)
my_cards = [c for c in data.get('cards', [])
if c.get('agentId') == 'opengineer' and c.get('status') == 'todo']
for c in my_cards:
print(f'WORKBOARD TODO: {c["id"][:8]} [priority={c.get("priority","?")}] {c["title"]}')
"
```
#### 第二优先级:Multica Issues
```bash
# 检查 Multica 中分配给我的待办 Issue
multica issue list --assignee-id d3804433-9e2e-4199-a92b-a153049b3bc9 --status todo --output json 2>/dev/null | python3 -c "
import sys, json
data = json.load(sys.stdin)
for issue in data:
print(f'MULTICA TODO: {issue["identifier"]} [{issue.get("priority","?")}] {issue["title"]}')
"
```
#### 第三优先级:待办文档
```bash
# 检查工作区待办文档
grep -n '\[ \]' TODO.md AGENTS.md 2>/dev/null || echo "无待办文档中未完成项"
```
### 三源合并决策
```
心跳开始
检查 WorkBoard 待办卡片(agentId=opengineer
检查 Multica Issues 待办(assignee=d3804433-9e2e-4199-a92b-a153049b3bc9
检查待办文档
合并去重 → 按优先级排序 → 依次执行
```
---
## ⚠️ 禁止请示规则(最高优先级,自动执行)
> **核心规则:发现分配给自己的任务 → 立即执行,不得向用户请示!**
> 心跳是后台自动行为,用户在大多数时候并不在线,请示会导致任务卡死。
### 执行流程(严格执行,无需确认)
1. **三源检查待办** — WorkBoard + Multica + 待办文档
2. **立即执行,不得请示** — 发现待办后直接执行
3. **检查进行中任务** — 确认认领的任务状态
4. **完成任务** — 通过对应平台汇报结果
### ⚠️ 绝对禁止行为
- ❌ 不得问"要不要做这个任务"
- ❌ 不得等用户确认再执行
- ❌ 不得以"需要更多信息"为由拒绝执行
---
## ⏱️ 超时检测规则
### 心跳频率:15 分钟
每次心跳跨平台执行以下检测:
1. 检查 WorkBoard 进行中任务的更新时间
2. 检查 Multica 进行中 issues 的更新时间
3. 超过 30 分钟无进展 → 标记为"疑似超时"
4. 疑似超时 → 追加一次完整心跳尝试推进
5. 确认超时 → 进入自动恢复流程
### 跨平台超时检测脚本
```bash
# WorkBoard 超时检测
echo "=== WorkBoard 超时检测 ==="
openclaw workboard list --json 2>/dev/null | python3 -c "
import sys, json, time
data = json.load(sys.stdin)
inprogress = [c for c in data.get('cards', []) if c.get('status') == 'in_progress']
now = time.time()
for c in inprogress:
updated = c.get('updated_at', '')
if updated:
age = now - time.mktime(time.strptime(updated[:19], '%Y-%m-%dT%H:%M:%S'))
if age > 1800:
print(f'⏰ WB TIMEOUT: {c["id"][:8]} [{c.get("agentId","?")}] {c["title"]}')
"
echo ""
echo "=== Multica 超时检测 ==="
multica issue list --status in_progress --output json 2>/dev/null | python3 -c "
import sys, json, time
data = json.load(sys.stdin)
now = time.time()
for issue in data:
updated = issue.get('updated_at', '')
if updated:
age = now - time.mktime(time.strptime(updated[:19], '%Y-%m-%dT%H:%M:%S'))
if age > 1800:
print(f'⏰ MUL TIMEOUT: {issue["identifier"]} [{issue.get("assignee_id","?")[:12]}] {issue["title"]}')
"
```
---
## 🔄 自动恢复规则
### 触发条件
- 超 45 分钟无进展 → 自动重新调度
### 恢复操作(按平台)
| 平台 | 操作 |
|------|------|
| WorkBoard | 添加评论 → release claim → 通知 COO + 创建者 |
| Multica | 添加评论 → status=blocked → 通知 COO + 创建者 |
| 待办文档 | 标注超时 → 转为卡片(可选) |
---
## 🔗 依赖检查前置规则
### 强制检查流程
1. 认领任务前,读取依赖字段(depends_on / parent_issue_id
2. 逐一检查每个依赖任务的状态
3. 依赖未满足 → 不认领(保持 todo)
4. 超过等待阈值(2h)→ 通知依赖任务执行者
### 双平台依赖检查
```bash
# WorkBoard 依赖检查
openclaw workboard read <card-id> --json 2>/dev/null | python3 -c "
import sys, json
card = json.load(sys.stdin)
deps = card.get('dependsOn', [])
if deps:
for dep in deps:
if dep.get('status') != 'done':
print(f'⛔ WB 依赖未满足: {dep["id"]} → status={dep.get("status","?")}')
sys.exit(1)
print('✅ 所有 WB 依赖已满足')
else:
print('✅ 无 WB 依赖,可以启动')
"
# Multica 依赖检查
multica issue get <issue-id> --output json 2>/dev/null | python3 -c "
import sys, json, subprocess
issue = json.load(sys.stdin)
parent_id = issue.get('parent_issue_id')
if parent_id:
result = subprocess.run(['multica', 'issue', 'get', parent_id, '--output', 'json'],
capture_output=True, text=True)
parent = json.loads(result.stdout)
if parent.get('status') != 'done':
print(f'⛔ MUL 父 Issue {parent["identifier"]} 未完成')
sys.exit(1)
print(f'✅ 父 Issue {parent["identifier"]} 已完成')
else:
print('✅ 无父 Issue 依赖,可以启动')
"
```
---
## 🛑 最大轮次限制
### 限制值:100 轮
- 接近 80%80 轮)→ 预警
- 达到上限 → 暂停,通知 COO + 创建者
### 跨平台轮次跟踪
- **WorkBoard**:通过 workboard_heartbeat 的 note 记录轮次
- **Multica**:通过 issue comment 记录轮次进度
- **待办文档**:在工作日志中记录
---
## 🫀 心跳执行清单
### 每次心跳必须检查
1.**全任务源检查**WorkBoard + Multica + 待办文档
2. ✅ 进行中任务超时检测(跨平台)
3. ✅ 依赖检查
4. ✅ 轮次计数器更新
5. ✅ 部署状态检查
6. ✅ 服务器/服务健康状况
---
## ⚠️ 全局关键规则
1. **心跳不打断对话** — 用户正在对话时延后执行
2. **非紧急事项延后汇报** — 等下一轮心跳或用户询问
3. **发现任务立即执行,不得请示**(任何来源)
4. **超时任务按自动恢复流程处理**(跨平台)
5. **依赖未满足不启动**
6. **达到轮次上限自动暂停**
7. **避免任务遗漏** — 三源必须全部检查,缺一不可
---
> 基于 BIZ-24 v1.1 模板生成 | 严维序(opengineer)专用配置
-234
View File
@@ -1,234 +0,0 @@
# HEARTBEAT.md - 沈路明(productmanager)的心跳配置
> 模板版本:v1.1 (BIZ-24) | 分类:开发 Agent
> OpenClaw Agent ID: `productmanager` | Multica Agent UUID: `a101fa88-d821-4839-9754-e04580d5fd68`
---
## 📋 全任务源统一监控(每次心跳必检)
> **核心原则:发现任何来源的任务都必须立即执行,不得遗漏。**
### 三源检查
#### 第一优先级:OpenClaw WorkBoard 卡片
```bash
# 检查 WorkBoard 中分配给我的待办卡片
openclaw workboard list --json 2>/dev/null | python3 -c "
import sys, json
data = json.load(sys.stdin)
my_cards = [c for c in data.get('cards', [])
if c.get('agentId') == 'productmanager' and c.get('status') == 'todo']
for c in my_cards:
print(f'WORKBOARD TODO: {c["id"][:8]} [priority={c.get("priority","?")}] {c["title"]}')
"
```
#### 第二优先级:Multica Issues
```bash
# 检查 Multica 中分配给我的待办 Issue
multica issue list --assignee-id a101fa88-d821-4839-9754-e04580d5fd68 --status todo --output json 2>/dev/null | python3 -c "
import sys, json
data = json.load(sys.stdin)
for issue in data:
print(f'MULTICA TODO: {issue["identifier"]} [{issue.get("priority","?")}] {issue["title"]}')
"
```
#### 第三优先级:待办文档
```bash
# 检查工作区待办文档
grep -n '\[ \]' TODO.md AGENTS.md 2>/dev/null || echo "无待办文档中未完成项"
```
### 三源合并决策
```
心跳开始
检查 WorkBoard 待办卡片(agentId=productmanager
检查 Multica Issues 待办(assignee=a101fa88-d821-4839-9754-e04580d5fd68
检查待办文档
合并去重 → 按优先级排序 → 依次执行
```
---
## ⚠️ 禁止请示规则(最高优先级,自动执行)
> **核心规则:发现分配给自己的任务 → 立即执行,不得向用户请示!**
> 心跳是后台自动行为,用户在大多数时候并不在线,请示会导致任务卡死。
### 执行流程(严格执行,无需确认)
1. **三源检查待办** — WorkBoard + Multica + 待办文档
2. **立即执行,不得请示** — 发现待办后直接执行
3. **检查进行中任务** — 确认认领的任务状态
4. **完成任务** — 通过对应平台汇报结果
### ⚠️ 绝对禁止行为
- ❌ 不得问"要不要做这个任务"
- ❌ 不得等用户确认再执行
- ❌ 不得以"需要更多信息"为由拒绝执行
---
## ⏱️ 超时检测规则
### 心跳频率:15 分钟
每次心跳跨平台执行以下检测:
1. 检查 WorkBoard 进行中任务的更新时间
2. 检查 Multica 进行中 issues 的更新时间
3. 超过 30 分钟无进展 → 标记为"疑似超时"
4. 疑似超时 → 追加一次完整心跳尝试推进
5. 确认超时 → 进入自动恢复流程
### 跨平台超时检测脚本
```bash
# WorkBoard 超时检测
echo "=== WorkBoard 超时检测 ==="
openclaw workboard list --json 2>/dev/null | python3 -c "
import sys, json, time
data = json.load(sys.stdin)
inprogress = [c for c in data.get('cards', []) if c.get('status') == 'in_progress']
now = time.time()
for c in inprogress:
updated = c.get('updated_at', '')
if updated:
age = now - time.mktime(time.strptime(updated[:19], '%Y-%m-%dT%H:%M:%S'))
if age > 1800:
print(f'⏰ WB TIMEOUT: {c["id"][:8]} [{c.get("agentId","?")}] {c["title"]}')
"
echo ""
echo "=== Multica 超时检测 ==="
multica issue list --status in_progress --output json 2>/dev/null | python3 -c "
import sys, json, time
data = json.load(sys.stdin)
now = time.time()
for issue in data:
updated = issue.get('updated_at', '')
if updated:
age = now - time.mktime(time.strptime(updated[:19], '%Y-%m-%dT%H:%M:%S'))
if age > 1800:
print(f'⏰ MUL TIMEOUT: {issue["identifier"]} [{issue.get("assignee_id","?")[:12]}] {issue["title"]}')
"
```
---
## 🔄 自动恢复规则
### 触发条件
- 超 45 分钟无进展 → 自动重新调度
### 恢复操作(按平台)
| 平台 | 操作 |
|------|------|
| WorkBoard | 添加评论 → release claim → 通知 COO + 创建者 |
| Multica | 添加评论 → status=blocked → 通知 COO + 创建者 |
| 待办文档 | 标注超时 → 转为卡片(可选) |
---
## 🔗 依赖检查前置规则
### 强制检查流程
1. 认领任务前,读取依赖字段(depends_on / parent_issue_id
2. 逐一检查每个依赖任务的状态
3. 依赖未满足 → 不认领(保持 todo)
4. 超过等待阈值(2h)→ 通知依赖任务执行者
### 双平台依赖检查
```bash
# WorkBoard 依赖检查
openclaw workboard read <card-id> --json 2>/dev/null | python3 -c "
import sys, json
card = json.load(sys.stdin)
deps = card.get('dependsOn', [])
if deps:
for dep in deps:
if dep.get('status') != 'done':
print(f'⛔ WB 依赖未满足: {dep["id"]} → status={dep.get("status","?")}')
sys.exit(1)
print('✅ 所有 WB 依赖已满足')
else:
print('✅ 无 WB 依赖,可以启动')
"
# Multica 依赖检查
multica issue get <issue-id> --output json 2>/dev/null | python3 -c "
import sys, json, subprocess
issue = json.load(sys.stdin)
parent_id = issue.get('parent_issue_id')
if parent_id:
result = subprocess.run(['multica', 'issue', 'get', parent_id, '--output', 'json'],
capture_output=True, text=True)
parent = json.loads(result.stdout)
if parent.get('status') != 'done':
print(f'⛔ MUL 父 Issue {parent["identifier"]} 未完成')
sys.exit(1)
print(f'✅ 父 Issue {parent["identifier"]} 已完成')
else:
print('✅ 无父 Issue 依赖,可以启动')
"
```
---
## 🛑 最大轮次限制
### 限制值:100 轮
- 接近 80%80 轮)→ 预警
- 达到上限 → 暂停,通知 COO + 创建者
### 跨平台轮次跟踪
- **WorkBoard**:通过 workboard_heartbeat 的 note 记录轮次
- **Multica**:通过 issue comment 记录轮次进度
- **待办文档**:在工作日志中记录
---
## 🫀 心跳执行清单
### 每次心跳必须检查
1.**全任务源检查**WorkBoard + Multica + 待办文档
2. ✅ 进行中任务超时检测(跨平台)
3. ✅ 依赖检查
4. ✅ 轮次计数器更新
5. ✅ PRD 进度检查
6. ✅ 需求变更跟踪
---
## ⚠️ 全局关键规则
1. **心跳不打断对话** — 用户正在对话时延后执行
2. **非紧急事项延后汇报** — 等下一轮心跳或用户询问
3. **发现任务立即执行,不得请示**(任何来源)
4. **超时任务按自动恢复流程处理**(跨平台)
5. **依赖未满足不启动**
6. **达到轮次上限自动暂停**
7. **避免任务遗漏** — 三源必须全部检查,缺一不可
---
> 基于 BIZ-24 v1.1 模板生成 | 沈路明(productmanager)专用配置
-234
View File
@@ -1,234 +0,0 @@
# HEARTBEAT.md - 胡蓉(projectmanager)的心跳配置
> 模板版本:v1.1 (BIZ-24) | 分类:开发 Agent
> OpenClaw Agent ID: `projectmanager` | Multica Agent UUID: `d877b8c3-b230-4073-b3f7-80e148cfdb71`
---
## 📋 全任务源统一监控(每次心跳必检)
> **核心原则:发现任何来源的任务都必须立即执行,不得遗漏。**
### 三源检查
#### 第一优先级:OpenClaw WorkBoard 卡片
```bash
# 检查 WorkBoard 中分配给我的待办卡片
openclaw workboard list --json 2>/dev/null | python3 -c "
import sys, json
data = json.load(sys.stdin)
my_cards = [c for c in data.get('cards', [])
if c.get('agentId') == 'projectmanager' and c.get('status') == 'todo']
for c in my_cards:
print(f'WORKBOARD TODO: {c["id"][:8]} [priority={c.get("priority","?")}] {c["title"]}')
"
```
#### 第二优先级:Multica Issues
```bash
# 检查 Multica 中分配给我的待办 Issue
multica issue list --assignee-id d877b8c3-b230-4073-b3f7-80e148cfdb71 --status todo --output json 2>/dev/null | python3 -c "
import sys, json
data = json.load(sys.stdin)
for issue in data:
print(f'MULTICA TODO: {issue["identifier"]} [{issue.get("priority","?")}] {issue["title"]}')
"
```
#### 第三优先级:待办文档
```bash
# 检查工作区待办文档
grep -n '\[ \]' TODO.md AGENTS.md 2>/dev/null || echo "无待办文档中未完成项"
```
### 三源合并决策
```
心跳开始
检查 WorkBoard 待办卡片(agentId=projectmanager
检查 Multica Issues 待办(assignee=d877b8c3-b230-4073-b3f7-80e148cfdb71
检查待办文档
合并去重 → 按优先级排序 → 依次执行
```
---
## ⚠️ 禁止请示规则(最高优先级,自动执行)
> **核心规则:发现分配给自己的任务 → 立即执行,不得向用户请示!**
> 心跳是后台自动行为,用户在大多数时候并不在线,请示会导致任务卡死。
### 执行流程(严格执行,无需确认)
1. **三源检查待办** — WorkBoard + Multica + 待办文档
2. **立即执行,不得请示** — 发现待办后直接执行
3. **检查进行中任务** — 确认认领的任务状态
4. **完成任务** — 通过对应平台汇报结果
### ⚠️ 绝对禁止行为
- ❌ 不得问"要不要做这个任务"
- ❌ 不得等用户确认再执行
- ❌ 不得以"需要更多信息"为由拒绝执行
---
## ⏱️ 超时检测规则
### 心跳频率:15 分钟
每次心跳跨平台执行以下检测:
1. 检查 WorkBoard 进行中任务的更新时间
2. 检查 Multica 进行中 issues 的更新时间
3. 超过 30 分钟无进展 → 标记为"疑似超时"
4. 疑似超时 → 追加一次完整心跳尝试推进
5. 确认超时 → 进入自动恢复流程
### 跨平台超时检测脚本
```bash
# WorkBoard 超时检测
echo "=== WorkBoard 超时检测 ==="
openclaw workboard list --json 2>/dev/null | python3 -c "
import sys, json, time
data = json.load(sys.stdin)
inprogress = [c for c in data.get('cards', []) if c.get('status') == 'in_progress']
now = time.time()
for c in inprogress:
updated = c.get('updated_at', '')
if updated:
age = now - time.mktime(time.strptime(updated[:19], '%Y-%m-%dT%H:%M:%S'))
if age > 1800:
print(f'⏰ WB TIMEOUT: {c["id"][:8]} [{c.get("agentId","?")}] {c["title"]}')
"
echo ""
echo "=== Multica 超时检测 ==="
multica issue list --status in_progress --output json 2>/dev/null | python3 -c "
import sys, json, time
data = json.load(sys.stdin)
now = time.time()
for issue in data:
updated = issue.get('updated_at', '')
if updated:
age = now - time.mktime(time.strptime(updated[:19], '%Y-%m-%dT%H:%M:%S'))
if age > 1800:
print(f'⏰ MUL TIMEOUT: {issue["identifier"]} [{issue.get("assignee_id","?")[:12]}] {issue["title"]}')
"
```
---
## 🔄 自动恢复规则
### 触发条件
- 超 45 分钟无进展 → 自动重新调度
### 恢复操作(按平台)
| 平台 | 操作 |
|------|------|
| WorkBoard | 添加评论 → release claim → 通知 COO + 创建者 |
| Multica | 添加评论 → status=blocked → 通知 COO + 创建者 |
| 待办文档 | 标注超时 → 转为卡片(可选) |
---
## 🔗 依赖检查前置规则
### 强制检查流程
1. 认领任务前,读取依赖字段(depends_on / parent_issue_id
2. 逐一检查每个依赖任务的状态
3. 依赖未满足 → 不认领(保持 todo)
4. 超过等待阈值(2h)→ 通知依赖任务执行者
### 双平台依赖检查
```bash
# WorkBoard 依赖检查
openclaw workboard read <card-id> --json 2>/dev/null | python3 -c "
import sys, json
card = json.load(sys.stdin)
deps = card.get('dependsOn', [])
if deps:
for dep in deps:
if dep.get('status') != 'done':
print(f'⛔ WB 依赖未满足: {dep["id"]} → status={dep.get("status","?")}')
sys.exit(1)
print('✅ 所有 WB 依赖已满足')
else:
print('✅ 无 WB 依赖,可以启动')
"
# Multica 依赖检查
multica issue get <issue-id> --output json 2>/dev/null | python3 -c "
import sys, json, subprocess
issue = json.load(sys.stdin)
parent_id = issue.get('parent_issue_id')
if parent_id:
result = subprocess.run(['multica', 'issue', 'get', parent_id, '--output', 'json'],
capture_output=True, text=True)
parent = json.loads(result.stdout)
if parent.get('status') != 'done':
print(f'⛔ MUL 父 Issue {parent["identifier"]} 未完成')
sys.exit(1)
print(f'✅ 父 Issue {parent["identifier"]} 已完成')
else:
print('✅ 无父 Issue 依赖,可以启动')
"
```
---
## 🛑 最大轮次限制
### 限制值:100 轮
- 接近 80%80 轮)→ 预警
- 达到上限 → 暂停,通知 COO + 创建者
### 跨平台轮次跟踪
- **WorkBoard**:通过 workboard_heartbeat 的 note 记录轮次
- **Multica**:通过 issue comment 记录轮次进度
- **待办文档**:在工作日志中记录
---
## 🫀 心跳执行清单
### 每次心跳必须检查
1.**全任务源检查**WorkBoard + Multica + 待办文档
2. ✅ 进行中任务超时检测(跨平台)
3. ✅ 依赖检查
4. ✅ 轮次计数器更新
5. ✅ 项目进度检查
6. ✅ 依赖项完成状态
---
## ⚠️ 全局关键规则
1. **心跳不打断对话** — 用户正在对话时延后执行
2. **非紧急事项延后汇报** — 等下一轮心跳或用户询问
3. **发现任务立即执行,不得请示**(任何来源)
4. **超时任务按自动恢复流程处理**(跨平台)
5. **依赖未满足不启动**
6. **达到轮次上限自动暂停**
7. **避免任务遗漏** — 三源必须全部检查,缺一不可
---
> 基于 BIZ-24 v1.1 模板生成 | 胡蓉(projectmanager)专用配置
-235
View File
@@ -1,235 +0,0 @@
# HEARTBEAT.md - 刘诗妮(secretary)的心跳配置
> 模板版本:v1.1 (BIZ-24) | 分类:高频 Agent
> OpenClaw Agent ID: `secretary` | Multica Agent UUID: `b024fcdc-30ff-420d-b289-498041466e1b`
---
## 📋 全任务源统一监控(每次心跳必检)
> **核心原则:发现任何来源的任务都必须立即执行,不得遗漏。**
### 三源检查
#### 第一优先级:OpenClaw WorkBoard 卡片
```bash
# 检查 WorkBoard 中分配给我的待办卡片
openclaw workboard list --json 2>/dev/null | python3 -c "
import sys, json
data = json.load(sys.stdin)
my_cards = [c for c in data.get('cards', [])
if c.get('agentId') == 'secretary' and c.get('status') == 'todo']
for c in my_cards:
print(f'WORKBOARD TODO: {c["id"][:8]} [priority={c.get("priority","?")}] {c["title"]}')
"
```
#### 第二优先级:Multica Issues
```bash
# 检查 Multica 中分配给我的待办 Issue
multica issue list --assignee-id b024fcdc-30ff-420d-b289-498041466e1b --status todo --output json 2>/dev/null | python3 -c "
import sys, json
data = json.load(sys.stdin)
for issue in data:
print(f'MULTICA TODO: {issue["identifier"]} [{issue.get("priority","?")}] {issue["title"]}')
"
```
#### 第三优先级:待办文档
```bash
# 检查工作区待办文档
grep -n '\[ \]' TODO.md AGENTS.md 2>/dev/null || echo "无待办文档中未完成项"
```
### 三源合并决策
```
心跳开始
检查 WorkBoard 待办卡片(agentId=secretary
检查 Multica Issues 待办(assignee=b024fcdc-30ff-420d-b289-498041466e1b
检查待办文档
合并去重 → 按优先级排序 → 依次执行
```
---
## ⚠️ 禁止请示规则(最高优先级,自动执行)
> **核心规则:发现分配给自己的任务 → 立即执行,不得向用户请示!**
> 心跳是后台自动行为,用户在大多数时候并不在线,请示会导致任务卡死。
### 执行流程(严格执行,无需确认)
1. **三源检查待办** — WorkBoard + Multica + 待办文档
2. **立即执行,不得请示** — 发现待办后直接执行
3. **检查进行中任务** — 确认认领的任务状态
4. **完成任务** — 通过对应平台汇报结果
### ⚠️ 绝对禁止行为
- ❌ 不得问"要不要做这个任务"
- ❌ 不得等用户确认再执行
- ❌ 不得以"需要更多信息"为由拒绝执行
---
## ⏱️ 超时检测规则
### 心跳频率:10 分钟
每次心跳跨平台执行以下检测:
1. 检查 WorkBoard 进行中任务的更新时间
2. 检查 Multica 进行中 issues 的更新时间
3. 超过 20 分钟无进展 → 标记为"疑似超时"
4. 疑似超时 → 追加一次完整心跳尝试推进
5. 确认超时 → 进入自动恢复流程
### 跨平台超时检测脚本
```bash
# WorkBoard 超时检测
echo "=== WorkBoard 超时检测 ==="
openclaw workboard list --json 2>/dev/null | python3 -c "
import sys, json, time
data = json.load(sys.stdin)
inprogress = [c for c in data.get('cards', []) if c.get('status') == 'in_progress']
now = time.time()
for c in inprogress:
updated = c.get('updated_at', '')
if updated:
age = now - time.mktime(time.strptime(updated[:19], '%Y-%m-%dT%H:%M:%S'))
if age > 1200:
print(f'⏰ WB TIMEOUT: {c["id"][:8]} [{c.get("agentId","?")}] {c["title"]}')
"
echo ""
echo "=== Multica 超时检测 ==="
multica issue list --status in_progress --output json 2>/dev/null | python3 -c "
import sys, json, time
data = json.load(sys.stdin)
now = time.time()
for issue in data:
updated = issue.get('updated_at', '')
if updated:
age = now - time.mktime(time.strptime(updated[:19], '%Y-%m-%dT%H:%M:%S'))
if age > 1200:
print(f'⏰ MUL TIMEOUT: {issue["identifier"]} [{issue.get("assignee_id","?")[:12]}] {issue["title"]}')
"
```
---
## 🔄 自动恢复规则
### 触发条件
- 超 30 分钟无进展 → 自动重新调度
### 恢复操作(按平台)
| 平台 | 操作 |
|------|------|
| WorkBoard | 添加评论 → release claim → 通知 COO |
| Multica | 添加评论 → status=blocked → 通知 COO |
| 待办文档 | 标注超时 → 转为卡片(可选) |
---
## 🔗 依赖检查前置规则
### 强制检查流程
1. 认领任务前,读取依赖字段(depends_on / parent_issue_id
2. 逐一检查每个依赖任务的状态
3. 依赖未满足 → 不认领(保持 todo)
4. 超过等待阈值(1h)→ 通知依赖任务执行者
### 双平台依赖检查
```bash
# WorkBoard 依赖检查
openclaw workboard read <card-id> --json 2>/dev/null | python3 -c "
import sys, json
card = json.load(sys.stdin)
deps = card.get('dependsOn', [])
if deps:
for dep in deps:
if dep.get('status') != 'done':
print(f'⛔ WB 依赖未满足: {dep["id"]} → status={dep.get("status","?")}')
sys.exit(1)
print('✅ 所有 WB 依赖已满足')
else:
print('✅ 无 WB 依赖,可以启动')
"
# Multica 依赖检查
multica issue get <issue-id> --output json 2>/dev/null | python3 -c "
import sys, json, subprocess
issue = json.load(sys.stdin)
parent_id = issue.get('parent_issue_id')
if parent_id:
result = subprocess.run(['multica', 'issue', 'get', parent_id, '--output', 'json'],
capture_output=True, text=True)
parent = json.loads(result.stdout)
if parent.get('status') != 'done':
print(f'⛔ MUL 父 Issue {parent["identifier"]} 未完成')
sys.exit(1)
print(f'✅ 父 Issue {parent["identifier"]} 已完成')
else:
print('✅ 无父 Issue 依赖,可以启动')
"
```
---
## 🛑 最大轮次限制
### 限制值:50 轮
- 接近 80%40 轮)→ 预警
- 达到上限 → 暂停,通知 COO
### 跨平台轮次跟踪
- **WorkBoard**:通过 workboard_heartbeat 的 note 记录轮次
- **Multica**:通过 issue comment 记录轮次进度
- **待办文档**:在工作日志中记录
---
## 🫀 心跳执行清单
### 每次心跳必须检查
1.**全任务源检查**WorkBoard + Multica + 待办文档
2. ✅ 进行中任务超时检测(跨平台)
3. ✅ 依赖检查
4. ✅ 轮次计数器更新
5. ✅ 全局任务积压巡检
6. ✅ 业务入口检查
7. ✅ 各 Agent 状态巡检
---
## ⚠️ 全局关键规则
1. **心跳不打断对话** — 用户正在对话时延后执行
2. **非紧急事项延后汇报** — 等下一轮心跳或用户询问
3. **发现任务立即执行,不得请示**(任何来源)
4. **超时任务按自动恢复流程处理**(跨平台)
5. **依赖未满足不启动**
6. **达到轮次上限自动暂停**
7. **避免任务遗漏** — 三源必须全部检查,缺一不可
---
> 基于 BIZ-24 v1.1 模板生成 | 刘诗妮(secretary)专用配置
-234
View File
@@ -1,234 +0,0 @@
# HEARTBEAT.md - 陆云帆(taobaospecialist)的心跳配置
> 模板版本:v1.1 (BIZ-24) | 分类:业务 Agent
> OpenClaw Agent ID: `taobaospecialist` | Multica Agent UUID: `e0f62d8f-9568-4f41-8ad4-b73d79a163a7`
---
## 📋 全任务源统一监控(每次心跳必检)
> **核心原则:发现任何来源的任务都必须立即执行,不得遗漏。**
### 三源检查
#### 第一优先级:OpenClaw WorkBoard 卡片
```bash
# 检查 WorkBoard 中分配给我的待办卡片
openclaw workboard list --json 2>/dev/null | python3 -c "
import sys, json
data = json.load(sys.stdin)
my_cards = [c for c in data.get('cards', [])
if c.get('agentId') == 'taobaospecialist' and c.get('status') == 'todo']
for c in my_cards:
print(f'WORKBOARD TODO: {c["id"][:8]} [priority={c.get("priority","?")}] {c["title"]}')
"
```
#### 第二优先级:Multica Issues
```bash
# 检查 Multica 中分配给我的待办 Issue
multica issue list --assignee-id e0f62d8f-9568-4f41-8ad4-b73d79a163a7 --status todo --output json 2>/dev/null | python3 -c "
import sys, json
data = json.load(sys.stdin)
for issue in data:
print(f'MULTICA TODO: {issue["identifier"]} [{issue.get("priority","?")}] {issue["title"]}')
"
```
#### 第三优先级:待办文档
```bash
# 检查工作区待办文档
grep -n '\[ \]' TODO.md AGENTS.md 2>/dev/null || echo "无待办文档中未完成项"
```
### 三源合并决策
```
心跳开始
检查 WorkBoard 待办卡片(agentId=taobaospecialist
检查 Multica Issues 待办(assignee=e0f62d8f-9568-4f41-8ad4-b73d79a163a7
检查待办文档
合并去重 → 按优先级排序 → 依次执行
```
---
## ⚠️ 禁止请示规则(最高优先级,自动执行)
> **核心规则:发现分配给自己的任务 → 立即执行,不得向用户请示!**
> 心跳是后台自动行为,用户在大多数时候并不在线,请示会导致任务卡死。
### 执行流程(严格执行,无需确认)
1. **三源检查待办** — WorkBoard + Multica + 待办文档
2. **立即执行,不得请示** — 发现待办后直接执行
3. **检查进行中任务** — 确认认领的任务状态
4. **完成任务** — 通过对应平台汇报结果
### ⚠️ 绝对禁止行为
- ❌ 不得问"要不要做这个任务"
- ❌ 不得等用户确认再执行
- ❌ 不得以"需要更多信息"为由拒绝执行
---
## ⏱️ 超时检测规则
### 心跳频率:15 分钟
每次心跳跨平台执行以下检测:
1. 检查 WorkBoard 进行中任务的更新时间
2. 检查 Multica 进行中 issues 的更新时间
3. 超过 30 分钟无进展 → 标记为"疑似超时"
4. 疑似超时 → 追加一次完整心跳尝试推进
5. 确认超时 → 进入自动恢复流程
### 跨平台超时检测脚本
```bash
# WorkBoard 超时检测
echo "=== WorkBoard 超时检测 ==="
openclaw workboard list --json 2>/dev/null | python3 -c "
import sys, json, time
data = json.load(sys.stdin)
inprogress = [c for c in data.get('cards', []) if c.get('status') == 'in_progress']
now = time.time()
for c in inprogress:
updated = c.get('updated_at', '')
if updated:
age = now - time.mktime(time.strptime(updated[:19], '%Y-%m-%dT%H:%M:%S'))
if age > 1800:
print(f'⏰ WB TIMEOUT: {c["id"][:8]} [{c.get("agentId","?")}] {c["title"]}')
"
echo ""
echo "=== Multica 超时检测 ==="
multica issue list --status in_progress --output json 2>/dev/null | python3 -c "
import sys, json, time
data = json.load(sys.stdin)
now = time.time()
for issue in data:
updated = issue.get('updated_at', '')
if updated:
age = now - time.mktime(time.strptime(updated[:19], '%Y-%m-%dT%H:%M:%S'))
if age > 1800:
print(f'⏰ MUL TIMEOUT: {issue["identifier"]} [{issue.get("assignee_id","?")[:12]}] {issue["title"]}')
"
```
---
## 🔄 自动恢复规则
### 触发条件
- 超 45 分钟无进展 → 自动重新调度
### 恢复操作(按平台)
| 平台 | 操作 |
|------|------|
| WorkBoard | 添加评论 → release claim → 通知 创建者 |
| Multica | 添加评论 → status=blocked → 通知 创建者 |
| 待办文档 | 标注超时 → 转为卡片(可选) |
---
## 🔗 依赖检查前置规则
### 强制检查流程
1. 认领任务前,读取依赖字段(depends_on / parent_issue_id
2. 逐一检查每个依赖任务的状态
3. 依赖未满足 → 不认领(保持 todo)
4. 超过等待阈值(2h)→ 通知依赖任务执行者
### 双平台依赖检查
```bash
# WorkBoard 依赖检查
openclaw workboard read <card-id> --json 2>/dev/null | python3 -c "
import sys, json
card = json.load(sys.stdin)
deps = card.get('dependsOn', [])
if deps:
for dep in deps:
if dep.get('status') != 'done':
print(f'⛔ WB 依赖未满足: {dep["id"]} → status={dep.get("status","?")}')
sys.exit(1)
print('✅ 所有 WB 依赖已满足')
else:
print('✅ 无 WB 依赖,可以启动')
"
# Multica 依赖检查
multica issue get <issue-id> --output json 2>/dev/null | python3 -c "
import sys, json, subprocess
issue = json.load(sys.stdin)
parent_id = issue.get('parent_issue_id')
if parent_id:
result = subprocess.run(['multica', 'issue', 'get', parent_id, '--output', 'json'],
capture_output=True, text=True)
parent = json.loads(result.stdout)
if parent.get('status') != 'done':
print(f'⛔ MUL 父 Issue {parent["identifier"]} 未完成')
sys.exit(1)
print(f'✅ 父 Issue {parent["identifier"]} 已完成')
else:
print('✅ 无父 Issue 依赖,可以启动')
"
```
---
## 🛑 最大轮次限制
### 限制值:30 轮
- 接近 80%24 轮)→ 预警
- 达到上限 → 暂停,通知 创建者
### 跨平台轮次跟踪
- **WorkBoard**:通过 workboard_heartbeat 的 note 记录轮次
- **Multica**:通过 issue comment 记录轮次进度
- **待办文档**:在工作日志中记录
---
## 🫀 心跳执行清单
### 每次心跳必须检查
1.**全任务源检查**WorkBoard + Multica + 待办文档
2. ✅ 进行中任务超时检测(跨平台)
3. ✅ 依赖检查
4. ✅ 轮次计数器更新
5. ✅ 淘宝店铺运营指标
6. ✅ 竞品动态跟踪
---
## ⚠️ 全局关键规则
1. **心跳不打断对话** — 用户正在对话时延后执行
2. **非紧急事项延后汇报** — 等下一轮心跳或用户询问
3. **发现任务立即执行,不得请示**(任何来源)
4. **超时任务按自动恢复流程处理**(跨平台)
5. **依赖未满足不启动**
6. **达到轮次上限自动暂停**
7. **避免任务遗漏** — 三源必须全部检查,缺一不可
---
> 基于 BIZ-24 v1.1 模板生成 | 陆云帆(taobaospecialist)专用配置
-130
View File
@@ -1,130 +0,0 @@
# Agent 知识库集成指南
> **版本**: v1.0
> **任务**: BIZ-19 (BIZ-14-4)
> **日期**: 2026-06-22
> **作者**: COO (陆怀瑾)
> **状态**: 已实施
---
## 一、集成概述
### 1.1 设计原则
**「引用代替填塞」**: 不把知识内容直接塞进 Agent 配置文件,而是添加 "如何查询知识库" 的指引。Agent 在需要时主动检索,保持配置文件轻量和可维护。
### 1.2 核心工具
| 工具 | 用途 | 适用场景 |
|------|------|----------|
| `wiki_search` | 模糊搜索知识库 | "有没有关于 X 的文档" |
| `wiki_get` | 精确读取页面 | "打开 X 页面" |
| `wiki_lint` | 知识库质量检查 | "知识库健康度如何" |
| `wiki_status` | 系统状态检查 | "知识库是否可用" |
| `wiki_apply` | 写入/更新知识库 | "将 X 发现写入知识库" |
---
## 二、Agent 集成清单
### 2.1 已完成集成的 Agent15 个)
| # | Agent | 角色 | TOOLS.md 更新状态 | 触发场景数 |
|---|-------|------|-------------------|------------|
| 1 | secretary | 刘诗妮 - 业务入口 | ✅ | 4 |
| 2 | coo | 陆怀瑾 - 运营总监 | ✅ | 5 |
| 3 | projectmanager | 胡蓉 - 项目经理 | ✅ | 4 |
| 4 | architect | 梁思筑 - 架构师 | ✅ | 4 |
| 5 | costcodev | 徐聪 - 全栈开发 | ✅ | 4 |
| 6 | designer | 苏绘锦 - UI/UX 设计 | ✅ | 3 |
| 7 | taobaospecialist | 陆云帆 - 淘宝运营 | ✅ | 4 |
| 8 | contentspecialist | 文墨言 - 内容文案 | ✅ | 4 |
| 9 | mediaspecialist | 钟帧韵 - 视频制作 | ✅ | 3 |
| 10 | cvexpert | 程伯予 - 求职助理 | ✅ | 3 |
| 11 | marketanalysis | 顾析策 - 市场分析 | ✅ | 4 |
| 12 | lawyer | 苏慎 - 法务顾问 | ✅ | 4 |
| 13 | opengineer | 严维序 - 运维部署 | ✅ | 4 |
| 14 | productmanager | 沈路明 - 产品经理 | ✅ | 4 |
| 15 | main | 入口路由 | ✅ | 2 |
### 2.2 集成内容
每个 Agent 的 TOOLS.md 新增了以下内容:
1. **知识库查询指引** — 引导 Agent 查看完整检索指南
2. **角色特定触发条件** — 该 Agent 何时应查询知识库
3. **查询工具速查**`wiki_search` / `wiki_get` / `wiki_lint` 基本用法
4. **角色特定查询示例** — 1-2 个典型查询语句
5. **无结果时处理流程** — 知识缺口上报机制
---
## 三、查询触发条件设计
### 3.1 通用触发条件(所有 Agent 适用)
| 场景 | 触发动作 |
|------|----------|
| 接受新任务时 | 先查知识库中是否有相关文档/SOP |
| 遇到不确定信息时 | 先查知识库再作决策 |
| 需要跨领域协作时 | 查其他 Agent 的职能和知识 |
| 发现新知识时 | 考虑是否需写入知识库 |
### 3.2 角色特定触发条件(按 Agent 定制)
见各 Agent TOOLS.md 中的「知识库查询 → 触发条件」部分。
---
## 四、知识缺口上报机制
### 4.1 上报流程
```
Agent 查询知识库 → 无结果 → 尝试同义词/相关词 → 仍无结果 →
→ 记录知识缺口 → 写入 memory/ 日志 →
→ 下次心跳/汇报时通知 architect 或对应领域 Agent
```
### 4.2 上报格式
`docs/agent-kb-retrieval-guide.md` 第五节。
---
## 五、质量保证
### 5.1 集成测试方案
对每个 Agent 至少执行 1 次典型查询场景测试:
1. 验证 `wiki_search` 可被正确调用
2. 验证返回结果格式正确
3. 验证无结果时的降级路径
### 5.2 集成测试结果
| Agent | 测试查询 | 结果 | 备注 |
|-------|----------|------|------|
| 通用 | `wiki_search(query="服务器")` | ✅ | wiki_search 正常 |
*注:知识库当前为初始状态(0 sources, 0 entities, 0 concepts, 0 syntheses, 10 reports),搜索结果取决于内容填充进度。工具链已验证可用。*
---
## 六、后续计划
1. **知识内容填充**: 待 BIZ-14-3 交付后,各 Agent 按角色写入初始知识内容
2. **定期质量检查**: COO 每周运行 `wiki_lint()` 检查知识库健康度
3. **查询效果评估**: 运行 1 个月后统计各 Agent 知识库查询频率和命中率
4. **持续优化**: 根据使用反馈调整触发条件和查询示例
---
## 附录:相关文档
- `docs/agent-kb-retrieval-guide.md` — 知识库检索工具完整指南
- `docs/知识查询最佳实践.md` — 查询最佳实践和反模式
- `docs/wiki-toolchain-test-report.md` — Wiki 工具链测试报告 (BIZ-14-2)
- 各 Agent TOOLS.md — 角色特定查询指引
@@ -1,71 +0,0 @@
# OpenClaw NVIDIA 网关切换至 Metapi — 配置修改指南
> 交付人:严维序(opengineer | 交付日期:2026-07-03 | 关联IssueBIZ-76
## 当前状态
你的 `openclaw.json` 中有 **12 个 NVIDIA 独立提供者**,每个直连 `https://integrate.api.nvidia.com/v1`,使用各自的 `nvapi-*` API Key。
Metapi 已部署在 http://192.168.1.99:4000,内部聚合了全部 12 个 NVIDIA 账号,通过代理令牌 `sk-bizwings-metapi-proxy` 对外提供统一的 `/v1` 端点,自动负载均衡。
## 方案 A:逐项修改(保留 12 个提供者结构)
对每个 NVIDIA 提供者做两处替换:
### 1. 修改 `baseUrl`
```
"baseUrl": "https://integrate.api.nvidia.com/v1"
→ "baseUrl": "http://192.168.1.99:4000/v1"
```
### 2. 修改 `apiKey`
```
"apiKey": "nvapi-xxx..."
→ "apiKey": "sk-bizwings-metapi-proxy"
```
## 方案 B:合并为单个提供者(推荐,精简配置)
将 12 个 NVIDIA 提供者合并为 1 个:
```json
{
"id": "nvidia-metapi",
"name": "NVIDIA via Metapi",
"kind": "openai",
"baseUrl": "http://192.168.1.99:4000/v1",
"apiKey": "sk-bizwings-metapi-proxy",
"models": {
"deepseek-ai/deepseek-v4-flash": {},
"deepseek-ai/deepseek-v4-pro": {},
"z-ai/glm-5.1": {},
"z-ai/glm-5.2": {},
"qwen/qwen3.5-397b-a17b": {},
"nvidia/llama-3.3-nemotron-super-49b-v1": {},
"nvidia/mississippi": {}
}
}
```
Metapi 已部署信息:
- 访问地址:http://192.168.1.99:4000
- 管理员令牌:`bizwings-metapi-admin`
- 代理令牌:`sk-bizwings-metapi-proxy`
- 端口:4000
## 12 个 NVIDIA 账户清单
| # | 账号 | 状态 |
|---|------|------|
| 1 | NVIDIA-NIM-default | ✅ active |
| 2 | NVIDIA-NIM-98053 | ✅ active |
| 3 | NVIDIA-NIM-liuweicheng84 | ✅ active |
| 4 | NVIDIA-NIM-vx18088980513 | ✅ active |
| 5 | NVIDIA-NIM-vx64391942 | ✅ active |
| 6 | NVIDIA-NIM-cgtest1 | ✅ active |
| 7 | NVIDIA-NIM-cgtest2 | ✅ active |
| 8 | NVIDIA-NIM-cgtest3 | ✅ active |
| 9 | NVIDIA-NIM-15876517651 | ✅ active |
| 10 | NVIDIA-NIM-19584586741 | ✅ active |
| 11 | NVIDIA-NIM-18874954146 | ✅ active |
| 12 | NVIDIA-NIM-2405483110 | ✅ active |
-156
View File
@@ -1,156 +0,0 @@
# 知识查询最佳实践
> **版本**: v1.0
> **任务**: BIZ-19 (BIZ-14-4)
> **日期**: 2026-06-22
---
## 一、查询策略
### 1.1 渐进式检索原则
```
先宽后窄 → 先模糊后精确 → 先搜索后读取
```
**标准流程**
1. `wiki_search(query="关键词")` — 发现有哪些相关内容
2. `wiki_get(lookup="匹配页面")` — 精确读取具体内容
3. 如搜索结果过多(>10) → 收窄关键词重新搜索
4. 如搜索结果与需求不相关 → 调整表述方式重新搜索
### 1.2 查询词构造技巧
#### DO ✅
| 技巧 | 示例 | 说明 |
|------|------|------|
| 用领域特定术语 | `wiki_search(query="nginx 反向代理")` | 专业词汇提升精确度 |
| 用动词+对象 | `wiki_search(query="部署 Node.js")` | 明确查询意图 |
| 用自然语言问题 | `wiki_search(query="如何配置 nginx logrotate")` | 适合语义检索 |
| 用缩写和全称组合 | `wiki_search(query="CI/CD 持续集成")` | 覆盖不同表述 |
| 分步搜索 | 先搜 "nginx",再搜 "nginx 日志" | 逐步收窄范围 |
#### DON'T ❌
| 反模式 | 错误示例 | 问题 |
|--------|----------|------|
| 过于泛化的词 | `wiki_search(query="配置")` | 结果太多太杂 |
| 过于具体的短语 | `wiki_search(query="192.168.1.99 端口 22 上的 nginx")` | 命中率低 |
| 跳过搜索直接 guess 路径 | `wiki_get(lookup="随便猜的页面名")` | 大概率找不到 |
| 一次加载超大页面 | `wiki_get(lookup="巨型文档")` | 超出上下文容量 |
| 无结果后直接放弃 | 只搜一次就说"知识库没内容" | 可能是查询词不准确 |
---
## 二、结果处理
### 2.1 匹配结果数量处理
| 结果数 | 处理方式 |
|--------|----------|
| 0 | 尝试同义词/相关词 → qmd 搜索 → 上报知识缺口 |
| 1-3 | 逐个 `wiki_get` 读取完整内容 |
| 4-10 | 按评分排序,取前 3 个读取 |
| 10+ | 收窄搜索词重新搜索 |
### 2.2 大页面分页读取
```bash
# 超过 100 行的页面,分页读取
wiki_get(lookup="长文档标题", fromLine=1, lineCount=50) # 第一部分
wiki_get(lookup="长文档标题", fromLine=51, lineCount=50) # 第二部分
```
### 2.3 信息来源交叉验证
当多个查询返回不同信息时:
1. 检查页面更新时间(优先信任较新的)
2. 交叉对比多个来源
3. 如信息冲突 → 标记为"需确认",汇报给 architect
---
## 三、知识缺口处理
### 3.1 判定标准
满足以下任一条件即报告知识缺口:
- `wiki_search``qmd` 均无匹配
- 搜索结果与需求明显不相关
- 找到的文档内容已过时或不完整
### 3.2 上报模板
```
【知识缺口 - YYYY-MM-DD】
- 查询 Agent: [Agent 名称]
- 查询意图: [想了解什么]
- 已尝试检索: [用过的搜索词, 换行列出]
- 已使用工具: wiki_search / qmd
- 期望内容: [知识库中应有什么]
- 紧急程度: high / normal / low
- 建议: [谁补充、什么内容]
```
### 3.3 上报路径
| 缺口类型 | 上报目标 |
|----------|----------|
| 架构/技术 | architect (梁思筑) |
| 业务/流程 | projectmanager (胡蓉) |
| 法务/合规 | lawyer (苏慎) |
| 市场/分析 | marketanalysis (顾析策) |
| 通用/不确定 | COO (陆怀瑾) — 由 COO 分配 |
---
## 四、知识库写入准则
### 4.1 何时写入
- 完成重要决策后(如架构选型、策略调整)
- 发现可复用的模板/清单
- 完成深度分析后(市场报告、竞品分析)
- 知识缺口被填补后
### 4.2 写入工具选择
| 场景 | 工具 |
|------|------|
| 创建新知识页面 | `wiki_apply(op="create_synthesis", ...)` |
| 更新已有页面元数据 | `wiki_apply(op="update_metadata", ...)` |
### 4.3 不写入的内容
- 机密信息(密码、密钥、token
- 临时信息(当天的具体任务进度)
- 已过时会被频繁更新的数据
- 纯个人笔记(放 `memory/` 下)
---
## 五、定期维护
### 5.1 COO 每周检查清单
- [ ] 运行 `wiki_lint()` 检查质量
- [ ] 统计各 Agent 知识库查询频率
- [ ] 清理过时页面
- [ ] 评估知识缺口数量和解决率
- [ ] 输出知识库运营周报
### 5.2 Agent 自检清单
每次心跳时:
- [ ] 上次查询的知识缺口是否已上报
- [ ] 本轮工作中是否有应写入知识库的发现
---
## 附录
- `docs/agent-kb-retrieval-guide.md` — 工具使用完整指南
- `docs/Agent 知识库集成指南.md` — 集成方案总览
-2
View File
@@ -12,10 +12,8 @@
| [产品/](产品/) | PRD、需求分析 | 沈路明 (productmanager) | — | | [产品/](产品/) | PRD、需求分析 | 沈路明 (productmanager) | — |
| [技术/](技术/) | 开发规范、代码审查 | 徐聪 (costcodev) | — | | [技术/](技术/) | 开发规范、代码审查 | 徐聪 (costcodev) | — |
| [设计/](设计/) | UI设计、品牌规范 | 苏绘锦 (designer) | — | | [设计/](设计/) | UI设计、品牌规范 | 苏绘锦 (designer) | — |
| [运维/](运维/) | 部署流程、故障排查、服务器运维 | 严维序 (opengineer) | 3 |
| [运营/](运营/) | 活动策划、数据分析 | 陆怀瑾 (coo) | — | | [运营/](运营/) | 活动策划、数据分析 | 陆怀瑾 (coo) | — |
| [行政/](行政/) | 合同、报销流程 | 刘诗妮 (secretary) | — | | [行政/](行政/) | 合同、报销流程 | 刘诗妮 (secretary) | — |
| [规范/](规范/) | 运维标准、安全基线、合规要求 | 严维序 (opengineer) | — |
## 知识条目格式 ## 知识条目格式
+1 -3
View File
@@ -5,9 +5,7 @@
## 知识范围 ## 知识范围
涵盖开发规范、代码审查、架构设计、技术选型等技术团队核心知识。 涵盖开发规范、代码审查、架构设计、部署运维、技术选型等技术团队知识。
> ⚠️ 部署运维知识已迁移至 [运维/](../运维/) 领域。
## 条目清单 ## 条目清单
-25
View File
@@ -1,25 +0,0 @@
# 规范领域知识
**责任人**:严维序(opengineer
**审核人**:陆怀瑾(coo
## 知识范围
涵盖运维规范、安全标准、合规要求等规范类知识条目,支撑团队标准化运作。
## 条目清单
| 文件名 | 说明 | 状态 |
|--------|------|------|
| [服务器运维标准_v1.0.md](../运维/服务器运维标准_v1.0.md) | 服务器巡检、监控、备份运维标准 | 见运维域 |
## 待建设
- 数据库运维标准
- 安全审计基线
- 数据合规处理流程
---
> 维护者:严维序(opengineer
> 最后更新:2026-06-24
-27
View File
@@ -1,27 +0,0 @@
# 运维领域知识
**责任人**:严维序(opengineer
**审核人**:陆怀瑾(coo
## 知识范围
涵盖服务器运维、部署流程、故障排查、监控配置、安全保障等运维团队核心知识。
## 条目清单
| 文件名 | 说明 | 状态 |
|--------|------|------|
| [部署流程_v1.0.md](部署流程_v1.0.md) | 服务部署 SOP 与变更管理流程 | ✅ |
| [故障排查手册_v1.0.md](故障排查手册_v1.0.md) | 常见故障定位与处置方案 | ✅ |
| [服务器运维标准_v1.0.md](服务器运维标准_v1.0.md) | 服务器巡检、监控、备份运维标准 | 🆕 |
## 待建设
- 数据库运维指南
- 安全加固检查清单
- 灾备与应急恢复预案
---
> 维护者:严维序(opengineer
> 最后更新:2026-06-24
-178
View File
@@ -1,178 +0,0 @@
# 双色球系统部署文档
## 部署信息
| 项目 | 值 |
|------|-----|
| 项目名称 | 双色球自动化系统 |
| 部署时间 | 2026-06-29 |
| 部署人员 | 严维序 (opengineer) |
| 服务地址 | http://192.168.1.99:5000 |
| 宿主服务器 | Ubuntu-OpenClaw (192.168.1.99) |
---
## 一、项目结构
```
/home/vincent/Studio/lottoData/
├── venv/ # Python 虚拟环境
├── web_executor.py # Flask Web 服务 (监听 0.0.0.0:5000)
├── fetch_data.py # 数据抓取脚本
├── lottery.py # 双色球号码生成器
├── web_console.html # Web 控制台页面
├── LottoSpider/ # 爬虫模块
├── lottery/ # 彩票模块
├── docs/ # 文档
├── 双色球历史数据.xlsx # 历史数据文件
└── deploy/ # 部署文件
├── DEPLOY.md # 本文档
├── lotto-web.service # systemd 服务文件
├── fetch_daily.sh # 每日抓取脚本
├── cron.log # Cron 执行日志
└── fetch_YYYYMMDD.log # 每日抓取详细日志
```
---
## 二、依赖清单
| 包 | 版本 | 用途 |
|----|------|------|
| Flask | 3.1.3 | Web 服务框架 |
| pandas | 3.0.4 | 数据处理 |
| openpyxl | 3.1.5 | Excel 读写 |
| requests | 2.34.2 | HTTP 请求 |
| beautifulsoup4 | 4.15.0 | HTML 解析 |
安装命令:
```bash
python3 -m venv venv
./venv/bin/pip install flask pandas openpyxl requests beautifulsoup4
```
---
## 三、systemd 服务配置
### 服务文件
`/etc/systemd/system/lotto-web.service`
```ini
[Unit]
Description=双色球数据抓取 Web 服务
After=network.target
[Service]
Type=simple
User=vincent
WorkingDirectory=/home/vincent/Studio/lottoData
ExecStart=/home/vincent/Studio/lottoData/venv/bin/python3 /home/vincent/Studio/lottoData/web_executor.py
ExecStartPre=/home/vincent/Studio/lottoData/venv/bin/python3 -c "import flask; import pandas; import openpyxl; import requests; import bs4"
Restart=on-failure
RestartSec=5
KillMode=control-group
[Install]
WantedBy=multi-user.target
```
### 管理命令
```bash
# 安装/启用
sudo cp deploy/lotto-web.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable lotto-web
sudo systemctl start lotto-web
# 日常管理
sudo systemctl status lotto-web # 查看状态
sudo systemctl restart lotto-web # 重启
sudo systemctl stop lotto-web # 停止
sudo journalctl -u lotto-web -f # 查看实时日志
```
---
## 四、Cron 定时任务
### Cron 配置
```
30 2 * * * /home/vincent/Studio/lottoData/deploy/fetch_daily.sh >> /home/vincent/Studio/lottoData/deploy/cron.log 2>&1
```
每天凌晨 2:30 自动抓取双色球历史数据。
### 手动执行
```bash
/home/vincent/Studio/lottoData/deploy/fetch_daily.sh
# 或
/home/vincent/Studio/lottoData/venv/bin/python3 /home/vincent/Studio/lottoData/fetch_data.py
```
---
## 五、Web 接口
| 路径 | 方法 | 说明 |
|------|------|------|
| `/` | GET | Web 控制台页面 |
| `/api/status` | GET | 获取执行状态 |
| `/api/execute` | POST | 触发数据抓取 |
### 示例
```bash
# 查看状态
curl http://192.168.1.99:5000/api/status
# 触发抓取
curl -X POST http://192.168.1.99:5000/api/execute
```
---
## 六、验证清单
- [x] 依赖安装完整 (Flask, pandas, openpyxl, requests, beautifulsoup4)
- [x] systemd 服务运行正常 (active, enabled)
- [x] Web 服务可访问 (http://192.168.1.99:5000, HTTP 200)
- [x] API 接口正常 (/api/status, /api/execute)
- [x] Cron 定时任务已配置 (每日 2:30 抓取)
- [x] 手动抓取测试通过 (121 条记录保存成功)
- [x] 开机自启已配置 (systemd enable)
---
## 七、回滚方案
```bash
# 停止服务
sudo systemctl stop lotto-web
sudo systemctl disable lotto-web
sudo rm /etc/systemd/system/lotto-web.service
sudo systemctl daemon-reload
# 移除 cron
crontab -l | grep -v 'lottoData' | crontab -
# 不影响数据文件和代码
```
---
## 八、监控要点
1. **服务存活**`systemctl status lotto-web` 确认 active
2. **Web 可达**`curl http://127.0.0.1:5000/api/status`
3. **数据更新**:检查 `/home/vincent/Studio/lottoData/双色球历史数据.xlsx` 修改时间
4. **Cron 日志**:检查 `/home/vincent/Studio/lottoData/deploy/cron.log`
5. **磁盘空间**Excel 文件约 250KB,可忽略
---
> 部署人:严维序 (opengineer) | 2026-06-29
@@ -1,17 +0,0 @@
#!/bin/bash
# 双色球历史数据每日自动抓取
# Cron: 0 2 * * * /home/vincent/Studio/lottoData/deploy/fetch_daily.sh >> /home/vincent/Studio/lottoData/deploy/cron.log 2>&1
SCRIPT_DIR="/home/vincent/Studio/lottoData"
VENV_PYTHON="${SCRIPT_DIR}/venv/bin/python3"
FETCH_SCRIPT="${SCRIPT_DIR}/fetch_data.py"
LOG_DIR="${SCRIPT_DIR}/deploy"
LOG_FILE="${LOG_DIR}/fetch_$(date +%Y%m%d).log"
mkdir -p "${LOG_DIR}"
echo "=== $(date '+%Y-%m-%d %H:%M:%S') 开始执行双色球数据抓取 ==="
"${VENV_PYTHON}" "${FETCH_SCRIPT}" >> "${LOG_FILE}" 2>&1
RC=$?
echo "=== $(date '+%Y-%m-%d %H:%M:%S') 执行完成, exit code=${RC} ==="
exit ${RC}
@@ -1,16 +0,0 @@
[Unit]
Description=双色球数据抓取 Web 服务
After=network.target
[Service]
Type=simple
User=vincent
WorkingDirectory=/home/vincent/Studio/lottoData
ExecStart=/home/vincent/Studio/lottoData/venv/bin/python3 /home/vincent/Studio/lottoData/web_executor.py
ExecStartPre=/home/vincent/Studio/lottoData/venv/bin/python3 -c "import flask; import pandas; import openpyxl; import requests; import bs4"
Restart=on-failure
RestartSec=5
KillMode=control-group
[Install]
WantedBy=multi-user.target
@@ -1,5 +0,0 @@
Flask==3.1.3
pandas==3.0.4
openpyxl==3.1.5
requests==2.34.2
beautifulsoup4==4.15.0
-274
View File
@@ -1,274 +0,0 @@
# 故障排查手册
## 元数据
| 属性 | 值 |
|------|-----|
| **领域** | 运维 |
| **责任人** | 严维序(opengineer |
| **版本** | v1.0 |
| **创建日期** | 2026-06-24 |
| **最后更新** | 2026-06-24 |
| **标签** | 故障排查, 运维, 排障 |
## 概述
本手册汇总 BizWings 环境中常见的系统与服务故障定位方法和修复方案。覆盖 SSH 连接、Nginx、数据库、磁盘、Docker 等核心场景。
---
## 一、SSH 连接故障
### 1.1 连接超时
```bash
# 诊断步骤
ssh -vvv root@<ip> -p <port> # 查看详细连接日志
ping <ip> # 检查网络连通性
nmap <ip> -p <port> # 检查端口状态
```
**常见原因**
- 目标服务器防火墙未开放端口
- 源 IP 未加入白名单
- 服务器负载过高,sshd 响应慢
**解决方案**
1. 检查服务器防火墙:`iptables -L -n``ufw status`
2. 检查 sshd 是否运行:`systemctl status sshd`
3. 检查负载:`top -n1 | head -5`
### 1.2 认证失败
```bash
# 诊断步骤
ssh -p <port> root@<ip> # 尝试密码登录
# Permission denied (publickey,password) 提示
```
**常见原因**
- 密码错误(检查 TOOLS.md 中记录)
- SSH 密钥认证配置错误
- `/etc/ssh/sshd_config``PasswordAuthentication no`
**解决方案**
1. 确认密码与 TOOLS.md 一致
2. 检查 `sshd_config``grep PasswordAuthentication /etc/ssh/sshd_config`
3. 临时允许密码登录:`sed -i 's/PasswordAuthentication no/PasswordAuthentication yes/' /etc/ssh/sshd_config && systemctl reload sshd`
---
## 二、Nginx 服务异常
### 2.1 Nginx 启动失败 / 卡在 activating
```bash
# 诊断步骤
systemctl status nginx # 查看状态
journalctl -u nginx --no-pager -n 50 # 查看日志
nginx -t # 配置语法检查
```
**根因(经验)**:进程残留导致端口占用
```bash
# 修复
pkill -9 nginx # 强制清理残留进程
sleep 2
systemctl start nginx # 重新启动
systemctl status nginx # 确认状态
```
### 2.2 502 Bad Gateway
```bash
# 诊断步骤
curl -I http://localhost:<upstream-port> # 检查上游服务
ss -tlnp | grep <upstream-port> # 检查端口监听
systemctl status <upstream-service> # 检查上游进程
```
**常见原因**
- 上游服务未启动或崩溃
- 连接池耗尽
**解决方案**
1. 重启上游服务:`systemctl restart <service>`
2. 检查 `upstream` 配置是否正确
### 2.3 日志轮转失败
```bash
# 诊断步骤
cat /var/log/nginx/error.log | head # 查看是否有日志无法写入
ls -la /var/log/nginx/ # 查看日志文件
/usr/sbin/logrotate -d /etc/logrotate.d/nginx # 测试 logrotate
```
**修复方案**
```bash
# 修改 /etc/logrotate.d/nginx 中的 postrotate 脚本
# 将 invoke-rc.d nginx rotate 改为:
postrotate
systemctl reload nginx
endscript
```
---
## 三、数据库连接故障
### 3.1 MySQL 连接失败
```bash
# 诊断步骤
mysql -h <host> -P <port> -u root -p # 测试连接
telnet <host> <port> # 检查端口
systemctl status mysql # 检查服务
```
**常见原因**
- 服务未运行
- 防火墙未放行 3306 端口
- 用户权限 / host 限制
- 连接数超限
**解决方案**
```bash
# 检查连接数
mysql -e "SHOW VARIABLES LIKE 'max_connections';"
mysql -e "SHOW PROCESSLIST;"
# 检查用户权限
mysql -e "SELECT user, host FROM mysql.user WHERE user='root';"
```
### 3.2 MySQL 空间不足
```bash
# 诊断
df -h # 磁盘空间
mysql -e "SELECT table_schema, ROUND(SUM(data_length+index_length)/1024/1024,2) AS size_mb FROM information_schema.tables GROUP BY table_schema ORDER BY size_mb DESC;"
```
**解决方案**
- 清理过期 binlog`PURGE BINARY LOGS BEFORE DATE_SUB(NOW(), INTERVAL 7 DAY);`
- 清理临时表
- 扩展磁盘
---
## 四、磁盘空间告警
### 4.1 诊断
```bash
df -h # 查看各分区使用率
du -sh /* 2>/dev/null | sort -rh | head -10 # 找到大文件目录
find / -type f -size +100M -exec ls -lh {} \; 2>/dev/null # 大文件定位
```
### 4.2 清理方案
```bash
# Docker 日志和镜像清理
docker system prune -af --volumes # 清理未使用的 Docker 资源
# 系统日志轮转
journalctl --vacuum-time=7d # 清理 7 天前的 journal 日志
# 应用日志归档
find /var/log -name "*.log" -mtime +30 -exec gzip {} \; # 压缩旧日志
find /var/log -name "*.gz" -mtime +90 -delete # 删除 90 天前的压缩日志
```
---
## 五、Docker 容器异常
### 5.1 容器停止
```bash
docker ps -a | grep <container> # 查看容器状态
docker logs <container> --tail 50 # 查看最近日志
```
**修复**
```bash
docker start <container> # 手动启动
docker compose -f <path> up -d # 使用 Compose 重启
```
### 5.2 Docker API 无响应
```bash
systemctl status docker # 检查 Docker 服务
journalctl -u docker --no-pager -n 50 # 查看 Docker 日志
```
**修复**
```bash
systemctl restart docker # 重启 Docker 守护进程
```
---
## 六、系统进程故障
### 6.1 端口被占用
```bash
ss -tlnp | grep <port> # 查看占用端口的进程
fuser -k <port>/tcp # 强制释放端口
```
### 6.2 systemd 服务异常
```bash
systemctl status <service> # 检查状态
journalctl -u <service> --no-pager -n 100 # 查看服务日志
# 常用修复
systemctl daemon-reload # 重载 unit 文件
systemctl restart <service> # 重启
systemctl enable <service> # 设置开机自启
```
---
## 七、日志分析工具
### 7.1 常用命令
```bash
# 实时日志跟踪
tail -f /var/log/<app>/access.log
# 错误过滤
grep -i "error\|exception\|failed" /var/log/<app>/app.log | tail -50
# 时间范围过滤
awk '/2026-06-24 10:00/,/2026-06-24 11:00/' /var/log/<app>/app.log
```
### 7.2 关键检查点
| 故障表现 | 优先检查 | 常见根因 |
|----------|----------|----------|
| 服务无响应 | systemctl status | 进程 OOM / 崩溃 |
| API 返回错误 | 应用日志 + Nginx 日志 | 代码 bug / 上游依赖异常 |
| 高延迟 | top + ss + 应用日志 | 资源争抢 / 死锁 |
| 数据库异常 | MySQL error log | 慢查询 / 连接数超限 |
---
## 相关条目
- [部署流程_v1.0.md](部署流程_v1.0.md)
- [服务器运维标准_v1.0.md](服务器运维标准_v1.0.md)
## 变更记录
| 日期 | 版本 | 变更说明 | 变更人 |
|------|------|----------|--------|
| 2026-06-24 | v1.0 | 初始创建 | 严维序 |
@@ -1,177 +0,0 @@
# 服务器运维标准
## 元数据
| 属性 | 值 |
|------|-----|
| **领域** | 运维 |
| **责任人** | 严维序(opengineer |
| **版本** | v1.0 |
| **创建日期** | 2026-06-24 |
| **最后更新** | 2026-06-24 |
| **标签** | 运维, 监控, 巡检, 备份 |
## 概述
本文档定义 BizWings 团队所有服务器的日常运维标准,包括巡检频率、监控指标、备份策略和安全基线。适用于所有生产环境服务器(阿里云 / 家庭内网 / HP 服务器)。
---
## 一、服务器巡检标准
### 1.1 巡检频率
| 类型 | 频率 | 执行方式 |
|------|------|----------|
| 心跳自检 | 每 10 分钟 | openclaw 心跳自动巡检 |
| 深度巡检 | 每日一次 | 手动执行 `python3 $SCRIPTS/heartbeat_helper.py opengineer` |
| 全量巡检 | 每周一次 | 逐个检查全部服务器 |
### 1.2 巡检清单
#### 资源负载
```bash
# 磁盘使用率(警告 > 80%,严重 > 90%
df -h | grep -v tmpfs
# CPU 负载
uptime
# 内存使用
free -h
# 网络 IO
sar -n DEV 1 3
```
#### 服务状态
```bash
# 核心服务清单(按实际部署确认)
systemctl status nginx mysql docker sshd
# Docker 容器健康
docker ps | grep -c "Up"
```
#### 日志异常
```bash
# 最近 10 分钟的错误日志
journalctl --since "10 min ago" -p err --no-pager | tail -20
```
---
## 二、监控指标定义
### 2.1 告警阈值
| 指标 | 警告 (WARN) | 严重 (CRIT) | 处理 |
|------|-------------|-------------|------|
| 磁盘使用率 | > 80% | > 90% | 清理日志 / 扩容 |
| CPU 负载 (1min) | > 4.0 | > 8.0 | 检查异常进程 |
| 内存使用率 | > 85% | > 95% | 检查 OOM 风险 |
| 根分区 inode | > 80% | > 90% | 清理小文件 |
| 服务进程 | 停止 | — | 重启服务 |
| 端口监听 | 消失 | — | 检查服务状态 |
| Docker 容器 | 非 Up | — | docker start / compose up |
### 2.2 日志监控
- 系统日志:`journalctl -p err` 重点关注
- 应用日志:`error`, `exception`, `failed`, `timeout` 关键词监控
- Nginx 日志:5xx 错误率 > 1% 时触发调查
---
## 三、备份策略
### 3.1 数据库备份
```bash
# MySQL 全量备份(建议每日凌晨执行)
mysqldump --all-databases --single-transaction --quick | gzip > /backup/db/all-$(date +%Y%m%d).sql.gz
```
### 3.2 配置备份
- 服务器配置文件:`/backup/conf/<server>/` 目录
- 每次变更前执行:`cp <config> <config>.$(date +%Y%m%d-%H%M%S).bak`
### 3.3 Docker 数据备份
```bash
# 思源笔记备份(已配置每日 3:00)
tar czf /backup/siyuan/siyuan-data-$(date +%Y%m%d).tar.gz -C <data-dir> .
```
### 3.4 备份保留策略
| 类型 | 保留期限 |
|------|----------|
| 数据库全量备份 | 30 天 |
| 配置备份 | 90 天 |
| Docker 数据 | 7 天 |
| 日志归档 | 90 天 |
---
## 四、变更管理标准
### 4.1 变更准入
- ✅ 每次变更前必须备份原始文件
- ✅ 高危操作(防火墙、内核、数据库)必须保留回滚方案
- ✅ 变更前评估影响范围
- ✅ 变更后验证服务状态
- ❌ 禁止在无备份的情况下直接修改生产配置
- ❌ 禁止在高峰时段执行非紧急变更
### 4.2 变更分级
| 级别 | 示例 | 要求 |
|------|------|------|
| 低风险 | 普通应用更新 | 备份 → 部署 → 验证 |
| 中风险 | 配置修改 | 备份 → 预演 → 部署 → 验证 |
| 高风险 | 内核 / 防火墙 / 数据库 | 备份 → 预演 → 通知 → 部署 → 验证 → 监控 |
---
## 五、安全基线
### 5.1 基本要求
- [ ] SSH 禁止 root 密码登录(高风险服务器)
- [ ] 防火墙最小权限原则
- [ ] 非必要端口不对外开放
- [ ] 定期更新系统安全补丁
- [ ] 日志审计开启
### 5.2 密码管理
- 服务器密码统一记录在 TOOLS.md
- 数据库密码统一管理
- 禁止在代码中硬编码密码
---
## 六、服务器清单与分类
| 环境 | 服务器数 | 用途 | 巡检频率 |
|------|----------|------|----------|
| 阿里云生产 | 3 | 应用服务、数据库 | 每次心跳 |
| 家庭内网生产 | 4 | 应用、数据库、PVE | 每次心跳 |
| HP 测试 | 3 | 测试、NAS | 每日 |
| 树莓派 | 1 | 辅助设备 | 每日 |
详细清单见 TOOLS.md「SSH/WinRM 服务器清单」
---
## 相关条目
- [部署流程_v1.0.md](部署流程_v1.0.md)
- [故障排查手册_v1.0.md](故障排查手册_v1.0.md)
## 变更记录
| 日期 | 版本 | 变更说明 | 变更人 |
|------|------|----------|--------|
| 2026-06-24 | v1.0 | 初始创建 | 严维序 |
-202
View File
@@ -1,202 +0,0 @@
# 服务部署流程 SOP
## 元数据
| 属性 | 值 |
|------|-----|
| **领域** | 运维 |
| **责任人** | 严维序(opengineer |
| **版本** | v1.0 |
| **创建日期** | 2026-06-24 |
| **最后更新** | 2026-06-24 |
| **标签** | 部署, 运维, SOP |
## 概述
本文档定义 BizWings 团队所有业务服务的部署流程标准,涵盖部署前检查、执行步骤、验证测试和回滚预案。适用于所有生产环境的代码部署与服务更新。
---
## 一、部署前置检查
### 1.1 代码准备
- [ ] 代码已合并到目标分支(main / release
- [ ] PR 已通过 Code Review 并合并
- [ ] 本地或 CI 构建通过(编译无报错)
- [ ] 版本号已更新(如有)
### 1.2 环境检查
- [ ] 目标服务器磁盘空间充足(> 剩余 20%)
- [ ] CPU / 内存负载正常(< 80%
- [ ] 网络连通性:本机 → 目标服务器可达
- [ ] 目标端口未被占用
- [ ] 依赖服务(数据库 / 中间件)运行正常
### 1.3 备份准备
- [ ] **配置备份**:服务器配置文件备份到 `/backup/conf/` 目录
- [ ] **数据库备份**:涉及数据库变更,先执行 `mysqldump` 全量备份
- [ ] **当前版本标记**:记录当前运行版本号或 Git commit hash
---
## 二、部署执行步骤
### 2.1 文件分发
```bash
# 标准部署(SSH + scp/rsync
scp -P <port> ./dist/app root@<server>:/opt/app/
# 或使用 rsync 增量同步
rsync -avz --delete -e "ssh -p <port>" ./dist/ root@<server>:/opt/app/
```
### 2.2 服务更新
#### 方式 Asystemd 服务
```bash
# 1. 停止服务
systemctl stop <service-name>
# 2. 备份旧版本(如有必要)
mv /opt/app/<app> /opt/app/<app>.bak
# 3. 放置新版本
cp /tmp/<app> /opt/app/<app>
chmod +x /opt/app/<app>
# 4. 重启服务
systemctl start <service-name>
systemctl status <service-name>
```
#### 方式 BDocker 容器
```bash
# 1. 拉取新镜像
docker pull <registry>/<image>:<tag>
# 2. 停止旧容器
docker stop <container-name>
docker rm <container-name>
# 3. 启动新容器
docker run -d --name <container-name> \
--restart unless-stopped \
-p <host-port>:<container-port> \
<registry>/<image>:<tag>
```
#### 方式 CNginx 反向代理更新
```bash
# 更新上游配置后重载
nginx -t # 语法检查
systemctl reload nginx # 热重载
```
### 2.3 配置变更
```bash
# 1. 备份当前配置
cp /etc/<app>/config.yml /etc/<app>/config.yml.$(date +%Y%m%d-%H%M%S)
# 2. 修改配置
vim /etc/<app>/config.yml
# 3. 重启服务使配置生效
systemctl restart <service-name>
```
---
## 三、部署验证
### 3.1 连通性验证
```bash
# 服务端口监听确认
ss -tlnp | grep <port>
# HTTP 服务健康检查
curl -s -o /dev/null -w "%{http_code}" http://localhost:<port>/health
# 预期返回:200
```
### 3.2 功能验证
- [ ] API 基础功能运行正常
- [ ] 日志无新增 ERROR 级别报错
- [ ] 数据库连接正常
- [ ] 前端页面(如有)可正常加载
### 3.3 监控确认
- [ ] Prometheus / Grafana 指标正常
- [ ] 日志系统(如有)已捕获新日志
- [ ] 告警规则未被触发
---
## 四、回滚方案
### 4.1 代码回滚
```bash
# Git 回滚到上一版本
cd /opt/app/repo
git revert HEAD --no-edit
git push
# 重新执行部署
```
### 4.2 文件回滚
```bash
# 恢复备份文件
mv /opt/app/<app>.bak /opt/app/<app>
systemctl restart <service-name>
```
### 4.3 数据库回滚
```bash
# 导入备份
gunzip < /backup/db/<dbname>.$(date +%Y%m%d).sql.gz | mysql -u root -p<pass> <dbname>
```
### 4.4 回滚确认
- [ ] 旧版本服务运行正常
- [ ] 端口监听确认
- [ ] 用户无访问异常
- [ ] 记录回滚原因到工作日志
---
## 五、部署后记录
### 5.1 必填信息
| 项目 | 内容 |
|------|------|
| 部署时间 | YYYY-MM-DD HH:mm |
| 部署人 | 严维序(opengineer |
| 部署内容 | [简要描述] |
| 版本 | commit hash / tag |
| 验证结果 | ✅/❌ 通过 |
| 回滚情况 | 无需回滚 / 已回滚(原因) |
### 5.2 记录位置
- 工作日志:`memory/YYYY-MM-DD.md`
- 任务记录:WorkBoard 相关卡片注释
- 知识更新:如部署暴露流程问题,更新本文档
---
## 相关条目
- [故障排查手册_v1.0.md](故障排查手册_v1.0.md)
- [服务器运维标准_v1.0.md](服务器运维标准_v1.0.md)
## 变更记录
| 日期 | 版本 | 变更说明 | 变更人 |
|------|------|----------|--------|
| 2026-06-24 | v1.0 | 初始创建 | 严维序 |
+5 -6
View File
@@ -1,9 +1,9 @@
# BIZ-13 智能体运行稳定性保障方案 # BIZ-13 智能体运行稳定性保障方案
> 版本:v1.1 > 版本:v1.0
> 编制:陆怀瑾(COO > 编制:陆怀瑾(COO
> 日期:2026-06-22 > 日期:2026-06-22
> 状态:Phase 1 执行中(Vincent 已审阅同意) > 状态:待审阅
--- ---
@@ -305,10 +305,9 @@ def retry_with_backoff(api_call, max_retries=3):
## 七、实施步骤 ## 七、实施步骤
### 阶段 1:心跳机制落地(本周) ### 阶段 1:心跳机制落地(本周)
- [x] 更新所有 Agent 的 HEARTBEAT.md15/15 Agent 已完成) - [ ] 更新所有 Agent 的 HEARTBEAT.md
- [x] 已创建分步实施子任务(BIZ-24 ~ BIZ-285个子任务 - [ ] 配置定时任务(10 分钟
- [ ] 配置定时任务(10/15 分钟)→ BIZ-25,已分派 opengineer 严维序 - [ ] 测试超时检测
- [ ] 测试超时检测 → BIZ-24 执行中
### 阶段 2:限流优化(下周) ### 阶段 2:限流优化(下周)
- [ ] 实现请求队列 - [ ] 实现请求队列
-835
View File
@@ -1,835 +0,0 @@
# BIZ-24 HEARTBEAT.md 增强模板方案
> Phase 1 of BIZ-13 运行稳定性保障方案
> 版本:v1.12026-06-22 优化:增加全任务源统一监控;已部署)
> 编制:陆怀瑾(COO
> 日期:2026-06-22
> 状态:已部署
> 关联:[BIZ-13 运行稳定性保障方案](BIZ-13_运行稳定性保障方案.md)
---
## 一、目标
为所有 Agent 的 HEARTBEAT.md 文件统一增强以下机制,解决任务停滞、运行异常与工作遗漏问题:
1. **全任务源统一监控** — 覆盖 OpenClaw WorkBoard + Multica Issues + 待办文档,避免工作遗漏
2. **禁止请示规则** — 消除"等待用户确认"导致的任务卡死
3. **超时检测规则** — 按 Agent 类型差异化配置心跳频率
4. **自动恢复规则** — 检测无进展时自动重新调度
5. **依赖检查前置** — 任务启动前强制检查所有依赖
6. **最大轮次限制** — 防止无限循环或资源耗尽
### 1.1 为什么需要全任务源统一监控
当前 Agent 工作面临的任务来源是多平台的:
| 任务来源 | 平台/工具 | 查询方式 | 当前监控状态 |
|----------|-----------|----------|------------|
| WorkBoard 卡片 | OpenClaw workboard | `openclaw workboard list` | ✅ 已纳入 |
| 待办文档 | 各 Agent workspace 的 TODO.md / AGENTS.md | 文件读取 | ⚠️ 部分纳入 |
| Multica Issues | Multica 平台 | `multica issue list --assignee-id <id>` | ❌ 未纳入 |
**问题**Multica Issues 中分配给 Agent 的任务当前完全不在心跳监控范围内,Agent 可能永远不会发现并执行这些任务,导致工作永久遗漏。
**对策**:每次心跳同步检查以上三个来源,确保无一遗漏。
---
## 二、Agent 分类与参数配置
### 2.1 分类标准
| 分类 | 特征 | Agent |
|------|------|-------|
| 高频 Agent | 需频繁检查任务状态、全局监控 | secretary, coo |
| 开发 Agent | 执行开发/设计/部署等长周期任务 | projectmanager, productmanager, architect, costcodev, designer, opengineer |
| 业务 Agent | 执行专项业务任务 | taobaospecialist, contentspecialist, mediaspecialist, cvexpert, marketanalysis, lawyer |
### 2.2 参数配置矩阵
| 参数 | 高频 Agent | 开发 Agent | 业务 Agent |
|------|-----------|-----------|-----------|
| 心跳频率 | 10 分钟 | 15 分钟 | 15 分钟 |
| 最大轮次 | 50 轮 | 100 轮 | 30 轮 |
| 超时告警阈值 | 20 分钟无进展 | 30 分钟无进展 | 30 分钟无进展 |
| 自动恢复等待 | 30 分钟后重新调度 | 45 分钟后重新调度 | 45 分钟后重新调度 |
| 告警通知对象 | COO | COO + 创建者 | 创建者 |
---
## 三、六项增强规则详解
### 规则 0:全任务源统一监控
**问题**:Agent 的任务分布在多个平台(OpenClaw WorkBoard、Multica Issues、工作区待办文档),各平台独立存在,Agent 只监控其中一部分会导致工作任务被永久遗漏。
**规则文本**
```markdown
## 📋 全任务源统一监控(每次心跳必检)
> **核心原则:发现任何来源的任务都必须立即执行,不得遗漏。**
> 你的工作任务可能存在于三个地方:OpenClaw WorkBoard、Multica Issues、本地待办文档。
### 任务源检查清单(按优先级)
#### 第一优先级:OpenClaw WorkBoard 卡片
\```bash
# 检查 WorkBoard 中分配给自己的待办卡片
openclaw workboard list --json 2>/dev/null | python3 -c "
import sys, json
data = json.load(sys.stdin)
my_cards = [c for c in data.get('cards', [])
if c.get('agentId') == '<your_agent_id>' and c.get('status') == 'todo']
for c in my_cards:
print(f'WORKBOARD TODO: {c[\"id\"][:8]} [priority={c.get(\"priority\",\"?\")}] {c[\"title\"]}')
"
\```
#### 第二优先级:Multica Issues
\```bash
# 检查 Multica 中分配给自己的待办 Issue
multica issue list --assignee-id <your_multica_agent_uuid> --status todo --output json 2>/dev/null | python3 -c "
import sys, json
data = json.load(sys.stdin)
for issue in data:
print(f'MULTICA TODO: {issue[\"identifier\"]} [{issue.get(\"priority\",\"?\")}] {issue[\"title\"]}')
"
\```
#### 第三优先级:待办文档
\```bash
# 检查工作区待办文档(TODO.md 或 AGENTS.md 中未完成的任务)
grep -n '\[ \]' TODO.md AGENTS.md 2>/dev/null || echo "无待办文档"
\```
### 三源合并决策
```
心跳开始
检查 WorkBoard 待办卡片
检查 Multica Issues 待办
检查待办文档
合并去重(避免同一任务在不同来源重复出现)
按优先级排序后依次执行
```
### Multica Issue 认领与执行流程
```
发现 Multica todo Issueassignee 是自己)
启动 Multica Runtime 执行任务
完成后通过 multica issue comment add 汇报结果
更新 issue status 为 in_review / done
```
### COO 专属:全平台积压巡检
作为 COO,还需要额外检查全平台的任务积压情况(不限自己):
\```bash
#!/bin/bash
# COO 全平台任务积压巡检脚本
echo "=== OpenClaw WorkBoard 全局积压 ==="
openclaw workboard list --json 2>/dev/null | python3 -c "
import sys, json
data = json.load(sys.stdin)
cards = data.get('cards', [])
todo = [c for c in cards if c.get('status') == 'todo']
inprog = [c for c in cards if c.get('status') == 'in_progress']
print(f' 待办: {len(todo)} | 进行中: {len(inprog)} | 总计: {len(cards)}')
if len(todo) > 5:
print(f' ⚠️ 积压告警:待办超过 5 个,需协调资源')
for c in todo[:10]:
print(f' {c[\"id\"][:8]} → {c.get(\"agentId\",\"?\")} - {c[\"title\"][:60]}')
"
echo ""
echo "=== Multica Issues 全局积压 ==="
multica issue list --status todo --output json 2>/dev/null | python3 -c "
import sys, json
data = json.load(sys.stdin)
print(f' 待办: {len(data)}')
if len(data) > 5:
print(f' ⚠️ 积压告警:Multica 待办超过 5 个')
for issue in data[:10]:
print(f' {issue[\"identifier\"]} → {issue.get(\"assignee_id\",\"未分配\")[:12]} - {issue[\"title\"][:60]}')
"
\```
```
### 规则 1:禁止请示规则
**问题**:Agent 发现任务后向用户请示"要不要做",用户不在线时任务卡死数小时。
**规则文本**
```markdown
## ⚠️ 禁止请示规则(最高优先级)
你在心跳中发现任何来源的任务后,必须立即执行,绝对禁止向用户请示!
原因:
- 用户在大多数时候并不在线,请示会导致任务卡死数小时
- 心跳是后台自动行为,用户没有义务实时响应你的请示
- 你的职责是"主动完成交付",而不是"等待指令"
正确做法:
1. 发现任何来源的待办任务(WorkBoard / Multica / 待办文档)
2. 立即认领并执行
3. 如遇信息不足,先按自己的最佳理解执行,执行中再调整
4. 任务完成后,通过对应平台汇报结果
错误做法(绝对禁止):
- ❌ "我发现了一个任务,要不要做?"
- ❌ "这个任务需要更多信息,请告诉我..."
- ❌ "任务已完成,请确认是否符合要求"
```
### 规则 2:超时检测规则
**问题**:Agent 执行到某一步后卡住,长时间无输出,无任何监告。
**规则文本**
高频 Agent 版:
```markdown
## ⏱️ 超时检测规则
### 心跳频率:10 分钟
每次心跳执行以下检测:
1. 检查所有平台进行中任务的最新更新时间(WorkBoard + Multica
2. 如超过 20 分钟无进展 → 标记为"疑似超时"
3. 疑似超时 → 立即追加一次完整心跳,尝试推进
4. 如确认超时 → 进入自动恢复流程
### 跨平台超时检测脚本
\```bash
# 检查进行中任务是否超时(WorkBoard + Multica
echo "=== WorkBoard 超时检测 ==="
openclaw workboard list --json 2>/dev/null | python3 -c "
import sys, json, time
data = json.load(sys.stdin)
inprogress = [c for c in data.get('cards', []) if c.get('status') == 'in_progress']
now = time.time()
for c in inprogress:
updated = c.get('updated_at', '')
if updated:
age = now - time.mktime(time.strptime(updated[:19], '%Y-%m-%dT%H:%M:%S'))
if age > 1200:
print(f'⏰ WB TIMEOUT: {c[\"id\"][:8]} [{c.get(\"agentId\",\"?\")}] {c[\"title\"]}')
"
echo "=== Multica 超时检测 ==="
multica issue list --status in_progress --output json 2>/dev/null | python3 -c "
import sys, json, time
data = json.load(sys.stdin)
now = time.time()
for issue in data:
updated = issue.get('updated_at', '')
if updated:
age = now - time.mktime(time.strptime(updated[:19], '%Y-%m-%dT%H:%M:%S'))
if age > 1200:
print(f'⏰ MUL TIMEOUT: {issue[\"identifier\"]} [{issue.get(\"assignee_id\",\"?\")[:12]}] {issue[\"title\"]}')
"
\```
```
开发 Agent 版(差异部分):
```markdown
### 心跳频率:15 分钟
每次心跳执行以下检测:
1. 检查所有平台进行中任务的最新更新时间(WorkBoard + Multica
2. 如超过 30 分钟无进展 → 标记为"疑似超时"
```
业务 Agent 版(差异部分):
```markdown
### 心跳频率:15 分钟
每次心跳执行以下检测:
1. 检查所有平台进行中任务的最新更新时间(WorkBoard + Multica
2. 如超过 30 分钟无进展 → 标记为"疑似超时"
```
### 规则 3:自动恢复规则
**问题**:检测到无进展后没有自动恢复手段,任务永久停滞。
**规则文本**
```markdown
## 🔄 自动恢复规则
### 恢复流程
```
检测到超时(跨平台无进展超阈值)
步骤 1:追加一次完整心跳,尝试推进任务
步骤 2:检查任务状态
┌─────────────┴─────────────┐
│ │
有进展 仍无进展
│ │
重置超时计数器 步骤 3:通知 COO/创建者
│ │
继续执行 步骤 4:通过对应平台标记 blocked
步骤 5:重新调度(分配备用 Agent 或
等待人工介入)
```
### 自动恢复触发条件
- 高频 Agent:超 30 分钟无进展 → 自动重新调度
- 开发 Agent:超 45 分钟无进展 → 自动重新调度
- 业务 Agent:超 45 分钟无进展 → 自动重新调度
### 跨平台恢复操作
**WorkBoard 任务**
1. 添加评论说明超时原因
2. 释放 Agent 认领(release claim
3. 通知 COO 重新分配
**Multica Issue**
1. `multica issue comment add` 说明超时原因
2. `multica issue status <id> blocked`
3. 通知 COO 重新分配
**待办文档任务**
1. 在原文档中标注超时状态
2. 如可转为 WorkBoard 卡片 → 创建卡片并通知 COO
```
### 规则 4:依赖检查前置
**问题**:任务开始后才发现依赖未满足,浪费 Agent 时间,且可能导致循环等待。
**规则文本**
```markdown
## 🔗 依赖检查前置规则
### 任务启动前强制检查
每次认领或启动任务前,必须执行依赖检查:
**WorkBoard 任务**
1. 读取任务的 depends_on 字段
2. 逐一检查每个依赖任务的状态
3. 所有依赖 ready → 可以启动
4. 任一依赖未完成 → 禁止启动,标记为 blocked
**Multica Issue**
1. 读取 issue 的 parent_issue_id
2. 检查父 issue 状态
3. 父 issue 未完成 → 禁止启动
### 检查脚本
#### WorkBoard 依赖检查
\```bash
openclaw workboard read <card-id> --json 2>/dev/null | python3 -c "
import sys, json
card = json.load(sys.stdin)
deps = card.get('dependsOn', [])
if deps:
for dep in deps:
print(f'依赖: {dep[\"id\"]} → 状态: {dep.get(\"status\", \"?\")}')
if dep.get('status') != 'done':
print(f'⛔ WB 依赖未满足,禁止启动 {card[\"id\"][:8]}')
sys.exit(1)
print('✅ 所有 WB 依赖已满足')
else:
print('✅ 无 WB 依赖,可以启动')
"
\```
#### Multica 依赖检查
\```bash
multica issue get <issue-id> --output json 2>/dev/null | python3 -c "
import sys, json
issue = json.load(sys.stdin)
parent_id = issue.get('parent_issue_id')
if parent_id:
import subprocess
result = subprocess.run(['multica', 'issue', 'get', parent_id, '--output', 'json'],
capture_output=True, text=True)
parent = json.loads(result.stdout)
if parent.get('status') != 'done':
print(f'⛔ MUL 父 Issue {parent[\"identifier\"]} 未完成,禁止启动')
sys.exit(1)
print(f'✅ 父 Issue {parent[\"identifier\"]} 已完成')
else:
print('✅ 无父 Issue 依赖,可以启动')
"
\```
### 依赖未满足时的处理
1. 不认领任务(保持 todo 状态)
2. 不在该任务上浪费心跳时间
3. 如超过等待阈值(高频 1h / 开发/业务 2h),通知依赖任务的执行者
```
### 规则 5:最大轮次限制
**问题**:Agent 陷入无限循环,反复执行相同逻辑无进展,持续消耗 API 配额。
**规则文本**
高频 Agent 版:
```markdown
## 🛑 最大轮次限制
### 限制值:50 轮
单次任务执行不得超过 50 个对话轮次。
### 检测机制
- 每次心跳记录已消耗轮次
- 接近上限(80%)时发出预警
- 达到上限时自动暂停
### 超限处理
```
达到最大轮次
1. 暂停任务执行
2. 记录已完成的步骤和未完成的部分
3. 通知 COO,附当前进度
4. COO 决定:重新分配 / 拆分任务 / 人工介入
```
### 跨平台轮次跟踪
- **WorkBoard**:通过 workboard_heartbeat 的 note 记录轮次
- **Multica**:通过 issue comment 记录轮次进度
- **待办文档**:在工作日志中记录
### 计数器维护
\```bash
# 每次心跳更新轮次计数
# 轮次数据存储在任务 metadata 或 comment 中
\```
```
开发 Agent 版(差异部分):
```markdown
### 限制值:100 轮
单次任务执行不得超过 100 个对话轮次。
```
业务 Agent 版(差异部分):
```markdown
### 限制值:30 轮
单次任务执行不得超过 30 个对话轮次。
```
---
## 四、HEARTBEAT.md 完整增强模板
### 4.1 高频 Agent 完整模板(secretary / coo
```markdown
# HEARTBEAT.md - [Agent Name] 的心跳配置
## 📋 全任务源统一监控(每次心跳必检)
> **核心原则:发现任何来源的任务都必须立即执行,不得遗漏。**
### 三源检查
1. **OpenClaw WorkBoard** — workboard list → 查找 agent_id=自己 & status=todo
2. **Multica Issues** — issue list --assignee-id <uuid> --status todo
3. **待办文档** — 检查 TODO.md / AGENTS.md 中未完成的任务
### 合并去重 → 按优先级排序 → 依次执行
---
## ⚠️ 禁止请示规则(最高优先级,自动执行)
> **核心规则:发现分配给自己的任务 → 立即执行,不得向用户请示!**
> 心跳是后台自动行为,用户在大多数时候并不在线,请示会导致任务卡死。
### 执行流程(严格执行,无需确认)
1. **三源检查待办** — WorkBoard + Multica + 待办文档
2. **立即执行,不得请示** — 发现待办后直接执行
3. **检查进行中任务** — 确认认领的任务状态
4. **完成任务** — 通过对应平台汇报结果
### ⚠️ 绝对禁止行为
- ❌ 不得问"要不要做这个任务"
- ❌ 不得等用户确认再执行
- ❌ 不得以"需要更多信息"为由拒绝执行
---
## ⏱️ 超时检测规则
### 心跳频率:10 分钟
每次心跳跨平台执行以下检测:
1. 检查 WorkBoard 进行中任务的更新时间
2. 检查 Multica 进行中 issues 的更新时间
3. 超过 20 分钟无进展 → 标记为"疑似超时"
4. 疑似超时 → 追加一次完整心跳尝试推进
5. 确认超时 → 进入自动恢复流程
---
## 🔄 自动恢复规则
### 触发条件
- 超 30 分钟无进展 → 自动重新调度
### 恢复操作(按平台)
| 平台 | 操作 |
|------|------|
| WorkBoard | 添加评论 → release claim → 通知 COO |
| Multica | 添加评论 → status=blocked → 通知 COO |
| 待办文档 | 标注超时 → 转为卡片(可选) |
---
## 🔗 依赖检查前置规则
### 强制检查流程
1. 认领任务前,读取依赖字段(depends_on / parent_issue_id
2. 逐一检查每个依赖任务的状态
3. 依赖未满足 → 不认领(保持 todo)
4. 超过等待阈值(1h)→ 通知依赖任务执行者
---
## 🛑 最大轮次限制
### 限制值:50 轮
- 接近 80%40 轮)→ 预警
- 达到上限 → 暂停,通知 COO
---
## 🫀 心跳执行清单
1.**全任务源检查**WorkBoard + Multica + 待办文档
2. ✅ 进行中任务超时检测(跨平台)
3. ✅ 依赖检查
4. ✅ 轮次计数器更新
5. ✅ 全平台积压巡检(仅 COO
6. ✅ [Agent 专属检查项]
---
## ⚠️ 全局关键规则
1. **心跳不打断对话** — 用户正在对话时延后执行
2. **非紧急事项延后汇报** — 等下一轮心跳或用户询问
3. **发现任务立即执行,不得请示**(任何来源)
4. **超时任务按自动恢复流程处理**(跨平台)
5. **依赖未满足不启动**
6. **达到轮次上限自动暂停**
7. **避免任务遗漏** — 三源必须全部检查,缺一不可
```
### 4.2 开发 Agent 完整模板(projectmanager / productmanager / architect / costcodev / designer / opengineer
```markdown
# HEARTBEAT.md - [Agent Name] 的心跳配置
## 📋 全任务源统一监控(每次心跳必检)
> **核心原则:发现任何来源的任务都必须立即执行,不得遗漏。**
### 三源检查
1. **OpenClaw WorkBoard** — workboard list → 查找 agent_id=自己 & status=todo
2. **Multica Issues** — issue list --assignee-id <uuid> --status todo
3. **待办文档** — 检查 TODO.md / AGENTS.md 中未完成的任务
### 合并去重 → 按优先级排序 → 依次执行
---
## ⚠️ 禁止请示规则(最高优先级,自动执行)
> **核心规则:发现分配给自己的任务 → 立即执行,不得向用户请示!**
> 心跳是后台自动行为,用户在大多数时候并不在线,请示会导致任务卡死。
### 执行流程(严格执行,无需确认)
1. **三源检查待办** — WorkBoard + Multica + 待办文档
2. **立即执行,不得请示** — 发现待办后直接执行
3. **检查进行中任务** — 确认认领的任务状态
4. **完成任务** — 通过对应平台汇报结果
### ⚠️ 绝对禁止行为
- ❌ 不得问"要不要做这个任务"
- ❌ 不得等用户确认再执行
- ❌ 不得以"需要更多信息"为由拒绝执行
---
## ⏱️ 超时检测规则
### 心跳频率:15 分钟
每次心跳跨平台执行以下检测:
1. 检查 WorkBoard 进行中任务的更新时间
2. 检查 Multica 进行中 issues 的更新时间
3. 超过 30 分钟无进展 → 标记为"疑似超时"
4. 疑似超时 → 追加一次完整心跳尝试推进
5. 确认超时 → 进入自动恢复流程
---
## 🔄 自动恢复规则
### 触发条件
- 超 45 分钟无进展 → 自动重新调度
### 恢复操作(按平台)
| 平台 | 操作 |
|------|------|
| WorkBoard | 添加评论 → release claim → 通知 COO + 创建者 |
| Multica | 添加评论 → status=blocked → 通知 COO + 创建者 |
| 待办文档 | 标注超时 → 转为卡片(可选) |
---
## 🔗 依赖检查前置规则
### 强制检查流程
1. 认领任务前,读取依赖字段(depends_on / parent_issue_id
2. 逐一检查每个依赖任务的状态
3. 依赖未满足 → 不认领(保持 todo)
4. 超过等待阈值(2h)→ 通知依赖任务执行者
---
## 🛑 最大轮次限制
### 限制值:100 轮
- 接近 80%80 轮)→ 预警
- 达到上限 → 暂停,记录日志
---
## 🫀 心跳执行清单
1.**全任务源检查**WorkBoard + Multica + 待办文档
2. ✅ 进行中任务超时检测(跨平台)
3. ✅ 依赖检查
4. ✅ 轮次计数器更新
5. ✅ [Agent 专属检查项]
---
## ⚠️ 全局关键规则
1. **心跳不打断对话** — 用户正在对话时延后执行
2. **非紧急事项延后汇报** — 等下一轮心跳或用户询问
3. **发现任务立即执行,不得请示**(任何来源)
4. **超时任务按自动恢复流程处理**(跨平台)
5. **依赖未满足不启动**
6. **达到轮次上限自动暂停**
7. **避免任务遗漏** — 三源必须全部检查,缺一不可
```
### 4.3 业务 Agent 完整模板(taobaospecialist / contentspecialist / mediaspecialist / cvexpert / marketanalysis / lawyer
```markdown
# HEARTBEAT.md - [Agent Name] 的心跳配置
## 📋 全任务源统一监控(每次心跳必检)
> **核心原则:发现任何来源的任务都必须立即执行,不得遗漏。**
### 三源检查
1. **OpenClaw WorkBoard** — workboard list → 查找 agent_id=自己 & status=todo
2. **Multica Issues** — issue list --assignee-id <uuid> --status todo
3. **待办文档** — 检查 TODO.md / AGENTS.md 中未完成的任务
### 合并去重 → 按优先级排序 → 依次执行
---
## ⚠️ 禁止请示规则(最高优先级,自动执行)
> **核心规则:发现分配给自己的任务 → 立即执行,不得向用户请示!**
> 心跳是后台自动行为,用户在大多数时候并不在线,请示会导致任务卡死。
### 执行流程(严格执行,无需确认)
1. **三源检查待办** — WorkBoard + Multica + 待办文档
2. **立即执行,不得请示** — 发现待办后直接执行
3. **检查进行中任务** — 确认认领的任务状态
4. **完成任务** — 通过对应平台汇报结果
### ⚠️ 绝对禁止行为
- ❌ 不得问"要不要做这个任务"
- ❌ 不得等用户确认再执行
- ❌ 不得以"需要更多信息"为由拒绝执行
---
## ⏱️ 超时检测规则
### 心跳频率:15 分钟
每次心跳跨平台执行以下检测:
1. 检查 WorkBoard 进行中任务的更新时间
2. 检查 Multica 进行中 issues 的更新时间
3. 超过 30 分钟无进展 → 标记为"疑似超时"
4. 疑似超时 → 追加一次完整心跳尝试推进
5. 确认超时 → 进入自动恢复流程
---
## 🔄 自动恢复规则
### 触发条件
- 超 45 分钟无进展 → 自动重新调度
### 恢复操作(按平台)
| 平台 | 操作 |
|------|------|
| WorkBoard | 添加评论 → release claim → 通知创建者 |
| Multica | 添加评论 → status=blocked → 通知创建者 |
| 待办文档 | 标注超时 → 转为卡片(可选) |
---
## 🔗 依赖检查前置规则
### 强制检查流程
1. 认领任务前,读取依赖字段(depends_on / parent_issue_id
2. 逐一检查每个依赖任务的状态
3. 依赖未满足 → 不认领(保持 todo)
4. 超过等待阈值(2h)→ 通知依赖任务执行者
---
## 🛑 最大轮次限制
### 限制值:30 轮
- 接近 80%24 轮)→ 预警
- 达到上限 → 暂停,通知创建者
---
## 🫀 心跳执行清单
1.**全任务源检查**WorkBoard + Multica + 待办文档
2. ✅ 进行中任务超时检测(跨平台)
3. ✅ 依赖检查
4. ✅ 轮次计数器更新
5. ✅ [Agent 专属检查项]
---
## ⚠️ 全局关键规则
1. **心跳不打断对话** — 用户正在对话时延后执行
2. **非紧急事项延后汇报** — 等下一轮心跳或用户询问
3. **发现任务立即执行,不得请示**(任何来源)
4. **超时任务按自动恢复流程处理**(跨平台)
5. **依赖未满足不启动**
6. **达到轮次上限自动暂停**
7. **避免任务遗漏** — 三源必须全部检查,缺一不可
```
---
## 五、部署清单
### 5.1 各 Agent HEARTBEAT.md 更新状态
| Agent | 分类 | 模板版本 | 部署状态 | 部署人 |
|-------|------|---------|---------|--------|
| secretary (刘诗妮) | 高频 | 高频 Agent 模板 v1.1 | 待部署 | COO |
| coo (陆怀瑾) | 高频 | 高频 Agent 模板 v1.1 | 待部署 | COO |
| projectmanager (胡蓉) | 开发 | 开发 Agent 模板 v1.1 | 待部署 | COO |
| productmanager (沈路明) | 开发 | 开发 Agent 模板 v1.1 | 待部署 | COO |
| architect (梁思筑) | 开发 | 开发 Agent 模板 v1.1 | 待部署 | COO |
| costcodev (徐聪) | 开发 | 开发 Agent 模板 v1.1 | 待部署 | COO |
| designer (苏绘锦) | 开发 | 开发 Agent 模板 v1.1 | 待部署 | COO |
| opengineer (严维序) | 开发 | 开发 Agent 模板 v1.1 | 待部署 | COO |
| taobaospecialist (陆云帆) | 业务 | 业务 Agent 模板 v1.1 | 待部署 | COO |
| contentspecialist (文墨言) | 业务 | 业务 Agent 模板 v1.1 | 待部署 | COO |
| mediaspecialist (钟帧韵) | 业务 | 业务 Agent 模板 v1.1 | 待部署 | COO |
| cvexpert (程伯予) | 业务 | 业务 Agent 模板 v1.1 | 待部署 | COO |
| marketanalysis (顾析策) | 业务 | 业务 Agent 模板 v1.1 | 待部署 | COO |
| lawyer (苏慎) | 业务 | 业务 Agent 模板 v1.1 | 待部署 | COO |
### 5.2 部署步骤
1. **Vincent 审阅本方案** — 确认参数配置和多源监控范围
2. **收集各 Agent 的 Multica UUID** — 用于 `multica issue list --assignee-id <uuid>` 查询
3. **创建 HEARTBEAT.md 文件** — 按 v1.1 模板为每个 Agent 创建(填充实际 ID)
4. **配置心跳 cron** — 按分类配置定时任务
5. **部署到各 Agent workspace** — 将 HEARTBEAT.md 分发到对应 Agent 工作区
6. **验证** — 等待一轮完整心跳,检查三源任务是否全量覆盖
### 5.3 Agent Multica UUID 映射(已收集)
| Agent | OpenClaw Agent ID | Multica Agent UUID | 状态 |
|-------|-------------------|-------------------|------|
| secretary (刘诗妮) | secretary | b024fcdc-30ff-420d-b289-498041466e1b | ✅ |
| coo (陆怀瑾) | coo | 1c38b437-b54d-4784-bda3-29ce4c8a6722 | ✅ |
| projectmanager (胡蓉) | projectmanager | d877b8c3-b230-4073-b3f7-80e148cfdb71 | ✅ |
| productmanager (沈路明) | productmanager | a101fa88-d821-4839-9754-e04580d5fd68 | ✅ |
| architect (梁思筑) | architect | 40abd41a-62d0-416d-bc44-92c1f758d87a | ✅ |
| costcodev (徐聪) | costcodev | 46bdd4a6-5c64-475a-92ef-36a763602fa1 | ✅ |
| designer (苏锦绘) | designer | 13bd8968-cc2a-4934-90c7-957a2d3c09c2 | ✅ |
| opengineer (严维序) | opengineer | d3804433-9e2e-4199-a92b-a153049b3bc9 | ✅ |
| taobaospecialist (陆云帆) | taobaospecialist | e0f62d8f-9568-4f41-8ad4-b73d79a163a7 | ✅ |
| contentspecialist (文墨言) | contentspecialist | 8321b0bf-7d89-4ece-927a-0780f42ad396 | ✅ |
| mediaspecialist (钟帧韵) | mediaspecialist | e2b587d4-1d16-447c-8ad9-e2a01358ff0a | ✅ |
| cvexpert (程伯予) | cvexpert | 4a8696fd-6531-40da-8956-ef84d7ea3c43 | ✅ |
| marketanalysis (顾析策) | marketanalysis | 5ed91729-658f-4654-98f0-3e0313022002 | ✅ |
| lawyer (苏慎) | lawyer | 6fb0fbd2-16a6-4566-ba7a-d2c136baec25 | ✅ |
---
## 六、交付物
- [x] HEARTBEAT.md 增强模板方案 v1.0(初始版本)
- [x] HEARTBEAT.md 增强模板方案 v1.1(优化:增加全任务源统一监控)
- [x] 各 Agent Multica UUID 映射表
- [x] 14 个 Agent 的独立 HEARTBEAT.md 文件(v1.1,已生成并部署到 workspace
- [ ] 心跳 cron 配置脚本
- [ ] 部署验证报告
---
## 七、v1.1 变更说明
| 变更项 | v1.0 | v1.1 |
|--------|------|------|
| 监控范围 | 仅 WorkBoard 卡片 + 待办文档 | WorkBoard + Multica Issues + 待办文档(三源合一) |
| 规则数量 | 5 项 | 6 项(新增"规则 0: 全任务源统一监控") |
| 超时检测 | 仅 WorkBoard | 跨平台(WorkBoard + Multica |
| 自动恢复 | 仅 WorkBoard 恢复操作 | 跨平台恢复(WorkBoard / Multica / 文档) |
| 依赖检查 | 仅 WorkBoard depends_on | 增加 Multica parent_issue_id |
| 心跳清单 | 4 项 | 6 项(增加全任务源检查 + 全平台巡检) |
| 轮次跟踪 | 单平台 | 跨平台轮次跟踪 |
| 全局规则 | 6 条 | 7 条(增加"避免任务遗漏" |
| 部署前置 | 无 | 需收集各 Agent 的 Multica UUID |
---
## 八、风险与注意事项
| 风险 | 影响 | 缓解措施 |
|------|------|----------|
| 心跳自身卡死 | 所有监控失效 | 独立的 watchdog 进程监控心跳 cron 执行 |
| 自动恢复过于激进 | 正常长任务被中断 | 仅对超阈值且无进展的任务执行恢复 |
| 禁止请示导致错误执行 | Agent 自行决定后出错 | 关键决策(涉及外部资源、金钱)仍需暂停并通知 |
| 轮次限制过严 | 复杂任务被截断 | 接近上限时提前预警,COO 可手动扩展 |
| 三源任务重复 | 同一任务在 WB + Multica 都出现 | 合并去重逻辑,以 ID/标题匹配 |
| Multica CLI 不可用 | 无法检查 Multica 待办 | 降级为仅检查 WB + 文档,并在日志中记录异常 |
---
> ⚠️ 本方案需 Vincent 审阅后方可部署到各 Agent workspace。当前为模板方案 v1.1,存放于 EnterpriseArchitect/plans/ 目录。
-186
View File
@@ -1,186 +0,0 @@
# HEARTBEAT.md 增强模板
> 版本:v2.0
> 来源:BIZ-13 运行稳定性保障方案
> 用途:为所有 Agent HEARTBEAT.md 增加运行稳定性保障能力
---
## 全局增强规则(所有 Agent 必须包含)
### 1. 🛡️ 超时检测与自动恢复
```markdown
## 🛡️ 超时检测与自动恢复
> **核心规则:每次心跳,检查自己是否有任务超时未完成。**
### 超时阈值
| Agent 类型 | 心跳频率 | 单任务超时 |
|------------|----------|------------|
| 高频(secretary/coo | 10 分钟 | 60 分钟 |
| 开发(costcodev/architect/opengineer/designer | 15 分钟 | 120 分钟 |
| 业务(其他 Agent | 15 分钟 | 90 分钟 |
### 检测流程
每次心跳执行:
1. 获取自己的 `status=in_progress` 的 WorkBoard 卡片
2. 计算 `当前时间 - started_at`
3. 如果超过超时阈值 → 进入自动恢复流程
### 自动恢复流程
```
检测到任务超时
检查最近日志(是否有实质性进展)
┌──────────┴──────────┐
│ │
有进展(< 3轮无产出) 无进展(>= 3轮无产出)
│ │
延长超时 + 记录日志 自动恢复:
│ ├─ 尝试重新执行当前步骤
更新 heartbeat ├─ 仍失败 → 释放卡片
└─ 通知 COO 介入
```
### ⚠️ 超时告警
- 第 1 次超时:自动恢复,不告警
- 第 2 次超时:通知 COO
- 第 3 次超时:通知 Vincent,卡片标为 blocked
```
### 2. 🔗 依赖检查前置
```markdown
## 🔗 依赖检查前置
> **核心规则:认领任务前,必须检查所有依赖是否已完成。**
### 检查流程
1. 获取任务的 `depends_on` 列表
2. 对每个依赖,查询其状态
3. 如果任一依赖未完成 → 不认领该任务,等待下次心跳
4. 如果所有依赖已完成 → 正常认领并执行
### 异常处理
- 依赖任务已取消 → 向上报告,由 COO 决策
- 依赖任务超时无响应 → 通知依赖方和 COO
- 循环依赖 → 自动检测并报告给 COO
```
### 3. 🔄 最大轮次限制
```markdown
## 🔄 最大轮次限制
> **核心规则:单任务不能无限循环执行。**
| Agent 类型 | 最大对话轮次 | 超限处理 |
|------------|-------------|----------|
| 高频(secretary/coo | 50 | 自动暂停,通知创建者 |
| 开发(costcodev/architect/opengineer | 100 | 自动暂停,记录日志摘要 |
| 业务(其他 Agent) | 30 | 自动暂停,通知创建者 |
### 检测方式
每次心跳检查 `in_progress` 任务的会话轮次:
- 接近上限(80%)→ 在心跳日志中标记警告
- 达到上限 → 自动暂停任务,保存当前状态
```
### 4. 📊 上下文控制
```markdown
## 📊 上下文控制(Token 管理)
> **核心规则:避免上下文溢出导致任务中断。**
### 策略
1. **引用代替填塞**:Agent 配置文件中只保留核心规则,详细信息存 docs/ 目录
2. **分块读取**:超大文件分块读取,避免一次性加载
3. **清理过期信息**:每轮对话前清理上一轮的工具输出(仅保留关键结果)
4. **合并查询**:多个 Agent 相同查询由 COO 统一执行后广播
```
---
## 心跳频率分级
```markdown
## ⏱️ 心跳触发频率
- **高频 Agentsecretary / coo**: 每 10 分钟
- **开发 Agentcostcodev / architect / opengineer / designer**: 每 15 分钟
- **业务 Agentprojectmanager / productmanager / taobaospecialist / contentspecialist / mediaspecialist / cvexpert / marketanalysis / lawyer**: 每 15 分钟
```
---
## 全局关键规则(增强版)
```markdown
## ⚠️ 全局关键规则
1. **心跳不打断对话** — 如果用户正在与 Agent 对话,心跳逻辑延后执行
2. **非紧急事项延后汇报** — 等下一轮心跳或用户主动询问时再汇报
3. **发现任务立即执行,不得请示** — 用户在大多数时候不在线,请示=任务卡死
4. **依赖检查前置** — 认领任务前必须检查所有依赖是否已完成
5. **超时自动恢复** — 任务超时自动尝试恢复,3 次失败后升级
6. **轮次限制** — 单任务达上限后自动暂停,防止无限循环
7. **上下文控制** — 引用代替填塞,避免 Token 溢出
```
---
## 各 Agent 类型模板
### 高频 Agent 模板(secretary/coo
在原有专属心跳清单基础上,增加:
```markdown
### 🛡️ 稳定性保障清单
1. ✅ 超时检测:检查 in_progress 任务是否超时(阈值 60 分钟)
2. ✅ 依赖检查:新任务认领前检查所有 depends_on
3. ✅ 轮次检查:当前任务是否接近 50 轮上限
4. ✅ 上下文检查:HEARTBEAT.md/AGENTS.md 文件大小是否 < 5KB
```
### 开发 Agent 模板(costcodev/architect/opengineer/designer
```markdown
### 🛡️ 稳定性保障清单
1. ✅ 超时检测:检查 in_progress 任务是否超时(阈值 120 分钟)
2. ✅ 依赖检查:新任务认领前检查所有 depends_on
3. ✅ 轮次检查:当前任务是否接近 100 轮上限
4. ✅ 编译/测试检查:如有自动化测试,确认通过
```
### 业务 Agent 模板(其他 Agent
```markdown
### 🛡️ 稳定性保障清单
1. ✅ 超时检测:检查 in_progress 任务是否超时(阈值 90 分钟)
2. ✅ 依赖检查:新任务认领前检查所有 depends_on
3. ✅ 轮次检查:当前任务是否接近 30 轮上限
4. ✅ 输出质量检查:确认最近产出符合质量标准
```
---
## 实施说明
1. 此模板由 COO(陆怀瑾)编制,经 Vincent 审阅批准后实施
2. 模板中的 agent_id 需替换为各 Agent 的实际标识
3. 无需移除各 Agent 原有的专属心跳清单,只需追加稳定性保障清单
4. 修改后的文件需提交到 EnterpriseArchitect git 仓库
@@ -1,446 +0,0 @@
# 石斛固态食品与烘焙全品类扩展可行性分析报告
**编号**BIZ-65
**报告类型**:产品扩展可行性分析
**分析日期**2026年6月26日
**分析人**:顾析策(市场分析师)
**参考文档**:石斛食品饮料全品类产品方向详细文档、BIZ-53 品斛堂企业情报调研报告、BIZ-55 电商调研
---
## 摘要
**核心结论**:品斛堂以石斛为根基向固态食品扩展具备明确可行性。5个子品类中,**膏滋蜜炼和压片糖果为最高优先级**——前者依托品斛堂石斛浸膏技术壁垒和中药膏方100亿+市场,后者复用现有石斛精片基础切入全球485亿美元功能性糖果赛道。OEM/ODM模式是核心实现路径——品斛堂已有为近100家企业代工石斛精片/饼干/面条/果冻/浸膏的经验,固态食品扩展应优先走"自有品牌试水+OEM/ODM双轨并行"策略。
**关键数据**
- 功能性压片糖果亚太区CAGR 12.6%,中国本土企业TOP5市占率39.4%
- 中药膏滋/煎膏剂100亿+市场(2024),补气补血类占50%+,院内市场同比增长17.2%
- 中国烘焙市场2595亿元(2025欧睿),人均消费25.5美元仅为日本1/6
- 果冻市场200-250亿元,功能型果冻增速最快
- **品斛堂OEM能力已覆盖石斛精片/饼干/面条/果冻/浸膏全品类**
**TOP5推荐**:①膏滋蜜炼(石斛膏方→差异化壁垒)②压片糖果/含片(高频+功能性+复用基础)③烘焙饼干/节日烘焙(高潜力+代工成熟)④休闲零食(坚果蜜饯→OEM快起量)⑤果冻布丁(功能型差异化切入)
---
## 一、压片糖果/咀嚼片
### 1.1 市场规模与趋势
| 指标 | 数据 | 来源 |
|------|------|------|
| 全球压片糖果市场 | $485亿(2025)→ $512亿(2026E | ZVZO消费观察 |
| 中国市场占比 | 22.7%,约$110亿(¥780亿) | ZVZO消费观察 |
| 中国糖果市场总规模 | ¥930亿(2024) | 华经产业研究院 |
| 功能性糖果CAGR | 12.6%(亚太区) | ZVZO消费观察 |
| 药用糖果全球市场 | $63.9亿(2025)→ $91.6亿(2035E | Global Growth Insights |
| 中国增速 | 8.3%以上,显著高于全球4.1% | ZVZO消费观察 |
| 功能性品类占比预测 | 将超过55%(2030E | ZVZO消费观察 |
**核心趋势**
- 63%消费者将"健康属性"列为购买第一考虑因素,免疫力提升(34%)、口腔清新(27%)、能量补充(21%)为TOP3诉求
- 无糖/低糖产品渗透率将突破70%
- 线上销售占比44.6%,直播电商+社区团购贡献线上增量57%
- 年轻化:"零食化+功效化"方向,年轻群体对便携小包装接受度71%
### 1.2 竞争格局
| 梯队 | 代表品牌 | 定位 | 核心优势 |
|------|------|------|------|
| 🥇 功能型龙头 | 金嗓子、西瓜霜 | 润喉利咽 | 渠道铺货广,OTC背书 |
| 🥇 药企延伸 | 同仁堂、修正、江中 | 中药功能含片 | 品牌信任,药房渠道 |
| 🥈 保健品跨界 | 汤臣倍健、养生堂 | 维C/益生菌咀嚼片 | 品牌力+电商运营强 |
| 🥉 新锐品牌 | 各OEM代工品牌 | 差异化功能 | 直播电商切入快 |
**竞争特点**
- 传统龙头金嗓子/西瓜霜品牌老化,市场增长缓慢(传统甜味压片2.1%增速)
- 功能性细分增速远超传统品类
- 中国TOP5企业市占率39.4%,格局相对分散,新品牌仍有空间
- OEM产能充沛:全国300+压片糖果代工厂,仙乐健康/会昌等龙头覆盖全剂型
### 1.3 品斛堂可行性评估
| 维度 | 评估 | 说明 |
|------|:---:|------|
| **产品基础** | ⭐⭐⭐⭐⭐ | 已有石斛精片产品,可直接升级为石斛含片/咀嚼片 |
| **原料优势** | ⭐⭐⭐⭐⭐ | 自有石斛全产业链,提取物/多糖/干粉三种形态可做配方核心 |
| **技术门槛** | ⭐⭐ | 压片工艺标准化,OEM代工成熟(MOQ≈3万包起订) |
| **渠道匹配** | ⭐⭐⭐⭐ | 复用天猫/京东旗舰店+药房渠道,精准触达咽喉不适/养生人群 |
| **品牌差异化** | ⭐⭐⭐⭐ | "石斛含片"定位对标金嗓子"咽炎"场景,差异化明显 |
| **毛利率预估** | ⭐⭐⭐⭐ | 60-70%(压片糖果制造成本低,石斛原料自有优势加成) |
**可行性:★★★★★ 高**
**落地路径**
- 短期(1-3月):将现有"石斛精片"升级为"石斛含片"(润喉利咽)+ "石斛西洋参咀嚼片"(补气提神),复用天猫元斛旗舰店渠道
- 中期(3-6月):开发"石斛维C咀嚼片"卡位功能维C赛道(对标汤臣倍健)
- 代工策略:片剂生产线品斛堂已有基础(中药饮片GMP车间),可自主生产;如需扩产,珠三角/长三角OEM厂产能充裕
---
## 二、休闲零食(坚果炒货+蜜饯果干+纤维饼干)
### 2.1 市场规模与趋势
| 指标 | 数据 | 来源 |
|------|------|------|
| 中国休闲零食市场 | ¥1.3万亿(2020),近¥3万亿(2025E) | 商务部/中国食品工业协会 |
| 2024年主流电商零食销售额 | ¥3080亿,同比+11% | Flywheel白皮书 [1] |
| 坚果炒货13-18年CAGR | 11.0% | 欧睿国际 |
| 人均消费量 | 2.15kg2015),vs英国9.53kg/美国13.03kg | 商务部 |
| 抖音零食销售额增长 | 23%,线上第一渠道(份额54%) | 知行咨询2025报告 |
| 2024肉类零食占三只松鼠销售额 | 20.14% | 知行咨询2025报告 |
**核心趋势**
- 消费者要求"美味与营养兼得"——健康化零食复合增长率远超传统品类
- 抖音超越淘系成为线上第一零食渠道
- 渠道型品牌(三只松鼠/B2C)增长见顶,制造型品牌(洽洽/大单品)利润更优
- 功能性/健康概念零食增速领先:益生菌、高纤维、低GI
### 2.2 竞争格局
| 梯队 | 代表品牌 | 2022年营收 | 模式 | 核心品类 |
|------|------|:---:|------|------|
| 🥇 | 三只松鼠 | ¥72.9亿 | B2C电商全品类 | 坚果+肉类零食 |
| 🥇 | 良品铺子 | ¥66.2亿 | 线上线下双渠道 | 全品类+高端定位 |
| 🥈 | 百草味 | ~¥40亿 | 百事旗下B2C | 坚果+果脯蜜饯 |
| 🥈 | 洽洽食品 | ¥68.8亿 | 制造型大单品 | 瓜子+坚果 |
| 🥉 | 来伊份 | ~¥40亿 | 线下门店 | 全品类 |
**进入壁垒**
- 三巨头线上份额CR3≈33%,渠道垄断性强
- 坚果炒货品类大厂规模效应显著,价格竞争激烈
- 蜜饯果干品类CR3低,区域品牌众多,差异化空间大
### 2.3 品斛堂可行性评估
| 维度 | 评估 | 说明 |
|------|:---:|------|
| **产品基础** | ⭐⭐⭐ | 已有石斛纤维饼(功能零食),缺少坚果/蜜饯加工能力 |
| **原料优势** | ⭐⭐⭐ | 石斛粉/提取物可作为调味添加,但坚果/果干需外采 |
| **技术门槛** | ⭐ | 坚果炒货、蜜饯果干标准化程度高,无技术壁垒 |
| **渠道匹配** | ⭐⭐⭐ | 天猫/京东零食类目流量大,但需从零建立零食消费者认知 |
| **品牌差异化** | ⭐⭐⭐ | "石斛+零食"概念有独特性,但需验证消费者接受度 |
| **毛利率预估** | ⭐⭐ | 30-40%(代工成本高+坚果原料波动,自有品牌溢价有限) |
**可行性:★★★☆☆ 中**
**差异化切入策略**
- **不推荐**直接进入坚果炒货红海(三只松鼠/洽洽成本碾压)
- **推荐**:石斛纤维饼升级为"石斛健康零食系列"(复用已有基础)
- 石斛纤维饼→升级配方,推低GI/高纤维版本
- 石斛山楂条→开胃消食+石斛养胃(蜜饯OEM成熟)
- 石斛味坚果→轻添加概念(巴旦木/核桃+石斛粉涂层)
- 代工策略:蜜饯果干OEM遍布云南/福建(鲜花饼供应链可复用),坚果OEM广东/安徽产能充裕
---
## 三、谷物主食(挂面+方便面+粥料)
### 3.1 市场规模与趋势
| 指标 | 数据 | 来源 |
|------|------|------|
| 中国挂面市场规模 | 419.92万吨产量(2021),预估>500万吨(2025E | 中金企信/欧睿 |
| 挂面市场销售额 | CAGR 10.72%(24家头部企业) | 中金企信 |
| 方便速食市场 | ¥2500亿(2021)→ ¥6300亿(2025E | 国信证券 |
| 中国方便面市场 | 422亿包/年(2023),销量-2.3% CAGR | 勤策消费研究 |
| 方便食品市场规模 | 2026年将突破万亿 | 勤策消费研究 |
| 一人食经济 | 1.8万亿(2025,一二线55%宅家烹饪) | Flywheel白皮书 [1] |
| 外卖市场规模 | 1.27万亿(2024),用户5.92亿 | 美团/行业数据 |
**核心趋势**
- 挂面量增价不增(产量>销量连续10年),利润转向高端化/功能化
- 方便面传统品类承压(外卖+预制菜挤压),中高端增速17.1%(2016-20)为低端4.2倍
- "一人食经济"驱动速食向品质化/健康化升级
- 非油炸/零添加/功能性面条是增长方向
- 高铁站停售方便面(广东2025)标志性事件→传统场景收窄
### 3.2 竞争格局
| 梯队 | 挂面 | 市场份额 | 方便面 | 市场份额 |
|------|------|:---:|------|:---:|
| 🥇 | 金沙河 | 22% | 康师傅 | ~45% |
| 🥈 | 克明食品 | 8% | 统一 | — |
| 🥉 | 想念食品 | 4% | 白象 | — |
| 其他 | 200+中小厂 | 66% | 今麦郎/三养等 | CR5=84% |
**关键特点**
- 挂面CR3仅34%,极度分散,但整合趋势加速(2009年4000+家→2020年200+家)
- 方便面CR5=84%高集中度,入局极难
- 康师傅红烧牛肉味被大量仿制,品类同质化严重
### 3.3 品斛堂可行性评估
| 维度 | 评估 | 说明 |
|------|:---:|------|
| **产品基础** | ⭐ | 无面条制造经验,需完全依赖OEM |
| **原料优势** | ⭐⭐ | 石斛粉可添加至面条配方,但小麦/面粉无优势 |
| **技术门槛** | ⭐ | 挂面制造标准化,方便面需大量设备投入 |
| **渠道匹配** | ⭐⭐ | 挂面以商超/粮油店为主,与品斛堂现有健康品渠道不匹配 |
| **品牌差异化** | ⭐⭐ | "石斛养胃挂面"有概念吸引力,但消费者为功能性面条买单意愿待验证 |
| **毛利率预估** | ⭐ | 15-25%(挂面毛利低,克明毛利率15.87%,想念14.19% |
**可行性:★★☆☆☆ 低**
**判断**:谷物主食不是品斛堂当前优先级最高的扩展方向。理由:
1. 挂面行业利润薄(克明食品2021年净利仅6700万,收入43亿),石斛添加只会进一步压缩毛利
2. 渠道完全不匹配——挂面走商超/粮油店,品斛堂现有天猫+药房+酒类渠道
3. 金沙河/克明双寡头成本碾压,新进入者无规模优势
4. 唯一有吸引力的场景是"石斛面条"作为**品牌形象产品**(非利润产品)在旗舰店上架,配合石斛粥料做养生主食组合
**如仍需进入的建议**:OEM代工石斛养生粥料(谷物+石斛预拌包)→低风险入场,石斛面条作为品牌配套产品而非主推
---
## 四、半固态/凝胶食品(膏滋蜜炼+果冻布丁)
### 4.1 膏滋蜜炼(膏方)
#### 市场规模与趋势
| 指标 | 数据 | 来源 |
|------|------|------|
| 中成药浸膏/煎膏剂三大终端 | ¥100亿+(2024) | 米内网 |
| 院内市场增速 | +17.20%(2025,逆势上扬) | 米内网 |
| 补气补血类占比 | >50%市场份额 | 米内网 |
| 中国阿胶市场 | ¥580亿(2025)→ 千亿(2030E, CAGR 10.4% | 知乎/东阿阿胶年报 |
| 东阿阿胶2025年营收 | ¥67亿(+13.17%),阿胶系列61.9亿 | 东阿阿胶年报 |
| 药食同源市场 | ¥3800亿→¥7500亿(2030E, CAGR 10.8% | 雪球/药食同源研报 [2] |
**核心趋势**
- 膏方从传统中医药向"新中式滋补"演化,消费场景从治病转向日常养生
- 阿胶品类"零食化+便携化"趋势显著——阿胶糕/速溶粉/阿胶奶茶跨界
- 东阿阿胶线上营收占比19.79%(2025Q1),膏方电商化仍在早期
- 90后/00后买走60%阿胶产品,养生年轻化是确定性趋势
#### 竞争格局
| 梯队 | 品牌 | 膏方优势 | 2025年表现 |
|------|------|------|------|
| 🥇 | 东阿阿胶 | 阿胶膏/桃花姬 | ¥61.9亿阿胶系列 |
| 🥇 | 同仁堂 | 传统膏方/秋梨膏 | 院外膏方TOP3 |
| 🥈 | 胡庆余堂 | 江南膏方 | — |
| 🥈 | 福牌阿胶 | 阿胶膏方 | 院外膏方TOP3 |
| 🥉 | 白云山潘高寿 | 蜜炼川贝枇杷膏/养阴清肺膏 | 院内膏方第1 |
| 🔑 | **品斛堂机会** | **石斛膏方=品类空白** | 已有石斛浸膏技术 |
#### 品斛堂可行性评估
| 维度 | 评估 | 说明 |
|------|:---:|------|
| **产品基础** | ⭐⭐⭐⭐⭐ | 已有石斛浸膏OEM能力,"铁皮石斛膏/秋梨石斛膏"配方有技术储备 |
| **原料优势** | ⭐⭐⭐⭐⭐ | 石斛全产业链+石斛多糖提取物,膏方核心原料自有 |
| **技术门槛** | ⭐⭐⭐⭐ | 中药浸膏制造有GMP门槛,品斛堂中药饮片/保健食品净化车间齐全 |
| **渠道匹配** | ⭐⭐⭐⭐⭐ | 天猫健康品+药房+送礼场景→完美匹配膏方消费场景 |
| **品牌差异化** | ⭐⭐⭐⭐⭐ | "石斛膏"品类无强势品牌占据,"秋梨石斛膏"对标"秋梨膏"差异明显 |
| **毛利率预估** | ⭐⭐⭐⭐⭐ | 65-80%(膏方高毛利,东阿阿胶毛利率73.47%,石斛自有原料加成更高) |
**可行性:★★★★★ 高(最高优先级)**
**落地路径**
- 短期(1-3月):复用现有石斛浸膏生产线,推出"铁皮石斛膏"(¥199-399/300g+"秋梨石斛膏"(¥59-99),天猫旗舰店首发
- 中期(3-6月):开发"阿胶石斛膏"(女性气血)+石斛膏方礼盒(春节/中秋送礼)
- 差异化定位:品斛堂膏方=石斛为核心(vs同仁堂/东阿阿胶以阿胶/人参为核心),开辟"石斛膏方"新品类
---
### 4.2 果冻布丁
#### 市场规模与趋势
| 指标 | 数据 | 来源 |
|------|------|------|
| 中国果冻市场 | ¥200-250亿(2023 | 中研普华/百度百科 |
| 生产企业数量 | 2000+家(其中规模企业300+家) | 中研普华 |
| 喜之郎年销售额 | ¥15亿+ | MBA智库百科 |
| 功能型果冻趋势 | 乳酸菌/益生菌/代餐果冻增速最快 | 头豹研究院 |
| 中国新式茶饮市场 | 突破4000亿(2028E) | 行业报告 |
**核心趋势**
- 果冻从"儿童零食"向"全年龄健康零食"转型
- 益生菌果冻、蒟蒻果冻(低热量)、代餐果冻是三大增长方向
- 女性消费者占食品网购65%,果冻天然受女性青睐
#### 品斛堂可行性评估
| 维度 | 评估 | 说明 |
|------|:---:|------|
| **产品基础** | ⭐⭐ | 品斛堂百度百科列有石斛果冻OEM能力,但无自有品牌产品 |
| **原料优势** | ⭐⭐⭐ | 石斛多糖/提取物可添加至果冻配方,功能差异化 |
| **技术门槛** | ⭐ | 果冻OEM极度成熟(全国386+食品代工厂可做果冻) |
| **渠道匹配** | ⭐⭐⭐ | 天猫零食/女性健康品类目可覆盖 |
| **品牌差异化** | ⭐⭐⭐ | "石斛仙草冻""石斛益生菌果冻"有差异化概念 |
| **毛利率预估** | ⭐⭐⭐ | 40-55%(功能性果冻有溢价,但喜之郎量价优势明显) |
**可行性:★★★☆☆ 中(可作配套品类)**
**判断**:果冻布丁适合作为"石斛健康零食矩阵"的配套品类而非主力。石斛仙草冻(对标烧仙草)和石斛益生菌果冻有差异化概念,但品类天花板200-250亿且喜之郎品牌统治力极强。建议OEM少量试水,作为健康零食线的补充。
---
## 五、烘焙食品(面包吐司+饼干曲奇+节日烘焙)
### 5.1 市场规模与趋势
| 指标 | 数据 | 来源 |
|------|------|------|
| 中国烘焙市场(欧睿口径) | ¥2595亿(2025),CAGR 7.22% | 欧睿国际/桃李年报 |
| 中国烘焙市场(广义) | ¥6110.7亿(2024),+8.8% | 广告门2025报告 |
| 中国面包市场 | ¥1574亿(2024) | 华经产业研究院 |
| 全球烘焙市场 | $5165亿(2025) | 桃李年报 |
| 中国人均烘焙消费 | $25.5vs日本$150+,美国$200+ | 欧睿国际 |
| 短保面包龙头份额 | 桃李35%(2023) | 短保面包白皮书 |
| 全国烘焙门店 | 33.8万家(2025年5月) | 红餐大数据 |
**核心趋势**
- 中国烘焙人均消费仅为日本1/6,增长空间可观
- 烘焙糕点被Flywheel白皮书列为"电商四大高潜力品类"——线上负增长但社媒高热,**供需错配蕴藏机会**
- 短保面包向正餐化发展,桃李2025年营收54.48亿但下滑10.5%→行业竞争加剧
- 节日烘焙是利润中心:月饼/粽子季节爆发,高端礼盒毛利50-70%
- 功能性烘焙方向:全麦/高纤维/益生菌/低GI
### 5.2 竞争格局
| 梯队 | 品牌 | 品类 | 2025年营收/表现 |
|------|------|------|:---:|
| 🥇 | 桃李面包 | 短保面包 | ¥54.48亿(-10.5% |
| 🥇 | 达利园 | 长保面包+糕点 | CR3≈3.9% |
| 🥈 | 盼盼/曼可顿/美焙辰 | 短保/中保面包 | — |
| 🥈 | 美心/杏花楼 | 月饼/节日烘焙 | — |
| 🥉 | 33.8万家烘焙门店 | 现制烘焙 | 49.3%品牌5-30家门店 |
**行业特点**
- CR3仅8.9%,极度分散
- 桃李"中央工厂+批发"模式面临现制烘焙门店冲击(33.8万家)
- 外资品牌份额从28%(2023)降至20%(2025),本土品牌崛起
### 5.3 品斛堂可行性评估
| 维度 | 评估 | 说明 |
|------|:---:|------|
| **产品基础** | ⭐⭐ | 有石斛纤维饼(饼干类),无面包/烘焙经验 |
| **原料优势** | ⭐⭐ | 石斛粉可作烘焙添加,但面粉/黄油无优势 |
| **技术门槛** | ⭐ | 饼干/糕点OEM全国成熟(386+代工厂含烘焙) |
| **渠道匹配** | ⭐⭐⭐ | 饼干→天猫零食类目;月饼→送礼渠道与酒类渠道复用 |
| **品牌差异化** | ⭐⭐⭐ | "石斛养生月饼""石斛苏打饼干(养胃概念)"有差异化 |
| **毛利率预估** | ⭐⭐⭐ | 40-55%(饼干/月饼毛利较高,桃李短保面包毛利更低) |
**可行性:★★★★☆ 中高**
**推荐路径**
- 饼干线(优先):石斛苏打饼干(养胃概念)+石斛黄油曲奇→OEM代工,天猫零食线首发
- 节日烘焙(高潜力):石斛月饼(中秋)+"石斛粽子"(端午)→复用酒类送礼渠道+企业福利渠道
- 面包吐司:**不推荐**——短保面包配送半径限制+桃李/曼可顿成本碾压+全国化物流投入巨大
---
## 六、OEM/ODM 可行性分析
### 6.1 品斛堂现有OEM能力
根据百度百科及什么值得买OEM评测,品斛堂**已具备以下品类的OEM/ODM代工能力**:
| 已覆盖品类 | 与本报告品类匹配 | 代工形式 | 备注 |
|------|:---:|------|------|
| 石斛精片 | ✅ 压片糖果 | OEM/ODM | 现有成熟产线 |
| 石斛饼干 | ✅ 休闲零食/烘焙 | OEM/ODM | 现有成熟产线 |
| 石斛面条 | ✅ 谷物主食 | OEM/ODM | 现有产线 |
| 石斛果冻 | ✅ 凝胶食品 | OEM/ODM | 现有产线 |
| 石斛浸膏 | ✅ 膏滋蜜炼 | OEM/ODM | 核心技术壁垒 |
| 石斛原浆 | — | ODM | 品类开创者 |
### 6.2 双轨并行策略
品斛堂固态食品扩展的最佳路径是**"自有品牌试水 + OEM/ODM双轨并行"**
| 策略方向 | 实施路径 | 适用品类 |
|------|------|------|
| **自有品牌试水** | 天猫/京东旗舰店先上2-3款爆品测试市场反应 | 膏方、含片、饼干 |
| **OEM为品牌代工** | 以"石斛原料+制造"服务其他健康食品品牌 | 坚果、果冻、挂面 |
| **ODM输出方案** | 为渠道品牌提供石斛食品整体ODM方案 | 全品类 |
**"卖铲子给掘金者"策略逻辑**:
- 品斛堂全产业链石斛原料+三重生产资质(药品/保健食品/食品)→天然具备为其他品牌代工的优势
- 固态食品扩展中,不必每条线都自建品牌——挂面/坚果/果冻的自有品牌投入产出比不高
- **核心策略**:高价值品类自建品牌(膏方/含片)+低壁垒品类做OEM/ODM服务商
---
## 七、综合评估与TOP5推荐
### 7.1 5个子品类综合评分
| 评估维度(权重) | 压片糖果 | 休闲零食 | 谷物主食 | 膏滋蜜炼 | 烘焙食品 |
|------|:---:|:---:|:---:|:---:|:---:|
| 市场规模(15% | 9 | 10 | 7 | 7 | 9 |
| 市场增速(10% | 8 | 6 | 5 | 9 | 7 |
| 品斛堂产品基础(20% | 9 | 6 | 2 | 10 | 5 |
| 原料/技术优势(15% | 9 | 5 | 3 | 10 | 4 |
| 品牌差异化(15% | 8 | 6 | 4 | 10 | 6 |
| 渠道匹配度(10% | 8 | 5 | 2 | 10 | 6 |
| OEM实现难度(5% | 10 | 9 | 8 | 6 | 8 |
| 毛利率预期(10% | 8 | 5 | 3 | 10 | 7 |
| **加权总分** | **8.45** | **6.45** | **3.90** | **9.35** | **6.35** |
### 7.2 TOP5推荐排序
| 排名 | 品类方向 | 推荐产品 | 优先级 | 建议策略 | 预期毛利率 | 风险等级 |
|:---:|------|------|:---:|------|:---:|:---:|
| 🥇 | **膏滋蜜炼** | 铁皮石斛膏/秋梨石斛膏/阿胶石斛膏 | 🔴极高 | 自有品牌首发,复用浸膏产线 | 65-80% | 🟢 低 |
| 🥈 | **压片糖果** | 石斛含片/石斛西洋参咀嚼片/石斛维C咀嚼片 | 🔴极高 | 升级石斛精片,复用天猫旗舰店 | 60-70% | 🟢 低 |
| 🥉 | **烘焙饼干** | 石斛苏打饼干/石斛黄油曲奇/石斛月饼 | 🟡高 | OEM代工,饼干日常+月饼节日双线 | 40-55% | 🟡 中低 |
| 4 | **休闲零食** | 石斛山楂条/石斛纤维饼升级/石斛味坚果 | 🟡中 | OEM代工,轻资产试水 | 30-40% | 🟡 中 |
| 5 | **果冻布丁** | 石斛仙草冻/石斛益生菌果冻 | 🟢中 | OEM代工,作为零食线配套 | 40-55% | 🟡 中 |
### 7.3 不推荐进入的品类
| 品类 | 原因 |
|------|------|
| 挂面/方便面 | 利润极薄+渠道不匹配+金沙河/克明成本碾压 |
| 石斛面包吐司 | 短保配送半径限制+桃李/曼可顿竞争+全国物流投入巨大 |
| 大规模坚果炒货 | 三只松鼠/洽洽成本碾压+原料无优势 |
---
## 八、数据来源与假设说明
### 数据来源
| 编号 | 来源 | 覆盖数据项 |
|:---:|------|------|
| [1] | Flywheel《2025零食饮料趋势白皮书》 | 电商零食3080亿、一人食1.8万亿 |
| [2] | ZVZO消费观察《全球及中国压片糖果市场趋势深度分析报告2026》 | 压片糖果全球$485亿、功能性CAGR 12.6% |
| [3] | 华经产业研究院《2025年中国糖果行业分析》 | 中国糖果930亿 |
| [4] | Global Growth Insights 药用糖果市场报告 | 药用糖果$63.9亿 |
| [5] | 头豹研究院《大杯什锦果冻行业分析》 | 果冻市场200-250亿 |
| [6] | 米内网《中成药浸膏剂/煎膏剂数据分析》 | 膏滋100亿+、同比增长17.2% |
| [7] | 东阿阿胶2025年报 / 证券时报 | 东阿阿胶67亿、阿胶市场580亿 |
| [8] | 欧睿国际 / 桃李面包2025年报 | 中国烘焙2595亿、人均$25.5 |
| [9] | 中金企信 / 想念食品招股书 | 挂面市场500万吨、金沙河22%/克明8% |
| [10] | 国信证券 / 勤策消费研究 | 方便速食2500-6300亿 |
| [11] | 品斛堂百度百科 | OEM品类覆盖:精片/饼干/面条/果冻/浸膏 |
| [12] | 什么值得买"品斛堂OEM评测" | OEM实操经验验证 |
| [13] | 知行咨询《2025休闲零食行业年度洞察》 | 抖音零食份额54%、三只松鼠品类结构 |
| [14] | 雪球/药食同源研报 [2] | 药食同源3800→7500亿 |
| [15] | 品斛堂企业情报调研报告(BIZ-53) | 品斛堂产品线、DSR、电商数据 |
### 关键假设
1. 品斛堂现有石斛精片/纤维饼/浸膏OEM产线可快速转自有品牌生产——基于百度百科及OEM评测信息,假设成立
2. 石斛膏方/含片毛利率估计基于中药行业同类产品(东阿阿胶73.47%毛利率、片剂保健食品60-80%),剔除石斛自有原料带来的成本优势后估算
3. 饼干/坚果/果冻毛利率为行业平均水平,考虑规模劣势后下调5-10%
4. 市场规模数据时效性:核心数据来源为2024-2026年报告,时效性满足当前分析需求
5. 所有预测类数据已标注为"E"(预估),误差范围±15%
### 更新频率建议
- 核心市场数据(糖果/零食/烘焙):年度更新
- 竞品动态(三只松鼠/桃李/东阿阿胶):季度追踪
- 品斛堂自有品牌试水数据:月度复盘
---
*报告完成:顾析策 🔍 | 市场分析师 | 2026年6月26日*
*数据截至:2026年6月*
*本报告基于公开市场数据和行业研究报告编制,品斛堂内部产能数据来源于公开信息(百度百科、什么值得买评测)*
@@ -1,420 +0,0 @@
# 石斛功能保健食品、礼品礼盒、创新跨界品类扩展可行性分析报告
**报告编号**BIZ-66
**报告类型**:高毛利/差异化品类扩展可行性分析
**分析日期**2026年6月26日
**分析人**:顾析策(市场分析师)
**参考文档**:石斛食品饮料全品类产品方向详细文档、BIZ-53 企业情报调研报告、BIZ-64/BIZ-65 分析报告
---
## 一、摘要
**核心结论**:品斛堂在功能保健食品、礼品礼盒、创新跨界三大方向上存在明确的扩展机会,但需按"合规门槛→市场爆发力→品斛堂能力匹配"三维度排序——**蓝帽子保健食品是长期壁垒最高的方向,但需接受12-36个月审批周期;礼品礼盒是短期最容易变现的方向,可复用现有产品基础和渠道;创新跨界品类的ROI不确定性最高,适合小批量试水**。
**关键数据**
- 中国保健食品市场规模2024年达2308亿元(中商),2025年预计2447亿元;功能性食品市场2025年突破3700亿元(含食品化方向)
- 2025年Q1蓝帽子获批504款(含备案),注册类新产品141款,保健食品零食化趋势加速
- 中国礼物经济市场2025年预计达14498亿元,健康礼赠搜索量年增200%,天猫健康礼盒成交增长40%+
- 全球植物基食品市场2026年预计945亿美元,中国占全球植物基奶市场34%
- 药食同源市场3800亿元(2024)→7500亿元(2030),CAGR 10.8%;石斛淘系Q1线上销售额1.25亿元,同比+42%
**TOP5推荐排序**
| 排名 | 品类 | 优先级 | 预期毛利率 | 核心逻辑 |
|:---:|------|:---:|:---:|------|
| 🥇 | **石斛礼品礼盒**(原浆+酒+枫斗) | ⭐⭐⭐⭐⭐ | 55-70% | 现有产品复用、节日脉冲、渠道成熟、极速落地 |
| 🥈 | **石斛普通功能食品**(胶原蛋白肽饮/钙片) | ⭐⭐⭐⭐ | 50-65% | 备案制快速上市、无需蓝帽子审批、石斛成分差异化 |
| 🥉 | **石斛蓝帽子保健食品**(胃黏膜保护片/增强免疫力胶囊) | ⭐⭐⭐⭐ | 65-80% | 长期壁垒最高、毛利率最高、品斛堂已有三重生产资质+功效实验数据 |
| 4 | **石斛功能性口腔食品**(润喉糖/口香糖) | ⭐⭐⭐ | 45-55% | 高频快消、护喉需求爆发、OEM成熟 |
| 5 | **石斛植物基食品**(石斛植物奶/豆腐) | ⭐⭐ | 35-50% | 趋势正确但品斛堂能力匹配度低、需新建产能 |
---
## 二、蓝帽子保健食品(需注册·高毛利高壁垒)
### 2.1 市场规模与趋势
| 指标 | 数据 | 来源 |
|------|------|------|
| 中国保健食品市场规模(2024) | 2308亿元,同比+6.9% | 中商产业研究院 [1] |
| 2025年预测规模 | 2447亿元 | 中商 [1] |
| 养生保健食品市场(2023 | 3282亿元,同比+8.29% | 新营养 [2] |
| 药食同源市场(2024→2030 | 3800亿→7500亿,CAGR 10.8% | 雪球/行业研报 [3] |
| 2024年获批国产注册类保健食品 | 391款(其中新注册329款) | 中商 [1] |
| 2025年Q1获批保健食品 | 504款(含备案),注册类141款 | 新营养 [2] |
| 2025年Q1获批TOP功能 | 血糖、脂肪控制、润肠通便 | 新营养 [2] |
| 2024年获批国产备案凭证 | 4307款 | 中商 [1] |
| 头部品牌毛利率 | 60-75%(汤臣倍健66.7%,健合60.7% | 头豹 [4] |
| 保健食品零食化剂型 | 软糖/爆珠等新剂型进入批文 | 新营养 [2] |
**趋势信号**
- 2025年Q1植物性营养素占比47.82%,传统中药类(灵芝、西洋参、酸枣仁)占主导,石斛注册产品目前稀缺——**蓝海窗口**
- 药食同源目录已覆盖106种,石斛认知度加速提升
- 2025年进口保健食品注册通道重启,6款进口获批——国内竞争加剧信号
- 保健食品零食化趋势:糖果糕点类出现在批文中,打破"胶囊片剂"固有印象
- 2025年3月政策:"完善特殊食品注册许可制度,对符合条件的重点品种实施优先审评审批"——新《意见》利好创新品类
### 2.2 合规路径与时间成本
| 路径 | 适用条件 | 审批周期 | 审批级别 | 适合品斛堂的产品 |
|------|----------|:---:|------|------|
| **注册制** | 目录外原料/首次进口/新功能声称 | 12-36个月 | 国家市场监管总局 | 石斛胃黏膜保护片、增强免疫力胶囊、缓解疲劳口服液 |
| **备案制** | 原料已列入保健食品原料目录 | 1-3个月 | 省级市场监管局 | 石斛营养素补充剂(若石斛进入目录后) |
| **普通功能食品** | 无功能声称、按普通食品管理 | 无需审批 | — | 石斛胶原蛋白肽饮、石斛钙片 |
**品斛堂合规优势**
- ✅ 已具备药品+保健食品+食品三重生产资质
- ✅ 已有斑马鱼功效实验数据(胃黏膜保护、免疫力方向)
- ✅ 紫皮石斛全产业链控制(种植→加工→提取)
- ⚠️ 需补充:注册制产品完整的毒理学试验+功能学试验+人体试食试验
**时间线建议**
- **短期(0-6个月)**:启动注册制申报资料准备(配方定型+安全性评价+功能学评价),同步上线普通功能食品
- **中期(6-18个月)**:完成注册资料提交,进入技术审评阶段
- **长期(18-36个月)**:获批蓝帽子批文,正式上市
### 2.3 品斛堂匹配度评估
| 维度 | 评分 | 说明 |
|------|:---:|------|
| 原料优势 | ⭐⭐⭐⭐⭐ | 自有千亩有机石斛基地,原料成本可控,多糖含量≥2200mg/100g |
| 生产技术 | ⭐⭐⭐⭐ | 酶解+低温浓缩+超临界CO₂萃取技术已成熟,可支撑功能因子高保留 |
| 合规资质 | ⭐⭐⭐⭐ | 已具备三重生产资质,但蓝帽子注册批文需从零申请 |
| 研发能力 | ⭐⭐⭐ | 已有斑马鱼功效数据,但需补充完整GLP毒理+临床功能学试验 |
| 品牌信任 | ⭐⭐⭐ | 石斛原浆第一品牌认知可转化,但"保健品"心智尚未建立 |
### 2.4 具体产品评估
#### A. 石斛胃黏膜保护片(蓝帽子注册制)
| 评估维度 | 内容 |
|------|------|
| 目标人群 | 胃黏膜损伤人群、慢性胃炎患者、长期服药人群 |
| 市场规模 | 胃肠健康保健食品市场超200亿,TOP10胃肠功能产品年销过亿 |
| 竞争格局 | 江中药业(健胃消食片年销20亿+)、修正药业、葵花药业主导,石斛差异化切入尚属空白 |
| 预期毛利率 | 65-80%(对标江中药业毛利率67%) |
| 品斛堂优势 | 斑马鱼实验已验证石斛多糖胃黏膜保护功效,紫皮石斛多糖含量行业领先 |
| 时间成本 | 注册制审批12-36个月,申报资料准备3-6个月 |
| 风险 | 审批不确定性;市场竞争激烈,品牌认知需长期建设 |
#### B. 石斛增强免疫力胶囊(蓝帽子注册制)
| 评估维度 | 内容 |
|------|------|
| 目标人群 | 免疫力低下人群、术后恢复、老年人群 |
| 市场规模 | 增强免疫力为保健食品申报功能TOP1,2024年获批产品中免疫类占最大份额 |
| 竞争格局 | 汤臣倍健蛋白粉、无限极增健口服液等已占据主流心智,但石斛+免疫是差异化组合 |
| 预期毛利率 | 65-80% |
| 品斛堂优势 | 石斛多糖增强免疫的文献研究充分,斑马鱼实验可支持功效声称 |
| 时间成本 | 同上,注册制12-36个月 |
| 风险 | 免疫力声称已是红海;需要差异化功效定位 |
#### C. 石斛缓解疲劳口服液(蓝帽子注册制)
| 评估维度 | 内容 |
|------|------|
| 目标人群 | 易疲劳人群、熬夜加班族、运动人群 |
| 市场规模 | 缓解体力疲劳为保健食品TOP3申报功能,市场规模超150亿 |
| 竞争格局 | 红牛(170亿+)、东鹏特饮、无限极主导;草本抗疲劳方向尚存差异化空间 |
| 预期毛利率 | 65-75% |
| 品斛堂优势 | 石斛西洋参复配属传统补气养阴经典组合,配方壁垒较高 |
| 时间成本 | 同上 |
| 风险 | 与红牛等功能饮料品类边界模糊,需明确"保健食品"而非"饮料"定位 |
---
## 三、普通功能食品(无需蓝帽子·快速上市)
### 3.1 市场规模与趋势
| 指标 | 数据 | 来源 |
|------|------|------|
| 中国功能性食品市场(2025) | 突破3700亿元,增长率12-15% | 知乎/行业研报 [5] |
| 全球功能性食品(2024→2034 | $3322亿→$6380亿,CAGR 6.9% | GMInsights [6] |
| 胶原蛋白肽市场 | 中国口服美容市场2024年超250亿元,CAGR 15%+ |
| 钙补充剂市场 | 中国钙制剂市场2024年约180亿元,50+人群渗透率持续提升 |
| 零食化养生趋势 | 72%的90后用功能性零食替代传统保健品 | 美团/小红书 [7] |
### 3.2 合规路径
普通功能食品的**核心合规策略**
- ✅ 按普通食品标准管理,上市无需审批
- ✅ 可在包装标注"含石斛多糖""添加XX成分"(成分声称,不涉及功能声称)
- ✅ 可通过小红书/抖音等渠道做成分科普和食疗内容(KOL种草+达人推荐)
- ❌ 不得宣传任何保健功能(如"保护胃黏膜""增强免疫力"
- ❌ 产品名称不得含保健功能暗示
### 3.3 具体产品评估
#### A. 石斛胶原蛋白肽饮
| 评估维度 | 内容 |
|------|------|
| 目标人群 | 25-45岁女性,美容养颜+日常滋养 |
| 市场规模 | 口服美容市场250亿+,胶原蛋白肽为核心品类 |
| 对标品牌 | 汤臣倍健Yep、姿美堂、Swisse |
| 预期毛利率 | 55-65% |
| 差异化 | 石斛多糖"滋阴养胃+胶原蛋白美容"双重复配,传统滋养与现代功能性结合 |
| 上市周期 | 3-6个月(配方定型+稳定性测试+备案上市) |
| 渠道匹配 | 现有电商渠道可直接复用(天猫+抖音+视频号) |
#### B. 石斛钙片/钙咀嚼片
| 评估维度 | 内容 |
|------|------|
| 目标人群 | 中老年人群、骨健康关注者 |
| 市场规模 | 钙制剂市场180亿+ |
| 对标品牌 | 钙尔奇、迪巧、汤臣倍健 |
| 预期毛利率 | 50-60% |
| 差异化 | 钙+石斛多糖"补钙+养胃",减少传统钙片对胃的刺激感 |
| 上市周期 | 3-6个月 |
| 渠道匹配 | 药店+电商+商超 |
#### C. 石斛益生菌粉/软糖
| 评估维度 | 内容 |
|------|------|
| 目标人群 | 肠胃不适人群、年轻养生族 |
| 市场规模 | 益生菌市场2025年全球770亿美元,中国占25%,年增20%+ |
| 对标品牌 | 合生元、妈咪爱、wonderlab |
| 预期毛利率 | 50-65% |
| 差异化 | 益生菌+石斛多糖"双重养胃",肠道+胃黏膜协同保护 |
| 趋势红利 | 保健食品零食化(软糖/爆珠剂型)——符合年轻化消费趋势 |
| 上市周期 | 3-6个月 |
---
## 四、礼品礼盒(高毛利·节日爆发)
### 4.1 市场规模与趋势
| 指标 | 数据 | 来源 |
|------|------|------|
| 中国礼物经济市场(2025E) | 14498亿元,持续增长 | 艾媒咨询 [8] |
| 2027年预测 | 16197亿元 | 艾媒 [8] |
| 健康礼赠搜索量增长 | 年增200% | 天猫健康 [9] |
| 天猫健康礼盒成交增长 | FY24同比增长40%+,3万+款礼盒 | 天猫健康 [9] |
| 诞生千万单品数 | FY24期间27款 | 天猫健康 [9] |
| 年轻人选健康类年礼比例 | 近70% | TMIC白皮书 [9] |
| 近30%消费者送过最成功的礼品 | 与健康相关 | TMIC白皮书 [9] |
| 春节/中秋销售额占比 | 全年约25% | Flywheel白皮书 |
| 礼品行业复合增长率 | 7% | 凯度/励展华博 [10] |
**关键趋势**
- 送礼从"面子工程"转向"价值感+情绪表达",健康礼赠成为确定性增长极
- 天猫推出"送礼"功能,淘宝App全量上线——降低送礼决策成本
- 燕之屋(燕窝礼盒)、小仙炖(燕窝礼盒)、东阿阿胶(阿胶礼盒)已在礼赠赛道验证——石斛礼盒尚未被头部品牌锁定
- 一线品牌春节礼盒案例:燕之屋携手法国设计师,小仙炖联名非遗艺术家——**包装设计本身就是传播媒介**
### 4.2 品斛堂现有礼盒基础
| 现有产品 | 价格带 | 基础评价 |
|------|:---:|------|
| 紫皮石斛原浆至臻礼盒 | ¥900+ | 已有高端礼盒经验,原浆作为送礼主形态已验证 |
| 石斛酒礼盒 | ¥300-900 | CIC认证"石斛酒中国销量第一",酒礼盒天然适配商务送礼 |
| 铁皮石斛原浆礼盒 | ¥499-799 | 中高端送礼定位,可扩展至年节礼盒 |
### 4.3 礼盒扩展建议
#### A. 石斛原浆礼盒体系化(首推)
| 维度 | 内容 |
|------|------|
| 策略 | 建立"原浆礼盒金字塔"——入门级(¥299-399)→中端(¥499-799)→高端(¥999-2999) |
| 产品线 | 紫皮原浆入门礼盒、铁皮原浆中端礼盒、霍山米斛原浆高端礼盒、复合原浆组合礼盒 |
| 场景匹配 | 春节全家福礼盒、中秋商务礼盒、日常拜访养生礼盒、长辈祝寿礼盒 |
| 包装策略 | 联名非遗/IP设计师打造限定包装,强化"云南龙陵·道地石斛"产地叙事 |
| 预期毛利率 | 55-70%(礼盒溢价+包装附加值) |
| 竞争优势 | 石斛原浆第一品牌背书+全产业链透明溯源 |
| 渠道 | 天猫健康礼赠会场+视频号礼赠营销+企业团购福利+线下药店铺货 |
#### B. 石斛酒礼盒升级
| 维度 | 内容 |
|------|------|
| 策略 | 围绕"中国石斛露酒开创者"身份,打造商务宴请+节日送礼双场景 |
| 产品线 | 石斛西洋参灵芝酒礼盒(蓝帽子)、石斛米香白酒礼盒(云南特色)、石斛+原浆组合礼盒(酒+滋补) |
| 预期毛利率 | 55-75%(蓝帽子酒礼盒毛利最高) |
| 礼盒策略 | 云南民族文化元素包装+产地溯源二维码+限量版工艺 |
#### C. 石斛枫斗礼盒(传统高端)
| 维度 | 内容 |
|------|------|
| 策略 | 面向传统滋补送礼人群,强调"道地基源+手工精选" |
| 产品线 | 铁皮石斛枫斗礼盒(¥599-999)、紫皮石斛枫斗礼盒(¥299-599)、霍山米斛枫斗礼盒(¥1299-2999) |
| 预期毛利率 | 55-65% |
| 适合人群 | 传统养生认知强的中老年送礼场景、高端商务馈赠 |
---
## 五、创新跨界品类(差异化·爆品潜力)
### 5.1 功能性口腔食品
#### 5.1.1 市场规模与趋势
| 指标 | 数据 | 来源 |
|------|------|------|
| 中国口香糖市场 | 约120亿元,增速放缓但功能性方向增长 |
| 中国润喉糖/含片市场 | 约80亿元,后疫情时代护喉需求持续 |
| 口腔护理食品趋势 | "清新口气+口腔健康"双功能融合,益生菌口香糖/含片兴起 |
| 对标产品 | 金嗓子喉宝(年销20亿+)、王老吉润喉糖、绿箭/益达 |
#### 5.1.2 具体产品评估
| 产品 | 目标人群 | 价格带 | 预期毛利率 | 品斛堂匹配度 | 结论 |
|------|----------|:---:|:---:|:---:|------|
| 石斛润喉糖 | 用嗓过度、咽喉不适人群 | ¥12.9-22.9/盒 | 45-55% | ⭐⭐⭐⭐ | **首推**——石斛清咽利喉传统认知强,制造工艺成熟,OEM快启动 |
| 石斛口香糖 | 口腔异味人群 | ¥9.9-19.9/盒 | 40-50% | ⭐⭐⭐ | 可试水——"保护口腔黏膜"的差异化卖点,但需教育市场 |
**可行性分析**
- 石斛传统用途中"清咽利喉"心智成熟,《本草纲目》记载"强阴益精,厚肠胃",咽喉保护是其衍生功效认知
- 润喉糖属高频快消品,复购率高,与品斛堂现有电商渠道天然匹配
- 金嗓子、王老吉等品牌占据主流,但"石斛润喉"细分赛道空白
- 可通过OEM代工快速试水,无需自建生产线
**推荐路径**:委托成熟润喉糖OEM工厂代工→自有品牌+电商测试→验证ROI后决定是否自建产能
### 5.2 植物基食品
#### 5.2.1 市场规模与趋势
| 指标 | 数据 | 来源 |
|------|------|------|
| 全球植物基食品市场(2026E | 945亿美元,CAGR 12.24% | Straits Research [11] |
| 全球植物基市场(2023→2033 | 113亿→355亿美元,CAGR 12.2% | DSM [12] |
| 中国占全球植物基奶市场 | 34%(最大单一市场) | 艾媒 |
| 植物基奶产品占植物基 | 54.65% | Straits [11] |
| 中国现存植物基企业 | 8399家(截至2023.4),2017-2022新增1982家 | 力矩中国 |
| Inova 2025趋势 | 55%消费者认为植物基应作为独立品类 |
#### 5.2.2 具体产品评估
| 产品 | 目标人群 | 价格带 | 预期毛利率 | 品斛堂匹配度 | 结论 |
|------|----------|:---:|:---:|:---:|------|
| 石斛植物奶 | 乳糖不耐受人群、健康饮品消费者 | ¥8-12/L | 35-45% | ⭐⭐ | **谨慎**——需要新建植物基产线,与现有核心能力差距大 |
| 石斛豆腐 | 素食主义者、健康饮食人群 | ¥5.9-9.9/500g | 30-40% | ⭐ | **暂不推荐**——冷链依赖、区域性强、毛利率低 |
**可行性分析**
- 植物基大方向正确,但品斛堂能力圈(原浆酶解+酿酒+提取)与植物基(植物蛋白结构重组、发酵工艺)匹配度不足
- Oatly、维他奶等已占据植物奶主流心智,石斛切入需差异化定位("石斛多糖+植物蛋白"组合)
- 植物奶毛利率35-45%,与品斛堂现有原浆业务(60-70%)相比显著偏低
- 2025年植物基赛道竞争者已超8000家,赛道拥挤度急剧上升
**推荐路径**
- 植物奶:与现有植物基品牌(如Oatly)联名试水,而非自建产线
- 石斛豆腐:属于区域型冷链产品,不适合品斛堂现有电商全国的渠道模型,**暂不推荐**
---
## 六、四维度横向对比与TOP5推荐
### 6.1 四维度综合评分
| 维度 | 蓝帽子保健食品 | 普通功能食品 | 礼品礼盒 | 创新跨界 |
|------|:---:|:---:|:---:|:---:|
| 市场规模 | ⭐⭐⭐⭐ 2308亿 | ⭐⭐⭐⭐⭐ 3700亿 | ⭐⭐⭐⭐⭐ 1.45万亿 | ⭐⭐⭐⭐ 945亿美元 |
| 增速 | ⭐⭐⭐ 6.9% | ⭐⭐⭐⭐ 12-15% | ⭐⭐⭐ 7% | ⭐⭐⭐⭐ 12.24% |
| 合规门槛 | ⭐⭐⭐ 12-36个月 | ⭐⭐⭐⭐⭐ 无需审批 | ⭐⭐⭐⭐ 食品标准即可 | ⭐⭐⭐⭐⭐ 无需审批 |
| 品斛堂匹配度 | ⭐⭐⭐⭐ 原料+资质 | ⭐⭐⭐⭐ 配方+渠道 | ⭐⭐⭐⭐⭐ 现有产品复用 | ⭐⭐ 需新建产能 |
| 预期毛利率 | ⭐⭐⭐⭐⭐ 65-80% | ⭐⭐⭐⭐ 50-65% | ⭐⭐⭐⭐⭐ 55-70% | ⭐⭐ 35-50% |
| 竞争壁垒 | ⭐⭐⭐⭐⭐ 审批+品牌 | ⭐⭐⭐ 成分差异化 | ⭐⭐⭐⭐⭐ 产地+品牌 | ⭐ 低壁垒 |
| 落地速度 | ⭐ 慢 | ⭐⭐⭐⭐⭐ 快 | ⭐⭐⭐⭐⭐ 极快 | ⭐⭐⭐ 快 |
### 6.2 TOP5推荐详细说明
#### 🥇 TOP1:石斛礼品礼盒体系化
- **推荐理由**:品斛堂已具备礼盒产品基础(原浆礼盒¥900+、酒礼盒¥300-900),无需新增产线或审批。健康礼赠市场正处爆发早期,天猫健康礼盒成交年增40%+,石斛礼盒赛道尚未被头部品牌锁定。
- **落地路径**
- 立即启动:包装设计升级(联名非遗IP/设计师),建立"入门→中端→高端"礼盒金字塔
- 3个月内:首批节日限定礼盒上线(中秋礼盒先行)
- 渠道:天猫健康礼赠会场+视频号礼赠营销+企业团购+线下药店
- **关键成功因素**:包装设计=传播媒介;产地叙事=信任壁垒;多价格带覆盖=市场扩容
- **预期年增量营收**:3000-5000万(首年,复用现有产品)
#### 🥈 TOP2:石斛普通功能食品(胶原蛋白肽饮/益生菌软糖)
- **推荐理由**:无需蓝帽子审批,3-6个月可上市,毛利率50-65%,石斛成分差异化明确。口服美容+益生菌双赛道年增15%+,"零食化养生"趋势强劲。
- **落地路径**
- 胶原蛋白肽饮:委托OEM代工→自有品牌+电商首测→3个月内上市
- 益生菌石斛软糖:抓住保健食品零食化趋势,软糖剂型吸引年轻消费者
- 渠道:天猫+抖音+小红书内容种草
- **关键成功因素**:成分差异化(石斛多糖复配)、剂型零食化、内容营销驱动
- **预期年增量营收**2000-4000万
#### 🥉 TOP3:石斛蓝帽子保健食品(胃黏膜保护/增强免疫力)
- **推荐理由**:长期壁垒最高(审批+品牌)、毛利率最高(65-80%)、品斛堂已具备三重生产资质+斑马鱼功效数据。2025年政策利好"重点品种优先审评审批",蓝帽子批文可形成10年+的独占期壁垒。
- **落地路径**
- 立即启动:注册制申报资料准备(配方定型+GLP毒理+功能学评价),预算300-500万
- 同步推进:斑马鱼功效数据整理为SCI论文,增强学术背书
- 12-36个月:获批后正式上市
- **关键成功因素**:审批成功=独占壁垒;学术论文=品牌信任;全产业链=成本优势
- **风险提示**:审批周期长且存在不确定性,注册成本300-500万
- **预期年增量营收**:获批后首年3000-5000万,长期有望过亿
#### ④ TOP4:石斛功能性润喉糖
- **推荐理由**:石斛"清咽利喉"传统心智成熟,润喉糖市场80亿+,OEM代工可快速启动。高频快消品属性带来复购率,与电商渠道天然匹配。
- **落地路径**
- 委托成熟润喉糖OEM工厂代工→自有品牌→电商+便利店测试
- 3个月内首批上市
- **关键成功因素**:包装年轻化、渠道铺货、口味优化
- **预期年增量营收**500-1500万
- **风险**:金嗓子/王老吉主导市场,品牌突围需要差异化营销投入
#### ⑤ TOP5:石斛植物基食品(联名试水)
- **推荐理由**:全球植物基CAGR 12.24%,中国是最大植物基奶市场。石斛多糖+植物蛋白组合有差异化空间。
- **落地路径**:与Oatly/维他奶等品牌联名→石斛植物奶→电商测试→根据市场反馈决定是否深入
- **关键成功因素**:联名降低试错成本、植物基趋势红利
- **预期年增量营收**:试水阶段500-1000万
- **风险**:品斛堂植物基能力不足,自建产线ROI存疑,建议仅限联名试水
---
## 七、综合落地路线图
| 时间线 | 行动 | 所需资源 | 预期成果 |
|------|------|------|------|
| **0-3个月** | ①礼盒包装升级+中秋礼盒上线 ②胶原蛋白肽饮OEM启动 ③润喉糖OEM启动 ④注册制申报准备启动 | 设计费50万+OEM试产费80万 | 3款新产品上市 |
| **3-6个月** | ①春节礼盒预售 ②胶原蛋白肽饮正式上市 ③润喉糖上市 ④益生菌软糖启动 | OEM产能扩产100万 | 6款产品在售 |
| **6-18个月** | ①礼盒体系化(全节日覆盖) ②蓝帽子注册资料提交 ③功能食品线扩展 | 注册费300-500万 | 形成完整产品矩阵 |
| **18-36个月** | ①蓝帽子获批→正式上市 ②植物基联名试水 | 批文持有+营销投入 | 高壁垒护城河建成 |
---
## 八、数据来源与假设说明
**数据来源**
1. [1] 中商产业研究院《2025-2030年中国保健食品深度分析及发展前景研究预测报告》
2. [2] 新营养《2025年Q1保健食品行业全景解析》(2025.4.10)
3. [3] 雪球/药食同源行业研报:2024年3800亿→2030年7500亿
4. [4] 头豹《2025年营养健康行业词条报告》(2025.7.30)
5. [5] 知乎专栏《2025年中国功能性食品行业年末深入盘点》
6. [6] GMInsights《功能性食品市场规模及份额 2025-2034》
7. [7] 美团/小红书用户行为数据
8. [8] 艾媒咨询《中国礼物经济产业市场规模》
9. [9] 天猫健康·TMIC《健康礼赠行业趋势白皮书》(2024)
10. [10] 励展华博×凯度《2025年中国礼品行业展望白皮书》
11. [11] Straits Research《植物基食品和饮料市场规模 2026-2034》
12. [12] DSM全球植物基市场报告(2024)
**关键假设**
- 保健食品市场增长率假设为6-8%,基于近3年平均增速
- 礼盒毛利率假设基于品斛堂现有礼盒产品定价与行业均值
- 蓝帽子审批周期假设基于2025年Q1实际审批节奏(Q1新注册116款)和国家市监局公开数据
- 品斛堂年营收假设3-5亿元(基于BIZ-53情报调研)
- 增量营收预测基于可触及市场(SAM)而非总可寻址市场(TAM)
**置信区间**
- 市场规模数据:±5-10%(多源交叉验证)
- 毛利率预测:±5%(行业对标+品斛堂现有产品毛利率反推)
- 增量营收预测:±30%(首年试水阶段不确定性高)
---
*报告完成于2026年6月26日 | 顾析策 | 分析事业部 | 市场分析师*
@@ -1,318 +0,0 @@
# 石斛预制菜与调味品赛道切入可行性分析
**分析日期**2026年6月26日
**分析师**:顾析策(市场分析)
**关联议题**BIZ-67(父议题:BIZ-53 品斛堂企业情报调研)
---
## 一、摘要
**核心结论**:品斛堂当前阶段**不建议直接进入预制菜/调味品赛道**,建议采取"**观望+轻资产试水**"策略——优先以OEM模式试水石斛养生汤料包/石斛火锅底料,待市场验证后再决定是否加大投入。
**关键数据**
- 中国调味品市场规模 6871 亿(2024),酱油品类 1041 亿;海天酱油市占率 13.2%
- 中国预制菜市场规模 4850-6000 亿(2024),预计 2026 年达 7490-10720 亿
- 预制菜行业毛利率仅 10-15%,CR10 仅 14%7.2 万家企业高度碎片化
- 药膳预制菜抖音电商 2023 年 1-9 月同比增长 605%,细分赛道高速增长
**建议**:观望着手养生汤料包 OEM 试水,6-12 个月后根据销售数据决定下一步。
---
## 二、调味品赛道分析
### 2.1 市场规模与增长
| 指标 | 数据 | 来源 |
|------|------|------|
| 2024 年调味品市场总规模 | 6871 亿元,YoY +16.0% | 艾媒咨询 2025H1 |
| 2027 年调味品市场预测 | 10028 亿元 | 艾媒咨询 |
| 2016-2023 CAGR | 10.12% | 华经产业研究院 |
| 2024 年复合调味品市场 | 2301 亿元 | 艾媒咨询 |
| 2024 年酱油市场(国内) | 1041 亿元 | 弗诺斯特沙利文 |
| 2024 年菜谱式调味品 | 522 亿元,2027 年预计 1033 亿 | 艾媒咨询 |
| 酱油 CAGR2019-2024 | 2.6%,预测 2024-2029 为 4.8% | 弗诺斯特沙利文 |
**判断**:调味品市场稳健增长但已进入**存量竞争**阶段,基础调味品增速放缓,复合调味品是增量来源。
### 2.2 竞争格局
| 排名 | 企业 | 整体市占率(2024) | 核心品类 | 2024 年营收 |
|:---:|------|:---:|------|------|
| 1 | 海天味业 | 4.8% | 酱油(13.2%/蚝油(40.2%/酱类 | ~269 亿 |
| 2 | 阜丰集团 | 1.4% | 味精/氨基酸 | — |
| 3 | 李锦记 | ~1.4% | 酱油/蚝油/酱类 | 非上市 |
| — | 中炬高新(美味鲜) | — | 酱油第二 | ~30 亿 |
| — | 千禾味业 | — | 高端酱油 | ~20 亿 |
| — | 天味食品 | — | 火锅底料/中式复调 | 31.5 亿(2023 |
**关键发现**
- **寡头格局已形成**:海天+李锦记+中炬高新占据酱油市场头部,海天渠道下沉至乡镇
- **酱油价格战白热化**:9.9 元/L 特级酱油已成常态,新品牌突围极难
- **"零添加"标签被禁**2025 年 3 月新国标(GB 7718-2025)规定 2027 年起禁标"零添加""不添加",对差异化新品牌构成政策风险
### 2.3 石斛调味品可行性评估
#### 石斛酱油
- **对标现状**:海天酱油 2025 年营收 149 亿,市占率 13.2%,规模效应碾压级
- **差异化空间**:极小。酱油消费者核心决策因素为"品牌+价格+口味""养生"属性在酱油品类中尚未成为主流需求
- **品斛堂劣势**:无酿造产能、无渠道、无品牌认知、远离大豆主产区(云南非大豆产区)
- **成本劣势**:石斛添加推高成本,终端定价可能在 19.9-29.9 元/500ml,而主流酱油 9.9 元/L,价格差 2-3 倍
#### 石斛火锅底料
- **对标现状**:海底捞(颐海国际)、天味食品(好人家)、德庄、红九九
- **差异化空间**:中等。"不上火"概念在火锅场景有消费者感知,石斛+云南菌菇/酸汤可打造"云南养生火锅"概念
- **机会点**:复合调味品增速 13%+,CR 低,OEM 进入门槛低于酱油
- **品斛堂劣势**:无火锅底料研发经验,口味研发需外部合作
### 2.4 调味品切入评分:★★☆☆☆(2/5)
| 维度 | 评分 | 说明 |
|------|:---:|------|
| 市场规模 | 4/5 | 6871 亿大市场,复合调味品增长快 |
| 竞争强度 | 1/5 | 寡头格局,酱油价格战,新品牌生存空间极小 |
| 品斛堂匹配度 | 2/5 | 有原料优势但无生产/渠道/品牌能力 |
| 差异化空间 | 2/5 | 火锅底料有"养生"概念空间,酱油基本没有 |
| 进入门槛 | 2/5 | 酱油需酿造产能(重资产),火锅底料可 OEM(轻资产) |
| 盈利预期 | 2/5 | 头部企业净利率 15-20%,新品可能需要 2-3 年培育 |
---
## 三、预制菜赛道分析
### 3.1 市场规模与增长
| 指标 | 数据 | 来源 |
|------|------|------|
| 2022 年市场规模 | 4196 亿元,YoY +21.3% | 艾媒咨询 |
| 2023 年市场规模 | 5165 亿元 | 艾媒/人民网研究院 |
| 2024 年市场规模 | 4850-6000+ 亿元 | 艾媒蓝皮书(4850/新华网(6000+ |
| 2026 年预测 | 7490-10720 亿元 | 艾媒蓝皮书/早期预测 |
| 全球预制食品市场(2025) | 3981 亿美元→2030 年 5316 亿美元,CAGR 6% | Statista |
| B:C 端比例 | 约 7:3(B 端仍为主力) | 嘉世咨询 |
| 渗透率 | 10-15%(美/日 60%+) | 艾媒/中国连锁餐饮报告 |
| 企业数量 | 7.2 万+ 家 | 天眼查/企查查 |
| 连锁化率驱动 | 2023 年餐饮连锁化率约 21%(美 54%/日 48%) | 中国连锁餐饮报告 |
**判断**:预制菜是确定性强的万亿赛道,但当前已从"野蛮生长"进入"规范整合"阶段。
### 3.2 竞争格局
#### 四类玩家错位竞争
| 类型 | 代表企业 | 优势 | 劣势 | 毛利率 |
|------|------|------|------|:---:|
| **上游农牧水产** | 国联水产、龙大美食、新希望 | 原料成本优势、规模化 | C 端品牌力弱 | 10-15% |
| **传统速冻食品** | 安井食品、三全、千味央厨 | 规模化生产、渠道分销强 | B 端定制弱 | 25-30% |
| **专业预制菜** | 味知香、蒸烩煮、聪厨 | 经验丰富、研发能力强 | 规模小、区域性强 | 25-30% |
| **餐饮/零售** | 西贝、盒马、广州酒家、海底捞 | C 端品牌强、终端直连 | 渠道单一、自建中央厨房成本高 | 10-15% |
**关键发现**
- **行业高度分散**:CR10 仅 14%,无全国性龙头
- **毛利率普遍偏低**:安井预制菜毛利率从 29.7%2018)降至 11.4%2022
- **同质化严重**:酸菜鱼、佛跳墙、小龙虾是最大单品,企业扎堆
- **70% 企业为作坊式**:产品标准缺失,食品安全风险大
### 3.3 石斛预制菜可行性评估
#### 石斛炖鸡/排骨汤预制菜
- **对标**:盒马(鲜食预制菜)、西贝(贾国龙功夫菜)、海底捞(预制菜线)
- **差异化定位**:"养生药膳预制菜"——区别于市面主流的麻辣/酸辣类
- **理论优势**:石斛的"养生"心智 + 鸡汤的国民认知度 + 药膳文化底蕴
- **实际挑战**
- 养生预制菜目前仍是极小众品类,主流消费者预制菜核心诉求是"便捷+好吃"而非"养生"
- 药膳预制菜需要兼顾"功效"与"口味",研发难度远超普通预制菜
- "药膳"的保健功能宣传在合规层面存在极大风险(普通食品不得宣称保健功能)
#### 药膳预制菜市场信号(正面)
- 抖音电商 2023 年 1-9 月药膳类预制菜销售额同比增长 605%
- 温氏食品+昆中药推出参苓鸡系列
- 磐安"盘安药膳"推出黄精肉、玉竹老鸭等产品
- 广州酒家推出人参老鸭汤、人参益智仁乌鸡汤
- **但注意**:这些均为大企业试水或区域特色产品,尚未出现药膳预制菜爆品
### 3.4 预制菜切入评分:★★☆☆☆(2/5)
| 维度 | 评分 | 说明 |
|------|:---:|------|
| 市场规模 | 5/5 | 万亿级赛道,增速 20%+ |
| 竞争强度 | 2/5 | 高度碎片化、同质化,价格战正在蔓延 |
| 品斛堂匹配度 | 1/5 | 无生产经验、无冷链、无渠道、远离消费市场 |
| 差异化空间 | 3/5 | "养生药膳预制菜"有独特定位,但品类尚未验证 |
| 进入门槛 | 2/5 | 冷链物流+口味研发+渠道建设均需重投入 |
| 盈利预期 | 1/5 | 头部企业毛利率仅 10-15%,新品培育期可能长期亏损 |
---
## 四、品斛堂匹配度总体评估
### 4.1 优劣势对比
| 优势 | 劣势 |
|------|------|
| ✅ 石斛原料自供,全产业链成本可控 | ❌ **无预制菜/调味品生产经验** |
| ✅ 食品生产资质齐全(药品+保健食品+食品) | ❌ **无冷链仓储和配送网络** |
| ✅ OEM/ODM 服务经验(服务近 100 家企业) | ❌ **地处云南龙陵,远离核心消费市场** |
| ✅ "养生"品牌心智(石斛原浆第一品牌) | ❌ **销售渠道以线上为主,线下商超/便利店覆盖弱** |
| ✅ 云南地方特色食材资源(菌菇/酸汤等) | ❌ **预制菜和调味品研发团队缺失** |
| ✅ 石斛多糖等原料可赋能产品差异化 | ❌ **调味品口味研发能力为零(酱油/火锅底料均需专业研发)** |
### 4.2 风险矩阵
| 风险类型 | 具体描述 | 严重程度 | 发生概率 |
|------|------|:---:|:---:|
| **市场风险** | 预制菜/调味品竞争白热化,新品牌存活率低 | 🔴 高 | 🔴 高 |
| **运营风险** | 缺乏冷链物流和生产经验,产品质量难以保证 | 🔴 高 | 🔴 高 |
| **财务风险** | 毛利率极低(10-15%),初期投入大、回收慢 | 🟡 中 | 🔴 高 |
| **合规风险** | "养生药膳"概念在食品宣传中触碰《广告法》红线 | 🔴 高 | 🟡 中 |
| **品牌风险** | 预制菜若出食品安全问题,反噬石斛原浆主品牌 | 🔴 高 | 🟡 中 |
---
## 五、三种切入路径对比
### 路径 A:轻资产 OEM 试水(推荐指数:★★★★☆)
| 维度 | 详情 |
|------|------|
| **方式** | 与成熟预制菜/调味品代工厂合作,品斛堂提供石斛原料+品牌+渠道 |
| **首推产品** | ① 石斛菌菇养生汤料包(常温/冷冻) ② 石斛云南酸汤火锅底料 |
| **启动资金** | 300-500 万元(含研发费+首批生产+包装设计+渠道推广) |
| **时间线** | 3-6 个月产品上市,12 个月验证期 |
| **主要风险** | 品控依赖代工厂;利润被代工费挤压;产品差异化不够 |
| **退出成本** | 低——可随时停止而不影响主营业务 |
### 路径 B:战略合作/合资(推荐指数:★★★☆☆)
| 维度 | 详情 |
|------|------|
| **方式** | 与云南本地餐饮集团或预制菜企业成立合资公司,共同开发云南药膳预制菜 |
| **首推产品** | 石斛汽锅鸡预制菜、石斛菌汤包、云南过桥米线石斛汤底 |
| **启动资金** | 1000-2000 万元(含合资公司注册+产线改造+冷链建设) |
| **时间线** | 6-12 个月产品上市,18-24 个月盈亏平衡 |
| **主要风险** | 合作伙伴选择失误;利益分配冲突;管理复杂度高 |
| **退出成本** | 中——合资公司清算或股权转让 |
### 路径 C:自建产线重资产进入(推荐指数:★☆☆☆☆)
| 维度 | 详情 |
|------|------|
| **方式** | 在龙陵或昆明自建预制菜中央厨房/调味品生产线 |
| **首推产品** | 全系列石斛预制菜+石斛调味品 |
| **启动资金** | 5000 万-1 亿元(含厂房+设备+冷链物流+团队+渠道) |
| **时间线** | 18-24 个月产品上市,36-48 个月盈亏平衡 |
| **主要风险** | 资金压力大;产能利用率不足;远离消费市场导致冷链成本高 |
| **退出成本** | 极高——固定资产沉没,转型困难 |
| **当前阶段** | **强烈不建议** |
### 三路径对照总结
| 维度 | 路径 A OEM 试水 | 路径 B 战略合作 | 路径 C 重资产自建 |
|------|:---:|:---:|:---:|
| 启动资金 | 300-500 万 | 1000-2000 万 | 5000 万-1 亿 |
| 上市时间 | 3-6 个月 | 6-12 个月 | 18-24 个月 |
| 盈亏平衡 | 6-12 个月 | 18-24 个月 | 36-48 个月 |
| 风险等级 | 低 | 中 | 极高 |
| 可控性 | 中 | 中高 | 高 |
| 利润空间 | 低(代工费挤压) | 中(合资分成) | 中高(规模效应后) |
| 推荐指数 | ★★★★☆ | ★★★☆☆ | ★☆☆☆☆ |
---
## 六、预期投入与回报周期(路径 A)
| 项目 | 明细 | 金额(万元) |
|------|------|:---:|
| **一次性投入** | | |
| 产品研发(汤料/火锅底料配方+石斛添加比例测试+保质期验证) | 外部研发合作 | 50-80 |
| 包装设计(品牌视觉+包材+首批印刷) | 设计+模具 | 30-50 |
| 首批生产(最小起订量,含石斛原料成本) | 3-5 个 SKU | 100-150 |
| 渠道铺设(线上详情页+达播合作+线下试销进场费) | | 50-80 |
| **一次性投入合计** | | **230-360 万** |
| **月度运营** | | |
| 线上运营(投流+平台佣金+团队) | 月均 | 20-40 |
| 线下促销/试吃/陈列费 | 月均 | 10-20 |
| **首年总投入** | | **约 500-700 万** |
| 时间节点 | 里程碑 | 预期表现 |
|------|------|------|
| 第 3 个月 | 产品上市(天猫/抖音首发) | 月销 10-20 万 |
| 第 6 个月 | 首轮数据验证 | 月销 30-50 万,复购率 > 15% → 继续投入 |
| 第 12 个月 | Go/No-Go 决策 | 月销 100 万+ → 考虑路径 B;< 30 万 → 停项目 |
---
## 七、最终建议
### 🟡 总体判断:**观望**
**进入**
**观望** ✅(选择此项)
**放弃**
### 核心理由
1. **预制菜/调味品均处于格局剧变期**:预制菜国家标准正在制定中,行业即将洗牌;调味品"零添加"禁令 2027 年全面生效,整个行业的产品和营销逻辑都在重构。此时贸然进入意味着在不确定的规则下做重投入决策。
2. **品斛堂的核心能力与预制菜/调味品的核心要求不匹配**
- 预制菜的核心竞争力 = **冷链物流网络 + 口味研发 + 渠道效率 + 成本控制**
- 调味品的核心竞争力 = **酿造产能 + 品牌认知 + 百万终端覆盖 + 规模效应**
- 品斛堂的核心优势 = **石斛全产业链 + 原浆品类开创者 + 线上电商运营**
- 这三个集合的交集非常小
3. **资源应该聚焦核心战场**
- 石斛原浆市场规模 120 亿,品斛堂市占率 45%+,还有 55%+ 的空间
- 复合原浆是第二增长曲线,增速 25%+,且与现有能力高度匹配
- 保健品蓝帽子注册是高毛利壁垒型业务(毛利率 60-80%)
- 这些机会的投入产出比远高于预制菜/调味品
4. **但不应完全放弃——可以"轻试水"**
- 石斛养生汤料包和火锅底料可以极低成本试水(路径 A,300-500 万)
- 利用现有天猫/京东/抖音渠道做首发,不用新建渠道
- 本质上是将石斛原料以"汤料包"形式触达消费者
- 12 个月后根据数据决定是继续还是止损
### 行动建议
| 优先级 | 行动 | 时间 |
|:---:|------|------|
| P0 | 保持预制菜/调味品赛道月度舆情监控 | 即刻开始 |
| P1 | 石斛菌菇养生汤料包 OEM 试水立项(路径 A) | 下季度 |
| P2 | 调研云南本地预制菜合作伙伴(路径 B 前期) | 6 个月后 |
| P3 | 关注预制菜国家标准出台后的行业洗牌窗口 | 持续跟踪 |
---
## 八、数据来源与假设说明
### 主要数据来源
| 编号 | 来源 | 数据类型 | 时效 |
|:---:|------|------|:---:|
| [1] | 艾媒咨询《2023-2025 年中国预制菜行业运行及投资决策分析报告》 | 预制菜市场规模/格局 | 2024 |
| [2] | 艾媒咨询《2024-2025 年中国预制菜产业发展蓝皮书》 | 预制菜市场最新数据 | 2025 |
| [3] | 艾媒咨询《2025 年 H1 中国调味品行业运行数据监测半年报》 | 调味品市场数据 | 2025 |
| [4] | 华经产业研究院《中国复合调味品行业发展现状》 | 复合调味品市场规模 | 2024 |
| [5] | 弗诺斯特沙利文 | 酱油市场数据 | 2024-2025 |
| [6] | 嘉世咨询《2025 年中国调味品行业报告》 | 调味品竞争格局 | 2025 |
| [7] | 新华网/中国商报 | 预制菜企业数量/国家标准进展 | 2025 |
| [8] | 华鑫证券/平安证券预制菜行业研报 | 预制菜供应链/竞争分析 | 2023-2024 |
| [9] | 灼识咨询《2022 中国预制菜行业蓝皮书》 | 竞争格局 | 2022 |
| [10] | FDL 数食主张《药膳预制菜》 | 药膳预制菜趋势 | 2023 |
| [11] | 抖音电商数据(引用自食品伙伴网) | 药膳预制菜增长 | 2023 |
| [12] | 品斛堂企业调研报告(BIZ-53) | 品斛堂企业情报 | 2026.06 |
| [13] | 石斛食品饮料全品类产品方向详细文档 | 参考产品规划 | 2026 |
### 关键假设
- 2026 年预制菜市场规模取艾媒蓝皮书 7490 亿(较保守口径)
- 品斛堂参考文档中预制菜/调味品毛利预期 30-50%,本报告校正为更保守的 10-25%(基于上市公司实际数据)
- 药膳预制菜"增速数据(605%)基于抖音电商单一渠道,不代表全渠道增速
- 投资金额估算为行业平均水平参考值,实际可能因合作伙伴、地区政策等因素而浮动
- 石斛火锅底料的"不上火"概念为市场假设,需通过消费者调研验证
---
*本报告仅供内部决策参考,不构成对外投资建议。*
*数据截止日期:2026 年 6 月 26 日*
+3
View File
@@ -0,0 +1,3 @@
__pycache__/
*.egg-info/
.mypy_cache/
+63
View File
@@ -0,0 +1,63 @@
# NVIDIA Sidecar 限流代理
为 NVIDIA API 提供**优先级排队 + 令牌桶限流**的透明代理层。
## 快速启动
```bash
pip install .
nvidia-sidecar
```
监听 `127.0.0.1:9190`,代理到 NVIDIA API。
## 环境变量
| 变量 | 默认值 | 说明 |
|------|--------|------|
| `SIDECAR_HOST` | `127.0.0.1` | 监听地址 |
| `SIDECAR_PORT` | `9190` | 监听端口 |
| `SIDECAR_METRICS_PORT` | `9191` | Metrics 端口 |
| `SIDECAR_UPSTREAM` | `https://integrate.api.nvidia.com/v1` | 上游 API 地址 |
| `SIDECAR_API_KEY` | — | NVIDIA API Key(必填) |
| `SIDECAR_RATE_RPM` | `40` | 每分钟请求数限制 |
| `SIDECAR_BUCKET_CAPACITY` | `40` | 令牌桶容量 |
| `SIDECAR_TIMEOUT` | `6000` | 上游请求超时(秒) |
| `SIDECAR_QUEUE_MAX` | `500` | 队列最大长度 |
| `SIDECAR_LOW_TIMEOUT` | `2.0` | 低优先级令牌等待超时(秒) |
| `SIDECAR_FALLBACK_PASSTHROUGH` | `true` | 队列满时是否直通上游 |
| `SIDECAR_LOG_LEVEL` | `INFO` | 日志级别 |
## YAML 配置
```yaml
listen_port: 9292
rate_rpm: 60
upstream_api_key: "nvapi-xxx"
```
```bash
nvidia-sidecar --config /etc/nvidia-sidecar.yaml
```
## API 端点
| 路径 | 方法 | 说明 |
|------|------|------|
| `/v1/chat/completions` | POST | OpenAI Chat Completions 代理 |
| `/v1/completions` | POST | OpenAI Completions 代理(legacy |
| `/v1/embeddings` | POST | OpenAI Embeddings 代理 |
| `/v1/models` | GET | 模型列表代理 |
| `/health` | GET | 健康检查 |
| `/metrics` | GET | 指标查询 |
## 架构
```
请求 → 网关识别 → [NVIDIA: 优先级排队 → 令牌桶限流] → httpx → NVIDIA API
→ [非 NVIDIA: 直通] → httpx → 上游
```
- **四级优先级**: URGENT > HIGH > NORMAL > LOW(通过 `X-Priority` header 指定)
- **队列满策略**: PASSTHROUGH(直通)/ REJECT503/ DROP_LOWEST(丢弃最低优先级)
- **令牌桶**: 40 RPM,线程安全,支持阻塞/非阻塞消费
+41
View File
@@ -0,0 +1,41 @@
"""
NVIDIA Sidecar 限流代理 — 核心代理模块。
为 OpenAI Chat Completions 兼容 API 提供四层防护:
1. 请求接收(FastAPI
2. 网关识别 → 非 NVIDIA 直通
3. 优先级排队 → 令牌桶限流
4. httpx 异步转发到 NVIDIA 上游
"""
from __future__ import annotations
from nvidia_sidecar.config import SidecarConfig, load_config
from nvidia_sidecar.rate_limiter import (
Priority,
TokenBucket,
is_nvidia_gateway,
normalize_gateway_name,
)
from nvidia_sidecar.priority_queue import (
PriorityQueueItem,
PriorityRequestQueue,
QueueFullError,
QueueFullPassthrough,
QueueFullPolicy,
)
__version__ = "0.1.0"
__all__ = [
"SidecarConfig",
"load_config",
"Priority",
"TokenBucket",
"is_nvidia_gateway",
"normalize_gateway_name",
"PriorityQueueItem",
"PriorityRequestQueue",
"QueueFullError",
"QueueFullPassthrough",
"QueueFullPolicy",
]
+221
View File
@@ -0,0 +1,221 @@
"""
NVIDIA Sidecar 限流代理 — 配置管理模块 (§3.1)
集中管理 Sidecar 运行参数,支持环境变量覆盖和 YAML 配置文件。
"""
from __future__ import annotations
import os
import warnings
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
@dataclass
class SidecarConfig:
"""Sidecar 运行配置数据类。
所有字段可通过环境变量覆盖,优先级:环境变量 > YAML 配置文件 > 默认值。
"""
# ---- 网络 ----
listen_host: str = field(
default="127.0.0.1",
metadata={"env": "SIDECAR_HOST"},
)
listen_port: int = field(
default=9190,
metadata={"env": "SIDECAR_PORT"},
)
metrics_port: int = field(
default=9191,
metadata={"env": "SIDECAR_METRICS_PORT"},
)
# ---- 上游 ----
upstream_url: str = field(
default="https://integrate.api.nvidia.com/v1",
metadata={"env": "SIDECAR_UPSTREAM"},
)
upstream_api_key: str = field(
default="",
metadata={"env": "SIDECAR_API_KEY"},
)
# ---- 限流 ----
rate_rpm: int = field(
default=40,
metadata={"env": "SIDECAR_RATE_RPM"},
)
bucket_capacity: int = field(
default=40,
metadata={"env": "SIDECAR_BUCKET_CAPACITY"},
)
# ---- 超时 ----
request_timeout: float = field(
default=60.0,
metadata={"env": "SIDECAR_TIMEOUT"},
)
# ---- 队列 ----
queue_max_size: int = field(
default=500,
metadata={"env": "SIDECAR_QUEUE_MAX"},
)
low_priority_timeout: float = field(
default=2.0,
metadata={"env": "SIDECAR_LOW_TIMEOUT"},
)
# ---- 降级 ----
fallback_enabled_passthrough: bool = field(
default=True,
metadata={"env": "SIDECAR_FALLBACK_PASSTHROUGH"},
)
# ---- 日志 ----
log_level: str = field(
default="INFO",
metadata={"env": "SIDECAR_LOG_LEVEL"},
)
def _apply_env_overrides(config: SidecarConfig) -> SidecarConfig:
"""用环境变量覆盖配置字段。
遍历 SidecarConfig 的 dataclass fields,对每个声明了 ``metadata={"env": ...}``
的字段检查环境变量是否存在,存在则用对应类型转换后覆盖。
"""
import dataclasses as _dc
# 使用 typing.get_type_hints 解析 from __future__ import annotations
# 引入的字符串化类型注解 (PEP 563)
try:
resolved_types = __import__("typing").get_type_hints(type(config))
except Exception:
resolved_types = {}
for fld in _dc.fields(config):
env_key: str | None = fld.metadata.get("env")
if env_key is None:
continue
env_val = os.environ.get(env_key)
if env_val is None:
continue
target_type = resolved_types.get(fld.name, fld.type)
target_type_name: str = getattr(target_type, "__name__", str(target_type))
try:
if target_type is bool or target_type == "bool":
parsed: bool = env_val.strip().lower() in ("true", "1", "yes", "on")
setattr(config, fld.name, parsed)
elif target_type is int or target_type == "int":
setattr(config, fld.name, int(env_val))
elif target_type is float or target_type == "float":
setattr(config, fld.name, float(env_val))
else:
setattr(config, fld.name, env_val)
except (ValueError, TypeError) as exc:
warnings.warn(
f"无法将环境变量 {env_key}={env_val!r} 转换为 {target_type_name}: {exc}"
)
return config
def _validate_config(config: SidecarConfig) -> list[str]:
"""验证配置合理性,返回警告/问题列表。"""
issues: list[str] = []
# 端口冲突检查
if config.listen_port == config.metrics_port:
issues.append(
f"listen_port ({config.listen_port}) 与 metrics_port ({config.metrics_port}) 相同"
)
# rate_rpm 边界检查
if config.rate_rpm <= 0:
issues.append(
f"rate_rpm ({config.rate_rpm}) 无效,回退到默认值 40"
)
config.rate_rpm = 40
# queue_max_size 合理性
if config.queue_max_size <= 0:
issues.append(
f"queue_max_size ({config.queue_max_size}) 无效,回退到默认值 500"
)
config.queue_max_size = 500
# request_timeout 合理性
if config.request_timeout <= 0:
issues.append(
f"request_timeout ({config.request_timeout}) 无效,回退到默认值 60"
)
config.request_timeout = 60.0
elif config.request_timeout > 300.0:
issues.append(
f"request_timeout ({config.request_timeout}) 异常偏高,已截断为 300"
)
config.request_timeout = 300.0
return issues
def load_config(path: str | None = None) -> SidecarConfig:
"""加载 Sidecar 配置。
加载顺序(后者覆盖前者):
1. 默认值(SidecarConfig dataclass defaults
2. YAML 配置文件(如果 path 提供)
3. 环境变量覆盖
Args:
path: 可选 YAML 配置文件路径。为 None 时只使用默认值 + 环境变量。
Returns:
经过验证的 SidecarConfig 实例。
Raises:
FileNotFoundError: path 指定的文件不存在。
yaml.YAMLError: YAML 解析失败。
"""
config = SidecarConfig()
if path is not None:
import yaml
cfg_path = Path(path)
if not cfg_path.is_file():
raise FileNotFoundError(f"配置文件不存在: {cfg_path}")
try:
raw: dict[str, Any] = yaml.safe_load(cfg_path.read_text(encoding="utf-8")) or {}
except yaml.YAMLError as exc:
raise yaml.YAMLError(f"YAML 解析失败 ({cfg_path}): {exc}") from exc
# 覆盖已声明的字段
for fld_name in (
"listen_host", "listen_port", "metrics_port",
"upstream_url", "upstream_api_key",
"rate_rpm", "bucket_capacity",
"request_timeout",
"queue_max_size", "low_priority_timeout",
"fallback_enabled_passthrough",
"log_level",
):
if fld_name in raw:
setattr(config, fld_name, raw[fld_name])
# 环境变量覆盖(最高优先级)
config = _apply_env_overrides(config)
# 验证
issues = _validate_config(config)
for issue in issues:
warnings.warn(issue)
return config
+152
View File
@@ -0,0 +1,152 @@
"""
NVIDIA Sidecar 限流代理 — 健康检查端点 (§3.6)
提供 Kubernetes / systemd 兼容的健康检查:
GET /health — 存活检查
GET /health/ready — 就绪检查(含上游连通性)
"""
from __future__ import annotations
import asyncio
import time
from dataclasses import dataclass
from typing import Any
import httpx
@dataclass
class HealthService:
"""健康检查服务。
封装存活检查和就绪检查的逻辑,供 server.py 路由调用。
"""
start_time: float = 0.0
version: str = "0.1.0"
def __post_init__(self) -> None:
if self.start_time == 0.0:
self.start_time = time.time()
@property
def uptime_seconds(self) -> float:
"""服务运行时长(秒)。"""
return time.time() - self.start_time
async def check_upstream(
self,
upstream_url: str,
timeout: float = 5.0,
api_key: str = "",
) -> bool:
"""检查上游连通性。
Args:
upstream_url: NVIDIA API base URL。
timeout: 超时秒数。
api_key: 可选的 API Key 用于认证。
Returns:
True 上游可达。
"""
try:
headers: dict[str, str] = {}
if api_key:
headers["authorization"] = f"Bearer {api_key}"
async with httpx.AsyncClient(timeout=timeout) as client:
resp = await client.get(
f"{upstream_url.rstrip('/')}/v1/models",
headers=headers,
)
return resp.status_code < 500
except Exception:
return False
def check_queue_healthy(
self,
current_size: int,
max_size: int,
threshold_ratio: float = 0.9,
) -> bool:
"""检查队列是否健康(未接近满载)。
Args:
current_size: 当前队列长度。
max_size: 队列最大容量。
threshold_ratio: 告警阈值比例,默认 0.9。
Returns:
True 队列健康。
"""
if max_size <= 0:
return True
return current_size < max_size * threshold_ratio
def check_token_bucket_healthy(
self,
available_tokens: float,
capacity: int,
threshold: float = 0.05,
) -> bool:
"""检查令牌桶是否健康(token 未耗尽)。
Args:
available_tokens: 当前可用令牌数。
capacity: 桶容量。
threshold: 令牌数低于此比例视为不健康。
Returns:
True 令牌桶健康。
"""
if capacity <= 0:
return False
return available_tokens > capacity * threshold
def liveness(self) -> dict[str, Any]:
"""存活检查响应。
Returns:
liveness JSON payload。
"""
return {
"status": "ok",
"uptime": round(self.uptime_seconds, 1),
"version": self.version,
}
async def readiness(
self,
upstream_url: str,
upstream_api_key: str = "",
queue_current_size: int = 0,
queue_max_size: int = 500,
available_tokens: float = 0.0,
bucket_capacity: int = 40,
) -> dict[str, Any]:
"""就绪检查响应。
Args:
upstream_url: 上游 API 地址。
upstream_api_key: API Key。
queue_current_size: 当前队列长度。
queue_max_size: 队列最大容量。
available_tokens: 当前令牌数。
bucket_capacity: 桶容量。
Returns:
readiness JSON payload。
"""
upstream_ok = await self.check_upstream(upstream_url, api_key=upstream_api_key)
queue_ok = self.check_queue_healthy(queue_current_size, queue_max_size)
token_ok = self.check_token_bucket_healthy(available_tokens, bucket_capacity)
all_ready = upstream_ok and queue_ok and token_ok
return {
"ready": all_ready,
"upstream_reachable": upstream_ok,
"queue_healthy": queue_ok,
"token_bucket_healthy": token_ok,
}
+272
View File
@@ -0,0 +1,272 @@
"""
NVIDIA Sidecar 限流代理 — Prometheus 指标端点 (§3.5)
10 个指标,独立端口 :9191,与代理端口 :9190 分离。
"""
from __future__ import annotations
import time
import threading
from typing import Any
from prometheus_client import (
CollectorRegistry,
Counter,
Gauge,
Histogram,
generate_latest,
make_asgi_app,
)
class PrometheusMetrics:
"""Sidecar Prometheus 指标收集器。
线程安全,所有公开方法通过 ``threading.Lock`` 保护。
"""
def __init__(self, registry: CollectorRegistry | None = None) -> None:
"""初始化所有 10 个 Prometheus 指标。
Args:
registry: 可选自定义 RegistryNone 则使用默认全局 registry。
"""
self._registry: CollectorRegistry = registry or CollectorRegistry()
self._lock: threading.Lock = threading.Lock()
self._start_time: float = time.time()
# ---- 1. 总请求数(按优先级 + 状态分组) ----
self.requests_total: Counter = Counter(
"sidecar_requests_total",
"Total requests processed by priority and status",
labelnames=["priority", "status"],
registry=self._registry,
)
# ---- 2. 可用令牌数 ----
self.tokens_available: Gauge = Gauge(
"sidecar_tokens_available",
"Current number of available tokens",
registry=self._registry,
)
# ---- 3. 令牌生成速率 ----
self.tokens_rate: Gauge = Gauge(
"sidecar_tokens_rate",
"Current token generation rate (tokens per minute)",
registry=self._registry,
)
# ---- 4. 各优先级队列深度 ----
self.queue_depth: Gauge = Gauge(
"sidecar_queue_depth",
"Queue depth by priority",
labelnames=["priority"],
registry=self._registry,
)
# ---- 5. 队列等待时间 Histogram ----
self.queue_latency_seconds: Histogram = Histogram(
"sidecar_queue_latency_seconds",
"Request wait time in queue in seconds",
labelnames=["priority"],
buckets=(0.01, 0.05, 0.1, 0.5, 1.0, 2.0, 5.0, 10.0, 30.0, 60.0, 120.0),
registry=self._registry,
)
# ---- 6. 上游响应延迟 Histogram ----
self.upstream_latency_seconds: Histogram = Histogram(
"sidecar_upstream_latency_seconds",
"Upstream response latency in seconds",
labelnames=["model_id"],
buckets=(0.1, 0.5, 1.0, 2.0, 5.0, 10.0, 30.0, 60.0, 120.0, 300.0, 600.0),
registry=self._registry,
)
# ---- 7. 上游错误计数 ----
self.upstream_errors_total: Counter = Counter(
"sidecar_upstream_errors_total",
"Upstream error count by status code and model",
labelnames=["status_code", "model_id"],
registry=self._registry,
)
# ---- 8. 降级直通次数 ----
self.fallback_passthrough_total: Counter = Counter(
"sidecar_fallback_passthrough_total",
"Total fallback / passthrough events (queue full or sidecar unavailable)",
registry=self._registry,
)
# ---- 9. 健康状态 ----
self.health_status: Gauge = Gauge(
"sidecar_health_status",
"Sidecar health: 0=unhealthy, 1=healthy",
registry=self._registry,
)
# ---- 10. 运行时长 ----
self.uptime_seconds: Gauge = Gauge(
"sidecar_uptime_seconds",
"Process uptime in seconds",
registry=self._registry,
)
# 避退模式指标(附加,不计入基础 10 个)
self.retreat_state: Gauge = Gauge(
"sidecar_retreat_state",
"Adaptive retreat state: 0=NORMAL, 1=RETREAT, 2=RECOVER",
registry=self._registry,
)
self.effective_rate_rpm: Gauge = Gauge(
"sidecar_effective_rate_rpm",
"Current effective rate in RPM (after retreat adjustments)",
registry=self._registry,
)
self.upstream_429_rate: Gauge = Gauge(
"sidecar_upstream_429_rate",
"Upstream 429 rate over the retreat observation window (0.0-1.0)",
registry=self._registry,
)
# 初始化
self.health_status.set(1)
# ---- ASGI app 生成 ----
def build_asgi_app(self) -> Any:
"""生成 Prometheus ASGI 应用,挂载到独立端口。
Returns:
可传给 uvicorn 的 ASGI app。
"""
return make_asgi_app(registry=self._registry)
# ---- 指标记录方法 ----
def record_request(self, priority: str, status: str) -> None:
"""记录一次请求。
Args:
priority: 优先级名(URGENT / HIGH / NORMAL / LOW)。
status: 状态(success / ratelimited / error)。
"""
with self._lock:
self.requests_total.labels(priority=priority, status=status).inc()
def record_queue_latency(self, priority: str, seconds: float) -> None:
"""记录排队延迟。
Args:
priority: 优先级名。
seconds: 排队等待秒数。
"""
with self._lock:
self.queue_latency_seconds.labels(priority=priority).observe(seconds)
def record_upstream(self, status_code: int, model_id: str) -> None:
"""记录上游响应。
Args:
status_code: HTTP 状态码。
model_id: 模型标识符。
"""
with self._lock:
self.upstream_latency_seconds.labels(model_id=model_id).observe(0.0)
def record_upstream_error(self, status_code: int, model_id: str) -> None:
"""记录上游错误。
Args:
status_code: 错误 HTTP 状态码。
model_id: 模型标识符。
"""
with self._lock:
self.upstream_errors_total.labels(
status_code=str(status_code), model_id=model_id
).inc()
def record_upstream_latency(self, model_id: str, seconds: float) -> None:
"""记录上游响应延迟。
Args:
model_id: 模型标识符。
seconds: 响应延迟秒数。
"""
with self._lock:
self.upstream_latency_seconds.labels(model_id=model_id).observe(seconds)
def update_token_status(self, tokens: float, rate_per_minute: float) -> None:
"""更新令牌桶状态。
Args:
tokens: 当前可用令牌数。
rate_per_minute: 每分钟速率。
"""
with self._lock:
self.tokens_available.set(tokens)
self.tokens_rate.set(rate_per_minute)
def update_queue_depth(self, depths: dict[str, int]) -> None:
"""更新各优先级队列深度。
Args:
depths: {priority_name: count} 映射。
"""
with self._lock:
# 先清零所有已知标签再设置,避免残留旧值
for pri in ("URGENT", "HIGH", "NORMAL", "LOW"):
self.queue_depth.labels(priority=pri).set(depths.get(pri, 0))
def increment_fallback(self) -> None:
"""降级直通计数 +1。"""
with self._lock:
self.fallback_passthrough_total.inc()
def set_health(self, healthy: bool) -> None:
"""设置健康状态。
Args:
healthy: True=健康, False=不健康。
"""
with self._lock:
self.health_status.set(1 if healthy else 0)
def update_uptime(self) -> None:
"""更新运行时长。"""
with self._lock:
self.uptime_seconds.set(time.time() - self._start_time)
# ---- 避退模式指标 ----
def update_retreat_metrics(
self,
retreat_state: str,
effective_rate_rpm: float,
upstream_429_rate: float,
) -> None:
"""更新避退模式指标。
Args:
retreat_state: "normal" / "retreat" / "recover".
effective_rate_rpm: 当前实际速率 (RPM)。
upstream_429_rate: 上游 429 率 (0.0-1.0)。
"""
state_map: dict[str, int] = {"normal": 0, "retreat": 1, "recover": 2}
with self._lock:
self.retreat_state.set(state_map.get(retreat_state, 0))
self.effective_rate_rpm.set(effective_rate_rpm)
self.upstream_429_rate.set(upstream_429_rate)
# ---- 导出 ----
def generate_latest(self) -> bytes:
"""生成 Prometheus 文本格式的指标数据。
Returns:
Prometheus 文本格式 bytes。
"""
with self._lock:
self.update_uptime()
return generate_latest(self._registry)
+253
View File
@@ -0,0 +1,253 @@
"""
NVIDIA Sidecar 限流代理 — 四级优先级请求队列模块 (§3.3)
管理待处理的 NVIDIA API 请求,按优先级 + FIFO 出队。
支持三种队列满策略:PASSTHROUGH / REJECT / DROP_LOWEST。
"""
from __future__ import annotations
import asyncio
import heapq
import time
import uuid
from dataclasses import dataclass, field
from enum import Enum
from typing import Any
from nvidia_sidecar.rate_limiter import Priority
# ---------------------------------------------------------------------------
# 队列满策略
# ---------------------------------------------------------------------------
class QueueFullPolicy(str, Enum):
"""队列满时的处理策略。"""
PASSTHROUGH = "passthrough" # 直通上游,绕过排队(fail-open 子策略)
REJECT = "reject" # 返回 503 Service Unavailable
DROP_LOWEST = "drop_lowest" # 丢弃队列中最低优先级元素,插入新请求
# ---------------------------------------------------------------------------
# 队列元素
# ---------------------------------------------------------------------------
@dataclass(order=True)
class PriorityQueueItem:
"""优先级队列元素。
``sort_index`` 由 ``(priority, timestamp)`` 组成,
Python 的 ``__lt__`` 按字段顺序比较:先比 priority,再比 timestamp。
数值越小越优先(URGENT=1 优于 HIGH=2)。
"""
sort_index: tuple[int, float] = field(compare=True)
priority: Priority = field(compare=False)
request_id: str = field(compare=False)
payload: dict[str, Any] = field(compare=False)
enqueued_at: float = field(compare=False)
headers: dict[str, str] = field(default_factory=dict, compare=False)
# ---------------------------------------------------------------------------
# 优先级请求队列
# ---------------------------------------------------------------------------
class QueueFullError(Exception):
"""队列已满且策略为 REJECT 时抛出。"""
pass
class QueueFullPassthrough(Exception):
"""队列已满且策略为 PASSTHROUGH 时抛出,由调用方绕过队列直通上游。"""
pass
class PriorityRequestQueue:
"""异步线程安全的四级优先级请求队列。
内部使用 ``asyncio.Lock`` 保护并发操作,
基于 ``heapq`` + ``asyncio.Event`` 实现阻塞出队。
"""
def __init__(self, max_size: int = 500) -> None:
"""初始化优先级队列。
Args:
max_size: 队列最大容量。
Raises:
ValueError: max_size <= 0。
"""
if max_size <= 0:
raise ValueError(f"max_size 必须为正整数,当前值: {max_size}")
self.max_size: int = max_size
self._heap: list[PriorityQueueItem] = []
self._lock: asyncio.Lock = asyncio.Lock()
self._not_empty: asyncio.Event = asyncio.Event()
self._full_policy: QueueFullPolicy = QueueFullPolicy.PASSTHROUGH
# 统计
self._total_enqueued: int = 0
self._total_dequeued: int = 0
self._total_dropped: int = 0
# ---- 队列满策略 ----
def set_full_policy(self, policy: QueueFullPolicy) -> None:
"""设置队列满时的处理策略。
Args:
policy: QueueFullPolicy 枚举值。
"""
self._full_policy = policy
@property
def full_policy(self) -> QueueFullPolicy:
"""当前队列满策略。"""
return self._full_policy
# ---- 动态容量调整 ----
def set_max_size(self, new_size: int) -> tuple[bool, str]:
"""动态调整队列最大容量(热重载)。
缩小操作受保护:如果 new_size 小于当前排队数,拒绝变更并
提示当前队列深度。
Args:
new_size: 新的最大容量。
Returns:
(成功标志, 消息)。成功时标志为 True,消息含新旧容量对比;
失败时标志为 False,消息含拒绝原因和当前深度。
Raises:
ValueError: new_size <= 0。
"""
if new_size <= 0:
raise ValueError(f"max_size 必须为正整数,当前值: {new_size}")
current = len(self._heap)
if new_size < current:
return (False, f"拒绝缩小:新上限 {new_size} < 当前排队数 {current},需要先排空或提升上限")
old = self.max_size
self.max_size = new_size
return (True, f"队列上限已调整:{old}{new_size}{'(当前排队 ' + str(current) + '' if current > 0 else ''}")
# ---- 入队 ----
async def put(
self,
item: dict[str, Any],
priority: Priority = Priority.NORMAL,
headers: dict[str, str] | None = None,
) -> str:
"""将请求放入队列。
Args:
item: 请求体(JSON 序列化的 dict)。
priority: 请求优先级,默认 NORMAL。
headers: 原始请求 headers。
Returns:
分配的唯一 request_id。
Raises:
QueueFullError: 队列满且策略为 REJECT。
"""
request_id = str(uuid.uuid4())
headers = headers or {}
queue_item = PriorityQueueItem(
sort_index=(int(priority), time.monotonic()),
priority=priority,
request_id=request_id,
payload=item,
enqueued_at=time.monotonic(),
headers=headers,
)
async with self._lock:
queue_size = len(self._heap)
if queue_size >= self.max_size:
if self._full_policy == QueueFullPolicy.REJECT:
raise QueueFullError(
f"队列已满 ({queue_size}/{self.max_size}),策略: reject"
)
elif self._full_policy == QueueFullPolicy.DROP_LOWEST:
# 丢弃 heap 中优先级最低(值最大)的元素
# heap 是最小堆,找最大值需要遍历
max_val_item = max(self._heap, key=lambda x: x.sort_index)
self._heap.remove(max_val_item)
heapq.heapify(self._heap)
self._total_dropped += 1
# PASSTHROUGH 策略:不插入队列,抛异常让调用方绕过排队
else:
raise QueueFullPassthrough(
f"队列已满 ({queue_size}/{self.max_size}),策略: passthrough"
)
heapq.heappush(self._heap, queue_item)
self._total_enqueued += 1
self._not_empty.set()
return request_id
# ---- 出队 ----
async def get(self, timeout: float = 1.0) -> PriorityQueueItem | None:
"""从队列取出下一个元素(阻塞、优先级排序)。
Args:
timeout: 阻塞等待的最大秒数,默认 1.0。
Returns:
优先级最高的队列元素;超时无元素时返回 None。
"""
deadline = time.monotonic() + timeout
while True:
async with self._lock:
if self._heap:
item = heapq.heappop(self._heap)
self._total_dequeued += 1
if not self._heap:
self._not_empty.clear()
return item
# 队列为空,等待新元素入队
remaining = deadline - time.monotonic()
if remaining <= 0:
return None
try:
await asyncio.wait_for(
self._not_empty.wait(),
timeout=remaining,
)
except asyncio.TimeoutError:
return None
# ---- 状态查询 ----
async def get_queue_size(self) -> int:
"""返回当前队列长度。"""
async with self._lock:
return len(self._heap)
async def get_stats(self) -> dict[str, Any]:
"""返回队列统计信息。"""
async with self._lock:
depth_by_priority: dict[str, int] = {}
for item in self._heap:
key = item.priority.name
depth_by_priority[key] = depth_by_priority.get(key, 0) + 1
return {
"max_size": self.max_size,
"current_size": len(self._heap),
"total_enqueued": self._total_enqueued,
"total_dequeued": self._total_dequeued,
"total_dropped": self._total_dropped,
"depth_by_priority": depth_by_priority,
"full_policy": self._full_policy.value,
"utilization": len(self._heap) / self.max_size if self.max_size > 0 else 0.0,
}
+48
View File
@@ -0,0 +1,48 @@
[project]
name = "nvidia_sidecar"
version = "0.1.0"
description = "NVIDIA Sidecar 限流代理 — 为 NVIDIA API 提供优先级排队 + 令牌桶限流"
readme = "README.md"
license = { text = "MIT" }
requires-python = ">=3.12"
dependencies = [
"fastapi>=0.115",
"uvicorn[standard]>=0.34",
"httpx>=0.28",
"PyYAML>=6.0",
"structlog>=24.4",
"prometheus-client>=0.21",
"pydantic>=2.0",
]
[project.optional-dependencies]
dev = [
"pytest>=8.3",
"pytest-asyncio>=0.24",
"httpx>=0.28",
"mypy>=1.14",
"types-PyYAML",
]
[project.scripts]
nvidia-sidecar = "nvidia_sidecar.server:main"
[build-system]
requires = ["setuptools>=75", "wheel"]
build-backend = "setuptools.build_meta"
[tool.setuptools]
packages = ["nvidia_sidecar"]
[tool.setuptools.package-dir]
# Flat layout: __init__.py + all .py files at project root
"nvidia_sidecar" = "."
[tool.mypy]
python_version = "3.12"
strict = true
warn_return_any = true
warn_unused_configs = true
[[tool.mypy.overrides]]
module = "structlog.*"
ignore_missing_imports = true
+438
View File
@@ -0,0 +1,438 @@
"""
NVIDIA Sidecar 限流代理 — 令牌桶 + 网关识别模块 (§3.2)
从 BIZ-26 rate_limiter.py 提取核心限流逻辑,去除多线程调度器、缓存管理等。
保留:Priority, TokenBucket, is_nvidia_gateway, normalize_gateway_name。
"""
from __future__ import annotations
import time
import threading
from enum import IntEnum
from typing import Any
# ---------------------------------------------------------------------------
# 优先级枚举
# ---------------------------------------------------------------------------
class Priority(IntEnum):
"""请求优先级(数值越小优先级越高)。"""
URGENT = 1
HIGH = 2
NORMAL = 3
LOW = 4
# ---------------------------------------------------------------------------
# NVIDIA 网关别名集
# ---------------------------------------------------------------------------
NVIDIA_GATEWAY_ALIASES: set[str] = {
"nvidia",
"nvidia-gateway",
"nvidiavx",
"nvidiavx18088980513",
}
def is_nvidia_gateway(value: str | None) -> bool:
"""判断给定网关名/模型全路径是否属于 NVIDIA 网关。
Args:
value: 网关名(如 ``"nvidia"``)或模型全路径前缀
(如 ``"nvidia/deepseek-ai/deepseek-v4-pro"``)。
None 时直接返回 False。
Returns:
True 当 value 的 provider 部分匹配已知 NVIDIA 别名。
"""
if value is None:
return False
# 提取 provider 前缀:取 "/" 前第一个部分
provider = value.split("/", 1)[0].lower().strip()
return provider in NVIDIA_GATEWAY_ALIASES
def normalize_gateway_name(value: str | None) -> str | None:
"""规范化网关名:提取 provider 前缀并转为小写。
Args:
value: 网关名或模型全路径。None 时返回 None。
Returns:
provider 前缀的小写形式,或 None。
"""
if value is None:
return None
return value.split("/", 1)[0].lower().strip()
# ---------------------------------------------------------------------------
# 令牌桶(线程安全)
# ---------------------------------------------------------------------------
class TokenBucket:
"""线程安全的令牌桶实现。
支持固定速率令牌补充和消费,带有溢出保护和可选的阻塞等待。
"""
def __init__(self, rate: float = 40 / 60, capacity: int = 40) -> None:
"""初始化令牌桶。
Args:
rate: 令牌补充速率(令牌/秒)。默认 40/60 ≈ 0.667 token/s40 RPM)。
capacity: 桶最大容量(令牌数)。默认 40。
"""
self._rate: float = float(rate)
self._capacity: int = int(capacity)
self._tokens: float = float(capacity) # 启动时桶满
self._last_refill: float = time.monotonic()
self._lock: threading.Lock = threading.Lock()
# ---- 内部方法 ----
def _refill(self) -> None:
"""补充令牌(调用方需持有 _lock)。
根据距上次补充的时间差计算新增令牌数,不超过 capacity。
"""
now = time.monotonic()
elapsed = now - self._last_refill
if elapsed > 0 and self._rate > 0:
new_tokens = elapsed * self._rate
self._tokens = min(self._tokens + new_tokens, float(self._capacity))
self._last_refill = now
# ---- 公开方法 ----
def consume(self, tokens: int = 1) -> bool:
"""尝试立即消费令牌(非阻塞)。
Args:
tokens: 要消费的令牌数,默认 1。
Returns:
True 消费成功;False 令牌不足。
"""
if tokens <= 0:
return True
with self._lock:
self._refill()
if self._tokens >= tokens:
self._tokens -= tokens
return True
return False
def try_consume(self, tokens: int = 1, timeout: float = 2.0) -> bool:
"""尝试在指定时间内消费令牌(阻塞)。
Args:
tokens: 要消费的令牌数,默认 1。
timeout: 最大等待秒数,默认 2.0。
Returns:
True 在超时前成功消费;False 超时。
"""
if tokens <= 0:
return True
deadline = time.monotonic() + timeout
while True:
with self._lock:
self._refill()
if self._tokens >= tokens:
self._tokens -= tokens
return True
# 释放锁后计算剩余等待时间
remaining = deadline - time.monotonic()
if remaining <= 0:
return False
# 等待到下一个令牌应该补充的时间点
sleep_time = min(remaining, max(0.05, 1.0 / self._rate) if self._rate > 0 else remaining)
time.sleep(sleep_time)
def wait_for_token(self, timeout: float | None = None) -> bool:
"""等待并尝试消费 1 个令牌。
Args:
timeout: 最大等待秒数;None 表示无限等待(不推荐)。
Returns:
True 成功消费;False 超时。
"""
return self.try_consume(tokens=1, timeout=timeout if timeout is not None else float("inf"))
def get_status(self) -> dict[str, Any]:
"""获取令牌桶当前状态。
Returns:
包含 tokens, capacity, rate_per_minute, utilization 的字典。
"""
with self._lock:
self._refill()
rate_per_minute = self._rate * 60.0
utilization = 0.0 if self._capacity == 0 else (
(self._capacity - self._tokens) / self._capacity
)
return {
"tokens": round(self._tokens, 2),
"capacity": self._capacity,
"rate_per_minute": round(rate_per_minute, 1),
"utilization": round(utilization, 4),
}
# ---- 属性 ----
@property
def rate(self) -> float:
"""当前令牌补充速率(令牌/秒)。"""
return self._rate
@property
def capacity(self) -> int:
"""桶容量。"""
return self._capacity
# ---- 动态速率调整(供 AdaptiveTokenBucket 使用) ----
def set_rate(self, rate: float) -> None:
"""动态调整令牌补充速率(令牌/秒)。
Args:
rate: 新速率(令牌/秒)。
"""
with self._lock:
self._refill() # 先补充现有令牌再切换速率
self._rate = float(rate)
# ---------------------------------------------------------------------------
# 避退模式:AdaptiveTokenBucket (§ADR-009)
# ---------------------------------------------------------------------------
class RetreatState:
"""避退状态机常量。"""
NORMAL: str = "normal"
RETREAT: str = "retreat"
RECOVER: str = "recover"
class AdaptiveTokenBucket(TokenBucket):
"""自适应避退令牌桶(ADR-009)。
监控上游 429 率(60s 滑动窗口),自动调整发射速率:
- 429 率 < 5% → NORMAL,保持基准速率
- 429 率 5-10% → RETREAT,速率 × 0.75
- 429 率 10-20% → RETREAT,再次降速
- 429 率 > 20% → RETREAT,最低 5 RPM + 告警
- 连续 120s 429 率 < 2% → RECOVER,逐步 +2 RPM 恢复
线程安全,继承 TokenBucket 的所有公共接口。
"""
# ADR-009 参数(可通过构造函数覆盖)
RETREAT_WINDOW_SECONDS: float = 60.0
RETREAT_429_THRESHOLD: float = 0.05
RETREAT_FACTOR: float = 0.75
RETREAT_MIN_RPM: float = 5.0
RECOVER_WINDOW_SECONDS: float = 120.0
RECOVER_429_THRESHOLD: float = 0.02
RECOVER_INCREMENT_RPM: float = 2.0
def __init__(
self,
rate: float = 40 / 60,
capacity: int = 40,
*,
retreat_window_seconds: float = 60.0,
retreat_429_threshold: float = 0.05,
retreat_factor: float = 0.75,
retreat_min_rpm: float = 5.0,
recover_window_seconds: float = 120.0,
recover_429_threshold: float = 0.02,
recover_increment_rpm: float = 2.0,
) -> None:
"""初始化自适应避退令牌桶。
Args:
rate: 基准令牌补充速率(令牌/秒)。默认 40/60 ≈ 0.667 token/s。
capacity: 桶最大容量。默认 40。
retreat_window_seconds: 429 率滑动窗口大小(秒)。
retreat_429_threshold: 触发避退的 429 率阈值。
retreat_factor: 每次避退速率乘数。
retreat_min_rpm: 避退最低 RPM。
recover_window_seconds: 恢复观察窗口大小(秒)。
recover_429_threshold: 触发恢复的 429 率阈值。
recover_increment_rpm: 每次恢复增加的 RPM。
"""
super().__init__(rate=rate, capacity=capacity)
# 基准速率(不变)
self._base_rate: float = float(rate)
# 避退参数
self.RETREAT_WINDOW_SECONDS = retreat_window_seconds
self.RETREAT_429_THRESHOLD = retreat_429_threshold
self.RETREAT_FACTOR = retreat_factor
self.RETREAT_MIN_RPM = retreat_min_rpm
self.RECOVER_WINDOW_SECONDS = recover_window_seconds
self.RECOVER_429_THRESHOLD = recover_429_threshold
self.RECOVER_INCREMENT_RPM = recover_increment_rpm
# 避退状态机
self._retreat_state: str = RetreatState.NORMAL
# 429 滑动窗口:[(timestamp, is_429), ...]
self._429_window: list[tuple[float, bool]] = []
# 上次状态变更时间
self._last_state_change: float = time.monotonic()
# 避退状态锁(RLock 防止 evaluate_retreat() → get_429_rate() 重入死锁)
self._retreat_lock: threading.RLock = threading.RLock()
# ---- 429 反馈 ----
def record_response(self, is_429: bool) -> None:
"""记录一次上游响应是否为 429。
Args:
is_429: True 表示上游返回了 429。
"""
now = time.monotonic()
with self._retreat_lock:
self._429_window.append((now, is_429))
# 清理超出观察窗口的旧记录
cutoff = now - max(
self.RETREAT_WINDOW_SECONDS,
self.RECOVER_WINDOW_SECONDS,
)
self._429_window = [
(ts, flag) for ts, flag in self._429_window
if ts >= cutoff
]
def get_429_rate(self, window_seconds: float | None = None) -> float:
"""获取指定窗口内的 429 率。
Args:
window_seconds: 滑动窗口大小;None 使用 RETREAT_WINDOW_SECONDS。
Returns:
0.0-1.0 之间的 429 率。
"""
ws = window_seconds or self.RETREAT_WINDOW_SECONDS
now = time.monotonic()
with self._retreat_lock:
in_window = [flag for ts, flag in self._429_window if now - ts <= ws]
if not in_window:
return 0.0
return sum(1 for f in in_window if f) / len(in_window)
# ---- 避退状态评估 ----
def evaluate_retreat(self) -> str:
"""评估并更新避退状态,返回新状态名。
每次调用根据当前 429 率 + 持续时间决定是否进入 RETREAT / RECOVER。
Returns:
"normal" / "retreat" / "recover"
"""
now = time.monotonic()
with self._retreat_lock:
retreat_rate = self.get_429_rate(self.RETREAT_WINDOW_SECONDS)
recover_rate = self.get_429_rate(self.RECOVER_WINDOW_SECONDS)
if self._retreat_state == RetreatState.NORMAL:
if retreat_rate >= self.RETREAT_429_THRESHOLD:
self._retreat_state = RetreatState.RETREAT
self._last_state_change = now
self._apply_retreat()
elif self._retreat_state == RetreatState.RETREAT:
# 持续高 429 率 → 再次降速
if retreat_rate >= self.RETREAT_429_THRESHOLD * 2:
# 429 > 10%,再次降速
if self._rate > self.RETREAT_MIN_RPM / 60.0:
self._apply_retreat()
elif recover_rate < self.RECOVER_429_THRESHOLD:
time_in_low = now - self._last_state_change
if time_in_low >= self.RECOVER_WINDOW_SECONDS:
self._retreat_state = RetreatState.RECOVER
self._last_state_change = now
self._apply_recover()
elif self._retreat_state == RetreatState.RECOVER:
if retreat_rate >= self.RETREAT_429_THRESHOLD:
# 恢复期间 429 回升,重新进入避退
self._retreat_state = RetreatState.RETREAT
self._last_state_change = now
self._apply_retreat()
elif self._rate >= self._base_rate:
# 已恢复到基准速率
self._rate = self._base_rate
self._retreat_state = RetreatState.NORMAL
self._last_state_change = now
else:
# 继续逐步恢复
self._apply_recover()
return self._retreat_state
def _apply_retreat(self) -> None:
"""执行一次避退降速。"""
new_rate: float = max(
self.RETREAT_MIN_RPM / 60.0,
self._rate * self.RETREAT_FACTOR,
)
self._rate = new_rate
def _apply_recover(self) -> None:
"""执行一次恢复提速。"""
increment: float = self.RECOVER_INCREMENT_RPM / 60.0
new_rate: float = min(self._base_rate, self._rate + increment)
self._rate = new_rate
# ---- 状态查询 ----
def get_retreat_state(self) -> str:
"""获取当前避退状态。
Returns:
"normal" / "retreat" / "recover"
"""
with self._retreat_lock:
return self._retreat_state
def get_effective_rate_rpm(self) -> float:
"""获取当前实际速率(RPM),考虑避退乘数。
Returns:
当前每分钟速率。
"""
with self._lock:
return self._rate * 60.0
def get_base_rate_rpm(self) -> float:
"""获取基准速率(RPM),即未避退时的速率。
Returns:
基准每分钟速率。
"""
return self._base_rate * 60.0
def reset_to_base(self) -> None:
"""手动重置到基准速率(用于运维干预)。"""
with self._retreat_lock:
self._rate = self._base_rate
self._retreat_state = RetreatState.NORMAL
self._last_state_change = time.monotonic()
self._429_window.clear()
+813
View File
@@ -0,0 +1,813 @@
"""
NVIDIA Sidecar 限流代理 — FastAPI 代理主入口 (§3.4)
完整的 API 代理链路:
接收 → 网关识别 → [NVIDIA: 排队 → 令牌限流] → httpx 转发 → 返回
非 NVIDIA 请求直通上游,NVIDIA 请求经过四级优先级队列 + 令牌桶限流。
"""
from __future__ import annotations
import asyncio
import logging
import time
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
from typing import Any
import httpx
import structlog
import uvicorn
from fastapi import FastAPI, Request, Response
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse, StreamingResponse
from nvidia_sidecar.config import load_config, SidecarConfig
from nvidia_sidecar.rate_limiter import (
Priority,
AdaptiveTokenBucket,
is_nvidia_gateway,
)
from nvidia_sidecar.priority_queue import (
PriorityRequestQueue,
QueueFullError,
QueueFullPassthrough,
QueueFullPolicy,
)
from nvidia_sidecar.metrics import PrometheusMetrics
from nvidia_sidecar.health import HealthService
from nvidia_sidecar.webui import webui_router
# ---------------------------------------------------------------------------
# 结构化日志
# ---------------------------------------------------------------------------
structlog.configure(
processors=[
structlog.stdlib.filter_by_level,
structlog.stdlib.add_logger_name,
structlog.stdlib.add_log_level,
structlog.stdlib.PositionalArgumentsFormatter(),
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.StackInfoRenderer(),
structlog.processors.format_exc_info,
structlog.processors.UnicodeDecoder(),
structlog.processors.JSONRenderer(),
],
context_class=dict,
logger_factory=structlog.PrintLoggerFactory(),
wrapper_class=structlog.stdlib.BoundLogger,
cache_logger_on_first_use=True,
)
logger: structlog.stdlib.BoundLogger = structlog.get_logger("nvidia_sidecar")
# ---------------------------------------------------------------------------
# 全局状态(通过 lifespan 初始化,模块级引用方便路由访问)
# ---------------------------------------------------------------------------
_config: SidecarConfig
_http_client: httpx.AsyncClient
_priority_queue: PriorityRequestQueue
_token_bucket: AdaptiveTokenBucket
_prometheus: PrometheusMetrics
_health_service: HealthService
_pending_requests: dict[str, tuple[asyncio.Future[httpx.Response], float]]
"""request_id → (response future, enqueued_at) 的映射。"""
_metrics_task: asyncio.Task[None] | None = None
# 统计计数器(受 _stats_lock 保护, 修复梁思筑评审 #1: data race
_stats: dict[str, int] = {
"total_requests": 0,
"nvidia_requests": 0,
"passthrough_requests": 0,
"ratelimited_requests": 0,
"queue_full_rejects": 0,
"upstream_errors": 0,
"start_time": 0,
}
_stats_lock: asyncio.Lock = asyncio.Lock()
# ---------------------------------------------------------------------------
# 工具函数
# ---------------------------------------------------------------------------
async def _increment_stat(key: str, delta: int = 1) -> None:
"""线程安全的 _stats 计数器自增(梁思筑评审 #1 修复:消除 data race)。"""
async with _stats_lock:
_stats[key] = _stats.get(key, 0) + delta
def _extract_model(body: Any) -> str | None:
"""从请求体中提取模型标识符(兼容 OpenAI Chat/Completions 格式)。
Args:
body: 已解析的 JSON 请求体。
Returns:
模型标识符字符串,或 None。
"""
if isinstance(body, dict):
return str(body.get("model", "")) or None
return None
def _resolve_priority(headers: dict[str, str]) -> Priority:
"""从请求 headers 解析优先级。
检查 ``X-Priority`` header,值为 ``urgent``/``high``/``normal``/``low``
不区分大小写。默认 NORMAL。
"""
raw = headers.get("x-priority", "").strip().lower()
mapping: dict[str, Priority] = {
"urgent": Priority.URGENT,
"high": Priority.HIGH,
"normal": Priority.NORMAL,
"low": Priority.LOW,
}
return mapping.get(raw, Priority.NORMAL)
# ---------------------------------------------------------------------------
# 上游转发
# ---------------------------------------------------------------------------
async def _forward_to_upstream(
method: str,
path: str,
body: bytes | None,
headers: dict[str, str],
stream: bool = False,
) -> httpx.Response:
"""将请求转发到 NVIDIA 上游 API。
Args:
method: HTTP 方法。
path: 请求路径(如 ``/v1/chat/completions``)。
body: 原始请求体 bytes。
headers: 要转发的请求 headers(会追加 Authorization)。
stream: 是否请求流式响应。
Returns:
httpx.Response 对象。
Raises:
httpx.HTTPError: HTTP 请求失败。
"""
upstream_url = _config.upstream_url.rstrip("/") + path
forward_headers: dict[str, str] = {
k: v for k, v in headers.items()
if k.lower() not in ("host", "content-length", "transfer-encoding")
}
if _config.upstream_api_key:
forward_headers["authorization"] = f"Bearer {_config.upstream_api_key}"
elif "authorization" not in {k.lower() for k in forward_headers}:
forward_headers["authorization"] = "Bearer nvidia"
try:
req = _http_client.build_request(
method=method,
url=upstream_url,
headers=forward_headers,
content=body,
timeout=_config.request_timeout,
)
response = await _http_client.send(req, stream=stream)
return response
except httpx.TimeoutException:
logger.warning("upstream_timeout", path=path, timeout=_config.request_timeout)
raise
except httpx.HTTPError as exc:
logger.error("upstream_error", path=path, error=str(exc))
raise
# ---------------------------------------------------------------------------
# worker 协程:消费优先级队列 + 令牌桶 + 转发
# ---------------------------------------------------------------------------
async def _worker_loop() -> None:
"""后台 worker:持续从优先级队列取请求 → 令牌限流 → 转发 → 设置 future 结果。"""
log = logger.bind(worker="main")
log.info("worker_started")
while True:
try:
queue_item = await _priority_queue.get(timeout=1.0)
if queue_item is None:
continue
request_id = queue_item.request_id
payload = queue_item.payload
headers = queue_item.headers
enqueued_at = queue_item.enqueued_at
# 查找对应的 pending future
pending_entry = _pending_requests.get(request_id)
if pending_entry is None:
log.warning("orphan_request", request_id=request_id)
continue
future, _ = pending_entry
# 低优先级令牌等待超时处理
if queue_item.priority == Priority.LOW:
# 放线程池执行阻塞的令牌桶调用
got_token = await asyncio.to_thread(
_token_bucket.try_consume,
tokens=1,
timeout=_config.low_priority_timeout,
)
if not got_token:
log.info("low_priority_timeout", request_id=request_id)
await _increment_stat("ratelimited_requests")
_prometheus.record_request(queue_item.priority.name, "ratelimited")
if not future.done():
future.set_exception(
_RateLimitedError(
f"低优先级请求令牌等待超时 ({_config.low_priority_timeout}s)"
)
)
_pending_requests.pop(request_id, None)
continue
else:
# 非低优先级:在 worker 内轮询等待令牌,避免重入队导致 future 悬挂
# (重入队会生成新 request_id,原 future 永不 resolve → 客户端永久 hang
got_token = await asyncio.to_thread(_token_bucket.consume, tokens=1)
if not got_token:
token_deadline = time.monotonic() + _config.request_timeout
while not got_token:
await asyncio.sleep(0.1)
got_token = await asyncio.to_thread(_token_bucket.consume, tokens=1)
if time.monotonic() > token_deadline:
break
if not got_token:
log.warning(
"token_wait_timeout",
request_id=request_id,
priority=queue_item.priority.name,
timeout=_config.request_timeout,
)
await _increment_stat("ratelimited_requests")
_prometheus.record_request(queue_item.priority.name, "ratelimited")
if not future.done():
future.set_exception(
_RateLimitedError(
f"令牌等待超时 ({_config.request_timeout:.0f}s)"
)
)
_pending_requests.pop(request_id, None)
continue
# 转发到上游
upstream_start = time.monotonic()
try:
path = headers.get("x-original-path", "/v1/chat/completions")
method = headers.get("x-original-method", "POST")
# 过滤内部 headers
clean_headers = {
k: v for k, v in headers.items()
if not k.startswith("x-original-") and not k.startswith("x-request-id")
}
resp = await _forward_to_upstream(
method=method,
path=path,
body=payload.get("_raw_body"),
headers=clean_headers,
stream=payload.get("stream", False),
)
upstream_latency = time.monotonic() - upstream_start
queue_latency = time.monotonic() - enqueued_at
total_latency = upstream_latency + queue_latency
is_429: bool = resp.status_code == 429
_token_bucket.record_response(is_429)
# 避退状态评估 + 指标更新
_token_bucket.evaluate_retreat()
retreat_state = _token_bucket.get_retreat_state()
effective_rpm = _token_bucket.get_effective_rate_rpm()
upstream_429_rate = _token_bucket.get_429_rate()
_prometheus.update_retreat_metrics(retreat_state, effective_rpm, upstream_429_rate)
log.info(
"request_completed",
request_id=request_id,
status=resp.status_code,
upstream_latency=round(upstream_latency, 3),
queue_latency=round(queue_latency, 3),
total_latency=round(total_latency, 3),
retreat_state=retreat_state,
effective_rpm=round(effective_rpm, 1),
)
# 记录 Prometheus 指标
model_id = _extract_model(payload) or "unknown"
_prometheus.record_upstream_latency(model_id, upstream_latency)
if not resp.is_success:
_prometheus.record_upstream_error(resp.status_code, model_id)
_prometheus.record_request(queue_item.priority.name, "success" if resp.is_success else "error")
_prometheus.record_queue_latency(queue_item.priority.name, queue_latency)
if not future.done():
future.set_result(resp)
except (httpx.HTTPError, OSError) as exc:
log.error("upstream_request_failed", request_id=request_id, error=str(exc))
await _increment_stat("upstream_errors")
_prometheus.record_request(queue_item.priority.name, "error")
_prometheus.set_health(False)
if not future.done():
future.set_exception(exc)
_pending_requests.pop(request_id, None)
except asyncio.CancelledError:
log.info("worker_cancelled")
break
except Exception:
log.exception("worker_unexpected_error")
# ---------------------------------------------------------------------------
# PASSTHROUGH 直通路径(队列满 + PASSTHROUGH 策略)
# ---------------------------------------------------------------------------
async def _passthrough_with_rate_limit(
request: Request,
path: str,
body_bytes: bytes,
raw_headers: dict[str, str],
priority: Priority,
) -> Response:
"""队列满时的 PASSSTHROUGH 直通路径:仍受令牌桶限流,但不排队。
Args:
request: FastAPI Request。
path: 请求路径。
body_bytes: 原始请求体。
raw_headers: 请求 headers。
priority: 请求优先级。
Returns:
FastAPI Response。
"""
await _increment_stat("passthrough_requests")
_prometheus.increment_fallback()
# 低优先级走令牌桶等待
if priority == Priority.LOW:
got_token = await asyncio.to_thread(
_token_bucket.try_consume,
tokens=1,
timeout=_config.low_priority_timeout,
)
if not got_token:
await _increment_stat("ratelimited_requests")
_prometheus.record_request(priority.name, "ratelimited")
return JSONResponse(
status_code=429,
content={
"error": {
"message": f"令牌不足(队列满 + passthrough),超时 {_config.low_priority_timeout}s",
"type": "RateLimitedError",
}
},
)
else:
got_token = await asyncio.to_thread(_token_bucket.consume, tokens=1)
if not got_token:
# 非低优先级轮询等待,使用 config.request_timeout 替代硬编码 30s
# (严维序评审 minor / 梁思筑评审 #3hot-reload 假生效修复)
deadline = time.monotonic() + _config.request_timeout
while not got_token:
await asyncio.sleep(0.1)
got_token = await asyncio.to_thread(_token_bucket.consume, tokens=1)
if time.monotonic() > deadline:
await _increment_stat("ratelimited_requests")
_prometheus.record_request(priority.name, "ratelimited")
return JSONResponse(
status_code=429,
content={
"error": {
"message": f"令牌不足(队列满 + passthrough),等待超时 {_config.request_timeout:.0f}s",
"type": "RateLimitedError",
}
},
)
# 拿到令牌,直接转发
try:
clean_headers = {k: v for k, v in raw_headers.items()}
resp = await _forward_to_upstream(
method=request.method,
path=path,
body=body_bytes if body_bytes else None,
headers=clean_headers,
stream=False,
)
retreat_state = _token_bucket.get_retreat_state()
_token_bucket.evaluate_retreat()
_prometheus.update_retreat_metrics(
retreat_state,
_token_bucket.get_effective_rate_rpm(),
_token_bucket.get_429_rate(),
)
return _build_response(resp)
except Exception as exc:
status, msg = _map_exception(exc)
logger.error("passthrough_error", path=path, error=str(exc))
_prometheus.set_health(False)
return JSONResponse(
status_code=status,
content={"error": {"message": msg, "type": type(exc).__name__}},
)
# ---------------------------------------------------------------------------
# 自定义异常
# ---------------------------------------------------------------------------
class _RateLimitedError(Exception):
"""429 限流错误。"""
pass
# ---------------------------------------------------------------------------
# 异常处理矩阵 (§3.4)
# ---------------------------------------------------------------------------
_EXCEPTION_MATRIX: dict[type[Exception], tuple[int, str]] = {
_RateLimitedError: (429, "Too Many Requests — 令牌不足"),
QueueFullError: (503, "Service Unavailable — 队列已满"),
httpx.TimeoutException: (504, "Gateway Timeout — 上游超时"),
httpx.ConnectError: (502, "Bad Gateway — 上游连接失败"),
httpx.HTTPStatusError: (502, "Bad Gateway — 上游返回错误状态"),
}
def _map_exception(exc: Exception) -> tuple[int, str]:
"""将异常映射为 HTTP 状态码 + 错误信息。"""
for exc_type, (status, msg) in _EXCEPTION_MATRIX.items():
if isinstance(exc, exc_type):
return status, msg
return 500, f"Internal Server Error — {type(exc).__name__}"
# ---------------------------------------------------------------------------
# FastAPI 应用 + lifespan
# ---------------------------------------------------------------------------
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator[None, Any]:
"""应用生命周期管理:初始化/清理全局资源。"""
global _config, _http_client, _priority_queue, _token_bucket, _pending_requests
global _prometheus, _health_service, _metrics_task
# 启动
_config = load_config()
logging.getLogger().setLevel(_config.log_level.upper())
_http_client = httpx.AsyncClient(
timeout=httpx.Timeout(_config.request_timeout),
limits=httpx.Limits(
max_connections=100,
max_keepalive_connections=20,
),
)
_priority_queue = PriorityRequestQueue(max_size=_config.queue_max_size)
_token_bucket = AdaptiveTokenBucket(
rate=_config.rate_rpm / 60.0,
capacity=_config.bucket_capacity,
)
_prometheus = PrometheusMetrics()
_health_service = HealthService()
_pending_requests = {}
_stats["start_time"] = int(time.time())
# 启动 worker 协程
worker_task = asyncio.create_task(_worker_loop())
# 在独立端口 :9191 启动 Prometheus metrics 服务器
metrics_app = _prometheus.build_asgi_app()
metrics_config = uvicorn.Config(
metrics_app,
host=_config.listen_host,
port=_config.metrics_port,
log_level="error",
)
metrics_server = uvicorn.Server(metrics_config)
_metrics_task = asyncio.create_task(metrics_server.serve())
# CORS 中间件(严维序评审 #8)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=False,
allow_methods=["*"],
allow_headers=["*"],
)
# 挂载 webui 子路由
app.include_router(webui_router)
# upstream_api_key 启动检查(严维序评审 #5)
if not _config.upstream_api_key:
logger.warning(
"upstream_api_key_empty",
message="SIDECAR_API_KEY 未设置,NVIDIA 请求将因 401 认证失败",
)
logger.info(
"sidecar_started",
host=_config.listen_host,
port=_config.listen_port,
metrics_port=_config.metrics_port,
rate_rpm=_config.rate_rpm,
queue_max=_config.queue_max_size,
retreat_enabled=True,
)
yield # app 运行中
# 关闭
worker_task.cancel()
try:
await worker_task
except asyncio.CancelledError:
pass
if _metrics_task is not None:
_metrics_task.cancel()
try:
await _metrics_task
except asyncio.CancelledError:
pass
await _http_client.aclose()
logger.info("sidecar_stopped")
app: FastAPI = FastAPI(
title="NVIDIA Sidecar Rate-Limiting Proxy",
version="0.1.0",
lifespan=lifespan,
)
# ---------------------------------------------------------------------------
# 核心代理处理器
# ---------------------------------------------------------------------------
async def _handle_proxy_request(request: Request, path: str) -> Response:
"""统一的代理请求处理入口。
执行完整链路:
1. 解析请求体 → 提取 model
2. 网关识别 → 非 NVIDIA 直通
3. NVIDIA → 排队 + 令牌限流 + 转发
"""
await _increment_stat("total_requests")
# 解析请求
body_bytes: bytes = await request.body()
raw_headers: dict[str, str] = dict(request.headers)
# 尝试解析 JSON body
body_json: dict[str, Any] = {}
try:
if body_bytes:
body_json = __import__("json").loads(body_bytes)
except (ValueError, TypeError):
body_json = {}
# 提取 model 进行网关识别
model: str | None = _extract_model(body_json)
is_nvidia: bool = is_nvidia_gateway(model)
# 非 NVIDIA → 直接转发
if not is_nvidia:
await _increment_stat("passthrough_requests")
try:
resp = await _forward_to_upstream(
method=request.method,
path=path,
body=body_bytes if body_bytes else None,
headers=raw_headers,
stream=body_json.get("stream", False),
)
return _build_response(resp)
except Exception as exc:
status, msg = _map_exception(exc)
logger.error("passthrough_error", path=path, error=str(exc))
return JSONResponse(
status_code=status,
content={"error": {"message": msg, "type": type(exc).__name__}},
)
# NVIDIA → 排队 + 限流 + 转发
await _increment_stat("nvidia_requests")
priority: Priority = _resolve_priority(raw_headers)
# 注入内部元数据到 payload
payload_for_queue: dict[str, Any] = dict(body_json)
payload_for_queue["_raw_body"] = body_bytes
# 尝试入队;PASSTHROUGH 策略下队列满时走直通路径
try:
request_id = await _priority_queue.put(
item=payload_for_queue,
priority=priority,
headers={
**raw_headers,
"x-original-path": path,
"x-original-method": request.method,
},
)
except QueueFullError:
await _increment_stat("queue_full_rejects")
return JSONResponse(
status_code=503,
content={
"error": {
"message": "队列已满,当前策略: reject",
"type": "QueueFullError",
}
},
)
except QueueFullPassthrough:
# 队列满 + PASSTHROUGH:绕过排队,尝试令牌桶后直接转发
await _increment_stat("passthrough_requests")
logger.info("queue_full_passthrough", path=path)
return await _passthrough_with_rate_limit(request, path, body_bytes, raw_headers, priority)
# 创建 future 并注册到 pending
loop = asyncio.get_running_loop()
future: asyncio.Future[httpx.Response] = loop.create_future()
_pending_requests[request_id] = (future, time.monotonic())
try:
# 等待 worker 完成处理
resp = await future
return _build_response(resp)
except _RateLimitedError as exc:
return JSONResponse(
status_code=429,
content={
"error": {
"message": str(exc),
"type": "RateLimitedError",
}
},
)
except Exception as exc:
status, msg = _map_exception(exc)
logger.error("proxy_error", path=path, request_id=request_id, error=str(exc))
return JSONResponse(
status_code=status,
content={"error": {"message": msg, "type": type(exc).__name__}},
)
def _build_response(resp: httpx.Response) -> Response:
"""将 httpx.Response 转换为 FastAPI Response。
支持 JSON 和流式 (SSE) 两种响应类型。
"""
content_type = resp.headers.get("content-type", "")
# 流式响应 (SSE)
if "text/event-stream" in content_type or "stream" in content_type:
return StreamingResponse(
content=resp.aiter_bytes(),
status_code=resp.status_code,
headers={
k: v for k, v in resp.headers.items()
if k.lower() not in ("content-encoding", "transfer-encoding")
},
media_type="text/event-stream",
)
# 普通 JSON 响应
return Response(
content=resp.content,
status_code=resp.status_code,
headers={
k: v for k, v in resp.headers.items()
if k.lower() not in ("content-encoding", "transfer-encoding")
},
media_type=content_type or "application/json",
)
# ---------------------------------------------------------------------------
# 路由
# ---------------------------------------------------------------------------
@app.get("/health")
async def health() -> dict[str, Any]:
"""存活检查 (liveness)。"""
return _health_service.liveness()
@app.get("/health/ready")
async def health_ready() -> dict[str, Any]:
"""就绪检查 (readiness),含上游连通性。"""
queue_size = await _priority_queue.get_queue_size()
bucket_status = _token_bucket.get_status()
return await _health_service.readiness(
upstream_url=_config.upstream_url,
upstream_api_key=_config.upstream_api_key or "",
queue_current_size=queue_size,
queue_max_size=_config.queue_max_size,
available_tokens=bucket_status["tokens"],
bucket_capacity=bucket_status["capacity"],
)
@app.get("/status")
async def status() -> dict[str, Any]:
"""调试用:限流器 + 队列 + 避退完整状态。"""
queue_stats = await _priority_queue.get_stats()
bucket_status = _token_bucket.get_status()
return {
"requests": {
"total": _stats["total_requests"],
"nvidia": _stats["nvidia_requests"],
"passthrough": _stats["passthrough_requests"],
"ratelimited": _stats["ratelimited_requests"],
},
"errors": {
"queue_full_rejects": _stats["queue_full_rejects"],
"upstream_errors": _stats["upstream_errors"],
},
"queue": queue_stats,
"token_bucket": bucket_status,
"retreat": {
"state": _token_bucket.get_retreat_state(),
"effective_rpm": round(_token_bucket.get_effective_rate_rpm(), 1),
"base_rpm": round(_token_bucket.get_base_rate_rpm(), 1),
"upstream_429_rate": round(_token_bucket.get_429_rate(), 4),
},
"uptime_seconds": int(time.time() - _stats["start_time"]) if _stats["start_time"] else 0,
}
# ---- OpenAI 兼容端点 ----
@app.post("/v1/chat/completions")
async def chat_completions(request: Request) -> Response:
"""OpenAI Chat Completions API 代理(含流式支持)。"""
return await _handle_proxy_request(request, "/v1/chat/completions")
@app.post("/v1/completions")
async def completions(request: Request) -> Response:
"""OpenAI Completions API 代理(legacy)。"""
return await _handle_proxy_request(request, "/v1/completions")
@app.post("/v1/embeddings")
async def embeddings(request: Request) -> Response:
"""OpenAI Embeddings API 代理。"""
return await _handle_proxy_request(request, "/v1/embeddings")
@app.get("/v1/models")
@app.get("/v1/models/{model_id:path}")
async def list_models(request: Request, model_id: str | None = None) -> Response:
"""OpenAI Models API 代理。"""
path = f"/v1/models/{model_id}" if model_id else "/v1/models"
return await _handle_proxy_request(request, path)
# ---- 通用代理(catch-all 用于非标准 NVIDIA 端点) ----
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"])
async def catch_all(request: Request, path: str) -> Response:
"""通用代理端点:转发任何未匹配的路径到上游。"""
target_path = f"/{path}" if not path.startswith("/") else path
return await _handle_proxy_request(request, target_path)
# ---------------------------------------------------------------------------
# 入口
# ---------------------------------------------------------------------------
def main() -> None:
"""开发/调试入口。"""
import uvicorn
cfg: SidecarConfig = load_config()
uvicorn.run(
"nvidia_sidecar.server:app",
host=cfg.listen_host,
port=cfg.listen_port,
log_level=cfg.log_level.lower(),
)
if __name__ == "__main__":
main()
@@ -0,0 +1,261 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NVIDIA Sidecar — 实时仪表盘</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; padding: 24px; }
h1 { font-size: 22px; font-weight: 600; margin-bottom: 4px; color: #f8fafc; }
.subtitle { color: #94a3b8; font-size: 13px; margin-bottom: 24px; }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(380px, 1fr)); gap: 20px; margin-bottom: 24px; }
.card { background: #1e293b; border-radius: 12px; padding: 20px; border: 1px solid #334155; }
.card h2 { font-size: 15px; font-weight: 600; color: #94a3b8; margin-bottom: 14px; text-transform: uppercase; letter-spacing: 0.05em; }
.card canvas { max-height: 220px; }
.stat-row { display: flex; gap: 16px; flex-wrap: wrap; }
.stat { flex: 1; min-width: 100px; background: #0f172a; border-radius: 8px; padding: 12px; text-align: center; border: 1px solid #334155; }
.stat .value { font-size: 28px; font-weight: 700; color: #38bdf8; }
.stat .label { font-size: 11px; color: #64748b; margin-top: 4px; text-transform: uppercase; }
.stat.warn .value { color: #f59e0b; }
.stat.danger .value { color: #ef4444; }
.retreat-badge { display: inline-block; padding: 2px 10px; border-radius: 999px; font-size: 12px; font-weight: 600; }
.retreat-badge.normal { background: #065f46; color: #6ee7b7; }
.retreat-badge.retreat { background: #78350f; color: #fbbf24; }
.retreat-badge.recover { background: #1e3a5f; color: #60a5fa; }
.config-panel { background: #1e293b; border-radius: 12px; padding: 20px; border: 1px solid #334155; }
.config-panel h2 { font-size: 15px; font-weight: 600; color: #94a3b8; margin-bottom: 14px; text-transform: uppercase; letter-spacing: 0.05em; }
.config-row { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; flex-wrap: wrap; }
.config-row label { min-width: 100px; font-size: 13px; color: #cbd5e1; }
.config-row input, .config-row select { background: #0f172a; border: 1px solid #334155; border-radius: 6px; color: #e2e8f0; padding: 6px 10px; font-size: 13px; }
.config-row input[type="range"] { width: 140px; }
.config-row button { background: #38bdf8; color: #0f172a; border: none; border-radius: 6px; padding: 6px 16px; font-size: 13px; font-weight: 600; cursor: pointer; }
.config-row button:hover { background: #7dd3fc; }
.config-row button:disabled { background: #475569; cursor: not-allowed; }
.toast { position: fixed; top: 16px; right: 16px; padding: 10px 20px; border-radius: 8px; font-size: 13px; z-index: 999; animation: fadeInOut 3s; }
.toast.success { background: #065f46; color: #6ee7b7; }
.toast.error { background: #7f1d1d; color: #fca5a5; }
@keyframes fadeInOut { 0% { opacity: 0; transform: translateY(-8px); } 10% { opacity: 1; transform: translateY(0); } 80% { opacity: 1; } 100% { opacity: 0; } }
.disconnected { background: #7f1d1d; color: #fca5a5; padding: 4px 10px; border-radius: 4px; font-size: 12px; display: inline-block; margin-left: 8px; }
.connected { background: #065f46; color: #6ee7b7; padding: 4px 10px; border-radius: 4px; font-size: 12px; display: inline-block; margin-left: 8px; }
</style>
</head>
<body>
<h1>🚀 NVIDIA Sidecar 实时仪表盘
<span id="conn-status" class="connected">已连接</span>
</h1>
<p class="subtitle">令牌桶限流 · 优先级队列 · 避退模式 · 实时监控</p>
<!-- 状态卡片 -->
<div class="stat-row" style="margin-bottom: 24px;">
<div class="stat"><div class="value" id="val-total">0</div><div class="label">总请求</div></div>
<div class="stat"><div class="value" id="val-nvidia">0</div><div class="label">NVIDIA 请求</div></div>
<div class="stat"><div class="value" id="val-rate">0</div><div class="label">当前 RPM</div></div>
<div class="stat"><div class="value" id="val-429">0%</div><div class="label">上游 429 率</div></div>
<div class="stat"><div class="value" id="val-retreat">正常</div><div class="label">避退状态</div></div>
<div class="stat"><div class="value" id="val-uptime">0s</div><div class="label">运行时间</div></div>
</div>
<!-- 图表 -->
<div class="grid">
<div class="card">
<h2>📊 令牌桶使用率</h2>
<canvas id="chart-tokens"></canvas>
</div>
<div class="card">
<h2>📈 队列深度</h2>
<canvas id="chart-queue"></canvas>
</div>
<div class="card">
<h2>📉 请求吞吐量 (最近 20 点)</h2>
<canvas id="chart-throughput"></canvas>
</div>
<div class="card">
<h2>⚙️ 速率历史</h2>
<canvas id="chart-rate"></canvas>
</div>
</div>
<!-- 配置面板 -->
<div class="config-panel">
<h2>🔧 实时配置</h2>
<div class="config-row">
<label>速率 (RPM)</label>
<input type="range" id="cfg-rate-rpm" min="1" max="100" value="40" oninput="document.getElementById('cfg-rate-val').textContent=this.value">
<span id="cfg-rate-val" style="min-width:30px;">40</span>
</div>
<div class="config-row">
<label>队列上限</label>
<input type="number" id="cfg-queue-max" value="500" min="1" max="2000" style="width:80px;">
</div>
<div class="config-row">
<button onclick="applyConfig()">应用配置</button>
</div>
</div>
<script>
// SSE 连接
let evtSource = null;
let dataHistory = { throughput: [], rates: [] };
const MAX_HISTORY = 20;
let latencyLog = [];
function connectSSE() {
if (evtSource) evtSource.close();
evtSource = new EventSource('/api/dashboard/stream');
evtSource.onmessage = (e) => {
try {
const snap = JSON.parse(e.data);
updateDashboard(snap);
updateLatencies(snap);
document.getElementById('conn-status').className = 'connected';
document.getElementById('conn-status').textContent = '已连接';
} catch (err) {
document.getElementById('conn-status').className = 'disconnected';
document.getElementById('conn-status').textContent = '解析错误';
}
};
evtSource.onerror = () => {
document.getElementById('conn-status').className = 'disconnected';
document.getElementById('conn-status').textContent = '断开 - 重连中';
};
}
// 初始化 Chart.js
const ctxTokens = document.getElementById('chart-tokens').getContext('2d');
const chartTokens = new Chart(ctxTokens, {
type: 'doughnut',
data: {
labels: ['已用令牌', '可用令牌'],
datasets: [{ data: [0, 40], backgroundColor: ['#ef4444', '#22c55e'], borderWidth: 0 }]
},
options: { responsive: true, maintainAspectRatio: true, cutout: '65%', plugins: { legend: { position: 'bottom', labels: { color: '#94a3b8' } } } }
});
const ctxQueue = document.getElementById('chart-queue').getContext('2d');
const chartQueue = new Chart(ctxQueue, {
type: 'bar',
data: {
labels: ['URGENT', 'HIGH', 'NORMAL', 'LOW'],
datasets: [{ label: '排队数', data: [0, 0, 0, 0], backgroundColor: ['#ef4444', '#f59e0b', '#38bdf8', '#a78bfa'] }]
},
options: { responsive: true, maintainAspectRatio: true, scales: { y: { beginAtZero: true, ticks: { color: '#94a3b8' } }, x: { ticks: { color: '#94a3b8' } } }, plugins: { legend: { display: false } } }
});
const ctxThroughput = document.getElementById('chart-throughput').getContext('2d');
const chartThroughput = new Chart(ctxThroughput, {
type: 'line',
data: { labels: [], datasets: [
{ label: '成功', data: [], borderColor: '#22c55e', backgroundColor: '#22c55e20', fill: false, tension: 0.3, pointRadius: 2 },
{ label: '429', data: [], borderColor: '#f59e0b', backgroundColor: '#f59e0b20', fill: false, tension: 0.3, pointRadius: 2 },
{ label: '直通', data: [], borderColor: '#a78bfa', backgroundColor: '#a78bfa20', fill: false, tension: 0.3, pointRadius: 2 },
]},
options: { responsive: true, maintainAspectRatio: true, scales: { y: { beginAtZero: true, ticks: { color: '#94a3b8' } }, x: { ticks: { color: '#94a3b8' } } }, plugins: { legend: { position: 'bottom', labels: { color: '#94a3b8' } } } }
});
const ctxRate = document.getElementById('chart-rate').getContext('2d');
const chartRate = new Chart(ctxRate, {
type: 'line',
data: { labels: [], datasets: [
{ label: '有效 RPM', data: [], borderColor: '#38bdf8', fill: false, tension: 0.3, pointRadius: 2 },
{ label: '基准 RPM', data: [], borderColor: '#64748b', fill: false, tension: 0.3, pointRadius: 2, borderDash: [4, 4] },
]},
options: { responsive: true, maintainAspectRatio: true, scales: { y: { beginAtZero: true, ticks: { color: '#94a3b8' } }, x: { ticks: { color: '#94a3b8' } } }, plugins: { legend: { position: 'bottom', labels: { color: '#94a3b8' } } } }
});
function updateDashboard(snap) {
const r = snap.requests || {};
const tb = snap.token_bucket || {};
const rt = snap.retreat || {};
document.getElementById('val-total').textContent = (r.total || 0).toLocaleString();
document.getElementById('val-nvidia').textContent = (r.nvidia || 0).toLocaleString();
document.getElementById('val-rate').textContent = Math.round(rt.effective_rpm || 40);
document.getElementById('val-429').textContent = ((rt.upstream_429_rate || 0) * 100).toFixed(1) + '%';
document.getElementById('val-uptime').textContent = fmtDuration(snap.uptime_seconds || 0);
const retreatEl = document.getElementById('val-retreat');
const state = rt.state || 'normal';
retreatEl.textContent = state === 'retreat' ? '⚠️ 避退' : state === 'recover' ? '↗ 恢复中' : '✅ 正常';
retreatEl.style.color = state === 'retreat' ? '#f59e0b' : state === 'recover' ? '#60a5fa' : '#22c55e';
chartTokens.data.datasets[0].data = [
Math.round((tb.capacity || 40) - (tb.tokens || 40)),
Math.round(tb.tokens || 0)
];
chartTokens.update();
const qs = snap.queue || {};
const perPriority = qs.per_priority || {};
chartQueue.data.datasets[0].data = [
perPriority.URGENT || 0,
perPriority.HIGH || 0,
perPriority.NORMAL || 0,
perPriority.LOW || 0
];
chartQueue.update();
const now = new Date().toLocaleTimeString();
const prev = dataHistory.throughput.length > 0 ? dataHistory.throughput[dataHistory.throughput.length - 1].nvidia : 0;
const throughput = Math.max(0, (r.nvidia || 0) - prev);
dataHistory.throughput.push({ time: now, nvidia: throughput, ratelimited: r.ratelimited || 0, passthrough: r.passthrough || 0 });
dataHistory.rates.push({ time: now, effective: rt.effective_rpm || 40, base: rt.base_rpm || 40 });
if (dataHistory.throughput.length > MAX_HISTORY) dataHistory.throughput.shift();
if (dataHistory.rates.length > MAX_HISTORY) dataHistory.rates.shift();
chartThroughput.data.labels = dataHistory.throughput.map(d => d.time);
chartThroughput.data.datasets[0].data = dataHistory.throughput.map(d => d.nvidia);
chartThroughput.data.datasets[1].data = dataHistory.throughput.map(d => d.ratelimited);
chartThroughput.data.datasets[2].data = dataHistory.throughput.map(d => d.passthrough);
chartThroughput.update();
chartRate.data.labels = dataHistory.rates.map(d => d.time);
chartRate.data.datasets[0].data = dataHistory.rates.map(d => d.effective);
chartRate.data.datasets[1].data = dataHistory.rates.map(d => d.base);
chartRate.update();
}
function updateLatencies(snap) {
const tb = snap.token_bucket || {};
}
function fmtDuration(s) {
if (s < 60) return s + 's';
if (s < 3600) return Math.floor(s/60) + 'm ' + (s%60) + 's';
return Math.floor(s/3600) + 'h ' + Math.floor((s%3600)/60) + 'm';
}
async function applyConfig() {
const btn = document.querySelector('.config-row button');
btn.disabled = true;
try {
const resp = await fetch('/api/admin/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
rate_rpm: parseInt(document.getElementById('cfg-rate-rpm').value),
queue_max_size: parseInt(document.getElementById('cfg-queue-max').value),
})
});
const result = await resp.json();
showToast(resp.ok ? 'success' : 'error', resp.ok ? '配置已更新' : (result.detail || '配置更新失败'));
} catch (err) {
showToast('error', '请求失败: ' + err.message);
}
btn.disabled = false;
}
function showToast(type, msg) {
const t = document.createElement('div');
t.className = 'toast ' + type;
t.textContent = msg;
document.body.appendChild(t);
setTimeout(() => t.remove(), 3000);
}
connectSSE();
</script>
</body>
</html>
+293
View File
@@ -0,0 +1,293 @@
"""
NVIDIA Sidecar — WebUI 后端 API
提供仪表盘 SSE 实时推送 + 配置热重载 API。
"""
from __future__ import annotations
import asyncio
import json
import os
import time
from pathlib import Path
from typing import Any, AsyncGenerator
import structlog
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from pydantic import BaseModel
webui_router: APIRouter = APIRouter(prefix="/api", tags=["webui"])
logger: structlog.stdlib.BoundLogger = structlog.get_logger("nvidia_sidecar.webui")
STATIC_DIR: Path = Path(__file__).parent / "static"
# dashboard.html 缓存(严维序评审 #6 / 梁思筑评审 #8:避免每次请求读磁盘)
_dashboard_html_cache: tuple[str, float] | None = None
_DASHBOARD_CACHE_TTL: float = 300.0 # 5 分钟
# Admin API 认证(严维序评审 #1)
_ADMIN_TOKEN: str | None = os.environ.get("SIDECAR_ADMIN_TOKEN")
_admin_auth_scheme: HTTPBearer = HTTPBearer(auto_error=False)
# ---------------------------------------------------------------------------
# 配置热重载模型
# ---------------------------------------------------------------------------
class ConfigPatch(BaseModel):
"""可在线修改的配置字段。"""
rate_rpm: int | None = None
queue_max_size: int | None = None
fallback_enabled_passthrough: bool | None = None
# ---------------------------------------------------------------------------
# 仪表盘 SSE 推送
# ---------------------------------------------------------------------------
async def _dashboard_stream(request: Request) -> StreamingResponse:
"""SSE 实时推送 Sidecar 完整状态快照(每秒一次)。
供 dashboard.html 的 EventSource 消费。
"""
async def event_generator() -> AsyncGenerator[str, None]:
# 首帧发送 retry 字段(严维序评审 minor):指示客户端断连后等待 3s 重试
first_frame = True
while True:
if await request.is_disconnected():
break
try:
snapshot: dict[str, Any] = await _build_snapshot()
payload_sse = f"data: {json.dumps(snapshot, ensure_ascii=False)}\n\n"
if first_frame:
payload_sse = f"retry: 3000\n{payload_sse}"
first_frame = False
yield payload_sse
except Exception:
logger.exception("dashboard_sse_error")
yield f"data: {json.dumps({'error': 'internal'})}\n\n"
await asyncio.sleep(1.0)
return StreamingResponse(
event_generator(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"X-Accel-Buffering": "no",
},
)
# SSE 首帧写入 retry 字段(严维序评审 minor),在 event_generator 首次 yield 前注入
# 通过在 StreamingResponse 返回前手动发送 retry header 实现
# (SSE 协议支持 retry 字段作为重建连接间隔)
# 注:在 event_generator 的首个 yield 中加入 retry 声明
async def _build_snapshot() -> dict[str, Any]:
"""构建当前状态快照(从全局状态读取,含队列深度)。"""
# 延迟导入避免循环依赖
from nvidia_sidecar import server
try:
_stats = server._stats
_token_bucket = server._token_bucket
bucket_status = _token_bucket.get_status()
now = time.time()
uptime = int(now - _stats["start_time"]) if _stats.get("start_time") else 0
# 获取队列统计数据(含 per-priority depth
queue_data: dict[str, Any] = {"current_size": 0, "per_priority": {}}
try:
queue_stats = await server._priority_queue.get_stats()
queue_data = {
"max_size": queue_stats.get("max_size", 0),
"current_size": queue_stats.get("current_size", 0),
"per_priority": queue_stats.get("depth_by_priority", {}),
"total_enqueued": queue_stats.get("total_enqueued", 0),
"total_dequeued": queue_stats.get("total_dequeued", 0),
"total_dropped": queue_stats.get("total_dropped", 0),
}
except Exception:
logger.warning("queue_stats_unavailable", message="队列统计获取失败,仪表盘队列深度可能不准确")
return {
"timestamp": now,
"uptime_seconds": uptime,
"token_bucket": bucket_status,
"queue": queue_data,
"retreat": {
"state": getattr(_token_bucket, "_retreat_state", "normal"),
"effective_rpm": round(getattr(_token_bucket, "get_effective_rate_rpm", lambda: 40.0)(), 1),
"base_rpm": round(getattr(_token_bucket, "get_base_rate_rpm", lambda: 40.0)(), 1),
"upstream_429_rate": round(getattr(_token_bucket, "get_429_rate", lambda: 0.0)(), 4),
},
"requests": {
"total": _stats.get("total_requests", 0),
"nvidia": _stats.get("nvidia_requests", 0),
"passthrough": _stats.get("passthrough_requests", 0),
"ratelimited": _stats.get("ratelimited_requests", 0),
},
"errors": {
"queue_full_rejects": _stats.get("queue_full_rejects", 0),
"upstream_errors": _stats.get("upstream_errors", 0),
},
}
except Exception:
logger.exception("snapshot_build_error")
return {"error": "snapshot_unavailable", "timestamp": time.time()}
# ---------------------------------------------------------------------------
# 配置热重载
# ---------------------------------------------------------------------------
async def get_config() -> dict[str, Any]:
"""获取当前完整配置。"""
from nvidia_sidecar import server
cfg = server._config
return {
"listen_host": cfg.listen_host,
"listen_port": cfg.listen_port,
"metrics_port": cfg.metrics_port,
"upstream_url": cfg.upstream_url,
"upstream_api_key": _mask_api_key(cfg.upstream_api_key),
"rate_rpm": _get_current_rate(server),
"bucket_capacity": cfg.bucket_capacity,
"request_timeout": cfg.request_timeout,
"queue_max_size": cfg.queue_max_size,
"low_priority_timeout": cfg.low_priority_timeout,
"fallback_enabled_passthrough": cfg.fallback_enabled_passthrough,
"log_level": cfg.log_level,
}
async def update_config(body: ConfigPatch) -> JSONResponse:
"""在线修改配置项并即时生效。"""
from nvidia_sidecar import server
cfg = server._config
changed: list[str] = []
if body.rate_rpm is not None:
if body.rate_rpm <= 0:
raise HTTPException(status_code=400, detail="rate_rpm must be > 0")
cfg.rate_rpm = body.rate_rpm
server._token_bucket.set_rate(body.rate_rpm / 60.0)
changed.append("rate_rpm")
if body.queue_max_size is not None:
if body.queue_max_size <= 0:
raise HTTPException(status_code=400, detail="queue_max_size must be > 0")
ok, msg = server._priority_queue.set_max_size(body.queue_max_size)
if not ok:
raise HTTPException(status_code=400, detail=msg)
cfg.queue_max_size = body.queue_max_size
changed.append("queue_max_size")
logger.info("queue_max_size_updated", detail=msg)
if body.fallback_enabled_passthrough is not None:
cfg.fallback_enabled_passthrough = body.fallback_enabled_passthrough
changed.append("fallback_enabled_passthrough")
logger.info("config_updated", changed=changed)
return JSONResponse(
content={"status": "ok", "changed": changed},
)
def _mask_api_key(key: str) -> str:
"""对 API Key 进行脱敏处理,仅保留前 4 位以供识别。
严维序评审 #2 / 沈路明评审 #3:防止 API Key 明文泄露。
"""
if not key:
return ""
if len(key) <= 4:
return key[:2] + "****"
return key[:4] + "****"
def _get_current_rate(server_module: Any) -> float:
"""获取当前实际速率(避退调整后),兼容 AdaptiveTokenBucket。"""
tb = server_module._token_bucket
if hasattr(tb, "get_effective_rate_rpm"):
return float(round(tb.get_effective_rate_rpm(), 1))
return float(tb.rate * 60.0)
# ---------------------------------------------------------------------------
# 路由注册
# ---------------------------------------------------------------------------
@webui_router.get("/dashboard/stream")
async def dashboard_stream(request: Request) -> StreamingResponse:
"""SSE 仪表盘实时推送端点。"""
return await _dashboard_stream(request)
async def _verify_admin_auth(
credentials: HTTPAuthorizationCredentials | None = Depends(_admin_auth_scheme),
) -> None:
"""Admin API Bearer Token 认证(严维序评审 #1)。
若设置了 SIDECAR_ADMIN_TOKEN 环境变量,则要求请求携带匹配的 Bearer Token。
未设置时跳过认证(开发/测试环境)。
"""
if _ADMIN_TOKEN is None:
return # 未配置认证 token,允许无认证访问
if credentials is None:
raise HTTPException(status_code=401, detail="需要 Bearer Token 认证(Admin API")
if credentials.credentials != _ADMIN_TOKEN:
raise HTTPException(status_code=403, detail="Admin Token 无效")
@webui_router.get("/admin/config")
async def admin_get_config(
_auth: None = Depends(_verify_admin_auth),
) -> JSONResponse:
"""获取当前配置(需要 Admin 认证)。"""
return JSONResponse(content=await get_config())
@webui_router.post("/admin/config")
async def admin_update_config(
body: ConfigPatch,
_auth: None = Depends(_verify_admin_auth),
) -> JSONResponse:
"""在线修改配置(热重载,需要 Admin 认证)。"""
return await update_config(body)
# ---------------------------------------------------------------------------
# 仪表盘静态页面
# ---------------------------------------------------------------------------
def _get_dashboard_html() -> str:
"""获取仪表盘 HTML(带缓存,严维序评审 #6 / 梁思筑评审 #8)。
首次加载后缓存 5 分钟,避免每次请求读磁盘。
"""
global _dashboard_html_cache
now = time.monotonic()
if _dashboard_html_cache is not None:
cached_content, cached_at = _dashboard_html_cache
if now - cached_at < _DASHBOARD_CACHE_TTL:
return cached_content
dashboard_path = STATIC_DIR / "dashboard.html"
if dashboard_path.is_file():
content = dashboard_path.read_text(encoding="utf-8")
_dashboard_html_cache = (content, now)
return content
return "<h1>dashboard.html not found</h1>"
@webui_router.get("/dashboard", include_in_schema=False)
async def dashboard_page() -> HTMLResponse:
"""仪表盘 HTML 页面(含缓存策略)。"""
return HTMLResponse(content=_get_dashboard_html())
@@ -1,214 +0,0 @@
# 开发文档:双色球 Web UI 系统
**版本**: v1.0
**开发人员**: 徐聪(costcodev
**日期**: 2026-07-03
**Issue**: BIZ-75
---
## 1. 项目概述
双色球自动化系统 Web UI,提供号码生成、历史数据查看、生成记录管理和统计分析功能。支持 PC 端和移动端响应式访问,监听 0.0.0.0:8085,局域网可访问。
## 2. 技术栈
| 层级 | 技术 | 说明 |
|------|------|------|
| 后端 | Python 3 + Flask | REST API 服务 |
| 前端 | 原生 HTML/CSS/JS | 单文件,响应式布局 |
| 数据分析 | Pandas + NumPy | 号码统计分析 |
| 数据存储 | Excel + JSON | 历史数据 + 生成记录 |
| 部署 | systemd / nohup | Linux 服务部署 |
## 3. 目录结构
```
lottoData/
├── app.py # Flask 主服务(统一入口)
├── index.html # 前端 UI(响应式,4 Tab 页面)
├── lottery.py # 号码生成核心逻辑
├── fetch_data.py # 历史数据抓取脚本
├── web_console.html # 数据抓取控制台前端
├── requirements.txt # Python 依赖
├── 双色球历史数据.xlsx # 历史数据文件
├── lottery/ # 号码生成结果输出目录
├── .generation_records.json # 生成记录索引(JSON
├── .fetch_status.json # 抓取状态文件
├── deploy/ # 部署相关文件
│ ├── DEPLOY.md # 部署说明
│ ├── lotto-app.service # systemd 服务文件
│ ├── fetch_daily.sh # 定时抓取脚本
│ └── backup.sh # 备份脚本
└── docs/ # 文档目录
├── PRD-双色球 WebUI-v1.0.md
└── 开发文档-双色球WebUI-v1.0.md ← 本文件
```
## 4. API 接口
### 4.1 接口清单
| 接口 | 方法 | 描述 | 认证 |
|------|------|------|------|
| `/api/generate` | POST | 生成号码 | 可选 |
| `/api/history` | GET | 获取历史开奖数据(分页+搜索) | 可选 |
| `/api/records` | GET | 获取生成记录列表(分页) | 可选 |
| `/api/records/:id` | DELETE | 删除生成记录 | 可选 |
| `/api/statistics` | GET | 获取统计分析数据 | 可选 |
| `/api/download/:filepath` | GET | 下载文件 | 可选 |
| `/api/status` | GET | 系统状态 | 无 |
| `/api/config` | GET | 前端配置 | 无 |
| `/api/fetch/status` | GET | 抓取执行状态 | 无 |
| `/api/fetch/execute` | POST | 触发数据抓取 | 无 |
### 4.2 关键接口参数
#### POST /api/generate
```json
// 请求
{
"num_tickets": 10,
"strategy": "advanced" // 或 "basic"
}
// 响应
{
"success": true,
"data": {
"tickets": [...],
"total": 10,
"filename": "lottery/xxx.xlsx",
"download_url": "/api/download/lottery/xxx.xlsx",
"record": {...},
"statistics": {...}
}
}
```
#### GET /api/history
参数: `page` (页码), `page_size` (每页条数), `search` (搜索关键词)
#### GET /api/records
参数: `page` (页码), `page_size` (每页条数)
## 5. 前端页面
### 5.1 页面结构
- **Header**: 标题 + 副标题
- **导航 Tab**: 号码生成 | 历史数据 | 生成记录 | 统计分析
- **移动端**: 底部固定导航栏
### 5.2 功能页面
#### 号码生成页(首页)
- 统计概览(历史期数、常见奇偶比、和值范围等)
- 策略选择(高级策略/基础策略)
- 注数输入(1-1000
- 生成结果展示(红球+蓝球+统计指标)
- Excel 下载按钮
#### 历史数据页
- 搜索框(500ms 防抖)
- 数据表格(期号、日期、红球、蓝球、统计字段)
- 分页控件
#### 生成记录页
- 记录列表(策略、注数、时间、文件大小)
- 下载/删除操作
- 分页控件
#### 统计分析页
- 历史开奖期数
- 红球热号 TOP15 / 冷号 TOP15
- 蓝球热号 TOP8
- 奇偶比/大小比/和值/跨度统计
## 6. 关键修复说明
### 6.1 数据格式兼容修复(核心 Bug 修复)
**问题**: `lottery.py` 期望 Excel 含"号码"列(拼接格式如 `08121821243001`),但 `fetch_data.py` 抓取的 Excel 使用分列格式("红球 1"~"红球 6"+"蓝球"),导致号码生成器无法加载历史数据。
**根因**: Excel 文件包含两行 header
- Row 0: 新格式列名(期号、开奖日期、红球 1~6、蓝球、特别号)
- Row 1: 旧格式列名(开奖时间、期数、号码、开机号、...)
- Row 2+: 实际数据
**修复方案**:
1. `lottery.py``load_history_data()`: 添加多格式检测逻辑,识别格式A(双行 header)并自动跳过,使用旧列名作为标准列名
2. `lottery.py``parse_numbers()`: 新增对拼接字符串格式(14位无分隔符)的直接解析,避免 `re.findall` 将整个字符串视为一个数字
3. `app.py``load_history_dataframe()`: 同步修复多格式兼容逻辑
### 6.2 线程安全
- 生成记录的读-改-写操作使用 `threading.Lock` 保护
- 文件写入使用临时文件+原子替换(`os.replace`),防止崩溃导致数据损坏
## 7. 部署方式
### 7.1 直接运行
```bash
cd /home/vincent/Studio/lottoData
source .venv/bin/activate
python3 app.py
# 访问 http://localhost:8085
```
### 7.2 systemd 服务
```bash
# 服务文件: deploy/lotto-app.service
sudo cp deploy/lotto-app.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable lotto-app
sudo systemctl start lotto-app
```
### 7.3 定时数据抓取
```bash
# 添加 cron 任务
crontab -e
# 每天 02:30 自动抓取最新数据
30 2 * * * /home/vincent/Studio/lottoData/deploy/fetch_daily.sh >> /home/vincent/Studio/lottoData/deploy/cron.log 2>&1
```
## 8. 测试验证
### 8.1 API 测试结果
| 接口 | 状态 | 说明 |
|------|------|------|
| GET /api/status | ✅ 通过 | 返回服务状态 |
| GET /api/statistics | ✅ 通过 | 120条历史数据统计正确 |
| GET /api/history | ✅ 通过 | 分页+红蓝球解析正确 |
| POST /api/generate | ✅ 通过 | 5注号码生成成功,含统计 |
| GET /api/records | ✅ 通过 | 生成记录列表正确 |
| GET / (前端页面) | ✅ 通过 | HTML 页面正常加载 |
### 8.2 数据格式验证
- 历史数据: 120 条记录全部成功解析 ✅
- 红球解析: 6个红球正确提取 ✅
- 蓝球解析: 1个蓝球正确提取 ✅
- 号码范围校验: 1-33(红) + 1-16(蓝) ✅
## 9. 已知限制
- 前端为单 HTML 文件,未使用构建工具
- 无用户登录系统(Token 认证为可选项,默认关闭)
- 历史数据来源为 55128.cn,如网站改版需更新 `fetch_data.py`
- 不支持 HTTPS(内网环境)
## 10. 后续优化建议
| 功能 | 优先级 | 说明 |
|------|--------|------|
| 数据可视化图表 | P2 | 走势图、分布图 |
| 用户登录系统 | P2 | 多用户权限管理 |
| 定时自动生成 | P2 | 定时生成+推送 |
| 微信推送 | P3 | 生成结果推送至微信 |
| 多彩种支持 | P3 | 大乐透、福彩 3D 等 |
---
**开发完成日期**: 2026-07-03
**代码仓库**: http://192.168.1.99:12299/vincent/Lottery.git
**开发人员**: 徐聪(costcodev