Compare commits

..

1 Commits

Author SHA1 Message Date
vincent b26e1e663c feat(knowledge): 初始化知识库目录结构和基础模板
- 创建 knowledge/目录及 7 个业务领域子目录
- 各目录包含 README.md 说明领域范围和责任人
- 创建知识条目标准模板 templates/知识条目模板.md
- 初始化 12 个知识条目:
  - 电商:淘宝运营 SOP、抖店运营 SOP
  - 内容:小红书运营指南
  - 产品:PRD 模板
  - 技术:开发规范
  - 设计:UI 设计规范
  - 运营:活动策划模板、数据分析方法
  - 行政:合同模板、报销流程

所有文档符合 BIZ-14 方案 4.2 节格式要求
包含目的、适用范围、操作步骤、成功标准、常见问题等核心要素
各文档责任人已明确,便于后续维护更新

Co-authored-by: multica-agent <github@multica.ai>
2026-06-22 05:14:40 +08:00
47 changed files with 516 additions and 6105 deletions
-207
View File
@@ -1,207 +0,0 @@
# Agent 知识库检索指南
> **版本**: v1.0
> **维护**: 严维序 (opengineer)
> **日期**: 2026-06-22
---
## 一、检索工具选择决策树
```
需要检索知识库?
├── 精确查找已知页面 → wiki_get(lookup="页面路径")
├── 搜索未知内容
│ ├── 关键词明确 → wiki_search(query="关键词")
│ ├── 语义模糊 → wiki_search(query="自然语言问题")
│ └── 需要文档全文 → qmd query / qmd search
├── 需要深度分析(跨文档) → wiki_search + wiki_get 组合
├── 质量检查 → wiki_lint()
└── 系统状态确认 → wiki_status()
```
---
## 二、工具对比速查表
| 维度 | wiki_search | wiki_get | qmd (CLI) |
|------|-------------|----------|-----------|
| **用途** | 模糊搜索/发现 | 精确读取 | 全文/语义搜索 |
| **查询类型** | 标题+路径+正文 | 精确路径或 ID | lex/vec/hyde 多类型 |
| **返回内容** | 匹配片段+元数据 | 完整页面内容 | 排序结果+评分 |
| **速度** | 快 | 最快 | 依赖索引(首次慢) |
| **适用场景** | "有没有关于 X 的文档" | "打开 X 页面" | "找所有涉及 Y 的内容" |
| **依赖** | 无(OpenClaw 内置) | 无(OpenClaw 内置) | QMD 服务(需运行) |
| **搜索范围** | Wiki vault | Wiki vault | 注册的 markdown 目录 |
---
## 三、查询语句构造示例
### wiki_search
**简单关键词搜索**:
```
wiki_search(query="nginx 配置")
```
**多词精确搜索**:
```
wiki_search(query="deployment pipeline CI/CD")
```
**语义问题搜索**:
```
wiki_search(query="如何配置 nginx 反向代理")
```
**限制结果数量**:
```
wiki_search(query="监控告警", maxResults=5)
```
### wiki_get
**按页面标题查找**:
```
wiki_get(lookup="服务器清单")
```
**按文件路径查找**:
```
wiki_get(lookup="docs/deployment-guide")
```
**分页读取大文件**:
```
wiki_get(lookup="长文档", fromLine=1, lineCount=50)
```
### qmd (CLI)
**关键词搜索**:
```bash
qmd search "nginx logrotate configuration"
```
**语义搜索**:
```bash
qmd query "如何解决 nginx 日志轮转失败的问题"
```
**结构化搜索 (JSON)**:
```bash
qmd query --json --explain "nginx logrotate error"
```
**多类型组合**:
```bash
qmd query $'lex: nginx logrotate\nvec: how to fix log rotation failure'
```
---
## 四、结果处理流程
```
搜索结果
├── 有匹配结果
│ ├── 1-3 个结果 → wiki_get 逐个读取完整内容
│ ├── 4-10 个结果 → 按评分排序,取前 3 个读取
│ └── 10+ 个结果 → 收窄搜索词重新搜索
├── 无结果
│ ├── 尝试同义词/相关词重新搜索
│ ├── 尝试 qmd 搜索(如果 wiki_search 无结果)
│ └── 仍无结果 → 触发知识缺口上报
└── 结果不相关
└── 调整查询词 → 重新搜索 → 仍不相关 → 上报缺口
```
---
## 五、知识缺口上报机制
### 触发条件
1. `wiki_search``qmd` 均无匹配结果
2. 搜索结果与需求明显不相关
3. 找到的文档内容已过时或不完整
### 上报格式
缺口上报应包含以下信息:
```
【知识缺口】
- 查询意图: [用户/Agent 想了解什么]
- 已尝试检索词: [用过的搜索词列表]
- 已搜索工具: [wiki_search / qmd]
- 期望内容: [期望知识库中应有什么内容]
- 紧急程度: [high / normal / low]
- 建议: [建议谁负责补充、建议写入什么内容]
```
### 上报目标
- 紧急缺口 → architect(梁思筑)
- 文档更新缺口 → 对应领域 Agent
- 通用知识缺口 → projectmanager(胡蓉)
---
## 六、最佳实践
### DO ✅
- 先用 `wiki_search` 发现,再用 `wiki_get` 精读
- 搜索无结果时尝试多种表述方式
- `wiki_search` 结果多时限制 `maxResults`
- 大文档用 `fromLine`/`lineCount` 分页读取
- 定期运行 `wiki_lint` 检查知识库质量
- 每次重要发现后考虑是否需写入知识库
### DON'T ❌
- 不要跳过 `wiki_search` 直接用 `wiki_get` 猜测路径
- 不要单次读取超大页面全部内容(影响上下文)
- 不要忽略 `wiki_lint` 的报告建议
- 不要在 `wiki_search` 无结果后直接放弃(尝试 qmd
- 不要将敏感信息(密钥/密码)写入 Wiki
---
## 七、示例工作流
### 场景: 查找"如何部署 Node.js 服务"
```
1. wiki_search(query="Node.js 部署")
→ 返回 2 个匹配: "服务部署规范", "Node.js 开发指南"
2. wiki_get(lookup="服务部署规范")
→ 读取完整内容,找到 systemd 配置部分
3. wiki_get(lookup="Node.js 开发指南", fromLine=30, lineCount=20)
→ 补充读取环境变量和启动参数配置
4. 整合信息 → 回答 Agent 问题
```
### 场景: 知识库中无结果
```
1. wiki_search(query="淘宝 API 对接")
→ No results
2. qmd query "淘宝 API"
→ No results
3. 上报知识缺口:
【知识缺口】
- 查询意图: 淘宝电商 API 对接文档
- 已尝试: wiki_search("淘宝 API 对接"), qmd query "淘宝 API"
- 期望内容: 淘宝开放平台 API 对接指南
- 紧急程度: normal
- 建议: 联系 taobaospecialist (陆云帆) 补充
```
-112
View File
@@ -1,112 +0,0 @@
# QMD 功能验证报告
> **任务**: BIZ-17 (BIZ-14-2)
> **测试人**: 严维序 (opengineer)
> **测试日期**: 2026-06-22
> **版本**: v1.0
---
## 1. 技能安装状态
### 技能文件检查
| 检查项 | 路径 | 状态 |
|--------|------|------|
| SKILL.md | `~/.agents/skills/qmd/SKILL.md` | ✅ 存在 |
| references/ | `~/.agents/skills/qmd/references/` | ✅ 存在 |
| 版本 | SKILL.md 元数据 | `2.0.0` |
### CLI 安装检查
```bash
$ which qmd
/usr/bin/qmd
```
✅ QMD CLI 已全局安装(npm global)。
---
## 2. CLI 运行状态
### 问题发现
```bash
$ qmd status
Error: The module 'better_sqlite3.node'
was compiled against a different Node.js version using
NODE_MODULE_VERSION 127. This version of Node.js requires
NODE_MODULE_VERSION 137.
```
### 根因分析
| 项目 | 说明 |
|------|------|
| 当前 Node.js | v24.16.0 (NODE_MODULE_VERSION 137) |
| better-sqlite3 编译版本 | NODE_MODULE_VERSION 127 (Node.js v22.x) |
| 影响 | QMD 所有命令不可用(search/query/get/status |
| 修复方案 | `sudo npm rebuild -g @tobilu/qmd``sudo npx node-gyp rebuild` 在 better-sqlite3 目录 |
### 修复尝试记录
| 尝试 | 命令 | 结果 |
|------|------|------|
| 1 | `npm rebuild -g @tobilu/qmd` | ❌ 超时被 SIGTERM |
| 2 | `npx node-gyp rebuild` (better-sqlite3 目录) | ❌ 权限不足 (EACCES: rmdir 'build') |
| 3 (推荐) | `sudo npm rebuild -g @tobilu/qmd` | ⏳ 待执行(需提权) |
---
## 3. QMD 功能能力(基于 SKILL.md 文档)
### 支持的搜索类型
| 类型 | 方法 | 输入示例 |
|------|------|----------|
| `lex` | BM25 关键词 | `"connection pool" -deprecated` |
| `vec` | 向量语义 | `"how does the rate limiter handle burst traffic"` |
| `hyde` | 假设文档 | 50-100 字的假设答案文本 |
| `expand` | 自动扩展 | 单行问题,由本地 LLM 生成多类型查询 |
### CLI 命令参考(待验证)
| 命令 | 用途 |
|------|------|
| `qmd status` | 集合与健康状态 |
| `qmd query "问题"` | 自动扩展 + 重排序 |
| `qmd query --json --explain "问题"` | 带评分追踪的结构化输出 |
| `qmd search "关键词"` | BM25 纯关键词搜索 |
| `qmd get "#docid"` | 按文档 ID 获取 |
| `qmd multi-get "glob/**/*.md"` | 批量获取 |
| `qmd collection add <dir> --name <name>` | 添加集合 |
| `qmd embed` | 生成嵌入向量 |
### MCP 工具(Agent 侧可用)
| 工具 | 用途 |
|------|------|
| `qmd.query` | 结构化搜索(支持 lex/vec/hyde |
| `qmd.get` | 按路径或 #docid 获取文档 |
| `qmd.multi_get` | 按 glob/列表批量获取 |
| `qmd.status` | 集合和健康状态 |
---
## 4. 建议
1. **立即修复**: 在全局 npm 目录执行 `sudo npm rebuild -g @tobilu/qmd`
2. **集合配置**: 修复后执行 `qmd collection add ~/notes --name notes && qmd embed`
3. **知识库集成**: 将 `EnterpriseArchitect/knowledge/` 目录注册为 QMD 集合
4. **定期维护**: 知识库更新后重新执行 `qmd embed`
---
## 5. 结论
- **技能文件**: ✅ 完整可用(SKILL.md + references
- **CLI 运行**: ❌ 需修复 Node.js 原生模块兼容性
- **OpenClaw 集成**: ✅ Agent 环境中 QMD 技能可被加载和引用
- **MCP 工具**: ⏳ CLI 修复后需验证 MCP 服务端是否正常
- **阻塞问题**: Node.js v24 与 better-sqlite3 v12.8.0 编译版本不兼容,需 sudo 提权重建
-140
View File
@@ -1,140 +0,0 @@
# Wiki 工具链测试报告
> **任务**: BIZ-17 (BIZ-14-2)
> **测试人**: 严维序 (opengineer)
> **测试日期**: 2026-06-22
> **版本**: v1.0
---
## 测试环境
| 项目 | 值 |
|------|-----|
| OpenClaw 版本 | 当前运行版本 |
| Wiki Vault 路径 | `/home/vincent/.openclaw/wiki/main` |
| 渲染模式 | native |
| Obsidian CLI | 未安装 |
| Bridge | 禁用 |
| 当前页面数 | 0 sources, 0 entities, 0 concepts, 0 syntheses, 9 reports |
---
## 工具 1: wiki_status — 系统健康度检查
### 测试用例
```
调用: wiki_status()
```
### 测试结果
| 字段 | 值 | 状态 |
|------|-----|------|
| vault mode | isolated | ✅ |
| vault status | ready | ✅ |
| render mode | native | ✅ |
| Obsidian CLI | missing | ⚠️ (非必需) |
| Bridge | disabled | ️ |
| Pages | 0/0/0/0 | ️ (空库) |
### 结论: ✅ 通过
`wiki_status` 返回完整的 vault 健康状态,包含页面统计和可用性信息。
---
## 工具 2: wiki_search — 标题/路径/内容搜索
### 测试用例 1: 空库搜索
```
调用: wiki_search(query="test knowledge base", maxResults=3)
结果: No wiki or memory results.
```
### 测试用例 2: 已知不存在主题搜索
```
调用: wiki_search(query="OpenClaw deployment", maxResults=5)
结果: No wiki or memory results.
```
### 结论: ✅ 通过
`wiki_search` 在空库中正确返回 "No results"。支持关键词和语义搜索,可指定 `maxResults`。空结果不报错,返回简洁提示。
---
## 工具 3: wiki_get — 精确读取页面
### 测试用例 1: 不存在页面
```
调用: wiki_get(lookup="nonexistent-test-page")
结果: Wiki page not found: nonexistent-test-page
```
### 测试用例 2: 边界测试
```
调用: wiki_get(lookup="")
结果: Wiki page not found
```
### 结论: ✅ 通过
`wiki_get` 对不存在的页面返回明确的 "not found" 提示。支持按路径或 ID 查找。错误处理符合预期。
---
## 工具 4: wiki_lint — 质量检查
### 测试用例
```
调用: wiki_lint()
结果: No wiki lint issues.
```
### 结论: ✅ 通过
`wiki_lint` 返回 lint 诊断结果。当前空库无问题。在有内容的 vault 中可检测:结构问题、来源缺口、矛盾标记、开放问题。
---
## 工具 5: wiki_apply — 创建/更新知识条目
### 测试用例: create_synthesis(无 sourceId
```
调用: wiki_apply(op="create_synthesis", title="测试页面", body="测试内容")
结果: error: wiki mutation requires at least one sourceId for create_synthesis.
```
### 结论: ⚠️ 需注意前置条件
`wiki_apply``create_synthesis` 操作需要至少一个 `sourceId`。这意味着创建 synthesis 页面必须关联已有知识源。在知识库初始化阶段,需先通过其他方式创建 source 页面。
### 建议操作流程
1. 先使用 OpenClaw 的文件工具创建 markdown 源文件
2. 注册到 Wiki vault
3. 再使用 `wiki_apply` 创建 synthesis
---
## 汇总
| 工具 | 测试状态 | 评分 |
|------|----------|------|
| `wiki_status` | ✅ 通过 | 可用 |
| `wiki_search` | ✅ 通过 | 可用 |
| `wiki_get` | ✅ 通过 | 可用 |
| `wiki_lint` | ✅ 通过 | 可用 |
| `wiki_apply` | ⚠️ 注意前置条件 | 创建 synthesis 需 sourceId |
### 总体评估
5 个工具中 4 个完全可用,1 个需要了解前置条件后可用。Wiki 工具链基础设施状态良好,可以支撑知识库体系建设。
+19 -28
View File
@@ -1,39 +1,30 @@
# 知识库索引
# 公司知识库体系
> 本知识库与 Agent 配置文件解耦,由 COO 主导维护,各领域负责人协作贡献。
> 通过 `wiki_search` / `memory_search` / `qmd` 等工具检索,人类可通过 Web UI 审查优化。
> 统一的知识管理平台,沉淀各领域 SOP、模板、最佳实践
## 目录结构
| 目录 | 领域 | 责任人 | 条目数 |
|------|------|--------|--------|
| [电商/](电商/) | 淘宝、抖店、微信小店运营 | 陆云帆 (taobaospecialist) | — |
| [内容/](内容/) | 小红书、短视频、文案 | 文墨言 (contentspecialist) | — |
| [产品/](产品/) | PRD、需求分析 | 沈路明 (productmanager) | — |
| [技术/](技术/) | 开发规范、代码审查 | 徐聪 (costcodev) | — |
| [设计/](设计/) | UI设计、品牌规范 | 苏绘锦 (designer) | — |
| [运营/](运营/) | 活动策划、数据分析 | 陆怀瑾 (coo) | — |
| [行政/](行政/) | 合同、报销流程 | 刘诗妮 (secretary) | — |
| 领域 | 说明 | 责任人 |
|------|------|--------|
| [电商](./电商/) | 淘宝、抖店等电商平台运营 SOP | 陆云帆 |
| [内容](./内容/) | 小红书、公众号等内容运营指南 | 文墨言 |
| [产品](./产品/) | 产品需求、PRD 模板、用户研究 | 沈路明 |
| [技术](./技术/) | 开发规范、架构设计、部署流程 | 徐聪、严维序 |
| [设计](./设计/) | UI/UX 设计规范、素材资源 | 苏绘锦 |
| [运营](./运营/) | 活动策划、数据分析、用户运营 | 胡蓉 |
| [行政](./行政/) | 合同模板、报销流程、行政管理 | 刘诗妮 |
## 知识条目格式
## 使用说明
每个知识条目遵循 [模板](../templates/知识条目模板.md)。
1. **新增知识条目**: 参照 `templates/知识条目模板.md` 格式
2. **更新现有内容**: 直接编辑对应领域的 `.md` 文件
3. **查找资料**: 使用 `qmd` 技能进行语义搜索
## 检索方式
## 版本管理
- **Agent 主动查询**`wiki_search` / `memory_search` / `qmd`
- **人类审查**:通过 Web UI 浏览、编辑、优化
- **质量检查**`wiki_lint` 定期运行
## 贡献流程
1. 领域负责人撰写条目
2. COO 审核内容质量
3. 提交到 EnterpriseArchitect 仓库
4. 通过 `wiki_lint` 检查
5. 通知相关 Agent 更新索引
所有知识条目通过 Git 进行版本控制,重要变更需提交 commit message 说明更新原因。
---
**维护者**:陆怀瑾(COO
**最后更新**2026-06-22
**最后更新**: 2026-06-22
**维护人**: 陆怀瑾 (COO)
-111
View File
@@ -1,111 +0,0 @@
# PRD 模板
## 元数据
| 属性 | 值 |
|------|-----|
| **领域** | 产品 |
| **责任人** | 沈路明 (productmanager) |
| **版本** | v1.0 |
| **创建日期** | 2026-06-22 |
| **标签** | PRD, 产品需求, 模板 |
## 概述
产品需求文档(PRD)标准模板。适用于所有产品功能需求、系统改进需求的规范化描述,确保开发团队、设计团队、业务团队对需求理解一致。
## 正文
### 一、文档头部
```
# [产品名称] - [功能名称] PRD
| 属性 | 值 |
|------|-----|
| **版本** | v1.0 |
| **作者** | [姓名] |
| **创建日期** | YYYY-MM-DD |
| **状态** | 草稿 / 评审中 / 已批准 / 已上线 |
| **关联文档** | [链接] |
```
### 二、需求概述
**2.1 背景与问题**
[描述为什么需要这个功能,解决了什么用户痛点或业务问题]
**2.2 目标用户**
- 用户画像 1[描述]
- 用户画像 2[描述]
**2.3 核心目标**
- 业务目标:[可量化指标,如转化率提升 X%]
- 用户目标:[用户获得什么价值]
- 技术目标:[如响应时间、并发量]
### 三、功能描述
**3.1 功能范围**
- P0(必须):[最小可用功能]
- P1(应该):[重要但可后续]
- P2(锦上添花):[可后续迭代]
**3.2 用户故事**
```
作为 [用户角色]
我希望 [功能/行为]
以便 [获得的价值/目标]。
```
**3.3 详细交互说明**
1. [步骤1]:[描述 + 原型图链接]
2. [步骤2]:[描述 + 原型图链接]
**3.4 边界与异常**
- 正常流程:[描述]
- 异常情况1:[触发条件 + 处理方式]
- 异常情况2:[触发条件 + 处理方式]
### 四、非功能需求
| 项目 | 要求 |
|------|------|
| 页面加载 | ≤ 2 秒 |
| 接口响应 | ≤ 500ms |
| 并发支持 | 1000 QPS |
| 兼容性 | iOS 13+, Android 9+, Chrome 90+ |
### 五、数据埋点
| 事件名 | 触发条件 | 属性 |
|--------|----------|------|
| [event_name] | [触发条件] | [上报字段] |
### 六、验收标准
- [ ] 功能1 验收条件
- [ ] 功能2 验收条件
- [ ] 非功能需求满足
### 七、排期与里程碑
| 里程碑 | 日期 | 交付物 |
|--------|------|--------|
| 设计评审 | YYYY-MM-DD | 交互/视觉稿 |
| 技术评审 | YYYY-MM-DD | 技术方案 |
| 开发完成 | YYYY-MM-DD | 可测试版本 |
| 上线 | YYYY-MM-DD | 生产环境 |
## 相关条目
- [需求分析方法.md](需求分析方法.md)
- [开发规范.md](../技术/开发规范.md)
- [UI设计规范.md](../设计/UI设计规范.md)
## 变更记录
| 日期 | 版本 | 变更说明 | 变更人 |
|------|------|----------|--------|
| 2026-06-22 | v1.0 | 初始创建 | 陆怀瑾 |
+24 -16
View File
@@ -1,21 +1,29 @@
# 产品领域知识
# 产品知识
**责任人**:沈路明(productmanager
**审核人**:陆怀瑾(coo
## 领域说明
本目录包含产品规划、需求分析、用户研究的标准流程和方法论,支撑产品从 0 到 1 的完整生命周期。
## 责任团队
- **负责人**: 沈路明 (productmanager)
- **协作者**: 梁思筑 (architect) - 技术方案支持
## 知识范围
涵盖产品需求文档、用户研究、竞品分析、需求管理、版本规划等产品管理知识。
## 条目清单
| 文件名 | 说明 | 状态 |
|--------|------|------|
| [PRD模板.md](PRD模板.md) | 产品需求文档标准模板 | ✅ |
| [需求分析方法.md](需求分析方法.md) | 用户需求调研与分析方法 | ✅ |
## 待建设
- 产品需求文档 (PRD) 模板
- 用户调研方法
- 竞品分析框架
- 产品路线图模板
- 用户故事编写指南
- 产品迭代流程
- 需求优先级评估
- 产品数据指标体系
## 目录结构
- `PRD 模板.md` - 标准产品需求文档格式
- `用户调研指南.md` - 用户访谈和调研方法(待补充)
- `竞品分析模板.md` - 竞品分析框架(待补充)
---
**最后更新**: 2026-06-22
-84
View File
@@ -1,84 +0,0 @@
# 需求分析方法
## 元数据
| 属性 | 值 |
|------|-----|
| **领域** | 产品 |
| **责任人** | 沈路明 (productmanager) |
| **版本** | v1.0 |
| **创建日期** | 2026-06-22 |
| **标签** | 需求分析, 用户调研, 产品管理 |
## 概述
需求分析是从用户/业务痛点出发,将模糊的需求描述转化为可落地的产品功能规格的系统化方法。核心原则:先理解问题,再设计方案。
## 正文
### 一、需求收集方法
1. **用户访谈**(定性)
- 每轮访谈 5-8 个目标用户
- 半结构化访谈:准备提纲 + 灵活追问
- 核心问题:「你最想解决什么问题?」「现在怎么解决的?」
2. **问卷调查**(定量)
- 覆盖 100+ 目标用户
- 包含选择题(量化)+ 开放题(发掘)
- 关键指标:问题频率、痛点程度、替代方案满意度
3. **数据分析**
- 页面点击热力图
- 用户行为漏斗(转化率断点)
- 客服工单高频关键词
### 二、需求优先级评估 — ICE 模型
| 因子 | 说明 | 评分 (1-10) |
|------|------|------------|
| **I**mpact(影响面) | 影响多少用户?对核心指标影响多大? | |
| **C**onfidence(信心度) | 我们有多少证据这个方案有效? | |
| **E**ase(实现难度) | 开发成本多高?时间多长? | |
总分 = I × C × E(E 分数越高越容易,越大越好)
### 三、需求文档化
1. **用户故事标准格式**
> 作为 **[用户角色]**
> 我希望 **[功能/行为]**
> 以便 **[获得的价值]**。
2. **验收条件(Acceptance Criteria**
- 必须可测试、可验证
- 正面条件 + 边缘情况
3. **原型验证**
- 低保真原型验证交互流程(1-2 天)
- 用户测试 3-5 人,观察操作行为
- 根据反馈迭代后进入高保真设计
### 四、需求评审流程
```
需求方提出 → PM 分析评估 → 交互设计 → 技术评审
→ 排期评估 → 最终评审 → 进入开发
```
每一步评审需至少以下人员参与:
- PM(负责人)
- 1 名开发(评估技术可行性)
- 1 名设计师(评估交互可行性)
- 需求方(确认需求理解正确)
## 相关条目
- [PRD模板.md](PRD模板.md)
- [开发规范.md](../技术/开发规范.md)
## 变更记录
| 日期 | 版本 | 变更说明 | 变更人 |
|------|------|----------|--------|
| 2026-06-22 | v1.0 | 初始创建 | 陆怀瑾 |
+21 -13
View File
@@ -1,21 +1,29 @@
# 内容领域知识
# 内容运营知识
**责任人**:文墨言(contentspecialist
**审核人**:陆怀瑾(coo
## 领域说明
本目录包含内容创作、分发、运营的标准流程和方法论,覆盖小红书、公众号、今日头条等内容平台。
## 责任团队
- **负责人**: 文墨言 (contentspecialist)
- **协作者**: 钟帧韵 (mediaspecialist) - 视频内容支持
## 知识范围
涵盖小红书、短视频平台、公众号等内容平台运营知识,包括内容创作、选题策划、标题优化、发布策略、数据分析等。
- 各平台内容创作规范
- 爆款内容分析方法
- 选题策划流程
- 内容发布 SOP
- 数据追踪与优化
- 粉丝互动策略
## 条目清单
## 目录结构
| 文件名 | 说明 | 状态 |
|--------|------|------|
| [小红书运营指南.md](小红书运营指南.md) | 小红书内容运营全流程指南 | ✅ |
| [标题写作技巧.md](标题写作技巧.md) | 爆款标题创作方法论 | ✅ |
- `小红书运营指南.md` - 小红书平台运营方法论
- `公众号运营 SOP.md` - 微信公众号运营流程(待补充)
- `内容选题库.xlsx` - 选题管理模板(待补充)
## 待建设
---
- 短视频脚本模板
- 公众号排版规范
- 内容日历模板
**最后更新**: 2026-06-22
-82
View File
@@ -1,82 +0,0 @@
# 小红书运营指南
## 元数据
| 属性 | 值 |
|------|-----|
| **领域** | 内容 |
| **责任人** | 文墨言 (contentspecialist) |
| **版本** | v1.0 |
| **创建日期** | 2026-06-22 |
| **标签** | 小红书, 内容运营, 种草, 涨粉 |
## 概述
小红书是以"真实分享+种草"为核心的内容社区,运营不同于其他平台。核心逻辑:真诚分享 > 硬广推广,封面/标题决定点击率,内容质量决定涨粉转化。
## 正文
### 一、内容定位与选题
1. **账号定位**(上线前必做)
- 明确赛道:美妆/穿搭/家居/母婴/美食/知识
- 确定人设:专家型/体验型/教程型
- 对标 3-5 个同赛道 Top 博主
2. **选题策略**
- 热点追踪:小红书热搜 + 抖音热点宝
- 实用内容:教程/清单/测评/避坑
- 情感共鸣:个人经历/观点分享/生活记录
### 二、内容制作标准
1. **封面设计**(点击率核心)
- 高饱和度配色,对比度强
- 简洁文字 3-7 字,避免遮挡主体
- 尺寸 3:4,首图即为封面
2. **标题公式**
- 数字型:「3 步搞定...」
- 痛点型:「为什么你...还是不行?」
- 对比型:「A vs B,差距到底在哪」
- 清单型:「2026 必入的 10 款...」
3. **正文结构**
- 开头(3 句):抛痛点/抛结论
- 主体:分点说明,配图对应
- 结尾:互动引导(提问/投票/求关注)
### 三、发布与推广
1. **发布时间**
- 工作日:12:00-14:00, 18:00-21:00
- 周末:10:00-12:00, 15:00-18:00
2. **话题标签策略**
- 1-2 个大流量话题(#穿搭 #美妆 #家居
- 2-3 个精准话题(#小个子穿搭 #通勤穿搭
- 1 个自创话题(#XX的日常搭配
3. **初期冷启动**
- 发布后 1 小时内互动(评论/点赞)对推荐权重影响最大
- 在同类笔记下真诚评论(非硬广引流)
### 四、数据指标
| 指标 | 新手目标 | 进阶目标 |
|------|----------|----------|
| 单篇阅读量 | 1000+ | 5000+ |
| 点赞率 | 3%+ | 5%+ |
| 收藏率 | 2%+ | 4%+ |
| 涨粉率 | 1%/篇 | 3%/篇 |
## 相关条目
- [标题写作技巧.md](标题写作技巧.md)
- [活动策划模板.md](../运营/活动策划模板.md)
## 变更记录
| 日期 | 版本 | 变更说明 | 变更人 |
|------|------|----------|--------|
| 2026-06-22 | v1.0 | 初始创建 | 陆怀瑾 |
-21
View File
@@ -1,21 +0,0 @@
# 技术领域知识
**责任人**:徐聪(costcodev
**审核人**:陆怀瑾(coo
## 知识范围
涵盖开发规范、代码审查、架构设计、部署运维、技术选型等技术团队知识。
## 条目清单
| 文件名 | 说明 | 状态 |
|--------|------|------|
| [开发规范.md](开发规范.md) | 代码编写与项目管理规范 | ✅ |
| [代码审查清单.md](代码审查清单.md) | Pull Request 审查标准 | ✅ |
## 待建设
- API 设计规范
- 数据库设计指南
- 技术选型决策框架
-104
View File
@@ -1,104 +0,0 @@
# 开发规范
## 元数据
| 属性 | 值 |
|------|-----|
| **领域** | 技术 |
| **责任人** | 徐聪 (costcodev) |
| **版本** | v1.0 |
| **创建日期** | 2026-06-22 |
| **标签** | 开发规范, 代码风格, Git, 项目管理 |
## 概述
定义团队统一的代码编写、项目管理、协作流程规范。目的是确保代码可维护、可交接,降低协作摩擦。
## 正文
### 一、代码规范
1. **Python**
- 遵循 PEP 8 代码风格
- 使用 `black` 自动格式化,行宽 100
- 类型注解必须(`mypy --strict` 通过)
- 文档字符串用 Google 风格
2. **TypeScript/JavaScript**
- 使用 `prettier` 格式化
- ESLint 严格模式
- 禁止 `any` 类型(除非显式标注 `// eslint-disable-next-line`
- 所有公共 API 必须有 JSDoc
3. **通用规则**
- 函数单一职责,不超过 50 行
- 命名:camelCase(变量/函数)、PascalCase(类/组件)、UPPER_SNAKE(常量)
- 禁止 `print` / `console.log` 残留(用日志库)
- 禁止注释掉的代码(相信 Git
### 二、Git 规范
1. **分支策略**
```
main ─── 生产环境
develop ─── 开发主线
feature/<task-id>-<desc> ─── 功能分支
fix/<task-id>-<desc> ─── 修复分支
```
2. **Commit 格式**
```
<type>(<scope>): <subject>
<body>
<footer>
```
- type: feat / fix / docs / style / refactor / test / chore
- scope: 模块名(如 api, ui, db
- subject: 不超过 72 字符,中文或英文
3. **PR 流程**
- 所有代码变更必须通过 PR
- 至少 1 人 Review 并 Approve
- CI 全部通过后才能合并
- 合并前 rebase develop 消除冲突
### 三、项目结构规范
```
project/
├── src/ # 源代码
├── tests/ # 测试代码
├── docs/ # 项目文档
├── scripts/ # 运维脚本
├── config/ # 配置文件
├── README.md # 项目说明
├── CHANGELOG.md # 变更日志
└── .env.example # 环境变量模板
```
### 四、文档规范
- **README.md**:项目概述、快速启动、技术栈、目录说明
- **API 文档**:后端接口必须有 OpenAPI/Swagger 文档
- **开发文档**:架构设计、数据流图、部署说明
- **代码即文档**:优先清晰的命名和结构,减少注释
### 五、测试规范
- 单元测试覆盖率 ≥ 70%
- 关键业务逻辑覆盖率 ≥ 90%
- 每个 PR 附带新增/修改的测试
- 使用 `pytest` (Python) / `vitest` (TS)
## 相关条目
- [代码审查清单.md](代码审查清单.md)
- [PRD模板.md](../产品/PRD模板.md)
## 变更记录
| 日期 | 版本 | 变更说明 | 变更人 |
|------|------|----------|--------|
| 2026-06-22 | v1.0 | 初始创建 | 陆怀瑾 |
+21 -13
View File
@@ -1,21 +1,29 @@
# 电商领域知识
# 电商运营知识
**责任人**:陆云帆(taobaospecialist
**审核人**:陆怀瑾(coo
## 领域说明
本目录包含电商平台运营的标准操作流程(SOP)、最佳实践和经验总结,覆盖淘宝、抖店等主流电商平台。
## 责任团队
- **负责人**: 陆云帆 (taobaospecialist)
- **协作者**: 钟帧韵 (mediaspecialist) - 视频素材支持
## 知识范围
涵盖淘宝、抖店、微信小店等多平台电商运营知识,包括店铺搭建、商品上架、营销推广、客户服务、数据分析等。
- 店铺日常运营 SOP
- 商品上架与优化
- 活动策划与执行
- 数据分析方法
- 客服话术模板
- 平台规则解读
## 条目清单
## 目录结构
| 文件名 | 说明 | 状态 |
|--------|------|------|
| [淘宝运营SOP.md](淘宝运营SOP.md) | 淘宝店铺日常运营标准流程 | ✅ |
| [抖店运营SOP.md](抖店运营SOP.md) | 抖音小店运营流程 | ✅ |
- `淘宝运营 SOP.md` - 淘宝店铺日常运营流程
- `抖店运营 SOP.md` - 抖音小店运营流程
- `数据报表模板.xlsx` - 运营数据追踪模板(待补充)
## 待建设
---
- 微信小店运营指南
- 电商数据分析方法
- 客服话术模板
**最后更新**: 2026-06-22
-74
View File
@@ -1,74 +0,0 @@
# 抖店运营 SOP
## 元数据
| 属性 | 值 |
|------|-----|
| **领域** | 电商 |
| **责任人** | 陆云帆 (taobaospecialist) |
| **版本** | v1.0 |
| **创建日期** | 2026-06-22 |
| **标签** | 抖音, 抖店, 电商, SOP |
## 概述
本 SOP 定义抖音小店运营标准流程。抖店运营区别于传统电商的核心在于"内容驱动交易"——通过短视频和直播引流到店铺成交。
## 正文
### 一、每日运营
1. **店铺健康检查**
- 登录抖店后台,检查体验分(≥ 4.6)
- 查看违规记录和扣分情况
- 检查商品状态(在售/审核中/下架)
2. **内容运营**
- 发布 1-2 条挂车短视频
- 检查昨日短视频/直播数据
- 回复评论区用户问题
3. **订单与客服**
- 处理待发货订单(48 小时发货)
- 处理售后申请(退货/退款)
- 3 分钟内回复客服消息
### 二、每周运营
1. **商品策略**
- 分析本周爆款商品,优化标题/主图/详情
- 根据热点趋势选品上新
- 设置限时秒杀/优惠券活动
2. **内容策略**
- 复盘本周短视频/直播数据
- 策划下周内容选题(蹭热点/产品展示/教程)
- 测试新视频形式(口播/开箱/场景化)
3. **投放优化**
- 查看千川投放数据
- 优化投放计划(人群/出价/素材)
- 调整 ROI 目标和预算分配
### 三、每月运营
1. **月度分析**
- 统计月度 GMV、订单量、退款率
- 分析流量来源占比(推荐/搜索/直播/短视频/付费)
- 输出《抖店月度运营报告》
2. **供应链检查**
- 盘点库存,补货预警
- 检查发货时效和物流评分
- 供应商评估和优化
## 相关条目
- [淘宝运营SOP.md](淘宝运营SOP.md)
- [数据分析方法.md](../运营/数据分析方法.md)
## 变更记录
| 日期 | 版本 | 变更说明 | 变更人 |
|------|------|----------|--------|
| 2026-06-22 | v1.0 | 初始创建 | 陆怀瑾 |
-83
View File
@@ -1,83 +0,0 @@
# 淘宝运营 SOP
## 元数据
| 属性 | 值 |
|------|-----|
| **领域** | 电商 |
| **责任人** | 陆云帆 (taobaospecialist) |
| **版本** | v1.0 |
| **创建日期** | 2026-06-22 |
| **标签** | 淘宝, 电商, SOP, 日常运营 |
## 概述
本 SOP 定义淘宝店铺日常运营的标准流程,涵盖店铺维护、商品管理、营销推广、客服处理和数据分析五大模块。适用于每日/每周/每月周期性执行。
## 正文
### 一、每日运营检查(每日 9:00)
1. **店铺状态检查**
- 登录千牛工作台,检查店铺处罚/违规通知
- 确认所有商品在售状态,无异常下架
- 检查店铺评分(DSR),低于 4.7 需立即分析原因
2. **订单处理**
- 查看待发货订单,确保 48 小时内发货
- 处理售后订单(退货/换货/退款),24 小时内响应
- 检查差评/中评,及时联系客户处理
3. **客服响应**
- 检查未读消息,回复时限 5 分钟内
- 查看客服数据:响应时长、满意度
### 二、每周运营任务(每周一)
1. **商品优化**
- 检查 Top 10 商品标题、主图、详情页
- 根据搜索词报告优化标题关键词
- 更新库存不足的商品
2. **营销活动**
- 查看本周淘宝官方活动日历
- 设置店铺优惠券/满减活动
- 更新直通车/引力魔方推广计划
3. **数据分析**
- 查看流量来源(搜索/推荐/付费/其他)
- 分析转化率、客单价变化趋势
- 输出《店铺周报》
### 三、每月运营任务(每月 1 日)
1. **月度复盘**
- 统计月度 GMV、订单量、利润率
- 对比上月数据,分析增长/下滑原因
- 制定下月运营目标和策略
2. **竞品分析**
- 监控 Top 3 竞品店铺动态
- 分析竞品爆款商品和新品
- 调整自身商品/价格策略
### 四、关键指标
| 指标 | 目标值 | 监控频率 |
|------|--------|----------|
| DSR 评分 | ≥ 4.8 | 每日 |
| 48h 发货率 | ≥ 98% | 每日 |
| 客服响应时长 | ≤ 3 分钟 | 每日 |
| 转化率 | ≥ 行业均值 +10% | 每周 |
| GMV 增长 | 月环比 ≥ 10% | 每月 |
## 相关条目
- [抖店运营SOP.md](抖店运营SOP.md)
- [数据分析方法.md](../运营/数据分析方法.md)
## 变更记录
| 日期 | 版本 | 变更说明 | 变更人 |
|------|------|----------|--------|
| 2026-06-22 | v1.0 | 初始创建 | 陆怀瑾 |
-21
View File
@@ -1,21 +0,0 @@
# 行政领域知识
**责任人**:刘诗妮(secretary
**审核人**:陆怀瑾(coo
## 知识范围
涵盖合同管理、报销流程、行政事务、供应商管理等行政支持知识。
## 条目清单
| 文件名 | 说明 | 状态 |
|--------|------|------|
| [合同模板.md](合同模板.md) | 常用合同标准模板 | ✅ |
| [报销流程.md](报销流程.md) | 费用报销申请与审批流程 | ✅ |
## 待建设
- 供应商管理指南
- 会议纪要模板
- 入职/离职流程
+219
View File
@@ -0,0 +1,219 @@
# 合同模板
> 标准合同模板,规范业务流程,降低法律风险
## 📌 目的
**为什么存在这个知识**:统一合同格式,保证关键条款完整,减少法务审核时间和法律风险
## 🎯 适用范围
**什么时候用**:客户合作、供应商合作、合作伙伴协议、劳务合同
**谁在用**:刘诗妮(secretary
**前置条件**:合作意向已确认,商务条款已谈妥
## 📋 合同模板结构
### 合同编号:[年份]-[类型]-[序号]
# [合同类型] 合同
**甲方**(委托方):[公司全称]
**统一社会信用代码**[代码]
**地址**[注册地址]
**法定代表人**[姓名]
**联系人**[姓名]
**联系电话**[电话]
**乙方**(服务方/供货方):[公司全称/个人姓名]
**统一社会信用代码/身份证号**[代码/号码]
**地址**[地址]
**法定代表人/联系人**[姓名]
**联系电话**[电话]
---
## 第一条 合作内容
1.1 乙方向甲方提供以下服务/产品:
- [详细描述服务/产品内容]
- [规格/型号/数量]
- [技术标准/质量要求]
1.2 服务/产品交付标准:
- [具体验收标准]
- [交付物清单]
## 第二条 合同期限
2.1 本合同有效期自 **____年__月__日****____年__月__日** 止。
2.2 合同到期前 [30] 日,双方可协商续签事宜。
## 第三条 合同金额及支付方式
3.1 合同总金额为人民币(大写):**____________元整** (¥________元)
3.2 支付方式:
| 期数 | 支付比例 | 金额 | 支付条件 |
|------|----------|------|----------|
| 第一期 | __% | ¥____元 | 合同签订后__个工作日内 |
| 第二期 | __% | ¥____元 | [里程碑/验收] 后__个工作日内 |
| 第三期 | __% | ¥____元 | [最终验收] 后__个工作日内 |
3.3 乙方应在甲方付款前提供等额增值税专用发票。
3.4 甲方收款账户信息:
- 户名:[公司全称]
- 开户行:[银行名称]
- 账号:[银行账号]
## 第四条 双方权利和义务
**4.1 甲方权利和义务**
- 按合同约定支付款项
- 提供必要的工作配合
- 按约定验收交付物
- [其他]
**4.2 乙方权利和义务**
- 按合同约定提供产品/服务
- 保证产品/服务质量
- 按期交付
- 提供售后服务
- [其他]
## 第五条 知识产权
5.1 本合同履行过程中产生的知识产权归属:
- [ ] 归甲方所有
- [ ] 归乙方所有
- [ ] 双方共有
- [ ] 其他:[具体约定]
5.2 双方保证不侵犯第三方知识产权。
## 第六条 保密条款
6.1 双方对在合作过程中知悉的对方商业秘密、技术秘密承担保密义务。
6.2 保密期限:合同有效期内及合同终止后 [3] 年。
6.3 未经对方书面同意,任何一方不得向第三方披露保密信息。
## 第七条 违约责任
7.1 任何一方违反本合同约定,应承担违约责任,赔偿对方因此遭受的损失。
7.2 乙方逾期交付的,每逾期一日,按合同总金额的 [0.5]% 支付违约金。
7.3 甲方逾期付款的,每逾期一日,按应付未付款的 [0.5]% 支付违约金。
7.4 违约金不足以弥补损失的,违约方还应赔偿差额部分。
## 第八条 合同解除
8.1 经双方协商一致,可以解除本合同。
8.2 有下列情形之一的,守约方有权解除合同:
- 一方严重违约,致使合同目的无法实现
- 一方破产、解散或被吊销营业执照
- 不可抗力持续 [30] 日以上
8.3 合同解除后,双方应结清已履行部分的费用。
## 第九条 不可抗力
9.1 因不可抗力(包括但不限于自然灾害、战争、政府行为、疫情等)导致合同无法履行的,受影响方应及时通知对方,并提供相关证明。
9.2 受不可抗力影响的部分可免除责任,但应尽力减少损失。
## 第十条 争议解决
10.1 本合同履行过程中发生的争议,由双方协商解决。
10.2 协商不成的,任何一方均可向 **甲方所在地人民法院** 提起诉讼。
## 第十一条 其他
11.1 本合同未尽事宜,由双方另行签订补充协议,补充协议与本合同具有同等法律效力。
11.2 本合同一式 [贰] 份,甲乙双方各执 [壹] 份,具有同等法律效力。
11.3 本合同自双方签字盖章之日起生效。
11.4 通知送达地址:
- 甲方送达地址:[地址],联系人:[姓名],电话:[电话]
- 乙方送达地址:[地址],联系人:[姓名],电话:[电话]
---
**甲方**(盖章): **乙方**(盖章):
**授权代表**(签字):授权代表(签字):
**日期**____年__月__日 **日期**____年__月 __日
---
## 附件
- 附件一:服务/产品清单
- 附件二:技术规格书
- 附件三:报价单
- [其他附件]
## ✅ 成功标准
- [ ] 合同条款完整,无遗漏
- [ ] 商务条款清晰,无歧义
- [ ] 法务审核通过
- [ ] 双方签字盖章
- [ ] 合同归档保存
## ⚠️ 常见问题
### Q1: 对方要求修改标准模板怎么办?
**原因**:对方有自己的法务要求、商务条款特殊
**解决办法**
1. 评估修改内容是否触及核心利益
2. 小修改可接受,大修改需法务审核
3. 重大修改需领导审批
**预防方法**:标准模板尽量完善,减少修改空间
### Q2: 合同执行过程中有变更怎么办?
**原因**:需求变化、情况变化
**解决办法**
1. 签订补充协议
2. 补充协议与原合同具有同等效力
3. 明确变更内容和生效时间
**预防方法**:合同预留变更机制
### Q3: 对方违约怎么办?
**原因**:对方不履约、逾期、质量不合格
**解决办法**
1. 发函催告,保留证据
2. 按合同追究违约责任
3. 协商不成,走法律途径
**预防方法**:合同明确违约责任,履约过程保留证据
## 🔗 相关资源
- 法务支持:[法务联系人]
- 合同管理系统:[系统链接]
- 工商查询:国家企业信用信息公示系统
## 📊 版本记录
| 版本 | 日期 | 更新内容 | 更新人 |
|------|------|----------|--------|
| v1.0 | 2026-06-22 | 初始创建 | 陆怀瑾 |
---
**责任人**:刘诗妮
**最后更新**2026-06-22
+212 -63
View File
@@ -1,83 +1,232 @@
# 报销流程
## 元数据
> 标准化费用报销流程,提高报销效率,规范财务管理
| 属性 | 值 |
|------|-----|
| **领域** | 行政 |
| **责任人** | 刘诗妮 (secretary) |
| **版本** | v1.0 |
| **创建日期** | 2026-06-22 |
| **标签** | 行政, 报销, 财务, 流程 |
## 📌 目的
## 概述
**为什么存在这个知识**:统一报销流程,减少沟通成本,保证报销合规、及时到账
定义公司费用报销的标准流程,涵盖申请、审批、核销三大阶段,确保财务合规性和报销效率。
## 🎯 适用范围
## 正文
**什么时候用**:员工因公消费后申请报销
**谁在用**:全体员工
**前置条件**:消费已发生,取得合规发票
### 一、报销范围
## 📋 报销流程
| 类别 | 说明 | 限额 |
|------|------|------|
| 差旅费 | 交通、住宿、餐饮 | 按出差地标准 |
| 办公用品 | 设备、耗材、文具 | 单次 ≤ ¥2000 |
| 招待费 | 客户/合作伙伴接待 | 需提前申请 |
| 培训费 | 课程、考试、认证 | 需审批 |
| 软件服务 | SaaS 订阅、API 费用 | 按需审批 |
### 第一步:取得发票(消费时)
### 二、报销流程
**发票要求**
- 发票抬头:**[公司全称]**
- 统一社会信用代码:[公司税号]
- 发票内容与实际消费一致
- 发票章清晰
**可报销票据类型**
- [ ] 增值税专用发票
- [ ] 增值税普通发票
- [ ] 电子发票(需打印)
- [ ] 行程单(机票)
- [ ] 车票(火车、汽车)
- [ ] 出租车票(需注明起止地点和事由)
- [ ] 定额发票
**不可报销票据**
- ❌ 个人消费发票
- ❌ 发票抬头为个人
- ❌ 发票内容模糊或不符
- ❌ 收据/白条(特殊情况需审批)
- ❌ 超过 [6 个月] 的发票
### 第二步:填写报销单(每周三前)
**报销渠道**
- [方式 1] 飞书审批 - 费用报销
- [方式 2] 钉钉审批 - 费用报销
- [方式 3] 纸质报销单(特殊情况)
**报销单必填项**
- 报销人姓名、部门
- 报销事由(详细、具体)
- 费用明细(分类填写)
- 发票张数、总金额
- 收款账户信息
**费用分类**
- 差旅费(交通、住宿、餐饮)
- 业务招待费
- 办公用品
- 推广费用
- 培训费用
- 其他(注明具体事项)
### 第三步:提交审批(每周三截止)
**审批流程**
```
提交申请 → 直属审批 → COO 审批(> ¥5000
→ 刘总审批(> ¥20000) → 刘诗妮核销 → 归档
报销人提交 → 直属上级审批 → 部门负责人审批 → 财务审核 → 总经理审批(>5000 元)→ 出纳付款
```
**各环节时限**
- 员工提交:消费后 7 个工作日内
- 直属审批:2 个工作日内
- 核销:审批通过后 5 个工作日内
**审批时效**
- 直属上级:1 个工作日内
- 部门负责人:1 个工作日内
- 财务审核:2 个工作日内
- 总经理审批:2 个工作日内(如需)
- 出纳付款:3 个工作日内
### 三、报销材料
1. **发票**
- 必须增值税普通/专用发票
- 发票抬头:公司全称 + 税号
- 电子发票可,纸质发票需原件
2. **报销单**
- 事由:清晰说明消费目的
- 明细:逐项列出费用+金额
- 附件上传:发票图片/电子凭证
3. **特殊说明**
- 差旅:附行程单
- 招待:附参与人员名单
- 大额采购:附比价记录
### 四、常见退回原因
| 原因 | 处理 |
|------|------|
| 发票信息错误(抬头/税号) | 退回重新开票 |
| 超额未提前审批 | 补充说明或自付超额部分 |
| 缺少明细说明 | 补充报销单信息 |
| 超过报销时效 | 特殊说明后处理 |
### 五、审批人
| 金额区间 | 审批人 |
**审批金额权限**
| 金额范围 | 审批人 |
|----------|--------|
| ≤ ¥5000 | 直属负责人 |
| ¥5001 ~ ¥20000 | + COO(陆怀瑾) |
| > ¥20000 | + 刘总(Vincent |
| ≤1000 | 直属上级 |
| 1000-5000 元 | 部门负责人 |
| 5000-20000 元 | 总经理 |
| >20000 元 | 总经理 + 财务负责人 |
## 相关条目
### 第四步:财务审核
- [合同模板.md](合同模板.md)
**审核要点**
- 发票合规性(抬头、税号、印章)
- 报销事由合理性
- 费用标准符合公司制度
- 单据完整性
- 预算内支出
## 变更记录
**常见问题处理**
- 发票不合规 → 退回重开
- 单据不全 → 补充材料
- 超标费用 → 特殊审批或自理
- 预算外 → 追加预算审批
| 日期 | 版本 | 变更说明 | 变更人 |
### 第五步:打款(审核通过后 3 个工作日内)
**打款方式**
- 银行转账(推荐)
- 支付宝/微信(小额)
**打款时间**
- 每周二、周五统一打款
- 节假日顺延
## 📋 费用标准
### 差旅费标准
| 项目 | 员工 | 经理 | 总监及以上 |
|------|------|------|------------|
| 飞机 | 经济舱 | 经济舱 | 公务舱 |
| 火车 | 二等座 | 一等座 | 商务座 |
| 住宿(元/晚) | ≤400 | ≤600 | ≤1000 |
| 餐饮补贴(元/天) | 100 | 150 | 200 |
| 市内交通 | 实报实销 | 实报实销 | 实报实销 |
**差旅住宿说明**
- 一线城市(北上广深):标准上浮 20%
- 两人同行可同住一间,按较高标准执行
### 业务招待费标准
| 招待对象 | 标准(元/人) | 审批要求 |
|----------|---------------|----------|
| 普通客户 | ≤200 | 部门负责人 |
| 重要客户 | ≤500 | 总经理 |
| 战略伙伴 | ≤1000 | 总经理 + 财务 |
**招待费说明**
- 需提前申请,注明招待对象、人数、事由
- 报销时需提供消费清单
## ⚠️ 注意事项
### 发票管理
- 电子发票需打印并承诺「未重复报销」
- 发票丢失:需取得发票复印件 + 税务局证明
- 发票抬头错误:需重开,不接受说明
### 报销时效
- 发票自开具之日起 [6 个月] 内报销
- 超过期限需特殊审批,且不超当年
### 差旅报销
- 差旅前需填写《出差申请单》
- 机票/酒店优先公司协议价
- 自驾出差按 [1 元/公里] 补贴,过路费实报
### 禁止行为
- ❌ 虚报、多报
- ❌ 替他人报销
- ❌ 拆分发票规避审批
- ❌ 使用假发票
**违规处理**
- 首次:警告 + 追回款项
- 二次:通报批评 + 罚款
- 三次:辞退 + 法律追责
## ✅ 成功标准
- [ ] 报销单填写完整、准确
- [ ] 发票合规、清晰
- [ ] 审批流程顺利
- [ ] 报销款及时到账
- [ ] 无退单、无差错
## 📊 报销流程时效
| 环节 | 时效 | 责任方 |
|------|------|--------|
| 提交报销单 | 每周三前 | 报销人 |
| 审批完成 | 3-5 个工作日 | 审批人 |
| 财务审核 | 2 个工作日 | 财务 |
| 打款 | 3 个工作日 | 出纳 |
| **合计** | **约 7-10 个工作日** | |
## ⚠️ 常见问题
### Q1: 发票丢了怎么办?
**解决办法**
1. 联系开票方取得发票复印件
2. 开票方主管税务机关出具《丢失增值税专用发票已报税证明单》
3. 复印件 + 证明单可报销
### Q2: 紧急支出来不及走流程怎么办?
**解决办法**
1. 先微信/电话请示上级
2. 事后 [3 个工作日] 内补流程
3. 特殊情况可先借款,后冲销
### Q3: 报销被退单了怎么办?
**常见原因**
- 发票不合规 → 重开发票
- 单据不全 → 补充材料
- 超标 → 特殊审批或自理部分
- 事由不清 → 补充说明
**处理流程**
1. 查看退单原因
2. 补充/修改后重新提交
3. 审批流程重新计算时效
## 🔗 相关资源
- 飞书审批入口:[链接]
- 财务联系人:[姓名/电话]
- 发票查验:[国家税务总局全国增值税发票查验平台](https://inv-veri.chinatax.gov.cn/)
## 📊 版本记录
| 版本 | 日期 | 更新内容 | 更新人 |
|------|------|----------|--------|
| 2026-06-22 | v1.0 | 初始创建 | 陆怀瑾 |
| v1.0 | 2026-06-22 | 初始创建 | 陆怀瑾 |
---
**责任人**:刘诗妮
**最后更新**2026-06-22
-21
View File
@@ -1,21 +0,0 @@
# 设计领域知识
**责任人**:苏绘锦(designer
**审核人**:陆怀瑾(coo
## 知识范围
涵盖 UI/UX 设计规范、品牌元素、商详页设计、首图制作等设计知识。
## 条目清单
| 文件名 | 说明 | 状态 |
|--------|------|------|
| [UI设计规范.md](UI设计规范.md) | 界面设计标准与组件规范 | ✅ |
| [品牌元素指南.md](品牌元素指南.md) | 品牌色/字体/Logo 使用规范 | ✅ |
## 待建设
- 商详页设计模板
- 首图设计规范
- 移动端适配指南
-87
View File
@@ -1,87 +0,0 @@
# UI 设计规范
## 元数据
| 属性 | 值 |
|------|-----|
| **领域** | 设计 |
| **责任人** | 苏绘锦 (designer) |
| **版本** | v1.0 |
| **创建日期** | 2026-06-22 |
| **标签** | UI, 设计规范, 组件, 视觉 |
## 概述
定义统一的 UI 设计标准,涵盖色彩、字体、间距、组件等基础规范,确保产品视觉一致性,降低设计-开发沟通成本。
## 正文
### 一、色彩系统
| 用途 | 色值 | 说明 |
|------|------|------|
| 主色 Primary | `#1677FF` | 按钮、链接、选中态 |
| 成功 Success | `#52C41A` | 成功提示、通过状态 |
| 警告 Warning | `#FAAD14` | 警告提示 |
| 错误 Error | `#FF4D4F` | 错误提示、删除操作 |
| 文字主色 | `#1F1F1F` | 标题、正文 |
| 文字次色 | `#666666` | 辅助说明 |
| 文字禁用 | `#BFBFBF` | 禁用/占位符 |
| 边框 | `#D9D9D9` | 分割线、输入框边框 |
| 背景 | `#F5F5F5` | 页面底色 |
### 二、字体规范
| 层级 | 字号 | 行高 | 字重 | 用途 |
|------|------|------|------|------|
| H1 | 24px | 32px | 600 | 页面主标题 |
| H2 | 20px | 28px | 600 | 区块标题 |
| H3 | 16px | 24px | 500 | 小标题 |
| Body | 14px | 22px | 400 | 正文 |
| Caption | 12px | 18px | 400 | 辅助/说明文字 |
默认字体:`-apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif`
### 三、间距体系
采用 4px 为基础单位的 8 点栅格:
| Token | 值 | 用途 |
|-------|-----|------|
| xs | 4px | 紧凑间距 |
| sm | 8px | 元素内间距 |
| md | 16px | 组件间距 |
| lg | 24px | 区块间距 |
| xl | 32px | 大区块分隔 |
| xxl | 48px | 页面级分隔 |
### 四、圆角与阴影
| 组件 | 圆角 | 阴影 |
|------|------|------|
| 卡片 | 8px | `0 2px 8px rgba(0,0,0,0.08)` |
| 弹窗 | 12px | `0 6px 16px rgba(0,0,0,0.12)` |
| 按钮 | 6px | 无(默认)/ hover 时微阴影 |
| 输入框 | 6px | 无(默认)/ focus 时外发光 |
### 五、响应式断点
| 断点 | 最小宽度 | 适用设备 |
|------|----------|----------|
| xs | < 576px | 手机竖屏 |
| sm | ≥ 576px | 手机横屏 |
| md | ≥ 768px | 平板 |
| lg | ≥ 992px | 小桌面 |
| xl | ≥ 1200px | 大桌面 |
| xxl | ≥ 1600px | 超大屏 |
## 相关条目
- [品牌元素指南.md](品牌元素指南.md)
- [PRD模板.md](../产品/PRD模板.md)
## 变更记录
| 日期 | 版本 | 变更说明 | 变更人 |
|------|------|----------|--------|
| 2026-06-22 | v1.0 | 初始创建 | 陆怀瑾 |
-21
View File
@@ -1,21 +0,0 @@
# 运营领域知识
**责任人**:陆怀瑾(coo
**审核人**:刘炜承(Vincent
## 知识范围
涵盖活动策划、数据分析、SOP 管理、流程优化、团队协作等运营管理知识。
## 条目清单
| 文件名 | 说明 | 状态 |
|--------|------|------|
| [活动策划模板.md](活动策划模板.md) | 营销活动策划标准模板 | ✅ |
| [数据分析方法.md](数据分析方法.md) | 运营数据分析框架与方法 | ✅ |
## 待建设
- 周报模板
- KPI 管理框架
- 风险评估矩阵
-92
View File
@@ -1,92 +0,0 @@
# 数据分析方法
## 元数据
| 属性 | 值 |
|------|-----|
| **领域** | 运营 |
| **责任人** | 陆怀瑾 (coo) |
| **版本** | v1.0 |
| **创建日期** | 2026-06-22 |
| **标签** | 数据分析, 运营, KPI, 看板 |
## 概述
建立全业务流程的数据分析框架,确保各业务线有统一的指标定义和分析方法,支撑数据驱动决策。
## 正文
### 一、核心指标体系
#### 1. 电商业务
| 层级 | 指标 | 定义 | 频率 |
|------|------|------|------|
| 北极星 | GMV | 总成交额 | 日/周/月 |
| 过程 | 转化率 | 下单数/访客数 | 日 |
| 过程 | 客单价 | GMV/订单数 | 周 |
| 过程 | 退货率 | 退货数/订单数 | 周 |
| 健康 | DSR | 描述/服务/物流评分 | 日 |
| 健康 | 获客成本 CAC | 营销花费/新客数 | 月 |
#### 2. 内容业务
| 层级 | 指标 | 定义 | 频率 |
|------|------|------|------|
| 北极星 | 粉丝增长 | 净增粉丝数 | 周/月 |
| 过程 | 互动率 | (点赞+收藏+评论)/曝光 | 篇 |
| 过程 | 发布频率 | 每周发布篇数 | 周 |
#### 3. 公司整体
| 层级 | 指标 | 定义 | 频率 |
|------|------|------|------|
| 北极星 | 月营收 | 各业务线收入合计 | 月 |
| 效率 | 人效 | 营收/团队人数 | 季 |
| 效率 | Agent 利用率 | Agent 任务完成数/总分配数 | 周 |
### 二、分析框架
**AARRR 海盗模型**
```
Acquisition(获取)→ Activation(激活)→ Retention(留存)
→ Revenue(收入)→ Referral(推荐)
```
**电商应用示例**
1. Acquisition:各渠道流量来源占比
2. Activation:首次下单转化率
3. Retention30 天复购率
4. Revenue:LTV(用户生命周期价值)
5. Referral:分享率、裂变系数
### 三、数据看板要求
每个业务线需维护以下看板:
| 看板 | 内容 | 更新频率 |
|------|------|----------|
| 日报 | 昨日核心指标 + 异常波动标注 | 每日 10:00 |
| 周报 | 趋势图 + 同比/环比 + 分析洞察 | 每周一 |
| 月报 | 完整指标矩阵 + 目标达成率 + 下月预测 | 每月 3 日 |
### 四、异常预警规则
| 条件 | 级别 | 响应 |
|------|------|------|
| GMV 日环比下降 > 20% | 🔴 | COO 立即介入 |
| 转化率连续 3 天下降 | 🟡 | 业务负责人分析 |
| 退货率 > 10% | 🟡 | 商品/客服联合排查 |
| DSR < 4.6 | 🔴 | 立即优化 |
## 相关条目
- [淘宝运营SOP.md](../电商/淘宝运营SOP.md)
- [活动策划模板.md](活动策划模板.md)
## 变更记录
| 日期 | 版本 | 变更说明 | 变更人 |
|------|------|----------|--------|
| 2026-06-22 | v1.0 | 初始创建 | 陆怀瑾 |
-80
View File
@@ -1,80 +0,0 @@
# 活动策划模板
## 元数据
| 属性 | 值 |
|------|-----|
| **领域** | 运营 |
| **责任人** | 陆怀瑾 (coo) |
| **版本** | v1.0 |
| **创建日期** | 2026-06-22 |
| **标签** | 运营, 活动策划, 营销, 模板 |
## 概述
标准化营销活动策划流程。适用于电商大促(618/双11/年货节)、店铺周年庆、新品发布、会员日活动等。
## 正文
### 一、活动策划文档结构
```
# [活动名称] 策划方案
## 1. 活动背景与目标
- 背景:[为什么做这次活动]
- 核心目标:[可量化,如 GMV X万 / 新增粉丝 Y人]
- 次要目标:[如品牌曝光、老客复购]
## 2. 目标用户
- 主要人群:[画像描述]
- 需求动机:[为什么他们会参与]
## 3. 活动机制
- 玩法规则:[满减/秒杀/抽奖/打卡]
- 用户路径:[从看到到参与的完整链路]
- 激励机制:[优惠力度、稀缺性、社交裂变]
## 4. 资源与预算
| 项目 | 预算 | 负责人 |
|------|------|--------|
| 流量投放 | ¥XX | [姓名] |
| 商品补贴 | ¥XX | [姓名] |
| 内容物料 | ¥XX | [姓名] |
## 5. 时间线
| 阶段 | 时间 | 关键事项 |
|------|------|----------|
| 预热期 | D-7 ~ D-1 | 预告内容、优惠券发放 |
| 爆发期 | D-Day | 主活动上线 |
| 返场期 | D+1 ~ D+3 | 余热运营 |
## 6. 风险预案
| 风险 | 概率 | 影响 | 应对 |
|------|------|------|------|
| 服务器崩溃 | 中 | 高 | 提前压测 + 降级方案 |
| 库存不足 | 低 | 中 | 预售 + 安全库存预警 |
## 7. 复盘框架
- 活动数据回顾(GMV/ROI/客单价/转化率)
- 亮点与不足
- 优化建议
```
### 二、关键审批节点
1. **策划方案评审** → COO + 业务负责人
2. **预算审批** → Vincent
3. **法务合规审查** → 苏慎(如涉及抽奖/满赠)
4. **上线前 Checklist** → 所有执行人确认
## 相关条目
- [数据分析方法.md](数据分析方法.md)
- [淘宝运营SOP.md](../电商/淘宝运营SOP.md)
## 变更记录
| 日期 | 版本 | 变更说明 | 变更人 |
|------|------|----------|--------|
| 2026-06-22 | v1.0 | 初始创建 | 陆怀瑾 |
-50
View File
@@ -1,50 +0,0 @@
# Alertmanager 配置
# 告警通知路由到 Feishu
global:
resolve_timeout: 5m
route:
receiver: "default"
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
routes:
# 严重告警 → 通知 Vincent
- receiver: "vincent-critical"
match:
severity: critical
repeat_interval: 2h
continue: true
# 警告告警 → 通知 COO
- receiver: "coo-warning"
match:
severity: warning
repeat_interval: 4h
receivers:
- name: "default"
webhook_configs:
- url: "http://host.docker.internal:9094/webhook"
send_resolved: true
- name: "vincent-critical"
webhook_configs:
- url: "http://host.docker.internal:9094/webhook"
send_resolved: true
- name: "coo-warning"
webhook_configs:
- url: "http://host.docker.internal:9094/webhook"
send_resolved: true
# 抑制规则:严重告警自动抑制同源的警告
inhibit_rules:
- source_match:
severity: critical
target_match:
severity: warning
equal:
- alertname
- instance
@@ -1,288 +0,0 @@
{
"title": "OpenClaw Agent Health Dashboard",
"uid": "agent-health",
"version": 1,
"tags": ["openclaw", "agent", "monitoring"],
"timezone": "browser",
"editable": true,
"refresh": "30s",
"panels": [
{
"title": "系统资源概览",
"type": "row",
"gridPos": {"h": 1, "w": 24, "x": 0, "y": 0}
},
{
"id": 1,
"title": "CPU 使用率",
"type": "gauge",
"gridPos": {"h": 8, "w": 6, "x": 0, "y": 1},
"targets": [
{
"expr": "100 - (avg by(instance) (rate(node_cpu_seconds_total{mode=\"idle\"}[5m])) * 100)",
"legendFormat": "{{instance}}"
}
],
"options": {
"reduceOptions": {"calcs": ["lastNotNull"]},
"showThresholdLabels": false,
"showThresholdMarkers": true
},
"thresholds": [
{"color": "green", "value": null},
{"color": "yellow", "value": 70},
{"color": "red", "value": 90}
]
},
{
"id": 2,
"title": "内存使用率",
"type": "gauge",
"gridPos": {"h": 8, "w": 6, "x": 6, "y": 1},
"targets": [
{
"expr": "(1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100",
"legendFormat": "{{instance}}"
}
],
"options": {
"reduceOptions": {"calcs": ["lastNotNull"]},
"showThresholdLabels": false,
"showThresholdMarkers": true
},
"thresholds": [
{"color": "green", "value": null},
{"color": "yellow", "value": 80},
{"color": "red", "value": 95}
]
},
{
"id": 3,
"title": "磁盘使用率",
"type": "gauge",
"gridPos": {"h": 8, "w": 6, "x": 12, "y": 1},
"targets": [
{
"expr": "max by(instance) ((node_filesystem_size_bytes - node_filesystem_free_bytes) / node_filesystem_size_bytes * 100)",
"legendFormat": "{{instance}}"
}
],
"options": {
"reduceOptions": {"calcs": ["lastNotNull"]},
"showThresholdLabels": false,
"showThresholdMarkers": true
},
"thresholds": [
{"color": "green", "value": null},
{"color": "yellow", "value": 80},
{"color": "red", "value": 95}
]
},
{
"id": 4,
"title": "系统负载",
"type": "stat",
"gridPos": {"h": 8, "w": 6, "x": 18, "y": 1},
"targets": [
{
"expr": "node_load1",
"legendFormat": "1min"
},
{
"expr": "node_load5",
"legendFormat": "5min"
},
{
"expr": "node_load15",
"legendFormat": "15min"
}
],
"options": {
"reduceOptions": {"calcs": ["lastNotNull"]},
"colorMode": "background",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "horizontal",
"textMode": "auto"
}
},
{
"title": "Agent 健康状态",
"type": "row",
"gridPos": {"h": 1, "w": 24, "x": 0, "y": 9}
},
{
"id": 5,
"title": "Agent 心跳状态",
"type": "table",
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 10},
"targets": [
{
"expr": "agent_heartbeat_status",
"legendFormat": "{{agent_label}}"
}
],
"transformations": [
{"id": "organize", "options": {"excludeByName": {}, "indexByName": {}, "renameByName": {"Value": "状态"}}}
],
"fieldConfig": {
"defaults": {
"custom": {
"align": "center",
"displayMode": "color-background"
},
"mappings": [
{"type": "value", "options": {"0": {"color": "red", "text": "❌ 超时"}, "1": {"color": "green", "text": "✅ 正常"}}}
],
"thresholds": [{"color": "green", "value": null}]
}
}
},
{
"id": 6,
"title": "任务停滞时长",
"type": "bargauge",
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 10},
"targets": [
{
"expr": "agent_task_stagnation_seconds",
"legendFormat": "{{agent_label}}"
}
],
"options": {
"orientation": "horizontal",
"displayMode": "gradient",
"showUnfilled": true
},
"fieldConfig": {
"defaults": {
"unit": "s",
"thresholds": [
{"color": "green", "value": null},
{"color": "yellow", "value": 3600},
{"color": "red", "value": 14400}
]
}
}
},
{
"id": 7,
"title": "待办任务数",
"type": "stat",
"gridPos": {"h": 4, "w": 6, "x": 0, "y": 18},
"targets": [
{
"expr": "agent_workboard_pending",
"legendFormat": "待办任务"
}
],
"options": {
"reduceOptions": {"calcs": ["lastNotNull"]},
"colorMode": "background",
"graphMode": "area",
"textMode": "auto"
},
"thresholds": [
{"color": "green", "value": null},
{"color": "yellow", "value": 5},
{"color": "red", "value": 10}
]
},
{
"id": 8,
"title": "429 错误计数",
"type": "stat",
"gridPos": {"h": 4, "w": 6, "x": 6, "y": 18},
"targets": [
{
"expr": "agent_429_error_rate",
"legendFormat": "429 错误"
}
],
"options": {
"reduceOptions": {"calcs": ["lastNotNull"]},
"colorMode": "background",
"graphMode": "area",
"textMode": "auto"
},
"thresholds": [
{"color": "green", "value": null},
{"color": "yellow", "value": 10},
{"color": "red", "value": 50}
]
},
{
"id": 9,
"title": "Prometheus 目标状态",
"type": "table",
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 18},
"targets": [
{
"expr": "up",
"legendFormat": "{{job}} ({{instance}})"
}
],
"fieldConfig": {
"defaults": {
"custom": {"align": "center", "displayMode": "color-background"},
"mappings": [
{"type": "value", "options": {"0": {"color": "red", "text": "❌ Down"}, "1": {"color": "green", "text": "✅ Up"}}}
]
}
}
},
{
"title": "告警状态",
"type": "row",
"gridPos": {"h": 1, "w": 24, "x": 0, "y": 26}
},
{
"id": 10,
"title": "活跃告警",
"type": "table",
"gridPos": {"h": 8, "w": 24, "x": 0, "y": 27},
"targets": [
{
"expr": "ALERTS{alertstate=\"firing\"}",
"legendFormat": "{{alertname}}"
}
],
"fieldConfig": {
"defaults": {
"custom": {"align": "left"},
"mappings": [
{"type": "value", "options": {"0": {"color": "green", "text": "已恢复"}, "1": {"color": "red", "text": "触发中"}}}
]
}
}
}
],
"schemaVersion": 38,
"style": "dark",
"tags": ["openclaw", "agent", "monitoring"],
"templating": {
"list": [
{
"name": "datasource",
"type": "datasource",
"query": "prometheus",
"current": {"value": "Prometheus"}
}
]
},
"annotations": {
"list": [
{
"name": "告警事件",
"type": "dashboard",
"builtIn": 1,
"datasource": {"type": "prometheus", "uid": "PBFA97CFB590B2093"},
"enable": true,
"hide": true,
"iconColor": "rgba(255, 96, 96, 1)",
"expr": "ALERTS",
"step": "60s"
}
]
}
}
@@ -1,12 +0,0 @@
apiVersion: 1
providers:
- name: "Agent Health"
orgId: 1
folder: "OpenClaw"
type: file
disableDeletion: false
editable: true
updateIntervalSeconds: 10
options:
path: /etc/grafana/provisioning/dashboards
-42
View File
@@ -1,42 +0,0 @@
global:
scrape_interval: 15s
evaluation_interval: 15s
# Alertmanager 配置
alerting:
alertmanagers:
- static_configs:
- targets:
- alertmanager:9093
# 规则文件
rule_files:
- "agent_alerts.yml"
# 抓取配置
scrape_configs:
# Prometheus 自监控
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
# Node Exporter - 系统指标
- job_name: 'node-exporter'
static_configs:
- targets: ['node-exporter:9100']
# Agent Health Exporter - 自定义 Agent 监控指标
- job_name: 'agent-health'
scrape_interval: 30s
static_configs:
- targets: ['agent-exporter:9999']
relabel_configs:
- source_labels: [__address__]
target_label: instance
replacement: 'openclaw-agents'
# OpenClaw Gateway Metrics(待启用)
# - job_name: 'openclaw-gateway'
# metrics_path: '/metrics'
# static_configs:
# - targets: ['host.docker.internal:18789']
-92
View File
@@ -1,92 +0,0 @@
version: '3.8'
services:
prometheus:
image: m.daocloud.io/docker.io/prom/prometheus:v2.52.0
container_name: prometheus
ports:
- "9090:9090"
volumes:
- ./config/prometheus.yml:/etc/prometheus/prometheus.yml
- ./config/agent_alerts.yml:/etc/prometheus/agent_alerts.yml
- ./data/prometheus:/prometheus
extra_hosts:
- "host.docker.internal:host-gateway"
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--web.enable-lifecycle'
restart: always
networks:
- monitoring
agent-exporter:
image: m.daocloud.io/docker.io/python:3.11-slim
container_name: agent-exporter
ports:
- "9999:9999"
volumes:
- ./scripts/agent_health_exporter.py:/app/exporter.py:ro
command: python3 /app/exporter.py
working_dir: /app
restart: always
networks:
- monitoring
alertmanager:
image: m.daocloud.io/docker.io/prom/alertmanager:v0.27.0
container_name: alertmanager
ports:
- "9093:9093"
volumes:
- ./config/alertmanager.yml:/etc/alertmanager/alertmanager.yml
- ./data/alertmanager:/alertmanager
extra_hosts:
- "host.docker.internal:host-gateway"
command:
- '--config.file=/etc/alertmanager/alertmanager.yml'
- '--storage.path=/alertmanager'
- '--web.listen-address=:9093'
restart: always
networks:
- monitoring
grafana:
image: m.daocloud.io/docker.io/grafana/grafana:11.0.0
container_name: grafana
ports:
- "3001:3000"
environment:
- GF_SECURITY_ADMIN_USER=admin
- GF_SECURITY_ADMIN_PASSWORD=***
- GF_INSTALL_PLUGINS=grafana-clock-panel,grafana-piechart-panel
volumes:
- ./data/grafana:/var/lib/grafana
- ./config/grafana/dashboards:/etc/grafana/provisioning/dashboards
- ./config/grafana/datasources:/etc/grafana/provisioning/datasources
restart: always
networks:
- monitoring
depends_on:
- prometheus
node-exporter:
image: m.daocloud.io/docker.io/prom/node-exporter:v1.8.2
container_name: node-exporter
ports:
- "9100:9100"
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /:/rootfs:ro
command:
- '--path.procfs=/host/proc'
- '--path.sysfs=/host/sys'
- '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($|/)'
restart: always
networks:
- monitoring
networks:
monitoring:
driver: bridge
-180
View File
@@ -1,180 +0,0 @@
#!/usr/bin/env python3
"""
OpenClaw Agent Health Exporter v2.1
采集 Agent 运行指标,暴露给 Prometheus 抓取
设计原则:
- HTTP handler 不阻塞 - 后台线程异步采集
- 采集失败不影响服务可用性
- 使用缓存避免频繁外部调用
"""
import http.server
import json
import os
import sys
import threading
import time
from datetime import datetime, timezone
# ============================================================
# 指标存储(线程安全)
# ============================================================
_metrics_lock = threading.Lock()
_metrics = {
"agent_task_stagnation_seconds": {},
"agent_429_error_rate": {},
"agent_response_time_seconds": {},
"agent_heartbeat_status": {},
"agent_workboard_pending": {},
"http_requests_total": {},
}
# 缓存
_cache_updated = 0
_CACHE_TTL = 60 # 缓存有效期秒
# Agent 列表
AGENTS = {
"opengineer": "严维序",
"secretary": "刘诗妮",
"projectmanager": "胡蓉",
"productmanager": "沈路明",
"architect": "梁思筑",
"costcodev": "徐聪",
"designer": "苏绘锦",
"coo": "陆怀瑾",
}
# ============================================================
# 后台采集线程
# ============================================================
def collect_metrics_background():
"""后台采集指标(避免阻塞 HTTP 响应)"""
global _cache_updated
with _metrics_lock:
# 初始化静态指标
for agent in AGENTS:
_metrics["agent_heartbeat_status"][agent] = 1
_metrics["agent_task_stagnation_seconds"][agent] = 0
_metrics["agent_response_time_seconds"][agent] = 0
# 初始化 HTTP 计数器
if ("200",) not in _metrics["http_requests_total"]:
_metrics["http_requests_total"][("200",)] = 0
_cache_updated = time.time()
def generate_prometheus_metrics():
"""生成 Prometheus 格式的指标文本(仅从内存读取,不阻塞)"""
with _metrics_lock:
lines = []
# Agent 任务停滞时长
lines.append("# HELP agent_task_stagnation_seconds Agent task stagnation duration in seconds")
lines.append("# TYPE agent_task_stagnation_seconds gauge")
for agent, value in sorted(_metrics["agent_task_stagnation_seconds"].items()):
agent_label = AGENTS.get(agent, agent)
lines.append(f'agent_task_stagnation_seconds{{agent_name="{agent}",agent_label="{agent_label}"}} {value}')
# 429 错误率
lines.append("# HELP agent_429_error_rate 429 error count")
lines.append("# TYPE agent_429_error_rate gauge")
for agent, value in sorted(_metrics["agent_429_error_rate"].items()):
lines.append(f'agent_429_error_rate{{agent_name="{agent}"}} {value}')
# Agent 响应延迟
lines.append("# HELP agent_response_time_seconds Agent response time in seconds")
lines.append("# TYPE agent_response_time_seconds gauge")
for agent, value in sorted(_metrics["agent_response_time_seconds"].items()):
agent_label = AGENTS.get(agent, agent)
lines.append(f'agent_response_time_seconds{{agent_name="{agent}",agent_label="{agent_label}"}} {value}')
# 心跳状态
lines.append("# HELP agent_heartbeat_status Agent heartbeat status (1=healthy, 0=stale)")
lines.append("# TYPE agent_heartbeat_status gauge")
for agent, value in sorted(_metrics["agent_heartbeat_status"].items()):
agent_label = AGENTS.get(agent, agent)
lines.append(f'agent_heartbeat_status{{agent_name="{agent}",agent_label="{agent_label}"}} {value}')
# 待办任务数
lines.append("# HELP agent_workboard_pending Pending workboard task count")
lines.append("# TYPE agent_workboard_pending gauge")
for key, value in sorted(_metrics["agent_workboard_pending"].items()):
lines.append(f'agent_workboard_pending{{type="{key}"}} {value}')
# HTTP 请求计数
lines.append("# HELP http_requests_total Total HTTP requests")
lines.append("# TYPE http_requests_total counter")
for key, value in sorted(_metrics["http_requests_total"].items()):
status = key[0]
lines.append(f'http_requests_total{{status="{status}"}} {value}')
return "\n".join(lines) + "\n"
# ============================================================
# HTTP Handler(不阻塞)
# ============================================================
class MetricsHandler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/metrics":
# 只更新请求计数(轻量操作)
with _metrics_lock:
_metrics["http_requests_total"][("200",)] = \
_metrics["http_requests_total"].get(("200",), 0) + 1
response = generate_prometheus_metrics().encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "text/plain; charset=utf-8")
self.send_header("Content-Length", len(response))
self.end_headers()
self.wfile.write(response)
elif self.path == "/health":
self.send_response(200)
self.send_header("Content-Type", "application/json")
response = json.dumps({
"status": "ok",
"cache_age": time.time() - _cache_updated,
"timestamp": datetime.now(timezone.utc).isoformat()
}).encode()
self.send_header("Content-Length", len(response))
self.end_headers()
self.wfile.write(response)
else:
self.send_response(404)
self.end_headers()
def log_message(self, format, *args):
pass
# ============================================================
# 启动
# ============================================================
if __name__ == "__main__":
port = int(os.environ.get("EXPORTER_PORT", 9999))
# 初始化指标
collect_metrics_background()
# 启动后台线程:每 60 秒主动刷新
def refresh_loop():
while True:
time.sleep(60)
collect_metrics_background()
t = threading.Thread(target=refresh_loop, daemon=True)
t.start()
# 启动 HTTP 服务
server = http.server.HTTPServer(("0.0.0.0", port), MetricsHandler)
print(f"Agent Health Exporter v2.1 started on port {port}")
print(f" - Agents: {len(AGENTS)}")
print(f" - Refresh interval: 60s")
server.serve_forever()
-179
View File
@@ -1,179 +0,0 @@
#!/usr/bin/env python3
"""
Alertmanager → Feishu Webhook Bridge v2
将 Prometheus Alertmanager 告警转发到飞书消息
运行在宿主机(非容器内),以便使用 openclaw CLI 发送飞书消息。
路由规则:
- severity=critical → 通知 Vincent(飞书 ou_8782990ad09c2bd7732a5ef6b23b8508
- severity=warning → 通知 COO(飞书 ou_9f73b4e54af59f038e2b754793ea0908
"""
import http.server
import json
import os
import subprocess
import sys
import urllib.request
from datetime import datetime, timezone
# 飞书 Webhook URL(通过环境变量配置,可选)
FEISHU_WEBHOOK_CRITICAL = os.environ.get("FEISHU_WEBHOOK_CRITICAL", "")
FEISHU_WEBHOOK_WARNING = os.environ.get("FEISHU_WEBHOOK_WARNING", "")
# 接收人 Open ID
VINCENT_OPEN_ID = "ou_8782990ad09c2bd7732a5ef6b23b8508"
COO_OPEN_ID = "ou_9f73b4e54af59f038e2b754793ea0908"
# Grafana 面板 URL
GRAFANA_URL = "http://192.168.1.99:3001/d/agent-health"
def send_feishu_message_via_openclaw(open_id, title, content_block, severity):
"""通过 OpenClaw 飞书通道发送消息"""
card = build_feishu_card(title, content_block, severity)
payload = json.dumps({
"receive_id": open_id,
"msg_type": "interactive",
"content": json.dumps(card),
})
try:
result = subprocess.run(
["openclaw", "message", "send",
"--channel", "feishu",
"--target", open_id,
"--message", payload],
capture_output=True, text=True, timeout=10
)
if result.returncode == 0:
print(f"[bridge] Feishu sent to {open_id[:20]}...")
else:
print(f"[bridge] Feishu error: {result.stderr[:200]}", file=sys.stderr)
except Exception as e:
print(f"[bridge] Feishu exception: {e}", file=sys.stderr)
def send_feishu_webhook(webhook_url, title, content_block, severity):
"""通过飞书 Webhook URL 发送"""
if not webhook_url:
return
card = build_feishu_card(title, content_block, severity)
payload = json.dumps({"msg_type": "interactive", "content": json.dumps(card)}).encode("utf-8")
try:
req = urllib.request.Request(
webhook_url,
data=payload,
headers={"Content-Type": "application/json"},
method="POST"
)
with urllib.request.urlopen(req, timeout=10) as resp:
print(f"[bridge] Webhook sent: {resp.status}")
except Exception as e:
print(f"[bridge] Webhook error: {e}", file=sys.stderr)
def build_feishu_card(title, content, severity):
"""构建飞书消息卡片"""
color_map = {
"critical": "red",
"warning": "yellow",
"info": "blue",
}
color = color_map.get(severity, "blue")
return {
"config": {"wide_screen_mode": True},
"header": {
"title": {"tag": "plain_text", "content": f"🚨 {title}"},
"template": color,
},
"elements": [
{"tag": "markdown", "content": content},
{
"tag": "note",
"elements": [
{"tag": "plain_text", "content": f"BIZ-28 监控告警 | {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')}"}
]
}
]
}
def handle_alert(alert_data):
"""处理告警并发通知"""
alerts = alert_data.get("alerts", [])
for alert in alerts:
labels = alert.get("labels", {})
annotations = alert.get("annotations", {})
status = alert.get("status", "firing")
severity = labels.get("severity", "warning")
alertname = labels.get("alertname", "Unknown")
summary = annotations.get("summary", alertname)
description = annotations.get("description", "")
title = f"[{severity.upper()}] {summary}"
content = (
f"**告警名称**: {alertname}\n"
f"**状态**: {'🔥 触发中' if status == 'firing' else '✅ 已恢复'}\n"
f"**严重级别**: {severity}\n"
f"**详情**: {description}\n\n"
f"**监控面板**: {GRAFANA_URL}\n"
f"**告警时间**: {alert.get('startsAt', '')}"
)
if severity == "critical":
# 严重告警 → 通知 Vincent
if FEISHU_WEBHOOK_CRITICAL:
send_feishu_webhook(FEISHU_WEBHOOK_CRITICAL, title, content, severity)
send_feishu_message_via_openclaw(VINCENT_OPEN_ID, title, content, severity)
elif severity == "warning":
# 警告告警 → 通知 COO
if FEISHU_WEBHOOK_WARNING:
send_feishu_webhook(FEISHU_WEBHOOK_WARNING, title, content, severity)
send_feishu_message_via_openclaw(COO_OPEN_ID, title, content, severity)
class WebhookHandler(http.server.BaseHTTPRequestHandler):
def do_POST(self):
content_length = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(content_length)
try:
alert_data = json.loads(body)
handle_alert(alert_data)
self.send_response(200)
self.send_header("Content-Type", "application/json")
response = json.dumps({"status": "ok"}).encode()
self.send_header("Content-Length", len(response))
self.end_headers()
self.wfile.write(response)
except Exception as e:
print(f"[bridge] Handler error: {e}", file=sys.stderr)
self.send_response(500)
self.end_headers()
def do_GET(self):
if self.path == "/health":
self.send_response(200)
self.send_header("Content-Type", "application/json")
response = json.dumps({"status": "ok"}).encode()
self.send_header("Content-Length", len(response))
self.end_headers()
self.wfile.write(response)
else:
self.send_response(404)
self.end_headers()
def log_message(self, format, *args):
pass
if __name__ == "__main__":
port = int(os.environ.get("WEBHOOK_PORT", 9094))
server = http.server.HTTPServer(("0.0.0.0", port), WebhookHandler)
print(f"[bridge] Alert Webhook Bridge started on port {port}")
server.serve_forever()
@@ -1,210 +0,0 @@
# BIZ-25 定时心跳检查 cron 任务部署方案
> **版本:** v1.0
> **编制:** 严维序(opengineer
> **日期:** 2026-06-24
> **状态:** 已部署
> **父方案:** [BIZ-13 运行稳定性保障方案](./BIZ-13_运行稳定性保障方案.md)
---
## 一、概述
本方案是 BIZ-13 Phase1 的执行层方案,负责将 HEARTBEAT.md 模板+共享脚本部署为可运行的定时心跳检查机制。
### 部署架构
```
┌─────────────────────────────────────────────────────┐
│ OpenClaw Gateway Cron │
│ ┌────────────┐ ┌────────────┐ ┌──────────────┐ │
│ │ Agent A │ │ Agent B │ │ Agent C │ │
│ │ 心跳(10/15m)│ │ 心跳(15m) │ │ 心跳(15m) │ │
│ └─────┬──────┘ └─────┬──────┘ └──────┬───────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────────────────────────────────┐ │
│ │ shared/scripts/heartbeat_helper.py │ │
│ │ + multica_proxy.py │ │
│ │ + rate_limiter.py │ │
│ └──────────────────────────────────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────────────────────────────────┐ │
│ │ 三源任务检查: WorkBoard + Multica + 文档 │ │
│ └──────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
```
---
## 二、Agent 心跳频率分类
根据 BIZ-13 方案定义:
| 分类 | 频率 | Agent | 数量 |
|------|------|-------|------|
| **高频** | **10 分钟** | 陆怀瑾 (coo), 刘诗妮 (secretary) | 2 |
| **常规** | **15 分钟** | 严维序 (opengineer), 沈路明 (productmanager), 胡蓉 (projectmanager), 梁思筑 (architect), 苏锦绘 (designer), 徐聪 (costcodev), 文墨言 (contentspecialist), 程伯予 (cvexpert), 许言 (prompt-engineer), 钟帧韵 (mediaspecialist), 陆云帆 (taobaospecialist), 顾析策 (marketanalysis), 苏慎 (lawyer) | 13 |
---
## 三、部署清单
### 3.1 ✅ 已完成 — HEARTBEAT.md 模板
所有 15 个 Agent 的工作区均已部署 HEARTBEAT.md
| 工作区 | 频率 | 核心内容 |
|--------|------|----------|
| `coo/` | 10 min | BIZ-38 模板 + 全局积压巡检 |
| `secretary/` | 10 min | BIZ-38 模板 |
| `opengineer/` | 10 min | BIZ-38 模板 + 三源检查 |
| `projectmanager/` | 10 min | BIZ-38 模板 |
| `costcodev/` | 10 min | BIZ-38 模板 |
| 其余 10 个 Agent | 15 min | 标准模板 + 三源检查 |
### 3.2 ✅ 已完成 — 共享心跳脚本
路径:`shared/scripts/`
| 文件 | 用途 | 状态 |
|------|------|------|
| `rate_limiter.py` | 缓存管理 + 请求调度 + 协调轮询 | ✅ 已部署 |
| `multica_proxy.py` | Multica CLI 代理 + 缓存封装 | ✅ 已部署 |
| `heartbeat_helper.py` | 三源任务检查 + 超时检测 + 心跳入口 | ✅ 已部署 |
### 3.3 ⬜ 本次部署 — OpenClaw Cron 任务
使用 OpenClaw Gateway cron 系统创建定时任务,通过 `agentTurn` 隔离会话实现各 Agent 的周期性心跳触发。
#### Cron Job 规格
```yaml
每个 Agent:
schedule:
kind: cron
expr: "*/10 * * * *" # 高频 Agent
# expr: "*/15 * * * *" # 常规 Agent
tz: "Asia/Shanghai"
sessionTarget: "isolated"
payload:
kind: "agentTurn"
message: "运行心跳检查。执行你的 HEARTBEAT.md 中的三源任务检查。"
```
---
## 四、部署执行记录
### 执行时间:2026-06-24 00:14 CST
#### 创建的 Cron Job 清单
| Agent | 频率 | Cron Session | 状态 |
|-------|------|-------------|------|
| coo (陆怀瑾) | 10 min | isolated agentTurn | ✅ |
| secretary (刘诗妮) | 10 min | isolated agentTurn | ✅ |
| opengineer (严维序) | 10 min | isolated agentTurn | ✅ |
| projectmanager (胡蓉) | 10 min | isolated agentTurn | ✅ |
| costcodev (徐聪) | 10 min | isolated agentTurn | ✅ |
| productmanager (沈路明) | 15 min | isolated agentTurn | ✅ |
| architect (梁思筑) | 15 min | isolated agentTurn | ✅ |
| designer (苏锦绘) | 15 min | isolated agentTurn | ✅ |
| contentspecialist (文墨言) | 15 min | isolated agentTurn | ✅ |
| cvexpert (程伯予) | 15 min | isolated agentTurn | ✅ |
| prompt-engineer (许言) | 15 min | isolated agentTurn | ✅ |
| mediaspecialist (钟帧韵) | 15 min | isolated agentTurn | ✅ |
| taobaospecialist (陆云帆) | 15 min | isolated agentTurn | ✅ |
| marketanalysis (顾析策) | 15 min | isolated agentTurn | ✅ |
| lawyer (苏慎) | 15 min | isolated agentTurn | ✅ |
---
## 五、心跳检查内容
每次心跳触发后,Agent 在隔离会话中执行以下检查:
### 5.1 三源任务检查
```mermaid
flowchart TD
A[心跳触发] --> B[检查 WorkBoard 待办卡片]
A --> C[检查 Multica 待办 Issues]
A --> D[检查本地待办文档]
B --> E{有待办?}
C --> E
D --> E
E -->|有| F[自动执行任务]
E -->|无| G[结束心跳]
F --> H[任务完成?]
H -->|是| I[更新状态]
H -->|否| J[通知 COO]
```
### 5.2 超时检测
- 进行中任务超过 20 分钟无进展 → 标记"疑似超时"
- 确认超时 → 自动恢复流程
### 5.3 依赖检查
- 认领任务前检查 `depends_on`
- 依赖未满足 → 保持 todo,不认领
### 5.4 轮次控制
- 单任务最大 50 轮
- 接近 80%40 轮)→ 预警
- 达到上限 → 暂停,通知 COO
---
## 六、风险与规避
| 风险 | 影响 | 应对 |
|------|------|------|
| 心跳任务自身卡死 | 监控失效 | rate_limiter.py 缓存 + 超时保护 |
| 新增 Agent 未配心跳 | 遗漏 | 本方案作为部署 SOP 参考 |
| 会话隔离导致上下文丢失 | 心跳重复 | 心跳仅做检查,不承担复杂任务 |
| Agent 不在线 | 心跳无响应 | 系统事件 fallbackCOO 巡检兜底 |
---
## 七、验证方法
```bash
# 检查 cron job 列表
openclaw cron list
# 手动触发一次心跳 for a specific agent
openclaw cron run <job-id>
# 检查心跳脚本健康状态
python3 shared/scripts/heartbeat_helper.py <agent_id> --health
```
---
## 八、修复记录
### v1.1 — 2026-06-24
| 问题 | 修复 |
|------|------|
| cron delivery 报 Feishu 投递错误 | delivery 从 `announce` 改为 `none`(原方案未指定 delivery,不影响功能) |
| Multica workspace_id 未传递 | `multica_proxy.py` 新增 `_inject_workspace_id()`,自动在所有 multica CLI 调用注入 `--workspace-id` |
| AGENT_CONFIGS 仅 5 个 Agent | `heartbeat_helper.py` 扩展至全部 15 个 Agent |
| COO HEARTBEAT 显示未部署 | 更新 BIZ-38 集成清单表 |
## 九、后续优化方向
- [ ] 监控面板集成(BIZ-28 Phase3
- [ ] 心跳结果聚合展示
- [ ] Agent 健康状态告警
- [ ] 自动 Agent 发现(新增 Agent 自动配置心跳)
---
> **运维记录**:严维序 2026-06-24
> 所有 15 个 Agent 的 HEARTBEAT.md 已部署,共享脚本已就位,cron 定时器已配置。
-62
View File
@@ -1,62 +0,0 @@
#!/bin/bash
# wiki-lint-check.sh — Wiki 知识库质量检查脚本
#
# 用途: 定期运行 wiki_lint 检查知识库质量,生成报告
# 用法: ./scripts/wiki-lint-check.sh [--report-dir <dir>]
#
# 建议通过 cron 定期执行,例如每日凌晨:
# 0 2 * * * cd /path/to/EnterpriseArchitect && ./scripts/wiki-lint-check.sh
set -euo pipefail
REPORT_DIR="${REPORT_DIR:-/tmp/wiki-lint-reports}"
TIMESTAMP=$(date '+%Y-%m-%d_%H%M%S')
REPORT_FILE="${REPORT_DIR}/wiki-lint-${TIMESTAMP}.md"
mkdir -p "$REPORT_DIR"
echo "=== Wiki Lint Check ==="
echo "时间: $(date '+%Y-%m-%d %H:%M:%S %Z')"
echo "报告路径: $REPORT_FILE"
echo ""
# 运行 wiki_lint(通过 OpenClaw CLI
# 注意: 此脚本需在 OpenClaw 环境中执行
LINT_RESULT=$(openclaw skill wiki-lint 2>&1) || true
# 生成报告
cat > "$REPORT_FILE" << EOF
# Wiki Lint 检查报告
**检查时间**: $(date '+%Y-%m-%d %H:%M:%S %Z')
**执行主机**: $(hostname)
**执行用户**: $(whoami)
---
## 检查结果
\`\`\`
${LINT_RESULT:-No output from wiki_lint}
\`\`\`
---
## 状态
EOF
if echo "$LINT_RESULT" | grep -qi "error\|fail\|issue"; then
echo "**状态**: ⚠️ 发现问题,需处理" >> "$REPORT_FILE"
echo ""
echo "⚠️ Wiki Lint 发现问题,请检查: $REPORT_FILE"
else
echo "**状态**: ✅ 无问题" >> "$REPORT_FILE"
echo ""
echo "✅ Wiki Lint 检查通过"
fi
echo "报告已生成: $REPORT_FILE"
# 清理 30 天以前的旧报告
find "$REPORT_DIR" -name "wiki-lint-*.md" -mtime +30 -delete 2>/dev/null || true
-3
View File
@@ -1,3 +0,0 @@
__pycache__/
*.egg-info/
.mypy_cache/
-63
View File
@@ -1,63 +0,0 @@
# 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
@@ -1,41 +0,0 @@
"""
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",
]
-216
View File
@@ -1,216 +0,0 @@
"""
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=6000.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}) 无效,回退到默认值 6000"
)
config.request_timeout = 6000.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
@@ -1,152 +0,0 @@
"""
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
@@ -1,272 +0,0 @@
"""
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)
-226
View File
@@ -1,226 +0,0 @@
"""
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
# ---- 入队 ----
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
@@ -1,48 +0,0 @@
[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
@@ -1,438 +0,0 @@
"""
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()
# 避退状态锁
self._retreat_lock: threading.Lock = threading.Lock()
# ---- 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()
-785
View File
@@ -1,785 +0,0 @@
"""
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.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(),
# 生产环境推荐 JSONRenderer,开发环境可用 ConsoleRenderer
structlog.dev.ConsoleRenderer(),
],
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: 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,
}
# ---------------------------------------------------------------------------
# 工具函数
# ---------------------------------------------------------------------------
def _extract_model(body: dict[str, 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)
_stats["ratelimited_requests"] += 1
_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,
)
_stats["ratelimited_requests"] += 1
_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))
_stats["upstream_errors"] += 1
_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。
"""
_stats["passthrough_requests"] += 1
_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:
_stats["ratelimited_requests"] += 1
_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:
# 非低优先级轮询等待
deadline = time.monotonic() + 30.0
while not got_token:
await asyncio.sleep(0.1)
got_token = await asyncio.to_thread(_token_bucket.consume, tokens=1)
if time.monotonic() > deadline:
_stats["ratelimited_requests"] += 1
_prometheus.record_request(priority.name, "ratelimited")
return JSONResponse(
status_code=429,
content={
"error": {
"message": "令牌不足(队列满 + passthrough),等待超时 30s",
"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),
)
_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())
# 挂载 webui 子路由
app.include_router(webui_router)
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 → 排队 + 令牌限流 + 转发
"""
_stats["total_requests"] += 1
# 解析请求
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:
_stats["passthrough_requests"] += 1
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 → 排队 + 限流 + 转发
_stats["nvidia_requests"] += 1
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:
_stats["queue_full_rejects"] += 1
return JSONResponse(
status_code=503,
content={
"error": {
"message": "队列已满,当前策略: reject",
"type": "QueueFullError",
}
},
)
except QueueFullPassthrough:
# 队列满 + PASSTHROUGH:绕过排队,尝试令牌桶后直接转发
_stats["passthrough_requests"] += 1
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()
@@ -1,260 +0,0 @@
<!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 mb = (snap.metrics_buffer || {});
chartQueue.data.datasets[0].data = [
Math.round(Math.random() * 5),
Math.round(Math.random() * 10),
Math.round(Math.random() * 15),
Math.round(Math.random() * 20)
];
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>
-200
View File
@@ -1,200 +0,0 @@
"""
NVIDIA Sidecar — WebUI 后端 API
提供仪表盘 SSE 实时推送 + 配置热重载 API。
"""
from __future__ import annotations
import asyncio
import json
import time
from pathlib import Path
from typing import Any, AsyncGenerator
import structlog
from fastapi import APIRouter, HTTPException, Request
from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse
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"
# ---------------------------------------------------------------------------
# 配置热重载模型
# ---------------------------------------------------------------------------
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]:
while True:
if await request.is_disconnected():
break
try:
snapshot: dict[str, Any] = _build_snapshot()
yield f"data: {json.dumps(snapshot, ensure_ascii=False)}\n\n"
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",
},
)
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
return {
"timestamp": now,
"uptime_seconds": uptime,
"token_bucket": bucket_status,
"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,
"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")
cfg.queue_max_size = body.queue_max_size
changed.append("queue_max_size")
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 _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)
@webui_router.get("/admin/config")
async def admin_get_config() -> JSONResponse:
"""获取当前配置。"""
return JSONResponse(content=await get_config())
@webui_router.post("/admin/config")
async def admin_update_config(body: ConfigPatch) -> JSONResponse:
"""在线修改配置(热重载)。"""
return await update_config(body)
# ---------------------------------------------------------------------------
# 仪表盘静态页面
# ---------------------------------------------------------------------------
@webui_router.get("/dashboard", include_in_schema=False)
async def dashboard_page() -> HTMLResponse:
"""仪表盘 HTML 页面。"""
dashboard_path = STATIC_DIR / "dashboard.html"
if dashboard_path.is_file():
return HTMLResponse(content=dashboard_path.read_text(encoding="utf-8"))
return HTMLResponse(content="<h1>dashboard.html not found</h1>", status_code=404)
-279
View File
@@ -1,279 +0,0 @@
# 多智能体文档存储、命名与索引规范 v1.0
> 版本:v1.0(实施版)
> 编制:陆怀瑾(COO
> 日期:2026-06-22
> 状态:已批准,执行中
> 适用范围:所有 Agent 的 workspace 目录
---
## 一、统一目录结构
每个 Agent 的 workspace 必须采用以下标准目录结构:
```
workspace/
├── AGENTS.md # Agent 协作协议
├── MEMORY.md # 长期记忆 → 含文档索引表(核心)
├── SOUL.md # 角色定义 → 引用外部内容,不填塞
├── IDENTITY.md # 身份信息
├── USER.md # 用户画像
├── TOOLS.md # 工具清单 → 仅保留索引,详情外挂
├── HEARTBEAT.md # 心跳配置
├── memory/ # 记忆归档目录(按日期)
│ └── YYYY-MM-DD.md
├── docs/ # 项目文档目录(按项目分)
│ └── {project}/
│ ├── README.md
│ └── ...
├── plans/ # 方案文档目录
│ └── YYYY-MM-DD_{topic}.md
├── specs/ # 规范/标准文档目录
│ └── BIZ-XX_{name}_v{M}.{N}.md
├── reports/ # 运营报告目录
│ └── YYYY-Q{N}_{type}_v{M}.{N}.md
├── knowledge/ # 知识库目录(按领域分)
│ └── {domain}/
│ └── {topic}.md
├── tasks/ # 任务文件目录(可选)
│ └── ...
└── assets/ # 资源文件目录
├── images/
├── files/
└── templates/
```
### 目录用途速查
| 目录 | 用途 | Token 影响 |
|------|------|-----------|
| 根目录 .md | Agent 核心配置 | **直接影响 Token**,必须精简 |
| memory/ | 按日归档记忆 | 通过 memory_search 检索,不占用上下文 |
| docs/ | 项目文档 | 按需加载 |
| plans/ | 方案文档 | 仅 COO 维护 |
| specs/ | 规范标准 | 按需加载 |
| reports/ | 运营报告 | 仅 COO 维护 |
| knowledge/ | 知识库 | 知识库检索,不占用上下文 |
| assets/ | 二进制资源 | 不占用上下文 |
---
## 二、文件命名规则
### 2.1 强制命名格式
```
{日期/编号}_{中文主题}_{版本}.md
```
### 2.2 各目录命名约定
| 目录 | 命名模式 | 示例 |
|------|----------|------|
| memory/ | `YYYY-MM-DD.md` | `2026-06-22.md` |
| plans/ | `YYYY-MM-DD_{主题}.md` | `2026-06-22_多智能体协作体系总体方案.md` |
| specs/ | `BIZ-{编号}_{主题}_v{M}.{N}.md` | `BIZ-12_文档存储规范_v1.0.md` |
| reports/ | `YYYY-Q{N}_{类型}_v{M}.{N}.md` | `2026-Q2_运营效率报告_v1.0.md` |
| knowledge/ | `{主题}_v{M}.{N}.md` | `淘宝运营_SOP_v1.0.md` |
| docs/{project}/ | `{功能}_{版本}.md` | `requirements_v1.0.md` |
| memory/ day file | `YYYY-MM-DD.md` | `2026-06-22.md` |
### 2.3 禁止事项
- ❌ 使用特殊字符:`/ \ : * ? " < > |` 空格
- ❌ 超过 80 字符的文件名
- ❌ 不含日期/编号的裸文件名
- ❌ 中文和英文混排无分隔符
- ✅ 统一使用下划线 `_` 作为分隔符
---
## 三、索引机制(核心)
### 3.1 索引分离原则(刘总反馈已纳入)
> **配置文件只保留索引指针,详细内容外挂存储。**
此原则适用场景:
- **TOOLS.md**:只列工具名称 + 引用路径,不列完整参数
- **待办列表**:只记录 ID + 主题 + 状态,详情在独立文件中
- **Agent 协作表**:只列 Agent 名 + 职能 + Session Key,详情在各自文件
- **知识索引**:MEMORY.md 只保留索引表,知识条目在 knowledge/ 中
### 3.2 MEMORY.md 索引表模板
```markdown
# MEMORY.md - {Agent Name} 长期记忆
## 📑 文档索引
### 方案文档
| 日期 | 主题 | 路径 | 状态 |
|------|------|------|------|
| 2026-06-22 | 多智能体协作体系 | plans/2026-06-22_多智能体协作体系总体方案.md | 已批准 |
### 规范标准
| 编号 | 主题 | 路径 | 版本 |
|------|------|------|------|
| BIZ-12 | 文档存储规范 | specs/BIZ-12_文档存储规范_v1.0.md | v1.0 |
### 项目文档
| 项目 | 文档 | 路径 | 状态 |
|------|------|------|------|
### 运营报告
| 周期 | 类型 | 路径 | 状态 |
|------|------|------|------|
### 知识库条目
| 领域 | 主题 | 路径 | 更新时间 |
|------|------|------|----------|
---
(以下是实际记忆内容...
```
### 3.3 各目录 README.md 模板
每个目录应有一个 `README.md`
```markdown
# {目录名称}
> 最后更新:{YYYY-MM-DD}
> 维护者:{Agent Name}
## 目录说明
{简短描述本目录的用途和使用规范}
## 文件列表
| 文件名 | 描述 | 最后更新 |
|--------|------|----------|
| ... | ... | ... |
```
---
## 四、检索体系
### 4.1 分层检索路径
```
第一层:memory_search(语义检索,跨 memory/*.md + MEMORY.md
↓ 未命中
第二层:wiki_search / wiki_get(编译型知识库检索)
↓ 未命中
第三层:qmd(QMD 全文检索,已安装 —— 刘总反馈已纳入)
↓ 未命中
第四层:web_fetch / web_search(外部知识)
```
### 4.2 检索优先级
1. **memory_search**corpus=all):首选,零 token 消耗
2. **qmd**:本地全文检索,补充 memory_search 未覆盖的长文档
3. **wiki_search/wiki_get**:编译型结构化知识库
4. **web_search/web_fetch**:外部补充,仅在以上均未命中时使用
---
## 五、Token 预算控制
### 5.1 配置文件大小限制
| 文件 | 最大行数 | 说明 |
|------|----------|------|
| AGENTS.md | 200 行 | Agent 协议 + 协作表(精简) |
| MEMORY.md | 150 行 | 长期记忆 + 索引表 |
| SOUL.md | 80 行 | 角色定义 |
| IDENTITY.md | 30 行 | 身份信息 |
| USER.md | 30 行 | 用户画像 |
| TOOLS.md | 100 行 | 工具索引(不填塞完整参数) |
| HEARTBEAT.md | 60 行 | 心跳配置 |
### 5.2 引用代替填塞
**反例(填塞模式)**
```markdown
# TOOLS.md - 全部填入
- memory_search: 参数 query, maxResults, minScore, corpus=[memory|wiki|all|sessions]...
(占用大量 token
```
**正例(引用模式)**
```markdown
# TOOLS.md - 索引模式
## 已安装 Skills
- plantuml-skill → 详见 skills/plantuml-skill/SKILL.md
- qmd → 详见 skills/qmd/SKILL.md
- ...
## 核心工具(已内置于运行时,无需列出参数)
- memory_search / wiki_search / web_fetch
```
---
## 六、文档生命周期管理
### 6.1 状态流转
```
创建 → 草稿(draft) → 审阅中(in_review) → 已批准(approved) → 归档(archived)
废弃(deprecated)
```
### 6.2 操作规范
| 操作 | 规则 |
|------|------|
| 创建 | 在正确目录,按命名规则创建 |
| 更新 | 小改动直接覆盖;大改动新建版本 |
| 审阅 | 状态标记 `in_review`,通知审阅人 |
| 归档 | 移动到 `archive/` 子目录 |
| 删除 | 不直接删除,先归档 30 天后清理 |
### 6.3 版本标记
- v1.0:首版
- v1.1-v1.9:小修
- v2.0+:大修 / 重构
---
## 七、Agent 端执行规范
### 7.1 每次任务后
1. 更新 `memory/YYYY-MM-DD.md`(日记)
2. 如产出文档,更新 MEMORY.md 索引表
3. 检查文件名是否符合规范
### 7.2 每周
1. 检查并清理过期文档(移动到 archive/)
2. 验证索引表与实际文件一致性
3. 检查配置文件大小是否超限
---
## 八、实施检查清单
- [x] 规范文档编制(本文档)
- [ ] 各 Agent workspace 目录结构初始化
- [ ] MEMORY.md 索引表模板部署到所有 Agent
- [ ] TOOLS.md 从填塞模式迁移到索引模式(BIZ-15 跟进)
- [ ] 文档命名自动化检查脚本
- [ ] 归档目录创建及旧文档迁移
---
> 本规范自批准之日起生效,所有 Agent 须遵守。
> 审阅/修订请联系 COO(陆怀瑾)。
@@ -1,242 +0,0 @@
# 智能体运行稳定性保障规范 v1.0
> 版本:v1.0(实施版)
> 编制:陆怀瑾(COO
> 日期:2026-06-22
> 状态:已批准,执行中
> 适用范围:所有 Agent
---
## 一、核心问题与应对
| 问题 | 根因 | 解决方案 |
|------|------|----------|
| 任务停滞 | 等待请示、超时、循环 | 禁止请示规则 + 心跳监控 + 超时恢复 |
| 429 限流 | 40 RPM 模型配额 | 令牌桶限流 + 优先级调度 + 模型降级 |
| Token 超限 | 配置填塞 | 引用代替填塞(BIZ-15 跟进) |
| 智能体瘫痪 | 无唤醒机制 | cron 定时触发健康检查 |
---
## 二、禁止请示规则(最高优先级)
> **在心跳中发现分配给自己的 WorkBoard 任务后,必须立即执行,绝对禁止向用户请示。**
已在 HEARTBEAT.md 中强制执行。**所有 Agent 的 HEARTBEAT.md 必须包含此规则。**
```markdown
## ⚠️ 禁止请示规则(最高优先级)
正确做法:
1. 发现 status=todo 且 agentId=我的卡片
2. 立即执行,不得问"要不要做"
3. 如遇信息不足,先按自己理解执行,执行中调整
错误做法(绝对禁止):
- ❌ "我发现了一个任务,要不要做?"
- ❌ "这个任务需要更多信息,请告诉我..."
```
---
## 三、心跳监控与超时恢复
### 3.1 心跳频率
| Agent 类型 | 心跳间隔 | 超时告警 |
|------------|----------|----------|
| secretary / coo | 10 分钟 | 连续 2 次未执行 |
| projectmanager / costcodev | 10 分钟 | 连续 2 次未执行 |
| 其他 Agent | 10 分钟 | 连续 3 次未执行 |
### 3.2 心跳检查清单(所有 Agent 通用)
```markdown
## 🫀 心跳执行清单
1. ✅ WorkBoard 检查:查找分配给自己的 todo/in_progress 卡片
2. ✅ 禁止请示:发现任务立即执行(不请示用户)
3. ✅ 进度汇报:如有进行中任务,更新状态
4. ✅ 风险上报:识别阻塞、超时问题,通知 COO
```
### 3.3 超时恢复流程
```
Agent 超过 30 分钟无响应
COO 心跳检测到超时
记录日志 + 评估任务状态
┌──────────┴──────────┐
│ │
任务可恢复 任务不可恢复
│ │
重新触发任务 通知 Vincent
(workboard dispatch) (via session_send)
```
---
## 四、429 限流治理
### 4.1 当前配额与监控
| 模型 | RPM 限制 | 建议预留 |
|------|----------|----------|
| 主模型 | 40 | 保留 10 RPM 给紧急任务 |
| 备用模型 | 40 | 满 35 RPM 时切换 |
### 4.2 令牌桶限流策略
```
每个 Agent 独立的令牌桶:
- 容量:按 Agent 优先级分配
- COO/secretary: 8 RPM
- 开发 Agent: 6 RPM
- 业务 Agent: 4 RPM
- 预留池: 10 RPM (紧急任务)
令牌桶耗尽 → 自动降级到备用模型或排队
```
### 4.3 优先级调度
| 优先级 | 适用场景 | 处理方式 |
|--------|----------|----------|
| P1 紧急 | Vincent 直接指令 | 立即可用预留池 |
| P2 高 | 阻塞性任务、风险告警 | 优先分配令牌 |
| P3 正常 | 日常任务 | 正常排队 |
| P4 低 | 后台优化、报告生成 | 低峰期执行 |
### 4.4 模型降级链
```
主模型 (qwen3.5-397b) RPM 不足
备用模型 (deepseek-v4-pro)
等待 + 指数退避重试 (1s → 2s → 4s → 8s)
3 次重试后仍失败 → 记录日志,通知 COO
```
### 4.5 请求合并优化
| 优化项 | 当前做法 | 优化后 |
|--------|----------|--------|
| WorkBoard 轮询 | 每个 Agent 独立轮询 | COO 统一轮询,广播结果 |
| 重复检索 | 多个 Agent 重复查同一文档 | 缓存关键查询结果(5 分钟 TTL) |
| 连续调用 | 无间隔连续调用 API | 最小间隔 500ms |
---
## 五、唤醒机制
### 5.1 Cron 定时唤醒
```yaml
# COO 健康检查唤醒
cron:
schedule: "*/5 * * * *" # 每 5 分钟
action: health_check
targets:
- 检查所有 Agent 最后活跃时间
- 超过 15 分钟无活动 → 触发唤醒消息
- 超过 30 分钟无活动 → 通知 Vincent
```
### 5.2 唤醒消息模板
```markdown
## 🔔 唤醒检查
距上次活跃时间:{elapsed} 分钟
当前任务状态:{status}
是否存在阻塞:{blocked}
系统自动唤醒,请确认状态。
```
### 5.3 自唤醒规则
每个 Agent 在 HEARTBEAT.md 中配置:
```
如果距上次心跳超过 2 个周期(20 分钟):
→ 自动重新评估任务状态
→ 如有待办,立即执行
→ 如无待办,确认存活
```
---
## 六、上下文/Token 溢出防护
### 6.1 配置文件大小限制
| 文件 | 最大行数 | 超标处理 |
|------|----------|----------|
| AGENTS.md | 200 | 移到 docs/agent-roster.md |
| SOUL.md | 80 | 提取模块化引用 |
| TOOLS.md | 100 | 索引化(不填塞参数) |
| HEARTBEAT.md | 60 | 精简检查清单 |
| MEMORY.md | 150 | 定期归档旧条目 |
### 6.2 运行时监控
```
Token 使用量达到 80%
自动清理上下文
仍超 90%
重启会话
```
---
## 七、监控告警矩阵
| 指标 | 警告阈值 | 严重阈值 | 通知对象 |
|------|----------|----------|----------|
| Agent 无响应 | > 15 min | > 30 min | 警告 → COO,严重 → Vincent |
| 429 错误率 | > 5% | > 20% | COO |
| Token 使用量 | > 80% | > 95% | 该 Agent |
| 任务积压 | > 5 pending | > 10 pending | COO |
| 任务超时 | > 24h in_progress | > 48h | 警告 → Agent,严重 → Vincent |
---
## 八、实施步骤
### 阶段 1:即刻生效(今日)
- [x] 禁止请示规则 → 已在各 Agent HEARTBEAT.md 中落实
- [ ] 心跳频率统一为 10 分钟
- [ ] COO 端健康检查 cron 配置
### 阶段 2:本周完成
- [ ] 令牌桶限流配置(按 Agent 分配 RPM)
- [ ] 模型降级链配置
- [ ] 告警规则上线
### 阶段 3:持续优化
- [ ] 监控面板搭建
- [ ] 自动重启恢复
- [ ] 请求合并优化
---
## 九、交付物清单
- [x] 运行稳定性保障规范(本文档)
- [ ] HEARTBEAT.md 模板更新(含禁止请示 + 自唤醒规则)
- [ ] COO 端 cron 健康检查任务
- [ ] 令牌桶限流配置
- [ ] 告警规则配置
---
> 本规范自批准之日起生效。执行中如遇问题,联系 COO(陆怀瑾)。
@@ -1,261 +0,0 @@
# 智能体知识库体系建设规范 v1.0
> 版本:v1.0(实施版)
> 编制:陆怀瑾(COO
> 日期:2026-06-22
> 状态:已批准,执行中
> 适用范围:所有 Agent
---
## 一、核心目标
| 目标 | 实现方式 |
|------|----------|
| 知识与配置解耦 | 知识库独立于 Agent 配置文件,不计入 Token |
| Agent 可主动查询 | 通过多层检索体系按需获取知识 |
| 人类可审查优化 | Web UI / 飞书文档支持人工审阅 |
| 零 Token 增长 | 知识条目独立存储,仅在使用时加载 |
---
## 二、分层检索体系(刘总反馈已纳入)
### 2.1 检索优先级
```
Agent 需要知识
第一层: memory_search (corpus=all)
→ 搜索 memory/*.md + MEMORY.md + wiki 条目
→ 零 Token 消耗,语义检索
↓ 未命中
第二层: wiki_search / wiki_get
→ 编译型结构化知识库
→ 支持精确检索和页面读取
↓ 未命中
第三层: qmd (QMD 全文检索,已安装 ← 刘总反馈)
→ 本地全文检索 markdown 知识库
→ 补充 memory_search 未覆盖的长文档
↓ 未命中
第四层: web_search / web_fetch
→ 外部互联网补充
→ 仅在内部均未命中时使用
```
### 2.2 工具速查
| 工具 | 适用场景 | Token 消耗 |
|------|----------|-----------|
| memory_search | 通用语义检索(跨 memory + wiki | 0 |
| wiki_search / wiki_get | 结构化知识库精确查询 | 0 |
| qmd | 本地全文检索长文档 | 0 |
| web_search | 外部互联网信息 | 0 |
| web_fetch | 网页/文档详情获取 | 按内容量 |
---
## 三、知识库目录结构
```
knowledge/
├── 电商/ # 电商运营知识
│ └── {主题}_v{M}.{N}.md
├── 内容/ # 内容运营知识
│ └── {主题}_v{M}.{N}.md
├── 产品/ # 产品管理知识
│ └── {主题}_v{M}.{N}.md
├── 技术/ # 技术开发知识
│ └── {主题}_v{M}.{N}.md
├── 设计/ # UI/UX 设计知识
│ └── {主题}_v{M}.{N}.md
├── 运营/ # 通用运营知识
│ └── {主题}_v{M}.{N}.md
└── 规范/ # 流程规范知识
└── {主题}_v{M}.{N}.md
```
### 知识条目模板
```markdown
# {知识标题}
> 领域: {所属领域} | 版本: v{M}.{N}
> 维护者: {责任人} | 最后更新: {YYYY-MM-DD}
## 概述
{知识的用途和价值,1-2 句话}
## 适用范围
{在什么场景下使用}
## 核心内容
{知识主体}
## 操作步骤 / SOP
1. ...
2. ...
## 质量检查
- [ ] ...
## 常见问题
**Q**: ... **A**: ...
## 相关条目
- knowledge/{领域}/{关联主题}.md
## 版本历史
| 版本 | 日期 | 变更 | 作者 |
|------|------|------|------|
| v1.0 | 2026-06-22 | 初稿 | 陆怀瑾 |
```
---
## 四、与 Memory 系统的分工
| 维度 | Memory 系统 | Knowledge 系统 |
|------|------------|---------------|
| 内容类型 | 决策记录、经验教训、个性化记忆 | SOP、模板、规范、最佳实践 |
| 所有者 | 单个 Agent 专属 | 跨 Agent 共享 |
| 更新频率 | 每日/每周 | 按需/按版本 |
| 查询方式 | memory_search(语义检索) | wiki_search/wiki_get/qmd |
| 存储位置 | MEMORY.md + memory/*.md | knowledge/ 目录 |
---
## 五、Agent 查询指南
### 5.1 何时查询知识库
| 场景 | 查询示例 |
|------|----------|
| 执行 SOP 任务 | "淘宝 活动报名 SOP" |
| 撰写文档 | "PRD 模板" |
| 遇到问题 | "部署 故障排查" |
| 制定规范 | "开发规范" |
| 不熟悉领域 | "小红书 运营指南" |
### 5.2 查询标准流程
```
1. 先用 memory_search(corpus=all, query="...") 搜索
2. 如有结果,用 memory_get 或 wiki_get 读取详情
3. 如无结果,用 qmd 全文检索 knowledge/ 目录
4. 仍无结果,记录知识缺口,通知 COO
5. 使用获取的知识指导工作
```
### 5.3 知识缺口上报
```
Agent 发现知识缺口
在 memory/YYYY-MM-DD.md 中记录:
- 查询内容
- 使用场景
- 建议优先级
通知 COO 创建知识条目
```
---
## 六、人类审查机制
### 6.1 审查方式
| 方式 | 适用场景 | 工具 |
|------|----------|------|
| Obsidian Web UI | 日常浏览、编辑 | wiki_status 确认可用性 |
| 飞书文档同步 | 多人协作、审批 | 飞书 Wiki API |
| CLI 直接编辑 | 技术人员修改 | write/edit 工具 |
### 6.2 审核流程
```
Agent 发现缺口 → 记录 → 通知 COO
COO 评估优先级
高优先级 → 立即创建/指派
低优先级 → 记入 backlog
创建草稿 → wiki_apply(op="create_synthesis")
人类审查 → 通过/修改/拒绝
发布 → 通知相关 Agent
```
### 6.3 定期质量检查
```bash
# 每周运行一次
wiki_lint # 检查链接断裂、矛盾信息、过时内容
```
---
## 七、知识条目管理
### 7.1 创建
```
wiki_apply(op="create_synthesis", title="...", body="...", sourceIds=[])
```
### 7.2 更新
```
wiki_apply(op="synthesis", lookup="...", body="...")
```
### 7.3 版本管理
| 变更类型 | 版本变化 | 操作 |
|----------|----------|------|
| 内容微调 | v1.0 → v1.1 | 直接覆盖,更新版本历史 |
| 结构性变更 | v1.x → v2.0 | 保留旧版本,新建条目 |
| 废弃 | 添加 [deprecated] 标记 | 归档到 archive/ |
---
## 八、初始知识基础
以下条目作为知识库初始基础,需尽快创建:
| 领域 | 条目 | 优先级 | 负责 Agent |
|------|------|--------|-----------|
| 电商 | 淘宝运营 SOP | 高 | 陆云帆 |
| 电商 | 客服话术模板 | 中 | 陆云帆 |
| 内容 | 小红书运营指南 | 高 | 文墨言 |
| 内容 | 标题写作技巧 | 中 | 文墨言 |
| 产品 | PRD 模板 | 高 | 沈路明 |
| 技术 | 开发规范 | 高 | 梁思筑 |
| 技术 | 部署流程 | 中 | 严维序 |
| 设计 | UI 设计规范 | 中 | 苏锦绘 |
| 运营 | KPI 指标定义 | 中 | 陆怀瑾 |
| 规范 | 文档存储规范 | 已完成 | 陆怀瑾 |
---
## 九、交付物清单
- [x] 知识库体系建设规范(本文档)
- [ ] knowledge/ 目录结构创建
- [ ] 初始知识条目(至少 5 个优先)
- [ ] Agent 查询指南(已嵌入本文档)
- [ ] 知识审核流程(已嵌入本文档)
- [ ] wiki_lint 定期检查 cron 任务
---
> 本规范自批准之日起生效。知识条目创建请联系 COO 协调。
-31
View File
@@ -1,31 +0,0 @@
# [知识条目标题]
## 元数据
| 属性 | 值 |
|------|-----|
| **领域** | 电商 / 内容 / 产品 / 技术 / 设计 / 运营 / 行政 |
| **责任人** | [Agent 名称] |
| **版本** | v1.0 |
| **创建日期** | YYYY-MM-DD |
| **最后更新** | YYYY-MM-DD |
| **标签** | [标签1, 标签2, ...] |
## 概述
[用 2-3 句话描述本条目的核心内容和使用场景]
## 正文
[详细的知识内容,包括步骤、规则、示例等]
## 相关条目
- [相关知识条目1](链接)
- [相关知识条目2](链接)
## 变更记录
| 日期 | 版本 | 变更说明 | 变更人 |
|------|------|----------|--------|
| YYYY-MM-DD | v1.0 | 初始创建 | [姓名] |