diff --git a/.env.example b/.env.example deleted file mode 100644 index 760b464..0000000 --- a/.env.example +++ /dev/null @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..08c54ff --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +.env +.venv/ +__pycache__/ +*.py[cod] +.pytest_cache/ + +# Local docs and agent instructions +/docs/ +/read.md +/README.md +/AGENTS.md diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index 42ab329..0000000 --- a/AGENTS.md +++ /dev/null @@ -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. diff --git a/README.md b/README.md deleted file mode 100644 index 8b874b0..0000000 --- a/README.md +++ /dev/null @@ -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 -``` diff --git a/app/modules/feishu/events.py b/app/modules/feishu/events.py new file mode 100644 index 0000000..73f70fd --- /dev/null +++ b/app/modules/feishu/events.py @@ -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} diff --git a/app/modules/feishu/long_connection.py b/app/modules/feishu/long_connection.py new file mode 100644 index 0000000..55c4aa6 --- /dev/null +++ b/app/modules/feishu/long_connection.py @@ -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() diff --git a/app/modules/feishu/routes.py b/app/modules/feishu/routes.py index e0ef34a..71b11d8 100644 --- a/app/modules/feishu/routes.py +++ b/app/modules/feishu/routes.py @@ -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)]) diff --git a/app/modules/feishu/service.py b/app/modules/feishu/service.py index 5816370..27078e6 100644 --- a/app/modules/feishu/service.py +++ b/app/modules/feishu/service.py @@ -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, diff --git a/company_ai_local.db b/company_ai_local.db new file mode 100644 index 0000000..3eb727a Binary files /dev/null and b/company_ai_local.db differ diff --git a/docs/ai_integration.md b/docs/ai_integration.md deleted file mode 100644 index 8b7754e..0000000 --- a/docs/ai_integration.md +++ /dev/null @@ -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 不可以自动: - -- 审批付款 -- 修改绩效最终分 -- 删除业务数据 -- 自动投资下单 diff --git a/docs/architecture.md b/docs/architecture.md deleted file mode 100644 index 33654df..0000000 --- a/docs/architecture.md +++ /dev/null @@ -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 记忆:偏好、经验、技能,不保存财务事实 -``` diff --git a/docs/custom_flows.md b/docs/custom_flows.md deleted file mode 100644 index ad39789..0000000 --- a/docs/custom_flows.md +++ /dev/null @@ -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 -``` diff --git a/docs/feishu_integration.md b/docs/feishu_integration.md deleted file mode 100644 index aa314a9..0000000 --- a/docs/feishu_integration.md +++ /dev/null @@ -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 -} -``` diff --git a/docs/mysql_integration.md b/docs/mysql_integration.md deleted file mode 100644 index 2b4efdb..0000000 --- a/docs/mysql_integration.md +++ /dev/null @@ -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 时,再做受控写回,并必须包含: - -- 权限校验 -- 参数校验 -- 审批单号 -- 事务 -- 审计日志 -- 回滚方案 diff --git a/environment.yml b/environment.yml index 8a7ad59..14d9964 100644 --- a/environment.yml +++ b/environment.yml @@ -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 diff --git a/logs/feishu_long_connection.err.log b/logs/feishu_long_connection.err.log new file mode 100644 index 0000000..6b179a3 --- /dev/null +++ b/logs/feishu_long_connection.err.log @@ -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': 'Ԥ', 'content': '- ۺϷյȼlow\n- շ֣0\n- 0\n- Ŀ0\n- ԤĿ0\n- ʽ˻0', 'lines': ['- ۺϷյȼlow', '- շ֣0', '- 0', '- Ŀ0', '- ԤĿ0', '- ʽ˻0'], 'provider_response': {'code': 0, 'data': {'body': {'content': '{"title":"Ԥ","elements":[[{"tag":"text","text":"- ۺϷյȼlow\\n- շ֣0\\n- 0\\n- Ŀ0\\n- ԤĿ0\\n- ʽ˻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': 'Ԥ', 'content': '- ۺϷյȼlow\n- շ֣0\n- 0\n- Ŀ0\n- ԤĿ0\n- ʽ˻0', 'lines': ['- ۺϷյȼlow', '- շ֣0', '- 0', '- Ŀ0', '- ԤĿ0', '- ʽ˻0'], 'provider_response': {'code': 0, 'data': {'body': {'content': '{"title":"Ԥ","elements":[[{"tag":"text","text":"- ۺϷյȼlow\\n- շ֣0\\n- 0\\n- Ŀ0\\n- ԤĿ0\\n- ʽ˻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 ظ', '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': 'Ԥ', 'content': '- ۺϷյȼlow\n- շ֣0\n- 0\n- Ŀ0\n- ԤĿ0\n- ʽ˻0', 'lines': ['- ۺϷյȼlow', '- շ֣0', '- 0', '- Ŀ0', '- ԤĿ0', '- ʽ˻0'], 'provider_response': {'code': 0, 'data': {'body': {'content': '{"title":"Ԥ","elements":[[{"tag":"text","text":"- ۺϷյȼlow\\n- շ֣0\\n- 0\\n- Ŀ0\\n- ԤĿ0\\n- ʽ˻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] diff --git a/logs/feishu_long_connection.out.log b/logs/feishu_long_connection.out.log new file mode 100644 index 0000000..75207af --- /dev/null +++ b/logs/feishu_long_connection.out.log @@ -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] diff --git a/read.md b/read.md deleted file mode 100644 index 1644785..0000000 --- a/read.md +++ /dev/null @@ -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 -``` - diff --git a/scripts/run_feishu_long_connection.ps1 b/scripts/run_feishu_long_connection.ps1 new file mode 100644 index 0000000..dd0f05c --- /dev/null +++ b/scripts/run_feishu_long_connection.ps1 @@ -0,0 +1 @@ +conda run -n company-ai-platform python -m app.modules.feishu.long_connection diff --git a/tests/test_smoke.py b/tests/test_smoke.py index 1d680ce..f4a23b4 100644 --- a/tests/test_smoke.py +++ b/tests/test_smoke.py @@ -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",