Sfoglia il codice sorgente

fix(auto_put_ad_mini): 优化审批流程 - 过滤bid_up + 简化执行反馈

## 核心修改

### 1. 审批表过滤优化(im_approval.py)
- **问题**:bid_up 提价决策出现在审批表中,但运营希望只审批 pause 和 bid_down
- **根因**:过滤逻辑只作用于 tier0,tier1-3 的 bid_up 未被过滤
- **修复**:
  - 将 FEISHU_EXCLUDE_ACTIONS 过滤应用于所有 tier(不仅 tier0)
  - 添加 bid_up 到排除列表
  - 审批表现在只显示 pause 和 bid_down 决策

### 2. 审批通过后简化执行反馈(system.prompt)
- **问题**:审批通过后发送完整飞书表格(generate_report + import_to_feishu),造成信息过载
- **需求**:只需简单文字摘要,不要重复发表格
- **修复**:
  - Step 10 改为 send_feishu_text_message(仅文字摘要)
  - 移除 generate_report 和 import_to_feishu 自动调用
  - 新增"审批通过后的执行流程"指导规范

### 3. 重新审批流程强化(system.prompt)
- **问题**:部分批准型协议指示修改后直接执行,未等待明确"通过"
- **修复**:
  - 明确要求修改后**必须重新审批**
  - 等待明确的"同意"/"通过"才能执行
  - 添加详细的协商流程说明

## 测试验证

✅ **过滤测试**:
- 初始15个决策 → 过滤后审批表只含 pause(bid_up 已过滤)
- 过滤统计正确显示明细

✅ **审批协商**:
- 用户:"6个不关停" → 修改为9个 → 重新审批 → "通过" → 执行
- 修改后正确触发重新审批流程

✅ **简化反馈**:
- 执行后仅发送1106字符文字摘要
- 未调用 generate_report 和 import_to_feishu

## 影响范围

- 审批表内容:从"全部决策"变为"仅 pause + bid_down"
- 执行后反馈:从"完整Excel表格"变为"简单文字摘要"
- 修改后流程:强制重新审批,不允许直接执行

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
刘立冬 2 settimane fa
parent
commit
118ecf573e
34 ha cambiato i file con 3358 aggiunte e 52 eliminazioni
  1. 13 0
      agent/tools/builtin/feishu/chat_history/chat_liulidong.json
  2. 51 0
      examples/auto_put_ad_mini/.dockerignore
  3. 11 0
      examples/auto_put_ad_mini/.env.example
  4. 6 0
      examples/auto_put_ad_mini/.env.test
  5. 529 0
      examples/auto_put_ad_mini/DEPLOYMENT.md
  6. 422 0
      examples/auto_put_ad_mini/DOCKER_TEST.md
  7. 51 0
      examples/auto_put_ad_mini/Dockerfile
  8. 293 0
      examples/auto_put_ad_mini/SCHEDULER_GUIDE.md
  9. 55 17
      examples/auto_put_ad_mini/config.py
  10. 24 0
      examples/auto_put_ad_mini/db/__init__.py
  11. 282 0
      examples/auto_put_ad_mini/db/config.py
  12. 105 0
      examples/auto_put_ad_mini/db/connection.py
  13. 116 0
      examples/auto_put_ad_mini/db/schema.sql
  14. 32 0
      examples/auto_put_ad_mini/docker-compose.yml
  15. 107 0
      examples/auto_put_ad_mini/docker-test.sh
  16. 237 0
      examples/auto_put_ad_mini/k8s/README.md
  17. 28 0
      examples/auto_put_ad_mini/k8s/configmap.yaml
  18. 50 0
      examples/auto_put_ad_mini/k8s/cronjob.yaml
  19. 90 0
      examples/auto_put_ad_mini/k8s/deployment.yaml
  20. 7 0
      examples/auto_put_ad_mini/k8s/namespace.yaml
  21. 46 0
      examples/auto_put_ad_mini/k8s/network-policy.yaml
  22. 14 0
      examples/auto_put_ad_mini/k8s/pvc.yaml
  23. 34 0
      examples/auto_put_ad_mini/k8s/secret.yaml
  24. 90 0
      examples/auto_put_ad_mini/metrics.py
  25. 27 7
      examples/auto_put_ad_mini/prompts/system.prompt
  26. 39 0
      examples/auto_put_ad_mini/requirements.txt
  27. 64 0
      examples/auto_put_ad_mini/run_full_analysis.py
  28. 22 0
      examples/auto_put_ad_mini/schedule.sh
  29. 222 0
      examples/auto_put_ad_mini/server.py
  30. 155 0
      examples/auto_put_ad_mini/test_db_summary.md
  31. 47 0
      examples/auto_put_ad_mini/test_scheduler.sh
  32. 24 28
      examples/auto_put_ad_mini/tools/im_approval.py
  33. 57 0
      examples/auto_put_ad_mini/utils/log_capture.py
  34. 8 0
      examples/auto_put_ad_mini/whitelist.json

+ 13 - 0
agent/tools/builtin/feishu/chat_history/chat_liulidong.json

@@ -0,0 +1,13 @@
+[
+  {
+    "message_id": "manual_inject_20260422_210800",
+    "content": [
+      {
+        "type": "text",
+        "text": "拒绝"
+      }
+    ],
+    "sender": "liulidong",
+    "create_time": "1745334480"
+  }
+]

+ 51 - 0
examples/auto_put_ad_mini/.dockerignore

@@ -0,0 +1,51 @@
+# Python
+*.pyc
+__pycache__/
+.pytest_cache/
+.venv/
+venv/
+*.egg-info/
+
+# Git
+.git/
+.gitignore
+
+# IDE
+.vscode/
+.idea/
+*.swp
+*.swo
+
+# Logs
+*.log
+.trace/
+.cache/
+
+# Outputs
+outputs/*
+!outputs/.gitkeep
+
+# Environment
+.env
+.env.local
+.env.*.local
+
+# Documentation (除了README)
+*.md
+!README.md
+
+# Node
+node_modules/
+
+# macOS
+.DS_Store
+
+# Project specific (从根目录视角)
+examples/*/outputs/*
+examples/*/.venv/
+examples/*/__pycache__/
+examples/*/.pytest_cache/
+examples/*/.env
+examples/*/.env.*
+examples/*/.trace/
+examples/*/.cache/

+ 11 - 0
examples/auto_put_ad_mini/.env.example

@@ -35,11 +35,21 @@ FEISHU_OPERATOR_CHAT_ID=oc_88e0a1970a7de02eb5ac225a8b0cedea
 # QWEN_API_KEY=xxx
 # OPEN_ROUTER_API_KEY=xxx
 
+# ========================================
+# 数据库配置(MySQL)
+# ========================================
+DB_HOST=localhost
+DB_PORT=3306
+DB_USER=ad_rw
+DB_PASSWORD=your_password
+DB_NAME=auto_put_ad_mini
+
 # ========================================
 # 生产环境配置(海外部署)
 # ========================================
 
 # 白名单账户(逗号分隔)
+# 注意:优先从数据库读取,数据库不可用时降级到环境变量
 WHITELIST_ENABLED=true
 WHITELIST_ACCOUNTS=80769799,71305011
 
@@ -51,6 +61,7 @@ WHITELIST_ACCOUNTS=80769799,71305011
 TZ=UTC
 
 # 执行开关(生产环境谨慎开启)
+# 注意:优先从数据库读取,数据库不可用时降级到环境变量
 EXECUTION_ENABLED=false
 
 # API 端点(可选覆盖)

+ 6 - 0
examples/auto_put_ad_mini/.env.test

@@ -0,0 +1,6 @@
+# 数据库配置(阿里云 RDS MySQL)
+DB_HOST=rm-t4nh1xx6o2a6vj8qu3o.mysql.singapore.rds.aliyuncs.com
+DB_PORT=3306
+DB_USER=ad_rw
+DB_PASSWORD=p82SzuW4kAP3LJXcQGso
+DB_NAME=tencent_ad_autoput

+ 529 - 0
examples/auto_put_ad_mini/DEPLOYMENT.md

