```
feat(feishu): 添加飞书长连接支持并重构事件处理 添加了 FeishuEventService 来统一处理飞书消息事件, 新增 long_connection.py 实现长连接客户端, 修改 webhook 路由使用新的事件处理服务, 添加了 lark-oapi 依赖支持长连接功能, 更新测试用例覆盖新的事件处理逻辑。 BREAKING CHANGE: 飞书事件处理逻辑重构,统一使用 FeishuEventService 进行消息处理和审计记录。 ```
This commit is contained in:
40
.env.example
40
.env.example
@@ -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
11
.gitignore
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
.env
|
||||||
|
.venv/
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
|
# Local docs and agent instructions
|
||||||
|
/docs/
|
||||||
|
/read.md
|
||||||
|
/README.md
|
||||||
|
/AGENTS.md
|
||||||
41
AGENTS.md
41
AGENTS.md
@@ -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
311
README.md
@@ -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
|
|
||||||
```
|
|
||||||
43
app/modules/feishu/events.py
Normal file
43
app/modules/feishu/events.py
Normal 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}
|
||||||
79
app/modules/feishu/long_connection.py
Normal file
79
app/modules/feishu/long_connection.py
Normal 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()
|
||||||
@@ -3,8 +3,8 @@ from sqlalchemy.orm import Session
|
|||||||
|
|
||||||
from app.core.database import get_db
|
from app.core.database import get_db
|
||||||
from app.core.security import require_api_key
|
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.commands import FeishuCommandService
|
||||||
|
from app.modules.feishu.events import FeishuEventService
|
||||||
from app.modules.feishu.schemas import (
|
from app.modules.feishu.schemas import (
|
||||||
FeishuCardMessage,
|
FeishuCardMessage,
|
||||||
FeishuCommandRequest,
|
FeishuCommandRequest,
|
||||||
@@ -26,25 +26,7 @@ async def feishu_webhook(request: Request, db: Session = Depends(get_db)) -> dic
|
|||||||
service.verify_event(payload)
|
service.verify_event(payload)
|
||||||
if payload.get("challenge"):
|
if payload.get("challenge"):
|
||||||
return {"challenge": payload["challenge"]}
|
return {"challenge": payload["challenge"]}
|
||||||
service.audit.log(
|
return FeishuEventService(db).handle_event(payload, source="webhook", auto_reply=True)
|
||||||
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}
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/send-text", response_model=FeishuSendResult, dependencies=[Depends(require_api_key)])
|
@router.post("/send-text", response_model=FeishuSendResult, dependencies=[Depends(require_api_key)])
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ class FeishuService:
|
|||||||
def verify_event(self, payload: dict[str, Any]) -> None:
|
def verify_event(self, payload: dict[str, Any]) -> None:
|
||||||
settings = get_settings()
|
settings = get_settings()
|
||||||
expected = settings.feishu_verification_token
|
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:
|
if expected and token and token != expected:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
|||||||
BIN
company_ai_local.db
Normal file
BIN
company_ai_local.db
Normal file
Binary file not shown.
@@ -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 不可以自动:
|
|
||||||
|
|
||||||
- 审批付款
|
|
||||||
- 修改绩效最终分
|
|
||||||
- 删除业务数据
|
|
||||||
- 自动投资下单
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
# 架构说明
|
|
||||||
|
|
||||||
## 总体架构
|
|
||||||
|
|
||||||
```text
|
|
||||||
用户 / 管理层 / 财务 / 项目负责人
|
|
||||||
|
|
|
||||||
v
|
|
||||||
飞书入口:群聊、私聊、卡片、审批、文档
|
|
||||||
|
|
|
||||||
v
|
|
||||||
FastAPI 集成层
|
|
||||||
|
|
|
||||||
|-- legacy_mysql:读取现有项目管理系统 MySQL
|
|
||||||
|-- business:内部业务台账
|
|
||||||
|-- reports:日报、周报、经营报告
|
|
||||||
|-- risk:延期、超预算、资金风险
|
|
||||||
|-- feishu:飞书推送和 Webhook
|
|
||||||
|-- ai_agent:OpenClaw / 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:事实源,第一阶段只读
|
|
||||||
新业务库 MySQL:AI 中台自有数据、审计日志、补充台账
|
|
||||||
飞书:协同入口和消息入口
|
|
||||||
AI 记忆:偏好、经验、技能,不保存财务事实
|
|
||||||
```
|
|
||||||
@@ -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
|
|
||||||
```
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -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 时,再做受控写回,并必须包含:
|
|
||||||
|
|
||||||
- 权限校验
|
|
||||||
- 参数校验
|
|
||||||
- 审批单号
|
|
||||||
- 事务
|
|
||||||
- 审计日志
|
|
||||||
- 回滚方案
|
|
||||||
@@ -12,6 +12,7 @@ dependencies:
|
|||||||
- pydantic-settings==2.7.1
|
- pydantic-settings==2.7.1
|
||||||
- python-dotenv==1.0.1
|
- python-dotenv==1.0.1
|
||||||
- httpx==0.28.1
|
- httpx==0.28.1
|
||||||
|
- lark-oapi==1.6.8
|
||||||
- apscheduler==3.10.4
|
- apscheduler==3.10.4
|
||||||
- redis==5.2.1
|
- redis==5.2.1
|
||||||
- celery==5.4.0
|
- celery==5.4.0
|
||||||
|
|||||||
14
logs/feishu_long_connection.err.log
Normal file
14
logs/feishu_long_connection.err.log
Normal 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]
|
||||||
1
logs/feishu_long_connection.out.log
Normal file
1
logs/feishu_long_connection.out.log
Normal 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
48
read.md
@@ -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
|
|
||||||
```
|
|
||||||
|
|
||||||
1
scripts/run_feishu_long_connection.ps1
Normal file
1
scripts/run_feishu_long_connection.ps1
Normal file
@@ -0,0 +1 @@
|
|||||||
|
conda run -n company-ai-platform python -m app.modules.feishu.long_connection
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -7,6 +8,8 @@ _db.close()
|
|||||||
|
|
||||||
os.environ["DATABASE_URL"] = "sqlite:///" + _db.name.replace("\\", "/")
|
os.environ["DATABASE_URL"] = "sqlite:///" + _db.name.replace("\\", "/")
|
||||||
os.environ["API_KEY"] = "test-key"
|
os.environ["API_KEY"] = "test-key"
|
||||||
|
os.environ["FEISHU_APP_ID"] = ""
|
||||||
|
os.environ["FEISHU_APP_SECRET"] = ""
|
||||||
os.environ["MODEL_PROVIDER"] = "noop"
|
os.environ["MODEL_PROVIDER"] = "noop"
|
||||||
os.environ["SCHEDULER_ENABLED"] = "false"
|
os.environ["SCHEDULER_ENABLED"] = "false"
|
||||||
|
|
||||||
@@ -60,6 +63,26 @@ def test_project_report_and_feishu_command_preview() -> None:
|
|||||||
assert response.json()["command"] == "daily_brief"
|
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:
|
def test_approval_gate_for_high_risk_update() -> None:
|
||||||
create_response = client.post(
|
create_response = client.post(
|
||||||
"/api/v1/business/fund-accounts",
|
"/api/v1/business/fund-accounts",
|
||||||
|
|||||||
Reference in New Issue
Block a user