From 09933b15ad8176ac49402969b96d9306c63fc34e Mon Sep 17 00:00:00 2001 From: JiuContinent Date: Mon, 22 Jun 2026 14:54:20 +0800 Subject: [PATCH] =?UTF-8?q?```=20feat(feishu):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E9=A3=9E=E4=B9=A6=E9=95=BF=E8=BF=9E=E6=8E=A5=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=B9=B6=E9=87=8D=E6=9E=84=E4=BA=8B=E4=BB=B6=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加了 FeishuEventService 来统一处理飞书消息事件, 新增 long_connection.py 实现长连接客户端, 修改 webhook 路由使用新的事件处理服务, 添加了 lark-oapi 依赖支持长连接功能, 更新测试用例覆盖新的事件处理逻辑。 BREAKING CHANGE: 飞书事件处理逻辑重构,统一使用 FeishuEventService 进行消息处理和审计记录。 ``` --- .env.example | 40 ---- .gitignore | 11 + AGENTS.md | 41 ---- README.md | 311 ------------------------- app/modules/feishu/events.py | 43 ++++ app/modules/feishu/long_connection.py | 79 +++++++ app/modules/feishu/routes.py | 22 +- app/modules/feishu/service.py | 3 +- company_ai_local.db | Bin 0 -> 303104 bytes docs/ai_integration.md | 73 ------ docs/architecture.md | 55 ----- docs/custom_flows.md | 102 -------- docs/feishu_integration.md | 84 ------- docs/mysql_integration.md | 94 -------- environment.yml | 1 + logs/feishu_long_connection.err.log | 14 ++ logs/feishu_long_connection.out.log | 1 + read.md | 48 ---- scripts/run_feishu_long_connection.ps1 | 1 + tests/test_smoke.py | 23 ++ 20 files changed, 177 insertions(+), 869 deletions(-) delete mode 100644 .env.example create mode 100644 .gitignore delete mode 100644 AGENTS.md delete mode 100644 README.md create mode 100644 app/modules/feishu/events.py create mode 100644 app/modules/feishu/long_connection.py create mode 100644 company_ai_local.db delete mode 100644 docs/ai_integration.md delete mode 100644 docs/architecture.md delete mode 100644 docs/custom_flows.md delete mode 100644 docs/feishu_integration.md delete mode 100644 docs/mysql_integration.md create mode 100644 logs/feishu_long_connection.err.log create mode 100644 logs/feishu_long_connection.out.log delete mode 100644 read.md create mode 100644 scripts/run_feishu_long_connection.ps1 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 0000000000000000000000000000000000000000..3eb727a82167f88cdde5ac49f09508f1d4436426 GIT binary patch literal 303104 zcmeI5Yiu0XmFK%jkxf#fT8jvUEbsjj+B zmD%0ZuBw*A5ws2?Tb6@hkogc8Gs(`tGYba+#sLQKcsAo8i^XE+!+Z#KKI{SuWP^Q- zBWo7e%zoG)y8{gN-dnG(>MoLEB-`Zv1xc*F=iYP9{oQlVeH1A@_v|UdAwt8nTB;)y zQwLJ%bm|L&kV>Ua(?7pM|MF}Gw$A^AA)EqjH{%(3P^~=;hB&pqg`RKt> z-gx=J(cICgbjxV#OHOjOIOMfLKc{K^BFTx}}7KGLVzGVE2QNiLG6uRfQk zKCBzH5tiq_eE(=poJx25?AVU#bZy_p1Bo`=dQtsuS^g{cjpoWz>8pLZ^)69#)d{%Y zpQzoh8P)HX=3lxulY5L}8t*efE<40(t4+nw{XX26Xr-re)KHHgf9_K}g0A$L(o8+* z$-SP9Gm8t)o*5KKta8)_E6Kll@}5j?YASu>oTJv8fjZyyPkCyW=bkt<-xGur1AQ@%&AjHgo$x(=J~m$6JMNLnyHlX5g`j*!52#9a#ZTZq%RgFInNY& z!|WHwRdy`Hvlb?0gkFT(R*qUW@9K&o2%kyWKIT5o&xj&P8{BU}G(w}UhP9+SIurRxUp|F13SSZOWlcTxf;q=-7do++tD>Su#15>b;?^7w@F9QG3xn`@Egbhfhu;;ePT9OzF- zPeyZUZ*w-;!%zuC@eF5?m%GGvl#aUAG*vyw6Ey2}%(hMX1hJGF*B6fE;Ox*p`(GK) zJik6NoXI^&$LVk~71sk|^?9;qOPxH$jH(5BPJ%^7`8 z3~%m)mu=XZ-amg`VgA}z(wW?`W9jRro6qAF`&tfpcD?K#ufIW(4`MRNr}0GY&G+6+ z97*IsCa$$qn~AXBVIw~EYLe~q9=;7`xezaDyUlAVSRu)pvN1V8`;KmY_l00ck) z1V8`;KmY_lV7C&OU>(Exf42@V`UL_Y00JNY0w4eaAOHd&00JNY0-ONO{|E#S009sH z0T2KI5C8!X009sH0T9^z1aSV}{bP(Cf&d7B00@8p2!H?xfB*=900@8p&i{x35C8!X z009sH0T2KI5C8!X009u#{RD9S-~D5Z9)bV}fB*=900@8p2!H?xfB*=90M7r20T2KI z5C8!X009sH0T2KI5C8!X*!=`>{@?v$j2?mj2!H?xfB*=900@8p2!H?xfB??_hyf4) z0T2KI5C8!X009sH0T2KI5ZL_$Mraq)*?(jm{T=${txsH!Zhz&plIo?BSZFkeVPELlq^&EOYUxe$(v@k8XvDZk z6hogrE=-%6QX*=B=(R?@K}xzRshV1;YMNRjM3S_6u`+!`=xL-lYaPN`Y8O<8DWOuP zn-*P}*35RpSmBSC44o*pW)ad>TBgpLI98WjrPVJGRVNm&mB>X**1C>ra30)*pQD)}Ork;X7|nUu9}YleDPNrqXjkd)>Im8gd0aYMZLH zsDpFjhlb<-Jk#P4=JYe?W2NQ`oWFsw5Y%fzWSq|edmw= z?)~>a`1)IZ*$3~xN6T)#^@E$={=Q%K;d`%9KW~2j%~fs`0m?3yYZK- z0qx6+SFbX3oKb-~ud9yAnbb{vjXzMXK8Gvkr#ZCq)5p(8pq!sRaz0=}rRUDEs_F`_ z$awGy?gMSQLErm3Kd|?#u&?{9a8Unw`9J{Yr?0;F;?-$3qJBtiJuYc{sSZ&bqs3!V ztjdyDpz&W53lfV1od$_RSa3GfrcG!`i`cfh5`=$Cxhxh6^@>(+&^DEVrfPMup~}Uo zsugNQi4dlyWv>KrZ_uHzRL!Ath>6(b@`Bl&QCibbRJATib+J+{Rcp06X|Rfh9;i?| z9dG8it$PKmDjjRJ?I^2+jsCP)FP7vo5jB~pVzt!ZsJa~*B^z|yl?NSXbZIA;I=icB zl)>~>??Ea^m7|5qQArXCrQ@aY@ltKJSQJI6cs)BjnL2o|N;A&aHSwW2H=DSBH_i5n zLc_96C<(Kzkc)IG^88|J7f6f6@3b^qV7=*??!15?L)5 z%A{6lNDY~TFjq;NsbU|J##$CjWhQr(`{~JO&3Z#U>u0=+BGrSn}M% zx=S6q(mVd%VDd1yGHrHf=JS&5=9!v#)@?JAUX!*Wi=wQRXo?qWa=loWRFy_*sakDR zRGG39c^t$7h~|203!UWQ;7$bIiH#-&9hQPB*Tqt)ESKntLK>3B$2Dm@s?{2oCxxgl z-^OfbOO8Ll9jU{5>bve24_G=_9_0CtrajBBS82wfS(oNqmCl5Yn;!VulsF^g*;272 z$+a8i5Y}L7$p@n2yL zE|zCYvQ&|a*Bfbn4gSVGcYF;l)YWpaPB$3kYOPwU({-g#CG~1qFIGgcMtB-`HeZ7! zx&ll5oPaf$F5`E44VFuiP6~2~ZUH1&t*dljAnO&*d+^+&;~|#0eDE3^%J0q^+%fI= z@hy@v`ANh()5K3I>>Q-jo2EvWX?mb(s_m67-LLag%rmFv78aG|`LoN>vkYI<&$s=P z#O*BY#nNn<9`zUYu=oGt+2>Q)Z)9&~|8@4wte*W(*+0)dpZ({bxSIet1Ogxc0w4ea zAOHd&00JNY0w4eapD+TG!-vxB6$O9&JbrMvnjZM#Xe_A5F6uKA!w1u^LpLFAx9$5C8!X z009sH0T2KI5C8!X*ntEd9-d40e$!|9xLBmWXDhXWRFKC9i>29$ToP;5vACFj%V@bW zTb2rCNy-csmuYKBl*^-u;!3Snl1B!LgYQS(Gg$0@bKdYkaqyiqLsP?Z!3m00ck)1V8`;KmY_l00cl_ zD+C@LelER*A1@zgFY4W&2OZmDlgt+DjfSd6w%BA(*kpK%O@_vY501MXaL@nmrLzAg z`@gccvj3HS_y1pKf0TVM`{S(uLmdzR0T2KI5C8!X009sH0T2KI5CDO@M<6qNFa3#v zOXJ?xG4Ct?34#{Ujc#kyD;n{>?(x13dtZk}hwr675pc;p|9>l${onNT|DUseoBem$ zA7tOjzLovf-SZxrK>!3m00ck)1V8`;KmY_l00ck)1So;M!w;psf0TE;=XtE>In(nz z+Vecp^Sr0$dAR3!Xkz%GaZkT{{{JKP{r~?X`(LvEA^SJu+ zQN6q59>y4gys1!-#?lYr_$X%JGP@bUE8pZgS#(kp$YG*b_Ha<6CO%;LhcX9fils~ok#O7gFsyeE^J zno8d|=cx5&pw4&wQ=Z!8xhGD|_XOd@xS`W%U7mk(eo0t-dRbUJbL!L)VPc${d46u` z#24q5W-6t8M94x{@P$&j9F@8;>5D~4&NGGHF#E-El^x44EyGz0)E1?vR@##li`cf( zA(lqk&OoojxJ=Yj@9HbWQPh^%rAijhoSt7=I58uZjtDV>s^)a*@NZs`_RMm4I0`4} zieo+$R3?T}XBZyZcd1O;_`rkX|QB+wPr;f9tM zPR~cisoPaC&{xpKPH{B@B|VFuQ@F zuzuWFD9J06qq*YY^x6P>G>}XyG_`*NQ@m>23NOr?`;vPU#zAOiasxAA{kW}AlCSSg z946`!4dl?)z71@-b>nv2()_i36PeuM!|B&Fp0z@a;nQAEx0|`ba$zPhcX42m$r70r zZWspg_=NKu_7fGGYmw%3wzCA{taZX1=ub#bMssR!b2iz-Pzglw3}=y-yTo>sj=I(~ zRXxZPH0yQDwoUp3v6LFu7mnrN?9e~^Um4HjP8>_G9}J?nL(}d>ngu=IY{jLc#>*%A zA|PJFYsmv4E)!;WISi$TZz7kN}wabr9hrv9~*H!dL-E+ zi)f~$5AtYqb38%g1Xlt%b!E?{uF$SF4NYx3eSIF;+?Aklf-8YMzdkaY$vsKO>2NX? z*8^hpd9r6qp3rGXtYk;HIRDDfrqjpG8GTL+Z|;PbZP=RLKYv|e{@Pd4ncT5s>FcJO z&*K&QS`K-3z3d*Zzd@1@Vlv35@kH*;_ufn#N#sE&1seP`zS$B$yWxgaq-8|8+fzDZ zz2*n(gX2k??&$2EJz{(<(1~W)bnR6%v(;j|V1F|iW{ttgezUD0Q>;g~ip<;oHG-Z0 zA4ru``_E7Qa`Medao=C>dwTEx9RIWN&y4+xv4=DNDsyb~-|TsJ&;0PeA6^;y<DTY)anb0u>84cE*cRN5d=Mk?5D{$<3%|l)#q%P%oW~sl6t zt<=?~+SW*TYN+&`8p!1iy%nG!X><|eB0Gz1sX86#&Fb1}gE(tS(`av~atk|+w&{>N z7CHSPa_(sKpv#!h{fkY5Rww?4f;ZCf)ybFh$}Hje1;PwJ=w3WRa~wUf2YVD>t^~=& zO-6qCv3P3p1@Q(^V=-;{)^E`LH?cpe`a^&?Do__ou`d@$D)9)@qe(84ejn_V!QTk31n_nvo zUSwEO4o9zVb?NPPAf)MxHS=yh>~5W1Tn18q(esDM%ZPn)qkYjd*nNe2?)KNUc&?1i zn?Oyxo^Ngwb-(Jm)}Ni4pUdRRkEGZ4`T!Xj*0|hk~94LA#(aGF2=*Di%quMK*iAQ~}Fy4^OwqCg~6Vsg6KTD7EkAycny|w*2 z>;S`~8`|dAe|-k|efHJQWODPgw^IY*QkyLFIImv&pWDz}Z>#=(_Db{X!_)LkK5 z_j)b$K7You8e9$saj@MSxbX!sb#uV>nWZS|RS+P(nS;sfE^9_C9;|_o-9B$2xv`|o zw`E(%yIZ~YjnNKVif6jmOiZk|)r@Gi=mx-`rv%0BwpyySb{DsD%Ax4B<|WfwRUFk` zwH1ENAAX*Whi)ixtFmwn%Y+&CJyrK%y8WORwZZlytm407dUNc_a?^~&L4|vCP=eJMz`Ty{H0`LR^AOHd&00JNY0w4eaAOHd&00O&}0M7rrb!5>m5C8!X z009sH0T2KI5C8!X009sP3E=!6vcMAvfB*=900@8p2!H?xfB*=900`_>0yzKg){#ZO zKmY_l00ck)1V8`;KmY_l00clFB!KgO$O2Cw00JNY0w4eaAOHd&00JNY0wAzk3E=#{ zTSpfC0s#;J0T2KI5C8!X009sH0T2LzkO2Gt|569S&v*g>5C8!X009sH0T2KI5C8!X z009u#eFX6SfA@_kdISO>00JNY0w4eaAOHd&00JNY0&xO3|Hp~oGYEhH2!H?xfB*=9 z00@8p2!H?x>^=fG|L?vrMUOxL1V8`;KmY_l00ck)1V8`;Kp;*4=l?hndWJ_0!Z@4hibk3awfKmY_l00ck)1V8`;KmY_l zAWi`1|2PqR1_2NN0T2KI5C8!X009sH0T2Lz-A4fD|J^sH=n)8j00@8p2!H?xfB*=9 z00@8p2*e5C{2wQR&maH-AOHd&00JNY0w4eaAOHd&u=@z${J;Cg6g>g~5C8!X009sH z0T2KI5C8!X0D(9G{QrM(BKQmfAOHd&00JNY0w4eaAOHd&00O&@0N(%azA;6QKmY_l z00ck)1V8`;KmY_l00clFPGEAJq#j89ZfflRj$IwwJNnY-{L1jdLq8f? z++Ub{dvbc;o2lR3_xRrbxYwBY`-wN=I=1)u+Owa}rG;JJDsLMEIW8O zabjtHZh2ljwP`qOimjOzp&p!`Us^aZ zQ$8X{QN=oG8W)MRdBu)pXk=4$mSL|dO>&Vm1CNR^i}j|etu_tYQEW$bx^`kk%_6kd zx}rM5NeX&-;q-ijShu5ZD1T;Y;q=_n3&KcY7(OmIRdad7_+x4K? zBnz++RrR5JbPxIhGt)Pcw|QV!HV+ds-rpaP`cZ1 zz%d*;DSUj7CHv1S#@y#+`LC^2_zZh(&YfYtvOw!`Ppdb>eED`~nCpo@|N5qwZ&q>E zI^j(8=b6fD9$2`5m7LUoO*uoeBq*8#jpU%}M`bEk3Tkz@NJ`X8a;y#s!i8Qi(?8RQhEn z(QVVkLDS4`+p+nuMoP*Ffe@(_L_?(13yCX*Ozs4=cyO@AaIK3pF2w^oB2~gxEC?eq zVI~yjjbm{qIMrT4_g6&!sod`-@ZK`c8-0X;zzTFPF+@Zlu_Y@Yf)X#-uAa!`o}_z|!~S>&6_ognDUduFAM-%c*M_gazL&yG zTxEXsiA?S>YJR-md>E90`rl4A9M+DR30DlF=PPbZTt__HRcE6ng2vp)7)G&K~{>yP@lI>c(2R*MCo(jpGsEW0Ub zK=~7%y}lJHK?UpH05x9RtfwD$T?w+(fV#ZH1Ip*(T^~?~w@viH#gX|<8oU%o5WIu* zxC_q!$rrZbg|}=avGDdD*G@l{$rX#~>+YU%fUyBZpZDy$`^*6q?ox6Kwv>ux(nRDh z*#4H2#Z$Mb221KCVys+nf_ydLET=lEVw+uyCP81Bze!cQju|_>$$jZjGusWrqFd8J zHMg?Wz2o~-y#L?c?FCwa00@8p2!H?xfB*=900@8p2!Oy&B7pP%P8u!v3IZSi0w4ea zAOHd&00JNY0w4ea+arK~|8INT&?8vC_y2a%Xu($y009sH0T2KI5C8!X009sH0T9?80lfd;9yhcC z0T2KI5C8!X009sH0T2KI5CDOlL;&aioitkT6$C&41V8`;KmY_l00ck)1V8`;wnqTx z|Lt)@D-Zwy5C8!X009sH0T2KI5C8!X*hvKN^Zz?(wBRcUfB*=900@8p2!H?xfB*=9 z00?Z40N(#^j~iNn00@8p2!H?xfB*=900@8p2!Oy&B7pP%P8u!v3IZSi0w4eaAOHd& z00JNY0w4ea+arMU|Ms|{6$pR;2!H?xfB*=900@8p2!H?x>?8s>|L>&Hg0CO|0w4ea zAOHd&00JNY0w4eaAh10GIR9^t8(M(?2!H?xfB*=900@8p2!H?xfWS^7!2bRJJ*l@+ z``#RTWaM7@@Jai1!`!n!_1wWjsnosKAA6(w(45&-%0;Or>xH6T)U{%aVcOJ`5>X38uQlooQqomP)znH= z)6^OvlBCs(mFXiwPb0-y>k!sbyP!Hu36(P4wCKvTX0{u~3V*z0=tQwKi;%X`GIiF( zvAX0ct$u;1I&8XakSpj>+f=PprR6#s>O3mkeE)|ZeDj8@`{p}u-2AKW z{``l3NHLvn9~Iov4{ltiMFn2))gS%rJAeFl@4x@S*WdEXK6w8q;BeK{DE@y zIb1nE&7qy2K7Kv|<^1%K^8phoJ$H^(RabaL#)DUIA86AJ`rhC9fxTyiecfk;gZj_Q z2Ld=hef7l`uTHZO^+RgwaY^G#b%^2^Egq9%RhGm8jsKchkXRh(G)Nr6g0rDEZ9+?0 z#J1IyApBd(WwB7GSG0PAwy6{}RjZ2)RW4RltxzjUgfKNNdnJf_gARqIY7U)4OvEOa z7tHRA(wc^%s&z@KiK=~%06M_DCo^ryvou_Tv? zsL4bXtEC1<)$PzI*`VXDJm@&1OFO~T*)GMS)WL&QnsL6ai4V=W*~I<3X|`7s8kTKBNtkVgT%=Qx=NDVMKw2z*r={5f z>rKye=e4}dQ!ePTTA@j%Mh8rk8kK65$ZEMzCbddKYRDXfxk}nh75k7h*0NYCGr6nW zPftc`)*I?sKjU2#xz3H@QbpeIP_qNalII@QUFz7C-tqSalZU~TX|qE!pO<7e&(zGb zZkv(xnzR*J6lJYMQ@mJ{>&3dHsx(qd)oP=n%9NeR;~*A5G}l{O=p+vZcOvjkY&0q8 zuoP6eE|yAVxkOhK(vUPhu1Vujt=70aDMWqwHfB3pa{K}ANFCNw-*vxuz|z6;AkTj^ z?OBGsN;3}4x-{pibS8A%^uX7q#2F#amWm}wuH7()um)od#v0taH%!O_#j;f4dHJ^1 z;DS^XWwr*F<*Hm)OLe)@aMQ_-S%ZU){|akxu{>LnrHWj<-bnjv@Hg(c<7;rCu9k~+ zy1^({Yt>qvt}BHqsaMN-u_B5!!qd33`5G+I6s zTB%Z2>vd66`4M4foZfpnzP+78`M6l5zh^78f>e;NXUF`V!`(Q&*9#4)P*Uk&NmaQ~ zFVMRaU975v)a&(%%&$f^-#O4OOT}7=4H9+^cYfzUCq=zdVv~YGR;!{atF?xv^1&In zbD-lP%C#aJkAXXf9{JtbIo#pXyE`MK*@|2eYt`33o$=S;Yp=Yq_jYg4nQwRN2A%ht zUO3Gy2p!A3Nbk-q!LWt4=?Lzfde4%|Va1w_x$>=5mGZzzbzb`r1pQlk^4G!Fy5% zPhb1eZOl&9m{sjnmA(Vejc)78G@U`M4u5HMK{Xjje$=3El<0jeI~({jj9o0ZO_N_E zs%`rczsCP0PeIxhOTENzY&Y&>;@y6^Omm?m%H`|(M}pmfddKe-+mM_}zYy-fio2<5now2;VXf m+#M9@coamQng;F;dgR;4slWQ&KAr8ieYMXfM1`Ig 风险引擎检查延期/超预算 - -> 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",