Просмотр исходного кода

feat: switch web agent runtime to python langgraph

Sheep 1 час назад
Родитель
Сommit
850b421669

+ 9 - 0
.gitignore

@@ -36,6 +36,15 @@ target/
 **/.cache/
 .vite/
 **/.vite/
+__pycache__/
+**/__pycache__/
+.pytest_cache/
+**/.pytest_cache/
+*.pyc
+*.pyo
+*.pyd
+.venv/
+**/.venv/
 
 # Temporary files
 *.tmp

+ 14 - 4
scripts/start-web-agent.ps1

@@ -17,22 +17,32 @@ if (-not $env:WEB_AGENT_PORT) {
   $env:WEB_AGENT_PORT = "5188"
 }
 
-if (-not (Test-Path (Join-Path $runtimeDir "node_modules"))) {
+$venvDir = Join-Path $runtimeDir ".venv"
+$python = Join-Path $venvDir "Scripts\python.exe"
+
+if (-not (Test-Path $python)) {
   Push-Location $runtimeDir
   try {
-    npm install
+    python -m venv .venv
   } finally {
     Pop-Location
   }
 }
 
+Push-Location $runtimeDir
+try {
+  & $python -m pip install --disable-pip-version-check -r requirements.txt
+} finally {
+  Pop-Location
+}
+
 $stamp = Get-Date -Format "yyyyMMdd-HHmmss"
 $stdout = Join-Path $logsDir "web-agent-$stamp.out.log"
 $stderr = Join-Path $logsDir "web-agent-$stamp.err.log"
 
 Start-Process `
-  -FilePath "powershell" `
-  -ArgumentList @("-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", "npm run dev") `
+  -FilePath $python `
+  -ArgumentList @("-m", "app.server") `
   -WorkingDirectory $runtimeDir `
   -WindowStyle Hidden `
   -RedirectStandardOutput $stdout `

+ 9 - 7
smqjh-agent-runtime/README.md

@@ -4,14 +4,14 @@
 
 ## 架构
 
-- Web UI:负责聊天、表格渲染、工具轨迹展示。
-- LangGraph Runtime:负责多步规划、工具调用循环、结果汇总。
-- Java MCP:负责 DeepSeek、数据库只读查询、业务工具、配置和安全边界。
+- Web UI:负责聊天、表格渲染、工具轨迹展示。
+- Python LangGraph Runtime:负责多步规划、工具调用循环、结果汇总。
+- Java MCP:负责 DeepSeek、数据库只读查询、smqjh 业务工具、配置和安全边界。
 
 ```mermaid
 flowchart LR
   User["管理员 Web 对话"] --> Web["Web UI"]
-  Web --> Runtime["LangGraph Agent Runtime"]
+  Web --> Runtime["Python LangGraph Runtime"]
   Runtime --> MCP["Java MCP Server"]
   MCP --> DeepSeek["DeepSeek"]
   MCP --> DB["MySQL 只读查询"]
@@ -32,6 +32,8 @@ flowchart LR
 ..\scripts\start-web-agent.ps1
 ```
 
+`start-web-agent.ps1` 会自动创建 `.venv` 并安装 `requirements.txt` 中的 Python 依赖。
+
 浏览器访问:
 
 ```text
@@ -51,7 +53,7 @@ http://127.0.0.1:5188
 
 ## 设计约束
 
-- Runtime 不保存 DeepSeek Key、数据库账号或后台账号。
+- Runtime 不保存 DeepSeek Key、数据库账号或后台账号,敏感配置统一在 MCP 本机配置中管理
 - 业务事实查询必须通过 MCP 工具执行,不让管理员自己复制 SQL。
-- 工具输出中的 `columns/rows` 会独立渲染为表格。
-- 写入、发券、结算确认等危险动作继续走 MCP 审批/人工确认。
+- 工具输出中的 `columns/rows` 会独立渲染为表格,并支持下载 CSV
+- 写入、发券、结算确认等风险动作继续走 MCP 审批或人工确认。

+ 1 - 0
smqjh-agent-runtime/app/__init__.py

@@ -0,0 +1 @@
+"""Python LangGraph runtime for the SMQJH web agent."""

+ 5 - 0
smqjh-agent-runtime/app/__main__.py

@@ -0,0 +1,5 @@
+from .server import main
+
+
+if __name__ == "__main__":
+    main()

+ 532 - 0
smqjh-agent-runtime/app/agent_graph.py

@@ -0,0 +1,532 @@
+from __future__ import annotations
+
+import json
+import time
+from datetime import datetime, timezone
+from typing import Any, Literal
+
+from langgraph.graph import END, START, StateGraph
+
+from .config import RuntimeConfig
+from .mcp_client import McpClient
+from .models import (
+    AgentResultTable,
+    AgentRunResponse,
+    AgentState,
+    AgentTraceEvent,
+    ChatMessage,
+    McpToolDescriptor,
+    PlannedToolCall,
+    ToolObservation,
+)
+
+
+Route = Literal["execute_tools", "summarize", "chat"]
+
+
+class SmqjhAgentGraph:
+    def __init__(self, config: RuntimeConfig) -> None:
+        self.config = config
+        self.mcp = McpClient(config.mcp.url, config.mcp.token)
+        self.graph = self._build_graph()
+
+    def health(self) -> Any:
+        return self.mcp.status()
+
+    def tools(self) -> list[McpToolDescriptor]:
+        return self._load_runnable_tools()
+
+    def run(self, message: str, history: list[ChatMessage] | None = None) -> AgentRunResponse:
+        state: AgentState = {
+            "message": message,
+            "history": history or [],
+            "visitedSignatures": [],
+            "steps": 0,
+            "trace": [_trace("system", "收到用户请求", message)],
+        }
+        result = self.graph.invoke(state)
+        final = result.get("final")
+        if final:
+            return final
+        observations = result.get("observations", [])
+        trace_events = result.get("trace", [])
+        return {
+            "content": "这次没有生成有效回答,请稍后重试。",
+            "model": "none",
+            "usedMcp": False,
+            "steps": int(result.get("steps", 0)),
+            "toolCalls": observations,
+            "tables": _extract_tables(observations),
+            "trace": trace_events,
+        }
+
+    def _build_graph(self):
+        workflow = StateGraph(AgentState)
+        workflow.add_node("load_tools", self._load_tools_node)
+        workflow.add_node("plan", self._plan_node)
+        workflow.add_node("execute_tools", self._execute_tools_node)
+        workflow.add_node("summarize", self._summarize_node)
+        workflow.add_node("chat", self._chat_node)
+        workflow.add_edge(START, "load_tools")
+        workflow.add_edge("load_tools", "plan")
+        workflow.add_conditional_edges(
+            "plan",
+            self._route_after_plan,
+            {
+                "execute_tools": "execute_tools",
+                "summarize": "summarize",
+                "chat": "chat",
+            },
+        )
+        workflow.add_edge("execute_tools", "plan")
+        workflow.add_edge("summarize", END)
+        workflow.add_edge("chat", END)
+        return workflow.compile()
+
+    def _load_tools_node(self, state: AgentState) -> AgentState:
+        try:
+            tools = self._load_runnable_tools()
+            return {
+                "tools": tools,
+                "trace": _append_trace(state, _trace("system", "MCP 工具加载完成", f"可用工具 {len(tools)} 个")),
+            }
+        except Exception as error:
+            return {
+                "tools": [],
+                "trace": _append_trace(state, _trace("error", "MCP 工具加载失败", _error_message(error))),
+            }
+
+    def _plan_node(self, state: AgentState) -> AgentState:
+        if int(state.get("steps", 0)) >= self.config.max_steps:
+            return {
+                "plannedToolCalls": [],
+                "trace": _append_trace(state, _trace("plan", "达到最大工具步数", f"maxSteps={self.config.max_steps}")),
+            }
+
+        try:
+            result = self.mcp.call_tool(
+                "smqjh.ai.tool.plan",
+                {
+                    "request": self._assistant_request(state),
+                    "tools": state.get("tools", []),
+                    "observations": state.get("observations", []),
+                },
+            )
+            record = _as_dict(result.get("structuredContent"))
+            raw_calls = record.get("toolCalls")
+            raw_call_items = raw_calls if isinstance(raw_calls, list) else []
+            visited = set(state.get("visitedSignatures", []))
+            planned = [
+                call
+                for call in (_normalize_tool_call(item, state.get("message", "")) for item in raw_call_items)
+                if call and _tool_signature(call["name"], call["arguments"]) not in visited
+            ][:3]
+            return {
+                "plannedToolCalls": planned,
+                "trace": _append_trace(
+                    state,
+                    _trace(
+                        "plan",
+                        "DeepSeek 已规划工具调用" if planned else "DeepSeek 判断无需继续调用工具",
+                        _reason_text(record),
+                        {"toolCalls": planned},
+                    ),
+                ),
+            }
+        except Exception as error:
+            return {
+                "plannedToolCalls": [],
+                "trace": _append_trace(state, _trace("error", "DeepSeek 工具规划失败", _error_message(error))),
+            }
+
+    def _execute_tools_node(self, state: AgentState) -> AgentState:
+        by_name = {tool.get("name", ""): tool for tool in state.get("tools", [])}
+        observations: list[ToolObservation] = []
+        visited = set(state.get("visitedSignatures", []))
+
+        for call in state.get("plannedToolCalls", []):
+            requested_name = call["name"]
+            arguments = call["arguments"]
+            resolved_name = _resolve_tool_name(requested_name, by_name)
+            signature = _tool_signature(resolved_name or requested_name, arguments)
+            visited.add(signature)
+
+            if not resolved_name:
+                observations.append(
+                    {
+                        "name": requested_name,
+                        "source": "mcp",
+                        "arguments": arguments,
+                        "ok": False,
+                        "error": "DeepSeek 选择了未注册 MCP 工具",
+                    }
+                )
+                continue
+
+            started = time.perf_counter()
+            try:
+                result = self.mcp.call_tool(resolved_name, arguments)
+                observations.append(
+                    {
+                        "name": resolved_name,
+                        "source": "mcp",
+                        "arguments": arguments,
+                        "ok": True,
+                        "result": result.get("structuredContent", result),
+                        "durationMs": int((time.perf_counter() - started) * 1000),
+                    }
+                )
+            except Exception as error:
+                observations.append(
+                    {
+                        "name": resolved_name,
+                        "source": "mcp",
+                        "arguments": arguments,
+                        "ok": False,
+                        "error": _error_message(error),
+                        "durationMs": int((time.perf_counter() - started) * 1000),
+                    }
+                )
+
+        trace_events = state.get("trace", [])
+        for item in observations:
+            trace_events = [
+                *trace_events,
+                _trace(
+                    "tool" if item.get("ok") else "error",
+                    f"工具执行完成:{item['name']}" if item.get("ok") else f"工具执行失败:{item['name']}",
+                    f"{item.get('durationMs', 0)}ms" if item.get("ok") else item.get("error", ""),
+                    {"arguments": item.get("arguments"), "result": _summarize_result(item.get("result"))},
+                ),
+            ]
+
+        return {
+            "observations": [*state.get("observations", []), *observations],
+            "plannedToolCalls": [],
+            "visitedSignatures": list(visited),
+            "steps": int(state.get("steps", 0)) + 1,
+            "trace": trace_events,
+        }
+
+    def _summarize_node(self, state: AgentState) -> AgentState:
+        observations = state.get("observations", [])
+        tables = _extract_tables(observations)
+        try:
+            result = self.mcp.call_tool(
+                "smqjh.ai.tool.summarize",
+                {
+                    "request": self._assistant_request(state),
+                    "observations": observations,
+                },
+            )
+            record = _as_dict(result.get("structuredContent"))
+            content = str(record.get("content") or "").strip() or _fallback_summary(observations, tables)
+            model = str(record.get("model") or "mcp-deepseek")
+            trace_events = _append_trace(state, _trace("summary", "结果总结完成", f"{len(tables)} 个表格"))
+            return {
+                "final": {
+                    "content": content,
+                    "model": model,
+                    "usedMcp": True,
+                    "steps": int(state.get("steps", 0)),
+                    "toolCalls": observations,
+                    "tables": tables,
+                    "trace": trace_events,
+                },
+                "trace": trace_events,
+            }
+        except Exception as error:
+            trace_events = _append_trace(state, _trace("error", "结果总结失败,已使用工具结果兜底", _error_message(error)))
+            return {
+                "final": {
+                    "content": _fallback_summary(observations, tables),
+                    "model": "tool-only",
+                    "usedMcp": True,
+                    "steps": int(state.get("steps", 0)),
+                    "toolCalls": observations,
+                    "tables": tables,
+                    "trace": trace_events,
+                },
+                "trace": trace_events,
+            }
+
+    def _chat_node(self, state: AgentState) -> AgentState:
+        try:
+            result = self.mcp.call_tool("smqjh.ai.chat", {"request": self._assistant_request(state)})
+            record = _as_dict(result.get("structuredContent"))
+            content = str(record.get("content") or "").strip() or "DeepSeek 没有返回有效内容。"
+            model = str(record.get("model") or "mcp-deepseek")
+            trace_events = _append_trace(state, _trace("chat", "普通对话完成", model))
+            return {
+                "final": {
+                    "content": content,
+                    "model": model,
+                    "usedMcp": True,
+                    "steps": int(state.get("steps", 0)),
+                    "toolCalls": [],
+                    "tables": [],
+                    "trace": trace_events,
+                },
+                "trace": trace_events,
+            }
+        except Exception as error:
+            trace_events = _append_trace(state, _trace("error", "普通对话失败", _error_message(error)))
+            return {
+                "final": {
+                    "content": f"暂时无法完成回答:{_error_message(error)}",
+                    "model": "none",
+                    "usedMcp": False,
+                    "steps": int(state.get("steps", 0)),
+                    "toolCalls": [],
+                    "tables": [],
+                    "trace": trace_events,
+                },
+                "trace": trace_events,
+            }
+
+    def _route_after_plan(self, state: AgentState) -> Route:
+        if state.get("plannedToolCalls") and int(state.get("steps", 0)) < self.config.max_steps:
+            return "execute_tools"
+        if state.get("observations"):
+            return "summarize"
+        return "chat"
+
+    def _assistant_request(self, state: AgentState) -> dict[str, Any]:
+        return {
+            "message": state.get("message", ""),
+            "history": state.get("history", []),
+            "environmentName": self.config.environment_name,
+            "baseUrl": self.config.base_url,
+            "authenticated": True,
+            "username": "web-agent",
+        }
+
+    def _load_runnable_tools(self) -> list[McpToolDescriptor]:
+        return [tool for tool in self.mcp.list_tools() if not str(tool.get("name", "")).startswith("smqjh.ai.")]
+
+
+def _normalize_tool_call(value: Any, message: str) -> PlannedToolCall | None:
+    record = _as_dict(value)
+    name = str(record.get("name") or "").strip()
+    if not name:
+        return None
+    return {
+        "name": name,
+        "arguments": _repair_tool_arguments(name, _as_dict(record.get("arguments")), message),
+    }
+
+
+def _repair_tool_arguments(name: str, arguments: dict[str, Any], message: str) -> dict[str, Any]:
+    if name == "smqjh.database.smart.query" and not isinstance(arguments.get("question"), str):
+        return {**arguments, "question": message}
+    if name == "smqjh.product.lookup.summary":
+        current = str(arguments.get("productKeyword") or "").strip()
+        return {**arguments, "productKeyword": current or _extract_product_keyword(message)}
+    return arguments
+
+
+def _extract_product_keyword(message: str) -> str:
+    text = message
+    for token in [
+        "帮我",
+        "麻烦",
+        "查询一下",
+        "查一下",
+        "查询",
+        "查看",
+        "当前",
+        "业务系统",
+        "系统里面",
+        "系统里",
+        "后台",
+        "我方",
+        "我们的",
+        "商品库",
+        "商品表",
+        "商品描述是什么",
+        "商品描述",
+        "描述是什么",
+        "描述",
+        "价格是多少",
+        "价格",
+        "定价",
+        "是多少",
+        "是什么",
+        "呢",
+    ]:
+        text = text.replace(token, " ")
+    return " ".join(text.replace(",", " ").replace("。", " ").replace("?", " ").replace("?", " ").split())
+
+
+def _resolve_tool_name(name: str, by_name: dict[str, McpToolDescriptor]) -> str | None:
+    candidates = [
+        name,
+        name.removeprefix("smqjh.") if name.startswith("smqjh.") else f"smqjh.{name}",
+        "smqjh.product.lookup.summary" if name == "product.lookup.summary" else "",
+        "smqjh.order.count.query" if name == "order.count.query" else "",
+        "smqjh.database.smart.query" if name == "database.smart.query" else "",
+        "smqjh.database.readonly.query" if name == "database.readonly.query" else "",
+        "smqjh.cloud.health" if name == "cloud.health" else "",
+    ]
+    return next((candidate for candidate in candidates if candidate and candidate in by_name), None)
+
+
+def _extract_tables(observations: list[ToolObservation]) -> list[AgentResultTable]:
+    tables: list[AgentResultTable] = []
+    for observation in observations:
+        if not observation.get("ok"):
+            continue
+        tables.extend(_extract_tables_from_value(_tool_title(observation.get("name", "")), observation.get("result")))
+    return tables[:6]
+
+
+def _extract_tables_from_value(title: str, value: Any) -> list[AgentResultTable]:
+    tables: list[AgentResultTable] = []
+    seen: set[int] = set()
+
+    def visit(node: Any, current_title: str, depth: int) -> None:
+        if depth > 5 or not isinstance(node, dict):
+            return
+        node_id = id(node)
+        if node_id in seen:
+            return
+        seen.add(node_id)
+        add_rows_table(current_title, node, "rows")
+        add_rows_table(current_title, node, "comparisonRows")
+        for key in ("data", "structuredContent", "result", "record"):
+            if key in node:
+                visit(node[key], current_title, depth + 1)
+
+    def add_rows_table(current_title: str, record: dict[str, Any], rows_key: str) -> None:
+        rows = record.get(rows_key)
+        if not isinstance(rows, list) or not all(isinstance(row, dict) for row in rows):
+            return
+        row_records = [_normalize_row(row) for row in rows[:200]]
+        provided_columns = record.get("columns")
+        if isinstance(provided_columns, list) and provided_columns:
+            columns = [str(column) for column in provided_columns]
+        else:
+            columns = []
+            for row in row_records:
+                for key in row:
+                    if key not in columns:
+                        columns.append(key)
+        if not columns:
+            return
+        tables.append(
+            {
+                "title": _build_table_title(current_title, record, rows_key),
+                "columns": columns,
+                "rows": row_records,
+            }
+        )
+
+    visit(value, title, 0)
+    return tables
+
+
+def _fallback_summary(observations: list[ToolObservation], tables: list[AgentResultTable]) -> str:
+    ok_count = len([item for item in observations if item.get("ok")])
+    failed = [item for item in observations if not item.get("ok")]
+    lines = [f"已执行 {len(observations)} 个 MCP 工具,成功 {ok_count} 个。"]
+    if tables:
+        lines.append("结果已整理为下方表格。")
+    if failed:
+        lines.append("失败工具:" + ";".join(f"{item.get('name')}:{item.get('error') or '调用失败'}" for item in failed))
+    evidence = _find_evidence(observations)
+    if evidence:
+        lines.append(f"依据:{evidence}")
+    return "\n\n".join(lines)
+
+
+def _find_evidence(observations: list[ToolObservation]) -> str:
+    for observation in observations:
+        sql = _find_key_value(observation.get("result"), "executedSql", 0)
+        if isinstance(sql, str) and sql.strip():
+            return sql.strip()
+        evidence = _find_key_value(observation.get("result"), "evidence", 0)
+        if isinstance(evidence, str) and evidence.strip():
+            return evidence.strip()
+    return ""
+
+
+def _find_key_value(value: Any, key: str, depth: int) -> Any:
+    if depth > 4 or not isinstance(value, dict):
+        return None
+    if key in value:
+        return value[key]
+    for child in value.values():
+        result = _find_key_value(child, key, depth + 1)
+        if result is not None:
+            return result
+    return None
+
+
+def _trace(phase: str, title: str, detail: str = "", data: Any = None) -> AgentTraceEvent:
+    event: AgentTraceEvent = {
+        "at": datetime.now(timezone.utc).isoformat(),
+        "phase": phase,  # type: ignore[typeddict-item]
+        "title": title,
+    }
+    if detail:
+        event["detail"] = detail
+    if data is not None:
+        event["data"] = data
+    return event
+
+
+def _append_trace(state: AgentState, event: AgentTraceEvent) -> list[AgentTraceEvent]:
+    return [*state.get("trace", []), event]
+
+
+def _reason_text(record: dict[str, Any]) -> str:
+    reason = record.get("reason")
+    return reason.strip() if isinstance(reason, str) else ""
+
+
+def _summarize_result(value: Any) -> Any:
+    record = _as_dict(value)
+    if isinstance(record.get("rowCount"), int):
+        return {"rowCount": record.get("rowCount"), "title": record.get("title"), "summary": record.get("summary")}
+    if isinstance(record.get("rows"), list):
+        return {"rows": len(record["rows"])}
+    return value
+
+
+def _build_table_title(base_title: str, record: dict[str, Any], rows_key: str) -> str:
+    title = str(record.get("title") or base_title).strip()
+    count = f"({record['rowCount']} 行)" if "rowCount" in record else ""
+    return f"{title}:价格对比" if rows_key == "comparisonRows" else f"{title}{count}"
+
+
+def _tool_title(name: str) -> str:
+    titles = {
+        "smqjh.config.get": "运行配置",
+        "smqjh.cloud.health": "网关连通检查",
+        "smqjh.schema.search": "业务表搜索结果",
+        "smqjh.schema.getTable": "业务表说明",
+        "smqjh.schema.businessRules": "业务规则",
+        "smqjh.database.readonly.query": "数据库只读查询结果",
+        "smqjh.database.smart.query": "智能数据库查询结果",
+        "smqjh.order.count.query": "订单统计结果",
+        "smqjh.product.lookup.summary": "商品资料查询结果",
+        "smqjh.settlement.enterprise.list": "月结企业清单",
+        "smqjh.settlement.monthly.plan": "企业月结计划",
+    }
+    return titles.get(name, name)
+
+
+def _tool_signature(name: str, arguments: dict[str, Any]) -> str:
+    return f"{name}:{json.dumps(arguments, ensure_ascii=False, sort_keys=True, default=str)}"
+
+
+def _as_dict(value: Any) -> dict[str, Any]:
+    return value if isinstance(value, dict) else {}
+
+
+def _normalize_row(row: dict[str, Any]) -> dict[str, str]:
+    return {str(key): "" if value is None else str(value) for key, value in row.items()}
+
+
+def _error_message(error: Exception) -> str:
+    return str(error) or error.__class__.__name__

+ 49 - 0
smqjh-agent-runtime/app/config.py

@@ -0,0 +1,49 @@
+from __future__ import annotations
+
+import os
+from dataclasses import dataclass
+
+
+@dataclass(frozen=True)
+class McpConfig:
+    url: str
+    token: str
+
+
+@dataclass(frozen=True)
+class RuntimeConfig:
+    host: str
+    port: int
+    environment_name: str
+    base_url: str
+    max_steps: int
+    mcp: McpConfig
+
+
+def load_runtime_config() -> RuntimeConfig:
+    return RuntimeConfig(
+        host=os.getenv("WEB_AGENT_HOST", "0.0.0.0"),
+        port=_number_env("WEB_AGENT_PORT", 5188),
+        environment_name=os.getenv("SMQJH_ENVIRONMENT_NAME", "test-gateway"),
+        base_url=os.getenv("SMQJH_BASE_URL", "http://192.168.1.242:8080"),
+        max_steps=_number_env("SMQJH_AGENT_MAX_STEPS", 6),
+        mcp=McpConfig(
+            url=_normalize_url(os.getenv("SMQJH_MCP_URL") or os.getenv("MCP_URL") or "http://127.0.0.1:8765/mcp"),
+            token=os.getenv("SMQJH_MCP_TOKEN") or os.getenv("MCP_TOKEN") or "",
+        ),
+    )
+
+
+def _number_env(key: str, fallback: int) -> int:
+    raw = os.getenv(key)
+    if not raw:
+        return fallback
+    try:
+        value = int(raw)
+    except ValueError:
+        return fallback
+    return value if value > 0 else fallback
+
+
+def _normalize_url(value: str) -> str:
+    return value.strip().rstrip("/")

+ 78 - 0
smqjh-agent-runtime/app/mcp_client.py

@@ -0,0 +1,78 @@
+from __future__ import annotations
+
+import json
+import urllib.error
+import urllib.request
+from typing import Any
+
+from .models import McpToolDescriptor, McpToolResult
+
+
+class McpClient:
+    def __init__(self, endpoint: str, token: str = "") -> None:
+        self.endpoint = endpoint
+        self.token = token
+        self._request_id = 1
+
+    def status(self) -> Any:
+        request = urllib.request.Request(self.endpoint, headers=self._headers(), method="GET")
+        return self._send(request)
+
+    def list_tools(self) -> list[McpToolDescriptor]:
+        result = self._rpc("tools/list", {})
+        tools = result.get("tools") if isinstance(result, dict) else None
+        return tools if isinstance(tools, list) else []
+
+    def call_tool(self, name: str, arguments: dict[str, Any]) -> McpToolResult:
+        return self._rpc("tools/call", {"name": name, "arguments": arguments})
+
+    def _rpc(self, method: str, params: dict[str, Any]) -> Any:
+        payload = {
+            "jsonrpc": "2.0",
+            "id": self._request_id,
+            "method": method,
+            "params": params,
+        }
+        self._request_id += 1
+        data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
+        request = urllib.request.Request(self.endpoint, data=data, headers=self._headers(), method="POST")
+        response = self._send(request)
+        if not isinstance(response, dict):
+            raise RuntimeError("MCP returned invalid JSON-RPC payload")
+        if response.get("error"):
+            error = response["error"]
+            if isinstance(error, dict):
+                raise RuntimeError(str(error.get("message") or "MCP call failed"))
+            raise RuntimeError(str(error))
+        if "result" not in response:
+            raise RuntimeError("MCP returned no result")
+        return response["result"]
+
+    def _send(self, request: urllib.request.Request) -> Any:
+        try:
+            with urllib.request.urlopen(request, timeout=45) as response:
+                raw = response.read().decode("utf-8")
+                return json.loads(raw) if raw else {}
+        except urllib.error.HTTPError as error:
+            raw = error.read().decode("utf-8", errors="replace")
+            try:
+                payload = json.loads(raw)
+            except json.JSONDecodeError:
+                payload = {}
+            message = ""
+            if isinstance(payload, dict):
+                rpc_error = payload.get("error")
+                if isinstance(rpc_error, dict):
+                    message = str(rpc_error.get("message") or "")
+                message = message or str(payload.get("message") or "")
+            raise RuntimeError(message or f"MCP HTTP {error.code}") from error
+        except urllib.error.URLError as error:
+            raise RuntimeError(f"MCP connection failed: {error.reason}") from error
+
+    def _headers(self) -> dict[str, str]:
+        headers = {"Content-Type": "application/json"}
+        token = self.token.strip()
+        if token:
+            headers["Authorization"] = f"Bearer {token}"
+            headers["X-MCP-Token"] = token
+        return headers

+ 75 - 0
smqjh-agent-runtime/app/models.py

@@ -0,0 +1,75 @@
+from __future__ import annotations
+
+from typing import Any, Literal, TypedDict
+
+
+ChatRole = Literal["user", "assistant"]
+TracePhase = Literal["system", "plan", "tool", "summary", "chat", "error"]
+
+
+class ChatMessage(TypedDict):
+    role: ChatRole
+    content: str
+
+
+class McpToolDescriptor(TypedDict, total=False):
+    name: str
+    title: str
+    description: str
+    inputSchema: Any
+
+
+class McpToolResult(TypedDict, total=False):
+    content: list[dict[str, Any]]
+    structuredContent: Any
+
+
+class PlannedToolCall(TypedDict):
+    name: str
+    arguments: dict[str, Any]
+
+
+class ToolObservation(TypedDict, total=False):
+    name: str
+    source: Literal["mcp"]
+    arguments: dict[str, Any]
+    ok: bool
+    result: Any
+    error: str
+    durationMs: int
+
+
+class AgentTraceEvent(TypedDict, total=False):
+    at: str
+    phase: TracePhase
+    title: str
+    detail: str
+    data: Any
+
+
+class AgentResultTable(TypedDict):
+    title: str
+    columns: list[str]
+    rows: list[dict[str, str]]
+
+
+class AgentRunResponse(TypedDict):
+    content: str
+    model: str
+    usedMcp: bool
+    steps: int
+    toolCalls: list[ToolObservation]
+    tables: list[AgentResultTable]
+    trace: list[AgentTraceEvent]
+
+
+class AgentState(TypedDict, total=False):
+    message: str
+    history: list[ChatMessage]
+    tools: list[McpToolDescriptor]
+    plannedToolCalls: list[PlannedToolCall]
+    observations: list[ToolObservation]
+    visitedSignatures: list[str]
+    trace: list[AgentTraceEvent]
+    steps: int
+    final: AgentRunResponse

+ 180 - 0
smqjh-agent-runtime/app/server.py

@@ -0,0 +1,180 @@
+from __future__ import annotations
+
+import json
+import mimetypes
+import os
+from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
+from pathlib import Path
+from typing import Any
+from urllib.parse import unquote, urlparse
+
+from .agent_graph import SmqjhAgentGraph
+from .config import load_runtime_config
+
+
+RUNTIME_ROOT = Path(__file__).resolve().parents[1]
+REPO_ROOT = RUNTIME_ROOT.parent
+PUBLIC_ROOT = RUNTIME_ROOT / "public"
+LOGO_PATH = REPO_ROOT / "smqjh-admin-agent" / "build" / "logo.png"
+MAX_BODY_BYTES = 1024 * 1024
+
+CONFIG = load_runtime_config()
+AGENT = SmqjhAgentGraph(CONFIG)
+
+
+class AgentRequestHandler(BaseHTTPRequestHandler):
+    server_version = "SMQJHAgentPython/0.1"
+
+    def do_OPTIONS(self) -> None:
+        self._send_text(204, "")
+
+    def do_GET(self) -> None:
+        try:
+            path = urlparse(self.path).path
+            if path == "/api/health":
+                self._handle_health()
+                return
+            if path == "/api/tools":
+                self._send_json(200, {"ok": True, "tools": AGENT.tools()})
+                return
+            if path == "/assets/logo.png":
+                self._serve_file(LOGO_PATH, "image/png")
+                return
+            self._serve_static(path)
+        except Exception as error:
+            self._send_json(500, {"ok": False, "message": str(error) or error.__class__.__name__})
+
+    def do_POST(self) -> None:
+        try:
+            path = urlparse(self.path).path
+            if path != "/api/chat":
+                self._send_json(404, {"ok": False, "message": "Not found"})
+                return
+            payload = self._read_json_body()
+            message = str(payload.get("message") or "").strip() if isinstance(payload, dict) else ""
+            if not message:
+                self._send_json(400, {"ok": False, "message": "message is required"})
+                return
+            history = payload.get("history") if isinstance(payload, dict) else []
+            if not isinstance(history, list):
+                history = []
+            result = AGENT.run(message, history)
+            self._send_json(200, {"ok": True, "result": result})
+        except Exception as error:
+            self._send_json(500, {"ok": False, "message": str(error) or error.__class__.__name__})
+
+    def log_message(self, format: str, *args: Any) -> None:
+        if os.getenv("WEB_AGENT_ACCESS_LOG", "").lower() in {"1", "true", "yes"}:
+            super().log_message(format, *args)
+
+    def _handle_health(self) -> None:
+        try:
+            mcp = AGENT.health()
+            self._send_json(
+                200,
+                {
+                    "ok": True,
+                    "runtime": {
+                        "name": "smqjh-agent-runtime",
+                        "mode": "web",
+                        "framework": "Python + LangGraph",
+                        "environmentName": CONFIG.environment_name,
+                        "baseUrl": CONFIG.base_url,
+                    },
+                    "mcp": mcp,
+                },
+            )
+        except Exception as error:
+            self._send_json(
+                200,
+                {
+                    "ok": False,
+                    "runtime": {
+                        "name": "smqjh-agent-runtime",
+                        "mode": "web",
+                        "framework": "Python + LangGraph",
+                    },
+                    "mcp": {
+                        "ok": False,
+                        "message": str(error) or error.__class__.__name__,
+                    },
+                },
+            )
+
+    def _serve_static(self, request_path: str) -> None:
+        clean_path = "/index.html" if request_path == "/" else request_path
+        relative = unquote(clean_path).lstrip("/")
+        file_path = (PUBLIC_ROOT / relative).resolve()
+        try:
+            file_path.relative_to(PUBLIC_ROOT.resolve())
+        except ValueError:
+            self._send_text(403, "Forbidden")
+            return
+        self._serve_file(file_path)
+
+    def _serve_file(self, file_path: Path, content_type: str | None = None) -> None:
+        if not file_path.exists() or not file_path.is_file():
+            self._send_text(404, "Not found")
+            return
+        mime = content_type or mimetypes.guess_type(str(file_path))[0] or "application/octet-stream"
+        data = file_path.read_bytes()
+        self.send_response(200)
+        self._cors_headers()
+        self.send_header("Content-Type", _with_charset(mime))
+        self.send_header("Cache-Control", "no-cache" if file_path.suffix == ".html" else "public, max-age=300")
+        self.send_header("Content-Length", str(len(data)))
+        self.end_headers()
+        self.wfile.write(data)
+
+    def _read_json_body(self) -> dict[str, Any]:
+        length = int(self.headers.get("Content-Length") or "0")
+        if length > MAX_BODY_BYTES:
+            raise ValueError("Request body is too large")
+        raw = self.rfile.read(length).decode("utf-8") if length else "{}"
+        return json.loads(raw) if raw.strip() else {}
+
+    def _send_json(self, status: int, payload: Any) -> None:
+        body = json.dumps(payload, ensure_ascii=False, default=str).encode("utf-8")
+        self.send_response(status)
+        self._cors_headers()
+        self.send_header("Content-Type", "application/json; charset=utf-8")
+        self.send_header("Content-Length", str(len(body)))
+        self.end_headers()
+        self.wfile.write(body)
+
+    def _send_text(self, status: int, text: str) -> None:
+        body = text.encode("utf-8")
+        self.send_response(status)
+        self._cors_headers()
+        self.send_header("Content-Type", "text/plain; charset=utf-8")
+        self.send_header("Content-Length", str(len(body)))
+        self.end_headers()
+        self.wfile.write(body)
+
+    def _cors_headers(self) -> None:
+        self.send_header("Access-Control-Allow-Origin", "*")
+        self.send_header("Access-Control-Allow-Methods", "GET,POST,OPTIONS")
+        self.send_header("Access-Control-Allow-Headers", "Content-Type,Authorization,X-MCP-Token")
+
+
+def _with_charset(mime: str) -> str:
+    if mime.startswith("text/") or mime in {"application/javascript", "application/json"}:
+        return f"{mime}; charset=utf-8"
+    return mime
+
+
+def main() -> None:
+    server = ThreadingHTTPServer((CONFIG.host, CONFIG.port), AgentRequestHandler)
+    print(f"SMQJH Python Web Agent started: http://127.0.0.1:{CONFIG.port}", flush=True)
+    print(f"Runtime framework: Python + LangGraph", flush=True)
+    print(f"MCP endpoint: {CONFIG.mcp.url}", flush=True)
+    try:
+        server.serve_forever()
+    except KeyboardInterrupt:
+        pass
+    finally:
+        server.server_close()
+
+
+if __name__ == "__main__":
+    main()

+ 0 - 876
smqjh-agent-runtime/package-lock.json

@@ -1,876 +0,0 @@
-{
-  "name": "smqjh-agent-runtime",
-  "version": "0.1.0",
-  "lockfileVersion": 3,
-  "requires": true,
-  "packages": {
-    "": {
-      "name": "smqjh-agent-runtime",
-      "version": "0.1.0",
-      "dependencies": {
-        "@langchain/langgraph": "^1.0.2"
-      },
-      "devDependencies": {
-        "@types/node": "^20.11.30",
-        "tsx": "^4.20.5",
-        "typescript": "^5.9.3"
-      }
-    },
-    "node_modules/@cfworker/json-schema": {
-      "version": "4.1.1",
-      "resolved": "https://registry.npmjs.org/@cfworker/json-schema/-/json-schema-4.1.1.tgz",
-      "integrity": "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==",
-      "license": "MIT",
-      "peer": true
-    },
-    "node_modules/@esbuild/aix-ppc64": {
-      "version": "0.28.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
-      "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
-      "cpu": [
-        "ppc64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "aix"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/android-arm": {
-      "version": "0.28.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
-      "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
-      "cpu": [
-        "arm"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "android"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/android-arm64": {
-      "version": "0.28.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
-      "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "android"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/android-x64": {
-      "version": "0.28.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
-      "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "android"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/darwin-arm64": {
-      "version": "0.28.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
-      "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "darwin"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/darwin-x64": {
-      "version": "0.28.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
-      "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "darwin"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/freebsd-arm64": {
-      "version": "0.28.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
-      "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "freebsd"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/freebsd-x64": {
-      "version": "0.28.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
-      "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "freebsd"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/linux-arm": {
-      "version": "0.28.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
-      "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
-      "cpu": [
-        "arm"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/linux-arm64": {
-      "version": "0.28.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
-      "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/linux-ia32": {
-      "version": "0.28.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
-      "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
-      "cpu": [
-        "ia32"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/linux-loong64": {
-      "version": "0.28.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
-      "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
-      "cpu": [
-        "loong64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/linux-mips64el": {
-      "version": "0.28.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
-      "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
-      "cpu": [
-        "mips64el"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/linux-ppc64": {
-      "version": "0.28.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
-      "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
-      "cpu": [
-        "ppc64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/linux-riscv64": {
-      "version": "0.28.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
-      "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
-      "cpu": [
-        "riscv64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/linux-s390x": {
-      "version": "0.28.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
-      "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
-      "cpu": [
-        "s390x"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/linux-x64": {
-      "version": "0.28.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
-      "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/netbsd-arm64": {
-      "version": "0.28.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
-      "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "netbsd"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/netbsd-x64": {
-      "version": "0.28.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
-      "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "netbsd"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/openbsd-arm64": {
-      "version": "0.28.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
-      "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "openbsd"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/openbsd-x64": {
-      "version": "0.28.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
-      "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "openbsd"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/openharmony-arm64": {
-      "version": "0.28.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
-      "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "openharmony"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/sunos-x64": {
-      "version": "0.28.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
-      "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "sunos"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/win32-arm64": {
-      "version": "0.28.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
-      "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "win32"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/win32-ia32": {
-      "version": "0.28.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
-      "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
-      "cpu": [
-        "ia32"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "win32"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/win32-x64": {
-      "version": "0.28.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
-      "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "win32"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@langchain/core": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/@langchain/core/-/core-1.2.1.tgz",
-      "integrity": "sha512-NNG/cC5FGuHDOAP56h0ddp8Rfk8p+othWzEK5RV9JIG6RvnF5vGa5r0AEGtKfQieed7s1kC42GuIzVOBvMBL/g==",
-      "license": "MIT",
-      "peer": true,
-      "dependencies": {
-        "@cfworker/json-schema": "^4.0.2",
-        "@standard-schema/spec": "^1.1.0",
-        "js-tiktoken": "^1.0.12",
-        "langsmith": ">=0.5.0 <1.0.0",
-        "mustache": "^4.2.0",
-        "p-queue": "^6.6.2",
-        "zod": "^3.25.76 || ^4"
-      },
-      "engines": {
-        "node": ">=20"
-      }
-    },
-    "node_modules/@langchain/langgraph": {
-      "version": "1.4.6",
-      "resolved": "https://registry.npmjs.org/@langchain/langgraph/-/langgraph-1.4.6.tgz",
-      "integrity": "sha512-5xaohfKGmA2Aw1P9TfzC15RI8SqVP2Vi9Xtfz8ClBeksd5NXImBp6e0Hss6FnErDPgXP5uTohpm50ca4t5Pljg==",
-      "license": "MIT",
-      "dependencies": {
-        "@langchain/langgraph-checkpoint": "^1.1.3",
-        "@langchain/langgraph-sdk": "~1.9.25",
-        "@langchain/protocol": "^0.0.18",
-        "@standard-schema/spec": "1.1.0"
-      },
-      "engines": {
-        "node": ">=18"
-      },
-      "peerDependencies": {
-        "@langchain/core": "^1.1.48",
-        "zod": "^3.25.32 || ^4.2.0",
-        "zod-to-json-schema": "^3.x"
-      },
-      "peerDependenciesMeta": {
-        "zod-to-json-schema": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/@langchain/langgraph-checkpoint": {
-      "version": "1.1.3",
-      "resolved": "https://registry.npmjs.org/@langchain/langgraph-checkpoint/-/langgraph-checkpoint-1.1.3.tgz",
-      "integrity": "sha512-wgzdQNeEsdw1e+4lvlj0tdq/RYR/k1vPin10g0ymGoehZDDgd9nvIllGXSXN4TFgF9sf5qQP/KTkOcLfeseIhA==",
-      "license": "MIT",
-      "engines": {
-        "node": ">=18"
-      },
-      "peerDependencies": {
-        "@langchain/core": "^1.1.48"
-      }
-    },
-    "node_modules/@langchain/langgraph-sdk": {
-      "version": "1.9.25",
-      "resolved": "https://registry.npmjs.org/@langchain/langgraph-sdk/-/langgraph-sdk-1.9.25.tgz",
-      "integrity": "sha512-mRKW8zyQUaHox+HirRFMRrPqOvNbQI3xeXDt6kkk4PbBg77V92bsO1WzUVNrmJ81zCkvxyOrWSK8D6ioCj0a8A==",
-      "license": "MIT",
-      "dependencies": {
-        "@langchain/protocol": "^0.0.18",
-        "@types/json-schema": "^7.0.15",
-        "p-queue": "^9.0.1",
-        "p-retry": "^7.1.1"
-      },
-      "peerDependencies": {
-        "@langchain/core": "^1.1.48",
-        "react": "^18 || ^19",
-        "react-dom": "^18 || ^19",
-        "svelte": "^4.0.0 || ^5.0.0",
-        "vue": "^3.0.0"
-      },
-      "peerDependenciesMeta": {
-        "react": {
-          "optional": true
-        },
-        "react-dom": {
-          "optional": true
-        },
-        "svelte": {
-          "optional": true
-        },
-        "vue": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/@langchain/langgraph-sdk/node_modules/eventemitter3": {
-      "version": "5.0.4",
-      "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
-      "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
-      "license": "MIT"
-    },
-    "node_modules/@langchain/langgraph-sdk/node_modules/p-queue": {
-      "version": "9.3.0",
-      "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.3.0.tgz",
-      "integrity": "sha512-7NED7xhQ74Ngp4JP/2e0VZHp7vSWfJfqeiR92jPgxsz6m0Se4P03YoTKa9dDXyZ3r6P616gUXttrB6nnHYKang==",
-      "license": "MIT",
-      "dependencies": {
-        "eventemitter3": "^5.0.4",
-        "p-timeout": "^7.0.0"
-      },
-      "engines": {
-        "node": ">=20"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/@langchain/langgraph-sdk/node_modules/p-timeout": {
-      "version": "7.0.1",
-      "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-7.0.1.tgz",
-      "integrity": "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==",
-      "license": "MIT",
-      "engines": {
-        "node": ">=20"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/@langchain/protocol": {
-      "version": "0.0.18",
-      "resolved": "https://registry.npmjs.org/@langchain/protocol/-/protocol-0.0.18.tgz",
-      "integrity": "sha512-XW1egQtPfsGI41w2AMZNFZrUIwFSQHTjVMZs0OaTpCAvht/QLoaPN8FQcsysMVypOhupG28J29yOorrc70otBQ==",
-      "license": "MIT"
-    },
-    "node_modules/@standard-schema/spec": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
-      "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
-      "license": "MIT"
-    },
-    "node_modules/@types/json-schema": {
-      "version": "7.0.15",
-      "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
-      "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
-      "license": "MIT"
-    },
-    "node_modules/@types/node": {
-      "version": "20.19.43",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz",
-      "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "undici-types": "~6.21.0"
-      }
-    },
-    "node_modules/base64-js": {
-      "version": "1.5.1",
-      "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
-      "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
-      "funding": [
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/feross"
-        },
-        {
-          "type": "patreon",
-          "url": "https://www.patreon.com/feross"
-        },
-        {
-          "type": "consulting",
-          "url": "https://feross.org/support"
-        }
-      ],
-      "license": "MIT",
-      "peer": true
-    },
-    "node_modules/esbuild": {
-      "version": "0.28.1",
-      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
-      "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
-      "dev": true,
-      "hasInstallScript": true,
-      "license": "MIT",
-      "bin": {
-        "esbuild": "bin/esbuild"
-      },
-      "engines": {
-        "node": ">=18"
-      },
-      "optionalDependencies": {
-        "@esbuild/aix-ppc64": "0.28.1",
-        "@esbuild/android-arm": "0.28.1",
-        "@esbuild/android-arm64": "0.28.1",
-        "@esbuild/android-x64": "0.28.1",
-        "@esbuild/darwin-arm64": "0.28.1",
-        "@esbuild/darwin-x64": "0.28.1",
-        "@esbuild/freebsd-arm64": "0.28.1",
-        "@esbuild/freebsd-x64": "0.28.1",
-        "@esbuild/linux-arm": "0.28.1",
-        "@esbuild/linux-arm64": "0.28.1",
-        "@esbuild/linux-ia32": "0.28.1",
-        "@esbuild/linux-loong64": "0.28.1",
-        "@esbuild/linux-mips64el": "0.28.1",
-        "@esbuild/linux-ppc64": "0.28.1",
-        "@esbuild/linux-riscv64": "0.28.1",
-        "@esbuild/linux-s390x": "0.28.1",
-        "@esbuild/linux-x64": "0.28.1",
-        "@esbuild/netbsd-arm64": "0.28.1",
-        "@esbuild/netbsd-x64": "0.28.1",
-        "@esbuild/openbsd-arm64": "0.28.1",
-        "@esbuild/openbsd-x64": "0.28.1",
-        "@esbuild/openharmony-arm64": "0.28.1",
-        "@esbuild/sunos-x64": "0.28.1",
-        "@esbuild/win32-arm64": "0.28.1",
-        "@esbuild/win32-ia32": "0.28.1",
-        "@esbuild/win32-x64": "0.28.1"
-      }
-    },
-    "node_modules/eventemitter3": {
-      "version": "4.0.7",
-      "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
-      "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
-      "license": "MIT",
-      "peer": true
-    },
-    "node_modules/fsevents": {
-      "version": "2.3.3",
-      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
-      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
-      "dev": true,
-      "hasInstallScript": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "darwin"
-      ],
-      "engines": {
-        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
-      }
-    },
-    "node_modules/is-network-error": {
-      "version": "1.3.2",
-      "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.2.tgz",
-      "integrity": "sha512-PhBY86zaxNZUuWP6h13Vu5oFe0XY6/UlKzQnYFELzGVHygP3MxmvTfYSG7GN3aIab/iWudSMgjSnG9Dq+nHrgA==",
-      "license": "MIT",
-      "engines": {
-        "node": ">=16"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/js-tiktoken": {
-      "version": "1.0.21",
-      "resolved": "https://registry.npmjs.org/js-tiktoken/-/js-tiktoken-1.0.21.tgz",
-      "integrity": "sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==",
-      "license": "MIT",
-      "peer": true,
-      "dependencies": {
-        "base64-js": "^1.5.1"
-      }
-    },
-    "node_modules/langsmith": {
-      "version": "0.7.11",
-      "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.7.11.tgz",
-      "integrity": "sha512-kK7tjvLo3KbyrXonZDJJMvX6jE5Ek9uwqtS2WX8AEGc5IwLoMrJlGR8wWk1UD8H/x0goiJDMsRht4qZd7Ijvcg==",
-      "license": "MIT",
-      "peer": true,
-      "dependencies": {
-        "p-queue": "6.6.2"
-      },
-      "peerDependencies": {
-        "@opentelemetry/api": "*",
-        "@opentelemetry/exporter-trace-otlp-proto": "*",
-        "@opentelemetry/sdk-trace-base": "*",
-        "openai": "*",
-        "ws": ">=7"
-      },
-      "peerDependenciesMeta": {
-        "@opentelemetry/api": {
-          "optional": true
-        },
-        "@opentelemetry/exporter-trace-otlp-proto": {
-          "optional": true
-        },
-        "@opentelemetry/sdk-trace-base": {
-          "optional": true
-        },
-        "openai": {
-          "optional": true
-        },
-        "ws": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/mustache": {
-      "version": "4.2.0",
-      "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz",
-      "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==",
-      "license": "MIT",
-      "peer": true,
-      "bin": {
-        "mustache": "bin/mustache"
-      }
-    },
-    "node_modules/p-finally": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
-      "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==",
-      "license": "MIT",
-      "peer": true,
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/p-queue": {
-      "version": "6.6.2",
-      "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz",
-      "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==",
-      "license": "MIT",
-      "peer": true,
-      "dependencies": {
-        "eventemitter3": "^4.0.4",
-        "p-timeout": "^3.2.0"
-      },
-      "engines": {
-        "node": ">=8"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/p-retry": {
-      "version": "7.1.1",
-      "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-7.1.1.tgz",
-      "integrity": "sha512-J5ApzjyRkkf601HpEeykoiCvzHQjWxPAHhyjFcEUP2SWq0+35NKh8TLhpLw+Dkq5TZBFvUM6UigdE9hIVYTl5w==",
-      "license": "MIT",
-      "dependencies": {
-        "is-network-error": "^1.1.0"
-      },
-      "engines": {
-        "node": ">=20"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/p-timeout": {
-      "version": "3.2.0",
-      "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz",
-      "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==",
-      "license": "MIT",
-      "peer": true,
-      "dependencies": {
-        "p-finally": "^1.0.0"
-      },
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/tsx": {
-      "version": "4.22.4",
-      "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz",
-      "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "esbuild": "~0.28.0"
-      },
-      "bin": {
-        "tsx": "dist/cli.mjs"
-      },
-      "engines": {
-        "node": ">=18.0.0"
-      },
-      "optionalDependencies": {
-        "fsevents": "~2.3.3"
-      }
-    },
-    "node_modules/typescript": {
-      "version": "5.9.3",
-      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
-      "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
-      "dev": true,
-      "license": "Apache-2.0",
-      "bin": {
-        "tsc": "bin/tsc",
-        "tsserver": "bin/tsserver"
-      },
-      "engines": {
-        "node": ">=14.17"
-      }
-    },
-    "node_modules/undici-types": {
-      "version": "6.21.0",
-      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
-      "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
-      "dev": true,
-      "license": "MIT"
-    },
-    "node_modules/zod": {
-      "version": "4.4.3",
-      "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
-      "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
-      "license": "MIT",
-      "peer": true,
-      "funding": {
-        "url": "https://github.com/sponsors/colinhacks"
-      }
-    }
-  }
-}

+ 0 - 20
smqjh-agent-runtime/package.json

@@ -1,20 +0,0 @@
-{
-  "name": "smqjh-agent-runtime",
-  "version": "0.1.0",
-  "private": true,
-  "type": "module",
-  "description": "Web test runtime for the SMQJH LangGraph agent.",
-  "scripts": {
-    "dev": "tsx src/server.ts",
-    "build": "tsc -p tsconfig.json",
-    "start": "node dist/server.js"
-  },
-  "dependencies": {
-    "@langchain/langgraph": "^1.0.2"
-  },
-  "devDependencies": {
-    "@types/node": "^20.11.30",
-    "tsx": "^4.20.5",
-    "typescript": "^5.9.3"
-  }
-}

+ 59 - 11
smqjh-agent-runtime/public/index.html

@@ -7,12 +7,13 @@
     <link rel="stylesheet" href="/styles.css" />
   </head>
   <body>
-    <aside class="sidebar">
+    <div class="scanline"></div>
+    <header class="top-nav">
       <div class="brand">
         <img src="/assets/logo.png" alt="" />
         <div>
           <strong>市民请集合</strong>
-          <span>Web Agent 测试版</span>
+          <span>PYTHON LANGGRAPH AGENT</span>
         </div>
       </div>
       <nav class="nav">
@@ -27,19 +28,62 @@
           <span id="mcpStatus">MCP 连接检测中</span>
         </div>
       </div>
-    </aside>
+    </header>
 
     <main class="shell">
-      <header class="topbar">
-        <div>
+      <section class="hero">
+        <div class="hero-copy">
+          <span class="eyebrow">Y2K OPERATIONS CONSOLE</span>
           <h1>市民请集合智能助手</h1>
-          <p>LangGraph Runtime · MCP · DeepSeek</p>
+          <p>Python + LangGraph 驱动的后台业务 Agent。通过 MCP 调用 DeepSeek、只读数据库和 smqjh 业务工具,直接输出结论、依据和表格。</p>
+          <div class="hero-actions">
+            <button id="refreshBtn" class="chrome-button" type="button" title="刷新状态">刷新状态</button>
+            <span class="system-pill">MCP / DB / DEEPSEEK</span>
+          </div>
+        </div>
+        <div class="hero-panel" aria-hidden="true">
+          <div class="orbital-ring"></div>
+          <div class="hero-logo">
+            <img src="/assets/logo.png" alt="" />
+          </div>
+          <div class="mini-display">
+            <span>AGENT CORE</span>
+            <strong>ONLINE</strong>
+          </div>
         </div>
-        <button id="refreshBtn" class="icon-button" type="button" title="刷新状态">刷新</button>
-      </header>
+      </section>
+
+      <section class="feature-strip" aria-label="Agent capabilities">
+        <article class="feature-card">
+          <span>01</span>
+          <strong>智能查询</strong>
+          <p>商品、订单、会员、物流与经营数据自动判断工具。</p>
+        </article>
+        <article class="feature-card">
+          <span>02</span>
+          <strong>只读安全</strong>
+          <p>数据库查询走 MCP 白名单和 SELECT 校验。</p>
+        </article>
+        <article class="feature-card">
+          <span>03</span>
+          <strong>表格输出</strong>
+          <p>结构化 rows/columns 直接渲染,可下载 CSV。</p>
+        </article>
+        <article class="feature-card">
+          <span>04</span>
+          <strong>月结扩展</strong>
+          <p>企业月结、核对、人工确认流程继续接入。</p>
+        </article>
+      </section>
 
       <section class="workspace">
-        <section class="chat-card">
+        <section class="chat-card retro-window">
+          <div class="window-bar">
+            <span></span>
+            <span></span>
+            <span></span>
+            <strong>SMQJH_AGENT.EXE</strong>
+          </div>
           <div id="messages" class="messages"></div>
           <form id="chatForm" class="composer">
             <textarea
@@ -58,13 +102,17 @@
         </section>
 
         <aside class="inspector">
-          <section class="panel">
+          <section class="panel player-panel">
             <div class="panel-title">
               <h2>执行上下文</h2>
             </div>
+            <div class="player-screen">
+              <span>RUNTIME</span>
+              <strong>Python + LangGraph</strong>
+            </div>
             <dl class="kv">
               <div><dt>运行模式</dt><dd>Web 测试</dd></div>
-              <div><dt>框架</dt><dd>LangGraph</dd></div>
+              <div><dt>框架</dt><dd>Python + LangGraph</dd></div>
               <div><dt>MCP</dt><dd id="mcpEndpoint">-</dd></div>
               <div><dt>工具</dt><dd id="toolCount">-</dd></div>
             </dl>

+ 558 - 154
smqjh-agent-runtime/public/styles.css

@@ -1,31 +1,58 @@
 :root {
   color-scheme: light;
-  font-family: "Microsoft YaHei", "PingFang SC", "Segoe UI", Arial, sans-serif;
-  --bg: #f4f7f9;
-  --sidebar: #202731;
-  --sidebar-soft: #2c3644;
-  --line: #dce4ea;
-  --text: #10202b;
-  --muted: #657385;
-  --brand: #19735e;
-  --brand-strong: #0d5f4e;
-  --assistant: #f0f3f6;
-  --user: #e4f5ee;
-  --danger: #b42318;
-  --shadow: 0 16px 40px rgba(20, 37, 49, 0.08);
+  font-family: "Arial Narrow", "Bahnschrift", "Microsoft YaHei", "PingFang SC", "Segoe UI", Arial, sans-serif;
+  --bg: #f7fbff;
+  --ink: #0a1230;
+  --muted: #526076;
+  --chrome: linear-gradient(135deg, #ffffff 0%, #dfe9ff 30%, #8df7ff 52%, #ff9bf2 72%, #ffffff 100%);
+  --glass: rgba(255, 255, 255, 0.68);
+  --glass-strong: rgba(255, 255, 255, 0.86);
+  --line: rgba(75, 106, 171, 0.22);
+  --cyan: #00d9ff;
+  --mint: #22ffc8;
+  --pink: #ff4fd8;
+  --purple: #7d5cff;
+  --blue: #236dff;
+  --warning: #ffb02e;
+  --danger: #d70065;
+  --shadow: 0 22px 60px rgba(38, 70, 137, 0.18);
+  --glow-cyan: 0 0 0 1px rgba(0, 217, 255, 0.55), 0 0 24px rgba(0, 217, 255, 0.28);
+  --glow-pink: 0 0 0 1px rgba(255, 79, 216, 0.55), 0 0 24px rgba(255, 79, 216, 0.26);
 }
 
 * {
   box-sizing: border-box;
 }
 
+html {
+  min-height: 100%;
+}
+
 body {
   margin: 0;
   min-height: 100vh;
-  display: grid;
-  grid-template-columns: 236px minmax(0, 1fr);
-  background: var(--bg);
-  color: var(--text);
+  color: var(--ink);
+  background:
+    linear-gradient(rgba(35, 109, 255, 0.08) 1px, transparent 1px),
+    linear-gradient(90deg, rgba(255, 79, 216, 0.08) 1px, transparent 1px),
+    radial-gradient(circle at 20% 14%, rgba(0, 217, 255, 0.22), transparent 28%),
+    radial-gradient(circle at 78% 18%, rgba(255, 79, 216, 0.18), transparent 30%),
+    linear-gradient(135deg, #fdf8ff 0%, #edf8ff 45%, #fff7fd 100%);
+  background-size: 36px 36px, 36px 36px, 100% 100%, 100% 100%, 100% 100%;
+  overflow-x: hidden;
+}
+
+body::before {
+  content: "";
+  position: fixed;
+  inset: 0;
+  pointer-events: none;
+  background-image:
+    radial-gradient(circle, rgba(255, 255, 255, 0.88) 0 1px, transparent 1.4px),
+    linear-gradient(115deg, transparent 0%, rgba(255, 255, 255, 0.38) 42%, transparent 58%);
+  background-size: 92px 92px, 240px 240px;
+  opacity: 0.42;
+  mix-blend-mode: screen;
 }
 
 button,
@@ -33,177 +60,456 @@ textarea {
   font: inherit;
 }
 
-.sidebar {
-  min-height: 100vh;
-  padding: 28px 16px;
-  display: flex;
-  flex-direction: column;
-  gap: 24px;
-  background: var(--sidebar);
-  color: #fff;
+button {
+  transition: box-shadow 160ms ease, transform 160ms ease, border-color 160ms ease;
+}
+
+button:hover,
+button:focus-visible {
+  box-shadow: var(--glow-pink);
+  transform: translateY(-1px);
+  outline: none;
+}
+
+.scanline {
+  position: fixed;
+  inset: 0;
+  pointer-events: none;
+  z-index: 50;
+  background: repeating-linear-gradient(
+    to bottom,
+    rgba(255, 255, 255, 0.08) 0,
+    rgba(255, 255, 255, 0.08) 1px,
+    transparent 1px,
+    transparent 5px
+  );
+  opacity: 0.35;
+}
+
+.top-nav {
+  position: sticky;
+  top: 0;
+  z-index: 20;
+  min-height: 72px;
+  padding: 12px 24px;
+  display: grid;
+  grid-template-columns: minmax(220px, auto) 1fr minmax(190px, auto);
+  align-items: center;
+  gap: 18px;
+  border-bottom: 1px solid rgba(255, 255, 255, 0.72);
+  background:
+    linear-gradient(180deg, rgba(255, 255, 255, 0.82), rgba(241, 248, 255, 0.62)),
+    var(--chrome);
+  backdrop-filter: blur(18px) saturate(1.4);
+  box-shadow: 0 10px 28px rgba(33, 53, 120, 0.13);
 }
 
 .brand {
   display: flex;
   gap: 12px;
   align-items: center;
-  padding: 0 8px;
+  min-width: 0;
 }
 
 .brand img {
-  width: 42px;
-  height: 42px;
-  border-radius: 50%;
+  width: 44px;
+  height: 44px;
+  border-radius: 15px;
   object-fit: cover;
+  border: 1px solid rgba(255, 255, 255, 0.86);
+  box-shadow: var(--glow-cyan);
 }
 
 .brand strong {
   display: block;
   font-size: 18px;
   line-height: 1.2;
+  font-weight: 900;
 }
 
 .brand span,
 .session-card span {
   display: block;
-  color: #c4ccd8;
-  font-size: 12px;
   margin-top: 3px;
+  color: var(--muted);
+  font-size: 11px;
+  font-weight: 800;
+  letter-spacing: 0.08em;
 }
 
 .nav {
-  display: grid;
+  display: flex;
+  justify-content: center;
   gap: 8px;
 }
 
 .nav-item {
-  border: 0;
-  border-radius: 8px;
-  padding: 12px 14px;
-  color: #e9eef5;
-  background: transparent;
-  text-align: left;
+  border: 1px solid rgba(255, 255, 255, 0.78);
+  border-radius: 999px;
+  padding: 9px 18px;
+  color: #121a42;
+  background: rgba(255, 255, 255, 0.48);
+  text-align: center;
   cursor: pointer;
+  font-weight: 900;
 }
 
 .nav-item.active,
 .nav-item:hover {
-  background: var(--sidebar-soft);
+  border-color: rgba(0, 217, 255, 0.72);
+  background: linear-gradient(135deg, rgba(255, 255, 255, 0.95), rgba(146, 247, 255, 0.5));
 }
 
 .session-card {
-  margin-top: auto;
-  padding: 12px;
-  border: 1px solid rgba(255, 255, 255, 0.12);
-  border-radius: 8px;
+  justify-self: end;
+  min-width: 190px;
+  padding: 9px 12px;
+  border: 1px solid rgba(255, 255, 255, 0.82);
+  border-radius: 18px;
   display: flex;
   align-items: center;
   gap: 10px;
+  background: rgba(255, 255, 255, 0.52);
+  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8), 0 12px 24px rgba(55, 88, 167, 0.12);
 }
 
 .status-dot {
-  width: 10px;
-  height: 10px;
+  width: 11px;
+  height: 11px;
   border-radius: 999px;
-  background: #f59e0b;
-  box-shadow: 0 0 0 6px rgba(245, 158, 11, 0.16);
+  background: var(--warning);
+  box-shadow: 0 0 0 5px rgba(255, 176, 46, 0.18), 0 0 18px rgba(255, 176, 46, 0.8);
   flex: 0 0 auto;
 }
 
 .status-dot.ok {
-  background: #12b981;
-  box-shadow: 0 0 0 6px rgba(18, 185, 129, 0.16);
+  background: var(--mint);
+  box-shadow: 0 0 0 5px rgba(34, 255, 200, 0.18), 0 0 18px rgba(34, 255, 200, 0.85);
 }
 
 .status-dot.error {
-  background: #ef4444;
-  box-shadow: 0 0 0 6px rgba(239, 68, 68, 0.16);
+  background: var(--danger);
+  box-shadow: 0 0 0 5px rgba(215, 0, 101, 0.16), 0 0 18px rgba(215, 0, 101, 0.72);
 }
 
 .shell {
-  min-width: 0;
-  padding: 28px 24px 24px;
+  position: relative;
+  z-index: 1;
+  width: min(1500px, calc(100% - 40px));
+  margin: 0 auto;
+  padding: 22px 0 28px;
 }
 
-.topbar {
-  display: flex;
-  justify-content: space-between;
-  align-items: flex-start;
-  gap: 16px;
-  margin-bottom: 18px;
+.hero {
+  min-height: 245px;
+  display: grid;
+  grid-template-columns: minmax(0, 1fr) 320px;
+  gap: 18px;
+  align-items: stretch;
+  margin-bottom: 16px;
 }
 
-.topbar h1 {
-  margin: 0;
-  font-size: 28px;
+.hero-copy,
+.hero-panel,
+.feature-card,
+.chat-card,
+.panel {
+  border: 1px solid rgba(255, 255, 255, 0.78);
+  background: var(--glass);
+  backdrop-filter: blur(18px) saturate(1.32);
+  box-shadow: var(--shadow), inset 0 1px 0 rgba(255, 255, 255, 0.75);
+}
+
+.hero-copy {
+  position: relative;
+  overflow: hidden;
+  border-radius: 32px;
+  padding: 32px;
+}
+
+.hero-copy::after {
+  content: "";
+  position: absolute;
+  inset: auto 26px 22px auto;
+  width: 180px;
+  height: 22px;
+  border-radius: 999px;
+  background: linear-gradient(90deg, var(--cyan), var(--pink));
+  filter: blur(18px);
+  opacity: 0.58;
+}
+
+.eyebrow {
+  display: inline-flex;
+  align-items: center;
+  gap: 8px;
+  padding: 7px 12px;
+  border-radius: 999px;
+  border: 1px solid rgba(0, 217, 255, 0.46);
+  background: rgba(255, 255, 255, 0.58);
+  color: #1143a3;
+  font-size: 12px;
+  font-weight: 900;
+  letter-spacing: 0.12em;
+}
+
+.hero h1 {
+  margin: 16px 0 8px;
+  max-width: 900px;
+  font-size: 44px;
+  line-height: 1.08;
+  font-weight: 950;
   letter-spacing: 0;
+  color: #07143b;
+  text-shadow: 0 1px 0 #fff, 0 0 24px rgba(0, 217, 255, 0.42);
 }
 
-.topbar p {
-  margin: 4px 0 0;
-  color: var(--muted);
+.hero p {
+  margin: 0;
+  max-width: 760px;
+  color: #26344e;
+  font-size: 16px;
+  line-height: 1.8;
 }
 
-.icon-button {
-  border: 1px solid var(--line);
-  background: #fff;
-  color: var(--text);
-  border-radius: 8px;
-  height: 36px;
-  padding: 0 14px;
+.hero-actions {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  gap: 12px;
+  margin-top: 24px;
+}
+
+.chrome-button,
+.system-pill {
+  border-radius: 999px;
+  font-weight: 900;
+}
+
+.chrome-button {
+  border: 1px solid rgba(255, 255, 255, 0.92);
+  min-height: 42px;
+  padding: 0 22px;
+  color: #07143b;
+  background: var(--chrome);
   cursor: pointer;
 }
 
+.system-pill {
+  display: inline-flex;
+  align-items: center;
+  min-height: 42px;
+  padding: 0 16px;
+  border: 1px solid rgba(125, 92, 255, 0.36);
+  background: rgba(255, 255, 255, 0.62);
+  color: #5320bd;
+  font-size: 12px;
+  letter-spacing: 0.1em;
+}
+
+.hero-panel {
+  position: relative;
+  overflow: hidden;
+  border-radius: 32px;
+  padding: 24px;
+  display: grid;
+  place-items: center;
+  background:
+    linear-gradient(135deg, rgba(255, 255, 255, 0.78), rgba(190, 247, 255, 0.38)),
+    var(--chrome);
+}
+
+.orbital-ring {
+  position: absolute;
+  width: 210px;
+  height: 210px;
+  border-radius: 50%;
+  border: 2px solid rgba(255, 255, 255, 0.72);
+  box-shadow: inset 0 0 22px rgba(255, 255, 255, 0.86), 0 0 28px rgba(0, 217, 255, 0.42);
+}
+
+.orbital-ring::after {
+  content: "";
+  position: absolute;
+  inset: 26px;
+  border-radius: 50%;
+  border: 1px dashed rgba(83, 32, 189, 0.42);
+}
+
+.hero-logo {
+  position: relative;
+  z-index: 1;
+  width: 116px;
+  height: 116px;
+  border-radius: 36px;
+  display: grid;
+  place-items: center;
+  background: rgba(255, 255, 255, 0.55);
+  border: 1px solid rgba(255, 255, 255, 0.86);
+  box-shadow: var(--glow-cyan), inset 0 1px 0 #fff;
+}
+
+.hero-logo img {
+  width: 86px;
+  height: 86px;
+  border-radius: 50%;
+  object-fit: cover;
+}
+
+.mini-display {
+  position: absolute;
+  left: 18px;
+  right: 18px;
+  bottom: 18px;
+  padding: 12px 14px;
+  border-radius: 18px;
+  background: rgba(8, 18, 48, 0.74);
+  color: #e8fbff;
+  box-shadow: inset 0 0 0 1px rgba(141, 247, 255, 0.32);
+}
+
+.mini-display span,
+.mini-display strong {
+  display: block;
+}
+
+.mini-display span {
+  font-size: 11px;
+  color: #8df7ff;
+  letter-spacing: 0.14em;
+  font-weight: 900;
+}
+
+.mini-display strong {
+  margin-top: 4px;
+  font-size: 20px;
+  color: #fff;
+}
+
+.feature-strip {
+  display: grid;
+  grid-template-columns: repeat(4, minmax(0, 1fr));
+  gap: 14px;
+  margin-bottom: 16px;
+}
+
+.feature-card {
+  border-radius: 24px;
+  padding: 16px;
+  min-height: 118px;
+}
+
+.feature-card span {
+  display: inline-flex;
+  width: 36px;
+  height: 24px;
+  border-radius: 999px;
+  align-items: center;
+  justify-content: center;
+  background: #081230;
+  color: var(--mint);
+  font-size: 12px;
+  font-weight: 900;
+  box-shadow: 0 0 18px rgba(34, 255, 200, 0.36);
+}
+
+.feature-card strong {
+  display: block;
+  margin-top: 12px;
+  font-size: 18px;
+}
+
+.feature-card p {
+  margin: 6px 0 0;
+  color: var(--muted);
+  font-size: 13px;
+  line-height: 1.55;
+}
+
 .workspace {
   display: grid;
-  grid-template-columns: minmax(580px, 1fr) 360px;
+  grid-template-columns: minmax(600px, 1fr) 380px;
   gap: 16px;
   align-items: stretch;
 }
 
 .chat-card,
 .panel {
-  background: #fff;
-  border: 1px solid var(--line);
-  border-radius: 8px;
-  box-shadow: var(--shadow);
+  border-radius: 24px;
 }
 
 .chat-card {
-  min-height: calc(100vh - 118px);
+  min-height: calc(100vh - 444px);
   display: grid;
-  grid-template-rows: minmax(0, 1fr) auto;
+  grid-template-rows: auto minmax(360px, 1fr) auto;
   overflow: hidden;
 }
 
+.window-bar {
+  height: 44px;
+  padding: 0 16px;
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  border-bottom: 1px solid rgba(255, 255, 255, 0.74);
+  background: linear-gradient(180deg, rgba(255, 255, 255, 0.92), rgba(187, 229, 255, 0.56));
+}
+
+.window-bar span {
+  width: 12px;
+  height: 12px;
+  border-radius: 50%;
+  background: var(--pink);
+  box-shadow: 0 0 12px rgba(255, 79, 216, 0.75);
+}
+
+.window-bar span:nth-child(2) {
+  background: var(--warning);
+  box-shadow: 0 0 12px rgba(255, 176, 46, 0.72);
+}
+
+.window-bar span:nth-child(3) {
+  background: var(--mint);
+  box-shadow: 0 0 12px rgba(34, 255, 200, 0.72);
+}
+
+.window-bar strong {
+  margin-left: 8px;
+  color: #26344e;
+  font-size: 12px;
+  letter-spacing: 0.14em;
+}
+
 .messages {
-  padding: 18px;
+  padding: 20px;
   overflow: auto;
   scroll-behavior: smooth;
 }
 
 .message-row {
   display: grid;
-  grid-template-columns: 34px minmax(0, 1fr);
-  gap: 10px;
-  margin-bottom: 16px;
+  grid-template-columns: 38px minmax(0, 1fr);
+  gap: 12px;
+  margin-bottom: 18px;
 }
 
 .message-row.user {
-  grid-template-columns: minmax(0, 1fr) 34px;
+  grid-template-columns: minmax(0, 1fr) 38px;
 }
 
 .avatar {
-  width: 34px;
-  height: 34px;
-  border-radius: 50%;
-  background: #d9f0e9;
-  color: var(--brand-strong);
+  width: 38px;
+  height: 38px;
+  border-radius: 15px;
+  background: linear-gradient(135deg, #ffffff, #aaf7ff 55%, #ffb5ef);
+  color: #07143b;
   display: grid;
   place-items: center;
   font-size: 14px;
-  font-weight: 700;
+  font-weight: 900;
   overflow: hidden;
+  border: 1px solid rgba(255, 255, 255, 0.82);
+  box-shadow: var(--glow-cyan);
 }
 
 .avatar img {
@@ -213,17 +519,21 @@ textarea {
 }
 
 .bubble {
-  width: min(100%, 760px);
-  border-radius: 8px;
-  padding: 14px 16px;
-  background: var(--assistant);
-  line-height: 1.7;
+  width: min(100%, 820px);
+  border-radius: 20px;
+  padding: 15px 17px;
+  background: rgba(255, 255, 255, 0.76);
+  border: 1px solid rgba(255, 255, 255, 0.72);
+  line-height: 1.75;
   white-space: pre-wrap;
+  color: #101936;
+  box-shadow: 0 12px 28px rgba(50, 72, 140, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.8);
 }
 
 .message-row.user .bubble {
   justify-self: end;
-  background: var(--user);
+  background: linear-gradient(135deg, rgba(217, 255, 247, 0.86), rgba(255, 222, 249, 0.8));
+  box-shadow: var(--glow-pink);
 }
 
 .message-row.user .avatar {
@@ -239,45 +549,51 @@ textarea {
 .typing {
   display: inline-flex;
   align-items: center;
-  gap: 5px;
+  gap: 6px;
   white-space: nowrap;
+  font-weight: 900;
+  color: #5320bd;
 }
 
 .typing i {
-  width: 6px;
-  height: 6px;
+  width: 7px;
+  height: 7px;
   border-radius: 50%;
-  background: #7aa89e;
+  background: var(--pink);
   animation: pulse 1s infinite ease-in-out;
+  box-shadow: 0 0 10px rgba(255, 79, 216, 0.8);
 }
 
 .typing i:nth-child(2) {
   animation-delay: 0.15s;
+  background: var(--cyan);
 }
 
 .typing i:nth-child(3) {
   animation-delay: 0.3s;
+  background: var(--mint);
 }
 
 @keyframes pulse {
   0%,
   80%,
   100% {
-    opacity: 0.3;
+    opacity: 0.35;
     transform: translateY(0);
   }
   40% {
     opacity: 1;
-    transform: translateY(-2px);
+    transform: translateY(-3px);
   }
 }
 
 .table-card {
   margin-top: 14px;
-  border: 1px solid var(--line);
-  border-radius: 8px;
-  background: #fff;
+  border: 1px solid rgba(141, 247, 255, 0.52);
+  border-radius: 18px;
+  background: rgba(255, 255, 255, 0.9);
   overflow: hidden;
+  box-shadow: 0 0 28px rgba(0, 217, 255, 0.16);
 }
 
 .table-head {
@@ -285,9 +601,9 @@ textarea {
   justify-content: space-between;
   align-items: center;
   gap: 10px;
-  padding: 10px 12px;
-  background: #f8fafb;
-  border-bottom: 1px solid var(--line);
+  padding: 11px 13px;
+  background: linear-gradient(135deg, rgba(255, 255, 255, 0.94), rgba(205, 247, 255, 0.62));
+  border-bottom: 1px solid rgba(75, 106, 171, 0.2);
 }
 
 .table-head strong {
@@ -295,30 +611,31 @@ textarea {
 }
 
 .table-head button {
-  border: 1px solid var(--line);
-  background: #fff;
-  color: var(--brand-strong);
-  border-radius: 8px;
-  padding: 6px 10px;
+  border: 1px solid rgba(255, 79, 216, 0.42);
+  background: rgba(255, 255, 255, 0.84);
+  color: #9b147c;
+  border-radius: 999px;
+  padding: 6px 11px;
   cursor: pointer;
   white-space: nowrap;
+  font-weight: 900;
 }
 
 .table-wrap {
   overflow: auto;
-  max-height: 420px;
+  max-height: 430px;
 }
 
 table {
   width: 100%;
-  min-width: 640px;
+  min-width: 700px;
   border-collapse: collapse;
   font-size: 13px;
 }
 
 th,
 td {
-  border-bottom: 1px solid var(--line);
+  border-bottom: 1px solid rgba(75, 106, 171, 0.16);
   padding: 10px 12px;
   text-align: left;
   vertical-align: top;
@@ -329,32 +646,42 @@ th {
   position: sticky;
   top: 0;
   z-index: 1;
-  background: #f8fafb;
-  color: #2e3a46;
-  font-weight: 700;
+  background: #f7fbff;
+  color: #141e43;
+  font-weight: 900;
+}
+
+tbody tr:nth-child(even) {
+  background: rgba(235, 250, 255, 0.58);
 }
 
 .composer {
-  margin: 0 18px 18px;
-  border: 1px solid var(--line);
-  border-radius: 8px;
-  padding: 10px;
-  background: #fff;
+  margin: 0 20px 20px;
+  border: 1px solid rgba(255, 255, 255, 0.78);
+  border-radius: 22px;
+  padding: 11px;
+  background: rgba(255, 255, 255, 0.72);
+  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.86);
 }
 
 .composer:focus-within {
-  border-color: var(--brand);
-  box-shadow: 0 0 0 3px rgba(25, 115, 94, 0.12);
+  border-color: rgba(0, 217, 255, 0.8);
+  box-shadow: var(--glow-cyan), inset 0 1px 0 rgba(255, 255, 255, 0.86);
 }
 
 .composer textarea {
   width: 100%;
-  min-height: 68px;
-  max-height: 180px;
+  min-height: 76px;
+  max-height: 190px;
   border: 0;
   outline: 0;
   resize: vertical;
-  color: var(--text);
+  color: var(--ink);
+  background: transparent;
+}
+
+.composer textarea::placeholder {
+  color: #69768b;
 }
 
 .composer-footer {
@@ -366,18 +693,20 @@ th {
 }
 
 .send-button {
-  border: 0;
-  border-radius: 8px;
-  min-width: 92px;
+  border: 1px solid rgba(255, 255, 255, 0.84);
+  border-radius: 16px;
+  min-width: 98px;
   height: 42px;
   padding: 0 16px;
-  background: var(--brand);
+  background: linear-gradient(135deg, #081230, #5a2bff 48%, #ff4fd8);
   color: #fff;
   display: inline-flex;
   align-items: center;
   justify-content: center;
   gap: 8px;
   cursor: pointer;
+  font-weight: 900;
+  box-shadow: 0 12px 26px rgba(125, 92, 255, 0.28);
 }
 
 .send-button:disabled {
@@ -394,13 +723,12 @@ th {
   display: grid;
   align-content: start;
   gap: 14px;
-  max-height: calc(100vh - 118px);
+  max-height: calc(100vh - 210px);
   overflow: auto;
 }
 
 .panel {
   padding: 16px;
-  box-shadow: none;
 }
 
 .panel-title {
@@ -414,6 +742,34 @@ th {
 .panel h2 {
   margin: 0;
   font-size: 18px;
+  font-weight: 950;
+}
+
+.player-screen {
+  margin-bottom: 14px;
+  border-radius: 18px;
+  padding: 14px;
+  background: linear-gradient(180deg, #07143b, #151246);
+  color: #e8fbff;
+  border: 1px solid rgba(141, 247, 255, 0.44);
+  box-shadow: inset 0 0 24px rgba(0, 217, 255, 0.15), 0 0 24px rgba(125, 92, 255, 0.18);
+}
+
+.player-screen span,
+.player-screen strong {
+  display: block;
+}
+
+.player-screen span {
+  color: var(--mint);
+  font-size: 11px;
+  letter-spacing: 0.14em;
+  font-weight: 900;
+}
+
+.player-screen strong {
+  margin-top: 4px;
+  font-size: 20px;
 }
 
 .kv {
@@ -430,26 +786,28 @@ th {
 
 .kv dt {
   color: var(--muted);
+  font-weight: 800;
 }
 
 .kv dd {
   margin: 0;
-  font-weight: 700;
+  font-weight: 950;
   word-break: break-all;
 }
 
 .tool-list,
 .trace-list {
   display: grid;
-  gap: 8px;
+  gap: 9px;
 }
 
 .tool-item,
 .trace-item {
-  border: 1px solid var(--line);
-  border-radius: 8px;
-  padding: 10px;
-  background: #fff;
+  border: 1px solid rgba(255, 255, 255, 0.76);
+  border-radius: 18px;
+  padding: 11px;
+  background: rgba(255, 255, 255, 0.7);
+  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.76);
 }
 
 .tool-item strong,
@@ -465,38 +823,84 @@ th {
   font-size: 12px;
   margin-top: 4px;
   word-break: break-word;
+  line-height: 1.5;
 }
 
 .phase-error strong {
   color: var(--danger);
 }
 
-@media (max-width: 1120px) {
-  body {
+@media (max-width: 1180px) {
+  .top-nav {
     grid-template-columns: 1fr;
   }
 
-  .sidebar {
-    min-height: auto;
-    flex-direction: row;
-    align-items: center;
+  .session-card {
+    justify-self: stretch;
   }
 
   .nav {
-    grid-auto-flow: column;
-    margin-left: auto;
-  }
-
-  .session-card {
-    margin-top: 0;
-    min-width: 190px;
+    justify-content: flex-start;
+    overflow-x: auto;
   }
 
+  .hero,
   .workspace {
     grid-template-columns: 1fr;
   }
 
+  .hero-panel {
+    min-height: 260px;
+  }
+
+  .feature-strip {
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+  }
+
   .inspector {
     max-height: none;
   }
 }
+
+@media (max-width: 720px) {
+  .top-nav {
+    padding: 12px;
+  }
+
+  .shell {
+    width: min(100% - 20px, 1500px);
+    padding-top: 12px;
+  }
+
+  .hero-copy {
+    padding: 22px;
+  }
+
+  .hero h1 {
+    font-size: 34px;
+  }
+
+  .feature-strip {
+    grid-template-columns: 1fr;
+  }
+
+  .chat-card {
+    min-height: 620px;
+  }
+
+  .message-row,
+  .message-row.user {
+    grid-template-columns: 1fr;
+  }
+
+  .message-row .avatar,
+  .message-row.user .avatar {
+    display: none;
+  }
+
+  .message-row.user .bubble,
+  .message-row.user .avatar {
+    grid-column: auto;
+    grid-row: auto;
+  }
+}

+ 1 - 0
smqjh-agent-runtime/requirements.txt

@@ -0,0 +1 @@
+langgraph>=0.2.76

+ 0 - 583
smqjh-agent-runtime/src/agentGraph.ts

@@ -1,583 +0,0 @@
-import { Annotation, END, START, StateGraph } from "@langchain/langgraph";
-import type { RuntimeConfig } from "./config.js";
-import { McpClient } from "./mcpClient.js";
-import type {
-  AgentResultTable,
-  AgentRunRequest,
-  AgentRunResponse,
-  AgentTraceEvent,
-  ChatMessage,
-  McpToolDescriptor,
-  PlannedToolCall,
-  ToolObservation
-} from "./types.js";
-
-const AgentState = Annotation.Root({
-  message: Annotation<string>({
-    reducer: (_current, next) => next ?? "",
-    default: () => ""
-  }),
-  history: Annotation<ChatMessage[]>({
-    reducer: (_current, next) => next ?? [],
-    default: () => []
-  }),
-  tools: Annotation<McpToolDescriptor[]>({
-    reducer: (_current, next) => next ?? [],
-    default: () => []
-  }),
-  plannedToolCalls: Annotation<PlannedToolCall[]>({
-    reducer: (_current, next) => next ?? [],
-    default: () => []
-  }),
-  observations: Annotation<ToolObservation[]>({
-    reducer: (current, next) => [...(current ?? []), ...(next ?? [])],
-    default: () => []
-  }),
-  visitedSignatures: Annotation<string[]>({
-    reducer: (_current, next) => next ?? [],
-    default: () => []
-  }),
-  trace: Annotation<AgentTraceEvent[]>({
-    reducer: (current, next) => [...(current ?? []), ...(next ?? [])],
-    default: () => []
-  }),
-  steps: Annotation<number>({
-    reducer: (_current, next) => next ?? 0,
-    default: () => 0
-  }),
-  final: Annotation<AgentRunResponse | undefined>({
-    reducer: (_current, next) => next,
-    default: () => undefined
-  })
-});
-
-type AgentStateType = typeof AgentState.State;
-
-export class SmqjhAgentGraph {
-  private readonly mcp: McpClient;
-  private readonly graph: {
-    invoke(input: Partial<AgentStateType>): Promise<AgentStateType>;
-  };
-
-  constructor(private readonly config: RuntimeConfig) {
-    this.mcp = new McpClient(config.mcp.url, config.mcp.token);
-    this.graph = this.buildGraph();
-  }
-
-  async health(): Promise<unknown> {
-    return this.mcp.status();
-  }
-
-  async tools(): Promise<McpToolDescriptor[]> {
-    return this.loadRunnableTools();
-  }
-
-  async run(input: AgentRunRequest): Promise<AgentRunResponse> {
-    const result = await this.graph.invoke({
-      message: input.message,
-      history: input.history ?? [],
-      visitedSignatures: [],
-      trace: [trace("system", "收到用户请求", input.message)]
-    });
-
-    const final = result.final as AgentRunResponse | undefined;
-    const observations = asArray<ToolObservation>(result.observations);
-    const traceEvents = asArray<AgentTraceEvent>(result.trace);
-    return (
-      final ?? {
-        content: "这次没有生成有效回答,请稍后重试。",
-        model: "none",
-        usedMcp: false,
-        steps: typeof result.steps === "number" ? result.steps : 0,
-        toolCalls: observations,
-        tables: extractTables(observations),
-        trace: traceEvents
-      }
-    );
-  }
-
-  private buildGraph(): { invoke(input: Partial<AgentStateType>): Promise<AgentStateType> } {
-    const workflow = new StateGraph(AgentState)
-      .addNode("loadTools", this.loadToolsNode)
-      .addNode("plan", this.planNode)
-      .addNode("executeTools", this.executeToolsNode)
-      .addNode("summarize", this.summarizeNode)
-      .addNode("chat", this.chatNode)
-      .addEdge(START, "loadTools")
-      .addEdge("loadTools", "plan")
-      .addConditionalEdges("plan", this.routeAfterPlan, {
-        executeTools: "executeTools",
-        summarize: "summarize",
-        chat: "chat"
-      })
-      .addEdge("executeTools", "plan")
-      .addEdge("summarize", END)
-      .addEdge("chat", END);
-
-    return workflow.compile() as unknown as { invoke(input: Partial<AgentStateType>): Promise<AgentStateType> };
-  }
-
-  private loadToolsNode = async (): Promise<Partial<AgentStateType>> => {
-    try {
-      const tools = await this.loadRunnableTools();
-      return {
-        tools,
-        trace: [trace("system", "MCP 工具加载完成", `可用工具 ${tools.length} 个`)]
-      };
-    } catch (error) {
-      return {
-        tools: [],
-        trace: [trace("error", "MCP 工具加载失败", errorMessage(error))]
-      };
-    }
-  };
-
-  private planNode = async (state: AgentStateType): Promise<Partial<AgentStateType>> => {
-    if (state.steps >= this.config.maxSteps) {
-      return {
-        plannedToolCalls: [],
-        trace: [trace("plan", "达到最大工具步数", `maxSteps=${this.config.maxSteps}`)]
-      };
-    }
-
-    try {
-      const result = await this.mcp.callTool("smqjh.ai.tool.plan", {
-        request: this.buildAssistantRequest(state),
-        tools: state.tools,
-        observations: state.observations
-      });
-      const record = asRecord(result.structuredContent);
-      const rawCalls = Array.isArray(record.toolCalls) ? record.toolCalls : [];
-      const visited = new Set(state.visitedSignatures);
-      const plannedToolCalls = rawCalls
-        .map((item) => normalizeToolCall(item, state.message))
-        .filter((item): item is PlannedToolCall => Boolean(item))
-        .filter((item) => !visited.has(toolSignature(item.name, item.arguments)))
-        .slice(0, 3);
-
-      return {
-        plannedToolCalls,
-        trace: [
-          trace(
-            "plan",
-            plannedToolCalls.length ? "DeepSeek 已规划工具调用" : "DeepSeek 判断无需继续调用工具",
-            reasonText(record),
-            { toolCalls: plannedToolCalls }
-          )
-        ]
-      };
-    } catch (error) {
-      return {
-        plannedToolCalls: [],
-        trace: [trace("error", "DeepSeek 工具规划失败", errorMessage(error))]
-      };
-    }
-  };
-
-  private executeToolsNode = async (state: AgentStateType): Promise<Partial<AgentStateType>> => {
-    const byName = new Map(state.tools.map((tool) => [tool.name, tool]));
-    const observations: ToolObservation[] = [];
-    const visited = new Set(state.visitedSignatures);
-
-    for (const call of state.plannedToolCalls) {
-      const resolvedName = resolveToolName(call.name, byName);
-      const signature = toolSignature(resolvedName || call.name, call.arguments);
-      visited.add(signature);
-
-      if (!resolvedName) {
-        observations.push({
-          name: call.name,
-          source: "mcp",
-          arguments: call.arguments,
-          ok: false,
-          error: "DeepSeek 选择了未注册 MCP 工具"
-        });
-        continue;
-      }
-
-      const startedAt = Date.now();
-      try {
-        const result = await this.mcp.callTool(resolvedName, call.arguments);
-        observations.push({
-          name: resolvedName,
-          source: "mcp",
-          arguments: call.arguments,
-          ok: true,
-          result: result.structuredContent ?? result,
-          durationMs: Date.now() - startedAt
-        });
-      } catch (error) {
-        observations.push({
-          name: resolvedName,
-          source: "mcp",
-          arguments: call.arguments,
-          ok: false,
-          error: errorMessage(error),
-          durationMs: Date.now() - startedAt
-        });
-      }
-    }
-
-    return {
-      observations,
-      plannedToolCalls: [],
-      visitedSignatures: Array.from(visited),
-      steps: state.steps + 1,
-      trace: observations.map((item) =>
-        trace(
-          item.ok ? "tool" : "error",
-          item.ok ? `工具执行完成:${item.name}` : `工具执行失败:${item.name}`,
-          item.ok ? `${item.durationMs ?? 0}ms` : item.error,
-          { arguments: item.arguments, result: summarizeResult(item.result) }
-        )
-      )
-    };
-  };
-
-  private summarizeNode = async (state: AgentStateType): Promise<Partial<AgentStateType>> => {
-    const tables = extractTables(state.observations);
-    try {
-      const result = await this.mcp.callTool("smqjh.ai.tool.summarize", {
-        request: this.buildAssistantRequest(state),
-        observations: state.observations
-      });
-      const record = asRecord(result.structuredContent);
-      const content = typeof record.content === "string" && record.content.trim()
-        ? record.content.trim()
-        : fallbackSummary(state.observations, tables);
-      const model = typeof record.model === "string" && record.model ? record.model : "mcp-deepseek";
-
-      return {
-        final: {
-          content,
-          model,
-          usedMcp: true,
-          steps: state.steps,
-          toolCalls: state.observations,
-          tables,
-          trace: state.trace
-        },
-        trace: [trace("summary", "结果总结完成", `${tables.length} 个表格`)]
-      };
-    } catch (error) {
-      const content = fallbackSummary(state.observations, tables);
-      return {
-        final: {
-          content,
-          model: "tool-only",
-          usedMcp: true,
-          steps: state.steps,
-          toolCalls: state.observations,
-          tables,
-          trace: state.trace
-        },
-        trace: [trace("error", "结果总结失败,已使用工具结果兜底", errorMessage(error))]
-      };
-    }
-  };
-
-  private chatNode = async (state: AgentStateType): Promise<Partial<AgentStateType>> => {
-    try {
-      const result = await this.mcp.callTool("smqjh.ai.chat", {
-        request: this.buildAssistantRequest(state)
-      });
-      const record = asRecord(result.structuredContent);
-      const content = typeof record.content === "string" && record.content.trim()
-        ? record.content.trim()
-        : "DeepSeek 没有返回有效内容。";
-      const model = typeof record.model === "string" && record.model ? record.model : "mcp-deepseek";
-
-      return {
-        final: {
-          content,
-          model,
-          usedMcp: true,
-          steps: state.steps,
-          toolCalls: [],
-          tables: [],
-          trace: state.trace
-        },
-        trace: [trace("chat", "普通对话完成", model)]
-      };
-    } catch (error) {
-      return {
-        final: {
-          content: `暂时无法完成回答:${errorMessage(error)}`,
-          model: "none",
-          usedMcp: false,
-          steps: state.steps,
-          toolCalls: [],
-          tables: [],
-          trace: state.trace
-        },
-        trace: [trace("error", "普通对话失败", errorMessage(error))]
-      };
-    }
-  };
-
-  private routeAfterPlan = (state: AgentStateType): "executeTools" | "summarize" | "chat" => {
-    if (state.plannedToolCalls.length > 0 && state.steps < this.config.maxSteps) {
-      return "executeTools";
-    }
-    if (state.observations.length > 0) {
-      return "summarize";
-    }
-    return "chat";
-  };
-
-  private buildAssistantRequest(state: AgentStateType): Record<string, unknown> {
-    return {
-      message: state.message,
-      history: state.history,
-      environmentName: this.config.environmentName,
-      baseUrl: this.config.baseUrl,
-      authenticated: true,
-      username: "web-agent"
-    };
-  }
-
-  private async loadRunnableTools(): Promise<McpToolDescriptor[]> {
-    const tools = await this.mcp.listTools();
-    return tools.filter((tool) => !tool.name.startsWith("smqjh.ai."));
-  }
-}
-
-function normalizeToolCall(value: unknown, message: string): PlannedToolCall | undefined {
-  const record = asRecord(value);
-  const name = typeof record.name === "string" ? record.name.trim() : "";
-  if (!name) {
-    return undefined;
-  }
-  return {
-    name,
-    arguments: repairToolArguments(name, asRecord(record.arguments), message)
-  };
-}
-
-function repairToolArguments(name: string, args: Record<string, unknown>, message: string): Record<string, unknown> {
-  if (name === "smqjh.database.smart.query" && typeof args.question !== "string") {
-    return { ...args, question: message };
-  }
-  if (name === "smqjh.product.lookup.summary") {
-    const current = typeof args.productKeyword === "string" ? args.productKeyword.trim() : "";
-    return { ...args, productKeyword: current || extractProductKeyword(message) };
-  }
-  return args;
-}
-
-function extractProductKeyword(message: string): string {
-  return message
-    .replace(/[,。!??;;::]/g, " ")
-    .replace(/帮我|麻烦|查询一下|查一下|查询|查看|当前|业务系统|系统里面|系统里|后台|我方|我们的|商品库|商品表/g, " ")
-    .replace(/商品描述是什么|商品描述|描述是什么|描述|价格是多少|价格|定价|是多少|是什么|呢/g, " ")
-    .replace(/\s+/g, " ")
-    .trim();
-}
-
-function resolveToolName(name: string, byName: Map<string, McpToolDescriptor>): string | undefined {
-  const candidates = [
-    name,
-    name.startsWith("smqjh.") ? name.slice("smqjh.".length) : `smqjh.${name}`,
-    name === "product.lookup.summary" ? "smqjh.product.lookup.summary" : "",
-    name === "order.count.query" ? "smqjh.order.count.query" : "",
-    name === "database.smart.query" ? "smqjh.database.smart.query" : "",
-    name === "database.readonly.query" ? "smqjh.database.readonly.query" : "",
-    name === "cloud.health" ? "smqjh.cloud.health" : ""
-  ].filter(Boolean);
-  return candidates.find((candidate) => byName.has(candidate));
-}
-
-function extractTables(observations: ToolObservation[]): AgentResultTable[] {
-  const tables: AgentResultTable[] = [];
-  for (const observation of observations) {
-    if (!observation.ok) {
-      continue;
-    }
-    tables.push(...extractTablesFromValue(toolTitle(observation.name), observation.result));
-  }
-  return tables.slice(0, 6);
-}
-
-function extractTablesFromValue(title: string, value: unknown): AgentResultTable[] {
-  const tables: AgentResultTable[] = [];
-  const seen = new WeakSet<object>();
-
-  function visit(node: unknown, currentTitle: string, depth: number): void {
-    if (!node || typeof node !== "object" || depth > 5) {
-      return;
-    }
-    if (seen.has(node)) {
-      return;
-    }
-    seen.add(node);
-
-    const record = node as Record<string, unknown>;
-    addRowsTable(currentTitle, record, "rows");
-    addRowsTable(currentTitle, record, "comparisonRows");
-
-    for (const key of ["data", "structuredContent", "result", "record"]) {
-      if (record[key]) {
-        visit(record[key], currentTitle, depth + 1);
-      }
-    }
-  }
-
-  function addRowsTable(currentTitle: string, record: Record<string, unknown>, rowsKey: "rows" | "comparisonRows"): void {
-    const rows = Array.isArray(record[rowsKey]) ? record[rowsKey] : undefined;
-    if (!rows || !rows.every(isRecord)) {
-      return;
-    }
-    const rowRecords = rows.slice(0, 200).map((row) => normalizeRow(row as Record<string, unknown>));
-    const providedColumns = Array.isArray(record.columns) ? record.columns.map((item) => String(item)) : [];
-    const columns = providedColumns.length
-      ? providedColumns
-      : Array.from(new Set(rowRecords.flatMap((row) => Object.keys(row))));
-    if (!columns.length) {
-      return;
-    }
-    tables.push({
-      title: buildTableTitle(currentTitle, record, rowsKey),
-      columns,
-      rows: rowRecords
-    });
-  }
-
-  visit(value, title, 0);
-  return tables;
-}
-
-function fallbackSummary(observations: ToolObservation[], tables: AgentResultTable[]): string {
-  const okCount = observations.filter((item) => item.ok).length;
-  const failed = observations.filter((item) => !item.ok);
-  const lines = [`已执行 ${observations.length} 个 MCP 工具,成功 ${okCount} 个。`];
-  if (tables.length) {
-    lines.push("结果已整理为下方表格。");
-  }
-  if (failed.length) {
-    lines.push(`失败工具:${failed.map((item) => `${item.name}:${item.error || "调用失败"}`).join(";")}`);
-  }
-  const evidence = findEvidence(observations);
-  if (evidence) {
-    lines.push(`依据:${evidence}`);
-  }
-  return lines.join("\n\n");
-}
-
-function findEvidence(observations: ToolObservation[]): string {
-  for (const observation of observations) {
-    const sql = findKeyValue(observation.result, "executedSql", 0);
-    if (typeof sql === "string" && sql.trim()) {
-      return sql.trim();
-    }
-    const evidence = findKeyValue(observation.result, "evidence", 0);
-    if (typeof evidence === "string" && evidence.trim()) {
-      return evidence.trim();
-    }
-  }
-  return "";
-}
-
-function findKeyValue(value: unknown, key: string, depth: number): unknown {
-  if (!value || typeof value !== "object" || depth > 4) {
-    return undefined;
-  }
-  const record = value as Record<string, unknown>;
-  if (record[key] !== undefined) {
-    return record[key];
-  }
-  for (const child of Object.values(record)) {
-    const result = findKeyValue(child, key, depth + 1);
-    if (result !== undefined) {
-      return result;
-    }
-  }
-  return undefined;
-}
-
-function trace(phase: AgentTraceEvent["phase"], title: string, detail?: string, data?: unknown): AgentTraceEvent {
-  return {
-    at: new Date().toISOString(),
-    phase,
-    title,
-    detail,
-    data
-  };
-}
-
-function reasonText(record: Record<string, unknown>): string {
-  return typeof record.reason === "string" && record.reason.trim() ? record.reason.trim() : "";
-}
-
-function summarizeResult(value: unknown): unknown {
-  const record = asRecord(value);
-  if (typeof record.rowCount === "number") {
-    return { rowCount: record.rowCount, title: record.title, summary: record.summary };
-  }
-  if (Array.isArray(record.rows)) {
-    return { rows: record.rows.length };
-  }
-  return value;
-}
-
-function buildTableTitle(baseTitle: string, record: Record<string, unknown>, rowsKey: string): string {
-  const title = typeof record.title === "string" && record.title.trim() ? record.title.trim() : baseTitle;
-  const count = record.rowCount === undefined ? "" : `(${record.rowCount} 行)`;
-  return rowsKey === "comparisonRows" ? `${title}:价格对比` : `${title}${count}`;
-}
-
-function toolTitle(name: string): string {
-  const titles: Record<string, string> = {
-    "smqjh.config.get": "运行配置",
-    "smqjh.cloud.health": "网关连通检查",
-    "smqjh.schema.search": "业务表搜索结果",
-    "smqjh.schema.getTable": "业务表说明",
-    "smqjh.schema.businessRules": "业务规则",
-    "smqjh.database.readonly.query": "数据库只读查询结果",
-    "smqjh.database.smart.query": "智能数据库查询结果",
-    "smqjh.order.count.query": "订单统计结果",
-    "smqjh.product.lookup.summary": "商品资料查询结果",
-    "smqjh.settlement.enterprise.list": "月结企业清单",
-    "smqjh.settlement.monthly.plan": "企业月结计划"
-  };
-  return titles[name] ?? name;
-}
-
-function toolSignature(name: string, args: Record<string, unknown>): string {
-  return `${name}:${stableStringify(args)}`;
-}
-
-function stableStringify(value: unknown): string {
-  if (!value || typeof value !== "object") {
-    return JSON.stringify(value);
-  }
-  if (Array.isArray(value)) {
-    return `[${value.map(stableStringify).join(",")}]`;
-  }
-  const record = value as Record<string, unknown>;
-  return `{${Object.keys(record)
-    .sort()
-    .map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`)
-    .join(",")}}`;
-}
-
-function asRecord(value: unknown): Record<string, unknown> {
-  return value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : {};
-}
-
-function asArray<T>(value: unknown): T[] {
-  return Array.isArray(value) ? (value as T[]) : [];
-}
-
-function isRecord(value: unknown): value is Record<string, unknown> {
-  return Boolean(value && typeof value === "object" && !Array.isArray(value));
-}
-
-function normalizeRow(row: Record<string, unknown>): Record<string, string> {
-  return Object.fromEntries(
-    Object.entries(row).map(([key, value]) => [key, value === undefined || value === null ? "" : String(value)])
-  );
-}
-
-function errorMessage(error: unknown): string {
-  return error instanceof Error ? error.message : String(error);
-}

+ 0 - 39
smqjh-agent-runtime/src/config.ts

@@ -1,39 +0,0 @@
-export interface RuntimeConfig {
-  host: string;
-  port: number;
-  environmentName: string;
-  baseUrl: string;
-  maxSteps: number;
-  mcp: {
-    url: string;
-    token: string;
-  };
-}
-
-export function loadRuntimeConfig(): RuntimeConfig {
-  return {
-    host: process.env.WEB_AGENT_HOST || "0.0.0.0",
-    port: numberEnv("WEB_AGENT_PORT", 5188),
-    environmentName: process.env.SMQJH_ENVIRONMENT_NAME || "test-gateway",
-    baseUrl: process.env.SMQJH_BASE_URL || "http://192.168.1.242:8080",
-    maxSteps: numberEnv("SMQJH_AGENT_MAX_STEPS", 6),
-    mcp: {
-      url: normalizeUrl(process.env.SMQJH_MCP_URL || process.env.MCP_URL || "http://127.0.0.1:8765/mcp"),
-      token: process.env.SMQJH_MCP_TOKEN || process.env.MCP_TOKEN || ""
-    }
-  };
-}
-
-function numberEnv(key: string, fallback: number): number {
-  const raw = process.env[key];
-  if (!raw) {
-    return fallback;
-  }
-  const value = Number(raw);
-  return Number.isFinite(value) && value > 0 ? value : fallback;
-}
-
-function normalizeUrl(value: string): string {
-  const url = value.trim();
-  return url.endsWith("/") ? url.slice(0, -1) : url;
-}

+ 0 - 89
smqjh-agent-runtime/src/mcpClient.ts

@@ -1,89 +0,0 @@
-import type { McpToolDescriptor, McpToolResult } from "./types.js";
-
-interface McpRpcResponse<T> {
-  jsonrpc: "2.0";
-  id: number | string | null;
-  result?: T;
-  error?: {
-    code: number;
-    message: string;
-  };
-}
-
-export class McpClient {
-  private requestId = 1;
-
-  constructor(
-    private readonly endpoint: string,
-    private readonly token = ""
-  ) {}
-
-  async status(): Promise<unknown> {
-    const response = await fetch(this.endpoint, {
-      method: "GET",
-      headers: this.headers()
-    });
-    return parseJsonResponse(response);
-  }
-
-  async listTools(): Promise<McpToolDescriptor[]> {
-    const response = await this.rpc<{ tools?: McpToolDescriptor[] }>("tools/list", {});
-    return Array.isArray(response.tools) ? response.tools : [];
-  }
-
-  async callTool(name: string, args: Record<string, unknown>): Promise<McpToolResult> {
-    return this.rpc<McpToolResult>("tools/call", {
-      name,
-      arguments: args
-    });
-  }
-
-  private async rpc<T>(method: string, params: Record<string, unknown>): Promise<T> {
-    const response = await fetch(this.endpoint, {
-      method: "POST",
-      headers: this.headers(),
-      body: JSON.stringify({
-        jsonrpc: "2.0",
-        id: this.requestId++,
-        method,
-        params
-      })
-    });
-
-    const payload = (await parseJsonResponse(response)) as McpRpcResponse<T>;
-    if (!response.ok) {
-      throw new Error(payload.error?.message || `MCP HTTP ${response.status}`);
-    }
-    if (payload.error) {
-      throw new Error(payload.error.message || "MCP call failed");
-    }
-    if (payload.result === undefined) {
-      throw new Error("MCP returned no result");
-    }
-    return payload.result;
-  }
-
-  private headers(): Record<string, string> {
-    const headers: Record<string, string> = {
-      "Content-Type": "application/json"
-    };
-    const token = this.token.trim();
-    if (token) {
-      headers.Authorization = `Bearer ${token}`;
-      headers["X-MCP-Token"] = token;
-    }
-    return headers;
-  }
-}
-
-async function parseJsonResponse(response: Response): Promise<unknown> {
-  const text = await response.text();
-  if (!text) {
-    return {};
-  }
-  try {
-    return JSON.parse(text) as unknown;
-  } catch {
-    throw new Error(`MCP returned non-JSON response: ${text.slice(0, 200)}`);
-  }
-}

+ 0 - 204
smqjh-agent-runtime/src/server.ts

@@ -1,204 +0,0 @@
-import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
-import { createReadStream, existsSync } from "node:fs";
-import { readFile, stat } from "node:fs/promises";
-import path from "node:path";
-import { fileURLToPath } from "node:url";
-import { loadRuntimeConfig } from "./config.js";
-import { SmqjhAgentGraph } from "./agentGraph.js";
-import type { AgentRunRequest } from "./types.js";
-
-const __filename = fileURLToPath(import.meta.url);
-const __dirname = path.dirname(__filename);
-const runtimeRoot = path.resolve(__dirname, "..");
-const publicRoot = path.join(runtimeRoot, "public");
-const repoRoot = path.resolve(runtimeRoot, "..");
-const logoPath = path.join(repoRoot, "smqjh-admin-agent", "build", "logo.png");
-
-const config = loadRuntimeConfig();
-const agent = new SmqjhAgentGraph(config);
-
-const server = createServer(async (req, res) => {
-  applyCors(res);
-  if (req.method === "OPTIONS") {
-    sendText(res, 204, "");
-    return;
-  }
-
-  try {
-    const url = new URL(req.url || "/", `http://${req.headers.host || "127.0.0.1"}`);
-    if (url.pathname === "/api/health" && req.method === "GET") {
-      await handleHealth(res);
-      return;
-    }
-    if (url.pathname === "/api/tools" && req.method === "GET") {
-      await handleTools(res);
-      return;
-    }
-    if (url.pathname === "/api/chat" && req.method === "POST") {
-      await handleChat(req, res);
-      return;
-    }
-    if (url.pathname === "/assets/logo.png" && req.method === "GET") {
-      await serveLogo(res);
-      return;
-    }
-    if (req.method === "GET") {
-      await serveStatic(url.pathname, res);
-      return;
-    }
-
-    sendJson(res, 405, { ok: false, message: "Method not allowed" });
-  } catch (error) {
-    sendJson(res, 500, {
-      ok: false,
-      message: error instanceof Error ? error.message : String(error)
-    });
-  }
-});
-
-server.listen(config.port, config.host, () => {
-  console.log(`SMQJH Web Agent started: http://127.0.0.1:${config.port}`);
-  console.log(`MCP endpoint: ${config.mcp.url}`);
-});
-
-async function handleHealth(res: ServerResponse): Promise<void> {
-  try {
-    const mcp = await agent.health();
-    sendJson(res, 200, {
-      ok: true,
-      runtime: {
-        name: "smqjh-agent-runtime",
-        mode: "web",
-        framework: "LangGraph",
-        environmentName: config.environmentName,
-        baseUrl: config.baseUrl
-      },
-      mcp
-    });
-  } catch (error) {
-    sendJson(res, 200, {
-      ok: false,
-      runtime: {
-        name: "smqjh-agent-runtime",
-        mode: "web",
-        framework: "LangGraph"
-      },
-      mcp: {
-        ok: false,
-        message: error instanceof Error ? error.message : String(error)
-      }
-    });
-  }
-}
-
-async function handleTools(res: ServerResponse): Promise<void> {
-  const tools = await agent.tools();
-  sendJson(res, 200, {
-    ok: true,
-    tools
-  });
-}
-
-async function handleChat(req: IncomingMessage, res: ServerResponse): Promise<void> {
-  const body = await readJsonBody<AgentRunRequest>(req);
-  const message = typeof body.message === "string" ? body.message.trim() : "";
-  if (!message) {
-    sendJson(res, 400, { ok: false, message: "message is required" });
-    return;
-  }
-  const history = Array.isArray(body.history) ? body.history : [];
-  const result = await agent.run({ message, history });
-  sendJson(res, 200, {
-    ok: true,
-    result
-  });
-}
-
-async function readJsonBody<T>(req: IncomingMessage): Promise<T> {
-  const chunks: Buffer[] = [];
-  let size = 0;
-  for await (const chunk of req) {
-    const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
-    size += buffer.length;
-    if (size > 1024 * 1024) {
-      throw new Error("Request body is too large");
-    }
-    chunks.push(buffer);
-  }
-  const raw = Buffer.concat(chunks).toString("utf8");
-  if (!raw) {
-    return {} as T;
-  }
-  return JSON.parse(raw) as T;
-}
-
-async function serveLogo(res: ServerResponse): Promise<void> {
-  if (!existsSync(logoPath)) {
-    sendText(res, 404, "logo not found");
-    return;
-  }
-  res.writeHead(200, {
-    "Content-Type": "image/png",
-    "Cache-Control": "public, max-age=3600"
-  });
-  createReadStream(logoPath).pipe(res);
-}
-
-async function serveStatic(urlPath: string, res: ServerResponse): Promise<void> {
-  const cleanPath = decodeURIComponent(urlPath === "/" ? "/index.html" : urlPath);
-  const filePath = path.resolve(publicRoot, `.${cleanPath}`);
-  const relative = path.relative(publicRoot, filePath);
-  if (relative.startsWith("..") || path.isAbsolute(relative)) {
-    sendText(res, 403, "Forbidden");
-    return;
-  }
-
-  let info;
-  try {
-    info = await stat(filePath);
-  } catch {
-    sendText(res, 404, "Not found");
-    return;
-  }
-  if (!info.isFile()) {
-    sendText(res, 404, "Not found");
-    return;
-  }
-
-  const data = await readFile(filePath);
-  res.writeHead(200, {
-    "Content-Type": contentType(filePath),
-    "Cache-Control": filePath.endsWith(".html") ? "no-cache" : "public, max-age=300"
-  });
-  res.end(data);
-}
-
-function applyCors(res: ServerResponse): void {
-  res.setHeader("Access-Control-Allow-Origin", "*");
-  res.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS");
-  res.setHeader("Access-Control-Allow-Headers", "Content-Type,Authorization,X-MCP-Token");
-}
-
-function sendJson(res: ServerResponse, status: number, payload: unknown): void {
-  res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
-  res.end(JSON.stringify(payload));
-}
-
-function sendText(res: ServerResponse, status: number, text: string): void {
-  res.writeHead(status, { "Content-Type": "text/plain; charset=utf-8" });
-  res.end(text);
-}
-
-function contentType(filePath: string): string {
-  const ext = path.extname(filePath).toLowerCase();
-  const types: Record<string, string> = {
-    ".html": "text/html; charset=utf-8",
-    ".css": "text/css; charset=utf-8",
-    ".js": "text/javascript; charset=utf-8",
-    ".json": "application/json; charset=utf-8",
-    ".png": "image/png",
-    ".ico": "image/x-icon",
-    ".svg": "image/svg+xml"
-  };
-  return types[ext] || "application/octet-stream";
-}

+ 0 - 62
smqjh-agent-runtime/src/types.ts

@@ -1,62 +0,0 @@
-export type ChatRole = "user" | "assistant";
-
-export interface ChatMessage {
-  role: ChatRole;
-  content: string;
-}
-
-export interface McpToolDescriptor {
-  name: string;
-  title?: string;
-  description?: string;
-  inputSchema?: unknown;
-}
-
-export interface McpToolResult {
-  content?: Array<{ type: string; text?: string }>;
-  structuredContent?: unknown;
-}
-
-export interface PlannedToolCall {
-  name: string;
-  arguments: Record<string, unknown>;
-}
-
-export interface ToolObservation {
-  name: string;
-  source: "mcp";
-  arguments: Record<string, unknown>;
-  ok: boolean;
-  result?: unknown;
-  error?: string;
-  durationMs?: number;
-}
-
-export interface AgentTraceEvent {
-  at: string;
-  phase: "system" | "plan" | "tool" | "summary" | "chat" | "error";
-  title: string;
-  detail?: string;
-  data?: unknown;
-}
-
-export interface AgentResultTable {
-  title: string;
-  columns: string[];
-  rows: Array<Record<string, string>>;
-}
-
-export interface AgentRunRequest {
-  message: string;
-  history?: ChatMessage[];
-}
-
-export interface AgentRunResponse {
-  content: string;
-  model: string;
-  usedMcp: boolean;
-  steps: number;
-  toolCalls: ToolObservation[];
-  tables: AgentResultTable[];
-  trace: AgentTraceEvent[];
-}

+ 0 - 15
smqjh-agent-runtime/tsconfig.json

@@ -1,15 +0,0 @@
-{
-  "compilerOptions": {
-    "target": "ES2022",
-    "module": "NodeNext",
-    "moduleResolution": "NodeNext",
-    "lib": ["ES2022", "DOM"],
-    "strict": true,
-    "esModuleInterop": true,
-    "forceConsistentCasingInFileNames": true,
-    "skipLibCheck": true,
-    "outDir": "dist",
-    "rootDir": "src"
-  },
-  "include": ["src/**/*.ts"]
-}