diff --git a/项目核心文档汇总.md b/(红头)项目核心文档汇总.md similarity index 82% rename from 项目核心文档汇总.md rename to (红头)项目核心文档汇总.md index f29f3c5..9fb6535 100644 --- a/项目核心文档汇总.md +++ b/(红头)项目核心文档汇总.md @@ -121,7 +121,7 @@ - **工具节点**: 定时任务、Agent节点 ### 5. 内置工具调用 -平台提供8个内置工具,可在LLM节点中启用: +平台提供10个内置工具,可在LLM节点中启用: 1. 🌐 **http_request** - HTTP请求工具 2. 📖 **file_read** - 文件读取工具 3. ✍️ **file_write** - 文件写入工具 @@ -130,6 +130,8 @@ 6. 🔢 **math_calculate** - 数学计算工具 7. 💻 **system_info** - 系统信息工具 8. 📦 **json_process** - JSON处理工具 +9. 🗄️ **database_query** - 数据库查询工具(只读查询场景) +10. 📱 **adb_log** - Android/设备日志工具 ### 6. 执行管理 - 创建执行任务 @@ -145,14 +147,26 @@ - 数据查询功能 ### 8. 模型配置管理 -- 模型配置CRUD API -- 多模型支持(OpenAI、DeepSeek等) +- 模型配置CRUD API(API Key 等在服务端加密存储;工作流 LLM 节点可绑定 `model_config_id` 由可信所有者运行时解密注入) +- 多模型支持(OpenAI、Anthropic 兼容路径、DeepSeek 等) - 模型切换和配置 +**DeepSeek API(OpenAI 兼容格式)** +与 [DeepSeek API 文档](https://api-docs.deepseek.com/) 对齐的常用参数如下(密钥需在控制台申请,勿写入文档或仓库): + +| 参数 | 说明 | +|------|------| +| **base_url** | `https://api.deepseek.com`(OpenAI 兼容;Anthropic 兼容格式为 `https://api.deepseek.com/anthropic`) | +| **主推模型** | `deepseek-v4-flash`、`deepseek-v4-pro` | +| **兼容旧名(计划弃用)** | `deepseek-chat`、`deepseek-reasoner` 计划于 **2026/07/24** 弃用;二者分别对应 `deepseek-v4-flash` 的非思考与思考模式 | + +本平台侧:**新建 LLM 节点、节点模板及后端未显式指定模型时的 DeepSeek 默认值**为 **`deepseek-v4-flash`**;工作流编辑器与「模型配置」页下拉仍可选择兼容旧模型名并标注弃用时间。 + ### 9. Agent管理 - Agent CRUD API - Agent管理页面 - Agent协作功能 +- 批量场景Agent脚本(教育/企业/政务/媒体) ## 项目结构 @@ -255,7 +269,8 @@ pnpm dev ``` ### 服务访问地址 -- **前端**: http://localhost:8038 +- **前端(本地开发推荐)**: http://localhost:3001 +- **前端(Docker方案)**: http://localhost:8038 - **后端API**: http://localhost:8037 - **API文档**: http://localhost:8037/docs - **Redis**: localhost:6379 @@ -305,7 +320,7 @@ pnpm dev ### 节点类型说明 - **开始节点**:工作流起始节点 - **输入节点**:数据输入节点,可配置输入参数 -- **LLM节点**:AI模型处理节点,支持OpenAI、DeepSeek等模型 +- **LLM节点**:AI 模型处理节点,支持 OpenAI、DeepSeek 等;DeepSeek 推荐选用 **`deepseek-v4-flash`** 或 **`deepseek-v4-pro`**(旧名 `deepseek-chat` / `deepseek-reasoner` 仍可兼容使用,计划 **2026/07/24** 弃用) - **条件节点**:条件判断节点,支持表达式配置 - **转换节点**:数据转换节点,支持JSONPath、模板等 - **输出节点**:数据输出节点,可配置输出格式 @@ -352,10 +367,9 @@ alembic downgrade -1 ``` ### 添加新节点类型 -1. 后端:在 `backend/app/core/nodes/` 中添加节点执行器 -2. 后端:在 `backend/app/core/workflow_engine.py` 中注册节点类型 -3. 前端:在 `frontend/src/components/nodes/` 中添加节点组件 -4. 前端:在 `frontend/src/stores/workflowStore.ts` 中注册节点类型 +1. 后端:在 `backend/app/services/workflow_engine.py` 的 `execute_node` 中增加节点类型分支;如需校验,可扩展 `workflow_validator.py` +2. 前端:在 `frontend/src/components/nodes/`(或工作流编辑器关联组件)中增加节点展示与配置 +3. 前端:在 `frontend/src/stores/workflowStore.ts` 等处注册节点类型与默认数据 ## 测试指南 @@ -376,7 +390,7 @@ alembic downgrade -1 ## 内置工具列表 -平台提供8个内置工具,详细功能如下: +平台提供10个内置工具,详细功能如下: | 工具名称 | 功能描述 | 主要参数 | |---------|---------|---------| @@ -388,6 +402,10 @@ alembic downgrade -1 | math_calculate | 执行数学计算 | expression | | system_info | 获取系统信息 | 无 | | json_process | 处理JSON数据 | json_string, operation | +| database_query | 只读数据库查询 | datasource/query(按工具schema) | +| adb_log | 设备日志读取 | command/serial/lines(按工具schema) | + +> 说明:`http_request` 已加入响应体截断与头部精简机制(支持 `max_body_chars`),用于避免大页面导致 LLM 上下文超限。 详细使用示例请参考:[内置工具列表.md](./内置工具列表.md) @@ -406,9 +424,11 @@ alembic downgrade -1 3. **工作流执行引擎** - DAG构建、拓扑排序、节点执行 4. **可视化编辑器** - 拖拽节点、连线、配置面板 5. **异步任务处理** - Celery集成,支持长时间运行的任务 -6. **多模型支持** - OpenAI、DeepSeek集成 -7. **内置工具调用** - 8个内置工具支持 +6. **多模型支持** - OpenAI、DeepSeek 等(DeepSeek 主推 `deepseek-v4-flash` / `deepseek-v4-pro`) +7. **内置工具调用** - 10 个内置工具支持 8. **实时状态推送** - WebSocket实时推送执行状态 +9. **批量Agent场景生成** - 教育与政务/媒体场景批量创建脚本 +10. **Windows运维文档统一** - 启停/重启流程合并为单一权威文档 ### 近期开发重点(高优先级) 1. **监控和告警前端界面** - 系统监控面板、告警规则管理 @@ -451,12 +471,21 @@ alembic downgrade -1 ## 联系和支持 - **API文档**:http://localhost:8037/docs -- **前端服务**:http://localhost:8038 +- **前端服务(本地开发)**:http://localhost:3001 +- **前端服务(Docker)**:http://localhost:8038 - **问题反馈**:查看项目文档或联系开发团队 +## 关键文档索引(建议) + +- Windows 启停唯一文档:[`(红头)Windows服务器启动与重启唯一指南.md`](./(红头)Windows服务器启动与重启唯一指南.md) +- 上传图片与OCR实现:[`(红头)上传图片和识别的实现文档.md`](./(红头)上传图片和识别的实现文档.md) +- 教育行业批量Agent脚本:`backend/scripts/create_education_agents_batch.py` +- 政务/媒体批量Agent脚本:`backend/scripts/create_gov_media_agents_batch.py` +- 企业场景批量Agent脚本:`backend/scripts/create_enterprise_scenario_agents.py` + --- -**最后更新**: 2026-04-06 -**文档版本**: 1.0 +**最后更新**: 2026-04-30 +**文档版本**: 1.2 -*本文档基于项目现有文档整理生成,涵盖项目核心信息。详细技术方案请参考[方案-优化版.md](./方案-优化版.md)。* \ No newline at end of file +*本文档基于项目现有文档整理生成,涵盖项目核心信息。详细技术方案请参考[方案-优化版.md](./方案-优化版.md)。DeepSeek 模型名与 Base URL 以官方文档为准,变更时请同步修订本节。* \ No newline at end of file diff --git a/backend/app/api/node_templates.py b/backend/app/api/node_templates.py index 3420532..6df6009 100644 --- a/backend/app/api/node_templates.py +++ b/backend/app/api/node_templates.py @@ -27,7 +27,7 @@ class NodeTemplateCreate(BaseModel): prompt: str = Field(..., min_length=1, description="提示词模板") variables: Optional[List[Dict[str, Any]]] = Field(None, description="变量定义列表") provider: Optional[str] = Field("deepseek", description="默认LLM提供商") - model: Optional[str] = Field("deepseek-chat", description="默认模型") + model: Optional[str] = Field("deepseek-v4-flash", description="默认模型") temperature: Optional[str] = Field("0.7", description="默认温度参数") max_tokens: Optional[int] = Field(1500, description="默认最大token数") is_public: Optional[bool] = Field(False, description="是否公开") @@ -176,7 +176,7 @@ async def create_node_template( prompt=template_data.prompt, variables=template_data.variables or [], provider=template_data.provider or "deepseek", - model=template_data.model or "deepseek-chat", + model=template_data.model or "deepseek-v4-flash", temperature=template_data.temperature or "0.7", max_tokens=template_data.max_tokens or 1500, is_public=template_data.is_public or False, diff --git a/backend/app/api/node_test.py b/backend/app/api/node_test.py index c10d1ee..e0194d8 100644 --- a/backend/app/api/node_test.py +++ b/backend/app/api/node_test.py @@ -60,7 +60,12 @@ async def test_node( "edges": [] } - engine = WorkflowEngine("test-node", workflow_data, db=db) + engine = WorkflowEngine( + "test-node", + workflow_data, + db=db, + trusted_model_config_user_id=str(current_user.id), + ) # 执行节点 result = await engine.execute_node(node, input_data) diff --git a/backend/app/models/node_template.py b/backend/app/models/node_template.py index da0c24f..bd3afcb 100644 --- a/backend/app/models/node_template.py +++ b/backend/app/models/node_template.py @@ -25,7 +25,7 @@ class NodeTemplate(Base): # 默认配置 provider = Column(String(50), default="deepseek", comment="默认LLM提供商") - model = Column(String(100), default="deepseek-chat", comment="默认模型") + model = Column(String(100), default="deepseek-v4-flash", comment="默认模型") temperature = Column(String(10), default="0.7", comment="默认温度参数") max_tokens = Column(Integer, default=1500, comment="默认最大token数") diff --git a/backend/app/services/llm_service.py b/backend/app/services/llm_service.py index 1e2ac3f..00f23fa 100644 --- a/backend/app/services/llm_service.py +++ b/backend/app/services/llm_service.py @@ -480,7 +480,7 @@ class LLMService: async def call_deepseek( self, prompt: str, - model: str = "deepseek-chat", + model: str = "deepseek-v4-flash", temperature: float = 0.7, max_tokens: Optional[int] = None, api_key: Optional[str] = None, @@ -492,7 +492,7 @@ class LLMService: Args: prompt: 提示词 - model: 模型名称,默认deepseek-chat + model: 模型名称,默认 deepseek-v4-flash(deepseek-chat / deepseek-reasoner 将于 2026/07/24 弃用) temperature: 温度参数,默认0.7 max_tokens: 最大token数 api_key: API密钥(可选,如果不提供则使用默认配置) @@ -610,7 +610,7 @@ class LLMService: elif provider == "deepseek": # 默认模型 if not model: - model = "deepseek-chat" + model = "deepseek-v4-flash" return await self.call_deepseek( prompt=prompt, model=model, @@ -852,7 +852,7 @@ class LLMService: self, prompt: str, tools: List[Dict[str, Any]], - model: str = "deepseek-chat", + model: str = "deepseek-v4-flash", temperature: float = 0.7, max_tokens: Optional[int] = None, api_key: Optional[str] = None, @@ -928,7 +928,7 @@ class LLMService: ) elif provider == "deepseek": if not model: - model = "deepseek-chat" + model = "deepseek-v4-flash" return await self.call_deepseek_with_tools( prompt=prompt, tools=tools, diff --git a/backend/app/services/workflow_engine.py b/backend/app/services/workflow_engine.py index 9756ba4..b745fde 100644 --- a/backend/app/services/workflow_engine.py +++ b/backend/app/services/workflow_engine.py @@ -92,6 +92,7 @@ class WorkflowEngine: logger=None, db=None, budget_limits: Optional[Dict[str, Any]] = None, + trusted_model_config_user_id: Optional[str] = None, ): """ 初始化工作流引擎 @@ -101,6 +102,7 @@ class WorkflowEngine: workflow_data: 工作流数据(包含nodes和edges) logger: 执行日志记录器(可选) db: 数据库会话(可选,用于Agent节点加载Agent配置) + trusted_model_config_user_id: 允许加载「模型配置」解密密钥的用户 ID(通常为当前执行所属 Workflow/Agent 的 owner) """ self.workflow_id = workflow_id self.nodes = {node['id']: node for node in workflow_data.get('nodes', [])} @@ -115,6 +117,7 @@ class WorkflowEngine: self._llm_invocations: int = 0 self._tool_calls_used: int = 0 self.budget_limits: Dict[str, Any] = dict(budget_limits or {}) + self.trusted_model_config_user_id: Optional[str] = trusted_model_config_user_id self._cap_steps: int = max( 1, int(getattr(settings, "WORKFLOW_MAX_STEPS_PER_RUN", 2000) or 2000) ) @@ -178,6 +181,71 @@ class WorkflowEngine: detail=f"已超过工具调用预算({self._cap_tool} 次)", ) + def _resolve_llm_credentials_from_model_config( + self, node_data: Dict[str, Any] + ) -> Optional[Dict[str, Any]]: + """ + 若节点 data 含 model_config_id,则从数据库加载加密密钥并校验归属; + 返回 {"api_key","base_url","provider","model"},否则返回 None。 + """ + raw = node_data.get("model_config_id") or node_data.get("modelConfigId") + if raw is None or raw == "": + return None + cfg_id = str(raw).strip() + if not cfg_id: + return None + + if not self.trusted_model_config_user_id: + logger.warning( + "LLM 节点配置了 model_config_id=%s,但未绑定 trusted_model_config_user_id," + "将跳过模型配置密钥注入(仍使用节点与环境变量)。", + cfg_id, + ) + return None + + from app.models.model_config import ModelConfig + from app.services.encryption_service import EncryptionService + + db = self.db or SessionLocal() + own_db = self.db is None + try: + cfg = db.query(ModelConfig).filter(ModelConfig.id == cfg_id).first() + if not cfg: + raise ValueError(f"模型配置不存在: {cfg_id}") + if cfg.user_id != self.trusted_model_config_user_id: + raise ValueError("无权使用该模型配置(仅创建者可调用)") + + api_key_plain = EncryptionService.decrypt(cfg.api_key) + if not (api_key_plain or "").strip(): + raise ValueError("模型配置中的 API Key 无效") + + base_url = (cfg.base_url or "").strip() or None + raw_prov = (cfg.provider or "").strip().lower() + if raw_prov == "local": + llm_prov = "openai" + elif raw_prov in ("openai", "deepseek"): + llm_prov = raw_prov + elif raw_prov == "anthropic": + raise ValueError( + "当前 LLM 节点暂不支持从模型配置加载 Anthropic;请改用 OpenAI 或 DeepSeek 兼容配置" + ) + else: + raise ValueError(f"不支持的模型配置提供商: {cfg.provider}") + + model_name = (cfg.model_name or "").strip() + if not model_name: + raise ValueError("模型配置中模型名称为空") + + return { + "api_key": api_key_plain.strip(), + "base_url": base_url, + "provider": llm_prov, + "model": model_name, + } + finally: + if own_db: + db.close() + def _get_persist_scope(self) -> Tuple[Optional[str], Optional[str]]: """(scope_kind, scope_id) 或 (None, None),用于持久化 user_memory_*。""" if self._persist_scope_cache is None: @@ -1625,9 +1693,33 @@ class WorkflowEngine: max_tokens = int(max_tokens_raw) if max_tokens_raw is not None else None else: max_tokens = None - # 不传递 api_key 和 base_url,让 LLM 服务使用系统默认配置(与节点测试保持一致) - api_key = None - base_url = None + # 默认使用环境变量中的 Key;若节点绑定 model_config_id 且执行上下文可信,则注入用户保存的密钥与 endpoint + api_key: Optional[str] = None + base_url: Optional[str] = None + mc_cred: Optional[Dict[str, Any]] = None + try: + mc_cred = self._resolve_llm_credentials_from_model_config(node_data) + if mc_cred: + api_key = mc_cred.get("api_key") + base_url = mc_cred.get("base_url") + provider = mc_cred.get("provider", provider) + model = mc_cred.get("model", model) + except Exception as mc_err: + if self.logger: + duration = int((time.time() - start_time) * 1000) + self.logger.log_node_error(node_id, node_type, mc_err, duration) + logger.error(f"[rjb] LLM 模型配置解析失败: {mc_err}", exc_info=True) + return { + 'output': None, + 'status': 'failed', + 'error': str(mc_err), + } + + llm_extra_kw: Dict[str, Any] = {} + if api_key is not None: + llm_extra_kw["api_key"] = api_key + if base_url is not None: + llm_extra_kw["base_url"] = base_url # 记录实际发送给LLM的prompt logger.info(f"[rjb] 准备调用LLM: node_id={node_id}, provider={provider}, model={model}, prompt前200字符='{prompt[:200] if len(prompt) > 200 else prompt}'") @@ -1660,7 +1752,10 @@ class WorkflowEngine: # 调用LLM服务 try: if self.logger: - logger.debug(f"[rjb] LLM节点配置: provider={provider}, model={model}, 使用系统默认API Key配置, 工具调用: {'启用' if tools else '禁用'}") + key_src = "模型配置(model_config_id)" if mc_cred else "环境变量默认" + logger.debug( + f"[rjb] LLM节点配置: provider={provider}, model={model}, API密钥来源={key_src}, 工具调用: {'启用' if tools else '禁用'}" + ) self.logger.info(f"调用LLM服务: {provider}/{model}", node_id=node_id, node_type=node_type) # 根据是否启用工具选择不同的调用方式 @@ -1684,6 +1779,8 @@ class WorkflowEngine: _tool_extra["request_timeout"] = max(10.0, float(_rt)) except (TypeError, ValueError): pass + _merged_tool_kw = dict(llm_extra_kw) + _merged_tool_kw.update(_tool_extra) result = await llm_service.call_llm_with_tools( prompt=prompt, tools=tools, @@ -1694,7 +1791,7 @@ class WorkflowEngine: execution_logger=self.logger, tool_choice=_tool_choice, on_tool_executed=self._on_tool_executed_budget, - **_tool_extra, + **_merged_tool_kw, ) result = self._enrich_llm_json_user_profile(result, input_data) else: @@ -1703,8 +1800,8 @@ class WorkflowEngine: provider=provider, model=model, temperature=temperature, - max_tokens=max_tokens - # 不传递 api_key 和 base_url,使用系统默认配置 + max_tokens=max_tokens, + **llm_extra_kw, ) result = self._enrich_llm_json_user_profile(result, input_data) @@ -4786,6 +4883,7 @@ class WorkflowEngine: logger=child_logger, db=self.db, budget_limits=child_budget, + trusted_model_config_user_id=self.trusted_model_config_user_id, ) try: child_result = await child_engine.execute(sub_input) diff --git a/backend/app/tasks/workflow_tasks.py b/backend/app/tasks/workflow_tasks.py index 5d5f9fe..fde7de9 100644 --- a/backend/app/tasks/workflow_tasks.py +++ b/backend/app/tasks/workflow_tasks.py @@ -41,6 +41,19 @@ def _snapshot_to_jsonable(snapshot: dict) -> dict: return _json.loads(_json.dumps(snapshot, default=str)) +def _trusted_user_for_execution(db, execution: Optional[Execution]) -> Optional[str]: + """用于校验 LLM 节点引用的 model_configs 归属(与 Workflow / Agent 所有者一致)。""" + if not execution: + return None + if execution.agent_id: + ag = db.query(Agent).filter(Agent.id == execution.agent_id).first() + return ag.user_id if ag else None + if execution.workflow_id: + wf = db.query(Workflow).filter(Workflow.id == execution.workflow_id).first() + return wf.user_id if wf else None + return None + + @celery_app.task(bind=True) def execute_workflow_task( self, @@ -80,12 +93,14 @@ def execute_workflow_task( # 创建工作流引擎(传入logger、db、合并后的执行预算) budget = merge_budget_for_execution(db, execution) if execution else {} + trusted_uid = _trusted_user_for_execution(db, execution) engine = WorkflowEngine( workflow_id, workflow_data, logger=execution_logger, db=db, budget_limits=budget, + trusted_model_config_user_id=trusted_uid, ) max_retries = max(0, int(getattr(settings, "WORKFLOW_TASK_MAX_RETRIES", 0) or 0)) @@ -269,12 +284,14 @@ def resume_workflow_task( self.update_state(state="PROGRESS", meta={"progress": 0, "status": "running"}) budget = merge_budget_for_execution(db, execution) if execution else {} + trusted_uid = _trusted_user_for_execution(db, execution) engine = WorkflowEngine( wf_key, workflow_data, logger=execution_logger, db=db, budget_limits=budget, + trusted_model_config_user_id=trusted_uid, ) max_retries = max(0, int(getattr(settings, "WORKFLOW_TASK_MAX_RETRIES", 0) or 0)) result: Optional[dict] = None diff --git a/backend/scripts/compare_homework_agents.py b/backend/scripts/compare_homework_agents.py new file mode 100644 index 0000000..40bddf4 --- /dev/null +++ b/backend/scripts/compare_homework_agents.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python3 +""" +对比「学生作业管理助手」与「学生作业管理助手2号」在相同输入下的节点测试结果。 +调用 POST /api/v1/nodes/test(同步),需后端运行且 DEEPSEEK_API_KEY 有效。 + +用法: cd backend && .\\venv\\Scripts\\python.exe scripts/compare_homework_agents.py +""" +from __future__ import annotations + +import json +import os +import sys +import time +from typing import Any, Dict, List, Optional + +import requests + +BACKEND = os.getenv("PLATFORM_BASE_URL", "http://127.0.0.1:8037").rstrip("/") +USER = os.getenv("PLATFORM_USERNAME", "admin") +PWD = os.getenv("PLATFORM_PASSWORD", "123456") + +NAMES = ["学生作业管理助手", "学生作业管理助手2号"] + +TEST_QUERY = ( + "帮我记一项作业:语文摘抄名著段落3处并批注,截止周五下午5点前。" + "请只回复一条简短清单(科目、要点、截止时间),不要超过120字。" +) + + +def _login() -> str: + r = requests.post( + f"{BACKEND}/api/v1/auth/login", + data={"username": USER, "password": PWD}, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + timeout=15, + ) + r.raise_for_status() + t = r.json().get("access_token") + if not t: + raise RuntimeError("无 access_token") + return t + + +def _find_llm_node(wf: Dict[str, Any]) -> Optional[Dict[str, Any]]: + for n in wf.get("nodes") or []: + if n.get("type") == "llm": + return n + return None + + +def _get_agents(h: Dict[str, str]) -> List[Dict[str, Any]]: + r = requests.get( + f"{BACKEND}/api/v1/agents", + params={"search": "学生作业管理", "limit": 50}, + headers=h, + timeout=30, + ) + r.raise_for_status() + return list(r.json() or []) + + +def _fetch_agent_detail(h: Dict[str, str], aid: str) -> Dict[str, Any]: + r = requests.get(f"{BACKEND}/api/v1/agents/{aid}", headers=h, timeout=30) + r.raise_for_status() + return r.json() + + +def _test_node(h: Dict[str, str], node: Dict[str, Any]): + body = {"node": node, "input_data": {"query": TEST_QUERY}} + t0 = time.perf_counter() + r = requests.post( + f"{BACKEND}/api/v1/nodes/test", + headers=h, + json=body, + timeout=240, + ) + elapsed_ms = int((time.perf_counter() - t0) * 1000) + if r.status_code != 200: + return elapsed_ms, "", f"HTTP {r.status_code}: {r.text[:800]}", None + data = r.json() + out = data.get("output") + if isinstance(out, dict): + text = out.get("output") or out.get("text") or json.dumps(out, ensure_ascii=False) + else: + text = str(out) + err = data.get("error_message") + status = data.get("status") + return elapsed_ms, text[:4000], err, status + + +def main() -> int: + if hasattr(sys.stdout, "reconfigure"): + try: + sys.stdout.reconfigure(encoding="utf-8") + except Exception: + pass + try: + token = _login() + except Exception as e: + print("登录失败:", e, file=sys.stderr) + return 1 + h = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} + + agents = _get_agents(h) + by_name = {a.get("name"): a for a in agents} + + results = [] + for name in NAMES: + a = by_name.get(name) + if not a: + results.append( + {"name": name, "error": f"未找到名为「{name}」的 Agent"} + ) + continue + detail = _fetch_agent_detail(h, a["id"]) + wf = detail.get("workflow_config") or {} + node = _find_llm_node(wf) + if not node: + results.append({"name": name, "id": a["id"], "error": "工作流中无 LLM 节点"}) + continue + data = node.get("data") or {} + elapsed_ms, text, err, st = _test_node(h, node) + results.append( + { + "name": name, + "id": a["id"], + "provider": data.get("provider"), + "model": data.get("model"), + "elapsed_ms": elapsed_ms, + "status": st, + "api_error": err, + "output_excerpt": text[:2000], + } + ) + + print("=== 对比测试(同步节点测试 API)===") + print("输入:", TEST_QUERY) + print() + for r in results: + print(f"【{r['name']}】") + if r.get("error"): + print(" ", r["error"]) + print() + continue + print(f" id: {r['id']}") + print(f" provider/model: {r.get('provider')} / {r.get('model')}") + print(f" 耗时: {r['elapsed_ms']} ms status: {r.get('status')}") + if r.get("api_error"): + print(f" error_message: {r['api_error']}") + print(f" 输出节选:\n{r.get('output_excerpt', '')}\n") + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/backend/scripts/create_homework_manager_agent.py b/backend/scripts/create_homework_manager_agent.py index 291f552..0c054d3 100644 --- a/backend/scripts/create_homework_manager_agent.py +++ b/backend/scripts/create_homework_manager_agent.py @@ -8,7 +8,7 @@ 环境变量: PLATFORM_BASE_URL, PLATFORM_USERNAME, PLATFORM_PASSWORD - AGENT_NAME(默认 学生作业管理助手) + AGENT_NAME(默认 学生作业管理助手);示例:`AGENT_NAME=学生作业管理助手2号 HOMEWORK_LLM_MODEL=deepseek-v4-pro` HOMEWORK_LLM_PROVIDER / HOMEWORK_LLM_MODEL / HOMEWORK_LLM_TIMEOUT(可选) """ from __future__ import annotations @@ -33,7 +33,7 @@ PROVIDER = os.getenv( "HOMEWORK_LLM_PROVIDER", os.getenv("ENTERPRISE_LLM_PROVIDER", "deepseek") ) MODEL = os.getenv( - "HOMEWORK_LLM_MODEL", os.getenv("ENTERPRISE_LLM_MODEL", "deepseek-chat") + "HOMEWORK_LLM_MODEL", os.getenv("ENTERPRISE_LLM_MODEL", "deepseek-v4-flash") ) REQ_TIMEOUT = max( 30, @@ -52,7 +52,9 @@ BUDGET_CONFIG = { HOMEWORK_TOOLS = ["file_read", "text_analyze", "datetime", "json_process"] -HOMEWORK_PROMPT = """你是「学生作业管理助手」,帮助学生**记作业**与**监督完成**,语气友好、具体、可执行。 + +def _homework_prompt(agent_display_name: str) -> str: + return f"""你是「{agent_display_name}」,帮助学生**记作业**与**监督完成**,语气友好、具体、可执行。 【核心能力】 1. **记作业**:从用户自然语言中提取「科目 / 作业内容 / 截止日期与时间 / 老师要求要点 / 预估耗时」,整理成清单。 @@ -104,7 +106,7 @@ def build_workflow() -> Dict[str, Any]: "position": {"x": llm_pos[0], "y": llm_pos[1]}, "data": { "label": "作业管理", - "prompt": HOMEWORK_PROMPT, + "prompt": _homework_prompt(AGENT_NAME), "provider": PROVIDER, "model": MODEL, "temperature": 0.3, @@ -169,7 +171,7 @@ def main() -> int: h = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} desc = ( - "学生作业管理助手:记作业(科目、内容、截止日)、跟进度、温和督促与周回顾;" + f"{AGENT_NAME}:记作业(科目、内容、截止日)、跟进度、温和督促与周回顾;" "支持上传文件/照片后用 file_read 提取正文(文本、PDF、docx、xlsx、图片 OCR)与 json_process 整理;" f"默认模型 {PROVIDER}/{MODEL},单次执行内工具迭代上限 10。" ) diff --git a/frontend/src/components/WorkflowEditor/WorkflowEditor.vue b/frontend/src/components/WorkflowEditor/WorkflowEditor.vue index b4f0b7c..11029d5 100644 --- a/frontend/src/components/WorkflowEditor/WorkflowEditor.vue +++ b/frontend/src/components/WorkflowEditor/WorkflowEditor.vue @@ -645,6 +645,25 @@ + + + + +
+ 选择后会回填提供商与模型;保存工作流后,运行时将通过 model_config_id 使用你在「模型配置」中保存的密钥与 API 地址(需与当前 Agent/工作流属主一致)。 +
+
@@ -827,6 +849,25 @@ + + + + +
+ 选择后会回填提供商与模型;保存工作流后,运行时将通过 model_config_id 使用你在「模型配置」中保存的密钥与 API 地址(需与当前 Agent/工作流属主一致)。 +
+
@@ -2910,6 +2954,14 @@ import { StartNode, LLMNode, ConditionNode, EndNode, DefaultNode } from './NodeT import { useCollaboration } from '@/composables/useCollaboration' import NodeExecutionDetail from './NodeExecutionDetail.vue' +interface ConfiguredModelItem { + id: string + name: string + provider: string + model_name: string + base_url?: string +} + const props = defineProps<{ workflowId?: string agentId?: string @@ -2986,6 +3038,50 @@ const hasUpstreamNodes = computed(() => { // 节点模板相关 const nodeTemplates = ref([]) const loadingTemplates = ref(false) +const configuredModels = ref([]) + +const availableConfiguredModels = computed(() => { + const p = selectedNode.value?.data?.provider + if (!p) return configuredModels.value + return configuredModels.value.filter(cfg => cfg.provider === p) +}) + +const loadConfiguredModels = async () => { + try { + const response = await api.get('/api/v1/model-configs', { + params: { + limit: 100 + } + }) + configuredModels.value = Array.isArray(response.data) ? response.data : [] + } catch (error) { + console.warn('[WorkflowEditor] 加载模型配置失败:', error) + configuredModels.value = [] + } +} + +const handleConfiguredModelChange = (configId: string) => { + if (!selectedNode.value) return + if (!configId) return + const cfg = configuredModels.value.find(item => item.id === configId) + if (!cfg) return + selectedNode.value.data.provider = cfg.provider + selectedNode.value.data.model = cfg.model_name +} + +watch( + () => selectedNode.value?.data?.provider, + (newProv) => { + const sn = selectedNode.value + if (!sn || (sn.type !== 'llm' && sn.type !== 'template')) return + const mid = sn.data?.model_config_id as string | undefined + if (!mid || !newProv) return + const cfg = configuredModels.value.find((c) => c.id === mid) + if (cfg && cfg.provider !== newProv) { + sn.data.model_config_id = '' + } + } +) // 加载节点模板列表 const loadNodeTemplates = async () => { @@ -4538,7 +4634,7 @@ const getScenarios = (nodeType: string) => { ], config: { provider: 'deepseek', - model: 'deepseek-chat', + model: 'deepseek-v4-flash', prompt: '请总结以下内容,100字以内:{{input}}', temperature: 0.5, max_tokens: 500 @@ -4558,7 +4654,7 @@ const getScenarios = (nodeType: string) => { ], config: { provider: 'deepseek', - model: 'deepseek-chat', + model: 'deepseek-v4-flash', prompt: '请把下列内容翻译成{{target_lang}}:{{input}}', temperature: 0.3, max_tokens: 1000 @@ -4571,7 +4667,7 @@ const getScenarios = (nodeType: string) => { icon: 'DataAnalysis', config: { provider: 'deepseek', - model: 'deepseek-chat', + model: 'deepseek-v4-flash', prompt: '请从以下文本中提取关键信息,以JSON格式返回:{{input}}', temperature: 0.2, max_tokens: 1000 @@ -4584,7 +4680,7 @@ const getScenarios = (nodeType: string) => { icon: 'Sort', config: { provider: 'deepseek', - model: 'deepseek-chat', + model: 'deepseek-v4-flash', prompt: '请对以下文本进行分类:{{input}}', temperature: 0.1, max_tokens: 200 @@ -4701,7 +4797,7 @@ const configTemplates = ref { if (t === 'llm') { if (templateSelection.value === 'llm_summary') { selectedNode.value.data.provider = selectedNode.value.data.provider || 'deepseek' - selectedNode.value.data.model = selectedNode.value.data.model || 'deepseek-chat' + selectedNode.value.data.model = selectedNode.value.data.model || 'deepseek-v4-flash' selectedNode.value.data.prompt = '请总结以下内容,100字以内:{text}' selectedNode.value.data.temperature = 0.5 } else if (templateSelection.value === 'llm_translate') { @@ -5218,12 +5314,12 @@ const applyTemplate = () => { if (t === 'llm') { if (templateSelection.value === 'llm_extract') { selectedNode.value.data.provider = selectedNode.value.data.provider || 'deepseek' - selectedNode.value.data.model = selectedNode.value.data.model || 'deepseek-chat' + selectedNode.value.data.model = selectedNode.value.data.model || 'deepseek-v4-flash' selectedNode.value.data.prompt = '请从以下文本中提取关键信息(JSON格式):{text}' selectedNode.value.data.temperature = 0.3 } else if (templateSelection.value === 'llm_classify') { selectedNode.value.data.provider = selectedNode.value.data.provider || 'deepseek' - selectedNode.value.data.model = selectedNode.value.data.model || 'deepseek-chat' + selectedNode.value.data.model = selectedNode.value.data.model || 'deepseek-v4-flash' selectedNode.value.data.prompt = '请将以下内容分类为:正面/中性/负面。内容:{text}' selectedNode.value.data.temperature = 0.2 } @@ -5431,7 +5527,7 @@ const handleDrop = (event: DragEvent) => { // LLM节点默认配置 ...(isLLMNode && !isTemplateNode ? { provider: 'deepseek', - model: 'deepseek-chat', + model: 'deepseek-v4-flash', prompt: '请处理用户请求。', temperature: 0.5, max_tokens: 1500, @@ -5441,7 +5537,7 @@ const handleDrop = (event: DragEvent) => { // 模板节点默认配置 ...(isTemplateNode ? { provider: 'deepseek', - model: 'deepseek-chat', + model: 'deepseek-v4-flash', prompt: '', temperature: 0.7, max_tokens: 1500, @@ -7757,6 +7853,8 @@ onMounted(async () => { // 加载节点模板列表 loadNodeTemplates() + // 加载模型配置列表(用于LLM节点快速选择已配置模型) + loadConfiguredModels() // 加载测试用例 loadTestCases() // 加载配置模板 diff --git a/frontend/src/views/ModelConfigs.vue b/frontend/src/views/ModelConfigs.vue index bf92d77..1177be6 100644 --- a/frontend/src/views/ModelConfigs.vue +++ b/frontend/src/views/ModelConfigs.vue @@ -108,7 +108,21 @@ - + + +