制定双Agent能力提升与路由治理方案
This commit is contained in:
parent
331e9109e2
commit
2fd932e395
43
.env.example
43
.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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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`),不使用供应商前缀模型名
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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_GATEWAY_TOKEN>
|
||||
|
||||
# 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
|
||||
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,不要额外文本。
|
||||
"""
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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,避免相对路径导致多库混用
|
||||
|
|
|
|||
|
|
@ -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(" ,,::")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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<LLMProvider, LLMProviderPreset>;
|
||||
}
|
||||
|
||||
export const aiStewardApi = {
|
||||
async dispatchCommand(body: OpenClawCommandDispatchRequest): Promise<OpenClawCommandDispatchResponse> {
|
||||
const raw = await apiRequest<ApiEnvelope<OpenClawCommandDispatchResponse>>("/ai-steward/command/dispatch", {
|
||||
|
|
@ -71,4 +86,8 @@ export const aiStewardApi = {
|
|||
});
|
||||
return unwrap(raw);
|
||||
},
|
||||
async getLlmProviderPresets(): Promise<LLMProviderPresetResponse> {
|
||||
const raw = await apiRequest<ApiEnvelope<LLMProviderPresetResponse>>("/ai-steward/llm/providers");
|
||||
return unwrap(raw);
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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<LLMEngine["provider"], string> = {
|
|||
ollama: "Ollama(本地)",
|
||||
};
|
||||
|
||||
const ENGINE_PROVIDER_PRESETS: Record<LLMEngine["provider"], { model: string; base_url: string }> = {
|
||||
const ENGINE_PROVIDER_FALLBACK_PRESETS: Record<LLMEngine["provider"], { model: string; base_url: string }> = {
|
||||
deepseek: {
|
||||
model: "deepseek-chat",
|
||||
base_url: "https://api.deepseek.com/v1",
|
||||
|
|
@ -119,12 +120,15 @@ export default function AIStewardPage() {
|
|||
const [briefing, setBriefing] = useState<DailyBriefing | null>(null);
|
||||
const [featureLoading, setFeatureLoading] = useState<string | null>(null);
|
||||
const [engineData, setEngineData] = useState<LLMEngineListResponse>({ active_engine_id: null, engines: [] });
|
||||
const [engineProviderPresets, setEngineProviderPresets] = useState<Record<LLMEngine["provider"], { model: string; base_url: string }>>(
|
||||
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<string | null>(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<LLMEngine["provider"], { model: string; base_url: string }> = {
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
18
用户使用手册.md
18
用户使用手册.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,6 +614,7 @@ Neural Sync/
|
|||
│ ├── package.json
|
||||
│ └── vite.config.ts
|
||||
│
|
||||
├── .env.example # 环境变量模板
|
||||
├── CLAUDE.md # Claude Code 指导
|
||||
└── README.md # 项目说明
|
||||
```
|
||||
|
|
|
|||
Loading…
Reference in New Issue