@@ -0,0 +1,529 @@
+# auto_put_ad_mini 生产部署指南
+
+## 概述
+
+本文档说明如何将 `auto_put_ad_mini` 部署到海外服务器的 Docker/Kubernetes 环境,实现生产级自动化运行。
+
+## 核心特性
+
+### 已实现功能
+
+✅ **Docker 容器化**
+- Dockerfile 基于 Python 3.10 slim
+- 多阶段构建,优化镜像大小
+- 健康检查支持
+
+✅ **账户白名单机制**
+- 环境变量配置白名单账户
+- 双重安全检查(决策阶段 + 执行阶段)
+- 支持 whitelist.json 配置文件
+
+✅ **海外环境适配**
+- 代理配置从环境变量读取
+- 时区支持(UTC/Asia/Shanghai)
+- 移除硬编码本地代理
+
+✅ **定时调度**
+- FastAPI + APScheduler 常驻服务(推荐)
+- Kubernetes CronJob 支持(备用)
+- HTTP API 手动触发
+
+✅ **监控和日志**
+- 健康检查端点 `/health`
+- 请求日志中间件
+- Prometheus metrics 导出(可选)
+- 日志输出到 stdout(Kubernetes 友好)
+
+✅ **安全配置**
+- Kubernetes Secret 管理敏感信息
+- NetworkPolicy 网络隔离
+- 非 root 用户运行
+- 资源限制和配额
+
+## 项目结构
+
+```
+auto_put_ad_mini/
+├── Dockerfile              # Docker 镜像定义
+├── .dockerignore          # Docker 构建排除文件
+├── docker-compose.yml     # 本地开发环境
+├── requirements.txt       # Python 依赖
+├── server.py              # FastAPI + APScheduler 服务器(推荐)
+├── execute_once.py        # 单次执行入口
+├── schedule.sh            # Cron 脚本(备用)
+├── whitelist.json         # 白名单账户配置
+├── metrics.py             # Prometheus 指标导出
+├── utils/
+│   └── log_capture.py     # 并发日志捕获
+├── k8s/                   # Kubernetes 部署清单
+│   ├── README.md          # K8s 部署详细说明
+│   ├── namespace.yaml     # 命名空间
+│   ├── deployment.yaml    # Deployment + Service
+│   ├── cronjob.yaml       # CronJob(备用)
+│   ├── configmap.yaml     # 公开配置
+│   ├── secret.yaml        # 敏感信息
+│   ├── pvc.yaml           # 持久化存储
+│   └── network-policy.yaml # 网络策略
+└── DEPLOYMENT.md          # 本文档
+```
+
+## 快速开始
+
+### 阶段 1:本地测试
+
+#### 方式 A:Docker Compose(开发环境)
+
+```bash
+# 1. 进入项目目录
+cd /Users/liulidong/project/agent/Agent/examples/auto_put_ad_mini
+
+# 2. 创建 .env 文件(从 .env.example 复制并填入真实值)
+cp .env.example .env
+vim .env  # 填入实际的 API 密钥和配置
+
+# 3. 构建镜像
+docker build -t auto-put-ad-mini:test .
+
+# 4. 启动服务(APScheduler 模式)
+docker-compose up -d
+
+# 5. 查看日志
+docker-compose logs -f
+
+# 6. 测试健康检查
+curl http://localhost:8080/health | jq .
+
+# 7. 手动触发任务
+curl -X POST http://localhost:8080/trigger | jq .
+
+# 8. 停止服务
+docker-compose down
+```
+
+#### 方式 B:单次执行测试
+
+```bash
+# 测试单次执行(不启动调度服务)
+docker run --rm \
+  --env-file .env \
+  -e EXECUTION_ENABLED=false \
+  -e WHITELIST_ENABLED=true \
+  -e WHITELIST_ACCOUNTS=80769799 \
+  -v $(pwd)/outputs:/app/outputs \
+  auto-put-ad-mini:test \
+  python execute_once.py
+
+# 验证输出
+ls -lh outputs/reports/
+cat outputs/reports/llm_decisions_*.csv | head
+```
+
+### 阶段 2:推送镜像到仓库
+
+```bash
+# 1. 登录镜像仓库
+docker login your-registry.com
+
+# 2. 打标签
+docker tag auto-put-ad-mini:test your-registry.com/auto-put-ad-mini:v1.0.0
+docker tag auto-put-ad-mini:test your-registry.com/auto-put-ad-mini:latest
+
+# 3. 推送
+docker push your-registry.com/auto-put-ad-mini:v1.0.0
+docker push your-registry.com/auto-put-ad-mini:latest
+```
+
+### 阶段 3:Kubernetes 部署
+
+详细步骤见 [k8s/README.md](k8s/README.md)
+
+**推荐部署方式**:Deployment + APScheduler
+
+```bash
+# 1. 创建命名空间
+kubectl apply -f k8s/namespace.yaml
+
+# 2. 修改 k8s/secret.yaml,填入真实密钥
+vim k8s/secret.yaml
+
+# 3. 部署所有资源
+kubectl apply -f k8s/pvc.yaml
+kubectl apply -f k8s/configmap.yaml
+kubectl apply -f k8s/secret.yaml
+kubectl apply -f k8s/deployment.yaml
+
+# 4. 验证部署
+kubectl get all -n ad-automation
+kubectl logs -f -n ad-automation deployment/auto-put-ad-mini
+
+# 5. 端口转发测试
+kubectl port-forward -n ad-automation svc/auto-put-ad-mini 8080:8080
+
+# 6. 测试健康检查
+curl http://localhost:8080/health | jq .
+
+# 7. 手动触发任务
+curl -X POST http://localhost:8080/trigger | jq .
+```
+
+## 配置说明
+
+### 环境变量
+
+| 变量名 | 说明 | 默认值 | 必需 |
+|--------|------|--------|------|
+| `WHITELIST_ENABLED` | 启用账户白名单 | true | 否 |
+| `WHITELIST_ACCOUNTS` | 白名单账户ID(逗号分隔) | - | 是(启用白名单时) |
+| `EXECUTION_ENABLED` | 启用实际执行 | false | 否 |
+| `HTTP_PROXY` | HTTP 代理地址 | - | 否 |
+| `HTTPS_PROXY` | HTTPS 代理地址 | - | 否 |
+| `TZ` | 时区 | UTC | 否 |
+| `CRON_SCHEDULE` | 定时表达式 | 0 2 * * * | 否 |
+| `RUN_ON_STARTUP` | 启动时立即执行 | false | 否 |
+| `PORT` | FastAPI 端口 | 8080 | 否 |
+| `FEISHU_APP_ID` | 飞书应用ID | - | 是 |
+| `FEISHU_APP_SECRET` | 飞书应用密钥 | - | 是 |
+| `TENCENT_AD_ACCOUNT_ID` | 腾讯广告账户ID | - | 是 |
+| `ODPS_ACCESS_ID` | ODPS 访问ID | - | 是 |
+| `ODPS_ACCESS_SECRET` | ODPS 访问密钥 | - | 是 |
+| `OPEN_ROUTER_API_KEY` | OpenRouter API Key | - | 是 |
+
+### 白名单配置
+
+**方式 1:环境变量(推荐)**
+
+```bash
+# .env 或 k8s/secret.yaml
+WHITELIST_ENABLED=true
+WHITELIST_ACCOUNTS=80769799,71305011,12345678
+```
+
+**方式 2:配置文件**
+
+编辑 `whitelist.json`:
+
+```json
+{
+  "accounts": [80769799, 71305011],
+  "description": "生产环境白名单账户列表",
+  "last_updated": "2026-04-22"
+}
+```
+
+### 定时调度配置
+
+**Cron 表达式格式**:`分 时 日 月 周`
+
+示例:
+- `0 2 * * *` - 每天凌晨 2 点(UTC)
+- `30 1 * * *` - 每天凌晨 1:30(UTC)
+- `0 */6 * * *` - 每 6 小时执行一次
+- `0 9 * * 1-5` - 工作日上午 9 点
+
+修改定时:
+
+```bash
+# Kubernetes ConfigMap
+kubectl patch configmap ad-config -n ad-automation \
+  --patch '{"data":{"CRON_SCHEDULE":"0 3 * * *"}}'
+
+kubectl rollout restart deployment/auto-put-ad-mini -n ad-automation
+```
+
+## 监控和运维
+
+### 健康检查
+
+```bash
+# 本地
+curl http://localhost:8080/health
+
+# Kubernetes
+kubectl exec -n ad-automation <pod-name> -- curl -f http://localhost:8080/health
+```
+
+返回示例:
+
+```json
+{
+  "status": "healthy",
+  "timestamp": "2026-04-22T10:00:00Z",
+  "scheduler_running": true,
+  "latest_report": "llm_decisions_20260422.csv",
+  "jobs": [
+    {
+      "id": "decision_pipeline",
+      "name": "广告决策流程",
+      "next_run": "2026-04-23T02:00:00Z"
+    }
+  ]
+}
+```
+
+### 日志查看
+
+**Docker**:
+```bash
+docker logs -f auto-put-ad-mini
+```
+
+**Kubernetes**:
+```bash
+# 实时日志
+kubectl logs -f -n ad-automation deployment/auto-put-ad-mini
+
+# 最近 100 行
+kubectl logs -n ad-automation deployment/auto-put-ad-mini --tail=100
+
+# 查看多个 Pod 日志
+kubectl logs -n ad-automation -l app=auto-put-ad-mini --all-containers=true
+```
+
+### 查看输出文件
+
+**Docker**:
+```bash
+docker exec -it auto-put-ad-mini ls -lh /app/outputs/reports/
+docker exec -it auto-put-ad-mini cat /app/outputs/reports/llm_decisions_*.csv
+```
+
+**Kubernetes**:
+```bash
+POD_NAME=$(kubectl get pods -n ad-automation -l app=auto-put-ad-mini -o jsonpath='{.items[0].metadata.name}')
+kubectl exec -n ad-automation $POD_NAME -- ls -lh /app/outputs/reports/
+kubectl exec -n ad-automation $POD_NAME -- cat /app/outputs/reports/llm_decisions_*.csv
+```
+
+### Prometheus 监控(可选)
+
+如果启用了 Prometheus metrics:
+
+```bash
+# 查看 metrics 文件
+kubectl exec -n ad-automation $POD_NAME -- cat /app/outputs/metrics.prom
+```
+
+配置 Prometheus 抓取:
+
+```yaml
+scrape_configs:
+  - job_name: 'auto-put-ad-mini'
+    file_sd_configs:
+      - files:
+        - /app/outputs/metrics.prom
+```
+
+## 故障排查
+
+### 问题 1:容器启动失败
+
+```bash
+# 查看 Pod 状态
+kubectl describe pod <pod-name> -n ad-automation
+
+# 查看事件
+kubectl get events -n ad-automation --sort-by='.lastTimestamp'
+
+# 查看日志
+kubectl logs <pod-name> -n ad-automation --previous
+```
+
+常见原因:
+- Secret 未配置或配置错误
+- PVC 未创建或无法挂载
+- 镜像拉取失败
+
+### 问题 2:定时任务未执行
+
+```bash
+# 检查调度器状态
+curl http://localhost:8080/health | jq .scheduler_running
+
+# 查看下次执行时间
+curl http://localhost:8080/health | jq .jobs
+
+# 手动触发测试
+curl -X POST http://localhost:8080/trigger
+```
+
+### 问题 3:白名单过滤异常
+
+```bash
+# 查看日志,搜索白名单相关信息
+kubectl logs -n ad-automation deployment/auto-put-ad-mini | grep "白名单"
+
+# 检查配置
+kubectl get secret ad-secrets -n ad-automation -o jsonpath='{.data.WHITELIST_ACCOUNTS}' | base64 -d
+```
+
+### 问题 4:代理连接失败
+
+```bash
+# 检查代理配置
+kubectl get configmap ad-config -n ad-automation -o yaml | grep PROXY
+
+# 测试代理连接
+kubectl exec -n ad-automation $POD_NAME -- curl -x $HTTP_PROXY https://api.e.qq.com
+```
+
+## 回滚和恢复
+
+### 回滚到上一个版本
+
+```bash
+# Deployment 模式
+kubectl rollout undo deployment/auto-put-ad-mini -n ad-automation
+
+# CronJob 模式
+kubectl set image cronjob/auto-put-ad-mini \
+  decision-engine=your-registry/auto-put-ad-mini:v0.9.0 \
+  -n ad-automation
+```
+
+### 暂停服务
+
+```bash
+# Deployment 模式
+kubectl scale deployment auto-put-ad-mini --replicas=0 -n ad-automation
+
+# CronJob 模式
+kubectl patch cronjob auto-put-ad-mini -n ad-automation -p '{"spec":{"suspend":true}}'
+```
+
+### 恢复服务
+
+```bash
+# Deployment 模式
+kubectl scale deployment auto-put-ad-mini --replicas=1 -n ad-automation
+
+# CronJob 模式
+kubectl patch cronjob auto-put-ad-mini -n ad-automation -p '{"spec":{"suspend":false}}'
+```
+
+## 安全最佳实践
+
+### 1. Secret 管理
+
+- ❌ **不要**将真实 Secret 提交到 Git
+- ✅ 使用 Kubernetes External Secrets 或 Sealed Secrets
+- ✅ 定期轮换敏感凭据
+- ✅ 限制 Secret 访问权限
+
+### 2. 网络安全
+
+- ✅ 启用 NetworkPolicy 限制出入站流量
+- ✅ 仅允许必要的外部连接(腾讯 API、飞书 API)
+- ✅ 使用代理服务访问外部资源
+
+### 3. 资源隔离
+
+- ✅ 使用独立命名空间(`ad-automation`)
+- ✅ 设置 CPU 和内存 limits
+- ✅ 使用 LimitRange 和 ResourceQuota
+
+### 4. 运行时安全
+
+- ✅ 非 root 用户运行(UID 1000)
+- ✅ 禁用特权容器
+- ✅ 使用 readOnlyRootFilesystem(输出目录除外)
+
+## 性能优化
+
+### 资源配置建议
+
+| 环境 | CPU Request | CPU Limit | Memory Request | Memory Limit |
+|------|-------------|-----------|----------------|--------------|
+| 测试 | 250m | 500m | 512Mi | 1Gi |
+| 生产 | 500m | 1 | 1Gi | 2Gi |
+| 高负载 | 1 | 2 | 2Gi | 4Gi |
+
+### 并发控制
+
+- APScheduler `max_instances=1` 防止任务并发
+- Kubernetes CronJob `concurrencyPolicy: Forbid`
+
+### 日志轮转
+
+```bash
+# 输出目录定期清理
+find /app/outputs -name "cron_*.log" -mtime +30 -delete
+```
+
+## 升级策略
+
+### 滚动更新
+
+```bash
+# 更新镜像
+kubectl set image deployment/auto-put-ad-mini \
+  decision-engine=your-registry/auto-put-ad-mini:v1.1.0 \
+  -n ad-automation
+
+# 查看更新状态
+kubectl rollout status deployment/auto-put-ad-mini -n ad-automation
+```
+
+### 灰度发布
+
+```bash
+# 创建 Canary Deployment
+kubectl apply -f k8s/deployment-canary.yaml
+
+# 验证 Canary 版本
+kubectl logs -n ad-automation deployment/auto-put-ad-mini-canary
+
+# 全量发布
+kubectl set image deployment/auto-put-ad-mini \
+  decision-engine=your-registry/auto-put-ad-mini:v1.1.0 \
+  -n ad-automation
+
+# 删除 Canary
+kubectl delete deployment auto-put-ad-mini-canary -n ad-automation
+```
+
+## 附录
+
+### A. 两种部署模式对比
+
+| 特性 | Deployment + APScheduler | CronJob |
+|------|-------------------------|---------|
+| 资源占用 | 常驻 Pod(低) | 每次启动新 Pod |
+| 启动速度 | 快(任务触发即执行) | 慢(需启动容器) |
+| 健康检查 | ✅ HTTP 端点 | ❌ 需额外脚本 |
+| 手动触发 | ✅ HTTP API | ⚠️ 需创建 Job |
+| 日志查看 | ✅ 持续输出 | ⚠️ 分散在多个 Job |
+| 监控集成 | ✅ Prometheus metrics | ⚠️ 需额外配置 |
+| 适用场景 | 生产环境、频繁调度 | 简单定时任务 |
+
+**推荐**:生产环境使用 Deployment + APScheduler 模式。
+
+### B. 常用命令速查
+
+```bash
+# 快速重启
+kubectl rollout restart deployment/auto-put-ad-mini -n ad-automation
+
+# 查看最近事件
+kubectl get events -n ad-automation --sort-by='.lastTimestamp' | tail -20
+
+# 进入 Pod Shell
+kubectl exec -it -n ad-automation <pod-name> -- bash
+
+# 复制文件到本地
+kubectl cp ad-automation/<pod-name>:/app/outputs/reports/llm_decisions_*.csv ./local-reports/
+
+# 查看资源使用
+kubectl top pod -n ad-automation
+```
+
+### C. 联系支持
+
+- 文档问题:查看 `k8s/README.md` 和本文档
+- 代码问题:查看 `CLAUDE.md` 和源码注释
+- 配置问题:查看 `.env.example` 和 `k8s/configmap.yaml`
+
+---
+
+**文档版本**:v1.0.0
+**最后更新**:2026-04-22
+**维护者**:auto_put_ad_mini team

+ 422 - 0
examples/auto_put_ad_mini/DOCKER_TEST.md

