feat(feishu): 添加飞书长连接支持并重构事件处理

添加了 FeishuEventService 来统一处理飞书消息事件,
新增 long_connection.py 实现长连接客户端,
修改 webhook 路由使用新的事件处理服务,
添加了 lark-oapi 依赖支持长连接功能,
更新测试用例覆盖新的事件处理逻辑。

BREAKING CHANGE: 飞书事件处理逻辑重构,统一使用
FeishuEventService 进行消息处理和审计记录。
```
This commit is contained in:
2026-06-22 14:54:20 +08:00
parent 71ca804764
commit 09933b15ad
20 changed files with 177 additions and 869 deletions

View File

@@ -1,40 +0,0 @@
APP_NAME=Company AI Management Platform
APP_ENV=local
DEBUG=true
API_PREFIX=/api/v1
API_KEY=change-me
# Main application database. For production, point this to your service database.
DATABASE_URL=mysql+pymysql://root:password@127.0.0.1:3306/company_ai?charset=utf8mb4
# Existing project management system database. Keep this read-only at first.
LEGACY_DATABASE_URL=mysql+pymysql://readonly_user:readonly_password@127.0.0.1:3306/existing_project_system?charset=utf8mb4
LEGACY_PROJECT_QUERY=SELECT id, name, owner, status, progress, start_date, due_date, budget, actual_cost FROM projects ORDER BY id DESC LIMIT :limit
LEGACY_PROJECT_CODE_PREFIX=LEGACY
REDIS_URL=redis://127.0.0.1:6379/0
# Feishu / Lark Open Platform.
FEISHU_BASE_URL=https://open.feishu.cn/open-apis
FEISHU_APP_ID=
FEISHU_APP_SECRET=
FEISHU_VERIFICATION_TOKEN=
FEISHU_ENCRYPT_KEY=
FEISHU_DEFAULT_CHAT_ID=
# AI provider: openclaw, hermes, direct_llm, noop.
MODEL_PROVIDER=noop
OPENCLAW_BASE_URL=http://127.0.0.1:18789
OPENCLAW_API_KEY=
HERMES_BASE_URL=http://127.0.0.1:8080
HERMES_API_KEY=
DIRECT_LLM_BASE_URL=https://api.openai.com/v1
DIRECT_LLM_API_KEY=
DIRECT_LLM_MODEL=gpt-4.1-mini
SCHEDULER_ENABLED=false
DAILY_BRIEF_CRON_HOUR=9
DAILY_BRIEF_CRON_MINUTE=0
WEEKLY_PROJECT_REPORT_DAY_OF_WEEK=mon
WEEKLY_PROJECT_REPORT_CRON_HOUR=9
WEEKLY_PROJECT_REPORT_CRON_MINUTE=30

11
.gitignore vendored Normal file
View File

@@ -0,0 +1,11 @@
.env
.venv/
__pycache__/
*.py[cod]
.pytest_cache/
# Local docs and agent instructions
/docs/
/read.md
/README.md
/AGENTS.md

View File

@@ -1,41 +0,0 @@
# Repository Guidelines
## Project Structure & Module Organization
This is a FastAPI modular monolith for company AI management workflows. Source lives in `app/`. Shared infrastructure is under `app/core/`; `app/api/router.py` wires modules under `/api/v1`. Business capabilities live in `app/modules/`: `business`, `legacy_mysql`, `feishu`, `ai_agent`, `reports`, `risk`, `approvals`, and `audit`. Database tools are in `app/tools/`. Tests live in `tests/`, scripts in `scripts/`, and notes in `docs/`.
## Modular Design & Technology Direction
All new work must start with modular design: define boundaries, services, schemas, and adapters before coding. Prefer modern, maintained patterns and tech aligned with FastAPI, SQLAlchemy 2, Pydantic Settings, and async-ready integrations. Use service layer, adapter, dependency injection, and ports/adapters for external systems. Add dependencies only with clear benefit.
## Build, Test, and Development Commands
Common commands:
```powershell
conda env create -f environment.yml
conda env update -f environment.yml --prune
conda run -n company-ai-platform python -m app.tools.init_db
conda run -n company-ai-platform uvicorn app.main:app --reload --host 0.0.0.0 --port 8010
conda run -n company-ai-platform python -m compileall app tests scripts
conda run -n company-ai-platform python scripts\verify_smoke.py
conda run -n company-ai-platform pytest -q
```
## Coding Style & Naming Conventions
Use Python 3.11, 4-space indentation, type hints, and concise service classes. Follow `models.py`, `schemas.py`, `service.py`, and `routes.py`. Keep routes thin and put business logic in services. Use snake_case for functions, variables, filenames; use PascalCase for SQLAlchemy models and Pydantic schemas. Ruff uses line length `100`.
Follow the Google Python Style Guide: group imports as standard library, third-party, local; write docstrings for non-trivial public APIs; prefer explicit exceptions and early returns; and keep functions focused. Use type annotations instead of type comments.
## Testing Guidelines
Tests use `pytest` and FastAPI `TestClient`. Name files `test_*.py` and functions `test_*`. Prefer temporary SQLite databases, as in `tests/test_smoke.py`, so tests do not require MySQL, Feishu, or external AI providers. Cover approval gates, audit-sensitive flows, and API responses for high-risk modules.
## Commit & Pull Request Guidelines
No Git history is available. Use short, imperative commits such as `Add approval audit test`. Pull requests should describe the change, list verification, mention config or migration impacts, and link related issues. Include screenshots only for API docs or visible UI changes.
## Security & Configuration Tips
Do not commit real `.env` files or secrets. Start from `.env.example`. Keep `LEGACY_DATABASE_URL` read-only, set `API_KEY` outside local-only testing, and configure Feishu verification tokens before exposing webhooks. High-risk `fund-accounts` and `performance-metrics` updates must require approved tickets.

311
README.md
View File

@@ -1,311 +0,0 @@
# Company AI Management Platform
这是一个公司全生命周期 AI 管理系统的后端工程,定位是:
```text
现有项目管理系统 / MySQL
+
FastAPI AI 集成层
+
飞书推送与审批入口
+
OpenClaw / Hermes / 模型适配接口
```
第一版已经实现:
- FastAPI 后端工程
- Conda 独立环境配置
- MySQL 主库连接
- 现有项目管理系统 MySQL 只读接入
- 业务台账 CRUD项目、任务、采购、费用、资金、制度、规范、绩效、供应商
- 飞书文本和卡片推送接口
- 飞书 Webhook 入口
- 飞书消息命令路由日报、周报、风险、AI 问答
- AI 适配器OpenClaw、Hermes、OpenAI-compatible、noop
- 经营晨报、项目周报
- 风险检测:逾期任务、延期项目、超预算项目、资金低于安全线
- 审批单:高风险资金/绩效更新必须通过审批 ticket
- 现有 MySQL 项目数据同步到内部台账,支持 dry-run 和字段映射
- AI 操作审计日志
- Docker Compose 本地运行模板
## 1. 技术栈
```text
Python 3.11
FastAPI
SQLAlchemy 2
MySQL / PyMySQL
Redis
APScheduler / Celery 预留
httpx
Pydantic Settings
```
## 2. Conda 环境
重新创建专用环境:
```powershell
cd C:\Users\20143\Documents\Codex\2026-06-21\new-chat\outputs\company-ai-platform
conda env remove -n company-ai-platform
conda env create -f environment.yml
conda activate company-ai-platform
```
更新已有环境:
```powershell
conda env update -f environment.yml --prune
```
也可以直接运行:
```powershell
.\scripts\recreate_conda_env.ps1
```
## 3. 配置
复制配置文件:
```powershell
Copy-Item .env.example .env
```
至少配置:
```text
DATABASE_URL=你的新业务库
LEGACY_DATABASE_URL=现有项目管理系统的只读 MySQL 账号
LEGACY_PROJECT_QUERY=从现有系统读取项目的 SELECT 查询
API_KEY=你的内部 API Key
```
飞书和模型可以后续再配置。未配置模型时,`MODEL_PROVIDER=noop` 会返回可预测占位结果,确保项目先能启动。
## 4. 初始化数据库
```powershell
python -m app.tools.init_db
```
## 5. 启动服务
```powershell
uvicorn app.main:app --reload --host 0.0.0.0 --port 8010
```
访问:
```text
http://127.0.0.1:8010/docs
```
带 API Key 请求时加 header
```text
X-API-Key: 你的 API_KEY
```
## 6. 关键接口
### 系统健康
```text
GET /api/v1/health
```
### 业务台账
```text
GET /api/v1/business/domains
GET /api/v1/business/projects
POST /api/v1/business/projects
PATCH /api/v1/business/projects/{id}
```
支持的 domain
```text
projects
tasks
procurements
expenses
fund-accounts
policies
standards
performance-metrics
suppliers
```
### 现有 MySQL
```text
GET /api/v1/integrations/mysql/health
GET /api/v1/integrations/mysql/tables
GET /api/v1/integrations/mysql/tables/{table_name}
POST /api/v1/integrations/mysql/query
GET /api/v1/integrations/mysql/projects
POST /api/v1/integrations/mysql/projects/sync
```
只允许 `SELECT`,默认第一阶段不写回原系统。
项目同步默认 `dry_run=true`,确认映射无误后再设置为 `false`
### 飞书
```text
POST /api/v1/integrations/feishu/send-text
POST /api/v1/integrations/feishu/send-card
POST /api/v1/integrations/feishu/webhook
POST /api/v1/integrations/feishu/commands/preview
```
飞书命令支持:
```text
日报 / 晨报 / 经营日报
周报 / 项目周报
风险 / 预警
问 xxx / AI xxx / 普通问题
```
### AI
```text
POST /api/v1/ai/ask
POST /api/v1/ai/draft-policy
POST /api/v1/ai/investment-research
```
### 审批
```text
POST /api/v1/approvals
GET /api/v1/approvals
GET /api/v1/approvals/{ticket_id}
POST /api/v1/approvals/{ticket_id}/approve
POST /api/v1/approvals/{ticket_id}/reject
```
`fund-accounts``performance-metrics` 更新需要已批准的 `approval_ticket_id`
### 报表
```text
GET /api/v1/reports/daily-brief
GET /api/v1/reports/project-weekly
POST /api/v1/reports/daily-brief/push
POST /api/v1/reports/project-weekly/push
```
### 风险
```text
GET /api/v1/risks/summary
GET /api/v1/risks/overdue-tasks
GET /api/v1/risks/delayed-projects
GET /api/v1/risks/over-budget-projects
GET /api/v1/risks/funds
```
## 7. 接入现有项目管理系统
第一阶段建议只读接入:
```text
现有项目系统 MySQL -> FastAPI 只读查询 -> 报表/风险 -> 飞书推送
```
不要一开始让 AI 直接改原系统数据库。
配置示例:
```text
LEGACY_DATABASE_URL=mysql+pymysql://readonly_user:password@10.0.0.10:3306/project_system?charset=utf8mb4
LEGACY_PROJECT_QUERY=SELECT id, name, owner, status, progress, start_date, due_date, budget, actual_cost FROM project ORDER BY id DESC LIMIT :limit
```
## 8. 接入 OpenClaw / Hermes / 模型
OpenClaw
```text
MODEL_PROVIDER=openclaw
OPENCLAW_BASE_URL=http://127.0.0.1:18789
OPENCLAW_API_KEY=
```
Hermes
```text
MODEL_PROVIDER=hermes
HERMES_BASE_URL=http://127.0.0.1:8080
HERMES_API_KEY=
```
OpenAI-compatible
```text
MODEL_PROVIDER=direct_llm
DIRECT_LLM_BASE_URL=https://api.openai.com/v1
DIRECT_LLM_API_KEY=你的Key
DIRECT_LLM_MODEL=gpt-4.1-mini
```
## 9. 安全边界
已经内置的边界:
- 现有 MySQL 只允许 SELECT
- 高风险模块更新需要 `approval_ticket_id`
- 飞书和 AI 动作写入审计日志
- API 支持 `X-API-Key`
- AI 不直接审批付款、绩效、投资交易
后续建议补:
- 用户登录
- RBAC 角色权限
- 字段级脱敏
- 飞书审批回调
- 数据库迁移 Alembic
- 对接真实财务/采购系统 API
## 10. 下一步实施顺序
```text
1. 配置现有 MySQL 只读账号
2. 调通 /integrations/mysql/health
3. 调通 /integrations/mysql/projects
4. 初始化新业务库
5. 导入或同步项目数据
6. 配置飞书应用
7. 调通飞书 send-text/send-card
8. 配置 MODEL_PROVIDER
9. 开启日报、周报、风险推送
10. 再逐步做写回和审批闭环
```
## 11. 验证
编译:
```powershell
conda run -n company-ai-platform python -m compileall app tests scripts
```
Smoke 验证:
```powershell
conda run -n company-ai-platform python scripts\verify_smoke.py
```
测试:
```powershell
conda run -n company-ai-platform pytest -q
```

View File

@@ -0,0 +1,43 @@
from typing import Any
from sqlalchemy.orm import Session
from app.modules.audit.schemas import AuditLogCreate
from app.modules.feishu.commands import FeishuCommandService
from app.modules.feishu.service import FeishuService
class FeishuEventService:
"""Handle Feishu message events from webhook or long connection."""
def __init__(self, db: Session):
self.db = db
self.feishu = FeishuService(db)
self.commands = FeishuCommandService(db)
def handle_event(
self,
payload: dict[str, Any],
source: str,
auto_reply: bool = True,
) -> dict[str, Any]:
self.feishu.verify_event(payload)
self.feishu.audit.log(
AuditLogCreate(
actor="feishu",
source="feishu",
action=f"{source}_event",
request_payload=payload,
response_payload={"accepted": True},
)
)
command = self.commands.extract_event_command(payload)
if not command:
return {"ok": True, "handled": False}
result = self.commands.handle_text(
command["text"],
chat_id=command["chat_id"],
actor=command["actor"],
auto_reply=auto_reply,
)
return {"ok": True, "handled": True, "result": result}

View File

@@ -0,0 +1,79 @@
import json
import logging
from typing import Any
from urllib.parse import urlsplit
from app.core.config import get_settings
from app.core.database import SessionLocal
from app.modules.feishu.events import FeishuEventService
logger = logging.getLogger(__name__)
def _sdk_domain(base_url: str) -> str:
parsed = urlsplit(base_url)
if not parsed.scheme or not parsed.netloc:
return "https://open.feishu.cn"
return f"{parsed.scheme}://{parsed.netloc}"
def _sdk_event_to_payload(event: Any) -> dict[str, Any]:
try:
from lark_oapi.core.json import JSON
except ImportError as exc:
raise RuntimeError("lark-oapi is required for Feishu long connection") from exc
data = JSON.marshal(event)
if not data:
return {}
return json.loads(data)
def _handle_message_event(event: Any) -> None:
payload = _sdk_event_to_payload(event)
db = SessionLocal()
try:
result = FeishuEventService(db).handle_event(
payload,
source="long_connection",
auto_reply=True,
)
logger.info("Handled Feishu long connection event: %s", result)
finally:
db.close()
def run_long_connection() -> None:
"""Start the Feishu long connection client and block forever."""
settings = get_settings()
if not settings.feishu_app_id or not settings.feishu_app_secret:
raise RuntimeError("FEISHU_APP_ID and FEISHU_APP_SECRET are required")
try:
import lark_oapi as lark
except ImportError as exc:
raise RuntimeError("Install lark-oapi before starting Feishu long connection") from exc
event_handler = (
lark.EventDispatcherHandler.builder(
settings.feishu_encrypt_key or "",
settings.feishu_verification_token or "",
)
.register_p2_im_message_receive_v1(_handle_message_event)
.build()
)
client = lark.ws.Client(
app_id=settings.feishu_app_id,
app_secret=settings.feishu_app_secret,
event_handler=event_handler,
log_level=lark.LogLevel.WARNING,
domain=_sdk_domain(settings.feishu_base_url),
)
logger.info("Starting Feishu long connection client")
client.start()
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
run_long_connection()

View File

@@ -3,8 +3,8 @@ from sqlalchemy.orm import Session
from app.core.database import get_db
from app.core.security import require_api_key
from app.modules.audit.schemas import AuditLogCreate
from app.modules.feishu.commands import FeishuCommandService
from app.modules.feishu.events import FeishuEventService
from app.modules.feishu.schemas import (
FeishuCardMessage,
FeishuCommandRequest,
@@ -26,25 +26,7 @@ async def feishu_webhook(request: Request, db: Session = Depends(get_db)) -> dic
service.verify_event(payload)
if payload.get("challenge"):
return {"challenge": payload["challenge"]}
service.audit.log(
AuditLogCreate(
actor="feishu",
source="feishu",
action="webhook_event",
request_payload=payload,
response_payload={"accepted": True},
)
)
command = FeishuCommandService(db).extract_event_command(payload)
if not command:
return {"ok": True, "handled": False}
result = FeishuCommandService(db).handle_text(
command["text"],
chat_id=command["chat_id"],
actor=command["actor"],
auto_reply=True,
)
return {"ok": True, "handled": True, "result": result}
return FeishuEventService(db).handle_event(payload, source="webhook", auto_reply=True)
@router.post("/send-text", response_model=FeishuSendResult, dependencies=[Depends(require_api_key)])

View File

@@ -20,7 +20,8 @@ class FeishuService:
def verify_event(self, payload: dict[str, Any]) -> None:
settings = get_settings()
expected = settings.feishu_verification_token
token = payload.get("token")
header = payload.get("header") or {}
token = payload.get("token") or header.get("token")
if expected and token and token != expected:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,

BIN
company_ai_local.db Normal file

Binary file not shown.

View File

@@ -1,73 +0,0 @@
# AI 接入说明
## Provider 选择
```text
MODEL_PROVIDER=noop
MODEL_PROVIDER=openclaw
MODEL_PROVIDER=hermes
MODEL_PROVIDER=direct_llm
```
## 统一接口
业务层只调用:
```text
AIService.ask(prompt, context)
```
所以后续切换 OpenClaw、Hermes 或 OpenAI-compatible 模型时,不需要改业务模块。
## OpenClaw
```text
MODEL_PROVIDER=openclaw
OPENCLAW_BASE_URL=http://127.0.0.1:18789
OPENCLAW_API_KEY=
```
适合:
- 飞书/桌面/手机协同
- 工具调用
- 执行网关
## Hermes
```text
MODEL_PROVIDER=hermes
HERMES_BASE_URL=http://127.0.0.1:8080
HERMES_API_KEY=
```
适合:
- 长期记忆
- 技能沉淀
- 自学习
## Direct LLM
```text
MODEL_PROVIDER=direct_llm
DIRECT_LLM_BASE_URL=https://api.openai.com/v1
DIRECT_LLM_API_KEY=
DIRECT_LLM_MODEL=gpt-4.1-mini
```
## 安全原则
AI 可以:
- 查询数据
- 生成报告
- 生成草稿
- 推送提醒
AI 不可以自动:
- 审批付款
- 修改绩效最终分
- 删除业务数据
- 自动投资下单

View File

@@ -1,55 +0,0 @@
# 架构说明
## 总体架构
```text
用户 / 管理层 / 财务 / 项目负责人
|
v
飞书入口:群聊、私聊、卡片、审批、文档
|
v
FastAPI 集成层
|
|-- legacy_mysql读取现有项目管理系统 MySQL
|-- business内部业务台账
|-- reports日报、周报、经营报告
|-- risk延期、超预算、资金风险
|-- feishu飞书推送和 Webhook
|-- ai_agentOpenClaw / Hermes / 模型适配
|-- audit审计日志
|
v
MySQL / Redis / OpenClaw / Hermes / Feishu Open Platform
```
## 为什么先做模块化单体
当前最重要的是把流程跑通,而不是上来拆微服务。
模块化单体的好处:
- 开发和部署简单
- 业务边界清晰
- 后期可以按模块拆服务
- 适合已有 MySQL 系统的外挂式增强
后期可以拆分:
```text
ai-service
feishu-service
report-service
risk-service
finance-service
investment-service
```
## 数据边界
```text
现有项目系统 MySQL事实源第一阶段只读
新业务库 MySQLAI 中台自有数据、审计日志、补充台账
飞书:协同入口和消息入口
AI 记忆:偏好、经验、技能,不保存财务事实
```

View File

@@ -1,102 +0,0 @@
# 定制化流程
## 项目跟踪
```text
现有 MySQL 读取项目
-> 风险引擎检查延期/超预算
-> AI 生成项目摘要
-> 飞书项目群推送
-> 负责人确认
-> 必要时进入审批或整改
```
## 采购
```text
采购申请
-> 关联项目和预算
-> AI 检查重复采购、供应商风险、是否三方比价
-> 生成比价报告
-> 飞书审批
-> 下单/验收/付款
-> 审计归档
```
## 费用
```text
费用申请
-> 分类
-> 关联项目/部门/预算
-> 检查发票和凭证
-> 飞书审批
-> 财务付款
-> 月度分析
```
## 账户资金
```text
导入账户余额
-> 汇总应收应付
-> 计算安全线
-> 识别资金缺口
-> 飞书资金日报
```
## 制度和规范
```text
制度草案
-> AI 辅助起草
-> 管理层审批
-> 飞书 Wiki 发布
-> 签收
-> AI 抽取检查项
-> 执行检查
```
## 绩效
```text
指标定义
-> 绑定数据来源
-> 自动计算草稿
-> AI 解释
-> 部门负责人确认
-> 员工申诉
-> 管理层最终确认
```
AI 不能自动最终定绩效。
## 金融投资
```text
研究
-> 模拟交易
-> 风控验证
-> 人工审批
-> 半自动执行
```
第一版只做投资研究接口,不做真实交易。
## 审批闭环
```text
高风险动作
-> 创建审批单
-> 管理者批准或拒绝
-> 系统校验 approval_ticket_id
-> 执行业务更新
-> 写入审计日志
```
当前强制审批的模块:
```text
fund-accounts
performance-metrics
```

View File

@@ -1,84 +0,0 @@
# 飞书接入说明
## 飞书应用权限
建议先申请最小权限:
- 发送消息
- 读取群信息
- 接收消息事件
- 卡片消息
后续再逐步增加:
- 多维表格
- 审批
- 文档/Wiki
- 任务
- 日历
## 配置项
```text
FEISHU_APP_ID=
FEISHU_APP_SECRET=
FEISHU_VERIFICATION_TOKEN=
FEISHU_DEFAULT_CHAT_ID=
```
## 推送文本
```text
POST /api/v1/integrations/feishu/send-text
```
```json
{
"receive_id": "oc_xxx",
"receive_id_type": "chat_id",
"text": "项目日报测试"
}
```
## 推送卡片
```text
POST /api/v1/integrations/feishu/send-card
```
## Webhook
```text
POST /api/v1/integrations/feishu/webhook
```
当前 Webhook 已支持 challenge 验证和事件审计。后续可在这里接入:
- 飞书群聊问答
- 审批回调
- 卡片按钮回调
- 任务状态同步
## 消息命令
Webhook 已支持基础命令路由:
```text
日报 / 晨报 -> 每日经营晨报
周报 / 项目周报 -> 项目周报
风险 / 预警 -> 风险摘要
问 xxx / AI xxx -> AI 问答
```
本地预览:
```text
POST /api/v1/integrations/feishu/commands/preview
```
```json
{
"text": "日报",
"auto_reply": false
}
```

View File

@@ -1,94 +0,0 @@
# 现有 MySQL 接入说明
## 推荐方式
先创建只读账号:
```sql
CREATE USER 'company_ai_ro'@'%' IDENTIFIED BY 'strong_password';
GRANT SELECT ON existing_project_system.* TO 'company_ai_ro'@'%';
FLUSH PRIVILEGES;
```
配置:
```text
LEGACY_DATABASE_URL=mysql+pymysql://company_ai_ro:strong_password@host:3306/existing_project_system?charset=utf8mb4
```
## 表结构探查
```text
GET /api/v1/integrations/mysql/tables
GET /api/v1/integrations/mysql/tables/{table_name}
```
## 只读查询
```json
{
"sql": "SELECT id, name, status FROM projects WHERE status != :status",
"params": {"status": "closed"},
"limit": 100
}
```
系统会拒绝:
```text
INSERT
UPDATE
DELETE
DROP
ALTER
TRUNCATE
CREATE
```
## 项目同步
预览同步:
```text
POST /api/v1/integrations/mysql/projects/sync
```
```json
{
"dry_run": true,
"limit": 100,
"field_map": {
"name": "project_name",
"owner": "manager_name",
"status": "project_status",
"progress_percent": "progress",
"budget_amount": "budget",
"actual_amount": "actual_cost"
}
}
```
确认无误后正式同步:
```json
{
"dry_run": false,
"limit": 100,
"actor": "admin"
}
```
同步只写入本系统内部 `projects` 表,不写回原项目管理系统。
## 写回策略
第一阶段不要写回。
第二阶段如果必须写回,优先走原项目管理系统 API没有 API 时,再做受控写回,并必须包含:
- 权限校验
- 参数校验
- 审批单号
- 事务
- 审计日志
- 回滚方案

View File

@@ -12,6 +12,7 @@ dependencies:
- pydantic-settings==2.7.1
- python-dotenv==1.0.1
- httpx==0.28.1
- lark-oapi==1.6.8
- apscheduler==3.10.4
- redis==5.2.1
- celery==5.4.0

View File

@@ -0,0 +1,14 @@
INFO:__main__:Starting Feishu long connection client
INFO:httpx:HTTP Request: POST https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal "HTTP/1.1 200 OK"
INFO:httpx:HTTP Request: POST https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=chat_id "HTTP/1.1 200 OK"
INFO:__main__:Handled Feishu long connection event: {'ok': True, 'handled': True, 'result': {'command': 'risk_summary', 'reply_type': 'card', 'title': '<27><><EFBFBD><EFBFBD>Ԥ<EFBFBD><D4A4>', 'content': '- <20>ۺϷ<DBBA><CFB7>յȼ<D5B5><C8BC><EFBFBD>low\n- <20><><EFBFBD>շ֣<D5B7>0\n- <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>0\n- <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ŀ<EFBFBD><C4BF>0\n- <20><>Ԥ<EFBFBD><D4A4><EFBFBD><EFBFBD>Ŀ<EFBFBD><C4BF>0\n- <20>ʽ<EFBFBD><CABD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>˻<EFBFBD><CBBB><EFBFBD>0', 'lines': ['- <20>ۺϷ<DBBA><CFB7>յȼ<D5B5><C8BC><EFBFBD>low', '- <20><><EFBFBD>շ֣<D5B7>0', '- <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>0', '- <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ŀ<EFBFBD><C4BF>0', '- <20><>Ԥ<EFBFBD><D4A4><EFBFBD><EFBFBD>Ŀ<EFBFBD><C4BF>0', '- <20>ʽ<EFBFBD><CABD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>˻<EFBFBD><CBBB><EFBFBD>0'], 'provider_response': {'code': 0, 'data': {'body': {'content': '{"title":"<22><><EFBFBD><EFBFBD>Ԥ<EFBFBD><D4A4>","elements":[[{"tag":"text","text":"- <20>ۺϷ<DBBA><CFB7>յȼ<D5B5><C8BC><EFBFBD>low\\n- <20><><EFBFBD>շ֣<D5B7>0\\n- <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>0\\n- <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ŀ<EFBFBD><C4BF>0\\n- <20><>Ԥ<EFBFBD><D4A4><EFBFBD><EFBFBD>Ŀ<EFBFBD><C4BF>0\\n- <20>ʽ<EFBFBD><CABD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>˻<EFBFBD><CBBB><EFBFBD>0"}]]}'}, 'chat_id': 'oc_4ea0ed9fbfe4da2aca67cca9ee22cb36', 'create_time': '1782103535265', 'deleted': False, 'message_id': 'om_x100b6cbf902318acb15878ba4b86f90', 'msg_type': 'interactive', 'sender': {'id': 'cli_aab22b1674799bef', 'id_type': 'app_id', 'sender_type': 'app', 'tenant_key': '1b3485e1c8ea174f'}, 'update_time': '1782103535265', 'updated': False}, 'msg': 'success'}}}
INFO:httpx:HTTP Request: POST https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal "HTTP/1.1 200 OK"
INFO:httpx:HTTP Request: POST https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=chat_id "HTTP/1.1 200 OK"
INFO:__main__:Handled Feishu long connection event: {'ok': True, 'handled': True, 'result': {'command': 'risk_summary', 'reply_type': 'card', 'title': '<27><><EFBFBD><EFBFBD>Ԥ<EFBFBD><D4A4>', 'content': '- <20>ۺϷ<DBBA><CFB7>յȼ<D5B5><C8BC><EFBFBD>low\n- <20><><EFBFBD>շ֣<D5B7>0\n- <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>0\n- <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ŀ<EFBFBD><C4BF>0\n- <20><>Ԥ<EFBFBD><D4A4><EFBFBD><EFBFBD>Ŀ<EFBFBD><C4BF>0\n- <20>ʽ<EFBFBD><CABD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>˻<EFBFBD><CBBB><EFBFBD>0', 'lines': ['- <20>ۺϷ<DBBA><CFB7>յȼ<D5B5><C8BC><EFBFBD>low', '- <20><><EFBFBD>շ֣<D5B7>0', '- <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>0', '- <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ŀ<EFBFBD><C4BF>0', '- <20><>Ԥ<EFBFBD><D4A4><EFBFBD><EFBFBD>Ŀ<EFBFBD><C4BF>0', '- <20>ʽ<EFBFBD><CABD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>˻<EFBFBD><CBBB><EFBFBD>0'], 'provider_response': {'code': 0, 'data': {'body': {'content': '{"title":"<22><><EFBFBD><EFBFBD>Ԥ<EFBFBD><D4A4>","elements":[[{"tag":"text","text":"- <20>ۺϷ<DBBA><CFB7>յȼ<D5B5><C8BC><EFBFBD>low\\n- <20><><EFBFBD>շ֣<D5B7>0\\n- <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>0\\n- <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ŀ<EFBFBD><C4BF>0\\n- <20><>Ԥ<EFBFBD><D4A4><EFBFBD><EFBFBD>Ŀ<EFBFBD><C4BF>0\\n- <20>ʽ<EFBFBD><CABD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>˻<EFBFBD><CBBB><EFBFBD>0"}]]}'}, 'chat_id': 'oc_4ea0ed9fbfe4da2aca67cca9ee22cb36', 'create_time': '1782103549102', 'deleted': False, 'message_id': 'om_x100b6cbf91060cacb1fa837ac0932ee', 'msg_type': 'interactive', 'sender': {'id': 'cli_aab22b1674799bef', 'id_type': 'app_id', 'sender_type': 'app', 'tenant_key': '1b3485e1c8ea174f'}, 'update_time': '1782103549102', 'updated': False}, 'msg': 'success'}}}
INFO:httpx:HTTP Request: POST https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal "HTTP/1.1 200 OK"
INFO:httpx:HTTP Request: POST https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=chat_id "HTTP/1.1 200 OK"
INFO:__main__:Handled Feishu long connection event: {'ok': True, 'handled': True, 'result': {'command': 'fallback_ai', 'reply_type': 'text', 'title': 'AI <20>ظ<EFBFBD>', 'content': 'AI provider is not configured yet. This is a deterministic placeholder. Set MODEL_PROVIDER to openclaw, hermes, or direct_llm after credentials are ready.', 'provider_response': {'code': 0, 'data': {'body': {'content': '{"text":"AI provider is not configured yet. This is a deterministic placeholder. Set MODEL_PROVIDER to openclaw, hermes, or direct_llm after credentials are ready."}'}, 'chat_id': 'oc_4ea0ed9fbfe4da2aca67cca9ee22cb36', 'create_time': '1782103556285', 'deleted': False, 'message_id': 'om_x100b6cbfae8fc8acb03feca923be229', 'msg_type': 'text', 'sender': {'id': 'cli_aab22b1674799bef', 'id_type': 'app_id', 'sender_type': 'app', 'tenant_key': '1b3485e1c8ea174f'}, 'update_time': '1782103556285', 'updated': False}, 'msg': 'success'}}}
INFO:httpx:HTTP Request: POST https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal "HTTP/1.1 200 OK"
INFO:httpx:HTTP Request: POST https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=chat_id "HTTP/1.1 200 OK"
INFO:__main__:Handled Feishu long connection event: {'ok': True, 'handled': True, 'result': {'command': 'risk_summary', 'reply_type': 'card', 'title': '<27><><EFBFBD><EFBFBD>Ԥ<EFBFBD><D4A4>', 'content': '- <20>ۺϷ<DBBA><CFB7>յȼ<D5B5><C8BC><EFBFBD>low\n- <20><><EFBFBD>շ֣<D5B7>0\n- <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>0\n- <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ŀ<EFBFBD><C4BF>0\n- <20><>Ԥ<EFBFBD><D4A4><EFBFBD><EFBFBD>Ŀ<EFBFBD><C4BF>0\n- <20>ʽ<EFBFBD><CABD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>˻<EFBFBD><CBBB><EFBFBD>0', 'lines': ['- <20>ۺϷ<DBBA><CFB7>յȼ<D5B5><C8BC><EFBFBD>low', '- <20><><EFBFBD>շ֣<D5B7>0', '- <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>0', '- <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ŀ<EFBFBD><C4BF>0', '- <20><>Ԥ<EFBFBD><D4A4><EFBFBD><EFBFBD>Ŀ<EFBFBD><C4BF>0', '- <20>ʽ<EFBFBD><CABD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>˻<EFBFBD><CBBB><EFBFBD>0'], 'provider_response': {'code': 0, 'data': {'body': {'content': '{"title":"<22><><EFBFBD><EFBFBD>Ԥ<EFBFBD><D4A4>","elements":[[{"tag":"text","text":"- <20>ۺϷ<DBBA><CFB7>յȼ<D5B5><C8BC><EFBFBD>low\\n- <20><><EFBFBD>շ֣<D5B7>0\\n- <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>0\\n- <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ŀ<EFBFBD><C4BF>0\\n- <20><>Ԥ<EFBFBD><D4A4><EFBFBD><EFBFBD>Ŀ<EFBFBD><C4BF>0\\n- <20>ʽ<EFBFBD><CABD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>˻<EFBFBD><CBBB><EFBFBD>0"}]]}'}, 'chat_id': 'oc_4ea0ed9fbfe4da2aca67cca9ee22cb36', 'create_time': '1782105193432', 'deleted': False, 'message_id': 'om_x100b6cb8085c94a4b12d99465abb1ca', 'msg_type': 'interactive', 'sender': {'id': 'cli_aab22b1674799bef', 'id_type': 'app_id', 'sender_type': 'app', 'tenant_key': '1b3485e1c8ea174f'}, 'update_time': '1782105193432', 'updated': False}, 'msg': 'success'}}}
ERROR:Lark:receive message loop exit, err: no close frame received or sent [conn_id=7654075862284225524]

View File

@@ -0,0 +1 @@
[Lark] [2026-06-22 13:36:22,942] [ERROR] receive message loop exit, err: no close frame received or sent [conn_id=7654075862284225524]

48
read.md
View File

@@ -1,48 +0,0 @@
# 项目位置与启动说明
## 项目位置
当前项目已经移动到:
```text
D:\Python\AI\company-ai-platform
```
原来的临时输出目录已经不再使用:
```text
C:\Users\20143\Documents\Codex\2026-06-21\new-chat\outputs\company-ai-platform
```
## Conda 环境
Conda 环境名:
```text
company-ai-platform
```
## 启动命令
在 PowerShell 中执行:
```powershell
cd D:\Python\AI\company-ai-platform
conda activate company-ai-platform
uvicorn app.main:app --reload --host 0.0.0.0 --port 8010
```
启动后访问接口文档:
```text
http://127.0.0.1:8010/docs
```
## 常用验证命令
```powershell
conda run -n company-ai-platform python -m compileall app tests scripts
conda run -n company-ai-platform python scripts\verify_smoke.py
conda run -n company-ai-platform pytest -q
```

View File

@@ -0,0 +1 @@
conda run -n company-ai-platform python -m app.modules.feishu.long_connection

View File

@@ -1,3 +1,4 @@
import json
import os
import tempfile
from pathlib import Path
@@ -7,6 +8,8 @@ _db.close()
os.environ["DATABASE_URL"] = "sqlite:///" + _db.name.replace("\\", "/")
os.environ["API_KEY"] = "test-key"
os.environ["FEISHU_APP_ID"] = ""
os.environ["FEISHU_APP_SECRET"] = ""
os.environ["MODEL_PROVIDER"] = "noop"
os.environ["SCHEDULER_ENABLED"] = "false"
@@ -60,6 +63,26 @@ def test_project_report_and_feishu_command_preview() -> None:
assert response.json()["command"] == "daily_brief"
def test_feishu_webhook_routes_message_event() -> None:
payload = {
"schema": "2.0",
"header": {"event_type": "im.message.receive_v1"},
"event": {
"sender": {"sender_id": {"open_id": "ou_test"}},
"message": {
"chat_id": "oc_test",
"message_type": "text",
"content": json.dumps({"text": "risk"}),
},
},
}
response = client.post("/api/v1/integrations/feishu/webhook", json=payload)
assert response.status_code == 200
data = response.json()
assert data["handled"] is True
assert data["result"]["command"] == "risk_summary"
def test_approval_gate_for_high_risk_update() -> None:
create_response = client.post(
"/api/v1/business/fund-accounts",