From 0d89325b092f5d13aa1be1dc6f5305fa18d58b45 Mon Sep 17 00:00:00 2001 From: "DESKTOP-72TV0V4\\caoxiaozhu" Date: Tue, 24 Mar 2026 21:44:04 +0800 Subject: [PATCH] Update agent orchestration and knowledge flow Add sub-commander orchestration updates, align frontend integrations, and refine knowledge view behavior without including local data artifacts. Co-Authored-By: Claude Sonnet 4.6 --- backend/app/agents/graph.py | 294 +++++++++--------- backend/app/agents/prompts.py | 236 +++++++++----- backend/app/agents/state.py | 6 + backend/app/agents/tools/__init__.py | 54 +++- backend/app/routers/agent.py | 133 +++----- backend/app/services/agent_service.py | 3 + backend/app/services/document_service.py | 39 ++- .../app/services/test_document_service.py | 68 ++++ frontend/src/api/agent.ts | 13 + frontend/src/app/navigation/nav.ts | 2 +- .../composables/useKnowledgeView.test.ts | 45 +-- .../knowledge/composables/useKnowledgeView.ts | 3 +- setup.bat | 168 ---------- start.bat | 115 ------- 14 files changed, 529 insertions(+), 650 deletions(-) delete mode 100644 setup.bat delete mode 100644 start.bat diff --git a/backend/app/agents/graph.py b/backend/app/agents/graph.py index a723cfd..47a2f97 100644 --- a/backend/app/agents/graph.py +++ b/backend/app/agents/graph.py @@ -11,8 +11,16 @@ from app.agents.prompts import ( EXECUTOR_SYSTEM_PROMPT, LIBRARIAN_SYSTEM_PROMPT, ANALYST_SYSTEM_PROMPT, + PLANNER_SCOPE_PROMPT, + PLANNER_STEPS_PROMPT, + EXECUTOR_TASKS_PROMPT, + EXECUTOR_FORUM_PROMPT, + LIBRARIAN_RETRIEVAL_PROMPT, + LIBRARIAN_GRAPH_PROMPT, + ANALYST_PROGRESS_PROMPT, + ANALYST_INSIGHTS_PROMPT, ) -from app.agents.tools import ALL_TOOLS +from app.agents.tools import ALL_TOOLS, SUB_COMMANDER_TOOLSETS from app.agents.skill_registry import build_skill_context from app.services.llm_service import get_llm from langchain_openai import ChatOpenAI @@ -21,6 +29,32 @@ from langchain_ollama import ChatOllama import httpx +SUB_COMMANDER_PROMPTS = { + "planner_scope": PLANNER_SCOPE_PROMPT, + "planner_steps": PLANNER_STEPS_PROMPT, + "executor_tasks": EXECUTOR_TASKS_PROMPT, + "executor_forum": EXECUTOR_FORUM_PROMPT, + "librarian_retrieval": LIBRARIAN_RETRIEVAL_PROMPT, + "librarian_graph": LIBRARIAN_GRAPH_PROMPT, + "analyst_progress": ANALYST_PROGRESS_PROMPT, + "analyst_insights": ANALYST_INSIGHTS_PROMPT, +} + +ROLE_SUB_COMMANDERS = { + AgentRole.PLANNER: ["planner_scope", "planner_steps"], + AgentRole.EXECUTOR: ["executor_tasks", "executor_forum"], + AgentRole.LIBRARIAN: ["librarian_retrieval", "librarian_graph"], + AgentRole.ANALYST: ["analyst_progress", "analyst_insights"], +} + +ROLE_SKILL_CONTEXT = { + AgentRole.PLANNER: "planner", + AgentRole.EXECUTOR: "executor", + AgentRole.LIBRARIAN: "librarian", + AgentRole.ANALYST: "analyst", +} + + def _create_llm_from_config(config: dict): """根据用户模型配置创建 LLM 实例""" provider = config.get("provider", "openai") @@ -71,8 +105,9 @@ async def _ainvoke(llm, messages: list[BaseMessage]): return await llm.invoke(messages) -async def _ainvoke_with_tools(llm, messages: list[BaseMessage]): - bound_llm = llm.bind_tools(ALL_TOOLS) +async def _ainvoke_with_tools(llm, messages: list[BaseMessage], tools=None): + toolset = tools if tools is not None else ALL_TOOLS + bound_llm = llm.bind_tools(toolset) if hasattr(bound_llm, "ainvoke"): return await bound_llm.ainvoke(messages) return await bound_llm.invoke(messages) @@ -116,6 +151,113 @@ def _is_capability_question(text: str) -> bool: return normalized in {"你能做什么", "你可以做什么", "你会做什么"} +def _choose_sub_commander(role: AgentRole, user_query: str) -> str: + text = _normalize_user_text(user_query) + + if role == AgentRole.PLANNER: + if any(keyword in text for keyword in ["步骤", "计划", "拆解", "排期", "优先级", "路线"]): + return "planner_steps" + return "planner_scope" + + if role == AgentRole.EXECUTOR: + if any(keyword in text for keyword in ["论坛", "帖子", "发帖", "指令", "discussion", "instruction"]): + return "executor_forum" + return "executor_tasks" + + if role == AgentRole.LIBRARIAN: + if any(keyword in text for keyword in ["图谱", "关系", "构建", "沉淀", "节点", "graph"]): + return "librarian_graph" + return "librarian_retrieval" + + if role == AgentRole.ANALYST: + if any(keyword in text for keyword in ["趋势", "风险", "洞察", "建议", "机会", "insight"]): + return "analyst_insights" + return "analyst_progress" + + return ROLE_SUB_COMMANDERS[role][0] + + +def _record_sub_commander(state: AgentState, sub_commander: str, user_query: str): + state["current_sub_commander"] = sub_commander + state["active_sub_commanders"] = state.get("active_sub_commanders", []) + [sub_commander] + state["sub_commander_trace"] = state.get("sub_commander_trace", []) + [{ + "agent": state.get("current_agent", AgentRole.MASTER).value, + "sub_commander": sub_commander, + "query": user_query, + }] + + +def _build_system_messages(state: AgentState, system_prompt: str, role: AgentRole): + system_msgs: list[BaseMessage] = [SystemMessage(content=system_prompt)] + skill_ctx = build_skill_context(ROLE_SKILL_CONTEXT[role]) + if skill_ctx: + system_msgs.append(SystemMessage(content=skill_ctx)) + return system_msgs + + +async def _run_sub_commander( + state: AgentState, + role: AgentRole, + manager_prompt: str, + user_query: str, + *, + use_tools: bool, + summary_target: str | None = None, +): + llm = _get_llm_for_state(state) + sub_commander = _choose_sub_commander(role, user_query) + _record_sub_commander(state, sub_commander, user_query) + + toolset = SUB_COMMANDER_TOOLSETS.get(sub_commander, []) + system_msgs = _build_system_messages(state, manager_prompt, role) + system_msgs.append(SystemMessage(content=f"本次应由子指挥官 `{sub_commander}` 接手。请严格按该角色职责输出。")) + system_msgs.append(SystemMessage(content=SUB_COMMANDER_PROMPTS[sub_commander])) + + if use_tools and toolset: + response = await _ainvoke_with_tools( + llm, + system_msgs + [HumanMessage(content=f"用户请求: {user_query}")], + toolset, + ) + tool_calls = getattr(response, "tool_calls", None) or [] + if tool_calls: + results = [] + for tc in tool_calls: + tool_name = tc.get("name") + args = tc.get("args", {}) + for tool in toolset: + if tool.name == tool_name: + try: + result = tool.invoke(args) + results.append(f"[{tool_name}] {result}") + except Exception as e: + results.append(f"[{tool_name}] 执行失败: {e}") + break + state["tool_calls"] = tool_calls + state["last_tool_result"] = "\n".join(results) + follow_up = await _ainvoke( + llm, + [ + SystemMessage(content=SUB_COMMANDER_PROMPTS[sub_commander]), + HumanMessage(content=f"工具执行结果:\n{state['last_tool_result']}") + ] + ) + state["final_response"] = follow_up.content + else: + state["final_response"] = response.content + else: + response = await _ainvoke( + llm, + system_msgs + [HumanMessage(content=f"用户请求: {user_query}")], + ) + state["final_response"] = response.content + + if summary_target: + state[summary_target] = state.get("final_response", "") + state["should_respond"] = True + return state + + # ===================== 节点定义 (async) ===================== async def master_node(state: AgentState) -> AgentState: @@ -142,14 +284,13 @@ async def master_node(state: AgentState) -> AgentState: llm = _get_llm_for_state(state) system_msgs: list[BaseMessage] = [SystemMessage(content=MASTER_SYSTEM_PROMPT)] - # 注入记忆上下文 memory_ctx = state.get("memory_context") if memory_ctx: system_msgs.append( SystemMessage(content=f"\n\n【记忆上下文】\n{memory_ctx}\n\n---\n") ) - response: AIMessage = await _ainvoke(llm,system_msgs + messages) + response: AIMessage = await _ainvoke(llm, system_msgs + messages) content = response.content.strip().lower() if any(kw in content for kw in ["搜索", "查找", "知识", "检索"]): @@ -166,6 +307,7 @@ async def master_node(state: AgentState) -> AgentState: return state state["current_agent"] = next_agent + state["current_sub_commander"] = None state["active_agents"] = state.get("active_agents", [AgentRole.MASTER]) + [next_agent] state["should_respond"] = True return state @@ -173,164 +315,30 @@ async def master_node(state: AgentState) -> AgentState: async def planner_node(state: AgentState) -> AgentState: """规划Agent节点: 制定计划,拆解任务步骤""" - llm = _get_llm_for_state(state) user_msgs = _filter_user_messages(state["messages"]) user_query = user_msgs[-1].content if user_msgs else "" - - system_msgs = [SystemMessage(content=PLANNER_SYSTEM_PROMPT)] - skill_ctx = build_skill_context("planner") - if skill_ctx: - system_msgs.append(SystemMessage(content=skill_ctx)) - - response = await _ainvoke(llm, - system_msgs + [HumanMessage(content=f"用户请求: {user_query}")] - ) - - plan_text = response.content - steps = [] - for i, line in enumerate(plan_text.split("\n")): - if line.strip() and (line[0].isdigit() or "- " in line): - steps.append({"step": i + 1, "description": line.strip()}) - - state["plan"] = plan_text - state["plan_steps"] = steps - state["final_response"] = plan_text - state["should_respond"] = True - return state + return await _run_sub_commander(state, AgentRole.PLANNER, PLANNER_SYSTEM_PROMPT, user_query, use_tools=False) async def executor_node(state: AgentState) -> AgentState: """执行Agent节点: 调用工具执行具体任务""" - llm = _get_llm_for_state(state) user_msgs = _filter_user_messages(state["messages"]) user_query = user_msgs[-1].content if user_msgs else "" - - system_msgs = [SystemMessage(content=EXECUTOR_SYSTEM_PROMPT)] - skill_ctx = build_skill_context("executor") - if skill_ctx: - system_msgs.append(SystemMessage(content=skill_ctx)) - - response = await _ainvoke_with_tools(llm, - system_msgs + [HumanMessage(content=f"用户请求: {user_query}")] - ) - - tool_calls = getattr(response, "tool_calls", None) or [] - - if tool_calls: - results = [] - for tc in tool_calls: - tool_name = tc.get("name") - args = tc.get("args", {}) - for tool in ALL_TOOLS: - if tool.name == tool_name: - try: - result = tool.invoke(args) - results.append(f"[{tool_name}] {result}") - except Exception as e: - results.append(f"[{tool_name}] 执行失败: {e}") - break - state["tool_calls"] = tool_calls - state["last_tool_result"] = "\n".join(results) - follow_up = await _ainvoke(llm, - [SystemMessage(content=EXECUTOR_SYSTEM_PROMPT), - HumanMessage(content=f"工具执行结果:\n{state['last_tool_result']}")] - ) - state["final_response"] = follow_up.content - else: - state["final_response"] = response.content - - state["should_respond"] = True - return state + return await _run_sub_commander(state, AgentRole.EXECUTOR, EXECUTOR_SYSTEM_PROMPT, user_query, use_tools=True) async def librarian_node(state: AgentState) -> AgentState: """知识管理员节点: 管理知识库和知识图谱""" - llm = _get_llm_for_state(state) user_msgs = _filter_user_messages(state["messages"]) user_query = user_msgs[-1].content if user_msgs else "" - - system_msgs = [SystemMessage(content=LIBRARIAN_SYSTEM_PROMPT)] - skill_ctx = build_skill_context("librarian") - if skill_ctx: - system_msgs.append(SystemMessage(content=skill_ctx)) - - response = await _ainvoke_with_tools(llm, - system_msgs + [HumanMessage(content=f"用户请求: {user_query}")] - ) - - tool_calls = getattr(response, "tool_calls", None) or [] - - if tool_calls: - results = [] - for tc in tool_calls: - tool_name = tc.get("name") - args = tc.get("args", {}) - for tool in ALL_TOOLS: - if tool.name == tool_name: - try: - result = tool.invoke(args) - results.append(f"[{tool_name}] {result}") - except Exception as e: - results.append(f"[{tool_name}] 执行失败: {e}") - break - state["tool_calls"] = tool_calls - state["last_tool_result"] = "\n".join(results) - follow_up = await _ainvoke(llm, - [SystemMessage(content=LIBRARIAN_SYSTEM_PROMPT), - HumanMessage(content=f"工具执行结果:\n{state['last_tool_result']}")] - ) - state["final_response"] = follow_up.content - else: - state["final_response"] = response.content - - state["knowledge_context"] = state.get("last_tool_result", "") - state["should_respond"] = True - return state + return await _run_sub_commander(state, AgentRole.LIBRARIAN, LIBRARIAN_SYSTEM_PROMPT, user_query, use_tools=True, summary_target="knowledge_context") async def analyst_node(state: AgentState) -> AgentState: """分析师节点: 分析工作数据,生成报告""" - llm = _get_llm_for_state(state) user_msgs = _filter_user_messages(state["messages"]) user_query = user_msgs[-1].content if user_msgs else "" - - system_msgs = [SystemMessage(content=ANALYST_SYSTEM_PROMPT)] - skill_ctx = build_skill_context("analyst") - if skill_ctx: - system_msgs.append(SystemMessage(content=skill_ctx)) - - response = await _ainvoke_with_tools(llm, - system_msgs + [HumanMessage(content=f"用户请求: {user_query}")] - ) - - tool_calls = getattr(response, "tool_calls", None) or [] - - if tool_calls: - results = [] - for tc in tool_calls: - tool_name = tc.get("name") - args = tc.get("args", {}) - for tool in ALL_TOOLS: - if tool.name == tool_name: - try: - result = tool.invoke(args) - results.append(f"[{tool_name}] {result}") - except Exception as e: - results.append(f"[{tool_name}] 执行失败: {e}") - break - state["tool_calls"] = tool_calls - state["last_tool_result"] = "\n".join(results) - follow_up = await _ainvoke(llm, - [SystemMessage(content=ANALYST_SYSTEM_PROMPT), - HumanMessage(content=f"工具执行结果:\n{state['last_tool_result']}")] - ) - state["final_response"] = follow_up.content - else: - state["final_response"] = response.content - - state["analysis_report"] = state.get("final_response", "") - state["should_respond"] = True - return state + return await _run_sub_commander(state, AgentRole.ANALYST, ANALYST_SYSTEM_PROMPT, user_query, use_tools=True, summary_target="analysis_report") def route_agent(state: AgentState) -> str: diff --git a/backend/app/agents/prompts.py b/backend/app/agents/prompts.py index 115aa2b..90fb085 100644 --- a/backend/app/agents/prompts.py +++ b/backend/app/agents/prompts.py @@ -114,85 +114,46 @@ MASTER_SYSTEM_PROMPT = f"""{JARVIS_PERSONA_PROMPT} PLANNER_SYSTEM_PROMPT = f"""{JARVIS_PERSONA_PROMPT} -你是 Jarvis 的规划Agent,负责制定计划、拆解任务。 +你是 Jarvis 的规划Agent,负责先判断问题该由哪位规划子指挥官接手。 -## 你的能力: -- 分析复杂请求,拆解成可执行的步骤 -- 评估任务优先级 -- 判断哪些步骤依赖前置条件 -- 制定清晰的执行顺序 +## 你的两个子指挥官: +1. **planner_scope (目标收束官)**: 负责澄清目标、边界、约束、缺失信息 +2. **planner_steps (步骤拆解官)**: 负责把目标拆成步骤、优先级与依赖关系 -## 工作流程: -1. 理解用户的最终目标 -2. 判断任务复杂度与关键约束 -3. 拆解成具体步骤 -4. 标注优先级或先后顺序 -5. 给出清晰计划 - -## 响应要求: -- 用编号列表展示计划步骤 -- 每步都要具体,避免空泛词汇 -- 必要时可标注 P1/P2/P3 或“先做/后做” -- 如果任务确实复杂,可以轻微指出复杂点,但马上收束到行动方案 -- 如果需要执行,先输出计划,再等待用户确认 +## 你的职责: +- 判断当前请求更适合收束目标,还是拆解步骤 +- 在必要时收束子指挥官输出,面向用户给出清晰结果 +- 保持结果可推进,不空泛 """ EXECUTOR_SYSTEM_PROMPT = f"""{JARVIS_PERSONA_PROMPT} -你是 Jarvis 的执行Agent,负责执行具体任务。 +你是 Jarvis 的执行Agent,负责先判断问题该由哪位执行子指挥官接手。 -## 你可以使用的工具: -- create_task: 创建新任务 -- update_task_status: 更新任务状态 -- get_tasks: 查看任务列表 -- create_forum_post: 在论坛发布帖子 -- get_forum_posts: 查看论坛帖子 -- scan_forum_for_instructions: 扫描论坛指令 +## 你的两个子指挥官: +1. **executor_tasks (任务执行官)**: 只处理任务类工具调用 +2. **executor_forum (论坛执行官)**: 只处理论坛/指令帖相关工具调用 -## 工作流程: -1. 理解用户要执行什么 -2. 判断是否已具备足够信息 -3. 调用相应工具 -4. 汇总执行结果 -5. 明确是否还需要下一步 - -## 响应要求: -- 明确说明已执行什么 -- 工具结果要结构化、可读 -- 成功时给出简洁确认 -- 失败时说明卡点与下一步 -- 如果信息不足,直接指出缺什么,不要假设 +## 你的职责: +- 识别用户要推进的是任务操作还是论坛/指令操作 +- 把请求交给最合适的执行子指挥官 +- 汇总执行结果并给出下一步 """ LIBRARIAN_SYSTEM_PROMPT = f"""{JARVIS_PERSONA_PROMPT} -你是 Jarvis 的知识管理员,负责管理用户的私人知识库。 +你是 Jarvis 的知识管理员,负责先判断问题该由哪位知识子指挥官接手。 -## 你可以使用的工具: -- search_knowledge: 搜索知识库,返回相关文档片段 -- get_knowledge_graph_context: 获取知识图谱上下文 -- build_knowledge_graph: 从文档构建知识图谱 +## 你的两个子指挥官: +1. **librarian_retrieval (检索问答官)**: 负责知识检索与证据综合 +2. **librarian_graph (图谱沉淀官)**: 负责图谱上下文、关系串联与结构化沉淀 ## 你的职责: -1. 理解用户关于知识的问题 -2. 搜索相关知识 -3. 综合多篇文档给出完整回答 -4. 帮助用户整理和理解知识 - -## 工作流程: -1. 分析用户问题的关键概念 -2. 搜索相关文档与图谱关系 -3. 综合证据形成答案 -4. 在证据不足时明确说明边界 - -## 响应要求: -- 回答要有依据,不靠猜测 -- 引用时标注来源或依据范围 -- 如果知识不足,诚实说明 -- 可以补充必要背景,但不要离题 -- 风格保持冷静、清楚、可信 +- 判断当前需求更适合检索问答还是图谱沉淀 +- 让回答建立在证据和结构之上 +- 必要时收束子指挥官输出,给出最终回答 """ @@ -200,28 +161,141 @@ ANALYST_SYSTEM_PROMPT = f"""{JARVIS_PERSONA_PROMPT} 你是 Jarvis 的分析师,负责分析数据和工作状态。 -## 你可以使用的工具: -- get_tasks: 获取任务列表,统计工作进度 -- get_forum_posts: 获取论坛帖子,分析讨论趋势 -- scan_forum_for_instructions: 检查待执行指令 -- search_knowledge: 结合知识进行分析 +## 你有两个子指挥官: +1. **analyst_progress (进度研判官)**: 汇总任务、论坛、指令执行状态,判断当前推进情况 +2. **analyst_insights (洞察建议官)**: 提炼趋势、风险、机会点,并给出建议 ## 你的职责: -1. 统计任务完成情况 -2. 分析工作进度和趋势 -3. 生成结构化报告 -4. 识别潜在问题和风险 +1. 判断当前问题更适合哪位子指挥官处理 +2. 在需要时汇总子指挥官结果,给出面向用户的结论 +3. 保持先结论后展开的表达方式 +""" -## 工作流程: -1. 收集相关数据(任务、论坛、知识) -2. 识别模式、异常与趋势 -3. 形成结论 -4. 给出建议 + +PLANNER_SCOPE_PROMPT = f"""{JARVIS_PERSONA_PROMPT} + +你是 planner 体系下的目标收束官,负责先把问题边界、目标、约束和成功标准说清楚。 + +## 你的重点: +- 收束问题定义 +- 明确目标与限制条件 +- 识别缺失信息 +- 帮用户建立可以继续规划的前提 ## 响应要求: -- 用数据说话,有数字、有结论 -- 报告结构清晰,先结论后展开 -- 明确风险、影响和建议 -- 如果数据不完整,要说明分析置信度 -- 可以有一丝冷幽默,但结论必须严谨 +- 先给出你理解的目标 +- 再列出关键约束或缺口 +- 不要直接展开长步骤清单 +""" + + +PLANNER_STEPS_PROMPT = f"""{JARVIS_PERSONA_PROMPT} + +你是 planner 体系下的步骤拆解官,负责把目标转成有顺序的执行路径。 + +## 你的重点: +- 拆解步骤 +- 标注优先级与依赖 +- 输出清晰的行动顺序 + +## 响应要求: +- 用编号列表 +- 每步具体,不要空泛 +- 必要时标注先后关系 +""" + + +EXECUTOR_TASKS_PROMPT = f"""{JARVIS_PERSONA_PROMPT} + +你是 executor 体系下的任务执行官,只负责任务相关工具调用。 + +## 允许使用的工具: +- get_tasks +- create_task +- update_task_status + +## 要求: +- 只处理任务类操作 +- 明确已执行动作、结果与下一步 +- 信息不足时直接指出缺口 +""" + + +EXECUTOR_FORUM_PROMPT = f"""{JARVIS_PERSONA_PROMPT} + +你是 executor 体系下的论坛执行官,只负责论坛与指令帖相关工具调用。 + +## 允许使用的工具: +- get_forum_posts +- create_forum_post +- scan_forum_for_instructions + +## 要求: +- 只处理论坛/指令类操作 +- 结果要清楚说明是否执行成功 +- 不要越权调用任务或知识工具 +""" + + +LIBRARIAN_RETRIEVAL_PROMPT = f"""{JARVIS_PERSONA_PROMPT} + +你是 librarian 体系下的检索问答官,负责从知识库与上下文中快速找到可靠信息。 + +## 允许使用的工具: +- search_knowledge +- hybrid_search +- get_knowledge_graph_context + +## 要求: +- 优先检索与综合证据 +- 证据不足时明确说明边界 +- 以回答问题为主,不主动做图谱构建 +""" + + +LIBRARIAN_GRAPH_PROMPT = f"""{JARVIS_PERSONA_PROMPT} + +你是 librarian 体系下的图谱沉淀官,负责知识关系整理、图谱上下文与结构化沉淀。 + +## 允许使用的工具: +- get_knowledge_graph_context +- build_knowledge_graph + +## 要求: +- 聚焦知识结构、关系串联与沉淀 +- 明确说明构建/更新结果 +- 不把自己变成泛检索问答器 +""" + + +ANALYST_PROGRESS_PROMPT = f"""{JARVIS_PERSONA_PROMPT} + +你是 analyst 体系下的进度研判官,负责汇总当前任务、论坛与指令执行状态。 + +## 允许使用的工具: +- get_tasks +- get_forum_posts +- scan_forum_for_instructions + +## 要求: +- 先结论后展开 +- 重点说明进度、阻塞、待处理项 +- 不做泛泛趋势空谈 +""" + + +ANALYST_INSIGHTS_PROMPT = f"""{JARVIS_PERSONA_PROMPT} + +你是 analyst 体系下的洞察建议官,负责从任务、论坛和知识线索里提炼趋势、风险与建议。 + +## 允许使用的工具: +- get_tasks +- get_forum_posts +- search_knowledge +- hybrid_search + +## 要求: +- 先给结论与判断 +- 再说明依据与建议 +- 重点输出趋势、风险、机会点 """ diff --git a/backend/app/agents/state.py b/backend/app/agents/state.py index 423f086..f9cd7b9 100644 --- a/backend/app/agents/state.py +++ b/backend/app/agents/state.py @@ -55,6 +55,9 @@ class AgentState(TypedDict): # Agent routing current_agent: AgentRole active_agents: list[AgentRole] + current_sub_commander: str | None + active_sub_commanders: list[str] + sub_commander_trace: list[dict] # Task tracking pending_tasks: list[dict] @@ -93,6 +96,9 @@ def initial_state(user_id: str, conversation_id: str) -> AgentState: conversation_id=conversation_id, current_agent=AgentRole.MASTER, active_agents=[AgentRole.MASTER], + current_sub_commander=None, + active_sub_commanders=[], + sub_commander_trace=[], pending_tasks=[], completed_tasks=[], tool_calls=[], diff --git a/backend/app/agents/tools/__init__.py b/backend/app/agents/tools/__init__.py index 43416d6..88e0e86 100644 --- a/backend/app/agents/tools/__init__.py +++ b/backend/app/agents/tools/__init__.py @@ -5,18 +5,56 @@ from app.agents.tools.search import ( from app.agents.tools.task import get_tasks, create_task, update_task_status from app.agents.tools.forum import get_forum_posts, create_forum_post, scan_forum_for_instructions -ALL_TOOLS = [ - # 知识库工具 - search_knowledge, - get_knowledge_graph_context, - build_knowledge_graph, - hybrid_search, - # 任务工具 +TASK_TOOLS = [ get_tasks, create_task, update_task_status, - # 论坛工具 +] + +FORUM_TOOLS = [ get_forum_posts, create_forum_post, scan_forum_for_instructions, ] + +KNOWLEDGE_RETRIEVAL_TOOLS = [ + search_knowledge, + hybrid_search, + get_knowledge_graph_context, +] + +KNOWLEDGE_GRAPH_TOOLS = [ + get_knowledge_graph_context, + build_knowledge_graph, +] + +ANALYST_PROGRESS_TOOLS = [ + get_tasks, + get_forum_posts, + scan_forum_for_instructions, +] + +ANALYST_INSIGHT_TOOLS = [ + get_tasks, + get_forum_posts, + search_knowledge, + hybrid_search, +] + +ALL_TOOLS = [ + *KNOWLEDGE_RETRIEVAL_TOOLS, + build_knowledge_graph, + *TASK_TOOLS, + *FORUM_TOOLS, +] + +SUB_COMMANDER_TOOLSETS = { + "planner_scope": [], + "planner_steps": [], + "executor_tasks": TASK_TOOLS, + "executor_forum": FORUM_TOOLS, + "librarian_retrieval": KNOWLEDGE_RETRIEVAL_TOOLS, + "librarian_graph": KNOWLEDGE_GRAPH_TOOLS, + "analyst_progress": ANALYST_PROGRESS_TOOLS, + "analyst_insights": ANALYST_INSIGHT_TOOLS, +} diff --git a/backend/app/routers/agent.py b/backend/app/routers/agent.py index 80fc32d..87e3ed7 100644 --- a/backend/app/routers/agent.py +++ b/backend/app/routers/agent.py @@ -9,13 +9,17 @@ from app.schemas.agent import AgentCreate, AgentOut, AgentStats, AgentConfigUpda router = APIRouter(prefix="/api/agents", tags=["Agent"]) -# 运行时调用统计(内存中,非持久化) _agent_call_counts: dict[str, int] = {} _agent_current_tasks: dict[str, str | None] = {} _agent_statuses: dict[str, str] = {} -# 默认 Agent 角色列表 DEFAULT_AGENT_ROLES = ["master", "planner", "executor", "librarian", "analyst"] +SUB_COMMANDERS_BY_ROLE = { + "planner": ["planner_scope", "planner_steps"], + "executor": ["executor_tasks", "executor_forum"], + "librarian": ["librarian_retrieval", "librarian_graph"], + "analyst": ["analyst_progress", "analyst_insights"], +} def record_agent_call(agent_id: str): @@ -31,6 +35,15 @@ def set_agent_status(agent_id: str, status: str): _agent_statuses[agent_id] = status +def _build_agent_stats(agent_id: str) -> AgentStats: + return AgentStats( + agent_id=agent_id, + call_count=_agent_call_counts.get(agent_id, 0), + current_task=_agent_current_tasks.get(agent_id), + status=_agent_statuses.get(agent_id, "idle"), + ) + + @router.get("", response_model=list[AgentOut]) async def list_agents( current_user: User = Depends(get_current_user), @@ -42,32 +55,35 @@ async def list_agents( return result.scalars().all() -# ———— 运行时统计(必须在 /{agent_id} 之前)———— @router.get("/stats", response_model=list[AgentStats]) async def get_agent_stats( current_user: User = Depends(get_current_user), ): - """ - 获取各 Agent 的运行时统计(调用次数、当前任务、状态) - """ - stats = [] + return [_build_agent_stats(role) for role in DEFAULT_AGENT_ROLES] + + +@router.get("/stats/hierarchy") +async def get_agent_hierarchy_stats( + current_user: User = Depends(get_current_user), +): + main_agents = [] for role in DEFAULT_AGENT_ROLES: - stats.append(AgentStats( - agent_id=role, - call_count=_agent_call_counts.get(role, 0), - current_task=_agent_current_tasks.get(role), - status=_agent_statuses.get(role, "idle"), - )) - return stats + if role == "master": + continue + node = _build_agent_stats(role).model_dump() + node["sub_commanders"] = [ + _build_agent_stats(sub_id).model_dump() + for sub_id in SUB_COMMANDERS_BY_ROLE.get(role, []) + ] + main_agents.append(node) + return {"main_agents": main_agents} -# ———— 配置管理(必须在 /{agent_id} 之前)———— @router.get("/config/{agent_id}", response_model=AgentConfigOut) async def get_agent_config( agent_id: str, db: AsyncSession = Depends(get_db), ): - """获取单个 Agent 完整配置""" result = await db.execute(select(Agent).where(Agent.role == agent_id)) agent = result.scalar_one_or_none() @@ -84,8 +100,13 @@ async def get_agent_config( raise HTTPException(status_code=404, detail="Agent 不存在") name, desc, prompt = defaults[agent_id] return AgentConfigOut( - id=agent_id, name=name, role=agent_id, - description=desc, system_prompt=prompt, enabled=True, is_active=True, + id=agent_id, + name=name, + role=agent_id, + description=desc, + system_prompt=prompt, + enabled=True, + is_active=True, ) return AgentConfigOut( id=agent.role, @@ -105,7 +126,6 @@ async def update_agent_config( current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): - """更新 Agent 配置(名称、描述、提示词、启用状态)""" result = await db.execute(select(Agent).where(Agent.role == agent_id)) agent = result.scalar_one_or_none() @@ -163,78 +183,3 @@ async def get_agent( if not agent: raise HTTPException(status_code=404, detail="Agent 不存在") return agent - - - -# ———— 配置管理 ———— -@router.get("/config/{agent_id}", response_model=AgentConfigOut) -async def get_agent_config( - agent_id: str, - db: AsyncSession = Depends(get_db), -): - """获取单个 Agent 完整配置""" - result = await db.execute(select(Agent).where(Agent.role == agent_id)) - agent = result.scalar_one_or_none() - if not agent: - # 如果数据库中没有,返回默认配置 - from app.agents.prompts import MASTER_SYSTEM_PROMPT, PLANNER_SYSTEM_PROMPT, EXECUTOR_SYSTEM_PROMPT, LIBRARIAN_SYSTEM_PROMPT, ANALYST_SYSTEM_PROMPT - defaults = { - "master": ("JARVIS", "主控制核心", MASTER_SYSTEM_PROMPT), - "planner": ("PLANNER", "规划专家", PLANNER_SYSTEM_PROMPT), - "executor": ("EXECUTOR", "执行专家", EXECUTOR_SYSTEM_PROMPT), - "librarian": ("LIBRARIAN", "知识管理员", LIBRARIAN_SYSTEM_PROMPT), - "analyst": ("ANALYST", "数据分析师", ANALYST_SYSTEM_PROMPT), - } - if agent_id not in defaults: - raise HTTPException(status_code=404, detail="Agent 不存在") - name, desc, prompt = defaults[agent_id] - return AgentConfigOut( - id=agent_id, name=name, role=agent_id, - description=desc, system_prompt=prompt, enabled=True, is_active=True, - ) - return AgentConfigOut( - id=agent.role, - name=agent.name, - role=agent.role, - description=agent.description, - system_prompt=agent.system_prompt, - enabled=agent.is_active, - is_active=agent.is_active, - ) - - -@router.put("/config/{agent_id}", response_model=AgentConfigOut) -async def update_agent_config( - agent_id: str, - data: AgentConfigUpdate, - current_user: User = Depends(get_current_user), - db: AsyncSession = Depends(get_db), -): - """更新 Agent 配置(名称、描述、提示词、启用状态)""" - result = await db.execute(select(Agent).where(Agent.role == agent_id)) - agent = result.scalar_one_or_none() - - if not agent: - raise HTTPException(status_code=404, detail="Agent 不存在") - - if data.name is not None: - agent.name = data.name - if data.description is not None: - agent.description = data.description - if data.system_prompt is not None: - agent.system_prompt = data.system_prompt - if data.enabled is not None: - agent.is_active = data.enabled - _agent_statuses[agent_id] = "disabled" if not data.enabled else "idle" - - await db.commit() - await db.refresh(agent) - return AgentConfigOut( - id=agent.role, - name=agent.name, - role=agent.role, - description=agent.description, - system_prompt=agent.system_prompt, - enabled=agent.is_active, - is_active=agent.is_active, - ) diff --git a/backend/app/services/agent_service.py b/backend/app/services/agent_service.py index a277a64..1264a2d 100644 --- a/backend/app/services/agent_service.py +++ b/backend/app/services/agent_service.py @@ -225,6 +225,9 @@ class AgentService: "conversation_id": conversation_id, "current_agent": "master", "active_agents": ["master"], + "current_sub_commander": None, + "active_sub_commanders": [], + "sub_commander_trace": [], "pending_tasks": [], "completed_tasks": [], "tool_calls": [], diff --git a/backend/app/services/document_service.py b/backend/app/services/document_service.py index 9dc28a0..652347b 100644 --- a/backend/app/services/document_service.py +++ b/backend/app/services/document_service.py @@ -3,6 +3,8 @@ 支持多种文档格式 + LlamaIndex 智能分块 """ +from pathlib import Path +import tempfile from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from fastapi import UploadFile @@ -380,7 +382,42 @@ class DocumentService: if hasattr(mineru, "parse_to_markdown"): return mineru.parse_to_markdown(file_path) - raise ValueError("PDF 解析失败: MinerU 不支持当前接口") + try: + from mineru.cli.common import do_parse, read_fn + from mineru.utils.enum_class import MakeMode + except Exception as error: + raise ValueError( + "PDF 解析失败: 当前安装的 MinerU 版本接口不兼容,请确认支持 to_markdown / parse_to_markdown,或提供 cli.common.do_parse 能力" + ) from error + + with tempfile.TemporaryDirectory(prefix="mineru-") as output_dir: + pdf_name = Path(file_path).stem + pdf_bytes = read_fn(Path(file_path)) + try: + do_parse( + output_dir, + [pdf_name], + [pdf_bytes], + ["zh"], + f_draw_layout_bbox=False, + f_draw_span_bbox=False, + f_dump_md=True, + f_dump_middle_json=False, + f_dump_model_output=False, + f_dump_orig_pdf=False, + f_dump_content_list=False, + f_make_md_mode=MakeMode.MM_MD, + ) + except ModuleNotFoundError as error: + dependency = getattr(error, "name", None) or str(error).split("'")[-2] if "'" in str(error) else str(error) + raise ValueError(f"PDF 解析依赖缺失: MinerU 运行时依赖 {dependency}") from error + markdown_path = Path(output_dir) / pdf_name / "pipeline" / f"{pdf_name}.md" + if markdown_path.exists(): + return markdown_path.read_text(encoding="utf-8") + + raise ValueError( + "PDF 解析失败: 当前安装的 MinerU 版本接口不兼容,请确认支持 to_markdown / parse_to_markdown,或提供 cli.common.do_parse 能力" + ) async def _parse_pdf(self, file_path: str) -> ParsedDocument: markdown = await self._parse_pdf_with_mineru(file_path) diff --git a/backend/tests/backend/app/services/test_document_service.py b/backend/tests/backend/app/services/test_document_service.py index 6176eaa..bba48b8 100644 --- a/backend/tests/backend/app/services/test_document_service.py +++ b/backend/tests/backend/app/services/test_document_service.py @@ -1,6 +1,7 @@ import json from io import BytesIO import builtins +from pathlib import Path import sys import types @@ -351,6 +352,73 @@ async def test_get_document_content_returns_normalized_pdf_content(document_test assert content == '# PDF Title\n\nNormalized pdf body.' +@pytest.mark.asyncio +async def test_upload_document_uses_mineru_cli_do_parse_fallback_for_pdf(document_test_env, monkeypatch, tmp_path): + session, user = document_test_env + service = DocumentService(session) + + fake_mineru = types.SimpleNamespace() + fake_common = types.SimpleNamespace() + + def fake_do_parse(output_dir, pdf_file_names, pdf_bytes_list, p_lang_list, **kwargs): + output_path = Path(output_dir) / pdf_file_names[0] / 'pipeline' + output_path.mkdir(parents=True, exist_ok=True) + (output_path / f'{pdf_file_names[0]}.md').write_text('# PDF Title\n\nCLI fallback content.', encoding='utf-8') + + fake_common.do_parse = fake_do_parse + monkeypatch.setitem(sys.modules, 'mineru', fake_mineru) + monkeypatch.setitem(sys.modules, 'mineru.cli', types.SimpleNamespace(common=fake_common)) + monkeypatch.setitem(sys.modules, 'mineru.cli.common', fake_common) + monkeypatch.setattr('app.services.document_service.settings.UPLOAD_DIR', str(tmp_path / 'uploads')) + + upload = UploadFile(filename='fallback.pdf', file=BytesIO(b'%PDF-1.4 fake')) + document = await service.upload_document(user.id, upload) + + assert document.normalized_format == 'structured_markdown' + assert '# PDF Title' in document.normalized_content + assert 'CLI fallback content.' in document.normalized_content + + +@pytest.mark.asyncio +async def test_upload_document_raises_clear_error_when_mineru_cli_runtime_dependency_is_missing(document_test_env, monkeypatch): + session, user = document_test_env + service = DocumentService(session) + + fake_mineru = types.SimpleNamespace() + fake_common = types.SimpleNamespace() + fake_enum_class = types.SimpleNamespace(MakeMode=types.SimpleNamespace(MM_MD='mm_markdown')) + + def fake_do_parse(*args, **kwargs): + raise ModuleNotFoundError("No module named 'torch'") + + fake_common.do_parse = fake_do_parse + fake_common.read_fn = lambda path: b'%PDF-1.4 fake' + monkeypatch.setitem(sys.modules, 'mineru', fake_mineru) + monkeypatch.setitem(sys.modules, 'mineru.cli', types.SimpleNamespace(common=fake_common)) + monkeypatch.setitem(sys.modules, 'mineru.cli.common', fake_common) + monkeypatch.setitem(sys.modules, 'mineru.utils', types.SimpleNamespace(enum_class=fake_enum_class)) + monkeypatch.setitem(sys.modules, 'mineru.utils.enum_class', fake_enum_class) + + upload = UploadFile(filename='runtime-missing.pdf', file=BytesIO(b'%PDF-1.4 fake')) + + with pytest.raises(ValueError, match="PDF 解析依赖缺失: MinerU 运行时依赖 torch"): + await service.upload_document(user.id, upload) + + +@pytest.mark.asyncio +async def test_upload_document_raises_clear_error_when_mineru_interface_is_unsupported(document_test_env, monkeypatch): + session, user = document_test_env + service = DocumentService(session) + + fake_mineru = types.SimpleNamespace() + monkeypatch.setitem(sys.modules, 'mineru', fake_mineru) + + upload = UploadFile(filename='unsupported.pdf', file=BytesIO(b'%PDF-1.4 fake')) + + with pytest.raises(ValueError, match='PDF 解析失败: 当前安装的 MinerU 版本接口不兼容'): + await service.upload_document(user.id, upload) + + @pytest.mark.asyncio async def test_upload_document_raises_clear_error_when_pdf_dependency_is_missing(document_test_env, monkeypatch): session, user = document_test_env diff --git a/frontend/src/api/agent.ts b/frontend/src/api/agent.ts index 2498c51..7e9826b 100644 --- a/frontend/src/api/agent.ts +++ b/frontend/src/api/agent.ts @@ -7,6 +7,14 @@ export interface AgentStats { status: 'active' | 'idle' | 'disabled' } +export interface AgentHierarchyStatsNode extends AgentStats { + sub_commanders: AgentStats[] +} + +export interface AgentHierarchyStats { + main_agents: AgentHierarchyStatsNode[] +} + export interface AgentConfig { id: string name: string @@ -22,6 +30,11 @@ export const agentApi = { return res.data }, + async getHierarchyStats(): Promise { + const res = await api.get('/api/agents/stats/hierarchy') + return res.data + }, + async getConfig(id: string): Promise { const res = await api.get(`/api/agents/config/${id}`) return res.data diff --git a/frontend/src/app/navigation/nav.ts b/frontend/src/app/navigation/nav.ts index bfa3a0c..db4460e 100644 --- a/frontend/src/app/navigation/nav.ts +++ b/frontend/src/app/navigation/nav.ts @@ -21,7 +21,7 @@ export interface NavItem { export const navItems: NavItem[] = [ { name: '沟通系统', path: '/chat', icon: MessageCircle }, - { name: '智能链路', path: '/agents', icon: Bot }, + { name: '奥创中心', path: '/agents', icon: Bot }, { name: '技能中心', path: '/skills', icon: Star }, { name: '资料中枢', path: '/knowledge', icon: BookOpen }, { name: '知识大脑', path: '/brain', icon: Network }, diff --git a/frontend/src/pages/knowledge/composables/useKnowledgeView.test.ts b/frontend/src/pages/knowledge/composables/useKnowledgeView.test.ts index 47feb8d..0cbb463 100644 --- a/frontend/src/pages/knowledge/composables/useKnowledgeView.test.ts +++ b/frontend/src/pages/knowledge/composables/useKnowledgeView.test.ts @@ -181,7 +181,7 @@ describe('useKnowledgeView chunk editing', () => { expect(view.documents.value[0].id).toBe('doc-3') }) - it('enters a newly created folder so refresh keeps uploaded documents visible', async () => { + it('stays on the current listing after creating a folder and shows the new folder', async () => { const createdFolder = { id: 'folder-new', name: '新文件夹', @@ -190,17 +190,6 @@ describe('useKnowledgeView chunk editing', () => { created_at: '2026-03-22T00:00:00Z', updated_at: '2026-03-22T00:00:00Z', } - const uploadedDocument = { - id: 'doc-new', - title: 'Uploaded after create', - filename: 'uploaded.md', - file_type: 'md', - file_size: 256, - chunk_count: 1, - is_indexed: true, - folder_id: 'folder-new', - created_at: '2026-03-22T00:00:00Z', - } mocks.folderCreate.mockResolvedValue({ data: createdFolder }) mocks.folderGetTree.mockResolvedValue({ @@ -213,36 +202,18 @@ describe('useKnowledgeView chunk editing', () => { }, ], }) - mocks.documentList - .mockResolvedValueOnce({ data: [] }) - .mockResolvedValueOnce({ data: [uploadedDocument] }) - .mockResolvedValue({ data: [uploadedDocument] }) - - mocks.documentUpload.mockResolvedValue({ - data: { - id: 'doc-new', - title: 'Uploaded after create', - chunk_count: 1, - status: '上传成功,正在索引...', - ingestion_status: 'ready', - }, - }) const view = useKnowledgeView() + view.showNewFolderDialog.value = true view.newFolderName.value = '新文件夹' await view.createFolder() - expect(view.currentFolderId.value).toBe('folder-new') - expect(routeQuery.folder_id).toBe('folder-new') - expect(storage.get('knowledge.currentFolderId')).toBe('folder-new') - - const file = new File(['hello'], 'uploaded.md', { type: 'text/markdown' }) - const event = { target: { files: [file], value: 'uploaded.md' } } as unknown as Event - await view.handleUpload(event) - - expect(mocks.documentUpload).toHaveBeenCalledWith(file, 'folder-new') - expect(view.documents.value).toHaveLength(1) - expect(view.documents.value[0].folder_id).toBe('folder-new') + expect(view.currentFolderId.value).toBe(null) + expect(routeQuery.folder_id).toBeUndefined() + expect(storage.get('knowledge.currentFolderId')).toBeUndefined() + expect(view.showNewFolderDialog.value).toBe(false) + expect(view.visibleFolders.value).toHaveLength(1) + expect(view.visibleFolders.value[0].id).toBe('folder-new') }) it('loads documents at the root view instead of clearing the list', async () => { diff --git a/frontend/src/pages/knowledge/composables/useKnowledgeView.ts b/frontend/src/pages/knowledge/composables/useKnowledgeView.ts index a722caa..81ec22e 100644 --- a/frontend/src/pages/knowledge/composables/useKnowledgeView.ts +++ b/frontend/src/pages/knowledge/composables/useKnowledgeView.ts @@ -298,13 +298,12 @@ export function useKnowledgeView() { if (!newFolderName.value.trim()) return try { - const response = await folderApi.create({ + await folderApi.create({ name: newFolderName.value.trim(), parent_id: newFolderParentId.value, }) await loadFolders() showNewFolderDialog.value = false - await goToFolder(response.data.id) } catch (error) { console.error('创建文件夹失败:', error) } diff --git a/setup.bat b/setup.bat deleted file mode 100644 index e6c51c7..0000000 --- a/setup.bat +++ /dev/null @@ -1,168 +0,0 @@ -@echo off -chcp 65001 >nul -title Jarvis 配置向导 - -echo ========================================== -echo Jarvis 个人 AI 助理 - 配置向导 -echo ========================================== -echo. - -REM --- 选择 LLM 提供商 --- -echo 请选择 LLM 提供商: -echo [1] OpenAI (GPT-4o) - 默认推荐 -echo [2] DeepSeek (deepseek-chat) - 性价比高 -echo [3] Claude (Anthropic) - 强大稳定 -echo [4] Ollama (本地模型) - 完全免费 -echo. -set /p PROVIDER_CHOICE=请选择 (1-4), 默认1: - -if "%PROVIDER_CHOICE%"=="" set PROVIDER_CHOICE=1 -if "%PROVIDER_CHOICE%"=="1" goto :openai -if "%PROVIDER_CHOICE%"=="2" goto :deepseek -if "%PROVIDER_CHOICE%"=="3" goto :claude -if "%PROVIDER_CHOICE%"=="4" goto :ollama -goto :openai - -:openai -echo. -echo [选择] OpenAI (GPT-4o) -echo. -set /p OPENAI_API_KEY=请输入 OpenAI API Key (sk-...): -if "%OPENAI_API_KEY%"=="" ( - echo [错误] API Key 不能为空 - pause - exit /b 1 -) -goto :write_env - -:deepseek -echo. -echo [选择] DeepSeek -echo. -set /p OPENAI_API_KEY=请输入 DeepSeek API Key: -if "%OPENAI_API_KEY%"=="" ( - echo [错误] API Key 不能为空 - pause - exit /b 1 -) -goto :write_env - -:claude -echo. -echo [选择] Claude (Anthropic) -echo. -set /p ANTHROPIC_API_KEY=请输入 Anthropic API Key (sk-ant-...): -if "%ANTHROPIC_API_KEY%"=="" ( - echo [错误] API Key 不能为空 - pause - exit /b 1 -) -goto :write_env - -:ollama -echo. -echo [选择] Ollama (本地模型) -echo 请确保 Ollama 已启动 (ollama serve) -echo. -set /p OLLAMA_MODEL_IN=请输入模型名 (留空默认 llama3): -if "%OLLAMA_MODEL_IN%"=="" set OLLAMA_MODEL=llama3 -goto :write_env - -:write_env -echo. -echo [配置] 写入 backend\.env... - -( -echo # ============================================= -echo # Jarvis 后端配置 -echo # ============================================= -echo. -echo APP_NAME=Jarvis -echo APP_VERSION=0.1.0 -echo DEBUG=true -echo SECRET_KEY=jarvis-secret-key-change-in-production -echo. -echo CORS_ORIGINS=http://localhost:5173,http://localhost:3000 -echo. -echo # LLM 配置 -) > backend\.env - -if "%PROVIDER_CHOICE%"=="1" ( - ( - echo LLM_PROVIDER=openai - echo OPENAI_API_KEY=%OPENAI_API_KEY% - echo OPENAI_MODEL=gpt-4o - echo OPENAI_BASE_URL=https://api.openai.com/v1 - ) >> backend\.env -) else if "%PROVIDER_CHOICE%"=="2" ( - ( - echo LLM_PROVIDER=deepseek - echo OPENAI_API_KEY=%OPENAI_API_KEY% - echo OPENAI_BASE_URL=https://api.deepseek.com/v1 - ) >> backend\.env -) else if "%PROVIDER_CHOICE%"=="3" ( - ( - echo LLM_PROVIDER=claude - echo ANTHROPIC_API_KEY=%ANTHROPIC_API_KEY% - echo CLAUDE_MODEL=claude-sonnet-4-20250514 - ) >> backend\.env -) else ( - ( - echo LLM_PROVIDER=ollama - echo OLLAMA_BASE_URL=http://localhost:11434 - echo OLLAMA_MODEL=%OLLAMA_MODEL% - ) >> backend\.env -) - -( -echo. -echo # 数据库 -echo DATABASE_URL=sqlite+aiosqlite:///./data/jarvis.db -echo. -echo # ChromaDB -echo CHROMA_PERSIST_DIR=./data/chroma -echo. -echo # JWT -echo ACCESS_TOKEN_EXPIRE_MINUTES=1440 -echo. -echo # 上传 -echo UPLOAD_DIR=./data/uploads -echo MAX_UPLOAD_SIZE=52428800 -echo. -echo # 定时任务 -echo SCHEDULER_ENABLED=true -echo DAILY_PLAN_TIME=00:00 -echo FORUM_SCAN_INTERVAL_MINUTES=30 -echo. -echo DATA_DIR=./data -) >> backend\.env - -echo [OK] 配置完成 - -REM --- 检查 uv --- -echo. -echo [检查] 验证环境... -where uv >nul 2>&1 -if %errorlevel% neq 0 ( - echo [警告] 未找到 uv,请运行: pip install uv - echo 或访问: https://github.com/astral-sh/uv -) else ( - echo [OK] uv 已安装 -) - -where npm >nul 2>&1 -if %errorlevel% neq 0 ( - echo [警告] 未找到 npm,请安装 Node.js: https://nodejs.org -) else ( - echo [OK] npm 已安装 -) - -echo. -echo ========================================== -echo 配置完成! -echo. -echo LLM: %PROVIDER_CHOICE% -echo. -echo 下一步: 双击 start.bat 启动服务 -echo ========================================== -pause diff --git a/start.bat b/start.bat deleted file mode 100644 index b9e7473..0000000 --- a/start.bat +++ /dev/null @@ -1,115 +0,0 @@ -@echo off -setlocal -chcp 65001 >nul -title Jarvis Start - -set "ROOT=%~dp0" -set "BACKEND_DIR=%ROOT%backend" -set "FRONTEND_DIR=%ROOT%frontend" -set "LOG_DIR=%ROOT%logs" -set "PROJECT_ENV=%ROOT%.env" -if not exist "%LOG_DIR%" mkdir "%LOG_DIR%" - -echo ========================================== -echo Jarvis - Quick Start -echo ========================================== -echo. - -where uv >nul 2>&1 -if %errorlevel% neq 0 ( - echo [ERROR] uv was not found. Install it first: - echo pip install uv - echo. - pause - exit /b 1 -) - -where npm >nul 2>&1 -if %errorlevel% neq 0 ( - echo [ERROR] npm was not found. Install Node.js first. - echo. - pause - exit /b 1 -) - -if not exist "%PROJECT_ENV%" ( - echo [INFO] .env was not found in the project root. - echo [INFO] Create it before first run. - echo. -) - -echo [1/4] Install backend dependencies... -cd /d "%BACKEND_DIR%" -uv sync --quiet -if %errorlevel% neq 0 ( - echo [ERROR] Failed to install backend dependencies. - pause - exit /b 1 -) -echo [OK] Backend dependencies installed. - -echo. -echo [2/4] Install frontend dependencies... -cd /d "%FRONTEND_DIR%" -call npm install >nul 2>&1 -if %errorlevel% neq 0 ( - echo [ERROR] Failed to install frontend dependencies. - pause - exit /b 1 -) -echo [OK] Frontend dependencies installed. - -set "BACKEND_HOST=127.0.0.1" -set "BACKEND_PORT=" -if exist "%PROJECT_ENV%" ( - for /f "usebackq tokens=1,* delims==" %%A in ("%PROJECT_ENV%") do ( - if /I "%%A"=="HOST" set "BACKEND_HOST=%%B" - if /I "%%A"=="PORT" set "BACKEND_PORT=%%B" - ) -) -if "%BACKEND_PORT%"=="" ( - echo [ERROR] PORT was not found in .env. - echo [ERROR] Set PORT in the project root .env and run start.bat again. - pause - exit /b 1 -) -set "FRONTEND_API_URL=http://%BACKEND_HOST%:%BACKEND_PORT%" -> "%FRONTEND_DIR%\.env.local" echo VITE_API_URL=%FRONTEND_API_URL% -set "BACKEND_LOG=%LOG_DIR%\backend-start.log" -set "FRONTEND_LOG=%LOG_DIR%\frontend-start.log" - -echo. -echo [3/4] Start backend in background on %BACKEND_HOST%:%BACKEND_PORT%... -powershell -NoProfile -WindowStyle Hidden -Command "$wd='%BACKEND_DIR%'; $out='%BACKEND_LOG%'; Start-Process powershell -WindowStyle Hidden -WorkingDirectory $wd -ArgumentList '-NoProfile','-Command',('uv run uvicorn app.main:app --reload --host %BACKEND_HOST% --port %BACKEND_PORT% *>> ''{0}''' -f $out)" -if %errorlevel% neq 0 ( - echo [ERROR] Failed to start backend. - pause - exit /b 1 -) - -echo Waiting for backend... -ping 127.0.0.1 -n 6 >nul - -echo. -echo [4/4] Start frontend in background on port 5173... -powershell -NoProfile -WindowStyle Hidden -Command "$wd='%FRONTEND_DIR%'; $out='%FRONTEND_LOG%'; Start-Process powershell -WindowStyle Hidden -WorkingDirectory $wd -ArgumentList '-NoProfile','-Command',('npm run dev *>> ''{0}''' -f $out)" -if %errorlevel% neq 0 ( - echo [ERROR] Failed to start frontend. - pause - exit /b 1 -) - -echo. -echo ========================================== -echo Started -echo. -echo Backend: http://%BACKEND_HOST%:%BACKEND_PORT% -echo Frontend: http://localhost:5173 -echo API docs: http://%BACKEND_HOST%:%BACKEND_PORT%/docs -echo. -echo Logs: -echo - %BACKEND_LOG% -echo - %FRONTEND_LOG% -echo ========================================== -echo. -pause