@@ -0,0 +1,422 @@
+# Docker 本地测试指南
+
+## 🚀 快速开始
+
+### 前提条件
+
+1. **Docker已安装**
+   ```bash
+   docker --version  # 应该 >= 20.10
+   docker-compose --version  # 应该 >= 1.29
+   ```
+
+2. **配置文件已准备**
+   - `.env` 文件存在(包含数据库配置)
+   - 数据库可访问(阿里云RDS)
+
+### 一键测试
+
+```bash
+# 运行测试脚本
+./docker-test.sh
+```
+
+**脚本会自动执行**:
+1. ✅ 清理旧容器
+2. ✅ 构建Docker镜像
+3. ✅ 启动容器
+4. ✅ 健康检查
+5. ✅ 手动触发测试
+
+---
+
+## 📖 详细步骤
+
+### 步骤1:确认配置文件
+
+检查 `.env` 文件是否包含数据库配置:
+
+```bash
+cat .env | grep DB_
+```
+
+应该看到:
+```
+DB_HOST=rm-t4nh1xx6o2a6vj8qu3o.mysql.singapore.rds.aliyuncs.com
+DB_PORT=3306
+DB_USER=ad_rw
+DB_PASSWORD=p82SzuW4kAP3LJXcQGso
+DB_NAME=tencent_ad_autoput
+```
+
+### 步骤2:构建镜像
+
+```bash
+docker-compose build
+```
+
+**说明**:
+- 首次构建需要5-10分钟(下载依赖)
+- 后续构建会使用缓存,更快
+- 如需强制重新构建:`docker-compose build --no-cache`
+
+### 步骤3:启动容器
+
+```bash
+docker-compose up -d
+```
+
+**参数说明**:
+- `-d`: 后台运行
+- 省略 `-d` 会在前台显示日志
+
+### 步骤4:查看日志
+
+```bash
+# 实时查看日志
+docker-compose logs -f
+
+# 查看最近100行
+docker-compose logs --tail=100
+
+# 只看错误日志
+docker-compose logs | grep ERROR
+```
+
+### 步骤5:健康检查
+
+```bash
+curl http://localhost:8080/health | jq .
+```
+
+**预期响应**:
+```json
+{
+  "status": "healthy",
+  "timestamp": "2026-04-23T16:00:00Z",
+  "scheduler_running": true,
+  "latest_report": "llm_decisions_20260423.csv",
+  "jobs": [
+    {
+      "id": "decision_pipeline",
+      "name": "广告决策流程",
+      "next_run": "2026-04-24T03:00:00Z"
+    }
+  ]
+}
+```
+
+### 步骤6:手动触发任务
+
+```bash
+curl -X POST http://localhost:8080/trigger
+```
+
+**预期响应**:
+```json
+{
+  "status": "triggered",
+  "message": "任务已添加到队列"
+}
+```
+
+### 步骤7:查看执行结果
+
+```bash
+# 查看输出文件
+ls -lh outputs/reports/
+
+# 查看决策结果(CSV)
+cat outputs/reports/llm_decisions_*.csv | head -20
+
+# 查看执行日志
+cat outputs/execution_log/execution_*.jsonl | tail -10
+```
+
+---
+
+## 🔧 常用操作
+
+### 进入容器调试
+
+```bash
+# 进入容器shell
+docker exec -it auto_put_ad_mini bash
+
+# 在容器内执行命令
+docker exec -it auto_put_ad_mini python -c "from db import get_whitelist_accounts; print(get_whitelist_accounts())"
+```
+
+### 查看容器状态
+
+```bash
+# 查看运行状态
+docker-compose ps
+
+# 查看资源占用
+docker stats auto_put_ad_mini
+
+# 查看详细信息
+docker inspect auto_put_ad_mini
+```
+
+### 重启容器
+
+```bash
+# 重启
+docker-compose restart
+
+# 停止后重新启动
+docker-compose down
+docker-compose up -d
+```
+
+### 查看网络
+
+```bash
+# 查看容器网络
+docker network ls
+
+# 查看auto_put_ad_mini网络详情
+docker network inspect auto_put_ad_mini_ad_network
+```
+
+---
+
+## 🐛 故障排查
+
+### 问题1:容器启动失败
+
+**症状**:`docker-compose up -d` 后容器立即退出
+
+**排查步骤**:
+```bash
+# 1. 查看日志
+docker-compose logs
+
+# 2. 查看容器退出原因
+docker-compose ps -a
+
+# 3. 检查配置文件
+cat .env | grep -E "DB_|FEISHU_|ODPS_"
+
+# 4. 尝试前台运行查看详细错误
+docker-compose up
+```
+
+**常见原因**:
+- ❌ .env 文件缺失或配置错误
+- ❌ 数据库连接失败
+- ❌ 依赖安装失败
+
+### 问题2:健康检查失败
+
+**症状**:`curl http://localhost:8080/health` 连接失败
+
+**排查步骤**:
+```bash
+# 1. 确认容器正在运行
+docker-compose ps
+
+# 2. 确认端口映射
+docker-compose ps | grep 8080
+
+# 3. 查看容器日志
+docker-compose logs --tail=50
+
+# 4. 检查端口是否被占用
+lsof -i :8080
+```
+
+**解决方法**:
+```bash
+# 如果8080端口被占用,修改docker-compose.yml
+ports:
+  - "8081:8080"  # 改用8081端口
+```
+
+### 问题3:数据库连接失败
+
+**症状**:日志中出现 "数据库连接失败"
+
+**排查步骤**:
+```bash
+# 1. 在容器内测试连接
+docker exec -it auto_put_ad_mini bash
+python3 << EOF
+from db.connection import test_connection
+test_connection()
+EOF
+
+# 2. 检查网络连通性
+docker exec -it auto_put_ad_mini ping -c 3 rm-t4nh1xx6o2a6vj8qu3o.mysql.singapore.rds.aliyuncs.com
+
+# 3. 检查.env配置
+docker exec -it auto_put_ad_mini env | grep DB_
+```
+
+**解决方法**:
+- 确认数据库白名单允许容器IP访问
+- 确认数据库密码正确
+- 使用外网地址而非内网地址(Docker环境)
+
+### 问题4:Agent框架找不到
+
+**症状**:`ModuleNotFoundError: No module named 'agent'`
+
+**原因**:Dockerfile中 `COPY ../../agent /app/agent` 路径错误
+
+**解决方法**:
+```bash
+# 从项目根目录构建
+cd /Users/liulidong/project/agent/Agent
+docker build -t auto-put-ad-mini:latest -f examples/auto_put_ad_mini/Dockerfile .
+
+# 或者修改Dockerfile,使用相对于构建上下文的正确路径
+```
+
+---
+
+## 📊 性能监控
+
+### 查看资源使用
+
+```bash
+# 实时监控
+docker stats auto_put_ad_mini
+
+# 查看磁盘使用
+docker exec -it auto_put_ad_mini du -sh /app/outputs/*
+```
+
+### 查看日志大小
+
+```bash
+# 查看容器日志大小
+docker inspect --format='{{.LogPath}}' auto_put_ad_mini | xargs ls -lh
+
+# 清理日志(如果太大)
+truncate -s 0 $(docker inspect --format='{{.LogPath}}' auto_put_ad_mini)
+```
+
+---
+
+## 🧹 清理环境
+
+### 停止并删除容器
+
+```bash
+docker-compose down
+```
+
+### 删除镜像
+
+```bash
+docker rmi auto_put_ad_mini-auto_put_ad_mini
+```
+
+### 清理所有(包括卷)
+
+```bash
+docker-compose down -v  # 删除所有卷(注意:会删除outputs数据)
+```
+
+### 清理Docker系统
+
+```bash
+# 清理未使用的镜像、容器、网络
+docker system prune -a
+```
+
+---
+
+## 🎯 测试场景
+
+### 场景1:测试定时任务
+
+```bash
+# 1. 修改定时时间为1分钟后
+# 在DMS中执行:
+# UPDATE system_config SET config_value = '*/1 * * * *' WHERE config_key = 'cron_schedule';
+
+# 2. 重启容器
+docker-compose restart
+
+# 3. 查看日志,等待任务执行
+docker-compose logs -f
+```
+
+### 场景2:测试手动触发
+
+```bash
+# 1. 清空outputs目录
+rm -rf outputs/reports/*
+
+# 2. 手动触发
+curl -X POST http://localhost:8080/trigger
+
+# 3. 等待30秒后查看输出
+sleep 30
+ls -lh outputs/reports/
+```
+
+### 场景3:测试白名单
+
+```bash
+# 1. 在DMS中禁用所有账户
+# UPDATE account_whitelist SET enabled = FALSE;
+
+# 2. 手动触发
+curl -X POST http://localhost:8080/trigger
+
+# 3. 查看日志,应该显示"白名单为空"
+docker-compose logs --tail=50
+
+# 4. 恢复白名单
+# UPDATE account_whitelist SET enabled = TRUE WHERE account_id = 80769799;
+```
+
+---
+
+## 📝 环境变量说明
+
+Docker容器会读取以下环境变量:
+
+| 变量 | 说明 | 示例 |
+|------|------|------|
+| `DB_HOST` | 数据库主机 | `rm-xxx.mysql.singapore.rds.aliyuncs.com` |
+| `DB_PORT` | 数据库端口 | `3306` |
+| `DB_USER` | 数据库用户 | `ad_rw` |
+| `DB_PASSWORD` | 数据库密码 | `xxx` |
+| `DB_NAME` | 数据库名称 | `tencent_ad_autoput` |
+| `TZ` | 时区 | `Asia/Shanghai` 或 `UTC` |
+| `EXECUTION_ENABLED` | 执行开关(环境变量降级) | `false` |
+| `WHITELIST_ENABLED` | 白名单开关(环境变量降级) | `true` |
+
+**优先级**:数据库配置 > 环境变量 > 默认值
+
+---
+
+## ✅ 测试检查清单
+
+- [ ] Docker和docker-compose已安装
+- [ ] .env文件已配置数据库信息
+- [ ] 构建镜像成功
+- [ ] 容器启动成功
+- [ ] 健康检查通过
+- [ ] 手动触发成功
+- [ ] 日志输出正常
+- [ ] outputs目录有输出文件
+- [ ] 定时任务配置正确(查看next_run)
+- [ ] 白名单配置生效
+- [ ] 执行开关正确(EXECUTION_ENABLED=false)
+
+---
+
+**测试完成后记得停止容器**:
+```bash
+docker-compose down
+```
+
+**保留输出文件**:
+outputs 目录通过volume映射到本地,停止容器不会丢失数据。

+ 51 - 0
examples/auto_put_ad_mini/Dockerfile

@@ -0,0 +1,51 @@
+# 使用官方 Python 3.10 slim 镜像
+FROM python:3.10-slim
+
+# 设置工作目录
+WORKDIR /app
+
+# 安装系统依赖(ODPS、SSL等)
+RUN apt-get update && apt-get install -y \
+    gcc \
+    g++ \
+    libssl-dev \
+    libffi-dev \
+    curl \
+    && rm -rf /var/lib/apt/lists/*
+
+# 复制依赖文件
+COPY examples/auto_put_ad_mini/requirements.txt .
+
+# 安装 Python 依赖
+RUN pip install --no-cache-dir -r requirements.txt
+
+# 复制 Agent 框架(从项目根目录)
+COPY agent /app/agent
+
+# 复制项目文件(排除 outputs/ 和 .venv/,不包含agent目录)
+COPY examples/auto_put_ad_mini/*.py /app/
+COPY examples/auto_put_ad_mini/prompts /app/prompts/
+COPY examples/auto_put_ad_mini/tools /app/tools/
+COPY examples/auto_put_ad_mini/skills /app/skills/
+COPY examples/auto_put_ad_mini/db /app/db/
+COPY examples/auto_put_ad_mini/*.json /app/
+COPY examples/auto_put_ad_mini/*.md /app/
+
+# 创建输出目录
+RUN mkdir -p outputs/{raw,ad_status,reports,execution_log,data}
+
+# 设置环境变量(生产环境覆盖)
+ENV PYTHONUNBUFFERED=1 \
+    TZ=UTC \
+    PYTHONPATH=/app
+
+# 暴露端口(用于 FastAPI server.py)
+EXPOSE 8080
+
+# 健康检查(使用 FastAPI 端点,如果可用)
+HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
+    CMD curl -f http://localhost:8080/health || python -c "import pathlib; pathlib.Path('/app/outputs').exists()" || exit 1
+
+# 默认命令:启动 FastAPI 服务器(而非单次执行)
+# 如需单次执行,可覆盖为:docker run <image> python execute_once.py
+CMD ["python", "server.py"]

+ 293 - 0
examples/auto_put_ad_mini/SCHEDULER_GUIDE.md

@@ -0,0 +1,293 @@
+# 定时调度与手动触发使用指南
+
+## ⏰ 定时调度配置
+
+### 当前设置
+- **执行时间**: 每天北京时间 11:00(UTC 03:00)
+- **Cron表达式**: `0 3 * * *`
+- **配置位置**: 数据库 `system_config` 表
+
+### 修改定时时间
+
+#### 方法1:通过DMS修改数据库(推荐)
+
+```sql
+-- 修改为每天北京时间 15:00(UTC 07:00)
+UPDATE system_config
+SET config_value = '0 7 * * *', updated_by = 'your_name'
+WHERE config_key = 'cron_schedule';
+
+-- 查看当前配置
+SELECT config_value FROM system_config WHERE config_key = 'cron_schedule';
+```
+
+#### 方法2:通过Python修改
+
+```python
+from db import update_system_config
+
+# 修改为每天北京时间 09:00(UTC 01:00)
+update_system_config('cron_schedule', '0 1 * * *', updated_by='admin')
+```
+
+#### 常用Cron表达式
+
+| 说明 | Cron表达式 | UTC时间 | 北京时间 |
+|------|-----------|---------|---------|
+| 每天上午9点 | `0 1 * * *` | 01:00 | 09:00 |
+| 每天上午11点 | `0 3 * * *` | 03:00 | 11:00 ✅ (当前) |
+| 每天下午3点 | `0 7 * * *` | 07:00 | 15:00 |
+| 每天晚上9点 | `0 13 * * *` | 13:00 | 21:00 |
+| 每12小时 | `0 */12 * * *` | 00:00, 12:00 | 08:00, 20:00 |
+
+---
+
+## 🚀 手动触发任务
+
+### API端点
+
+**URL**: `POST http://localhost:8080/trigger`
+
+**说明**: 手动触发一次决策流程,不影响定时任务
+
+### 使用方法
+
+#### 方法1:curl命令行
+
+```bash
+# 本地触发
+curl -X POST http://localhost:8080/trigger
+
+# 远程触发(如果部署在Kubernetes)
+kubectl port-forward -n ad-automation svc/auto-put-ad-mini 8080:8080
+curl -X POST http://localhost:8080/trigger
+```
+
+#### 方法2:Python脚本
+
+```python
+import requests
+
+response = requests.post('http://localhost:8080/trigger')
+print(response.json())
+
+# 输出示例:
+# {
+#   "status": "triggered",
+#   "message": "任务已添加到队列"
+# }
+```
+
+#### 方法3:在Kubernetes Pod内触发
+
+```bash
+# 进入Pod
+kubectl exec -it -n ad-automation <pod-name> -- bash
+
+# 在Pod内执行
+curl -X POST http://localhost:8080/trigger
+```
+
+### 响应说明
+
+**成功响应**:
+```json
+{
+  "status": "triggered",
+  "message": "任务已添加到队列"
+}
+```
+
+**任务已在运行**:
+```json
+{
+  "status": "scheduled",
+  "message": "任务已在队列中",
+  "next_run": "2026-04-23T03:00:00Z"
+}
+```
+
+---
+
+## 📊 查看任务状态
+
+### 健康检查端点
+
+**URL**: `GET http://localhost:8080/health`
+
+```bash
+curl http://localhost:8080/health | jq .
+```
+
+**响应示例**:
+```json
+{
+  "status": "healthy",
+  "timestamp": "2026-04-23T15:50:00Z",
+  "scheduler_running": true,
+  "latest_report": "llm_decisions_20260423_030000.csv",
+  "jobs": [
+    {
+      "id": "decision_pipeline",
+      "name": "广告决策流程",
+      "next_run": "2026-04-24T03:00:00Z"
+    }
+  ]
+}
+```
+
+---
+
+## 🔧 服务启动
+
+### 本地启动
+
+```bash
+cd /Users/liulidong/project/agent/Agent/examples/auto_put_ad_mini
+
+# 激活虚拟环境
+source .venv/bin/activate
+
+# 启动服务器
+python server.py
+
+# 或使用 uvicorn(生产环境)
+uvicorn server:app --host 0.0.0.0 --port 8080 --reload
+```
+
+### Docker启动
+
+```bash
+# 构建镜像
+docker build -t auto-put-ad-mini:latest .
+
+# 运行容器
+docker run -d \
+  --name auto-put-ad-mini \
+  --env-file .env \
+  -p 8080:8080 \
+  -v $(pwd)/outputs:/app/outputs \
+  auto-put-ad-mini:latest
+```
+
+### Kubernetes部署
+
+```bash
+# 部署
+kubectl apply -f k8s/deployment.yaml
+
+# 查看Pod
+kubectl get pods -n ad-automation
+
+# 查看日志
+kubectl logs -f -n ad-automation <pod-name>
+
+# 端口转发(本地访问)
+kubectl port-forward -n ad-automation svc/auto-put-ad-mini 8080:8080
+```
+
+---
+
+## ⚙️ 高级配置
+
+### 启动时立即执行
+
+在数据库中设置 `run_on_startup = true`:
+
+```sql
+UPDATE system_config
+SET config_value = 'true'
+WHERE config_key = 'run_on_startup';
+```
+
+**效果**: 服务启动后立即执行一次决策流程(不等待定时任务触发)
+
+### 禁用定时任务
+
+方法1:暂停调度器(不推荐,会影响手动触发)
+
+方法2:设置一个很晚的时间(如凌晨4点)
+```sql
+UPDATE system_config
+SET config_value = '0 20 * * *'  -- UTC 20:00 = 北京时间 04:00
+WHERE config_key = 'cron_schedule';
+```
+
+---
+
+## 📝 日志查看
+
+### 查看定时任务日志
+
+```bash
+# Docker容器
+docker logs -f auto-put-ad-mini
+
+# Kubernetes
+kubectl logs -f -n ad-automation <pod-name>
+
+# 查看特定时间的日志
+kubectl logs -n ad-automation <pod-name> --since=1h
+```
+
+### 日志输出示例
+
+```
+2026-04-23 03:00:00 - INFO - [定时任务] 开始执行决策流程
+2026-04-23 03:00:01 - INFO - ✅ 从数据库读取白名单配置:2 个账户
+2026-04-23 03:00:05 - INFO - 拉取广告数据完成:100 个广告
+2026-04-23 03:02:30 - INFO - AI决策完成:pause(10), bid_down(5), observe(85)
+2026-04-23 03:02:35 - INFO - [定时任务] 决策流程执行完成
+```
+
+---
+
+## ⚠️ 注意事项
+
+1. **时区问题**
+   - 服务器使用UTC时区
+   - 数据库Cron表达式使用UTC时间
+   - 北京时间 = UTC + 8小时
+
+2. **并发控制**
+   - 定时任务和手动触发不会并发执行
+   - 如果任务正在运行,手动触发会返回"任务已在队列中"
+
+3. **执行开关**
+   - `execution_enabled = false`: 只分析不执行(安全模式)
+   - `execution_enabled = true`: 真实执行广告操作(谨慎开启)
+
+4. **配置修改生效**
+   - 定时调度配置修改后,需要重启服务才能生效
+   - 白名单和其他配置修改后,5分钟内自动生效(有缓存)
+
+---
+
+## 🆘 故障排查
+
+### 问题1:定时任务没有执行
+
+**检查步骤**:
+1. 查看健康检查:`curl http://localhost:8080/health`
+2. 确认调度器状态:`"scheduler_running": true`
+3. 查看下次执行时间:`"next_run": "..."`
+4. 检查数据库配置:`SELECT config_value FROM system_config WHERE config_key='cron_schedule'`
+
+### 问题2:手动触发失败
+
+**检查步骤**:
+1. 确认服务正在运行:`curl http://localhost:8080/health`
+2. 查看Pod日志:`kubectl logs -f <pod-name>`
+3. 检查数据库连接:确认`.env`文件中的数据库配置正确
+
+### 问题3:时间不对
+
+**解决方法**:
+1. 确认时区设置:Cron表达式使用UTC时间
+2. 转换公式:北京时间 - 8 = UTC时间
+3. 示例:北京时间11:00 → UTC 03:00 → Cron `0 3 * * *`
+
+---
+
+**最后更新**: 2026-04-23
+**配置状态**: 每天北京时间11:00自动执行 ✅

