From 2fd932e39593c3777b0bb1898fee9432d812109a Mon Sep 17 00:00:00 2001 From: Yuuuu <1569752928@qq.com> Date: Wed, 11 Mar 2026 12:05:16 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=B6=E5=AE=9A=E5=8F=8CAgent=E8=83=BD?= =?UTF-8?q?=E5=8A=9B=E6=8F=90=E5=8D=87=E4=B8=8E=E8=B7=AF=E7=94=B1=E6=B2=BB?= =?UTF-8?q?=E7=90=86=E6=96=B9=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 43 ++++++--- .openclaw/pmflow-routing-rules.json | 16 ++-- CLAUDE.md | 7 ++ DEPLOY_TENCENT_CLOUD.md | 8 +- backend/.env.example | 96 ------------------- backend/.openclaw/pmflow-routing-rules.json | 74 -------------- backend/Dockerfile | 1 + backend/app/api/__init__.py | 2 + backend/app/api/llm_providers.py | 58 +++++++++++ backend/app/api/router.py | 2 + backend/app/config/ai_prompts.py | 3 +- backend/app/core/config.py | 3 +- backend/app/services/ai_pmo_kernel_service.py | 39 +++++++- backend/app/services/ai_service.py | 81 +++++++++++++++- backend/app/services/llm.py | 35 ++++--- backend/app/services/llm_client.py | 4 +- .../services/openclaw_commander_service.py | 6 +- backend/tests/test_ai_pmo_kernel_service.py | 49 +++++++++- backend/tests/test_llm_providers_api.py | 40 ++++++++ .../tests/test_openclaw_commander_service.py | 17 ++++ deploy.sh | 2 +- docker-compose.dev.yml | 60 ++++++------ docker-compose.yml | 60 ++++++------ frontend/src/api/aiSteward.ts | 19 ++++ frontend/src/pages/AIStewardPage.tsx | 58 +++++++++-- mdproject/docker-redis-context-plan-en.md | 2 +- scripts/openclaw_routing_lint.py | 37 ++++++- tests/test_llm_fail_fast.py | 2 +- 用户使用手册.md | 20 ++-- 29 files changed, 540 insertions(+), 304 deletions(-) delete mode 100644 backend/.env.example delete mode 100644 backend/.openclaw/pmflow-routing-rules.json create mode 100644 backend/app/api/llm_providers.py create mode 100644 backend/tests/test_llm_providers_api.py diff --git a/.env.example b/.env.example index bf87aa9a..eddafbe9 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,6 @@ +# 唯一配置文件,backend/.env 已移除 +# 所有环境变量在此统一管理 + # Docker Compose Runtime Secrets / Config POSTGRES_DB=pmflow POSTGRES_USER=pmflow @@ -8,37 +11,53 @@ DB_PORT=5435 REDIS_PORT=6379 BACKEND_PORT=8000 FRONTEND_PORT=5174 +PMFLOW_EXPECTED_DB_DIALECT=postgresql # Backend Runtime CORS_ORIGINS=http://localhost:5174,http://127.0.0.1:5174 LLM_PROVIDER=glm LLM_FALLBACK_PROVIDER=glm -OPENAI_BASE_URL=https://api.edgefn.net/v1 -OPENAI_API_KEY= -OPENAI_MODEL=MiniMax-M2.5 OPENCLAW_ROUTING_ENABLED=false -OPENCLAW_ROUTING_RULES_PATH=/app/backend/.openclaw/pmflow-routing-rules.json +OPENCLAW_ROUTING_RULES_PATH=../.openclaw/pmflow-routing-rules.json OPENCLAW_AGENTS_DEFAULT_TIMEOUT_SECONDS=120 +LLM_DIRECT_EMERGENCY_ENABLED=true LLM_MIXED_ROUTING_ENABLED=true +LLM_ROUTING_DEFAULT_TIER=active LLM_LOW_TO_HIGH_ESCALATION_ENABLED=true +LLM_REQUEST_TIMEOUT=90 + +# 模型名统一使用裸名,不带供应商前缀 +# GLM-4.7 配 GLM_API_KEY +# MiniMax-M2.5 配 OPENAI_API_KEY +GLM_API_KEY= +GLM_BASE_URL=https://api.edgefn.net/v1 +GLM_MODEL=GLM-4.7 +OPENAI_API_KEY= +OPENAI_BASE_URL=https://api.edgefn.net/v1 +OPENAI_MODEL=MiniMax-M2.5 + LLM_ROUTING_LOW_PROVIDER=glm LLM_ROUTING_LOW_MODEL=GLM-4.7 LLM_ROUTING_HIGH_PROVIDER=openai LLM_ROUTING_HIGH_MODEL=MiniMax-M2.5 LLM_ROUTING_SAFETY_PROVIDER=glm LLM_ROUTING_SAFETY_MODEL=GLM-4.7 -LLM_DIRECT_EMERGENCY_ENABLED=true + +DEEPSEEK_API_KEY= +DEEPSEEK_BASE_URL=https://api.deepseek.com/v1 +DEEPSEEK_MODEL=deepseek-chat + +OLLAMA_HOST=http://host.docker.internal:11434 +OLLAMA_BASE_URL=http://host.docker.internal:11434 +OLLAMA_MODEL=llama3:latest +OLLAMA_CHAT_MODEL=llama3:8b +OLLAMA_EMBEDDING_MODEL=nomic-embed-text + +# Adaptive RAG 深入思考模式 RAG_DEEP_RETRIEVAL_STEPS=5 RAG_DEEP_VECTOR_TOP_K=12 RAG_DEEP_CONTEXT_BLOCKS=16 RAG_DEEP_ANSWER_MAX_TOKENS=1400 -GLM_API_KEY= -GLM_BASE_URL=https://api.edgefn.net/v1 -GLM_MODEL=GLM-4.7 -DEEPSEEK_API_KEY= -OLLAMA_HOST=http://host.docker.internal:11434 -OLLAMA_BASE_URL=http://host.docker.internal:11434 -OLLAMA_MODEL=llama3:latest # Feishu long connection retry policy (1s -> 2s -> 4s, max 3 attempts) FEISHU_LONG_CONNECTION_RETRY_BASE_SECONDS=1 diff --git a/.openclaw/pmflow-routing-rules.json b/.openclaw/pmflow-routing-rules.json index c509e59f..18749fb3 100644 --- a/.openclaw/pmflow-routing-rules.json +++ b/.openclaw/pmflow-routing-rules.json @@ -1,6 +1,6 @@ { "version": "v2", - "updated_at": "2026-03-05T00:00:00Z", + "updated_at": "2026-03-11T00:00:00Z", "source_of_truth": "openclaw", "description": "PMFlow 三引擎统一路由:所有主链路统一经 OpenClaw OpenAI-Compatible 网关。", "rules": [ @@ -9,8 +9,9 @@ "enabled": true, "priority": 300, "tier": "low", - "provider": "openai", - "model": "zai/glm-4-flash", + "provider": "glm", + "model": "GLM-4.7", + "api_env": "GLM_API_KEY", "match_any": [ "feishu_level_classification", "feishu_signal_extraction", @@ -26,7 +27,8 @@ "priority": 220, "tier": "high", "provider": "openai", - "model": "minimax/MiniMax-M2.5", + "model": "MiniMax-M2.5", + "api_env": "OPENAI_API_KEY", "match_any": [ "adaptive_rag_answer", "feishu_project_prefill", @@ -49,7 +51,8 @@ "priority": 200, "tier": "high", "provider": "openai", - "model": "minimax/MiniMax-M2.5", + "model": "MiniMax-M2.5", + "api_env": "OPENAI_API_KEY", "match_any": [ "analyze_food_calories", "feishu_new_project_brief", @@ -67,7 +70,8 @@ "priority": 50, "tier": "safety", "provider": "openai", - "model": "deepseek/deepseek-chat", + "model": "deepseek-chat", + "api_env": "OPENAI_API_KEY", "match_all": true } ] diff --git a/CLAUDE.md b/CLAUDE.md index 806e7f0f..492238d8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -50,6 +50,9 @@ backend/ ├── Dockerfile └── tests/ ``` +- 新增:`backend/app/api/llm_providers.py`(AI 管家 LLM provider preset 配置读取接口) +- 移除:`backend/.env.example`(配置模板统一到根目录 `.env.example`) +- 移除:`backend/.openclaw/pmflow-routing-rules.json`(路由规则统一到根目录 `.openclaw/`) ## 5. 前端结构(`frontend/`) ```text @@ -140,6 +143,7 @@ uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 ## 9. 新增代码的参考模式 - 新增后端接口:参考 `backend/app/api/sstc.py` 的结构 +- 配置读取类接口:参考 `backend/app/api/llm_providers.py`(`GET /ai-steward/llm/providers`) - 新增前端 API 调用:参考 `frontend/src/api/sstc.ts` - 新增数据库表:参考 `alembic/versions/` 最新 migration - 新增飞书事件处理:参考 `backend/app/services/feishu_integration.py` @@ -156,3 +160,6 @@ uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 - World Model 静态层已接入 Redis 缓存(key: `wm:static:{tenant_id}:{project_id}`,TTL 600s) - 写操作后主动 invalidate,动态层不缓存 - AI PMO 意图识别已启用 LLM 主路径(`AI_PMO_INTENT_LLM_ENABLED`,默认开启);当模型超时/不可用/结构化解析失败时自动降级到规则链路,不中断主流程 +- 环境变量以根目录 `.env` / `.env.example` 为唯一配置源;`backend/.env` 已移除且不再读取 +- OpenClaw 路由规则统一由根目录 `.openclaw/pmflow-routing-rules.json` 管理,容器通过 `OPENCLAW_ROUTING_RULES_PATH` 相对路径 `../.openclaw/pmflow-routing-rules.json` 读取 +- 模型命名统一使用裸模型名(如 `GLM-4.7`、`MiniMax-M2.5`),不使用供应商前缀模型名 diff --git a/DEPLOY_TENCENT_CLOUD.md b/DEPLOY_TENCENT_CLOUD.md index 375232a1..9324796b 100644 --- a/DEPLOY_TENCENT_CLOUD.md +++ b/DEPLOY_TENCENT_CLOUD.md @@ -120,11 +120,11 @@ source venv/bin/activate pip install -r requirements.txt # 配置环境变量 -cp .env.example .env -nano .env +cp /opt/neural-sync/.env.example /opt/neural-sync/.env +nano /opt/neural-sync/.env ``` -编辑 `.env` 文件: +编辑项目根目录 `.env` 文件: ```env # 数据库 - PostgreSQL @@ -227,7 +227,7 @@ Type=simple User=root WorkingDirectory=/opt/neural-sync/backend Environment="PATH=/opt/neural-sync/backend/venv/bin" -EnvironmentFile=/opt/neural-sync/backend/.env +EnvironmentFile=/opt/neural-sync/.env ExecStart=/opt/neural-sync/backend/venv/bin/uvicorn app.main:app --host 127.0.0.1 --port 8000 Restart=always RestartSec=10 diff --git a/backend/.env.example b/backend/.env.example deleted file mode 100644 index d9dd0e2b..00000000 --- a/backend/.env.example +++ /dev/null @@ -1,96 +0,0 @@ -# Database Configuration -DATABASE_URL=postgresql+asyncpg://pmflow:pmflow_pass@db:5432/pmflow -# 可选:启动方言守卫(postgresql / sqlite)。配置后若不一致将拒绝启动。 -PMFLOW_EXPECTED_DB_DIALECT=postgresql -# For PostgreSQL with pgvector: -# DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5435/neural_sync -# For local SQLite quick-start: -# DATABASE_URL=sqlite+aiosqlite:///./neural_sync.db - -# Redis Configuration -REDIS_URL=redis://:pmflow_redis_pass@redis:6379/0 - -# Knowledge Graph Incremental Scheduler -KG_INCREMENTAL_INTERVAL_MINUTES=5 -KG_INCREMENTAL_DELTA_LIMIT=1000 -KG_INCREMENTAL_EVENT_BATCH=200 -KG_CODE_SCAN_INTERVAL_HOURS=6 - -# ======================================== -# LLM / OpenClaw Control Plane -# ======================================== -# 单一控制面:PMFlow 仅调用 OpenClaw 网关(OpenAI-Compatible) -LLM_PROVIDER=openai -OPENAI_BASE_URL=http://openclaw:18789/v1 -OPENAI_API_KEY= - -# OpenClaw 函数路由 -OPENCLAW_ROUTING_ENABLED=true -OPENCLAW_ROUTING_RULES_PATH=/app/backend/.openclaw/pmflow-routing-rules.json -# OpenClaw agent 默认超时(秒):用于注入 chat/completions -> agents.defaults.timeoutSeconds -OPENCLAW_AGENTS_DEFAULT_TIMEOUT_SECONDS=120 - -# 关闭 PMFlow 内部 mixed routing,优先以 OpenClaw 规则为准 -LLM_MIXED_ROUTING_ENABLED=false -LLM_ROUTING_DEFAULT_TIER=active -LLM_LOW_TO_HIGH_ESCALATION_ENABLED=true - -# low/high/safety tier 默认模型(由网关解释) -LLM_ROUTING_LOW_PROVIDER=openai -LLM_ROUTING_LOW_MODEL=zai/glm-4-flash -LLM_ROUTING_HIGH_PROVIDER=openai -LLM_ROUTING_HIGH_MODEL=minimax/MiniMax-M2.5 -LLM_ROUTING_SAFETY_PROVIDER=openai -LLM_ROUTING_SAFETY_MODEL=deepseek/deepseek-chat - -# 应急开关:仅在网关不可用时允许直连 provider(默认关闭) -LLM_DIRECT_EMERGENCY_ENABLED=false -LLM_FALLBACK_PROVIDER=ollama - -# Adaptive RAG 深入思考模式(前端开关=deep 时生效) -RAG_DEEP_RETRIEVAL_STEPS=5 -RAG_DEEP_VECTOR_TOP_K=12 -RAG_DEEP_CONTEXT_BLOCKS=16 -RAG_DEEP_ANSWER_MAX_TOKENS=1400 - -# --- Emergency Direct Provider Keys (optional) --- -GLM_API_KEY= -GLM_BASE_URL=https://open.bigmodel.cn/api/paas/v4 -GLM_MODEL=glm-4-flash - -DEEPSEEK_API_KEY= -DEEPSEEK_BASE_URL=https://api.deepseek.com/v1 - -# --- Ollama 本地配置(应急本地兜底) --- -OLLAMA_HOST=http://127.0.0.1:11434 -OLLAMA_CHAT_MODEL=llama3:8b -OLLAMA_EMBEDDING_MODEL=nomic-embed-text -OLLAMA_BASE_URL=http://127.0.0.1:11434 -OLLAMA_MODEL=llama3:latest - -# ======================================== - -# Multi-Model Voting Configuration (仅 Ollama 支持) -# For high-impact signals (impact_score >= VOTING_THRESHOLD), multiple models will vote -# Available models: llama3, llama3.2, qwen2.5, mistral, deepseek-coder, etc. -VOTING_MODELS=llama3,qwen2.5,mistral -VOTING_THRESHOLD=0.8 - -# Business Context for AI Strategic Analysis -# Customize this to match your actual product/team -# This context is used by the AI to evaluate external signals -BUSINESS_CONTEXT=Pixelle-Video: An AI-powered automated short video generation platform. Core features: topic-to-video automation (script generation → AI images/videos → TTS → BGM → final video). Tech stack: Python, Streamlit, FastAPI, ComfyUI workflows. AI models: FLUX/Qwen/SDXL for images, WAN 2.1/2.2 for videos, Edge TTS/Index TTS for voice. Supports multiple pipelines: standard generation, custom assets, digital human, image-to-video, action transfer. - -# Plane PM Tool Integration (optional, simulation mode if not set) -PLANE_API_URL=https://api.plane.so/api/v1 -PLANE_API_KEY= -PLANE_WORKSPACE_SLUG= -PLANE_PROJECT_ID= - -# Lark/Feishu Bot Integration (optional) -LARK_APP_ID= -LARK_APP_SECRET= -LARK_VERIFICATION_TOKEN= -# 飞书长连接重试策略(指数退避 1s -> 2s -> 4s,最多 3 次) -FEISHU_LONG_CONNECTION_RETRY_BASE_SECONDS=1 -FEISHU_LONG_CONNECTION_RETRY_MAX_ATTEMPTS=3 diff --git a/backend/.openclaw/pmflow-routing-rules.json b/backend/.openclaw/pmflow-routing-rules.json deleted file mode 100644 index 55b9b700..00000000 --- a/backend/.openclaw/pmflow-routing-rules.json +++ /dev/null @@ -1,74 +0,0 @@ -{ - "version": "v2", - "updated_at": "2026-03-05T00:00:00Z", - "source_of_truth": "openclaw", - "description": "PMFlow 三引擎统一路由:所有主链路统一经 OpenClaw OpenAI-Compatible 网关。", - "rules": [ - { - "name": "engine_low_execute_glm_flash", - "enabled": true, - "priority": 300, - "tier": "low", - "provider": "glm", - "model": "GLM-4.7", - "match_any": [ - "feishu_level_classification", - "feishu_signal_extraction", - "rag_intent_classification", - "classify_intent", - "feishu_humanized_reply", - "extract_context_insights" - ] - }, - { - "name": "engine_high_reasoning_minimax", - "enabled": true, - "priority": 220, - "tier": "high", - "provider": "openai", - "model": "MiniMax-M2.5", - "match_any": [ - "adaptive_rag_answer", - "feishu_project_prefill", - "feishu_task_orchestration", - "generate_task_breakdown", - "generate_charter_from_signal", - "decompose_tasks_from_charter", - "recommend_assignees_for_task", - "evaluate_model_requirement", - "calculate_alignment_score", - "detect_cognitive_conflicts", - "rag_db_project_semantic_retrieval", - "workload_optimize_prompt", - "analyze_cost_roi" - ] - }, - { - "name": "engine_high_extended_reasoning", - "enabled": true, - "priority": 200, - "tier": "high", - "provider": "openai", - "model": "MiniMax-M2.5", - "match_any": [ - "analyze_food_calories", - "feishu_new_project_brief", - "feishu_project_signal_classification", - "sstc_ai_breakdown", - "sstc_charter_localize_zh", - "sstc_charter_prefill", - "skill_score_update", - "test_engine_connection" - ] - }, - { - "name": "engine_safety_fallback", - "enabled": true, - "priority": 50, - "tier": "safety", - "provider": "openai", - "model": "deepseek/deepseek-chat", - "match_all": true - } - ] -} diff --git a/backend/Dockerfile b/backend/Dockerfile index 332aa4e5..4193707d 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -13,6 +13,7 @@ COPY backend/requirements.txt /tmp/requirements.txt RUN pip install --no-cache-dir -r /tmp/requirements.txt COPY backend /app/backend +COPY .openclaw /app/.openclaw EXPOSE 8000 diff --git a/backend/app/api/__init__.py b/backend/app/api/__init__.py index 31515487..97e47afc 100644 --- a/backend/app/api/__init__.py +++ b/backend/app/api/__init__.py @@ -22,6 +22,7 @@ from .budget import router as budget_router from .scheduler import router as scheduler_router from .notifications import router as notifications_router from .llm_engines import router as llm_engines_router +from .llm_providers import router as llm_providers_router from .prompt_profiles import router as prompt_profiles_router from .knowledge_graph import router as knowledge_graph_router from .operation_catalog import router as operation_catalog_router @@ -46,6 +47,7 @@ __all__ = [ "scheduler_router", "notifications_router", "llm_engines_router", + "llm_providers_router", "prompt_profiles_router", "knowledge_graph_router", "operation_catalog_router", diff --git a/backend/app/api/llm_providers.py b/backend/app/api/llm_providers.py new file mode 100644 index 00000000..9022510c --- /dev/null +++ b/backend/app/api/llm_providers.py @@ -0,0 +1,58 @@ +"""LLM provider preset API.""" + +from __future__ import annotations + +import os +from typing import Any + +from fastapi import APIRouter, Depends + +from ..core.response import success_response +from ..dependencies.auth import get_current_user +from ..models import User + +router = APIRouter(prefix="/ai-steward/llm", tags=["AI Steward"]) + + +def _build_provider_payload() -> dict[str, Any]: + low_model = str(os.getenv("LLM_ROUTING_LOW_MODEL", "GLM-4.7") or "GLM-4.7").strip() + high_model = str(os.getenv("LLM_ROUTING_HIGH_MODEL", "MiniMax-M2.5") or "MiniMax-M2.5").strip() + base_url = str(os.getenv("OPENAI_BASE_URL", "https://api.edgefn.net/v1") or "https://api.edgefn.net/v1").strip() + ollama_base_url = str( + os.getenv("OLLAMA_BASE_URL", os.getenv("OLLAMA_HOST", "http://127.0.0.1:11434")) or "http://127.0.0.1:11434" + ).strip() + return { + "low": low_model, + "high": high_model, + "base_url": base_url, + "providers": { + "glm": { + "model": str(os.getenv("GLM_MODEL", low_model) or low_model).strip(), + "base_url": str(os.getenv("GLM_BASE_URL", base_url) or base_url).strip(), + "api_env": "GLM_API_KEY", + }, + "openai": { + "model": str(os.getenv("OPENAI_MODEL", high_model) or high_model).strip(), + "base_url": base_url, + "api_env": "OPENAI_API_KEY", + }, + "deepseek": { + "model": str(os.getenv("DEEPSEEK_MODEL", "deepseek-chat") or "deepseek-chat").strip(), + "base_url": str(os.getenv("DEEPSEEK_BASE_URL", "https://api.deepseek.com/v1") or "").strip(), + "api_env": "DEEPSEEK_API_KEY", + }, + "ollama": { + "model": str(os.getenv("OLLAMA_MODEL", "llama3:latest") or "llama3:latest").strip(), + "base_url": ollama_base_url, + "api_env": "", + }, + }, + } + + +@router.get("/providers") +async def get_llm_provider_presets( + current_user: User = Depends(get_current_user), +): + _ = current_user + return success_response(_build_provider_payload()) diff --git a/backend/app/api/router.py b/backend/app/api/router.py index b01edf04..c248d46f 100644 --- a/backend/app/api/router.py +++ b/backend/app/api/router.py @@ -25,6 +25,7 @@ from .feishu import router as feishu_router from .wecom import router as wecom_router from .notifications import router as notifications_router from .llm_engines import router as llm_engines_router +from .llm_providers import router as llm_providers_router from .ai_logs import router as ai_logs_router from .prompt_profiles import router as prompt_profiles_router from .rag import router as rag_router @@ -57,6 +58,7 @@ api_router.include_router(feishu_router) api_router.include_router(wecom_router) api_router.include_router(notifications_router) api_router.include_router(llm_engines_router) +api_router.include_router(llm_providers_router) api_router.include_router(ai_logs_router) api_router.include_router(prompt_profiles_router) api_router.include_router(rag_router) diff --git a/backend/app/config/ai_prompts.py b/backend/app/config/ai_prompts.py index 287f8270..3dc2770d 100644 --- a/backend/app/config/ai_prompts.py +++ b/backend/app/config/ai_prompts.py @@ -525,7 +525,8 @@ PROMPT_AI_PMO_INTENT_CLASSIFICATION = """ 3. project_id 无法确定时返回 null,不要猜测无依据项目。 4. 噪音/寒暄/无关消息返回 ignore。 5. 禁止输出 thinking、analysis、解释句、Markdown、代码块。 -6. 只输出 JSON,不要额外文本。 +6. 若消息表达“新增/增加/添加/新建/创建/建立 + 项目”(如“给我新增个项目”),优先判定 project.create;仅当明确是“信号源/来源/webhook/rss/github 接入或创建信号”时才判 signal_source.onboard 或 signal.create。 +7. 只输出 JSON,不要额外文本。 """ diff --git a/backend/app/core/config.py b/backend/app/core/config.py index e311d06e..b002f76c 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -9,10 +9,9 @@ from pathlib import Path from typing import Optional from dotenv import load_dotenv -# 统一从 backend/.env 加载配置,避免因启动目录不同读到错误环境 +# 仅从项目根目录 .env 加载,作为唯一配置源 BACKEND_DIR = Path(__file__).resolve().parents[2] PROJECT_ROOT = BACKEND_DIR.parent -load_dotenv(BACKEND_DIR / ".env", override=False) load_dotenv(PROJECT_ROOT / ".env", override=False) # 默认数据库固定到 backend/neural_sync.db,避免相对路径导致多库混用 diff --git a/backend/app/services/ai_pmo_kernel_service.py b/backend/app/services/ai_pmo_kernel_service.py index 83425729..71b7812f 100644 --- a/backend/app/services/ai_pmo_kernel_service.py +++ b/backend/app/services/ai_pmo_kernel_service.py @@ -139,7 +139,24 @@ FOLLOWUP_CONFIRM_HINTS = ("确认归属", "确认项目", "确认到项目", " FOLLOWUP_CONTINUE_HINTS = ("继续处理", "继续执行", "接着处理", "往下做", "继续推进") FOLLOWUP_WORK_ITEM_ID_RE = re.compile(r"(?:work[_ -]?item|工作项|队列项)\s*(?:id|ID)?\s*[:#:]?\s*(\d+)", re.IGNORECASE) SIGNAL_CREATE_HINTS = ("新增一个信号", "新增一条信号", "增加一个信号", "录入一个信号", "关注", "跟踪") -PROJECT_CREATE_HINTS = ("创建项目", "新建项目", "建立项目", "做个项目", "立项", "开个项目") +PROJECT_CREATE_HINTS = ( + "创建项目", + "新建项目", + "建立项目", + "做个项目", + "立项", + "开个项目", + "新增项目", + "新增个项目", + "新增一个项目", + "增加项目", + "增加个项目", + "增加一个项目", + "添加项目", + "添加一个项目", + "加个项目", + "加一个项目", +) POLICY_GATE_RULES: tuple[tuple[str, tuple[str, ...]], ...] = ( ("delete_or_archive", ("删除项目", "删除信号", "归档项目", "归档", "archive", "清空项目")), ("budget_overrun", ("超预算", "突破预算", "额外预算", "加预算")), @@ -268,7 +285,10 @@ class IntentResolver: pretty[:300], {"source_request": True}, ) - if any(token in pretty for token in PROJECT_CREATE_HINTS) or OpenClawCommanderService._extract_project_create_title(pretty): + if ( + IntentResolver._looks_like_project_create_request(text=pretty) + or OpenClawCommanderService._extract_project_create_title(pretty) + ): return CanonicalIntent( INTENT_PROJECT_CREATE, 0.9, @@ -321,6 +341,21 @@ class IntentResolver: has_operation = any(token in normalized for token in IntentResolver.OPERATION_VERBS) return not has_operation + @staticmethod + def _looks_like_project_create_request(*, text: str) -> bool: + normalized = re.sub(r"\s+", "", str(text or "")).lower() + if not normalized: + return False + if any(token in normalized for token in PROJECT_CREATE_HINTS): + return True + return bool( + re.search( + r"(?:新增|增加|添加|新建|创建|建立|开个|开一个|建个|建一个|做个|做一个|加个|加一个|立项)(?:一个|个)?项目", + normalized, + re.IGNORECASE, + ) + ) + @staticmethod def _build_signal_title(text: str, *, default: str) -> str: cleaned = re.sub(r"\s+", " ", str(text or "")).strip(" ,,::") diff --git a/backend/app/services/ai_service.py b/backend/app/services/ai_service.py index 299aab13..4b584a6b 100644 --- a/backend/app/services/ai_service.py +++ b/backend/app/services/ai_service.py @@ -2,7 +2,7 @@ PMFlow AI 核心能力封装 技术栈:LangChain 0.3+、OpenAI SDK、Redis -- 封装 GPT-4o mini 调用(流式/非流式) +- 封装统一 LLM 路由入口调用(流式/非流式) - Prompt 集中在 config/ai_prompts - AI 调用日志写入 ai_call_logs - Redis 结果缓存(1 小时 TTL) @@ -39,6 +39,9 @@ INPUT_PRICE_PER_1K = 0.00015 OUTPUT_PRICE_PER_1K = 0.0006 CACHE_TTL_SECONDS = 3600 # 1 小时 PROMPT_PROFILE_CACHE_TTL_SECONDS = 60 +DEFAULT_OPENAI_BASE_URL = "https://api.edgefn.net/v1" +DEFAULT_OPENAI_MODEL = "MiniMax-M2.5" +DEFAULT_GLM_MODEL = "GLM-4.7" from ..config.ai_prompts import build_default_role_prompt_bundle from ..database import AsyncSessionLocal @@ -73,27 +76,95 @@ class AIService: def __init__( self, api_key: Optional[str] = None, - model: str = "gpt-4o-mini", + model: Optional[str] = None, + base_url: Optional[str] = None, redis_url: Optional[str] = None, max_retries: int = 3, ): - self.api_key = api_key or os.getenv("OPENAI_API_KEY", "") - self.model = model + runtime = self._resolve_llm_runtime( + api_key=api_key, + model=model, + base_url=base_url, + ) + self.api_key = runtime["api_key"] + self.model = runtime["model"] + self.base_url = runtime["base_url"] self.redis_url = redis_url or os.getenv("REDIS_URL") self.max_retries = max_retries self._llm: Optional[BaseChatModel] = None self._redis: Optional[Any] = None self._prompt_profile_cache: dict[int, tuple[float, tuple[str, str]]] = {} + @staticmethod + def _is_env_true(name: str, default: bool) -> bool: + raw = str(os.getenv(name, str(default)) or "").strip().lower() + if not raw: + return default + return raw in {"1", "true", "yes", "y", "on", "是"} + + def _resolve_llm_runtime( + self, + *, + api_key: Optional[str], + model: Optional[str], + base_url: Optional[str], + ) -> dict[str, str]: + routing_enabled = self._is_env_true("OPENCLAW_ROUTING_ENABLED", True) + direct_emergency_enabled = self._is_env_true("LLM_DIRECT_EMERGENCY_ENABLED", False) + if routing_enabled and not direct_emergency_enabled: + return { + "api_key": str(api_key or os.getenv("OPENAI_API_KEY", "") or "").strip(), + "model": str( + model + or os.getenv("LLM_ROUTING_HIGH_MODEL") + or os.getenv("OPENAI_MODEL") + or DEFAULT_OPENAI_MODEL + or "" + ).strip(), + "base_url": str(base_url or os.getenv("OPENAI_BASE_URL", DEFAULT_OPENAI_BASE_URL) or "").strip(), + } + + provider = str(os.getenv("LLM_PROVIDER", "openai") or "openai").strip().lower() + if provider == "glm": + return { + "api_key": str(api_key or os.getenv("GLM_API_KEY", "") or "").strip(), + "model": str(model or os.getenv("GLM_MODEL", DEFAULT_GLM_MODEL) or "").strip(), + "base_url": str(base_url or os.getenv("GLM_BASE_URL", DEFAULT_OPENAI_BASE_URL) or "").strip(), + } + if provider == "deepseek": + return { + "api_key": str(api_key or os.getenv("DEEPSEEK_API_KEY", "") or "").strip(), + "model": str(model or os.getenv("DEEPSEEK_MODEL", "deepseek-chat") or "").strip(), + "base_url": str(base_url or os.getenv("DEEPSEEK_BASE_URL", "https://api.deepseek.com/v1") or "").strip(), + } + if provider == "ollama": + return { + "api_key": str(api_key or "").strip(), + "model": str(model or os.getenv("OLLAMA_MODEL", "llama3:latest") or "").strip(), + "base_url": str( + base_url + or os.getenv("OLLAMA_BASE_URL") + or os.getenv("OLLAMA_HOST", "http://127.0.0.1:11434") + or "" + ).strip(), + } + + return { + "api_key": str(api_key or os.getenv("OPENAI_API_KEY", "") or "").strip(), + "model": str(model or os.getenv("OPENAI_MODEL", DEFAULT_OPENAI_MODEL) or "").strip(), + "base_url": str(base_url or os.getenv("OPENAI_BASE_URL", DEFAULT_OPENAI_BASE_URL) or "").strip(), + } + def _get_llm(self) -> BaseChatModel: if not LANGCHAIN_AVAILABLE: raise RuntimeError("请安装 langchain-openai: pip install langchain-openai langchain-core") if not self.api_key: - raise RuntimeError("请设置 OPENAI_API_KEY 环境变量") + raise RuntimeError("请设置统一路由所需 API Key 环境变量") if self._llm is None: self._llm = ChatOpenAI( model=self.model, api_key=self.api_key, + base_url=self.base_url, temperature=0.3, ) return self._llm diff --git a/backend/app/services/llm.py b/backend/app/services/llm.py index 53eda2b4..b855c16f 100644 --- a/backend/app/services/llm.py +++ b/backend/app/services/llm.py @@ -43,9 +43,9 @@ logger = logging.getLogger(__name__) SUPPORTED_PROVIDERS = {"deepseek", "glm", "openai", "ollama"} OPENCLAW_PRIMARY_GATEWAY_URL = "http://openclaw:18789/v1" -OPENCLAW_DEFAULT_LOW_MODEL = "zai/glm-4-flash" -OPENCLAW_DEFAULT_HIGH_MODEL = "minimax/MiniMax-M2.5" -OPENCLAW_DEFAULT_SAFETY_MODEL = "deepseek/deepseek-chat" +OPENCLAW_DEFAULT_LOW_MODEL = "GLM-4.7" +OPENCLAW_DEFAULT_HIGH_MODEL = "MiniMax-M2.5" +OPENCLAW_DEFAULT_SAFETY_MODEL = "deepseek-chat" PROVIDER_DEFAULTS: dict[str, dict[str, str]] = { "deepseek": { "name": "DeepSeek", @@ -56,7 +56,7 @@ PROVIDER_DEFAULTS: dict[str, dict[str, str]] = { "glm": { "name": "GLM", "base_url": "https://open.bigmodel.cn/api/paas/v4", - "model": "glm-4-flash", + "model": "GLM-4.7", "api_env": "GLM_API_KEY", }, "openai": { @@ -81,8 +81,7 @@ PROVIDER_ALIASES = { PROMPT_PROFILE_CACHE_TTL_SECONDS = 60.0 BOOL_TRUE_VALUES = {"1", "true", "yes", "y", "on", "是"} OPENCLAW_ROUTING_CACHE_TTL_SECONDS = 30.0 -OPENCLAW_BACKEND_RULES_PATH = Path(__file__).resolve().parents[2] / ".openclaw" / "pmflow-routing-rules.json" -OPENCLAW_PROJECT_RULES_PATH = Path(__file__).resolve().parents[3] / ".openclaw" / "pmflow-routing-rules.json" +OPENCLAW_DEFAULT_RULES_RELATIVE_PATH = "../.openclaw/pmflow-routing-rules.json" OPENCLAW_AGENTS_DEFAULT_TIMEOUT_SECONDS = 120 OPENCLAW_AGENTS_TIMEOUT_SECONDS_MAX = 120 DEFAULT_TIER_MODEL_OVERRIDES = { @@ -1456,14 +1455,18 @@ class LLMService: return runtime def _openclaw_rules_path(self) -> str: - custom = str(os.getenv("OPENCLAW_ROUTING_RULES_PATH", "") or "").strip() - if custom: - return custom - if OPENCLAW_BACKEND_RULES_PATH.exists(): - return str(OPENCLAW_BACKEND_RULES_PATH) - if OPENCLAW_PROJECT_RULES_PATH.exists(): - return str(OPENCLAW_PROJECT_RULES_PATH) - return str(OPENCLAW_BACKEND_RULES_PATH) + custom = str( + os.getenv( + "OPENCLAW_ROUTING_RULES_PATH", + OPENCLAW_DEFAULT_RULES_RELATIVE_PATH, + ) + or "" + ).strip() + raw_path = custom or OPENCLAW_DEFAULT_RULES_RELATIVE_PATH + candidate = Path(raw_path) + if not candidate.is_absolute(): + candidate = (Path(__file__).resolve().parents[2] / candidate).resolve() + return str(candidate) def _load_openclaw_routing_bundle(self) -> dict[str, Any]: now = time.time() @@ -1580,6 +1583,7 @@ class LLMService: engine_id = str(rule.get("engine_id") or "").strip() model_override = str(rule.get("model") or "").strip() base_url_override = str(rule.get("base_url") or "").strip().rstrip("/") + api_env_override = str(rule.get("api_env") or "").strip() routed: Optional[dict[str, Any]] = None if engine_id: @@ -1618,6 +1622,9 @@ class LLMService: routed["model"] = model_override if base_url_override: routed["base_url"] = base_url_override + if api_env_override: + routed["api_key"] = str(os.getenv(api_env_override, "") or "").strip() + routed["api_env"] = api_env_override return routed def _resolve_function_tier(self, function_name: str) -> str: diff --git a/backend/app/services/llm_client.py b/backend/app/services/llm_client.py index 72aa9e81..5683e614 100644 --- a/backend/app/services/llm_client.py +++ b/backend/app/services/llm_client.py @@ -34,8 +34,8 @@ class LLMClient: elif self.provider == "openai": return OpenAICompatibleClient( api_key=os.getenv("OPENAI_API_KEY"), - base_url=os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1"), - model=os.getenv("OPENAI_MODEL", "gpt-4o-mini") + base_url=os.getenv("OPENAI_BASE_URL", "https://api.edgefn.net/v1"), + model=os.getenv("OPENAI_MODEL", "MiniMax-M2.5") ) elif self.provider == "qwen": return OpenAICompatibleClient( diff --git a/backend/app/services/openclaw_commander_service.py b/backend/app/services/openclaw_commander_service.py index 2b4273f4..2ba75481 100644 --- a/backend/app/services/openclaw_commander_service.py +++ b/backend/app/services/openclaw_commander_service.py @@ -819,8 +819,8 @@ class OpenClawCommanderService: if quoted_match: return OpenClawCommanderService._normalize_project_title_candidate(quoted_match.group(1)) for pattern in ( - r"(?:帮我|请|麻烦)?(?:创建|新建|建立|开个|开一个|建个|建一个|做个|做一个)(?:一个|个)?项目(?:叫|名为|名称是|名称为|是|为)?[::,,\s]*(.+)", - r"(?:创建|新建|建立)项目[::,,\s]*(.+)", + r"(?:帮我|请|麻烦|给我)?(?:新增|增加|添加|创建|新建|建立|开个|开一个|建个|建一个|做个|做一个|加个|加一个)(?:一个|个)?项目(?:叫|名为|名称是|名称为|是|为)?[::,,\s]*(.+)", + r"(?:新增|增加|添加|创建|新建|建立)项目[::,,\s]*(.+)", r"项目(?:叫|名为|名称是|名称为)[::,,\s]*(.+)", ): match = re.search(pattern, cleaned, re.IGNORECASE) @@ -840,7 +840,7 @@ class OpenClawCommanderService: if title: cleaned = cleaned.replace(title, "", 1).strip(" ,,::") cleaned = re.sub( - r"^(?:帮我|请|麻烦)?(?:创建|新建|建立|开个|开一个|建个|建一个|做个|做一个)(?:一个|个)?项目(?:叫|名为|名称是|名称为|是|为)?", + r"^(?:帮我|请|麻烦|给我)?(?:新增|增加|添加|创建|新建|建立|开个|开一个|建个|建一个|做个|做一个|加个|加一个)(?:一个|个)?项目(?:叫|名为|名称是|名称为|是|为)?", "", cleaned, flags=re.IGNORECASE, diff --git a/backend/tests/test_ai_pmo_kernel_service.py b/backend/tests/test_ai_pmo_kernel_service.py index 9cb87d5c..8c198376 100644 --- a/backend/tests/test_ai_pmo_kernel_service.py +++ b/backend/tests/test_ai_pmo_kernel_service.py @@ -25,6 +25,7 @@ from app.models import ( from app.services.ai_pmo_cadence_service import AIPMOCadenceService from app.services.ai_pmo_kernel_service import ( AIPMOKernelService, + INTENT_PROJECT_CREATE, INTENT_SIGNAL_SOURCE_ONBOARD, IntentResolver, ) @@ -183,6 +184,13 @@ class AIPMOKernelServiceTest(unittest.IsolatedAsyncioTestCase): ) self.assertEqual(intent.name, INTENT_SIGNAL_SOURCE_ONBOARD) + def test_intent_resolver_detects_project_create_for_add_project_phrase(self) -> None: + intent = IntentResolver.resolve( + text="给我新增个项目,openclaw自动化部署。", + project_id=None, + ) + self.assertEqual(intent.name, INTENT_PROJECT_CREATE) + async def test_build_world_model_includes_budget_capacity_and_skill_graph(self) -> None: async with self.session_factory() as db: service = AIPMOKernelService(db, tenant_id=1) @@ -375,7 +383,7 @@ class AIPMOKernelServiceTest(unittest.IsolatedAsyncioTestCase): }, source_field="content", provider="openai", - model="zai/glm-4.7", + model="GLM-4.7", raw_text='{"intent":"signal_source.onboard"}', validation="ok", ) @@ -422,7 +430,7 @@ class AIPMOKernelServiceTest(unittest.IsolatedAsyncioTestCase): payload=None, source_field="", provider="openai", - model="zai/glm-4.7", + model="GLM-4.7", error_code="model_not_allowed", raw_text="403 ModelNotAllowed", validation="invalid", @@ -509,6 +517,43 @@ class AIPMOKernelServiceTest(unittest.IsolatedAsyncioTestCase): self.assertTrue(result["direct_reply_required"]) self.assertEqual(work_item.project_id, 9) + async def test_process_add_project_phrase_routes_to_project_create(self) -> None: + async with self.session_factory() as db: + service = AIPMOKernelService(db, tenant_id=1) + with patch( + "app.services.ai_pmo_kernel_service.AIStewardService.create_project_from_natural_language", + AsyncMock( + return_value={ + "project": {"id": 12, "title": "OpenClaw 自动化部署", "name": "OpenClaw 自动化部署"}, + "signal": {"id": 31}, + "tasks": [], + } + ), + ): + result = await service.process_ingress( + source_system="pmflow_manual", + source_message_id="project-phrase-1", + message_text="给我新增个项目,openclaw自动化部署。", + sender="老板", + channel_ref="pmflow", + ) + work_item = ( + await db.execute( + select(WorkItem).where( + and_( + WorkItem.tenant_id == 1, + WorkItem.source_message_id == "project-phrase-1", + ) + ) + ) + ).scalar_one() + + self.assertEqual(result["status"], "captured") + self.assertEqual(result["created_project_id"], 12) + self.assertEqual(result["linked_project_ids"], [12]) + self.assertEqual(work_item.intent, INTENT_PROJECT_CREATE) + self.assertEqual(work_item.project_id, 12) + async def test_signal_source_request_hits_simulation_policy_gate_when_skill_gap_exists(self) -> None: async with self.session_factory() as db: service = AIPMOKernelService(db, tenant_id=1) diff --git a/backend/tests/test_llm_providers_api.py b/backend/tests/test_llm_providers_api.py new file mode 100644 index 00000000..4f3e1f65 --- /dev/null +++ b/backend/tests/test_llm_providers_api.py @@ -0,0 +1,40 @@ +import asyncio + +from app.api.llm_providers import _build_provider_payload, get_llm_provider_presets + + +def test_build_provider_payload_reads_env(monkeypatch) -> None: + monkeypatch.setenv("LLM_ROUTING_LOW_MODEL", "GLM-4.7") + monkeypatch.setenv("LLM_ROUTING_HIGH_MODEL", "MiniMax-M2.5") + monkeypatch.setenv("OPENAI_BASE_URL", "https://api.edgefn.net/v1") + monkeypatch.setenv("GLM_BASE_URL", "https://api.edgefn.net/v1") + monkeypatch.setenv("GLM_MODEL", "GLM-4.7") + monkeypatch.setenv("OPENAI_MODEL", "MiniMax-M2.5") + monkeypatch.setenv("DEEPSEEK_BASE_URL", "https://api.deepseek.com/v1") + monkeypatch.setenv("DEEPSEEK_MODEL", "deepseek-chat") + monkeypatch.setenv("OLLAMA_BASE_URL", "http://host.docker.internal:11434") + monkeypatch.setenv("OLLAMA_MODEL", "llama3:latest") + + payload = _build_provider_payload() + + assert payload["low"] == "GLM-4.7" + assert payload["high"] == "MiniMax-M2.5" + assert payload["base_url"] == "https://api.edgefn.net/v1" + assert payload["providers"]["glm"]["api_env"] == "GLM_API_KEY" + assert payload["providers"]["openai"]["api_env"] == "OPENAI_API_KEY" + assert payload["providers"]["glm"]["model"] == "GLM-4.7" + assert payload["providers"]["openai"]["model"] == "MiniMax-M2.5" + + +def test_get_llm_provider_presets_returns_envelope(monkeypatch) -> None: + monkeypatch.setenv("LLM_ROUTING_LOW_MODEL", "GLM-4.7") + monkeypatch.setenv("LLM_ROUTING_HIGH_MODEL", "MiniMax-M2.5") + monkeypatch.setenv("OPENAI_BASE_URL", "https://api.edgefn.net/v1") + + result = asyncio.run(get_llm_provider_presets(current_user=object())) + + assert result["code"] == 0 + assert result["message"] == "success" + assert isinstance(result["data"], dict) + assert result["data"]["low"] == "GLM-4.7" + assert result["data"]["high"] == "MiniMax-M2.5" diff --git a/backend/tests/test_openclaw_commander_service.py b/backend/tests/test_openclaw_commander_service.py index 15de8509..2230129e 100644 --- a/backend/tests/test_openclaw_commander_service.py +++ b/backend/tests/test_openclaw_commander_service.py @@ -373,6 +373,23 @@ class OpenClawCommanderServiceTest(unittest.TestCase): self.assertEqual(plan["operations"][0]["operation_code"], "project.create") self.assertEqual(plan["operations"][0]["payload"]["title"], "财务冲账协同") + def test_project_create_fallback_supports_add_project_phrase(self) -> None: + commander = _ProjectFallbackCommander() + + plan = self.async_run( + commander._build_fallback_plan( + tenant_id=1, + project_id=None, + instruction="给我新增个项目,openclaw自动化部署。", + max_operations=4, + error="structured_http_error", + project_directory=None, + ) + ) + + self.assertEqual(plan["operations"][0]["operation_code"], "project.create") + self.assertEqual(plan["operations"][0]["payload"]["title"], "openclaw自动化部署") + def test_project_status_fallback_resolves_project_id_from_directory(self) -> None: commander = _ProjectFallbackCommander() diff --git a/deploy.sh b/deploy.sh index 6f1ba0aa..87cac48f 100644 --- a/deploy.sh +++ b/deploy.sh @@ -300,7 +300,7 @@ User=root WorkingDirectory=$PROJECT_DIR/backend Environment="PATH=$PROJECT_DIR/backend/venv/bin" Environment="PYTHONPATH=$PROJECT_DIR/backend" -EnvironmentFile=$PROJECT_DIR/backend/.env +EnvironmentFile=$PROJECT_DIR/.env ExecStart=$PROJECT_DIR/backend/venv/bin/uvicorn app.main:app --host 127.0.0.1 --port 8000 Restart=always RestartSec=10 diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 0da93de1..b88a9760 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -55,41 +55,45 @@ services: redis: condition: service_healthy environment: - DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-pmflow}:${POSTGRES_PASSWORD:-pmflow_pass}@db:5432/${POSTGRES_DB:-pmflow} - REDIS_URL: redis://:${REDIS_PASSWORD:-pmflow_redis_pass}@redis:6379/0 - CORS_ORIGINS: ${CORS_ORIGINS:-http://localhost:5174,http://127.0.0.1:5174} - LLM_PROVIDER: ${LLM_PROVIDER:-openai} - LLM_FALLBACK_PROVIDER: ${LLM_FALLBACK_PROVIDER:-ollama} - OPENAI_BASE_URL: ${OPENAI_BASE_URL:-http://openclaw:18789/v1} - OPENCLAW_ROUTING_ENABLED: ${OPENCLAW_ROUTING_ENABLED:-true} - OPENCLAW_ROUTING_RULES_PATH: ${OPENCLAW_ROUTING_RULES_PATH:-/app/backend/.openclaw/pmflow-routing-rules.json} - LLM_MIXED_ROUTING_ENABLED: ${LLM_MIXED_ROUTING_ENABLED:-false} - LLM_LOW_TO_HIGH_ESCALATION_ENABLED: ${LLM_LOW_TO_HIGH_ESCALATION_ENABLED:-true} - LLM_ROUTING_LOW_PROVIDER: ${LLM_ROUTING_LOW_PROVIDER:-glm} - LLM_ROUTING_LOW_MODEL: ${LLM_ROUTING_LOW_MODEL:-GLM-4.7} - LLM_ROUTING_HIGH_PROVIDER: ${LLM_ROUTING_HIGH_PROVIDER:-openai} - LLM_ROUTING_HIGH_MODEL: ${LLM_ROUTING_HIGH_MODEL:-MiniMax-M2.5} - LLM_ROUTING_SAFETY_PROVIDER: ${LLM_ROUTING_SAFETY_PROVIDER:-glm} - LLM_ROUTING_SAFETY_MODEL: ${LLM_ROUTING_SAFETY_MODEL:-GLM-4.7} - LLM_DIRECT_EMERGENCY_ENABLED: ${LLM_DIRECT_EMERGENCY_ENABLED:-false} - LLM_REQUEST_TIMEOUT: ${LLM_REQUEST_TIMEOUT:-90} + DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:?missing_POSTGRES_USER}:${POSTGRES_PASSWORD:?missing_POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB:?missing_POSTGRES_DB} + REDIS_URL: redis://:${REDIS_PASSWORD:?missing_REDIS_PASSWORD}@redis:6379/0 + PMFLOW_EXPECTED_DB_DIALECT: ${PMFLOW_EXPECTED_DB_DIALECT:?missing_PMFLOW_EXPECTED_DB_DIALECT} + CORS_ORIGINS: ${CORS_ORIGINS:?missing_CORS_ORIGINS} + LLM_PROVIDER: ${LLM_PROVIDER:?missing_LLM_PROVIDER} + LLM_FALLBACK_PROVIDER: ${LLM_FALLBACK_PROVIDER:?missing_LLM_FALLBACK_PROVIDER} + OPENAI_BASE_URL: ${OPENAI_BASE_URL:?missing_OPENAI_BASE_URL} + OPENCLAW_ROUTING_ENABLED: ${OPENCLAW_ROUTING_ENABLED:?missing_OPENCLAW_ROUTING_ENABLED} + OPENCLAW_ROUTING_RULES_PATH: ${OPENCLAW_ROUTING_RULES_PATH:?missing_OPENCLAW_ROUTING_RULES_PATH} + OPENCLAW_AGENTS_DEFAULT_TIMEOUT_SECONDS: ${OPENCLAW_AGENTS_DEFAULT_TIMEOUT_SECONDS:?missing_OPENCLAW_AGENTS_DEFAULT_TIMEOUT_SECONDS} + LLM_MIXED_ROUTING_ENABLED: ${LLM_MIXED_ROUTING_ENABLED:?missing_LLM_MIXED_ROUTING_ENABLED} + LLM_ROUTING_DEFAULT_TIER: ${LLM_ROUTING_DEFAULT_TIER:?missing_LLM_ROUTING_DEFAULT_TIER} + LLM_LOW_TO_HIGH_ESCALATION_ENABLED: ${LLM_LOW_TO_HIGH_ESCALATION_ENABLED:?missing_LLM_LOW_TO_HIGH_ESCALATION_ENABLED} + LLM_ROUTING_LOW_PROVIDER: ${LLM_ROUTING_LOW_PROVIDER:?missing_LLM_ROUTING_LOW_PROVIDER} + LLM_ROUTING_LOW_MODEL: ${LLM_ROUTING_LOW_MODEL:?missing_LLM_ROUTING_LOW_MODEL} + LLM_ROUTING_HIGH_PROVIDER: ${LLM_ROUTING_HIGH_PROVIDER:?missing_LLM_ROUTING_HIGH_PROVIDER} + LLM_ROUTING_HIGH_MODEL: ${LLM_ROUTING_HIGH_MODEL:?missing_LLM_ROUTING_HIGH_MODEL} + LLM_ROUTING_SAFETY_PROVIDER: ${LLM_ROUTING_SAFETY_PROVIDER:?missing_LLM_ROUTING_SAFETY_PROVIDER} + LLM_ROUTING_SAFETY_MODEL: ${LLM_ROUTING_SAFETY_MODEL:?missing_LLM_ROUTING_SAFETY_MODEL} + LLM_DIRECT_EMERGENCY_ENABLED: ${LLM_DIRECT_EMERGENCY_ENABLED:?missing_LLM_DIRECT_EMERGENCY_ENABLED} + LLM_REQUEST_TIMEOUT: ${LLM_REQUEST_TIMEOUT:?missing_LLM_REQUEST_TIMEOUT} AUTO_CREATE_TABLES: "true" - GLM_API_KEY: ${GLM_API_KEY:-} - GLM_BASE_URL: ${GLM_BASE_URL:-https://open.bigmodel.cn/api/paas/v4} - GLM_MODEL: ${GLM_MODEL:-GLM-4.7} - OPENAI_API_KEY: ${OPENAI_API_KEY:-} - OPENAI_MODEL: ${OPENAI_MODEL:-MiniMax-M2.5} - DEEPSEEK_API_KEY: ${DEEPSEEK_API_KEY:-} - OLLAMA_HOST: ${OLLAMA_HOST:-http://host.docker.internal:11434} - OLLAMA_BASE_URL: ${OLLAMA_BASE_URL:-http://host.docker.internal:11434} - OLLAMA_MODEL: ${OLLAMA_MODEL:-llama3:latest} + GLM_API_KEY: ${GLM_API_KEY:?missing_GLM_API_KEY} + GLM_BASE_URL: ${GLM_BASE_URL:?missing_GLM_BASE_URL} + GLM_MODEL: ${GLM_MODEL:?missing_GLM_MODEL} + OPENAI_API_KEY: ${OPENAI_API_KEY:?missing_OPENAI_API_KEY} + OPENAI_MODEL: ${OPENAI_MODEL:?missing_OPENAI_MODEL} + DEEPSEEK_API_KEY: ${DEEPSEEK_API_KEY} + OLLAMA_HOST: ${OLLAMA_HOST:?missing_OLLAMA_HOST} + OLLAMA_BASE_URL: ${OLLAMA_BASE_URL:?missing_OLLAMA_BASE_URL} + OLLAMA_MODEL: ${OLLAMA_MODEL:?missing_OLLAMA_MODEL} PYTHONUNBUFFERED: "1" command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload", "--reload-dir", "app"] volumes: - ./backend:/app/backend + - ./.openclaw:/app/.openclaw:ro - ./logs:/app/logs ports: - - "${BACKEND_PORT:-8000}:8000" + - "${BACKEND_PORT:?missing_BACKEND_PORT}:8000" healthcheck: test: ["CMD-SHELL", "curl -fsS http://localhost:8000/healthz >/dev/null || exit 1"] interval: 10s diff --git a/docker-compose.yml b/docker-compose.yml index de5889d5..454ceffc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -55,37 +55,41 @@ services: redis: condition: service_healthy environment: - DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-pmflow}:${POSTGRES_PASSWORD:-pmflow_pass}@db:5432/${POSTGRES_DB:-pmflow} - REDIS_URL: redis://:${REDIS_PASSWORD:-pmflow_redis_pass}@redis:6379/0 - CORS_ORIGINS: ${CORS_ORIGINS:-http://localhost:5174,http://127.0.0.1:5174} - LLM_PROVIDER: ${LLM_PROVIDER:-openai} - LLM_FALLBACK_PROVIDER: ${LLM_FALLBACK_PROVIDER:-ollama} - OPENAI_BASE_URL: ${OPENAI_BASE_URL:-http://openclaw:18789/v1} - OPENCLAW_ROUTING_ENABLED: ${OPENCLAW_ROUTING_ENABLED:-true} - OPENCLAW_ROUTING_RULES_PATH: ${OPENCLAW_ROUTING_RULES_PATH:-/app/backend/.openclaw/pmflow-routing-rules.json} - LLM_MIXED_ROUTING_ENABLED: ${LLM_MIXED_ROUTING_ENABLED:-false} - LLM_LOW_TO_HIGH_ESCALATION_ENABLED: ${LLM_LOW_TO_HIGH_ESCALATION_ENABLED:-true} - LLM_ROUTING_LOW_PROVIDER: ${LLM_ROUTING_LOW_PROVIDER:-glm} - LLM_ROUTING_LOW_MODEL: ${LLM_ROUTING_LOW_MODEL:-GLM-4.7} - LLM_ROUTING_HIGH_PROVIDER: ${LLM_ROUTING_HIGH_PROVIDER:-openai} - LLM_ROUTING_HIGH_MODEL: ${LLM_ROUTING_HIGH_MODEL:-MiniMax-M2.5} - LLM_ROUTING_SAFETY_PROVIDER: ${LLM_ROUTING_SAFETY_PROVIDER:-glm} - LLM_ROUTING_SAFETY_MODEL: ${LLM_ROUTING_SAFETY_MODEL:-GLM-4.7} - LLM_DIRECT_EMERGENCY_ENABLED: ${LLM_DIRECT_EMERGENCY_ENABLED:-false} - LLM_REQUEST_TIMEOUT: ${LLM_REQUEST_TIMEOUT:-90} + DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:?missing_POSTGRES_USER}:${POSTGRES_PASSWORD:?missing_POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB:?missing_POSTGRES_DB} + REDIS_URL: redis://:${REDIS_PASSWORD:?missing_REDIS_PASSWORD}@redis:6379/0 + PMFLOW_EXPECTED_DB_DIALECT: ${PMFLOW_EXPECTED_DB_DIALECT:?missing_PMFLOW_EXPECTED_DB_DIALECT} + CORS_ORIGINS: ${CORS_ORIGINS:?missing_CORS_ORIGINS} + LLM_PROVIDER: ${LLM_PROVIDER:?missing_LLM_PROVIDER} + LLM_FALLBACK_PROVIDER: ${LLM_FALLBACK_PROVIDER:?missing_LLM_FALLBACK_PROVIDER} + OPENAI_BASE_URL: ${OPENAI_BASE_URL:?missing_OPENAI_BASE_URL} + OPENCLAW_ROUTING_ENABLED: ${OPENCLAW_ROUTING_ENABLED:?missing_OPENCLAW_ROUTING_ENABLED} + OPENCLAW_ROUTING_RULES_PATH: ${OPENCLAW_ROUTING_RULES_PATH:?missing_OPENCLAW_ROUTING_RULES_PATH} + OPENCLAW_AGENTS_DEFAULT_TIMEOUT_SECONDS: ${OPENCLAW_AGENTS_DEFAULT_TIMEOUT_SECONDS:?missing_OPENCLAW_AGENTS_DEFAULT_TIMEOUT_SECONDS} + LLM_MIXED_ROUTING_ENABLED: ${LLM_MIXED_ROUTING_ENABLED:?missing_LLM_MIXED_ROUTING_ENABLED} + LLM_ROUTING_DEFAULT_TIER: ${LLM_ROUTING_DEFAULT_TIER:?missing_LLM_ROUTING_DEFAULT_TIER} + LLM_LOW_TO_HIGH_ESCALATION_ENABLED: ${LLM_LOW_TO_HIGH_ESCALATION_ENABLED:?missing_LLM_LOW_TO_HIGH_ESCALATION_ENABLED} + LLM_ROUTING_LOW_PROVIDER: ${LLM_ROUTING_LOW_PROVIDER:?missing_LLM_ROUTING_LOW_PROVIDER} + LLM_ROUTING_LOW_MODEL: ${LLM_ROUTING_LOW_MODEL:?missing_LLM_ROUTING_LOW_MODEL} + LLM_ROUTING_HIGH_PROVIDER: ${LLM_ROUTING_HIGH_PROVIDER:?missing_LLM_ROUTING_HIGH_PROVIDER} + LLM_ROUTING_HIGH_MODEL: ${LLM_ROUTING_HIGH_MODEL:?missing_LLM_ROUTING_HIGH_MODEL} + LLM_ROUTING_SAFETY_PROVIDER: ${LLM_ROUTING_SAFETY_PROVIDER:?missing_LLM_ROUTING_SAFETY_PROVIDER} + LLM_ROUTING_SAFETY_MODEL: ${LLM_ROUTING_SAFETY_MODEL:?missing_LLM_ROUTING_SAFETY_MODEL} + LLM_DIRECT_EMERGENCY_ENABLED: ${LLM_DIRECT_EMERGENCY_ENABLED:?missing_LLM_DIRECT_EMERGENCY_ENABLED} + LLM_REQUEST_TIMEOUT: ${LLM_REQUEST_TIMEOUT:?missing_LLM_REQUEST_TIMEOUT} AUTO_CREATE_TABLES: "true" - GLM_API_KEY: ${GLM_API_KEY:-} - GLM_BASE_URL: ${GLM_BASE_URL:-https://open.bigmodel.cn/api/paas/v4} - GLM_MODEL: ${GLM_MODEL:-GLM-4.7} - OPENAI_API_KEY: ${OPENAI_API_KEY:-} - OPENAI_MODEL: ${OPENAI_MODEL:-MiniMax-M2.5} - DEEPSEEK_API_KEY: ${DEEPSEEK_API_KEY:-} - OLLAMA_HOST: ${OLLAMA_HOST:-http://host.docker.internal:11434} - OLLAMA_BASE_URL: ${OLLAMA_BASE_URL:-http://host.docker.internal:11434} - OLLAMA_MODEL: ${OLLAMA_MODEL:-llama3:latest} + GLM_API_KEY: ${GLM_API_KEY:?missing_GLM_API_KEY} + GLM_BASE_URL: ${GLM_BASE_URL:?missing_GLM_BASE_URL} + GLM_MODEL: ${GLM_MODEL:?missing_GLM_MODEL} + OPENAI_API_KEY: ${OPENAI_API_KEY:?missing_OPENAI_API_KEY} + OPENAI_MODEL: ${OPENAI_MODEL:?missing_OPENAI_MODEL} + DEEPSEEK_API_KEY: ${DEEPSEEK_API_KEY} + OLLAMA_HOST: ${OLLAMA_HOST:?missing_OLLAMA_HOST} + OLLAMA_BASE_URL: ${OLLAMA_BASE_URL:?missing_OLLAMA_BASE_URL} + OLLAMA_MODEL: ${OLLAMA_MODEL:?missing_OLLAMA_MODEL} ports: - - "${BACKEND_PORT:-8000}:8000" + - "${BACKEND_PORT:?missing_BACKEND_PORT}:8000" volumes: + - ./.openclaw:/app/.openclaw:ro - ./logs:/app/logs healthcheck: test: ["CMD-SHELL", "curl -fsS http://localhost:8000/healthz >/dev/null || exit 1"] diff --git a/frontend/src/api/aiSteward.ts b/frontend/src/api/aiSteward.ts index c18b72c5..5f2bcf6f 100644 --- a/frontend/src/api/aiSteward.ts +++ b/frontend/src/api/aiSteward.ts @@ -63,6 +63,21 @@ export interface OpenClawCommandDispatchResponse { }; } +export type LLMProvider = "deepseek" | "glm" | "openai" | "ollama"; + +export interface LLMProviderPreset { + model: string; + base_url: string; + api_env: string; +} + +export interface LLMProviderPresetResponse { + low: string; + high: string; + base_url: string; + providers: Record; +} + export const aiStewardApi = { async dispatchCommand(body: OpenClawCommandDispatchRequest): Promise { const raw = await apiRequest>("/ai-steward/command/dispatch", { @@ -71,4 +86,8 @@ export const aiStewardApi = { }); return unwrap(raw); }, + async getLlmProviderPresets(): Promise { + const raw = await apiRequest>("/ai-steward/llm/providers"); + return unwrap(raw); + }, }; diff --git a/frontend/src/pages/AIStewardPage.tsx b/frontend/src/pages/AIStewardPage.tsx index 530b83ef..8b0d77a9 100644 --- a/frontend/src/pages/AIStewardPage.tsx +++ b/frontend/src/pages/AIStewardPage.tsx @@ -20,6 +20,7 @@ import { formatProjectNameWithId, toZhPriority, toZhSeverity, toZhStatus } from import { formatBeijingDate } from "@/lib/datetime"; import { QAHistoryPanel } from "@/components/ai-steward/QAHistoryPanel"; import { OpenClawCommanderPanel } from "@/components/ai-steward/OpenClawCommanderPanel"; +import { aiStewardApi } from "@/api/aiSteward"; // ==================== Types ==================== @@ -83,7 +84,7 @@ const PROVIDER_ZH: Record = { ollama: "Ollama(本地)", }; -const ENGINE_PROVIDER_PRESETS: Record = { +const ENGINE_PROVIDER_FALLBACK_PRESETS: Record = { deepseek: { model: "deepseek-chat", base_url: "https://api.deepseek.com/v1", @@ -119,12 +120,15 @@ export default function AIStewardPage() { const [briefing, setBriefing] = useState(null); const [featureLoading, setFeatureLoading] = useState(null); const [engineData, setEngineData] = useState({ active_engine_id: null, engines: [] }); + const [engineProviderPresets, setEngineProviderPresets] = useState>( + ENGINE_PROVIDER_FALLBACK_PRESETS + ); const [engineForm, setEngineForm] = useState({ name: "", provider: "glm" as "deepseek" | "glm" | "openai" | "ollama", api_key: "", - model: ENGINE_PROVIDER_PRESETS.glm.model, - base_url: ENGINE_PROVIDER_PRESETS.glm.base_url, + model: ENGINE_PROVIDER_FALLBACK_PRESETS.glm.model, + base_url: ENGINE_PROVIDER_FALLBACK_PRESETS.glm.base_url, }); const [engineEditingId, setEngineEditingId] = useState(null); const [engineMessage, setEngineMessage] = useState<{ type: "success" | "error"; text: string } | null>(null); @@ -276,14 +280,51 @@ export default function AIStewardPage() { } }; + const loadEngineProviderPresets = async () => { + try { + const data = await aiStewardApi.getLlmProviderPresets(); + const providers = data?.providers ?? {}; + const nextPresets: Record = { + deepseek: { + model: providers.deepseek?.model || ENGINE_PROVIDER_FALLBACK_PRESETS.deepseek.model, + base_url: providers.deepseek?.base_url || ENGINE_PROVIDER_FALLBACK_PRESETS.deepseek.base_url, + }, + glm: { + model: providers.glm?.model || data?.low || ENGINE_PROVIDER_FALLBACK_PRESETS.glm.model, + base_url: providers.glm?.base_url || data?.base_url || ENGINE_PROVIDER_FALLBACK_PRESETS.glm.base_url, + }, + openai: { + model: providers.openai?.model || data?.high || ENGINE_PROVIDER_FALLBACK_PRESETS.openai.model, + base_url: providers.openai?.base_url || data?.base_url || ENGINE_PROVIDER_FALLBACK_PRESETS.openai.base_url, + }, + ollama: { + model: providers.ollama?.model || ENGINE_PROVIDER_FALLBACK_PRESETS.ollama.model, + base_url: providers.ollama?.base_url || ENGINE_PROVIDER_FALLBACK_PRESETS.ollama.base_url, + }, + }; + setEngineProviderPresets(nextPresets); + setEngineForm((prev) => { + if (engineEditingId) return prev; + const preset = nextPresets[prev.provider]; + return { + ...prev, + model: preset.model, + base_url: preset.base_url, + }; + }); + } catch (err) { + console.error("Failed to load provider presets:", err); + } + }; + const resetEngineForm = () => { setEngineEditingId(null); setEngineForm({ name: "", provider: "glm", api_key: "", - model: ENGINE_PROVIDER_PRESETS.glm.model, - base_url: ENGINE_PROVIDER_PRESETS.glm.base_url, + model: engineProviderPresets.glm.model, + base_url: engineProviderPresets.glm.base_url, }); }; @@ -479,7 +520,10 @@ export default function AIStewardPage() { if (activeTab === "conflicts") loadConflicts(); if (activeTab === "blockers") loadBlockers(); if (activeTab === "briefing") loadBriefing(); - if (activeTab === "engine") loadEngineConfigs(); + if (activeTab === "engine") { + loadEngineProviderPresets(); + loadEngineConfigs(); + } }, [activeTab]); // ==================== Render ==================== @@ -1559,7 +1603,7 @@ export default function AIStewardPage() { onChange={(e) => setEngineForm((prev) => { const provider = e.target.value as "deepseek" | "glm" | "openai" | "ollama"; - const preset = ENGINE_PROVIDER_PRESETS[provider]; + const preset = engineProviderPresets[provider]; return { ...prev, provider, diff --git a/mdproject/docker-redis-context-plan-en.md b/mdproject/docker-redis-context-plan-en.md index cfb07e3b..2a28352d 100644 --- a/mdproject/docker-redis-context-plan-en.md +++ b/mdproject/docker-redis-context-plan-en.md @@ -142,7 +142,7 @@ Return answer - [x] Added DB/Redis health checks and `depends_on` health gating. - [x] Added backend container build/runtime file: `backend/Dockerfile`. - [x] Added backend dependency lock baseline: `backend/requirements.txt`. -- [x] Added docker env template: `.env.example` and updated `backend/.env.example`. +- [x] Added docker env template: `.env.example` as the single environment template source. - [x] Redis policy configured to `maxmemory 512mb` + `allkeys-lru`. ### Part 2 — Database Migration Steps diff --git a/scripts/openclaw_routing_lint.py b/scripts/openclaw_routing_lint.py index beee4a07..d5bceda2 100644 --- a/scripts/openclaw_routing_lint.py +++ b/scripts/openclaw_routing_lint.py @@ -5,11 +5,15 @@ from __future__ import annotations import argparse import json +import os from pathlib import Path from typing import Any, Optional VALID_PROVIDERS = {"glm", "openai", "deepseek", "ollama"} VALID_TIERS = {"low", "high", "safety"} +DEFAULT_RULES_RELATIVE_PATH = "../.openclaw/pmflow-routing-rules.json" +SCRIPT_ROOT = Path(__file__).resolve().parents[1] +BACKEND_DIR = SCRIPT_ROOT / "backend" LOW_REQUIRED = { "feishu_level_classification", @@ -76,6 +80,14 @@ def _resolve_primary_tier(function_name: str, rules: list[dict[str, Any]]) -> Op return scored[0][2] +def _resolve_rules_path(raw: str) -> Path: + token = str(raw or "").strip() or DEFAULT_RULES_RELATIVE_PATH + path = Path(token) + if path.is_absolute(): + return path + return (BACKEND_DIR / path).resolve() + + def _lint(path: Path) -> tuple[bool, dict[str, Any]]: if not path.exists(): return False, {"error": f"rules file not found: {path}"} @@ -135,9 +147,14 @@ def _lint(path: Path) -> tuple[bool, dict[str, Any]]: break model_hints = { - "low": "zai/glm-4-flash", - "high": "minimax/MiniMax-M2.5", - "safety": "deepseek/deepseek-chat", + "low": "GLM-4.7", + "high": "MiniMax-M2.5", + "safety": "deepseek-chat", + } + api_env_hints = { + "low": "GLM_API_KEY", + "high": "OPENAI_API_KEY", + "safety": "OPENAI_API_KEY", } for tier, expected_model in model_hints.items(): tier_rows = [ @@ -146,6 +163,9 @@ def _lint(path: Path) -> tuple[bool, dict[str, Any]]: ] if tier_rows and not any(str(row.get("model") or "").strip() == expected_model for row in tier_rows): problems.append(f"tier={tier} missing expected model hint: {expected_model}") + expected_api_env = api_env_hints[tier] + if tier_rows and not any(str(row.get("api_env") or "").strip() == expected_api_env for row in tier_rows): + problems.append(f"tier={tier} missing expected api_env hint: {expected_api_env}") return len(problems) == 0, { "path": str(path), @@ -159,15 +179,22 @@ def _lint(path: Path) -> tuple[bool, dict[str, Any]]: def main() -> None: + default_input = str( + os.getenv( + "OPENCLAW_ROUTING_RULES_PATH", + DEFAULT_RULES_RELATIVE_PATH, + ) + or DEFAULT_RULES_RELATIVE_PATH + ).strip() or DEFAULT_RULES_RELATIVE_PATH parser = argparse.ArgumentParser(description="OpenClaw routing lint") parser.add_argument( "--input", - default=".openclaw/pmflow-routing-rules.json", + default=default_input, help="Rules file path", ) args = parser.parse_args() - ok, report = _lint(Path(args.input)) + ok, report = _lint(_resolve_rules_path(args.input)) print(json.dumps(report, ensure_ascii=False, indent=2)) raise SystemExit(0 if ok else 2) diff --git a/tests/test_llm_fail_fast.py b/tests/test_llm_fail_fast.py index 88d5ab2d..83cfa36f 100644 --- a/tests/test_llm_fail_fast.py +++ b/tests/test_llm_fail_fast.py @@ -30,7 +30,7 @@ def test_provider_circuit_breaker_opens_after_threshold(): circuit_key = service._provider_circuit_key( provider="openai", base_url="https://example.com/v1", - model="gpt-4o-mini", + model="MiniMax-M2.5", ) LLMService._provider_circuit_state.pop(circuit_key, None) diff --git a/用户使用手册.md b/用户使用手册.md index 199b0ebd..f791b506 100644 --- a/用户使用手册.md +++ b/用户使用手册.md @@ -60,7 +60,7 @@ **选项 A:使用云端 API(推荐,电脑配置低)** -编辑 `backend/.env` 文件: +编辑根目录 `.env` 文件: ```bash # 使用 DeepSeek(便宜,¥1/百万 tokens) @@ -96,7 +96,7 @@ ollama pull llama3:8b ollama pull nomic-embed-text ``` -编辑 `backend/.env` 文件: +编辑根目录 `.env` 文件: ```bash LLM_PROVIDER=ollama @@ -116,10 +116,10 @@ source venv/bin/activate # Windows: venv\Scripts\activate pip install -r requirements.txt # 配置环境变量 (可选,使用默认配置) -cp .env.example .env +cp ../.env.example ../.env -# 编辑 .env 设置你的 LLM 提供商和 API 密钥 -vim .env +# 编辑项目根目录 .env 设置你的 LLM 提供商和 API 密钥 +vim ../.env # 运行数据库迁移 alembic upgrade head @@ -302,7 +302,7 @@ curl -X POST http://localhost:8000/signals/ingest \ ### 5.1 环境变量配置 -编辑 `backend/.env` 文件: +编辑根目录 `.env` 文件: ```bash # 数据库配置 @@ -399,7 +399,7 @@ pip install -r requirements.txt **解决方案 A - 使用 SQLite(开发环境)**: -编辑 `backend/.env`: +编辑根目录 `.env`: ```bash DATABASE_URL=sqlite+aiosqlite:///./neural_sync.db ``` @@ -604,8 +604,7 @@ Neural Sync/ │ │ ├── database.py # 数据库连接 │ │ └── services/ # 业务逻辑 │ ├── alembic/ # 数据库迁移 -│ ├── requirements.txt -│ └── .env.example # 环境变量模板 +│ └── requirements.txt │ ├── frontend/ # Vue 3 前端 │ ├── src/ @@ -615,8 +614,9 @@ Neural Sync/ │ ├── package.json │ └── vite.config.ts │ +├── .env.example # 环境变量模板 ├── CLAUDE.md # Claude Code 指导 -└── README.md # 项目说明 +└── README.md # 项目说明 ``` ---