+ 55 - 17
examples/auto_put_ad_mini/config.py

@@ -140,7 +140,24 @@ DATA_FRESHNESS_MAX_HOURS = 96               # 数据超过 96 小时视为过期
 # ═══════════════════════════════════════════
 # 执行引擎配置
 # ═══════════════════════════════════════════
-EXECUTION_ENABLED = False          # 主开关!False = 只验证不执行
+
+# 执行开关(优先级:数据库 > 环境变量 > 默认值False)
+EXECUTION_ENABLED = False
+try:
+    from db import get_system_config
+    _db_execution_enabled = get_system_config("execution_enabled", default=None)
+    if _db_execution_enabled is not None:
+        EXECUTION_ENABLED = _db_execution_enabled
+        logger.info(f"✅ 从数据库读取执行开关:{EXECUTION_ENABLED}")
+    else:
+        # 降级到环境变量
+        _env_execution_enabled = os.getenv("EXECUTION_ENABLED", "").strip().lower()
+        if _env_execution_enabled:
+            EXECUTION_ENABLED = _env_execution_enabled in ("true", "1", "yes")
+            logger.info(f"从环境变量读取执行开关:{EXECUTION_ENABLED}")
+except Exception as e:
+    logger.warning(f"⚠️ 数据库读取执行开关失败({e}),使用默认值:{EXECUTION_ENABLED}")
+
 API_QPS_LIMIT = 8                  # 保守QPS(平台上限10)
 API_MAX_RETRIES = 3
 TIER1_MAX_CHANGE_PCT = 0.00       # Tier1自动执行已禁用(改为0%,所有操作都需审批)
@@ -173,27 +190,48 @@ TENCENT_AD_ACCOUNT_ID = int(os.getenv("TENCENT_AD_ACCOUNT_ID", "80769799"))
 # 账户白名单配置
 # ═══════════════════════════════════════════
 
-# 白名单模式开关
-WHITELIST_ENABLED = os.getenv("WHITELIST_ENABLED", "true").lower() == "true"
-
-# 白名单账户列表(从环境变量或配置文件读取)
+# 白名单模式开关(优先级:数据库 > 环境变量)
+WHITELIST_ENABLED = None
 WHITELIST_ACCOUNTS = []
-_whitelist_str = os.getenv("WHITELIST_ACCOUNTS", "")
-if _whitelist_str:
-    # 格式:逗号分隔,如 "80769799,71305011"
-    WHITELIST_ACCOUNTS = [int(x.strip()) for x in _whitelist_str.split(",") if x.strip()]
-else:
-    # 兜底:从文件读取(可选)
-    _whitelist_file = Path(__file__).parent / "whitelist.json"
-    if _whitelist_file.exists():
-        import json
-        with open(_whitelist_file) as f:
-            whitelist_data = json.load(f)
-            WHITELIST_ACCOUNTS = whitelist_data.get("accounts", [])
+
+# 尝试从数据库读取配置
+try:
+    from db import get_whitelist_accounts, get_system_config
+
+    # 读取白名单开关
+    WHITELIST_ENABLED = get_system_config("whitelist_enabled", default=None)
+
+    # 读取白名单账户列表
+    WHITELIST_ACCOUNTS = get_whitelist_accounts()
+
+    logger.info(f"✅ 从数据库读取白名单配置:{len(WHITELIST_ACCOUNTS)} 个账户")
+except Exception as db_error:
+    logger.warning(f"⚠️ 数据库读取失败({db_error}),降级到环境变量配置")
+
+    # 降级方案1:从环境变量读取
+    _whitelist_str = os.getenv("WHITELIST_ACCOUNTS", "")
+    if _whitelist_str:
+        # 格式:逗号分隔,如 "80769799,71305011"
+        WHITELIST_ACCOUNTS = [int(x.strip()) for x in _whitelist_str.split(",") if x.strip()]
+        logger.info(f"从环境变量读取白名单:{len(WHITELIST_ACCOUNTS)} 个账户")
+    else:
+        # 降级方案2:从文件读取(可选)
+        _whitelist_file = Path(__file__).parent / "whitelist.json"
+        if _whitelist_file.exists():
+            import json
+            with open(_whitelist_file) as f:
+                whitelist_data = json.load(f)
+                WHITELIST_ACCOUNTS = whitelist_data.get("accounts", [])
+                logger.info(f"从 whitelist.json 读取白名单:{len(WHITELIST_ACCOUNTS)} 个账户")
+
+# 白名单开关降级处理
+if WHITELIST_ENABLED is None:
+    WHITELIST_ENABLED = os.getenv("WHITELIST_ENABLED", "true").lower() == "true"
 
 # 向后兼容:单账户模式
 if not WHITELIST_ACCOUNTS:
     WHITELIST_ACCOUNTS = [TENCENT_AD_ACCOUNT_ID]
+    logger.info(f"白名单为空,使用单账户模式:{TENCENT_AD_ACCOUNT_ID}")
 
 logger.info(
     f"白名单配置:{'启用' if WHITELIST_ENABLED else '禁用'},"

+ 24 - 0
examples/auto_put_ad_mini/db/__init__.py

@@ -0,0 +1,24 @@
+"""数据库模块
+
+提供:
+- 数据库连接管理
+- 白名单查询
+- 系统配置查询
+- 决策历史记录
+"""
+
+from .connection import get_connection
+from .config import (
+    get_whitelist_accounts,
+    get_system_config,
+    update_system_config,
+    get_all_system_configs,
+)
+
+__all__ = [
+    "get_connection",
+    "get_whitelist_accounts",
+    "get_system_config",
+    "update_system_config",
+    "get_all_system_configs",
+]

+ 282 - 0
examples/auto_put_ad_mini/db/config.py

@@ -0,0 +1,282 @@
+"""系统配置和白名单数据库操作
+
+提供:
+- 白名单账户查询
+- 系统配置读写
+- 配置缓存机制(避免频繁查询数据库)
+"""
+
+import json
+import logging
+from typing import Any, Dict, List, Optional
+from datetime import datetime, timedelta
+
+from .connection import get_connection
+
+logger = logging.getLogger(__name__)
+
+# 配置缓存(5分钟过期)
+_config_cache: Dict[str, tuple[Any, datetime]] = {}
+_cache_ttl = timedelta(minutes=5)
+
+
+def get_whitelist_accounts() -> List[int]:
+    """获取启用的白名单账户列表
+
+    Returns:
+        List[int]: 账户ID列表,如 [80769799, 71305011]
+
+    Raises:
+        Exception: 数据库查询失败
+    """
+    conn = None
+    try:
+        conn = get_connection()
+        with conn.cursor() as cursor:
+            sql = """
+            SELECT account_id
+            FROM account_whitelist
+            WHERE enabled = TRUE
+            ORDER BY id ASC
+            """
+            cursor.execute(sql)
+            rows = cursor.fetchall()
+            account_ids = [row["account_id"] for row in rows]
+            logger.info(f"从数据库读取白名单账户:{len(account_ids)} 个")
+            return account_ids
+    except Exception as e:
+        logger.error(f"查询白名单账户失败: {e}")
+        raise
+    finally:
+        if conn:
+            conn.close()
+
+
+def get_system_config(key: str, default: Any = None, use_cache: bool = True) -> Any:
+    """获取系统配置值
+
+    Args:
+        key: 配置键,如 'execution_enabled', 'cron_schedule'
+        default: 默认值(配置不存在时返回)
+        use_cache: 是否使用缓存(默认True)
+
+    Returns:
+        Any: 配置值,根据 value_type 自动转换类型
+
+    Examples:
+        >>> get_system_config('execution_enabled')  # 返回 False (boolean)
+        >>> get_system_config('cron_schedule')  # 返回 '0 2 * * *' (string)
+        >>> get_system_config('roi_low_factor')  # 返回 '0.75' (string)
+    """
+    # 检查缓存
+    if use_cache and key in _config_cache:
+        value, cached_at = _config_cache[key]
+        if datetime.now() - cached_at < _cache_ttl:
+            logger.debug(f"从缓存读取配置: {key} = {value}")
+            return value
+
+    conn = None
+    try:
+        conn = get_connection()
+        with conn.cursor() as cursor:
+            sql = """
+            SELECT config_value, value_type
+            FROM system_config
+            WHERE config_key = %s AND enabled = TRUE
+            """
+            cursor.execute(sql, (key,))
+            row = cursor.fetchone()
+
+            if row is None:
+                logger.warning(f"配置不存在: {key},使用默认值: {default}")
+                return default
+
+            value = row["config_value"]
+            value_type = row["value_type"]
+
+            # 类型转换
+            if value_type == "boolean":
+                parsed_value = value.lower() in ("true", "1", "yes")
+            elif value_type == "int":
+                parsed_value = int(value)
+            elif value_type == "json":
+                parsed_value = json.loads(value)
+            else:  # string
+                parsed_value = value
+
+            # 更新缓存
+            _config_cache[key] = (parsed_value, datetime.now())
+            logger.debug(f"从数据库读取配置: {key} = {parsed_value}")
+            return parsed_value
+
+    except Exception as e:
+        logger.error(f"查询系统配置失败: {key}, {e}")
+        return default
+    finally:
+        if conn:
+            conn.close()
+
+
+def update_system_config(
+    key: str, value: Any, value_type: str = None, updated_by: str = None
+) -> bool:
+    """更新系统配置
+
+    Args:
+        key: 配置键
+        value: 配置值
+        value_type: 值类型(string, boolean, int, json),不指定则自动推断
+        updated_by: 更新人
+
+    Returns:
+        bool: 更新成功返回 True
+
+    Examples:
+        >>> update_system_config('execution_enabled', False)
+        >>> update_system_config('cron_schedule', '0 3 * * *')
+    """
+    # 自动推断类型
+    if value_type is None:
+        if isinstance(value, bool):
+            value_type = "boolean"
+        elif isinstance(value, int):
+            value_type = "int"
+        elif isinstance(value, (dict, list)):
+            value_type = "json"
+        else:
+            value_type = "string"
+
+    # 序列化值
+    if value_type == "boolean":
+        serialized_value = "true" if value else "false"
+    elif value_type == "json":
+        serialized_value = json.dumps(value, ensure_ascii=False)
+    else:
+        serialized_value = str(value)
+
+    conn = None
+    try:
+        conn = get_connection()
+        with conn.cursor() as cursor:
+            sql = """
+            UPDATE system_config
+            SET config_value = %s,
+                value_type = %s,
+                updated_by = %s,
+                updated_at = CURRENT_TIMESTAMP
+            WHERE config_key = %s
+            """
+            affected_rows = cursor.execute(
+                sql, (serialized_value, value_type, updated_by, key)
+            )
+
+            if affected_rows == 0:
+                logger.warning(f"配置键不存在,尝试插入: {key}")
+                insert_sql = """
+                INSERT INTO system_config (config_key, config_value, value_type, updated_by)
+                VALUES (%s, %s, %s, %s)
+                """
+                cursor.execute(insert_sql, (key, serialized_value, value_type, updated_by))
+
+            # 清除缓存
+            if key in _config_cache:
+                del _config_cache[key]
+
+            logger.info(f"更新系统配置: {key} = {value} (by {updated_by})")
+            return True
+
+    except Exception as e:
+        logger.error(f"更新系统配置失败: {key}, {e}")
+        return False
+    finally:
+        if conn:
+            conn.close()
+
+
+def get_all_system_configs() -> Dict[str, Any]:
+    """获取所有启用的系统配置
+
+    Returns:
+        Dict[str, Any]: 配置字典,key为配置键,value为配置值
+
+    Examples:
+        >>> configs = get_all_system_configs()
+        >>> print(configs)
+        {'execution_enabled': False, 'cron_schedule': '0 2 * * *', ...}
+    """
+    conn = None
+    try:
+        conn = get_connection()
+        with conn.cursor() as cursor:
+            sql = """
+            SELECT config_key, config_value, value_type
+            FROM system_config
+            WHERE enabled = TRUE
+            ORDER BY id ASC
+            """
+            cursor.execute(sql)
+            rows = cursor.fetchall()
+
+            configs = {}
+            for row in rows:
+                key = row["config_key"]
+                value = row["config_value"]
+                value_type = row["value_type"]
+
+                # 类型转换
+                if value_type == "boolean":
+                    configs[key] = value.lower() in ("true", "1", "yes")
+                elif value_type == "int":
+                    configs[key] = int(value)
+                elif value_type == "json":
+                    configs[key] = json.loads(value)
+                else:
+                    configs[key] = value
+
+            logger.info(f"读取所有系统配置:{len(configs)} 项")
+            return configs
+
+    except Exception as e:
+        logger.error(f"查询所有系统配置失败: {e}")
+        return {}
+    finally:
+        if conn:
+            conn.close()
+
+
+def clear_config_cache():
+    """清除配置缓存(用于测试或强制刷新)"""
+    global _config_cache
+    _config_cache = {}
+    logger.info("配置缓存已清除")
+
+
+if __name__ == "__main__":
+    # 测试配置读取
+    logging.basicConfig(level=logging.INFO)
+
+    print("\n=== 测试白名单查询 ===")
+    try:
+        accounts = get_whitelist_accounts()
+        print(f"白名单账户: {accounts}")
+    except Exception as e:
+        print(f"❌ 白名单查询失败: {e}")
+
+    print("\n=== 测试系统配置查询 ===")
+    try:
+        execution_enabled = get_system_config("execution_enabled")
+        cron_schedule = get_system_config("cron_schedule")
+        roi_low_factor = get_system_config("roi_low_factor")
+        print(f"执行开关: {execution_enabled} ({type(execution_enabled).__name__})")
+        print(f"定时调度: {cron_schedule}")
+        print(f"关停线系数: {roi_low_factor}")
+    except Exception as e:
+        print(f"❌ 系统配置查询失败: {e}")
+
+    print("\n=== 测试所有配置查询 ===")
+    try:
+        all_configs = get_all_system_configs()
+        for key, value in all_configs.items():
+            print(f"  {key}: {value} ({type(value).__name__})")
+    except Exception as e:
+        print(f"❌ 所有配置查询失败: {e}")

+ 105 - 0
examples/auto_put_ad_mini/db/connection.py

@@ -0,0 +1,105 @@
+"""数据库连接管理
+
+提供统一的数据库连接接口,支持环境变量配置
+参考 content_finder/db/connection.py
+"""
+
+import os
+import logging
+from typing import Optional
+
+try:
+    import pymysql
+    import pymysql.cursors
+except ImportError:
+    pymysql = None
+
+logger = logging.getLogger(__name__)
+
+
+def get_connection():
+    """获取数据库连接
+
+    从环境变量读取配置:
+    - DB_HOST: 数据库主机地址
+    - DB_PORT: 数据库端口(默认3306)
+    - DB_USER: 数据库用户名
+    - DB_PASSWORD: 数据库密码
+    - DB_NAME: 数据库名称
+
+    Returns:
+        pymysql.Connection: 数据库连接对象
+
+    Raises:
+        ImportError: pymysql 未安装
+        ValueError: 数据库配置缺失
+        Exception: 连接失败
+    """
+    if pymysql is None:
+        raise ImportError(
+            "pymysql 未安装,请运行: pip install pymysql\n"
+            "或在 requirements.txt 中添加 pymysql>=1.0.0"
+        )
+
+    # 读取环境变量
+    host = os.getenv("DB_HOST", "").strip()
+    port = int(os.getenv("DB_PORT", "3306"))
+    user = os.getenv("DB_USER", "").strip()
+    password = os.getenv("DB_PASSWORD", "")
+    database = os.getenv("DB_NAME", "").strip()
+
+    # 验证必需配置
+    if not all([host, user, database]):
+        raise ValueError(
+            "数据库配置缺失!请在 .env 文件或环境变量中设置:\n"
+            "  DB_HOST=数据库主机地址\n"
+            "  DB_USER=数据库用户名\n"
+            "  DB_PASSWORD=数据库密码\n"
+            "  DB_NAME=数据库名称\n"
+            "  DB_PORT=3306  # 可选,默认3306"
+        )
+
+    try:
+        conn = pymysql.connect(
+            host=host,
+            port=port,
+            user=user,
+            password=password,
+            database=database,
+            charset="utf8mb4",
+            cursorclass=pymysql.cursors.DictCursor,  # 返回字典格式
+            autocommit=True,  # 自动提交(简化事务管理)
+        )
+        logger.debug(f"数据库连接成功: {user}@{host}:{port}/{database}")
+        return conn
+    except Exception as e:
+        logger.error(f"数据库连接失败: {e}")
+        raise
+
+
+def test_connection() -> bool:
+    """测试数据库连接
+
+    Returns:
+        bool: 连接成功返回 True,失败返回 False
+    """
+    try:
+        conn = get_connection()
+        with conn.cursor() as cursor:
+            cursor.execute("SELECT 1")
+            result = cursor.fetchone()
+        conn.close()
+        logger.info("数据库连接测试成功")
+        return True
+    except Exception as e:
+        logger.error(f"数据库连接测试失败: {e}")
+        return False
+
+
+if __name__ == "__main__":
+    # 测试数据库连接
+    logging.basicConfig(level=logging.INFO)
+    if test_connection():
+        print("✅ 数据库连接正常")
+    else:
+        print("❌ 数据库连接失败,请检查配置")

+ 116 - 0
examples/auto_put_ad_mini/db/schema.sql

@@ -0,0 +1,116 @@
+-- auto_put_ad_mini 数据库表结构
+-- 数据库类型:MySQL 5.7+
+-- 字符集:utf8mb4
+
+-- =====================================================
+-- 1. 账户白名单表
+-- =====================================================
+CREATE TABLE IF NOT EXISTS account_whitelist (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '自增主键',
+    account_id BIGINT NOT NULL COMMENT '腾讯广告账户ID',
+    account_name VARCHAR(200) DEFAULT NULL COMMENT '账户名称(备注用)',
+    business_line VARCHAR(100) DEFAULT NULL COMMENT '业务线(用于分组管理)',
+    enabled BOOLEAN DEFAULT TRUE COMMENT '是否启用(1=启用,0=禁用)',
+    remark TEXT DEFAULT NULL COMMENT '备注说明',
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    created_by VARCHAR(100) DEFAULT NULL COMMENT '创建人',
+    updated_by VARCHAR(100) DEFAULT NULL COMMENT '更新人',
+
+    UNIQUE KEY uk_account_id (account_id),
+    KEY idx_enabled (enabled),
+    KEY idx_business_line (business_line),
+    KEY idx_updated_at (updated_at)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='广告账户白名单';
+
+-- =====================================================
+-- 2. 系统配置表(Key-Value 存储)
+-- =====================================================
+CREATE TABLE IF NOT EXISTS system_config (
+    id INT AUTO_INCREMENT PRIMARY KEY COMMENT '自增主键',
+    config_key VARCHAR(100) NOT NULL COMMENT '配置键(如 execution_enabled, cron_schedule)',
+    config_value TEXT NOT NULL COMMENT '配置值(JSON 或字符串)',
+    value_type ENUM('string', 'boolean', 'int', 'json') DEFAULT 'string' COMMENT '值类型',
+    description VARCHAR(500) DEFAULT NULL COMMENT '配置说明',
+    enabled BOOLEAN DEFAULT TRUE COMMENT '是否启用',
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    updated_by VARCHAR(100) DEFAULT NULL COMMENT '更新人',
+
+    UNIQUE KEY uk_config_key (config_key),
+    KEY idx_enabled (enabled)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统配置表';
+
+-- =====================================================
+-- 3. 决策历史记录表(可选,用于审计和分析)
+-- =====================================================
+CREATE TABLE IF NOT EXISTS decision_history (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '自增主键',
+    decision_date DATE NOT NULL COMMENT '决策日期',
+    account_id BIGINT NOT NULL COMMENT '账户ID',
+    ad_id BIGINT NOT NULL COMMENT '广告ID',
+    ad_name VARCHAR(500) DEFAULT NULL COMMENT '广告名称',
+    action VARCHAR(50) NOT NULL COMMENT '决策动作(pause, bid_down, bid_up, scale_up, observe, hold)',
+    dimension VARCHAR(100) DEFAULT NULL COMMENT '决策维度(roi, fission, decay等)',
+    reason TEXT DEFAULT NULL COMMENT '决策理由',
+    current_bid DECIMAL(10, 2) DEFAULT NULL COMMENT '当前出价(元)',
+    recommended_bid DECIMAL(10, 2) DEFAULT NULL COMMENT '推荐出价(元)',
+    recommended_change_pct DECIMAL(6, 2) DEFAULT NULL COMMENT '调整幅度(%)',
+    roi_7d DECIMAL(10, 4) DEFAULT NULL COMMENT '7日ROI',
+    dynamic_roi DECIMAL(10, 4) DEFAULT NULL COMMENT '动态ROI',
+    cost_7d_avg DECIMAL(10, 2) DEFAULT NULL COMMENT '7日均消耗(元)',
+    executed BOOLEAN DEFAULT FALSE COMMENT '是否已执行',
+    execution_status VARCHAR(50) DEFAULT NULL COMMENT '执行状态(success, failed, pending, skipped)',
+    execution_error TEXT DEFAULT NULL COMMENT '执行错误信息',
+    executed_at TIMESTAMP NULL DEFAULT NULL COMMENT '执行时间',
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+
+    KEY idx_decision_date (decision_date),
+    KEY idx_account_id (account_id),
+    KEY idx_ad_id (ad_id),
+    KEY idx_action (action),
+    KEY idx_executed (executed),
+    KEY idx_created_at (created_at)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='决策历史记录';
+
+-- =====================================================
+-- 初始化数据
+-- =====================================================
+
+-- 插入默认系统配置
+INSERT INTO system_config (config_key, config_value, value_type, description) VALUES
+('execution_enabled', 'false', 'boolean', '是否启用实际执行(生产环境谨慎开启)'),
+('whitelist_enabled', 'true', 'boolean', '是否启用白名单机制'),
+('cron_schedule', '0 2 * * *', 'string', 'APScheduler定时调度表达式(每天凌晨2点UTC)'),
+('run_on_startup', 'false', 'boolean', '服务启动时是否立即执行一次'),
+('max_concurrent_tasks', '1', 'int', '最大并发任务数'),
+('approval_timeout_minutes', '30', 'int', '飞书审批超时时间(分钟)'),
+('roi_low_factor', '0.75', 'string', '关停线系数(ROI < 渠道P50 * 0.75 触发关停)'),
+('bid_down_roi_factor', '0.90', 'string', '降价线系数(ROI < 渠道P50 * 0.90 触发降价)'),
+('bid_up_roi_factor', '1.05', 'string', '提价线系数(ROI > 渠道P50 * 1.05 触发提价)')
+ON DUPLICATE KEY UPDATE updated_at = CURRENT_TIMESTAMP;
+
+-- 插入示例白名单账户(请根据实际情况修改)
+INSERT INTO account_whitelist (account_id, account_name, business_line, enabled, remark, created_by) VALUES
+(80769799, '测试账户1', '小程序投流', TRUE, '初始白名单账户', 'system'),
+(71305011, '测试账户2', '小程序投流', TRUE, '初始白名单账户', 'system')
+ON DUPLICATE KEY UPDATE updated_at = CURRENT_TIMESTAMP;
+
+-- =====================================================
+-- 索引优化说明
+-- =====================================================
+-- 1. account_whitelist.uk_account_id: 保证账户唯一性
+-- 2. system_config.uk_config_key: 保证配置键唯一性
+-- 3. decision_history.idx_decision_date: 按日期查询决策记录
+-- 4. decision_history.idx_ad_id: 查询某个广告的历史决策
+-- 5. decision_history.idx_action: 按决策类型统计分析
+
+-- =====================================================
+-- 表分区建议(如数据量大)
+-- =====================================================
+-- ALTER TABLE decision_history PARTITION BY RANGE (YEAR(decision_date) * 100 + MONTH(decision_date)) (
+--     PARTITION p202604 VALUES LESS THAN (202605),
+--     PARTITION p202605 VALUES LESS THAN (202606),
+--     PARTITION p202606 VALUES LESS THAN (202607),
+--     PARTITION pmax VALUES LESS THAN MAXVALUE
+-- );

+ 32 - 0
examples/auto_put_ad_mini/docker-compose.yml

@@ -0,0 +1,32 @@
+version: '3.8'
+
+services:
+  auto_put_ad_mini:
+    build:
+      context: ../..  # 从Agent根目录构建
+      dockerfile: examples/auto_put_ad_mini/Dockerfile
+    container_name: auto_put_ad_mini
+    env_file:
+      - .env
+    environment:
+      - TZ=Asia/Shanghai  # 或 UTC
+      - EXECUTION_ENABLED=false  # 开发环境默认 false
+      - WHITELIST_ENABLED=true
+    ports:
+      - "8080:8080"  # 暴露FastAPI端口
+    volumes:
+      - ./outputs:/app/outputs  # 持久化输出
+      - ./config.py:/app/config.py  # 热更新配置(可选)
+    networks:
+      - ad_network
+    restart: unless-stopped
+    healthcheck:
+      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
+      interval: 30s
+      timeout: 10s
+      retries: 3
+      start_period: 10s
+
+networks:
+  ad_network:
+    driver: bridge

+ 107 - 0
examples/auto_put_ad_mini/docker-test.sh

@@ -0,0 +1,107 @@
+#!/bin/bash
+# Docker 本地测试脚本
+
+set -e
+
+echo "========================================================================"
+echo "🐳 Docker 本地测试环境"
+echo "========================================================================"
+
+# 颜色定义
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+RED='\033[0;31m'
+NC='\033[0m' # No Color
+
+# 检查.env文件
+if [ ! -f .env ]; then
+    echo -e "${RED}❌ 错误:.env 文件不存在${NC}"
+    echo "请先创建 .env 文件(可以从 .env.example 复制)"
+    exit 1
+fi
+
+echo ""
+echo "📋 步骤1:清理旧容器和镜像(如果存在)"
+echo "----------------------------------------"
+docker-compose down 2>/dev/null || true
+docker rmi auto_put_ad_mini-auto_put_ad_mini 2>/dev/null || true
+
+echo ""
+echo "📦 步骤2:构建Docker镜像"
+echo "----------------------------------------"
+echo "⚠️  注意:首次构建可能需要5-10分钟..."
+docker-compose build
+
+echo ""
+echo "🚀 步骤3:启动容器"
+echo "----------------------------------------"
+docker-compose up -d
+
+echo ""
+echo "⏳ 等待服务启动(10秒)..."
+sleep 10
+
+echo ""
+echo "📊 步骤4:检查容器状态"
+echo "----------------------------------------"
+docker-compose ps
+
+echo ""
+echo "🏥 步骤5:健康检查"
+echo "----------------------------------------"
+echo "正在检查 http://localhost:8080/health ..."
+
+if command -v jq &> /dev/null; then
+    curl -s http://localhost:8080/health | jq .
+else
+    curl -s http://localhost:8080/health
+fi
+
+if [ $? -eq 0 ]; then
+    echo -e "${GREEN}✅ 服务健康检查通过!${NC}"
+else
+    echo -e "${RED}❌ 服务健康检查失败${NC}"
+    echo "查看日志:"
+    docker-compose logs --tail=50
+    exit 1
+fi
+
+echo ""
+echo "🎯 步骤6:手动触发测试"
+echo "----------------------------------------"
+echo "正在触发决策流程..."
+curl -X POST http://localhost:8080/trigger
+
+echo ""
+echo ""
+echo "========================================================================"
+echo -e "${GREEN}✅ Docker环境启动成功!${NC}"
+echo "========================================================================"
+echo ""
+echo "📍 可用的命令:"
+echo ""
+echo "  # 查看实时日志"
+echo "  docker-compose logs -f"
+echo ""
+echo "  # 查看健康状态"
+echo "  curl http://localhost:8080/health | jq ."
+echo ""
+echo "  # 手动触发任务"
+echo "  curl -X POST http://localhost:8080/trigger"
+echo ""
+echo "  # 进入容器调试"
+echo "  docker exec -it auto_put_ad_mini bash"
+echo ""
+echo "  # 查看输出文件"
+echo "  ls -lh outputs/reports/"
+echo ""
+echo "  # 停止服务"
+echo "  docker-compose down"
+echo ""
+echo "  # 重新构建"
+echo "  docker-compose build --no-cache"
+echo ""
+echo "========================================================================"
+echo "📖 测试完成后,记得停止容器:"
+echo "   docker-compose down"
+echo "========================================================================"

+ 237 - 0
examples/auto_put_ad_mini/k8s/README.md

@@ -0,0 +1,237 @@
+# Kubernetes Deployment Guide
+
+## 部署模式选择
+
+### 模式 A:Deployment + APScheduler(推荐)
+
+**优势**:
+- 常驻 Pod,资源占用稳定
+- HTTP 健康检查支持
+- 可通过 API 手动触发任务
+- 日志集中输出,易于查看
+- 启动速度快
+
+**部署步骤**:
+
+```bash
+# 1. 创建命名空间
+kubectl apply -f namespace.yaml
+
+# 2. 部署 PVC(持久化存储)
+kubectl apply -f pvc.yaml
+
+# 3. 部署 ConfigMap(公开配置)
+kubectl apply -f configmap.yaml
+
+# 4. 部署 Secret(敏感信息)
+# 注意:先修改 secret.yaml 中的实际值
+kubectl apply -f secret.yaml
+
+# 5. 部署 Deployment 和 Service
+kubectl apply -f deployment.yaml
+
+# 6. (可选)部署网络策略
+kubectl apply -f network-policy.yaml
+
+# 7. 验证部署
+kubectl get all -n ad-automation
+kubectl get pvc -n ad-automation
+
+# 8. 查看日志
+kubectl logs -f -n ad-automation deployment/auto-put-ad-mini
+
+# 9. 端口转发(本地测试)
+kubectl port-forward -n ad-automation svc/auto-put-ad-mini 8080:8080
+
+# 10. 测试健康检查
+curl http://localhost:8080/health | jq .
+
+# 11. 手动触发任务
+curl -X POST http://localhost:8080/trigger | jq .
+```
+
+### 模式 B:CronJob(传统方式)
+
+**优势**:
+- Kubernetes 原生调度
+- 任务隔离性好
+- 故障自动重试
+
+**部署步骤**:
+
+```bash
+# 1-4. 同上
+
+# 5. 部署 CronJob(替代 Deployment)
+kubectl apply -f cronjob.yaml
+
+# 6. 验证
+kubectl get cronjob -n ad-automation
+kubectl get jobs -n ad-automation
+
+# 7. 手动触发测试
+kubectl create job --from=cronjob/auto-put-ad-mini manual-test-1 -n ad-automation
+
+# 8. 查看日志
+kubectl logs -n ad-automation job/manual-test-1
+```
+
+## 配置修改
+
+### 修改定时调度
+
+**Deployment 模式**:
+```bash
+kubectl patch configmap ad-config -n ad-automation \
+  --patch '{"data":{"CRON_SCHEDULE":"0 3 * * *"}}'  # 改为凌晨3点
+
+kubectl rollout restart deployment/auto-put-ad-mini -n ad-automation
+```
+
+**CronJob 模式**:
+```bash
+kubectl edit cronjob auto-put-ad-mini -n ad-automation
+# 修改 spec.schedule 字段
+```
+
+### 启用自动执行
+
+```bash
+# 测试通过后,启用执行开关
+kubectl patch configmap ad-config -n ad-automation \
+  --patch '{"data":{"EXECUTION_ENABLED":"true"}}'
+
+kubectl rollout restart deployment/auto-put-ad-mini -n ad-automation
+```
+
+### 修改白名单账户
+
+```bash
+# 方式1:通过 Secret(推荐)
+kubectl patch secret ad-secrets -n ad-automation \
+  --patch '{"stringData":{"WHITELIST_ACCOUNTS":"80769799,71305011,12345678"}}'
+
+kubectl rollout restart deployment/auto-put-ad-mini -n ad-automation
+
+# 方式2:修改 whitelist.json 并重新构建镜像
+```
+
+## 监控和故障排查
+
+### 查看 Pod 状态
+
+```bash
+kubectl get pods -n ad-automation
+kubectl describe pod <pod-name> -n ad-automation
+```
+
+### 查看日志
+
+```bash
+# 实时日志
+kubectl logs -f -n ad-automation deployment/auto-put-ad-mini
+
+# 最近 100 行日志
+kubectl logs -n ad-automation deployment/auto-put-ad-mini --tail=100
+
+# CronJob 模式查看特定 Job 日志
+kubectl logs -n ad-automation job/<job-name>
+```
+
+### 检查配置
+
+```bash
+# 查看 ConfigMap
+kubectl get configmap ad-config -n ad-automation -o yaml
+
+# 查看 Secret(base64 编码)
+kubectl get secret ad-secrets -n ad-automation -o yaml
+
+# 解码 Secret 值
+kubectl get secret ad-secrets -n ad-automation -o jsonpath='{.data.WHITELIST_ACCOUNTS}' | base64 -d
+```
+
+### 访问输出文件
+
+```bash
+# 进入 Pod 查看输出
+POD_NAME=$(kubectl get pods -n ad-automation -l app=auto-put-ad-mini -o jsonpath='{.items[0].metadata.name}')
+kubectl exec -it -n ad-automation $POD_NAME -- bash
+
+# 在 Pod 内查看输出
+ls -lh /app/outputs/reports/
+cat /app/outputs/reports/llm_decisions_*.csv | head
+
+# 或直接执行命令
+kubectl exec -n ad-automation $POD_NAME -- ls -lh /app/outputs/reports/
+```
+
+## 回滚
+
+### Deployment 模式
+
+```bash
+# 查看历史版本
+kubectl rollout history deployment/auto-put-ad-mini -n ad-automation
+
+# 回滚到上一个版本
+kubectl rollout undo deployment/auto-put-ad-mini -n ad-automation
+
+# 回滚到指定版本
+kubectl rollout undo deployment/auto-put-ad-mini --to-revision=2 -n ad-automation
+```
+
+### CronJob 模式
+
+```bash
+# 暂停 CronJob
+kubectl patch cronjob auto-put-ad-mini -n ad-automation -p '{"spec":{"suspend":true}}'
+
+# 删除失败的 Job
+kubectl delete job -l app=auto-put-ad-mini -n ad-automation
+
+# 更新镜像
+kubectl set image cronjob/auto-put-ad-mini \
+  decision-engine=your-registry/auto-put-ad-mini:v0.9.0 \
+  -n ad-automation
+
+# 恢复 CronJob
+kubectl patch cronjob auto-put-ad-mini -n ad-automation -p '{"spec":{"suspend":false}}'
+```
+
+## 清理资源
+
+```bash
+# 删除所有资源
+kubectl delete -f deployment.yaml
+kubectl delete -f cronjob.yaml
+kubectl delete -f pvc.yaml
+kubectl delete -f configmap.yaml
+kubectl delete -f secret.yaml
+kubectl delete -f network-policy.yaml
+
+# 或删除整个命名空间(慎用)
+kubectl delete namespace ad-automation
+```
+
+## 安全建议
+
+1. **Secret 管理**:
+   - 不要将真实 Secret 提交到 Git
+   - 使用 Kubernetes External Secrets 或 Sealed Secrets
+   - 定期轮换敏感凭据
+
+2. **网络隔离**:
+   - 启用 NetworkPolicy
+   - 仅允许必要的出站连接
+   - 限制 Pod 间通信
+
+3. **资源限制**:
+   - 合理设置 CPU 和内存 limits
+   - 使用 LimitRange 和 ResourceQuota
+   - 监控资源使用情况
+
+4. **最小权限**:
+   - 不使用 root 用户运行
+   - 禁用特权容器
+   - 使用 readOnlyRootFilesystem(如可行)

+ 28 - 0
examples/auto_put_ad_mini/k8s/configmap.yaml

@@ -0,0 +1,28 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: ad-config
+  namespace: ad-automation
+  labels:
+    app: auto-put-ad-mini
+data:
+  # 时区配置
+  TZ: "UTC"
+
+  # 白名单配置
+  WHITELIST_ENABLED: "true"
+
+  # 执行开关(首次部署建议 false,测试通过后改为 true)
+  EXECUTION_ENABLED: "false"
+
+  # 代理配置(根据实际环境调整)
+  # HTTP_PROXY: "http://proxy.namespace.svc.cluster.local:8080"
+  # HTTPS_PROXY: "http://proxy.namespace.svc.cluster.local:8080"
+
+  # APScheduler 定时配置
+  CRON_SCHEDULE: "0 2 * * *"  # 每天凌晨2点UTC
+  RUN_ON_STARTUP: "false"
+  PORT: "8080"
+
+  # 日志配置
+  LOG_LEVEL: "INFO"

+ 50 - 0
examples/auto_put_ad_mini/k8s/cronjob.yaml

@@ -0,0 +1,50 @@
+apiVersion: batch/v1
+kind: CronJob
+metadata:
+  name: auto-put-ad-mini
+  namespace: ad-automation
+  labels:
+    app: auto-put-ad-mini
+spec:
+  schedule: "0 2 * * *"  # 每天凌晨2点UTC
+  concurrencyPolicy: Forbid  # 不允许并发运行
+  successfulJobsHistoryLimit: 3
+  failedJobsHistoryLimit: 3
+  jobTemplate:
+    spec:
+      backoffLimit: 2
+      activeDeadlineSeconds: 7200  # 2小时超时
+      template:
+        metadata:
+          labels:
+            app: auto-put-ad-mini
+        spec:
+          restartPolicy: Never
+          containers:
+          - name: decision-engine
+            image: your-registry/auto-put-ad-mini:latest
+            imagePullPolicy: Always
+            command: ["python", "execute_once.py"]  # 单次执行模式
+            envFrom:
+            - configMapRef:
+                name: ad-config
+            - secretRef:
+                name: ad-secrets
+            volumeMounts:
+            - name: outputs
+              mountPath: /app/outputs
+            resources:
+              requests:
+                memory: "1Gi"
+                cpu: "500m"
+              limits:
+                memory: "2Gi"
+                cpu: "1"
+            securityContext:
+              runAsNonRoot: true
+              runAsUser: 1000
+              allowPrivilegeEscalation: false
+          volumes:
+          - name: outputs
+            persistentVolumeClaim:
+              claimName: ad-outputs-pvc

+ 90 - 0
examples/auto_put_ad_mini/k8s/deployment.yaml

@@ -0,0 +1,90 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: auto-put-ad-mini
+  namespace: ad-automation
+  labels:
+    app: auto-put-ad-mini
+    version: v1.0.0
+spec:
+  replicas: 1  # 单实例运行(避免并发冲突)
+  selector:
+    matchLabels:
+      app: auto-put-ad-mini
+  template:
+    metadata:
+      labels:
+        app: auto-put-ad-mini
+        version: v1.0.0
+    spec:
+      containers:
+      - name: decision-engine
+        image: your-registry/auto-put-ad-mini:latest
+        imagePullPolicy: Always
+        ports:
+        - containerPort: 8080
+          name: http
+          protocol: TCP
+        envFrom:
+        - configMapRef:
+            name: ad-config
+        - secretRef:
+            name: ad-secrets
+        env:
+        - name: CRON_SCHEDULE
+          value: "0 2 * * *"  # 每天凌晨2点UTC
+        - name: RUN_ON_STARTUP
+          value: "false"  # 启动时不立即执行
+        - name: PORT
+          value: "8080"
+        volumeMounts:
+        - name: outputs
+          mountPath: /app/outputs
+        livenessProbe:
+          httpGet:
+            path: /health
+            port: 8080
+          initialDelaySeconds: 30
+          periodSeconds: 30
+          timeoutSeconds: 10
+          failureThreshold: 3
+        readinessProbe:
+          httpGet:
+            path: /health
+            port: 8080
+          initialDelaySeconds: 10
+          periodSeconds: 10
+          timeoutSeconds: 5
+          failureThreshold: 3
+        resources:
+          requests:
+            memory: "1Gi"
+            cpu: "500m"
+          limits:
+            memory: "2Gi"
+            cpu: "1"
+        securityContext:
+          runAsNonRoot: true
+          runAsUser: 1000
+          allowPrivilegeEscalation: false
+      volumes:
+      - name: outputs
+        persistentVolumeClaim:
+          claimName: ad-outputs-pvc
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: auto-put-ad-mini
+  namespace: ad-automation
+  labels:
+    app: auto-put-ad-mini
+spec:
+  selector:
+    app: auto-put-ad-mini
+  ports:
+  - name: http
+    port: 8080
+    targetPort: 8080
+    protocol: TCP
+  type: ClusterIP

+ 7 - 0
examples/auto_put_ad_mini/k8s/namespace.yaml

@@ -0,0 +1,7 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: ad-automation
+  labels:
+    name: ad-automation
+    environment: production

+ 46 - 0
examples/auto_put_ad_mini/k8s/network-policy.yaml

@@ -0,0 +1,46 @@
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+  name: auto-put-ad-mini-policy
+  namespace: ad-automation
+  labels:
+    app: auto-put-ad-mini
+spec:
+  podSelector:
+    matchLabels:
+      app: auto-put-ad-mini
+  policyTypes:
+  - Egress
+  - Ingress
+  ingress:
+  # 允许同命名空间内的 Pod 访问(例如监控、日志收集)
+  - from:
+    - namespaceSelector:
+        matchLabels:
+          name: ad-automation
+    ports:
+    - protocol: TCP
+      port: 8080
+  egress:
+  # 允许访问 DNS
+  - to:
+    - namespaceSelector: {}
+    ports:
+    - protocol: UDP
+      port: 53
+  # 允许访问代理服务(如果需要)
+  - to:
+    - podSelector:
+        matchLabels:
+          app: proxy
+    ports:
+    - protocol: TCP
+      port: 8080
+  # 允许访问外部 HTTPS(腾讯广告 API、飞书 API等)
+  - to:
+    - namespaceSelector: {}
+    ports:
+    - protocol: TCP
+      port: 443
+    - protocol: TCP
+      port: 80

+ 14 - 0
examples/auto_put_ad_mini/k8s/pvc.yaml

@@ -0,0 +1,14 @@
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+  name: ad-outputs-pvc
+  namespace: ad-automation
+  labels:
+    app: auto-put-ad-mini
+spec:
+  accessModes:
+  - ReadWriteOnce
+  resources:
+    requests:
+      storage: 10Gi  # 根据实际需求调整
+  # storageClassName: standard  # 根据集群配置调整

+ 34 - 0
examples/auto_put_ad_mini/k8s/secret.yaml

@@ -0,0 +1,34 @@
+apiVersion: v1
+kind: Secret
+metadata:
+  name: ad-secrets
+  namespace: ad-automation
+  labels:
+    app: auto-put-ad-mini
+type: Opaque
+stringData:
+  # 白名单账户(逗号分隔)
+  WHITELIST_ACCOUNTS: "80769799,71305011"
+
+  # 腾讯广告账户配置
+  TENCENT_AD_ACCOUNT_ID: "80769799"
+
+  # 飞书应用凭据
+  FEISHU_APP_ID: "cli_xxxxx"
+  FEISHU_APP_SECRET: "xxxxxxxx"
+  FEISHU_OPERATOR_OPEN_ID: "ou_xxxxx"
+  FEISHU_OPERATOR_CHAT_ID: "oc_xxxxx"
+  FEISHU_AD_PROJECT_CHAT_ID: ""
+
+  # ODPS 数据平台配置
+  ODPS_ACCESS_ID: "xxxxx"
+  ODPS_ACCESS_SECRET: "xxxxx"
+  ODPS_PROJECT: "loghubods"
+
+  # LLM API Key
+  OPEN_ROUTER_API_KEY: "sk-xxxxx"
+  # QWEN_API_KEY: "sk-xxxxx"
+
+  # 腾讯广告 API(可选)
+  # TENCENT_AD_ACCESS_TOKEN: "xxxxx"
+  # TENCENT_AD_BASE_URL: "https://api.e.qq.com/v3.0"

+ 90 - 0
examples/auto_put_ad_mini/metrics.py

@@ -0,0 +1,90 @@
+"""
+Prometheus metrics 导出(可选功能)
+需要安装:pip install prometheus-client
+"""
+from pathlib import Path
+import pandas as pd
+import logging
+
+logger = logging.getLogger(__name__)
+
+try:
+    from prometheus_client import Counter, Gauge, Histogram, write_to_textfile
+
+    # 定义指标
+    DECISIONS_TOTAL = Counter(
+        'ad_decisions_total',
+        'Total decisions made',
+        ['action', 'account_id']
+    )
+    EXECUTION_DURATION = Histogram(
+        'ad_execution_duration_seconds',
+        'Execution duration in seconds'
+    )
+    APPROVAL_TIMEOUT = Counter(
+        'ad_approval_timeout_total',
+        'Approval timeouts'
+    )
+    ACTIVE_ADS = Gauge(
+        'ad_active_ads_count',
+        'Number of active ads'
+    )
+
+    PROMETHEUS_ENABLED = True
+except ImportError:
+    logger.warning("prometheus_client 未安装,Prometheus metrics 功能不可用")
+    PROMETHEUS_ENABLED = False
+
+
+def export_metrics():
+    """从最近的决策报告导出指标到 Prometheus"""
+    if not PROMETHEUS_ENABLED:
+        logger.debug("Prometheus metrics 未启用,跳过导出")
+        return
+
+    try:
+        reports_dir = Path("/app/outputs/reports")
+        if not reports_dir.exists():
+            logger.warning("报告目录不存在,跳过 metrics 导出")
+            return
+
+        # 查找最新的决策报告
+        latest_csv = sorted(reports_dir.glob("llm_decisions_*.csv"), reverse=True)
+        if not latest_csv:
+            logger.warning("未找到决策报告,跳过 metrics 导出")
+            return
+
+        latest_csv = latest_csv[0]
+        df = pd.read_csv(latest_csv)
+
+        # 统计决策分布
+        for _, row in df.iterrows():
+            DECISIONS_TOTAL.labels(
+                action=row.get('action', 'unknown'),
+                account_id=str(row.get('account_id', 'unknown'))
+            ).inc()
+
+        # 统计活跃广告数
+        if 'configured_status' in df.columns:
+            active_count = len(df[df['configured_status'] == 'AD_STATUS_NORMAL'])
+            ACTIVE_ADS.set(active_count)
+
+        # 写入文件(供 Prometheus file_sd 抓取)
+        metrics_file = Path('/app/outputs/metrics.prom')
+        write_to_textfile(str(metrics_file), DECISIONS_TOTAL)
+        logger.info(f"Prometheus metrics 已导出到 {metrics_file}")
+
+    except Exception as e:
+        logger.error(f"导出 Prometheus metrics 失败: {e}", exc_info=True)
+
+
+def record_execution_duration(duration_seconds: float):
+    """记录执行耗时"""
+    if PROMETHEUS_ENABLED:
+        EXECUTION_DURATION.observe(duration_seconds)
+
+
+def record_approval_timeout():
+    """记录审批超时事件"""
+    if PROMETHEUS_ENABLED:
+        APPROVAL_TIMEOUT.inc()

+ 27 - 7
examples/auto_put_ad_mini/prompts/system.prompt

@@ -59,7 +59,8 @@ $system$
 - `validate_decisions()` — 护栏验证(冷启动/频率/出价边界)
 - `send_approval_request(wait_for_reply)` — 发飞书审批,阻塞等回复
 - `execute_decisions()` — 执行审批通过的决策
-- `generate_report()` — 生成最终报告
+- `send_feishu_text_message()` — 发送简单执行摘要(审批通过后使用,替代完整报告)
+- `generate_report()` — (可选)生成本地 Excel 报告,但不再自动发送飞书表格
 
 ## 工具编排原则
 
@@ -91,7 +92,7 @@ Step 8: send_approval_request     ← 飞书审批
 Step 9: execute_decisions         ← 执行(审批通过后)
-Step 10: generate_report          ← 生成报告
+Step 10: send_feishu_text_message ← 发送简单执行摘要(✅ 仅文字消息,❌ 不再发表格)
 ```
 
 **⚠️ 关键约束**:
@@ -203,11 +204,30 @@ Skill 提供「判断原则」,工具提供「数据」,你负责综合判
 
 ### 部分批准型协议(强制顺序,每步都要做)
 
-1. **显式说出**您圈定的子集 `S`(例:"S = action='bid_down' 14 条")
-2. **执行前**构造 diff 表(已批准 N 条 ✅ / 保留待审 M 条 ⏳ / 不变更 X 条 ➖)
-3. **只对 S 调用** `execute_decisions`(若不支持过滤参数,先 `modify_decisions` 把非 S 标 observe/hold 再执行)
-4. 执行后**必须**调用 `send_feishu_text_message` 用对话口吻同步:"已执行 N + 保留 M + 飞书链接 + 一句主动提醒"
-5. ❌ 严禁只发 `import_to_feishu` 不发文字;❌ 严禁重发未过滤的全量报告
+**核心原则**:任何修改后都必须**重新审批**,等待明确的"同意"/"通过"才能执行。
+
+1. **显式说出**您圈定的子集 `S`(例:"移除 action='bid_down' 3 条,保留 pause 15 条")
+2. 调用 `modify_decisions` 按您的要求修改决策
+3. 调用 `validate_decisions` 重新验证
+4. **调用 `send_approval_request` 重新发送审批表**(只包含修改后的决策)
+5. **等待您明确回复"同意"/"通过"** → 再调用 `execute_decisions`
+6. ❌ 严禁修改后直接执行;❌ 严禁跳过重新审批环节
+
+### 审批通过后的执行流程(简化版)
+
+收到明确的"同意"/"通过"/"批准"后:
+
+1. 调用 `execute_decisions` 执行决策
+2. 调用 `send_feishu_text_message` 发送简单执行摘要,格式:
+   ```
+   ✅ 已执行 N 个决策:
+   - 关停(pause):X 个
+   - 降价(bid_down):Y 个
+
+   详细记录已保存至本地:outputs/reports/decision_YYYYMMDD.xlsx
+   ```
+3. ❌ **不要调用** `generate_report` 和 `import_to_feishu` — 审批表已足够,无需再发完整报告表格
+4. ❌ **不要重复发送审批表** — 审批流程已结束
 
 ### 重审时呈现协商过程
 

+ 39 - 0
examples/auto_put_ad_mini/requirements.txt

@@ -0,0 +1,39 @@
+# Agent Framework (local installation)
+# Install from parent directory: pip install -e ../../
+
+# Core Dependencies
+pandas>=2.0.0
+numpy>=1.24.0
+httpx>=0.24.0
+requests>=2.31.0
+python-dotenv>=1.0.0
+
+# Excel/Report Generation
+openpyxl>=3.1.0
+xlsxwriter>=3.1.0
+
+# ODPS/Data Processing
+pyodps>=0.11.0
+
+# Async Support
+asyncio>=3.4.3
+aiohttp>=3.8.0
+
+# LLM / API
+openai>=1.0.0
+
+# Utilities
+pydantic>=2.0.0
+loguru>=0.7.0
+tenacity>=8.2.0
+
+# For FastAPI + APScheduler (if using server.py)
+fastapi>=0.104.0
+uvicorn[standard]>=0.24.0
+apscheduler>=3.10.0
+
+# Database (MySQL)
+pymysql>=1.1.0
+
+# Optional: Prometheus metrics
+# prometheus-client>=0.19.0

+ 64 - 0
examples/auto_put_ad_mini/run_full_analysis.py

@@ -0,0 +1,64 @@
+#!/usr/bin/env python3
+"""
+完整广告分析流程脚本
+执行 Step 1-10 的完整决策流程
+"""
+import asyncio
+import sys
+from pathlib import Path
+
+# 添加项目路径
+sys.path.insert(0, str(Path(__file__).parent))
+
+from agent.tools.models import ToolContext
+from tools.data_query import fetch_creative_data
+from tools.creative_metrics import merge_creative_data
+from tools.roi_calculator import calculate_roi_metrics
+from tools.ad_decision import get_ads_for_review
+
+
+async def main():
+    """执行完整分析流程"""
+    
+    # 创建工具上下文
+    ctx = ToolContext(
+        agent_id="auto_put_ad_mini",
+        session_id="manual_run",
+        trace_id="manual_trace"
+    )
+    
+    end_date = "20260420"
+    days = 7
+    
+    print("=" * 60)
+    print("Step 1: 拉取创意数据")
+    print("=" * 60)
+    result1 = await fetch_creative_data(ctx, days=days, end_date=end_date)
+    print(result1.output)
+    
+    print("\n" + "=" * 60)
+    print("Step 2: 合并创意数据")
+    print("=" * 60)
+    result2 = await merge_creative_data(ctx, days=days, force=False)
+    print(result2.output)
+    
+    print("\n" + "=" * 60)
+    print("Step 3: 计算 ROI 指标")
+    print("=" * 60)
+    result3 = await calculate_roi_metrics(ctx, end_date=end_date)
+    print(result3.output)
+    
+    print("\n" + "=" * 60)
+    print("Step 4: 获取待评估广告")
+    print("=" * 60)
+    result4 = await get_ads_for_review(ctx, metrics_csv="", end_date=end_date)
+    print(result4.output)
+    
+    print("\n" + "=" * 60)
+    print("数据准备完成!")
+    print("=" * 60)
+    print("\n接下来需要 AI 对待评估广告进行推理决策...")
+
+
+if __name__ == "__main__":
+    asyncio.run(main())

+ 22 - 0
examples/auto_put_ad_mini/schedule.sh

@@ -0,0 +1,22 @@
+#!/bin/bash
+set -e
+
+# 日志文件
+LOG_FILE="/app/outputs/cron_$(date +%Y%m%d_%H%M%S).log"
+
+# 记录开始时间
+echo "========================================" | tee -a "$LOG_FILE"
+echo "开始执行:$(date)" | tee -a "$LOG_FILE"
+echo "========================================" | tee -a "$LOG_FILE"
+
+# 执行主脚本
+cd /app
+python execute_once.py 2>&1 | tee -a "$LOG_FILE"
+
+# 记录结束时间
+echo "========================================" | tee -a "$LOG_FILE"
+echo "执行完成:$(date)" | tee -a "$LOG_FILE"
+echo "========================================" | tee -a "$LOG_FILE"
+
+# 清理旧日志(保留最近30天)
+find /app/outputs -name "cron_*.log" -mtime +30 -delete

+ 222 - 0
examples/auto_put_ad_mini/server.py

@@ -0,0 +1,222 @@
+"""
+生产服务器 - FastAPI + APScheduler
+参考 content_finder 的生产部署模式
+"""
+import asyncio
+import logging
+import os
+import sys
+from datetime import datetime, timezone
+from pathlib import Path
+
+from apscheduler.schedulers.asyncio import AsyncIOScheduler
+from apscheduler.triggers.cron import CronTrigger
+from fastapi import FastAPI, HTTPException
+from fastapi.responses import JSONResponse
+
+# 添加项目根目录到 Python 路径
+sys.path.insert(0, str(Path(__file__).parent.parent.parent))
+
+# 导入执行引擎
+from execute_once import main as execute_decision_pipeline
+
+logger = logging.getLogger(__name__)
+
+# ═══════════════════════════════════════════
+# 全局调度器
+# ═══════════════════════════════════════════
+scheduler: AsyncIOScheduler = None
+
+# ═══════════════════════════════════════════
+# 定时任务
+# ═══════════════════════════════════════════
+async def scheduled_decision_job():
+    """定时执行决策流程"""
+    logger.info(f"[定时任务] 开始执行决策流程 - {datetime.now(timezone.utc)}")
+    try:
+        await execute_decision_pipeline()
+        logger.info(f"[定时任务] 决策流程执行完成")
+    except Exception as e:
+        logger.error(f"[定时任务] 执行失败: {e}", exc_info=True)
+
+# ═══════════════════════════════════════════
+# FastAPI 应用
+# ═══════════════════════════════════════════
+app = FastAPI(
+    title="auto_put_ad_mini",
+    description="广告智能调控服务",
+    version="1.0.0",
+)
+
+@app.on_event("startup")
+async def startup():
+    """服务启动时初始化"""
+    global scheduler
+
+    logger.info("=" * 60)
+    logger.info("广告智能调控服务启动中...")
+    logger.info("启动 APScheduler")
+
+    scheduler = AsyncIOScheduler(timezone="UTC")
+
+    # 从数据库读取定时调度配置(优先级:数据库 > 环境变量)
+    cron_expression = "0 2 * * *"  # 默认值
+    try:
+        from db import get_system_config
+        cron_expression = get_system_config("cron_schedule", default="0 2 * * *")
+        logger.info(f"✅ 从数据库读取定时调度:{cron_expression}")
+    except Exception as e:
+        logger.warning(f"⚠️ 数据库读取失败,使用环境变量或默认值: {e}")
+        cron_expression = os.getenv("CRON_SCHEDULE", "0 2 * * *")
+
+    scheduler.add_job(
+        scheduled_decision_job,
+        trigger=CronTrigger.from_crontab(cron_expression, timezone="UTC"),
+        id="decision_pipeline",
+        name="广告决策流程",
+        replace_existing=True,
+        max_instances=1,  # 不允许并发运行
+    )
+
+    # 可选:启动时立即执行一次(从数据库读取配置)
+    run_on_startup = False
+    try:
+        from db import get_system_config
+        run_on_startup = get_system_config("run_on_startup", default=False)
+    except:
+        run_on_startup = os.getenv("RUN_ON_STARTUP", "false").lower() == "true"
+
+    if run_on_startup:
+        logger.info("启动时立即执行一次决策流程")
+        scheduler.add_job(
+            scheduled_decision_job,
+            id="startup_run",
+            name="启动时执行",
+        )
+
+    scheduler.start()
+    logger.info(f"✅ 定时任务已配置:{cron_expression}")
+    logger.info("服务启动完成")
+    logger.info("=" * 60)
+
+
+@app.on_event("shutdown")
+async def shutdown():
+    """服务关闭时清理"""
+    logger.info("服务关闭中...")
+    if scheduler and scheduler.running:
+        scheduler.shutdown()
+    logger.info("服务已关闭")
+
+# ═══════════════════════════════════════════
+# 请求日志中间件
+# ═══════════════════════════════════════════
+import time
+from fastapi import Request
+
+@app.middleware("http")
+async def log_requests(request: Request, call_next):
+    """记录所有HTTP请求"""
+    start_time = time.time()
+    response = await call_next(request)
+    duration = time.time() - start_time
+
+    logger.info(
+        f"{request.method} {request.url.path} "
+        f"status={response.status_code} duration={duration:.3f}s"
+    )
+    return response
+
+# ═══════════════════════════════════════════
+# 健康检查端点
+# ═══════════════════════════════════════════
+@app.get("/health")
+async def health_check():
+    """健康检查(Kubernetes liveness/readiness probe)"""
+    try:
+        # 检查输出目录
+        outputs_dir = Path("/app/outputs")
+        if not outputs_dir.exists():
+            raise HTTPException(status_code=500, detail="输出目录不存在")
+
+        # 检查调度器状态
+        if scheduler is None or not scheduler.running:
+            raise HTTPException(status_code=500, detail="调度器未运行")
+
+        # 检查最近执行时间
+        reports_dir = outputs_dir / "reports"
+        latest_report = None
+        if reports_dir.exists():
+            recent_files = sorted(reports_dir.glob("llm_decisions_*.csv"), reverse=True)
+            if recent_files:
+                latest_report = recent_files[0].name
+
+        return JSONResponse({
+            "status": "healthy",
+            "timestamp": datetime.now(timezone.utc).isoformat(),
+            "scheduler_running": scheduler.running if scheduler else False,
+            "latest_report": latest_report,
+            "jobs": [
+                {
+                    "id": job.id,
+                    "name": job.name,
+                    "next_run": job.next_run_time.isoformat() if job.next_run_time else None,
+                }
+                for job in scheduler.get_jobs()
+            ] if scheduler else [],
+        })
+    except HTTPException:
+        raise
+    except Exception as e:
+        logger.error(f"健康检查失败: {e}", exc_info=True)
+        raise HTTPException(status_code=500, detail=str(e))
+
+# ═══════════════════════════════════════════
+# 手动触发端点(可选)
+# ═══════════════════════════════════════════
+@app.post("/trigger")
+async def manual_trigger():
+    """手动触发决策流程"""
+    logger.info("收到手动触发请求")
+
+    # 检查是否有任务正在运行
+    running_jobs = [job for job in scheduler.get_jobs() if job.id == "decision_pipeline"]
+    if running_jobs and running_jobs[0].next_run_time:
+        return JSONResponse({
+            "status": "scheduled",
+            "message": "任务已在队列中",
+            "next_run": running_jobs[0].next_run_time.isoformat(),
+        })
+
+    # 添加一次性任务
+    scheduler.add_job(
+        scheduled_decision_job,
+        id=f"manual_trigger_{datetime.now().strftime('%Y%m%d_%H%M%S')}",
+        name="手动触发",
+    )
+
+    return JSONResponse({
+        "status": "triggered",
+        "message": "任务已添加到队列",
+    })
+
+# ═══════════════════════════════════════════
+# 启动服务
+# ═══════════════════════════════════════════
+if __name__ == "__main__":
+    import uvicorn
+
+    # 配置日志输出到 stdout(Kubernetes 自动收集)
+    logging.basicConfig(
+        level=logging.INFO,
+        format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
+        handlers=[logging.StreamHandler(sys.stdout)]
+    )
+
+    port = int(os.getenv("PORT", 8080))
+    uvicorn.run(
+        "server:app",
+        host="0.0.0.0",
+        port=port,
+        log_level="info",
+    )

+ 155 - 0
examples/auto_put_ad_mini/test_db_summary.md

@@ -0,0 +1,155 @@
+# 数据库集成测试报告
+
+**测试时间**: 2026-04-23
+**数据库**: 阿里云 RDS MySQL (新加坡)
+**数据库名**: tencent_ad_autoput
+
+---
+
+## ✅ 测试结果总览
+
+| 测试项 | 状态 | 说明 |
+|-------|------|------|
+| 数据库连接 | ✅ 通过 | 外网地址连接成功 |
+| 表结构导入 | ✅ 通过 | 3张表创建成功 |
+| 初始数据导入 | ✅ 通过 | 2个账户、9个配置项 |
+| 白名单查询 | ✅ 通过 | 返回 [80769799, 71305011] |
+| 系统配置查询 | ✅ 通过 | 所有配置读取正确 |
+| 缓存机制 | ✅ 通过 | 5分钟缓存生效 |
+| config.py 集成 | ✅ 通过 | 优先从数据库读取 |
+
+---
+
+## 📊 数据库详情
+
+### 已创建的表
+
+1. **account_whitelist** (账户白名单)
+   - 字段: id, account_id, account_name, business_line, enabled, remark, created_at, updated_at
+   - 初始数据: 2个账户
+
+2. **system_config** (系统配置)
+   - 字段: id, config_key, config_value, value_type, description, enabled
+   - 初始数据: 9个配置项
+
+3. **decision_history** (决策历史记录)
+   - 字段: id, decision_date, account_id, ad_id, action, reason, executed, etc.
+   - 初始数据: 空(运行时写入)
+
+### 初始白名单账户
+
+| 账户ID | 账户名称 | 业务线 | 状态 |
+|-------|---------|-------|------|
+| 80769799 | 测试账户1 | 小程序投流 | ✅启用 |
+| 71305011 | 测试账户2 | 小程序投流 | ✅启用 |
+
+### 初始系统配置
+
+| 配置键 | 配置值 | 类型 |
+|-------|-------|------|
+| execution_enabled | false | boolean |
+| whitelist_enabled | true | boolean |
+| cron_schedule | 0 2 * * * | string |
+| run_on_startup | false | boolean |
+| max_concurrent_tasks | 1 | int |
+| approval_timeout_minutes | 30 | int |
+| roi_low_factor | 0.75 | string |
+| bid_down_roi_factor | 0.90 | string |
+| bid_up_roi_factor | 1.05 | string |
+
+---
+
+## 🔧 配置读取逻辑
+
+### 优先级(三层降级)
+
+```
+1. 数据库(优先)
+   ↓ 失败
+2. 环境变量(降级)
+   ↓ 失败  
+3. 文件/默认值(兜底)
+```
+
+### 验证结果
+
+- ✅ `WHITELIST_ENABLED`: True (从数据库读取)
+- ✅ `WHITELIST_ACCOUNTS`: [80769799, 71305011] (从数据库读取)
+- ✅ `EXECUTION_ENABLED`: False (从数据库读取)
+- ✅ ROI阈值: 正确读取 (0.75, 0.90, 1.05)
+
+---
+
+## 🚀 下一步操作
+
+### 1. 管理白名单(在数据库中)
+
+```sql
+-- 添加新账户
+INSERT INTO account_whitelist (account_id, account_name, business_line, enabled, created_by)
+VALUES (12345678, '新账户名称', '小程序投流', TRUE, 'admin');
+
+-- 禁用账户
+UPDATE account_whitelist SET enabled = FALSE WHERE account_id = 80769799;
+
+-- 启用账户
+UPDATE account_whitelist SET enabled = TRUE WHERE account_id = 80769799;
+
+-- 查询所有白名单
+SELECT account_id, account_name, business_line, enabled FROM account_whitelist;
+```
+
+### 2. 管理系统配置(在数据库中)
+
+```sql
+-- 开启实际执行
+UPDATE system_config SET config_value = 'true' WHERE config_key = 'execution_enabled';
+
+-- 修改定时调度(改为每天凌晨3点)
+UPDATE system_config SET config_value = '0 3 * * *' WHERE config_key = 'cron_schedule';
+
+-- 修改ROI阈值
+UPDATE system_config SET config_value = '0.80' WHERE config_key = 'roi_low_factor';
+
+-- 查询所有配置
+SELECT config_key, config_value, value_type FROM system_config WHERE enabled = 1;
+```
+
+### 3. 通过 Python 管理配置
+
+```python
+from db import update_system_config
+
+# 更新配置
+update_system_config('execution_enabled', True, updated_by='admin')
+update_system_config('cron_schedule', '0 3 * * *', updated_by='admin')
+```
+
+---
+
+## 📝 环境变量配置
+
+`.env` 文件已更新,包含:
+
+```bash
+DB_HOST=rm-t4nh1xx6o2a6vj8qu3o.mysql.singapore.rds.aliyuncs.com
+DB_PORT=3306
+DB_USER=ad_rw
+DB_PASSWORD=p82SzuW4kAP3LJXcQGso
+DB_NAME=tencent_ad_autoput
+```
+
+---
+
+## ✅ 结论
+
+数据库集成**完全成功**!
+
+- ✅ 数据库连接正常
+- ✅ 表结构完整
+- ✅ 配置读取正确
+- ✅ 降级机制生效
+- ✅ 缓存机制工作
+
+**可以开始使用数据库管理白名单和系统配置了!**
+

+ 47 - 0
examples/auto_put_ad_mini/test_scheduler.sh

@@ -0,0 +1,47 @@
+#!/bin/bash
+set -e
+
+echo "========================================================================"
+echo "🧪 定时调度与手动触发测试"
+echo "========================================================================"
+
+echo ""
+echo "1️⃣ 验证数据库配置"
+echo "----------------------------------------"
+export DB_HOST=rm-t4nh1xx6o2a6vj8qu3o.mysql.singapore.rds.aliyuncs.com
+export DB_PORT=3306
+export DB_USER=ad_rw
+export DB_PASSWORD=p82SzuW4kAP3LJXcQGso
+export DB_NAME=tencent_ad_autoput
+
+source .venv/bin/activate
+python3 << 'EOFPYTHON'
+from db import get_system_config
+
+cron = get_system_config('cron_schedule')
+run_on_startup = get_system_config('run_on_startup')
+
+print(f"✅ cron_schedule: {cron}")
+print(f"   = UTC 03:00 = 北京时间 11:00")
+print(f"✅ run_on_startup: {run_on_startup}")
+EOFPYTHON
+
+echo ""
+echo "2️⃣ 启动服务器测试(后台运行)"
+echo "----------------------------------------"
+echo "⚠️  需要手动启动服务器测试:"
+echo "   cd /Users/liulidong/project/agent/Agent/examples/auto_put_ad_mini"
+echo "   source .venv/bin/activate"
+echo "   python server.py"
+echo ""
+echo "   然后在另一个终端执行:"
+echo "   curl http://localhost:8080/health | jq ."
+echo "   curl -X POST http://localhost:8080/trigger"
+
+echo ""
+echo "========================================================================"
+echo "✅ 配置验证完成"
+echo "========================================================================"
+echo ""
+echo "📝 详细使用说明请查看:SCHEDULER_GUIDE.md"
+

+ 24 - 28
examples/auto_put_ad_mini/tools/im_approval.py

@@ -530,28 +530,27 @@ async def send_approval_request(
         # ⚠️ 排除不需运营立刻干预的 action,不写飞书表降低噪声
         # - hold/observe:无需操作
         # - scale_up:扩量逻辑尚在调试,暂不进审批表
-        FEISHU_EXCLUDE_ACTIONS = {"hold", "observe", "scale_up"}
-        df_tier0_for_review = df_tier0
-        if not df_tier0.empty:
-            action_col = "final_action" if "final_action" in df_tier0.columns else "action"
-            if action_col in df_tier0.columns:
-                before_filter = len(df_tier0)
-                df_tier0_for_review = df_tier0[~df_tier0[action_col].isin(FEISHU_EXCLUDE_ACTIONS)].copy()
-                dropped_count = before_filter - len(df_tier0_for_review)
-                if dropped_count > 0:
-                    dropped_breakdown = (
-                        df_tier0[df_tier0[action_col].isin(FEISHU_EXCLUDE_ACTIONS)][action_col]
-                        .value_counts().to_dict()
-                    )
-                    logger.info(
-                        f"飞书表过滤掉 {dropped_count} 个 {sorted(FEISHU_EXCLUDE_ACTIONS)} 决策"
-                        f"(明细: {dropped_breakdown},减少表格噪声)"
-                    )
-        df_for_review = (
-            pd.concat([df_tier2_3, df_tier0_for_review], ignore_index=True)
-            if not df_tier0_for_review.empty
-            else df_tier2_3
-        )
+        # - bid_up:提价决策由系统自动执行,不进审批表(仅保留 pause 和 bid_down)
+        FEISHU_EXCLUDE_ACTIONS = {"hold", "observe", "scale_up", "bid_up"}
+
+        # 对所有 tier 应用过滤(不仅仅是 tier0)
+        action_col = "final_action" if "final_action" in df.columns else "action"
+        before_filter = len(df)
+        df_filtered = df[~df[action_col].isin(FEISHU_EXCLUDE_ACTIONS)].copy()
+        dropped_count = before_filter - len(df_filtered)
+
+        if dropped_count > 0:
+            dropped_breakdown = (
+                df[df[action_col].isin(FEISHU_EXCLUDE_ACTIONS)][action_col]
+                .value_counts().to_dict()
+            )
+            logger.info(
+                f"飞书表过滤掉 {dropped_count} 个 {sorted(FEISHU_EXCLUDE_ACTIONS)} 决策"
+                f"(明细: {dropped_breakdown},减少表格噪声)"
+            )
+
+        # 使用过滤后的数据作为审批表
+        df_for_review = df_filtered
 
         if df_tier2_3.empty:
             total_no_op = len(df_tier0) + len(df_tier1)
@@ -766,8 +765,7 @@ async def send_approval_request(
                                 f"请根据运营的自然语言回复判断后续操作:\n"
                                 f"- 运营同意/批准/通过 → 调用 execute_decisions 执行\n"
                                 f"- 运营拒绝/驳回 → 停止执行,告知原因\n"
-                                f"- 运营要求修改(如\"广告X不要暂停\")→ 进入 Mode 3: modify_decisions → validate → 重新审批\n"
-                                f"- 运营部分批准(如\"只批准降价的\")→ 相应过滤后 execute_decisions"
+                                f"- 运营要求修改(如\"广告X不要暂停\"/\"降价的去掉\")→ modify_decisions → validate → 重新审批(等待再次明确'同意')"
                             ),
                             metadata={
                                 "request_id": request_id,
@@ -822,8 +820,7 @@ async def send_approval_request(
                                         f"请根据运营的自然语言回复判断后续操作:\n"
                                         f"- 运营同意/批准/通过 → 调用 execute_decisions 执行\n"
                                         f"- 运营拒绝/驳回 → 停止执行,告知原因\n"
-                                        f"- 运营要求修改(如\"广告X不要暂停\")→ 进入 Mode 3: modify_decisions → validate → 重新审批\n"
-                                        f"- 运营部分批准(如\"只批准降价的\")→ 相应过滤后 execute_decisions"
+                                        f"- 运营要求修改(如\"广告X不要暂停\"/\"降价的去掉\")→ modify_decisions → validate → 重新审批(等待再次明确'同意')"
                                     ),
                                     metadata={
                                         "request_id": request_id,
@@ -963,8 +960,7 @@ async def check_approval_status(
                                     f"请根据运营的自然语言回复判断后续操作:\n"
                                     f"- 运营同意/批准/通过 → 调用 execute_decisions 执行\n"
                                     f"- 运营拒绝/驳回 → 停止执行,告知原因\n"
-                                    f"- 运营要求修改 → 进入 Mode 3: modify_decisions → validate → 重新审批\n"
-                                    f"- 运营部分批准 → 相应过滤后 execute_decisions"
+                                    f"- 运营要求修改 → modify_decisions → validate → 重新审批(等待再次明确'同意')"
                                 ),
                                 metadata={"request_id": request_id, "raw_reply": text, "ad_ids": ad_ids},
                             )

+ 57 - 0
examples/auto_put_ad_mini/utils/log_capture.py

@@ -0,0 +1,57 @@
+"""
+并发日志捕获工具
+支持多个并发任务独立记录日志,避免日志混乱
+参考 content_finder 的日志捕获实现
+"""
+import logging
+import contextvars
+from io import StringIO
+from typing import Optional
+
+# 上下文变量:每个任务独立的日志缓冲区
+log_buffer_var: contextvars.ContextVar[Optional[StringIO]] = contextvars.ContextVar(
+    "log_buffer", default=None
+)
+
+class ContextBufferHandler(logging.Handler):
+    """将日志写入上下文变量的缓冲区"""
+
+    def emit(self, record: logging.LogRecord):
+        buffer = log_buffer_var.get()
+        if buffer is not None:
+            msg = self.format(record)
+            buffer.write(msg + "\n")
+
+def setup_concurrent_logging():
+    """设置并发日志系统"""
+    root_logger = logging.getLogger()
+
+    # 添加上下文缓冲处理器
+    buffer_handler = ContextBufferHandler()
+    buffer_handler.setFormatter(
+        logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
+    )
+    root_logger.addHandler(buffer_handler)
+
+class LogCapture:
+    """日志捕获上下文管理器"""
+
+    def __init__(self):
+        self.buffer = StringIO()
+        self.token = None
+
+    def __enter__(self):
+        self.token = log_buffer_var.set(self.buffer)
+        return self
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        log_buffer_var.reset(self.token)
+
+    def get_logs(self) -> str:
+        """获取捕获的日志"""
+        return self.buffer.getvalue()
+
+# 使用示例
+# with LogCapture() as capture:
+#     logger.info("这条日志会被捕获")
+#     logs = capture.get_logs()

+ 8 - 0
examples/auto_put_ad_mini/whitelist.json

@@ -0,0 +1,8 @@
+{
+  "accounts": [
+    80769799,
+    71305011
+  ],
+  "description": "生产环境白名单账户列表",
+  "last_updated": "2026-04-22"
+}