From c6ca9dc1262e241bbfd0a05719365a26d0f837d5 Mon Sep 17 00:00:00 2001 From: gongzhiyong Date: Fri, 10 Apr 2026 03:09:41 +0800 Subject: [PATCH] fix: restore workspace components accidentally dropped from git index workspace/ files existed on disk but were not included in previous incremental commit, causing git to record them as deleted. Re-adding all workspace card components, AgentWorkspace, ActivityTimeline, and WorkspaceCardRenderer to properly track them. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- doc/cot/README.md | 28 + doc/cot/architecture.md | 103 ++ doc/cot/backend.md | 286 ++++++ doc/cot/cot-visibility-plan.md | 480 +++++++++ doc/cot/frontend.md | 417 ++++++++ doc/gpthd.md | 918 ++++++++++++++++++ doc/plan/backend-plan.md | 324 +++++++ doc/plan/browser-full-test-plan.md | 329 +++++++ doc/plan/cot-trace-implementation-plan.md | 668 +++++++++++++ doc/start/EXTERNAL_SERVICES.md | 152 +++ doc/start/gpthd.md | 918 ++++++++++++++++++ doc/start/test.md | 61 ++ doc/start/uigoto-run (1).md | 395 ++++++++ doc/start/uigoto-run.md | 395 ++++++++ doc/start/uigoto.md | 715 ++++++++++++++ doc/uigoto/GPTui.md | 69 ++ doc/工单接口.md | 474 +++++++++ .../components/workspace/ActivityTimeline.tsx | 65 ++ .../components/workspace/AgentWorkspace.tsx | 100 ++ .../workspace/WorkspaceCardRenderer.tsx | 27 + .../workspace/cards/DocumentResultCard.tsx | 41 + .../components/workspace/cards/ErrorCard.tsx | 20 + .../workspace/cards/KnowledgeResultCard.tsx | 46 + .../workspace/cards/SandboxResultCard.tsx | 43 + .../workspace/cards/SearchResultCard.tsx | 54 ++ .../workspace/cards/TicketSummaryCard.tsx | 68 ++ 26 files changed, 7196 insertions(+) create mode 100644 doc/cot/README.md create mode 100644 doc/cot/architecture.md create mode 100644 doc/cot/backend.md create mode 100644 doc/cot/cot-visibility-plan.md create mode 100644 doc/cot/frontend.md create mode 100644 doc/gpthd.md create mode 100644 doc/plan/backend-plan.md create mode 100644 doc/plan/browser-full-test-plan.md create mode 100644 doc/plan/cot-trace-implementation-plan.md create mode 100644 doc/start/EXTERNAL_SERVICES.md create mode 100644 doc/start/gpthd.md create mode 100644 doc/start/test.md create mode 100644 doc/start/uigoto-run (1).md create mode 100644 doc/start/uigoto-run.md create mode 100644 doc/start/uigoto.md create mode 100644 doc/uigoto/GPTui.md create mode 100644 doc/工单接口.md create mode 100644 frontend/components/workspace/ActivityTimeline.tsx create mode 100644 frontend/components/workspace/AgentWorkspace.tsx create mode 100644 frontend/components/workspace/WorkspaceCardRenderer.tsx create mode 100644 frontend/components/workspace/cards/DocumentResultCard.tsx create mode 100644 frontend/components/workspace/cards/ErrorCard.tsx create mode 100644 frontend/components/workspace/cards/KnowledgeResultCard.tsx create mode 100644 frontend/components/workspace/cards/SandboxResultCard.tsx create mode 100644 frontend/components/workspace/cards/SearchResultCard.tsx create mode 100644 frontend/components/workspace/cards/TicketSummaryCard.tsx diff --git a/doc/cot/README.md b/doc/cot/README.md new file mode 100644 index 0000000..61be6a1 --- /dev/null +++ b/doc/cot/README.md @@ -0,0 +1,28 @@ +# CoT (Chain of Thought) 实现方案 + +> 目标:当用户选择 Auto 或 Pro 模式时,实时展示模型推理过程(正在做什么) + +## 文档结构 + +- [architecture.md](./architecture.md) — 整体架构与数据流 +- [backend.md](./backend.md) — 后端实现方案(Python/LangGraph) +- [frontend.md](./frontend.md) — 前端实现方案(Next.js/React) + +## 核心结论 + +**方案选择:Prompt 标签 + 流式状态机解析** + +| 方案 | 描述 | 结论 | +|------|------|------| +| A. 原生 reasoning tokens | Azure o1/o3 的 reasoning_content | 备用升级路径,gpt-5.4 支持情况待验证 | +| B. Prompt 标签解析 ✅ | 注入 `` 标签,流式解析 | **默认实现**,兼容所有 GPT 模型 | +| C. LangGraph 多节点 | 专门的 thinking 节点 | 双倍延迟/成本,不采用 | + +## Auto vs Pro 差异 + +| 维度 | Flash | Auto | Pro | +|------|-------|------|-----| +| CoT | 关闭 | 轻量(关键决策点) | 完整(每步详细推理) | +| max_tokens | 500 | 2048 | 4096 | +| temperature | 0.2 | 0.3 | 0.3 | +| 前端 thinking UI | 无 | 3个脉冲点,done后消失 | 可折叠 thinking block | diff --git a/doc/cot/architecture.md b/doc/cot/architecture.md new file mode 100644 index 0000000..494560f --- /dev/null +++ b/doc/cot/architecture.md @@ -0,0 +1,103 @@ +# CoT 整体架构与数据流 + +## 数据流 + +``` +用户选择 Auto / Pro + ↓ +后端注入 CoT system prompt +(要求模型输出 推理最终回答) + ↓ +LLM 流式输出: + "让我先分析...最终回答内容..." + ↓ +ThinkTagParser 状态机实时解析 + IN_THINK 状态 → thinking token + IN_ANSWER 状态 → answer token + ↓ +SSE 事件分流: + {"type": "thinking", "content": "让我先分析..."} + {"type": "token", "content": "最终回答..."} + {"type": "tool_start", "tool": "kb_search"} + {"type": "tool_end", "tool": "kb_search"} + {"type": "done"} + ↓ +前端 SSE 解析器接收事件 + ↓ +ThinkingBlock 组件(Auto: 脉冲动画 / Pro: 折叠块) +ToolCallIndicator 组件(工具调用进度) +消息内容正常渲染 +``` + +## SSE 事件协议 + +```jsonc +// CoT 推理过程(流式分块,Auto/Pro 模式) +{"type": "thinking", "content": "让我先分析这个问题..."} + +// 最终回答(流式分块,所有模式) +{"type": "token", "content": "根据分析,答案是..."} + +// 工具调用开始 +{"type": "tool_start", "tool": "kb_search"} + +// 工具调用结束 +{"type": "tool_end", "tool": "kb_search"} + +// 流结束 +{"type": "done"} + +// 错误 +{"type": "error", "content": "错误信息"} +``` + +## ThinkTagParser 状态机 + +``` + feed("") +INITIAL ─────────────────→ IN_THINK + │ + feed(text) │ emit("thinking", text) + │ + feed("") + ↓ + IN_ANSWER + │ + feed(text) │ emit("token", text) + ↓ + (done) +``` + +**边界处理**:流式 token 可能在标签中间截断(如 `""`),需要 buffer 积累直到标签完整。 + +## ReAct Agent 中的 CoT + +使用 `create_react_agent` 时,LangGraph 多轮调用 LLM: + +``` +轮1: LLM 决定调用工具 + → tool_start 事件告知前端"我在做什么" + → LLM 通常不输出 text content(tool_call 模式) + +工具执行中... + → tool_end 事件 + +轮N(最终): LLM 基于工具结果生成回答 + → 标签内是完整推理 + → thinking + token 事件流出 +``` + +结论:`tool_start`/`tool_end` 本身已经是"正在做什么"的可视化,最终轮的 `` 提供深度推理展示。 + +## 原生 Reasoning Tokens(升级路径) + +Azure OpenAI 从 API version `2024-12-17` 起,o1/o3/o4-mini 系列支持: +- 请求参数:`reasoning_effort: "low" | "medium" | "high"` +- 响应字段:`message.additional_kwargs.reasoning_content` + +如确认 `gpt-5.4` 部署支持,只需在 `_get_llm()` 中添加: +```python +if thinking and native_reasoning_supported: + kwargs["reasoning_effort"] = "medium" if model == "auto" else "high" +``` +SSE 层无需任何改动,因为 `reasoning_content` 和 `` 标签都走同一个 `thinking` 事件。 diff --git a/doc/cot/backend.md b/doc/cot/backend.md new file mode 100644 index 0000000..85e8e90 --- /dev/null +++ b/doc/cot/backend.md @@ -0,0 +1,286 @@ +# CoT 后端实现方案 + +## 改动文件清单 + +| 文件 | 改动性质 | +|------|---------| +| `app/schemas.py` | `model` 字段加入 `auto` 选项 | +| `app/graph/builder.py` | MODEL_PARAMS 加 `auto`;CoT prompt 注入逻辑 | +| `app/graph/nodes.py` | `call_model` 注入含 CoT 的 system prompt | +| `app/api/chat.py` | `ThinkTagParser` + `_stream_response` 扩展 | +| `app/graph/thinking.py` | **新建**:`ThinkTagParser` 状态机 | + +--- + +## 1. schemas.py + +```python +# 原来 +model: str = Field(default="flash", pattern="^(flash|pro)$") + +# 改为 +model: str = Field(default="flash", pattern="^(flash|auto|pro)$") +``` + +--- + +## 2. graph/thinking.py(新建) + +```python +"""Streaming tag parser for CoT extraction.""" +from __future__ import annotations + + +class ThinkTagParser: + """Parse streaming tokens and separate ... from answer. + + Yields (event_type, content) tuples where event_type is + "thinking" (inside block) or "token" (final answer). + + Handles token boundary issues: tags may arrive split across tokens. + """ + + _OPEN_TAG = "" + _CLOSE_TAG = "" + + def __init__(self) -> None: + self._buffer = "" + self._in_think = False + self._think_done = False + + def feed(self, token: str) -> list[tuple[str, str]]: + """Feed one streaming token. Returns list of (type, content) pairs.""" + self._buffer += token + events: list[tuple[str, str]] = [] + + while self._buffer: + if not self._in_think and not self._think_done: + # Waiting for + idx = self._buffer.find(self._OPEN_TAG) + if idx == -1: + # No opening tag found; check for partial tag at end + cut = self._safe_cut(self._buffer, "<") + if cut > 0: + events.append(("token", self._buffer[:cut])) + self._buffer = self._buffer[cut:] + elif cut == 0: + break # Entire buffer might be a partial tag + else: + events.append(("token", self._buffer)) + self._buffer = "" + else: + # Flush any content before as token + if idx > 0: + events.append(("token", self._buffer[:idx])) + self._buffer = self._buffer[idx + len(self._OPEN_TAG):] + self._in_think = True + + elif self._in_think: + # Inside , looking for + idx = self._buffer.find(self._CLOSE_TAG) + if idx == -1: + cut = self._safe_cut(self._buffer, "<") + if cut > 0: + events.append(("thinking", self._buffer[:cut])) + self._buffer = self._buffer[cut:] + elif cut == 0: + break + else: + events.append(("thinking", self._buffer)) + self._buffer = "" + else: + if idx > 0: + events.append(("thinking", self._buffer[:idx])) + self._buffer = self._buffer[idx + len(self._CLOSE_TAG):] + self._in_think = False + self._think_done = True + + else: + # After : everything is the final answer + events.append(("token", self._buffer)) + self._buffer = "" + + return events + + def flush(self) -> list[tuple[str, str]]: + """Flush remaining buffer at stream end.""" + if not self._buffer: + return [] + kind = "thinking" if self._in_think else "token" + result = [(kind, self._buffer)] + self._buffer = "" + return result + + @staticmethod + def _safe_cut(text: str, char: str) -> int: + """Return index of last occurrence of char, or -1 if not found. + + Returns 0 if char is at position 0 (entire string is potential tag). + """ + idx = text.rfind(char) + return idx # -1 if not found, 0 if at start +``` + +--- + +## 3. graph/builder.py + +```python +# Model parameter presets +MODEL_PARAMS: dict[str, dict] = { + "flash": {"max_tokens": 500, "temperature": 0.2, "thinking": False}, + "auto": {"max_tokens": 2048, "temperature": 0.3, "thinking": True}, + "pro": {"max_tokens": 4096, "temperature": 0.3, "thinking": True}, +} + +SYSTEM_PROMPT_BASE = ( + "You are SOC Assistant, an enterprise AI assistant. " + "You help users with knowledge base queries, ticket management, " + "and general questions. Always respond in the same language the user uses. " + "Be concise, accurate, and helpful." +) + +# Auto: concise thinking (key decisions only) +COT_PROMPT_AUTO = ( + "\n\nBefore answering, briefly think through the key decision points " + "inside tags, then give your final answer outside the tags.\n" + "Format:\n\n[key reasoning steps]\n\n\n[final answer]" +) + +# Pro: full step-by-step reasoning +COT_PROMPT_PRO = ( + "\n\nBefore answering, think through the problem step by step inside " + " tags. Analyze the question thoroughly, consider multiple " + "approaches, then provide your final answer outside the tags.\n" + "Format:\n\n[detailed step-by-step reasoning]\n\n\n[final answer]" +) + + +def _get_system_prompt(model: str) -> str: + if model == "auto": + return SYSTEM_PROMPT_BASE + COT_PROMPT_AUTO + if model == "pro": + return SYSTEM_PROMPT_BASE + COT_PROMPT_PRO + return SYSTEM_PROMPT_BASE + + +def _get_llm(model: str) -> AzureChatOpenAI: + params = MODEL_PARAMS.get(model, MODEL_PARAMS["flash"]) + return AzureChatOpenAI( + azure_endpoint=settings.azure_openai_endpoint, + api_key=settings.azure_openai_api_key, + api_version=settings.azure_openai_api_version, + azure_deployment=settings.azure_openai_deployment, + max_tokens=params["max_tokens"], + temperature=params["temperature"], + streaming=True, + ) +``` + +--- + +## 4. graph/nodes.py + +```python +from langchain_core.messages import SystemMessage +from app.graph.builder import _get_llm, _get_system_prompt + + +async def call_model(state: ChatState) -> dict: + model = state.get("model", "flash") + llm = _get_llm(model) + + messages = list(state["messages"]) + system_content = _get_system_prompt(model) + messages.insert(0, SystemMessage(content=system_content)) + + response = await llm.ainvoke(messages) + return {"messages": [response]} +``` + +--- + +## 5. api/chat.py(核心改动) + +在 `_stream_response` 中集成 `ThinkTagParser`: + +```python +from app.graph.thinking import ThinkTagParser + +async def _stream_response(request: ChatRequest) -> AsyncIterator[bytes]: + # ... 现有初始化代码 ... + + thinking_enabled = request.model in ("auto", "pro") + parser = ThinkTagParser() if thinking_enabled else None + + try: + async for event in graph.astream_events(input_data, config=config, version="v2"): + kind = event.get("event", "") + + if kind == "on_chat_model_stream": + chunk = event.get("data", {}).get("chunk") + if chunk and hasattr(chunk, "content") and chunk.content: + if isinstance(chunk.content, str): + raw_token = chunk.content + + if parser: + for evt_type, evt_content in parser.feed(raw_token): + if not evt_content: + continue + if evt_type == "thinking": + full_thinking.append(evt_content) + else: + full_content.append(evt_content) + sse = json.dumps( + {"type": evt_type, "content": evt_content}, + ensure_ascii=False, + ) + yield f"data: {sse}\n\n".encode("utf-8") + else: + # Flash mode: direct token passthrough + full_content.append(raw_token) + sse = json.dumps( + {"type": "token", "content": raw_token}, + ensure_ascii=False, + ) + yield f"data: {sse}\n\n".encode("utf-8") + + elif kind == "on_tool_start": + tool_name = event.get("name", "unknown") + sse = json.dumps({"type": "tool_start", "tool": tool_name}, ensure_ascii=False) + yield f"data: {sse}\n\n".encode("utf-8") + + elif kind == "on_tool_end": + tool_name = event.get("name", "unknown") + sse = json.dumps({"type": "tool_end", "tool": tool_name}, ensure_ascii=False) + yield f"data: {sse}\n\n".encode("utf-8") + + except Exception as exc: + # ... 现有错误处理 ... + pass + + finally: + # Flush parser buffer + if parser: + for evt_type, evt_content in parser.flush(): + if evt_content: + sse = json.dumps({"type": evt_type, "content": evt_content}, ensure_ascii=False) + yield f"data: {sse}\n\n".encode("utf-8") + + ai_content = "".join(full_content) + if ai_content: + await _persist_ai_message(request.conversation_id, ai_content) + + done_data = json.dumps({"type": "done"}) + yield f"data: {done_data}\n\n".encode("utf-8") +``` + +--- + +## 实施顺序 + +1. `app/schemas.py` — 加 `auto` +2. `app/graph/thinking.py` — 新建 `ThinkTagParser` +3. `app/graph/builder.py` — MODEL_PARAMS + prompt 函数 +4. `app/graph/nodes.py` — 注入 system prompt +5. `app/api/chat.py` — 集成 parser + 新 SSE 事件 diff --git a/doc/cot/cot-visibility-plan.md b/doc/cot/cot-visibility-plan.md new file mode 100644 index 0000000..2115061 --- /dev/null +++ b/doc/cot/cot-visibility-plan.md @@ -0,0 +1,480 @@ +# Auto / Pro 模式下的 CoT 可视化方案 + +## 目标 + +在不暴露模型原始 Chain-of-Thought(CoT)的前提下,让用户能够直观看到: + +- 当前系统正在做什么 +- 是否进入了工具调用 +- 调用了什么工具 +- 工具调用的大致输入/输出摘要 +- 当前步骤耗时与状态 +- 最终答案是如何逐步形成的 + +本方案的核心不是“展示原始 CoT”,而是展示一层**结构化执行轨迹(reasoning trace / agent activity trace)**。 + +--- + +## 为什么不建议直接展示原始 CoT + +### 风险 + +直接展示模型原始 CoT 会带来以下问题: + +1. **可能泄露系统提示词、工具策略、内部规则** +2. **推理内容冗长、不稳定、不适合用户阅读** +3. **不同模型对 CoT 的输出风格差异很大,难以统一前端体验** +4. **可能包含错误中间判断,影响用户信任** +5. **在 Auto / Pro 模式中,原始推理链可能过于技术化,用户看不懂** + +### 更合适的做法 + +把原始 CoT 转换成可控的、结构化的、面向用户的“执行过程摘要”,只暴露: + +- 当前阶段 +- 是否进入工具调用 +- 工具名称 +- 参数摘要 +- 返回摘要 +- 当前状态 +- 耗时 + +--- + +## 推荐产品形态 + +建议把“CoT 展示”做成三层结构。 + +### 第一层:状态条(简版) + +适合默认展示给所有用户。 + +示例: + +- 分析问题 +- 选择工具 +- 调用知识库 +- 调用工单系统 +- 整理答案 +- 已完成 + +这一层只表达“进度感”和“正在做什么”,不暴露细节。 + +### 第二层:事件时间线(中版) + +适合 Auto 模式下点击展开查看。 + +每条事件包含: + +- 时间点 +- 步骤名称 +- 工具名称(如有) +- 状态:进行中 / 成功 / 失败 / 重试 +- 耗时 + +示例: + +1. 分析用户问题 +2. 判断需要查询知识库 +3. 调用 `kb_search` +4. 知识库返回 3 条结果 +5. 调用 `ticket_list` +6. 返回最近 5 条工单 +7. 基于结果生成最终回复 + +### 第三层:可展开详情(详版) + +适合 Pro 模式。 + +每一步可展开查看: + +- 阶段说明 +- 工具输入摘要 +- 工具输出摘要 +- 错误信息(如有) +- 重试信息(如有) +- 耗时 +- 当前步骤说明 + +注意:这里依然不直接暴露原始 CoT 文本,只显示**受控摘要**。 + +--- + +## Auto / Pro 两种模式的建议差异 + +### Auto 模式 + +推荐默认展示“简版执行轨迹”: + +- 正在分析问题 +- 已调用知识库 +- 已调用工单系统 +- 正在整理答案 + +特点: + +- 信息量少 +- 不打扰主聊天体验 +- 用户可以看到系统不是“黑箱” +- 适合普通用户 + +### Pro 模式 + +推荐展示“详细执行轨迹”: + +- 当前阶段 +- 工具名 +- 入参摘要 +- 返回摘要 +- 耗时 +- 失败/重试信息 +- 最终归纳步骤 + +特点: + +- 更像开发者/高级用户视图 +- 更适合调试、排障、验收 +- 有助于建立系统透明度 + +--- + +## 后端事件流设计建议 + +如果当前后端已经有: + +- `on_tool_start` +- `on_tool_end` + +那已经具备基础条件。 + +建议在 SSE / Stream 事件中统一补齐以下事件类型。 + +### 1. reasoning 事件 + +用于表示阶段性思考摘要。 + +```json +{ + "type": "reasoning", + "stage": "分析问题", + "message": "正在判断是否需要外部工具" +} +``` + +### 2. tool_start 事件 + +```json +{ + "type": "tool_start", + "tool": "kb_search", + "title": "调用知识库", + "input_summary": "查询关键词:产品规划" +} +``` + +### 3. tool_end 事件 + +```json +{ + "type": "tool_end", + "tool": "kb_search", + "title": "知识库返回结果", + "output_summary": "命中 3 条知识库记录", + "duration_ms": 842, + "status": "success" +} +``` + +### 4. tool_error 事件 + +```json +{ + "type": "tool_error", + "tool": "ticket_list", + "title": "工单系统调用失败", + "error_summary": "请求超时", + "duration_ms": 3000, + "status": "error" +} +``` + +### 5. status 事件 + +```json +{ + "type": "status", + "stage": "整理答案", + "message": "正在结合上下文生成最终回复" +} +``` + +### 6. final 事件 + +```json +{ + "type": "final", + "message": "最终回复内容" +} +``` + +--- + +## 建议的数据结构 + +前端可以统一维护一个 trace item 数组,例如: + +```ts +interface TraceItem { + id: string; + type: "reasoning" | "tool_start" | "tool_end" | "tool_error" | "status"; + stage?: string; + tool?: string; + title: string; + message?: string; + inputSummary?: string; + outputSummary?: string; + errorSummary?: string; + status?: "running" | "success" | "error"; + durationMs?: number; + createdAt: number; +} +``` + +这样前端很好做时间线、折叠面板、状态图标和耗时展示。 + +--- + +## 前端展示建议 + +### 组件拆分建议 + +建议新增三个层次的组件: + +1. `TraceStatusBar` + - 展示当前阶段进度 + - 适合默认显示 + +2. `TraceTimeline` + - 展示完整事件流 + - 支持折叠/展开 + +3. `TraceTimelineItem` + - 每个步骤卡片 + - 可显示工具、耗时、状态、摘要 + +### 展示样式建议 + +每个步骤卡片包含: + +- 图标(思考 / 工具 / 成功 / 失败 / 生成中) +- 标题 +- 副标题 +- 时间 / 耗时 +- 可展开详情 + +比如: + +- `分析问题` +- `调用知识库` +- `知识库返回 3 条结果` +- `调用工单系统` +- `生成最终答案` + +颜色建议: + +- 蓝色:进行中 +- 绿色:成功 +- 红色:失败 +- 灰色:普通状态/历史步骤 + +--- + +## 工具调用摘要生成建议 + +重点:不要把完整参数和完整返回直接丢给前端。 + +应在后端做摘要清洗,例如: + +### 输入摘要 + +原始参数: + +```json +{ + "query": "搜索知识库中关于产品规划的内容", + "top_k": 5, + "filters": {"source": "internal"} +} +``` + +转换后: + +- 查询关键词:产品规划 +- 返回条数:5 +- 数据源:internal + +### 输出摘要 + +原始返回可能很长,不适合直接展示。 + +转换后: + +- 命中 3 条知识库记录 +- 返回最近 5 条工单 +- 找到 2 条相关外部搜索结果 + +--- + +## 安全边界 + +必须明确哪些信息可以展示,哪些不能展示。 + +### 可以展示 + +- 阶段名 +- 工具名 +- 参数摘要 +- 输出摘要 +- 耗时 +- 状态 +- 错误摘要 + +### 不建议直接展示 + +- 原始 system prompt +- 原始思维链文本 +- 完整工具参数(可能含敏感信息) +- 完整工具原始返回 +- 内部路由策略细节 +- 模型原始 scratchpad + +--- + +## 和现有 SOC 项目的对接建议 + +根据当前项目情况,最适合的落地方式是: + +### 后端 + +在现有流式聊天接口中,补充和规范以下事件: + +- reasoning / status +- tool_start +- tool_end +- tool_error +- final + +如果当前 `backend/app/api/chat.py` 已经处理: + +- `on_tool_start` +- `on_tool_end` + +那可以继续补一层“摘要映射”,把底层事件包装成前端可直接消费的 trace event。 + +### 前端 + +在聊天消息区域中,为 assistant message 增加一个“执行过程”区域: + +- 默认折叠 +- Auto 模式展示简版 +- Pro 模式展示详版 + +推荐位置: + +- 放在 assistant 回复消息上方或下方 +- 与最终答案同属一个回答块 +- 不要单独跳页面 + +--- + +## 推荐交互细节 + +### 方案 A:消息内嵌型(推荐) + +最终回复卡片中增加: + +- `查看执行过程` +- 展开后显示时间线 + +优点: + +- 用户不用切换页面 +- 和回答强绑定 +- 最符合聊天产品体验 + +### 方案 B:侧边抽屉型 + +在 Pro 模式下点击“过程详情”后,右侧打开一个 trace drawer。 + +优点: + +- 空间更大 +- 适合展示更多细节 + +缺点: + +- 实现更重 +- 对当前 SOC 页面结构改动更大 + +结论: + +- 先做消息内嵌型 +- 后续再扩展侧边抽屉型 + +--- + +## MVP 最小落地版本 + +如果要快速上线,建议只做以下能力: + +### 后端 MVP + +输出 4 类事件: + +- `status` +- `tool_start` +- `tool_end` +- `final` + +### 前端 MVP + +显示一个可折叠区域: + +- `分析问题` +- `调用工具:知识库` +- `工具返回:3 条结果` +- `生成答案` + +### Auto / Pro 区别 + +- Auto:默认折叠,只展示 1 行状态摘要 +- Pro:默认展开详细时间线 + +这样最省改动,也能马上解决“用户看不到模型在做什么”的问题。 + +--- + +## 最终建议 + +一句话总结: + +**不要展示原始 CoT,应该展示“结构化执行轨迹”。** + +对 SOC 项目最合理的方案是: + +1. 后端补齐 reasoning / tool / status 事件 +2. 前端把这些事件渲染成时间线 +3. Auto 展示简版,Pro 展示详版 +4. 只展示摘要,不暴露原始推理链 + +这样既能满足“用户想知道模型做了什么”,又不会引入原始 CoT 暴露风险。 + +--- + +## 可以继续的下一步 + +后续如果需要,可以继续细化为三份落地文档: + +1. 后端事件协议定义 +2. 前端组件与交互设计 +3. SOC 项目具体改造点(按文件路径拆解) diff --git a/doc/cot/frontend.md b/doc/cot/frontend.md new file mode 100644 index 0000000..aee6af1 --- /dev/null +++ b/doc/cot/frontend.md @@ -0,0 +1,417 @@ +# CoT 前端实现方案 + +## 改动文件清单 + +| 文件 | 改动性质 | +|------|---------| +| `lib/api.ts` | 修复 SSE `event:` 行解析;扩展 event type | +| `components/gemini/GeminiMessage.tsx` | `Message` 类型扩展;插入 ThinkingBlock + ToolCallIndicator | +| `components/gemini/GeminiChat.tsx` | 流回调处理新事件;传 `selectedModel` 给 GeminiMessage | +| `components/gemini/ThinkingBlock.tsx` | **新建**:CoT 折叠展示组件 | +| `components/gemini/ToolCallIndicator.tsx` | **新建**:工具调用状态指示器 | + +--- + +## 1. lib/api.ts — 类型扩展 + SSE 解析修复 + +### 类型扩展 + +```typescript +export interface ChatStreamEvent { + type: "token" | "tool_start" | "tool_end" | "done" | "error" | "thinking" | "answer"; + content?: string; + tool?: string; +} +``` + +### SSE 解析器修复 + +后端标准 SSE 格式: +``` +event: thinking +data: {"content": "让我先分析..."} +``` + +当前解析器只处理 `data:` 行,忽略 `event:` 行。需修复 buffer 解析逻辑: + +```typescript +// 在 streamChat 的 buffer 解析循环中 +let currentEventName: string | null = null; + +for (const line of lines) { + const trimmed = line.trim(); + + if (trimmed.startsWith("event: ")) { + currentEventName = trimmed.slice(7).trim(); + continue; + } + + if (trimmed.startsWith("data: ")) { + const json = trimmed.slice(6); + if (!json) { currentEventName = null; continue; } + try { + const parsed = JSON.parse(json); + // event: 行的类型覆盖 data 内的 type 字段 + const event: ChatStreamEvent = currentEventName + ? { ...parsed, type: currentEventName as ChatStreamEvent["type"] } + : parsed; + currentEventName = null; + onEvent(event); + if (event.type === "done") { onDone(); return; } + } catch { + currentEventName = null; + } + continue; + } + + if (trimmed === "") { + currentEventName = null; // SSE 事件边界 + } +} +``` + +**向后兼容**:如果后端仍发 `data: {"type": "token", ...}`(无 `event:` 行),走 `parsed.type` 分支,完全兼容。 + +--- + +## 2. GeminiMessage.tsx — 类型扩展 + +### Message 类型新增字段 + +```typescript +export interface Message { + id: string; + role: "user" | "assistant"; + content: string; + timestamp?: Date; + attachments?: AttachmentData[]; + // CoT 新增字段 + thinking?: string; // 推理过程全文(流式追加) + thinkingDone?: boolean; // thinking 流是否结束 + toolCalls?: ToolCallRecord[]; // 工具调用历史 +} + +export interface ToolCallRecord { + tool: string; + startedAt: number; // Date.now() + endedAt?: number; +} +``` + +### 渲染区插入新组件 + +在 assistant 消息内容区域顶部插入(现有内容渲染不变): + +```tsx +interface GeminiMessageProps { + message: Message; + onRegenerate?: (id: string) => void; + selectedModel?: "flash" | "auto" | "pro"; // 新增可选参数 +} + +// 在 assistant 消息的 flex-1 div 内,内容渲染前插入: +{message.thinking !== undefined && selectedModel !== "flash" && ( + +)} +{message.toolCalls && message.toolCalls.length > 0 && ( + +)} +``` + +--- + +## 3. GeminiChat.tsx — 流事件处理 + +在 `streamChat` 的 `onEvent` 回调中新增: + +```typescript +// thinking 事件:追加推理文本 +if (event.type === "thinking" && event.content) { + setConversations((prev) => + prev.map((c) => { + if (c.id !== streamConvId) return c; + return { + ...c, + messages: c.messages.map((m) => + m.id === aiMsgId + ? { ...m, thinking: (m.thinking ?? "") + event.content! } + : m + ), + }; + }) + ); +} + +// answer/token 事件:标记 thinkingDone,追加回答文本 +if ((event.type === "token" || event.type === "answer") && event.content) { + setConversations((prev) => + prev.map((c) => { + if (c.id !== streamConvId) return c; + return { + ...c, + messages: c.messages.map((m) => + m.id === aiMsgId + ? { + ...m, + thinkingDone: true, + content: m.content + event.content, + } + : m + ), + }; + }) + ); +} + +// tool_start:记录工具调用开始 +if (event.type === "tool_start" && event.tool) { + setConversations((prev) => + prev.map((c) => { + if (c.id !== streamConvId) return c; + return { + ...c, + messages: c.messages.map((m) => + m.id === aiMsgId + ? { + ...m, + toolCalls: [ + ...(m.toolCalls ?? []), + { tool: event.tool!, startedAt: Date.now() }, + ], + } + : m + ), + }; + }) + ); +} + +// tool_end:记录工具调用结束时间 +if (event.type === "tool_end" && event.tool) { + setConversations((prev) => + prev.map((c) => { + if (c.id !== streamConvId) return c; + return { + ...c, + messages: c.messages.map((m) => { + if (m.id !== aiMsgId) return m; + const calls = [...(m.toolCalls ?? [])]; + const idx = calls + .map((tc, i) => ({ tc, i })) + .reverse() + .find(({ tc }) => tc.tool === event.tool && !tc.endedAt)?.i ?? -1; + if (idx !== -1) calls[idx] = { ...calls[idx], endedAt: Date.now() }; + return { ...m, toolCalls: calls }; + }), + }; + }) + ); +} +``` + +在 `onDone` 回调中确保 `thinkingDone = true`: + +```typescript +// done 时 +setConversations((prev) => + prev.map((c) => ({ + ...c, + messages: c.messages.map((m) => + m.id === aiMsgId ? { ...m, thinkingDone: true } : m + ), + })) +); +``` + +将 `selectedModel` 传给 `GeminiMessage`: + +```tsx + +``` + +--- + +## 4. ThinkingBlock.tsx(新建) + +```tsx +"use client"; + +import { useState } from "react"; +import { ChevronDown, ChevronRight, Brain } from "lucide-react"; +import { cn } from "@/lib/utils"; + +interface ThinkingBlockProps { + content: string; + isDone: boolean; + model: "auto" | "pro"; +} + +export function ThinkingBlock({ content, isDone, model }: ThinkingBlockProps) { + const [expanded, setExpanded] = useState(false); + + // Auto 模式:只显示脉冲动画,thinking 结束后消失 + if (model === "auto") { + if (isDone) return null; + return ( +
+ + + + 正在思考... +
+ ); + } + + // Pro 模式:完整 thinking block,可折叠 + return ( +
+ + + {expanded && ( +
+

+ {content} + {!isDone && ( + + )} +

+
+ )} +
+ ); +} +``` + +--- + +## 5. ToolCallIndicator.tsx(新建) + +```tsx +"use client"; + +import { Database, Search, FileText, Code2, Box, Loader2 } from "lucide-react"; +import { cn } from "@/lib/utils"; +import type { ToolCallRecord } from "./GeminiMessage"; + +const TOOL_META: Record = { + kb_search: { label: "查询知识库", Icon: Database }, + web_search: { label: "搜索网络", Icon: Search }, + generate_document: { label: "生成文档", Icon: FileText }, + sandbox_run: { label: "执行代码", Icon: Code2 }, + ticket_list: { label: "查询工单列表", Icon: Box }, + ticket_detail: { label: "获取工单详情", Icon: Box }, +}; + +interface ToolCallIndicatorProps { + toolCalls: ToolCallRecord[]; +} + +export function ToolCallIndicator({ toolCalls }: ToolCallIndicatorProps) { + if (toolCalls.length === 0) return null; + + return ( +
+ {toolCalls.map((call, idx) => { + const meta = TOOL_META[call.tool] ?? { label: call.tool, Icon: Box }; + const { label, Icon } = meta; + const isDone = call.endedAt !== undefined; + const duration = isDone + ? ((call.endedAt! - call.startedAt) / 1000).toFixed(1) + : null; + + return ( +
+ {isDone ? ( + + ) : ( + + )} + + {isDone ? `已${label}` : `正在${label}...`} + + {isDone && duration && ( + {duration}s + )} +
+ ); + })} +
+ ); +} +``` + +--- + +## 视觉效果示意 + +``` +Auto 模式(thinking 进行中): +● ● ● 正在思考... +[第一个 token 到达后自动消失] + +────────────────────────────── +Pro 模式(thinking 进行中): +🧠 正在思考... ▶ 128字 + +Pro 模式(展开后): +🧠 已完成思考 ▼ +│ 让我先分析这个问题的关键点... +│ 考虑到用户提到了X,应该从Y角度 +│ 分析。工单系统的...▌ +────────────────────────────── + +工具调用进行中: +⟳ 正在查询知识库... [蓝色背景] + +工具调用完成: +✓ 已查询知识库 1.2s [灰色背景] + +[最终回答正常流式打字...] +``` + +--- + +## 实施顺序 + +1. `lib/api.ts` — 类型扩展 + SSE 解析器修复 +2. `components/gemini/ThinkingBlock.tsx` — 新建 +3. `components/gemini/ToolCallIndicator.tsx` — 新建 +4. `components/gemini/GeminiMessage.tsx` — 类型扩展 + 渲染插入 +5. `components/gemini/GeminiChat.tsx` — 流事件处理 + 传参 diff --git a/doc/gpthd.md b/doc/gpthd.md new file mode 100644 index 0000000..c6771b5 --- /dev/null +++ b/doc/gpthd.md @@ -0,0 +1,918 @@ +# SOC 项目后端功能方案(基于 LangChain,按功能拆解) + +> 约束:在未获得明确允许前,不修改前端交互,只补后端能力、编排链路和数据层。 +> 目标:严格围绕当前 `~/go/soc` 这个 Gemini 风格前端,按“每个功能用什么技术完成什么功能”来写,核心框架改为 **LangChain / LangGraph**,不再以 FastAPI 作为方案重点。 + +--- + +## 1. 对话流式回复 + +### 要完成什么功能 +- 用户在当前聊天输入框发送消息 +- 后端实时返回回答内容 +- 支持“思考中 / 检索中 / 生成中”的状态 +- 不改变现有前端交互,只替换当前前端 `simulateAIResponse()` + +### 用什么技术 +- **LangChain**:负责组织提示词、消息上下文、模型调用 +- **LangGraph**:负责整个对话节点编排与状态流转 +- **Azure OpenAI**:生成最终回答 +- **SSE**:把 LangChain/LangGraph 执行过程和回答流式推给前端 +- **PostgreSQL**:保存会话和消息记录 + +### 怎么落地 +- 以 `LangGraph StateGraph` 建立一个对话图: + - `receive_message` + - `load_history` + - `route_tools` + - `call_llm` + - `persist_message` +- 前端发送消息后,后端触发 graph 执行 +- Azure OpenAI 通过 LangChain chat model 调用 +- 生成的 token 和中间状态通过 SSE 返回给前端 + +### 输出结果 +- 前端仍然保持当前 Gemini 风格聊天交互 +- 从 mock 回复升级为真实流式 AI 回复 +- 后续所有工具调用都能接到同一个 graph 里 + +--- + +## 2. 会话管理 + +### 要完成什么功能 +- 左侧历史会话列表从真实数据读取 +- 支持新建、切换、删除会话 +- 支持自动生成标题 +- 会话上下文可在 LangChain 中继续使用 + +### 用什么技术 +- **PostgreSQL**:保存 conversation 和 message +- **LangChain Memory / Message History 抽象**:管理历史消息上下文 +- **SQLAlchemy / SQLModel**:管理数据表 + +### 怎么落地 +- conversations/messages 数据存入 PostgreSQL +- 在 LangChain 层使用 `BaseChatMessageHistory` 风格封装数据库消息 +- 每次进入 graph 时先加载历史消息 +- 标题生成可以由 LLM 在首轮消息后自动归纳 + +### 输出结果 +- 当前左侧 mock 会话可以替换为真实会话记录 +- 后端真正具备多轮上下文记忆能力 + +--- + +## 3. 内部知识库检索 + +### 要完成什么功能 +- 当用户问产品、方案、配置、内部文档时 +- 自动检索内部知识库 +- 再让模型基于检索结果生成回答 +- 回答中附带引用来源 + +### 用什么技术 +- **LangChain Tool**:把 KB_AGENT 封装成知识库工具 +- **KB_AGENT 接口**:作为实际搜索源 +- **Azure OpenAI**:总结检索结果并生成回答 +- **LangGraph**:决定何时调用知识库节点 + +### 怎么落地 +- 编写 `kb_search_tool` +- 工具内部调用 `KB_AGENT_URL + KB_AGENT_SEARCH_PATH` +- 返回统一的文档列表结构 +- graph 中当识别为内部知识类问题时,先进入 `kb_search` 节点,再进入 `llm_answer` + +### 输出结果 +- 产品知识问答不再靠模型空想 +- 回答可基于真实内部资料 +- 更适合售前、售后、研发支持 + +--- + +## 4. 外部 AI 搜索 + +### 要完成什么功能 +- 处理实时互联网问题、行业动态、外部资料调研 +- 支持搜索、网页读取、重排 +- 支持 fast / deep / auto 三种搜索质量 +- 后续支持图片和视频检索结果 + +### 用什么技术 +- **LangChain Tool**:封装外部搜索工具链 +- **Jina Search API**:外部搜索 +- **Jina Reader**:网页正文读取 +- **Rerank 模型**:重排结果 +- **LangGraph**:编排 Search -> Read -> Rerank -> Answer +- **Azure OpenAI**:生成最终总结回答 + +### 怎么落地 +- 分成三个 tool: + - `web_search_tool` + - `web_read_tool` + - `rerank_tool` +- 在 graph 中建立外部搜索链路: + - 搜索候选 + - 读取正文 + - 重排结果 + - 将高质量上下文交给 LLM +- `fast/deep/auto` 可作为 graph state 中的参数 + +### 输出结果 +- 外部信息回答准确度显著提升 +- 满足文档要求里的企业级外部搜索能力 +- 为后续图片、视频结果展示预留结构 + +--- + +## 5. 工单系统只读接入 + +### 要完成什么功能 +- 查询工单列表 +- 查询工单详情 +- 汇总 P0/P1 工单 +- 支持聊天中分析工单趋势、共性问题、故障重点 +- 替换当前前端 mock 工单摘要 + +### 用什么技术 +- **LangChain Tool**:把 gongdan API 封装为工单工具 +- **Gongdan HTTP API**:工单实际数据源 +- **Azure OpenAI**:对工单结果做总结和归纳 +- **PostgreSQL(可选缓存)**:保存查询结果和摘要缓存 + +### 怎么落地 +- 编写工具: + - `ticket_list_tool` + - `ticket_detail_tool` + - `ticket_summary_tool` +- 当用户问题涉及工单时,graph 路由到 ticket 节点 +- 工具取回结果后,再由 LLM 进行总结 +- 当前前端的 TicketSummary 数据以后改成读取真实接口结果,但不改交互样式 + +### 输出结果 +- 工单分析能力可直接在聊天里使用 +- 首页/聊天区的工单摘要可从 mock 变成真实数据 + +--- + +## 6. 文档生成 + +### 要完成什么功能 +- 用户要求生成方案、汇报、纪要、总结文档时 +- 后端自动进入文档生成流程 +- 返回任务状态和结果 +- 生成正式文档链接或结果卡片 + +### 用什么技术 +- **LangChain Tool / Runnable**:封装文档生成能力 +- **Doc Creator Agent HTTP API**:实际生成正式文档 +- **LangGraph**:把“文档生成”作为 graph 的分支节点 +- **PostgreSQL**:保存文档任务记录 +- **Azure OpenAI**:先整理文档结构或提纲 + +### 怎么落地 +- graph 中识别“生成文档”类意图 +- 先用 LLM 生成结构化文档提纲 +- 再调用 doc creator agent +- 把任务状态写入数据库 +- 前端依旧保持聊天式入口,只在消息中显示结果卡片 + +### 输出结果 +- 销售、售前、汇报场景可以直接从聊天进入正式文档输出 +- 文档生成成为对话系统中的标准能力节点 + +--- + +## 7. 沙盒代码执行 + +### 要完成什么功能 +- 分析 CSV、JSON、日志、结构化数据 +- 在安全环境中执行代码 +- 返回分析结果、图表和文件 +- 不改变前端交互,只把结果塞回当前聊天流里 + +### 用什么技术 +- **LangChain Tool**:把沙盒能力封装为可调用工具 +- **Daytona Sandbox**:安全执行环境 +- **Python 数据工具链**:pandas、matplotlib、json、csv +- **LangGraph**:按意图路由到 sandbox 节点 +- **PostgreSQL**:保存执行记录 + +### 怎么落地 +- 第一阶段不开放任意代码执行 +- 只先封装几个固定工具: + - `csv_summary_tool` + - `json_transform_tool` + - `data_analysis_tool` + - `chart_generate_tool` +- graph 根据问题和附件类型决定是否调用 sandbox + +### 输出结果 +- 后端不仅能“回答”,还能“执行”和“计算” +- 数据类问题能返回真正算出来的结果 + +--- + +## 8. 附件上传与解析 + +### 要完成什么功能 +- 接收用户上传的附件 +- 保存附件元数据 +- 提取文本内容进入上下文 +- 为知识问答、文档生成、沙盒分析提供输入 + +### 用什么技术 +- **对象存储/本地存储**:保存附件 +- **LangChain Document Loader**:解析 PDF、DOCX、TXT、CSV 等文件 +- **PostgreSQL**:保存附件元数据 +- **LangGraph**:把附件解析结果接入 graph state + +### 怎么落地 +- 上传后先保存附件和元数据 +- 再用 LangChain loader 抽取文本 +- 将解析结果挂到当前会话 state 中 +- 当用户继续提问时,graph 可以把附件内容作为上下文输入 LLM 或工具 + +### 输出结果 +- 未来可以真正支持“基于附件提问”和“基于附件分析” +- 为文档生成和沙盒执行提供输入材料 + +--- + +## 9. 工具编排层 + +### 要完成什么功能 +- 判断用户当前问题到底需要哪种能力 +- 决定先查 KB、先查工单、还是先查外部搜索 +- 决定是否触发文档生成或沙盒分析 +- 把多个工具结果统一整理给模型 + +### 用什么技术 +- **LangGraph**:整个系统的核心编排框架 +- **LangChain Tools**:封装 KB、Search、Tickets、Docs、Sandbox +- **Azure OpenAI**:辅助做意图判断、结果总结 + +### 怎么落地 +- graph 中至少有这些节点: + - `router` + - `kb_search` + - `web_search` + - `ticket_query` + - `doc_generate` + - `sandbox_run` + - `llm_answer` + - `persist` +- 第一阶段可以先“规则路由 + LLM总结” +- 第二阶段再升级为“LLM路由 + 工具调用决策” + +### 输出结果 +- 后端不再是散乱接口集合,而是统一 Agent 编排系统 +- 前端只保留一个 Gemini 风格聊天入口即可 + +--- + +## 10. 数据持久化与基础设施 + +### 要完成什么功能 +- 保存历史会话 +- 保存消息记录 +- 保存工具调用记录 +- 保存附件记录 +- 保存文档任务记录 +- 保存 graph 执行状态和日志 +- 提升缓存能力、异步任务能力和文件持久化能力 + +### 用什么技术 +- **PostgreSQL**:主数据库,保存会话、消息、任务、工具记录 +- **LangGraph Checkpointer / State Persistence**:保存 graph 执行状态 +- **Redis**:缓存热点结果、会话临时状态、短期上下文、速率控制 +- **Azure Storage Account**:保存附件、图表、导出文件、文档产物 +- **Azure Service Bus**:承载异步任务与解耦长链路处理 + +### 怎么落地 +- PostgreSQL 中至少建立以下表: + - `conversations` + - `messages` + - `tool_runs` + - `attachments` + - `document_tasks` + - `graph_runs` +- Redis 用于: + - 外部搜索结果缓存 + - KB 搜索缓存 + - 工单摘要缓存 + - 正在运行的 graph/session 临时状态 + - SSE 会话短状态同步 +- Azure Storage Account 用于: + - 用户上传附件原始文件 + - Sandbox 输出文件 + - 图表与中间产物 + - 文档生成结果文件 +- Azure Service Bus 用于: + - 文档生成异步任务派发 + - Sandbox 长任务调度 + - 外部搜索深度模式异步并发编排 + - 后续告警/通知类事件扩展 + +### 输出结果 +- 对话、工具、任务都有追踪记录 +- graph 执行链路具备可恢复能力 +- 系统具备缓存、异步任务和文件持久化基础设施 + +--- + +## 11. Redis 缓存层 + +### 要完成什么功能 +- 降低外部接口重复调用成本 +- 提升对话链路响应速度 +- 处理短期状态、热点数据和限流控制 + +### 用什么技术 +- **Azure Redis**:缓存层 +- **LangChain / LangGraph 外围状态管理**:结合缓存保存中间态 + +### 怎么落地 +- 缓存这些内容: + - 相同 query 的 KB 搜索结果 + - 相同 query 的外部搜索与重排结果 + - 工单摘要结果 + - 文档生成任务短状态 + - 会话级短期上下文摘要 +- 为外部搜索和知识库增加 TTL +- 为 Service Bus 异步任务增加状态缓存 + +### 输出结果 +- 系统速度更稳定 +- 外部服务成本更低 +- 可支撑更高并发下的会话请求 + +--- + +## 12. 存储账户(文件与产物存储) + +### 要完成什么功能 +- 持久化用户上传附件 +- 保存文档生成结果 +- 保存 Sandbox 执行生成的图表/文件 +- 为前端提供附件与结果文件访问地址 + +### 用什么技术 +- **Azure Blob Storage**:统一文件对象存储 +- **LangChain Document Loader**:结合存储文件做解析 + +### 怎么落地 +- 上传文件后先保存到 Blob Storage +- 数据库中记录 blob URL、文件类型、所属消息/会话 +- 文档生成与 Sandbox 产物统一落到 Blob Storage +- 前端保持现有交互,仅在消息中附带文件结果卡片或链接 + +### 输出结果 +- 所有附件和中间产物有统一落盘位置 +- 后续分析、下载、追踪都更方便 + +--- + +## 13. Service Bus 异步任务层 + +### 要完成什么功能 +- 处理长耗时任务 +- 解耦即时对话链路和后台异步处理链路 +- 支持重试、失败恢复、延后处理 + +### 用什么技术 +- **Azure Service Bus**:消息队列 / 异步任务总线 +- **LangGraph**:消费任务后继续执行长链路节点 + +### 怎么落地 +- 把这些任务异步化: + - 文档生成 + - Sandbox 长任务 + - 深度外部搜索 + - 未来的大批量分析任务 +- 聊天主链路先返回“任务已受理”状态 +- Worker 从 Service Bus 拉取任务继续执行 +- 执行结果写数据库和存储账户,再回推前端 + +### 输出结果 +- 避免主对话链路阻塞 +- 长任务处理更稳定 +- 适合企业级系统扩展 + +--- + +## 14. MCP 方式接入外部搜索 + +### 要完成什么功能 +- 利用 `https://mcp.jina.ai/sse` 这一类能力,以 MCP 方式接入外部搜索 +- 让外部搜索不只是普通 HTTP API,而是可作为标准工具节点接入 LangChain / LangGraph + +### 用什么技术 +- **MCP(Model Context Protocol)**:统一工具协议 +- **Jina MCP SSE / v1**:外部搜索与读取能力来源 +- **LangChain Tool 封装层**:把 MCP 调用转换成 graph 可调用工具 + +### 怎么落地 +- 优先测试 Jina 提供的 `/sse` 和 `/v1` 两种入口 +- 将 Search 和 Read 分别封装成两个 tool +- 在外部搜索节点中统一走 MCP 接入层,保留将来替换搜索供应商的可能 +- 重排仍保留单独节点,以便保障搜索质量控制 + +### 输出结果 +- 外部搜索链路更标准化 +- 更容易扩展到更多 MCP 服务 +- 对 LangChain / LangGraph 编排更友好 + +--- + +## 15. 基于当前前端代码补充的后端缺口与完善方案 + +> 这一章专门对应当前前端已经存在、但此前后端方案没有完整覆盖的功能点。不含认证和权限,只补业务后端能力。 + +### 15.1 消息反馈(赞 / 踩) + +#### 要完成什么 +- 用户对 assistant 消息进行点赞或点踩 +- 后端记录反馈结果 +- 后续可用于回答质量分析、提示词优化和问题回溯 + +#### 用什么技术 +- **PostgreSQL**:保存反馈记录 +- **LangGraph 旁路记录**:反馈不进入主对话 graph +- **Redis(可选)**:做短期统计缓存 + +#### 怎么落地 +- 新增表:`message_feedback` + - `id` + - `message_id` + - `conversation_id` + - `feedback_type` (`up` / `down`) + - `reason`(可空,后续扩展) + - `created_at` +- 新增接口: + - `POST /api/messages/{id}/feedback` +- 前端点击赞/踩后直接调用该接口 +- 第一阶段先只记录 `up/down`,不做复杂原因分类 + +--- + +### 15.2 模型切换映射 + +#### 要完成什么 +- 前端已有 Flash / Pro 与顶部模型选择入口 +- 第一阶段后端先统一固定使用 **GPT-5.4** +- 但保留字段和映射结构,后续再扩展多模型、多链路 + +#### 用什么技术 +- **LangChain model wrapper**:模型封装 +- **LangGraph state**:保存 `model_profile` +- **PostgreSQL conversation metadata**:记录选择结果 + +#### 怎么落地 +- 前端若传模型字段,第一阶段统一映射为: + - `model_provider = azure_openai` + - `model_name = gpt-5.4` +- 保留 metadata 字段: + - `selected_model` + - `selected_mode` +- 当前只做字段记录与透传,不做真正多模型切换 +- 第二阶段再扩为 flash/pro 对应不同 graph 策略 + +--- + +### 15.3 工具显式开关控制 + +#### 要完成什么 +- 前端工具 chips: + - 搜索 + - 内部知识库 + - 沙盒 + - 文档生成 +- 用户手动启用哪些工具,后端就只允许调用这些工具 +- 用户未选择时,后端才走自动路由 + +#### 用什么技术 +- **LangGraph state**:保存当前消息工具选择 +- **LangChain tools registry**:统一工具注册 +- **tool allowlist / denylist**:工具调用控制 + +#### 怎么落地 +- 前端发消息时附带: +```json +{ + "enabled_tools": ["search", "knowledge"] +} +``` +- graph state 增加: + - `enabled_tools` + - `tool_selection_mode` (`auto` / `manual`) +- router 节点规则: + - `manual` 模式:只能从 allowlist 中路由 + - `auto` 模式:按规则或模型自由决策 +- 工具执行前统一做可用性校验 + +--- + +### 15.4 多文件上传与消息绑定 + +#### 要完成什么 +- 一次上传多个文件 +- 每个文件单独保存 +- 文件和某条消息绑定 +- 文件可参与知识问答、搜索、Sandbox 分析和文档生成 + +#### 用什么技术 +- **Azure Blob Storage**:存储文件 +- **PostgreSQL**:存储附件元数据 +- **LangChain Document Loaders**:解析附件内容 +- **消息-附件关联机制**:支撑多文件场景 + +#### 怎么落地 +- 新增表:`attachments` + - `id` + - `conversation_id` + - `message_id`(允许先空,待消息发送后再绑定) + - `file_name` + - `content_type` + - `storage_url` + - `parse_status` + - `parsed_text` + - `created_at` +- 新增接口: + - `POST /api/attachments` + - `POST /api/messages/{id}/attachments/bind` +- 推荐流程: + 1. 前端先上传多个文件 + 2. 后端返回 attachment ids + 3. 前端发消息时附带 attachment ids + 4. 后端完成消息与附件绑定 +- 解析流程异步化,避免阻塞主聊天链路 + +--- + +### 15.5 扩展程序连接管理 + +#### 要完成什么 +- 支持扩展程序的连接、断开、修改 key、查看状态 +- 页面刷新后仍保留扩展连接状态 +- 扩展状态可被后端 graph 感知 + +#### 用什么技术 +- **PostgreSQL**:保存扩展配置与状态 +- **加密存储机制**:保存敏感配置 +- **extension registry**:统一扩展管理 +- **LangChain tool 注册机制**:根据扩展状态暴露工具 + +#### 怎么落地 +- 新增表:`extensions` + - `id` + - `extension_type` (`ticket` / `sales` / `cloud`) + - `display_name` + - `status` + - `config_encrypted` + - `last_check_at` + - `last_check_status` +- 新增接口: + - `GET /api/extensions` + - `POST /api/extensions/{type}/connect` + - `POST /api/extensions/{type}/disconnect` + - `POST /api/extensions/{type}/validate` +- 第一阶段先完成工单系统全链路,销售和云管先保留扩展框架 + +--- + +### 15.6 销售系统 / 云管系统预留 + +#### 要完成什么 +- 虽然当前两套系统还在开发,但后端要预留统一扩展接入结构 +- 避免未来工单、销售、云管三套系统接入方式不一致 + +#### 用什么技术 +- **统一 extension schema** +- **summary provider 接口** +- **tool provider 接口** +- **connection config schema** + +#### 怎么落地 +- 一期不要求真实接入销售/云管 API +- 但必须预留: + - 扩展类型定义 + - tool 注册入口 + - summary 注册入口 + - 状态位和配置结构 +- 后续新增业务系统时不需要推翻现有后端结构 + +--- + +### 15.7 通用扩展摘要机制 + +#### 要完成什么 +- 不只是工单系统,未来销售、云管系统接入后,也能输出首页/对话页摘要卡片 +- 后端统一提供摘要机制 + +#### 用什么技术 +- **summary provider registry**:每个扩展实现自己的摘要提供者 +- **Redis**:缓存摘要结果 +- **PostgreSQL**:记录摘要生成时间与状态 +- **LangChain summarizer(可选)**:对原始数据做摘要 + +#### 怎么落地 +- 新增统一摘要接口: + - `GET /api/extensions/summaries` +- 返回结构示例: +```json +[ + { + "extension_type": "ticket", + "status": "connected", + "summary_type": "ticket_summary", + "data": {} + } +] +``` +- 第一阶段先实现 ticket summary provider +- 但接口设计按多扩展统一返回 + +--- + +### 15.8 结构化消息块协议 + +#### 要完成什么 +- 后端不能只返回纯文本 +- 需要支持: + - 文本 + - 引用来源 + - 摘要卡片 + - 文件结果 + - 工具状态 + - 错误块 + +#### 用什么技术 +- **LangGraph 标准化事件输出** +- **message block schema** +- **前后端统一 JSON 协议** + +#### 怎么落地 +- 定义统一 block 结构: +```json +{ + "type": "text | citation | summary_card | artifact | tool_status | error", + "payload": {} +} +``` +- assistant message 最终存储结构: +```json +{ + "id": "...", + "blocks": [] +} +``` +- SSE 中间态也复用 block/event 体系 +- 第一阶段前端至少支持: + - `text` + - `tool_status` + - `citation` + - `summary_card` + +--- + +### 15.9 长任务状态回传 + +#### 要完成什么 +- 文档生成、Sandbox 数据分析、深度搜索等任务可能耗时较长 +- 前端需要看到任务状态,而不是一直假 loading + +#### 用什么技术 +- **Azure Service Bus**:异步任务投递 +- **PostgreSQL**:任务状态持久化 +- **Redis**:缓存短状态 +- **SSE / 轮询**:状态回传给前端 + +#### 怎么落地 +- 新增表:`async_tasks` + - `id` + - `task_type` + - `conversation_id` + - `message_id` + - `status` + - `progress_text` + - `result_payload` + - `created_at` + - `updated_at` +- 新增接口: + - `GET /api/tasks/{id}` +- 第一阶段先采用“数据库状态 + 前端轮询” +- 后续再增强为 SSE 任务事件推送 + +--- + +### 15.10 扩展连接状态注入 graph + +#### 要完成什么 +- 某个扩展是否已连接,必须直接决定 graph 中哪些工具可用 +- 未连接扩展不能被调用 +- 已连接扩展才能参与 agent 路由 + +#### 用什么技术 +- **extension registry** +- **LangGraph state injection** +- **tool availability resolver** + +#### 怎么落地 +- graph 执行前先加载当前扩展连接状态 +- 注入 state: +```json +{ + "available_extensions": ["ticket"] +} +``` +- router 节点判断: + - 工单问题 + ticket 已连接 -> 允许调用 + - 工单问题 + ticket 未连接 -> 返回“扩展未连接” +- 销售 / 云管未来直接复用该机制 + +--- + +### 15.11 会话重命名 / 置顶等预留 + +#### 要完成什么 +- 为左侧会话更多操作菜单预留后端能力 +- 支持未来扩展: + - 重命名 + - 置顶 + - 自定义排序 + +#### 用什么技术 +- **PostgreSQL conversation metadata** +- **排序字段 / pinned 字段** + +#### 怎么落地 +- conversations 表补充字段: + - `custom_title` + - `pinned` + - `sort_order` +- 接口统一走: + - `PATCH /api/conversations/{id}` +- 即使前端暂未开放置顶,也建议先预留字段 + +--- + +### 15.12 会话级偏好元数据 + +#### 要完成什么 +- 记录会话偏好信息,例如: + - 当前选中的模型 + - 当前启用工具 + - 默认搜索模式 + - 当前关联扩展 +- 会话恢复时自动延续这些设置 + +#### 用什么技术 +- **PostgreSQL JSON metadata** +- **LangGraph state hydration** + +#### 怎么落地 +- conversations 表增加: + - `metadata_json` +- 典型结构示例: +```json +{ + "selected_model": "gpt-5.4", + "selected_mode": "pro", + "enabled_tools": ["knowledge", "search"], + "preferred_search_mode": "deep" +} +``` +- 会话恢复时把 metadata 注入 graph 初始 state + +--- + +## 16. 接口层总表 + +> 虽然本方案不以 FastAPI 为重点,但前端要接入,仍然需要有 HTTP/SSE 出口。这里把它视为“接入层”,不是方案核心。 + +### 第一阶段建议建设的接口 + +#### 基础接口 +- `GET /health` + +#### 会话接口 +- `GET /api/conversations` +- `POST /api/conversations` +- `GET /api/conversations/{id}` +- `PATCH /api/conversations/{id}` +- `DELETE /api/conversations/{id}` + +#### 聊天接口 +- `POST /api/chat/stream` + +#### 工单接口 +- `GET /api/tickets/summary` +- `GET /api/tickets` +- `GET /api/tickets/{id}` + +#### 搜索接口 +- `POST /api/search/internal` +- `POST /api/search/external` + +#### 文档接口 +- `POST /api/documents/generate` +- `GET /api/documents/{task_id}` + +#### 附件接口 +- `POST /api/attachments` +- `GET /api/attachments/{id}` + +#### 沙盒接口 +- `POST /api/sandbox/run` + +--- + +## 16. 推荐技术组合总结 + +### 核心框架 +- **LangChain**:模型调用、Prompt 组织、Tool 封装、Memory 适配 +- **LangGraph**:对话状态机、工具路由、任务编排、长链路执行 + +### 数据层 +- **PostgreSQL**:会话/消息/工具调用/任务持久化 +- **Redis**:缓存、短状态、限流 +- **Azure Blob Storage**:附件、产物、文档存储 +- **Azure Service Bus**:异步任务编排 +- **SQLAlchemy / SQLModel**:ORM +- **Alembic**:迁移管理 + +### AI 与搜索 +- **Azure OpenAI**:LLM 生成与总结 +- **KB_AGENT**:内部知识库检索 +- **Jina MCP SSE / v1 + Search / Reader / Rerank**:外部搜索链路 + +### 外部业务系统 +- **Gongdan API**:工单只读 +- **Doc Creator Agent**:文档生成 +- **Daytona Sandbox**:受控代码执行 + +### 协议与接入 +- **SSE**:流式输出到前端 +- **HTTP API**:前端接入层 + +--- + +## 17. 第一阶段开发顺序 + +### 第一步 +先完成: +- LangChain + LangGraph 基础工程 +- PostgreSQL 接入 +- conversations/messages 表 +- 基础聊天 graph +- `/api/chat/stream` +- `/api/conversations` + +### 第二步 +接入: +- Azure OpenAI +- KB_AGENT tool +- 工单 tools + +### 第三步 +接入: +- Jina MCP SSE / v1 搜索链路 +- Search / Reader / Rerank tool chain +- 来源引用 +- graph 中间状态流式事件 +- Redis 缓存 + +### 第四步 +接入: +- 文档生成 tool +- 附件解析 loader +- sandbox tools +- Azure Blob Storage +- Azure Service Bus +- graph 持久化和恢复 + +--- + +## 18. 最终结论 + +这个项目当前最合适的后端方案,如果明确要求基于 LangChain 框架来做,那就应该是: + +- 用 **LangChain + LangGraph** 做整个后端核心 +- 用 **Azure OpenAI** 做模型生成和总结 +- 用 **KB_AGENT** 做内部知识检索工具 +- 用 **Jina Search/Reader/Rerank** 做外部搜索工具链 +- 用 **Gongdan API** 做工单查询工具 +- 用 **Doc Creator Agent** 做正式文档生成工具 +- 用 **Daytona Sandbox** 做受控执行工具 +- 用 **PostgreSQL** 做会话、消息、任务和 graph 状态持久化 +- 用 **Redis** 做缓存和短状态管理 +- 用 **Azure Blob Storage** 做附件与产物存储 +- 用 **Azure Service Bus** 做长任务异步编排 +- 用 **MCP 方式** 标准化接入 Jina 外部搜索 + +整个系统本质上是: +**一个基于 LangGraph 编排、具备缓存/存储/异步任务能力的企业级对话 Agent 后端。** + +而且整个过程中: +**前端交互不改,只替换数据来源和后端能力。** diff --git a/doc/plan/backend-plan.md b/doc/plan/backend-plan.md new file mode 100644 index 0000000..3a923b8 --- /dev/null +++ b/doc/plan/backend-plan.md @@ -0,0 +1,324 @@ +# so-c-chat-clone 后端建设计划 + +## Context + +基于 `gpthd.md` 的 18 项功能方案和 `EXTERNAL_SERVICES.md` 中的 10 个已接入外部服务,从零构建 `backend/` 目录下的企业级对话 Agent 后端。前端 `backend/` 目前为空,mock 数据需替换为真实 API。 + +**核心约束:** +- 前端代码未经明确指定不允许修改 +- Azure 资源只能操作 `AuthData` 和 `Operation` 两个资源组 +- CI/CD 由用户自行创建,Agent 只负责推代码到 GitHub + +**前端改动(已授权):** +`GeminiInput.tsx` 的 `onSubmit` 扩展参数,将 `activeTools` 和 `selectedModel` 传给后端。**工具是否实际调用由 LangGraph ReAct Agent 自行判断**;如果 Agent 决定不使用某个工具,必须在回复中向用户说明原因。视觉交互完全不变。 + +--- + +## 第一阶段:基础工程 + 对话核心 + +### 目标 +完成可运行的后端骨架,实现真实 LLM 对话,替换前端 `simulateAIResponse()`。 + +### 文件结构 +``` +backend/ +├── pyproject.toml # 依赖管理 +├── .env.example # 环境变量模板 +├── .gitignore # Python 忽略规则 +├── app/ +│ ├── main.py # Litestar 入口,路由注册,CORS +│ ├── config.py # 环境变量读取(pydantic-settings) +│ ├── schemas.py # 请求/响应 Pydantic 模型 +│ ├── graph/ +│ │ ├── state.py # LangGraph MessagesState 定义 +│ │ ├── nodes.py # call_model 节点 +│ │ └── builder.py # StateGraph 构建,compile with checkpointer +│ ├── store/ +│ │ └── postgres.py # AsyncPostgresSaver 初始化,conversations/messages 表 +│ └── api/ +│ ├── chat.py # POST /api/chat/stream(SSE) +│ ├── conversations.py # GET/POST/PATCH/DELETE /api/conversations +│ └── health.py # GET /health +``` + +### 关键实现 + +**LangGraph 基础图(graph/builder.py):** +```python +from langgraph.graph import StateGraph, MessagesState +from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver + +graph = StateGraph(MessagesState) +graph.add_node("agent", call_model_node) +graph.set_entry_point("agent") +graph.set_finish_point("agent") +app_graph = graph.compile(checkpointer=AsyncPostgresSaver.from_conn_string(DATABASE_URL)) +``` + +**SSE 流式输出(api/chat.py):** +```python +async def stream_chat(request: ChatRequest) -> ServerSentEvent: + async for event in app_graph.astream_events( + {"messages": [HumanMessage(content=request.message)]}, + config={"configurable": {"thread_id": request.conversation_id}}, + version="v2" + ): + if event["event"] == "on_chat_model_stream": + yield ServerSentEvent(data=event["data"]["chunk"].content) +``` + +**模型参数(nodes.py):** +- `model=flash` → `max_tokens=500, temperature=0.2` +- `model=pro` → `max_tokens=4096, temperature=0.3` + +**依赖:** +``` +litestar[standard] uvicorn +langchain langchain-openai langgraph +langgraph-checkpoint-postgres +asyncpg sqlalchemy[asyncio] +pydantic-settings python-dotenv httpx +``` + +**数据库表(PostgreSQL):** +- `conversations(id, title, created_at, updated_at)` +- `messages(id, conversation_id, role, content, created_at)` +- LangGraph checkpointer 自动建表 + +**完成后推 GitHub,同步前端改动(已授权):** +- `GeminiInput.tsx`:`onSubmit: (tools: string[], model: string) => void` +- `GeminiChat.tsx`:`handleSend` 接收 tools/model,传入 `/api/chat/stream` +- Agent system prompt 中注明:若决定不调用用户选中的工具,必须在回复中说明原因 + +--- + +## 第二阶段:Tool 接入(KB + 工单 + LLM 路由) + +### 目标 +接入内部知识库和工单系统,LangGraph ReAct Agent 自动路由工具调用。 + +### 新增文件 +``` +backend/app/ +├── tools/ +│ ├── kb.py # kb_search_tool → KB_AGENT /api/v1/search +│ └── tickets.py # ticket_list_tool, ticket_detail_tool, ticket_summary_tool +├── graph/ +│ └── builder.py # 升级为 create_react_agent,绑定 tools +└── api/ + └── tickets.py # GET /api/tickets/summary, /api/tickets, /api/tickets/{id} +``` + +**工具封装示例(tools/kb.py):** +```python +@tool +async def kb_search(query: str) -> str: + """检索企业内部知识库""" + resp = await client.post(KB_AGENT_URL + KB_AGENT_SEARCH_PATH, + json={"query": query, "top": 5, "search_mode": "hybrid"}, + headers={"api-key": KB_AGENT_API_KEY}) + results = resp.json()["results"] + return "\n\n".join(f"【{r['title']}】\n{r['content']}" for r in results) +``` + +**动态 Tool 绑定(根据前端传入 tools 参数):** +```python +ALL_TOOLS = {"knowledge": kb_search, "search": web_search, ...} +active = [ALL_TOOLS[t] for t in request.tools if t in ALL_TOOLS] +agent = create_react_agent(llm, active, checkpointer=checkpointer) +``` + +**工单接口:** 代理转发 Gongdan API,返回字段对齐前端 `TicketData`(`id/title/status/priority/createdAt`) + +**完成后推 GitHub。** + +--- + +## 第三阶段:外部搜索 + Redis 缓存 + +### 目标 +接入 Jina MCP/v1 搜索链路(Search + Reader + Rerank),Redis 缓存热点结果。 + +### 新增文件 +``` +backend/app/ +├── tools/ +│ └── search.py # web_search_tool(Jina Search → Reader → Rerank) +└── cache/ + └── redis.py # Redis 客户端,搜索结果 TTL 缓存 +``` + +**Jina 接入(优先测试 /v1,备选 MCP SSE):** +```python +# 搜索 +POST https://s.jina.ai/ Authorization: Bearer JINA_API_KEY + +# 读取全文 +GET https://r.jina.ai/{url} Authorization: Bearer JINA_API_KEY + +# 重排 +POST https://api.jina.ai/v1/rerank + model: jina-reranker-v2-base-multilingual +``` + +**按 model 深度区分:** +| model | top | timeout | Reader | Rerank | +|-------|-----|---------|--------|--------| +| flash | 3 | 8s | 跳过 | 跳过 | +| pro | 10 | 20s | 并发 | top 5 | + +**Redis 配置(EXTERNAL_SERVICES.md):** +``` +oper.redis.cache.windows.net:6380 +password=bY8ZNwyJX60UwN5NPqnl6HRODfTV0efkDAzCaF1PrOU=,ssl=True +``` +缓存 key:`search:{query_hash}:{model}`,TTL=300s + +**完成后推 GitHub。** + +--- + +## 第四阶段:文档生成 + 沙盒 + 附件 + 异步任务 + +### 目标 +接入 Doc Creator Agent、Daytona Sandbox、Azure Blob Storage、Azure Service Bus。 + +### 新增文件 +``` +backend/app/ +├── tools/ +│ ├── document.py # doc_generate_tool → Doc Creator Agent +│ └── sandbox.py # sandbox_run_tool → Daytona API +├── storage/ +│ └── blob.py # Azure Blob Storage 上传/下载 +└── tasks/ + └── bus.py # Azure Service Bus 异步任务派发/消费 +``` + +**Doc Creator(tools/document.py):** +```python +@tool +async def generate_document(prompt: str) -> str: + """生成 Word/PPT/Excel 文档,返回下载链接""" + output_type = detect_doc_type(prompt) # ppt/table/word + resp = await client.post(DOC_AGENT_URL, + json={"prompt": prompt, "output_type": output_type}, + headers={"Authorization": f"Bearer {DOC_AGENT_KEY}"}) + data = resp.json() + return f"📄 [{data['title']}]({data['file_url']})" +``` + +**Daytona(tools/sandbox.py):** +创建 workspace → 上传代码 → 执行 → 获取 stdout/stderr → 销毁 + +**Azure Blob Storage(EXTERNAL_SERVICES.md):** +``` +AccountName=authdatablol +AccountKey=sm3ysR0zAmS9OLti... +``` +用于保存附件、Sandbox 产物、文档生成结果。 + +**Azure Service Bus(EXTERNAL_SERVICES.md):** +``` +Endpoint=sb://databus.servicebus.windows.net/ +``` +文档生成、深度搜索等长任务通过 Service Bus 异步化,主链路先返回"任务受理"。 + +**完成后推 GitHub,通知用户配置 CI/CD。** + +--- + +## 环境变量清单(backend/.env.example) + +``` +# Azure OpenAI +AZURE_OPENAI_ENDPOINT= +AZURE_OPENAI_API_KEY= +AZURE_OPENAI_API_VERSION=2025-04-01-preview +AZURE_OPENAI_DEPLOYMENT=gpt-5.4 + +# KB Agent +KB_AGENT_URL=https://agnetdoc-cve0guf5h8eggmej.southeastasia-01.azurewebsites.net +KB_AGENT_API_KEY= +KB_AGENT_SEARCH_PATH=/api/v1/search + +# Jina +JINA_API_KEY= + +# Daytona +DAYTONA_API_KEY= +DAYTONA_API_URL=https://app.daytona.io/api + +# Doc Agent +DOC_AGENT_URL=http://doc-creator-agent-b0d02105-a557fe.taijiagnet.com +DOC_AGENT_KEY= + +# Gongdan +GONGDAN_API_BASE=https://gongdan-b5fzbtgteqd5gzfb.eastasia-01.azurewebsites.net +GONGDAN_API_KEY= + +# PostgreSQL +DATABASE_URL=postgresql+asyncpg://azuredb:PASSWORD@dataope.postgres.database.azure.com:5432/soc?ssl=require + +# Redis +REDIS_URL=rediss://:bY8ZNwyJX60UwN5NPqnl6HRODfTV0efkDAzCaF1PrOU=@oper.redis.cache.windows.net:6380 + +# Azure Blob Storage +AZURE_STORAGE_CONNECTION_STRING= + +# Azure Service Bus +AZURE_SERVICE_BUS_CONNECTION_STRING= +``` + +--- + +## Azure 资源操作约束 + +所有 `az` 命令必须带 `--resource-group Operation` 或 `--resource-group AuthData`,否则停止执行。 + +--- + +## GitHub 推送节奏 + +每完成一个阶段: +```bash +git pull origin main +git add backend/ +git commit -m "feat(backend): 阶段N - ..." +git push origin main +``` + +CI/CD 由用户自行在 GitHub Actions 中配置,Agent 不创建 workflow 文件。 + +--- + +## 验证方式 + +**第一阶段验证:** +```bash +cd backend && uvicorn app.main:app --port 8000 --reload +curl -N http://localhost:8000/api/chat/stream \ + -X POST -H "Content-Type: application/json" \ + -d '{"message":"你好","conversation_id":"conv-test","tools":[],"model":"flash"}' +# 期望:SSE 流式返回 AI 回复 +``` + +**第二阶段验证:** +```bash +curl http://localhost:8000/api/tickets +# 期望:真实工单数据数组,字段对齐前端 TicketData +``` + +**第三阶段验证:** +```bash +curl -N http://localhost:8000/api/chat/stream \ + -d '{"message":"查一下最新的 AI 新闻","tools":["search"],"model":"pro",...}' +# 期望:回复引用真实搜索结果 +``` + +**第四阶段验证:** +```bash +curl -N http://localhost:8000/api/chat/stream \ + -d '{"message":"帮我生成一份PPT方案","tools":["document"],...}' +# 期望:回复包含文档下载链接 +``` diff --git a/doc/plan/browser-full-test-plan.md b/doc/plan/browser-full-test-plan.md new file mode 100644 index 0000000..1e72053 --- /dev/null +++ b/doc/plan/browser-full-test-plan.md @@ -0,0 +1,329 @@ +# 浏览器全功能测试计划(SOC) + +更新时间:2026-04-08 +测试方式:通过浏览器进行真实页面操作验证 +目标:对当前 SOC 前后端已暴露能力做一轮全功能联调测试 + +--- + +## 1. 测试目标 + +通过浏览器从用户视角验证以下功能是否真正可用: + +1. 页面是否能正常打开和加载 +2. 会话列表是否正常展示 +3. 新建会话是否正常 +4. 历史会话读取是否正常 +5. 聊天发送与流式输出是否正常 +6. 工具启用/禁用是否生效 +7. 工单扩展连接和展示逻辑是否符合当前实现 +8. 工单摘要是否正常加载 +9. 文件上传、附件展示、附件下载是否正常 +10. 页面刷新、切换会话后的数据一致性是否正常 +11. 基础异常场景下前端是否有合理反馈 + +--- + +## 2. 测试范围 + +### 本轮重点测试 + +- 前端主聊天页面 +- 会话管理 +- 聊天流式响应 +- 工具选择 +- 扩展连接 +- 工单摘要 +- 文件上传与附件展示 +- 附件下载 +- 页面基础稳定性 + +### 本轮不重点覆盖 + +- 模型切换 +- 管理后台类功能 +- 大规模压测 +- 安全渗透测试 +- 权限隔离深测 +- Service Bus 后台任务全链路深度验收 +- Blob 内部对象人工逐个核验 + +--- + +## 3. 测试前置条件 + +执行浏览器测试前,需要确认: + +1. 前端页面可访问 +2. 后端 API 可访问 +3. 数据库已可正常连接 +4. Azure OpenAI 配置可用 +5. 工单接口可访问 +6. 上传相关 Blob Storage 配置可用 +7. 当前测试环境明确(本地或生产) + +--- + +## 4. 功能测试清单 + +### A. 页面与基础加载 + +#### 用例 A1:主页加载 +步骤: +1. 打开系统页面 +2. 观察首屏渲染 + +预期: +- 页面成功打开 +- 无明显白屏/崩溃 +- 控制台无阻断性错误 + +#### 用例 A2:侧边栏与主界面结构 +步骤: +1. 检查侧边栏 +2. 检查顶部栏 +3. 检查输入框 + +预期: +- 主界面结构完整 +- 关键操作入口可见 + +--- + +### B. 会话功能 + +#### 用例 B1:会话列表加载 +步骤: +1. 打开页面 +2. 观察历史会话列表 + +预期: +- 能成功拉取会话列表 +- 不报错 + +#### 用例 B2:打开已有会话 +步骤: +1. 点击一个已有会话 +2. 观察消息加载 + +预期: +- 会话详情正常打开 +- 历史消息正确显示 + +#### 用例 B3:新建会话 +步骤: +1. 点击新建会话 +2. 输入问题并发送 + +预期: +- 自动形成新会话 +- 新会话进入列表 + +#### 用例 B4:刷新后会话一致性 +步骤: +1. 新建或进入一个会话 +2. 刷新页面 +3. 再次查看会话内容 + +预期: +- 会话仍存在 +- 消息数据不丢失 + +--- + +### C. 聊天主链路 + +#### 用例 C1:普通聊天发送 +步骤: +1. 输入普通问题 +2. 发送 + +预期: +- 用户消息显示 +- 后端正常响应 +- 前端能收到流式结果 + +#### 用例 C2:流式输出体验 +步骤: +1. 发送稍复杂的问题 +2. 观察回答是否逐步出现 + +预期: +- 回答不是一次性卡死后才出现 +- token 流体验正常 + +#### 用例 C3:连续多轮对话 +步骤: +1. 连续发送多条消息 +2. 观察上下文是否连续 + +预期: +- 会话上下文连续 +- 消息顺序正常 + +#### 用例 C4:重新生成 +步骤: +1. 找到 assistant 消息 +2. 触发 regenerate + +预期: +- 能重新发起生成 +- 页面状态正常 + +--- + +### D. 工具 + +#### 用例 D1:工具开关 +步骤: +1. 开启/关闭工具选项 +2. 发送消息 + +预期: +- 请求参数随工具状态变化 +- 页面表现正常 + +--- + +### E. 扩展与工单能力 + +#### 用例 E1:打开扩展面板 +步骤: +1. 打开扩展面板 +2. 查看各扩展状态 + +预期: +- 面板可打开 +- 扩展项展示正常 + +#### 用例 E2:工单扩展连接 +步骤: +1. 在工单系统扩展中输入 key +2. 点击连接 + +预期: +- 按当前实现,前端会变成 connected +- 需要特别记录:这是前端模拟连接,不代表真实鉴权成功 + +#### 用例 E3:工单摘要加载 +步骤: +1. 让工单扩展处于 connected 状态 +2. 观察 ticket summary 是否出现 + +预期: +- 前端请求 `/api/tickets/summary` +- 成功时摘要展示正常 + +#### 用例 E4:工单数据真实性侧验证 +步骤: +1. 观察摘要/工单接口返回 +2. 检查网络请求 + +预期: +- 请求真实到后端 tickets 接口 +- 不是前端静态写死 + +--- + +### F. 上传与附件 + +#### 用例 F1:单文件上传 +步骤: +1. 选择一个小文件 +2. 上传并发送消息 + +预期: +- 上传成功 +- 消息中显示附件 + +#### 用例 F2:多文件上传 +步骤: +1. 一次选择多个文件 +2. 上传并发送消息 + +预期: +- 多文件都能显示 +- 状态分别可见 + +#### 用例 F3:附件下载 +步骤: +1. 点击附件下载 +2. 观察下载接口请求和结果 + +预期: +- 正常触发 `/api/attachments/{id}/download` +- 可下载或跳转到有效资源 + +#### 用例 F4:超限/失败上传 +步骤: +1. 上传异常文件或超大文件 +2. 观察页面反馈 + +预期: +- 前端有失败提示 +- 不出现假成功 + +--- + +### G. 稳定性与异常 + +#### 用例 G1:接口失败时页面反馈 +步骤: +1. 观察异常请求场景 +2. 记录页面提示与控制台错误 + +预期: +- 页面有基本反馈 +- 不应无提示失败 + +#### 用例 G2:页面刷新与重进 +步骤: +1. 刷新页面 +2. 再次进入关键功能 + +预期: +- 不应出现明显状态错乱 + +--- + +## 5. 记录方式 + +测试中记录以下内容: + +- 测试时间 +- 页面 URL +- 测试环境 +- 操作步骤 +- 页面表现 +- 控制台报错 +- 网络请求 URL / 状态码 +- 通过 / 失败 / 待确认 +- 问题归因(前端 / 后端 / 配置 / 外部依赖) + +--- + +## 6. 输出结果格式 + +测试完成后按以下格式输出: + +1. 已验证通过 +2. 存在问题 +3. 待进一步确认 + +并对每个问题补充: +- 复现步骤 +- 影响范围 +- 初步判断原因 + +--- + +## 7. 当前执行顺序 + +1. 打开页面确认能访问 +2. 验证会话功能 +3. 验证聊天主链路 +4. 验证工具切换 +5. 验证扩展和工单能力 +6. 验证上传和下载 +7. 验证刷新和异常场景 +8. 汇总结果 diff --git a/doc/plan/cot-trace-implementation-plan.md b/doc/plan/cot-trace-implementation-plan.md new file mode 100644 index 0000000..91b986a --- /dev/null +++ b/doc/plan/cot-trace-implementation-plan.md @@ -0,0 +1,668 @@ +# COT 可视化(结构化执行轨迹)生产落地计划 + +## 背景与方案评估 + +### 关于"Gemini CoT"的定位澄清 + +Gemini 的原生 CoT 是指模型在输出最终答案前的推理 token(类似 Claude Extended Thinking)。Azure OpenAI / GPT-4o **不暴露模型级别的推理 scratchpad**,因此本方案实现的是 **Agent Activity Trace(结构化执行轨迹)**,本质是: + +> 拦截 LangGraph ReAct 图的运行事件 → 结构化摘要 → SSE 推送 → 前端时间线渲染 + +这是比暴露原始 CoT 更合理的选择,也是 Gemini/ChatGPT Pro 实际采用的方式。 + +### 现状与差距 + +| 层 | 现状 | 生产缺口 | +|---|---|---| +| 后端 SSE | 只有 token/tool_start/tool_end/done,无摘要字段 | 需补全 6 类事件 + 摘要 + 耗时 + 错误检测 | +| 前端 SSE 消费 | tool_start/tool_end 完全忽略 | 新增全部事件处理分支 | +| 前端数据模型 | `Message` 无 traceItems 字段 | 扩展接口 | +| 前端 UI | 无 Trace 组件 | 新建 TracePanel,复用已有 Collapsible/Spinner | +| GeminiMessage | 不接收 model prop | 需透传 selectedModel | +| 数据持久化 | Message 表无 metadata 字段 | Trace 为会话内存态,不持久化(历史消息无 trace,合理) | + +--- + +## 生产级事件协议 + +### 后端完整事件集(6 类) + +```json +// 1. 状态事件 — 阶段感知 +{"type": "status", "stage": "分析问题", "message": "正在理解您的问题..."} + +// 2. 工具调用开始 +{ + "type": "tool_start", + "tool": "kb_search", + "title": "检索知识库", + "input_summary": "查询:产品规划路线图", + "ts": 1712620800000 +} + +// 3. 工具调用成功结束 +{ + "type": "tool_end", + "tool": "kb_search", + "title": "检索知识库", + "output_summary": "命中 3 条知识库记录", + "status": "success", + "duration_ms": 842, + "ts": 1712620800842 +} + +// 4. 工具调用失败 +{ + "type": "tool_error", + "tool": "web_search", + "title": "外部搜索", + "error_summary": "请求超时,已跳过", + "duration_ms": 8000, + "ts": 1712620808000 +} + +// 5. token(现有,不变) +{"type": "token", "content": "根据知识库..."} + +// 6. done(现有,不变) +{"type": "done"} +``` + +--- + +## 后端实现(backend/app/api/chat.py) + +### 全部改动 + +**新增导入:** +```python +import time +``` + +**在 `stream_response` 生成器函数中:** + +```python +async def generate(): + full_content: list[str] = [] + tool_start_ts: dict[str, int] = {} # 记录各工具的起始时间戳 + has_tool_activity = False # 是否有过工具调用 + final_status_emitted = False # "整理答案"状态是否已发出 + + try: + # ① 在 graph 开始前发出初始状态 + yield _sse({"type": "status", "stage": "分析问题", "message": "正在理解您的问题..."}) + + async for event in graph.astream_events(input_data, config=config, version="v2"): + kind = event.get("event", "") + + # ② token 事件:在首个 token 前,若有工具调用则发"整理答案"状态 + if kind == "on_chat_model_stream": + chunk = event.get("data", {}).get("chunk") + if chunk and hasattr(chunk, "content") and chunk.content: + if isinstance(chunk.content, str): + # 若工具调用已完成,在首 token 前插入"整理答案"状态 + if has_tool_activity and not final_status_emitted: + yield _sse({"type": "status", "stage": "整理答案", + "message": "正在结合检索结果生成回复..."}) + final_status_emitted = True + full_content.append(chunk.content) + yield _sse({"type": "token", "content": chunk.content}) + + # ③ 工具开始 + elif kind == "on_tool_start": + tool_name = event.get("name", "unknown") + tool_input = event.get("data", {}).get("input", {}) + ts = int(time.time() * 1000) + tool_start_ts[tool_name] = ts + has_tool_activity = True + yield _sse({ + "type": "tool_start", + "tool": tool_name, + "title": TOOL_TITLES.get(tool_name, tool_name), + "input_summary": _summarize_input(tool_name, tool_input), + "ts": ts, + }) + + # ④ 工具结束(含错误检测) + elif kind == "on_tool_end": + tool_name = event.get("name", "unknown") + output = event.get("data", {}).get("output", "") + output_str = output if isinstance(output, str) else str(output) + ts = int(time.time() * 1000) + duration_ms = ts - tool_start_ts.pop(tool_name, ts) + is_error = _is_tool_error(output_str) + if is_error: + yield _sse({ + "type": "tool_error", + "tool": tool_name, + "title": TOOL_TITLES.get(tool_name, tool_name), + "error_summary": _extract_error_summary(output_str), + "duration_ms": duration_ms, + "ts": ts, + }) + else: + yield _sse({ + "type": "tool_end", + "tool": tool_name, + "title": TOOL_TITLES.get(tool_name, tool_name), + "output_summary": _summarize_output(tool_name, output_str), + "status": "success", + "duration_ms": duration_ms, + "ts": ts, + }) + + except Exception as exc: + # 现有错误处理逻辑保持不变 + ... + finally: + ai_content = "".join(full_content) + if ai_content: + await _persist_ai_message(request.conversation_id, ai_content) + yield _sse({"type": "done"}) +``` + +**新增辅助函数(同文件尾部):** + +```python +def _sse(data: dict) -> bytes: + return f"data: {json.dumps(data, ensure_ascii=False)}\n\n".encode("utf-8") + + +TOOL_TITLES: dict[str, str] = { + "kb_search": "检索知识库", + "ticket_list": "查询工单列表", + "ticket_detail": "查询工单详情", + "web_search": "外部搜索", + "generate_document": "生成文档", + "sandbox_run": "执行沙盒代码", +} + +# 工具输出中的错误关键词(工具均返回字符串而非 raise) +_ERROR_KEYWORDS = ( + "出错", "失败", "超时", "error", "failed", "timeout", "not available", + "no download link", "检索出错", "检索超时", "execution failed", +) + +def _is_tool_error(output: str) -> bool: + lo = output.lower() + return any(kw in lo for kw in _ERROR_KEYWORDS) + +def _extract_error_summary(output: str) -> str: + # 取首行,截断到 60 字符 + first_line = output.split("\n")[0].strip() + return first_line[:60] if first_line else "工具调用失败" + +def _summarize_input(tool_name: str, inp: dict | str) -> str: + if isinstance(inp, str): + return inp[:60] + match tool_name: + case "kb_search": + return f"查询:{str(inp.get('query', ''))[:50]}" + case "ticket_list": + return f"第 {inp.get('page', 1)} 页,每页 {inp.get('page_size', 20)} 条" + case "ticket_detail": + return f"工单 ID:{inp.get('ticket_id', '')}" + case "web_search": + return f"搜索:{str(inp.get('query', ''))[:50]}" + case "generate_document": + return str(inp.get('prompt', ''))[:60] + case "sandbox_run": + lang = inp.get('language', 'python') + lines = len(str(inp.get('code', '')).splitlines()) + return f"{lang} 代码({lines} 行)" + case _: + return str(inp)[:60] + +def _summarize_output(tool_name: str, output: str) -> str: + if not output or output.strip() == "(no output)": + return "无结果" + match tool_name: + case "kb_search": + count = output.count("---") + 1 if "---" in output else 1 + return f"命中 {count} 条知识库记录" + case "ticket_list": + import re + m = re.search(r"Found (\d+) tickets", output) + return f"返回 {m.group(1)} 条工单" if m else "工单列表已获取" + case "ticket_detail": + return "工单详情已获取" + case "web_search": + count = output.count("##") + return f"找到 {max(count, 1)} 条搜索结果" + case "generate_document": + if "Download:" in output: + doc_type = "文档" + if "[PPT]" in output: + doc_type = "PPT" + elif "[Excel]" in output or "[Table]" in output: + doc_type = "表格" + elif "[Word]" in output: + doc_type = "Word 文档" + return f"{doc_type}已生成,可下载" + return "文档生成完成" + case "sandbox_run": + lines = len(output.splitlines()) + exit_match = output.startswith("[Exit code:") + suffix = "(含错误)" if exit_match else "" + return f"执行完成,输出 {lines} 行{suffix}" + case _: + return output[:60] +``` + +--- + +## 前端实现 + +### 文件 1:lib/api.ts — 类型扩展 + +```typescript +// 扩展 ChatStreamEvent(完整字段) +export interface ChatStreamEvent { + type: "token" | "status" | "tool_start" | "tool_end" | "tool_error" | "done" | "error"; + // token + content?: string; + // status + stage?: string; + message?: string; + // tool_start / tool_end / tool_error + tool?: string; + title?: string; + input_summary?: string; + output_summary?: string; + error_summary?: string; + status?: "success" | "error"; + duration_ms?: number; + ts?: number; +} + +// 前端 Trace 条目(统一结构) +export interface TraceItem { + id: string; // 唯一 id + type: "status" | "tool_start" | "tool_end" | "tool_error"; + tool?: string; // 工具名(tool_* 类型) + title: string; // 展示标题 + message?: string; // status 的描述文本 + inputSummary?: string; + outputSummary?: string; + errorSummary?: string; + itemStatus: "running" | "success" | "error" | "info"; // UI 状态 + durationMs?: number; + startTs: number; // 毫秒时间戳 +} +``` + +**streamChat 函数签名不变**,只需更新 `ChatStreamEvent` 类型定义即可。 + +--- + +### 文件 2:components/gemini/GeminiMessage.tsx — 接口扩展与 TracePanel 集成 + +**扩展 `Message` 接口:** +```typescript +import type { TraceItem } from "@/lib/api"; + +export interface Message { + id: string; + role: "user" | "assistant"; + content: string; + timestamp?: Date; + attachments?: AttachmentData[]; + traceItems?: TraceItem[]; // 新增:执行轨迹(流式构建,不持久化) +} +``` + +**扩展 `GeminiMessageProps`:** +```typescript +interface GeminiMessageProps { + message: Message; + model?: "flash" | "auto" | "pro"; // 新增:用于决定 TracePanel 展示层级 + onRegenerate?: (id: string) => void; +} +``` + +**Assistant 消息 JSX — 在 content 上方插入 TracePanel:** +```tsx +// 在 assistant 分支内,
之前: +{message.traceItems && message.traceItems.length > 0 && ( + +)} +
{renderContent(message.content)}
+``` + +--- + +### 文件 3:components/gemini/GeminiChat.tsx — 事件处理与 model 透传 + +**handleSend 的 onEvent 回调 — 完整替换:** +```typescript +(event) => { + if (event.type === "token" && event.content) { + // 现有 token 逻辑,不变 + setConversations((prev) => prev.map((c) => { + if (c.id !== streamConvId) return c; + const exists = c.messages.some((m) => m.id === aiMsgId); + if (!exists) { + return { ...c, messages: [...c.messages, { + id: aiMsgId, role: "assistant" as const, + content: event.content!, timestamp: new Date(), + traceItems: [], // 初始化 traceItems + }]}; + } + return { ...c, messages: c.messages.map((m) => + m.id === aiMsgId ? { ...m, content: m.content + event.content } : m + )}; + })); + + } else if (event.type === "status") { + // status 事件:追加 info 条目(分析问题 / 整理答案) + const item: TraceItem = { + id: `status-${event.ts ?? Date.now()}`, + type: "status", + title: event.stage ?? "处理中", + message: event.message, + itemStatus: "info", + startTs: event.ts ?? Date.now(), + }; + _appendTraceItem(streamConvId, aiMsgId, item, setConversations); + + } else if (event.type === "tool_start" && event.tool) { + // 工具开始:状态为 running + const item: TraceItem = { + id: `${event.tool}-${event.ts ?? Date.now()}`, + type: "tool_start", + tool: event.tool, + title: event.title ?? event.tool, + inputSummary: event.input_summary, + itemStatus: "running", + startTs: event.ts ?? Date.now(), + }; + _appendTraceItem(streamConvId, aiMsgId, item, setConversations); + + } else if (event.type === "tool_end" && event.tool) { + // 工具结束:更新 running → success + _updateTraceItem(streamConvId, aiMsgId, event.tool, { + type: "tool_end", + outputSummary: event.output_summary, + itemStatus: "success", + durationMs: event.duration_ms, + }, setConversations); + + } else if (event.type === "tool_error" && event.tool) { + // 工具错误:更新 running → error + _updateTraceItem(streamConvId, aiMsgId, event.tool, { + type: "tool_error", + errorSummary: event.error_summary, + itemStatus: "error", + durationMs: event.duration_ms, + }, setConversations); + } +} +``` + +**新增辅助函数(文件顶层,组件外部):** +```typescript +function _appendTraceItem( + convId: string, msgId: string, item: TraceItem, + setConversations: React.Dispatch> +) { + setConversations((prev) => prev.map((c) => { + if (c.id !== convId) return c; + return { ...c, messages: c.messages.map((m) => + m.id === msgId ? { ...m, traceItems: [...(m.traceItems ?? []), item] } : m + )}; + })); +} + +function _updateTraceItem( + convId: string, msgId: string, tool: string, + updates: Partial, + setConversations: React.Dispatch> +) { + setConversations((prev) => prev.map((c) => { + if (c.id !== convId) return c; + return { ...c, messages: c.messages.map((m) => { + if (m.id !== msgId) return m; + // 找到最后一个同名 running 条目并更新 + const items = [...(m.traceItems ?? [])]; + for (let i = items.length - 1; i >= 0; i--) { + if (items[i].tool === tool && items[i].itemStatus === "running") { + items[i] = { ...items[i], ...updates }; + break; + } + } + return { ...m, traceItems: items }; + })}; + })); +} +``` + +**透传 selectedModel 给 GeminiMessage:** + +找到 `GeminiMessage` 的渲染位置,新增 `model={selectedModel}` prop。 + +--- + +### 文件 4:components/gemini/TracePanel.tsx(新建) + +完整组件,使用已有的 `Collapsible`(`components/ui/collapsible.tsx`)和 `Spinner`(`components/ui/spinner.tsx`)。 + +```tsx +"use client"; + +import { useState } from "react"; +import { CheckCircle2, XCircle, ChevronDown, ChevronRight, Loader2, Zap, Brain } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; +import type { TraceItem } from "@/lib/api"; + +interface TracePanelProps { + items: TraceItem[]; + model: "flash" | "auto" | "pro"; + className?: string; +} + +// ─── 工具图标映射 ───────────────────────────────────────── +const TOOL_ICONS: Record = { + kb_search: "🗂️", + web_search: "🌐", + ticket_list: "🎫", + ticket_detail: "🎫", + generate_document: "📄", + sandbox_run: "⚙️", +}; + +// ─── 状态图标 ───────────────────────────────────────────── +function StatusIcon({ status }: { status: TraceItem["itemStatus"] }) { + switch (status) { + case "running": + return ; + case "success": + return ; + case "error": + return ; + case "info": + return ; + } +} + +// ─── 耗时格式化 ─────────────────────────────────────────── +function formatDuration(ms?: number): string { + if (!ms) return ""; + if (ms < 1000) return `${ms}ms`; + return `${(ms / 1000).toFixed(1)}s`; +} + +// ─── 单条 Trace 条目 ────────────────────────────────────── +function TraceItemRow({ item, expanded }: { item: TraceItem; expanded: boolean }) { + const icon = item.tool ? TOOL_ICONS[item.tool] ?? "🔧" : null; + const isRunning = item.itemStatus === "running"; + + return ( +
+ {/* 状态图标 */} +
+ +
+ + {/* 内容 */} +
+
+ {icon && {icon}} + + {item.title} + + {item.durationMs !== undefined && ( + + {formatDuration(item.durationMs)} + + )} +
+ + {/* 详情(Pro 模式或展开状态下显示) */} + {expanded && ( +
+ {item.inputSummary && ( +

{item.inputSummary}

+ )} + {item.outputSummary && ( +

{item.outputSummary}

+ )} + {item.errorSummary && ( +

{item.errorSummary}

+ )} + {item.message && item.type === "status" && ( +

{item.message}

+ )} +
+ )} +
+
+ ); +} + +// ─── 主组件 ─────────────────────────────────────────────── +export function TracePanel({ items, model, className }: TracePanelProps) { + const isPro = model === "pro"; + const [open, setOpen] = useState(isPro); // Pro 默认展开 + + // 生成单行摘要(Auto 模式折叠时显示) + const summaryText = (() => { + const running = items.filter((i) => i.itemStatus === "running"); + if (running.length > 0) return `正在 ${running[running.length - 1].title}...`; + const tools = items.filter((i) => i.type === "tool_end"); + const errors = items.filter((i) => i.type === "tool_error"); + if (errors.length > 0) return `已完成(${errors.length} 个工具调用失败)`; + if (tools.length > 0) { + const names = tools.map((t) => t.title).join("、"); + return `已完成:${names}`; + } + return "正在分析..."; + })(); + + const hasRunning = items.some((i) => i.itemStatus === "running"); + + return ( + + {/* 触发行(始终可见)*/} + + + + + {/* 展开内容 */} + +
+ {items.map((item) => ( + + ))} +
+
+
+ ); +} +``` + +--- + +## 关键文件清单 + +| 文件 | 改动类型 | 核心内容 | +|------|---------|---------| +| `backend/app/api/chat.py` | 修改 | 6 类 SSE 事件、摘要函数、错误检测、耗时计算 | +| `frontend/lib/api.ts` | 修改 | `ChatStreamEvent` 扩展、新增 `TraceItem` 类型 | +| `frontend/components/gemini/GeminiMessage.tsx` | 修改 | `Message` 加 `traceItems`、`GeminiMessageProps` 加 `model`、集成 `TracePanel` | +| `frontend/components/gemini/GeminiChat.tsx` | 修改 | onEvent 补全所有事件分支、`_appendTraceItem`/`_updateTraceItem` 辅助函数、透传 `model` | +| `frontend/components/gemini/TracePanel.tsx` | 新建 | Auto/Pro 双模式时间线,使用已有 Collapsible + Spinner | + +--- + +## 约束说明 + +- **前端文件为 read-only**(CLAUDE.md 限制),需用户显式授权后执行 +- `TracePanel` 仅使用现有 CSS 变量(`--gem-*`)和已有 UI 组件,不引入新依赖 +- Trace 数据为会话内存态,历史消息加载时 `traceItems` 为空(符合预期) +- 工具错误通过输出字符串检测(因工具层均 return string 不 raise),关键词见 `_ERROR_KEYWORDS` + +--- + +## 验证方式 + +1. **后端事件格式验证** + ```bash + curl -N -X POST http://localhost:8000/api/chat/stream \ + -H "Content-Type: application/json" \ + -d '{"message":"帮我搜索产品规划","conversation_id":"test-1","tools":["knowledge"],"model":"flash"}' + ``` + 期望看到:`status` → `tool_start`(含 input_summary)→ `tool_end`(含 output_summary + duration_ms)→ `token`... → `done` + +2. **工具错误验证**:断开 KB Agent,发送知识库查询,期望看到 `tool_error` 事件(含 error_summary) + +3. **Auto 模式**:选 Auto + 知识库工具,助手消息上方出现单行折叠状态栏,点击展开显示时间线 + +4. **Pro 模式**:切换 Pro,状态栏默认展开,每条工具调用显示标题 + 摘要 + 耗时 + +5. **无工具时**:不选任何工具,TracePanel 不出现(`traceItems` 为空) + +6. **多工具顺序调用**:同时开启 knowledge + tickets,验证时间线条目顺序正确,各自 duration 准确 diff --git a/doc/start/EXTERNAL_SERVICES.md b/doc/start/EXTERNAL_SERVICES.md new file mode 100644 index 0000000..94cbb4c --- /dev/null +++ b/doc/start/EXTERNAL_SERVICES.md @@ -0,0 +1,152 @@ +# 外部服务接入配置 + +> **使用说明**:此文档用于记录外部服务的接入方式、环境变量和调用示例,便于开发、联调与排障。 +> +> 当前服务按“代码已支持 + 部署环境变量由 Azure Web App 提供”的口径记录为已接入;实际运行效果仍以部署环境变量是否正确配置为准。 +> +> 已接入的服务会标注 ✅。 + +--- + +## 1. LLM 大语言模型 + +> 当前使用 Azure OpenAI,已在后端 graph.py / main.py 中集成。 + +### 环境变量(已配置) +``` +AZURE_OPENAI_ENDPOINT=https://ai-gzy0016231ai975636166896.cognitiveservices.azure.com/openai/responses?api-version=2025-04-01-preview/ +AZURE_OPENAI_API_KEY=DlsBBFJ0RgMGdKxsdBWnlYj6IRdULzflGsKFCXnMBzqs4ZVHMtqZJQQJ99CCACHYHv6XJ3w3AAAAACOG45do +AZURE_OPENAI_API_VERSION=2025-04-01-preview +AZURE_OPENAI_DEPLOYMENT=gpt-5.4 +``` + +### 请求示例 +```bash +curl -X POST "${AZURE_OPENAI_ENDPOINT}/openai/deployments/${AZURE_OPENAI_DEPLOYMENT}/chat/completions?api-version=${AZURE_OPENAI_API_VERSION}" \ + -H "Content-Type: application/json" \ + -H "api-key: ${AZURE_OPENAI_API_KEY}" \ + -d '{ + "messages": [{"role": "user", "content": "你好"}], + "max_tokens": 1000 + }' +``` + +--- + +## 2. 内部知识库检索 + +> 当前通过 agnetdoc Function App 调用 Azure AI Search。 + +### 环境变量(已配置) +``` +KB_AGENT_URL=https://agnetdoc-cve0guf5h8eggmej.southeastasia-01.azurewebsites.net +KB_AGENT_API_KEY=LdyzZlS3Nn1xFejqPsHn1nW-zsj9FLpC5KCbopCkQWKCAzFuLEUU4w== +KB_AGENT_SEARCH_PATH=/api/v1/search +KB_AGENT_SEARCH_TIMEOUT_SEC=15 +``` + +### 请求示例 +```bash +curl -X POST "${KB_AGENT_URL}/api/v1/search" \ + -H "Content-Type: application/json" \ + -H "api-key: ${KB_AGENT_API_KEY}" \ + -d '{ + "query": "Taiji Agent 产品规划", + "top": 8, + "search_mode": "hybrid" + }' +``` + +### 响应格式 +```json +{ + "results": [ + { + "id": "xxx", + "title": "文档标题", + "content": "文档内容...", + "category": "分类", + "score": 0.85, + "url": "https://...", + "tags": ["tag1"], + "project": "项目名" + } + ] +} +``` + +--- + +## 3. 外部 AI 搜索 +目前外部搜索采用https://mcp.jina.ai/sse 或者 /v1 可优先测试 +jina_e26dc30420a44a1e859216528065b203TkMRmsoz-FgMDQC5FZX9jr5oF2CI +要求使用搜索和读取两个工具,并且要结合重排模型使用。 +满足企业级的搜索准确度,包括不限于图片和视频 +按照深度和快速来定义搜索内容和搜索的质量,还需要满足前端的展示。 + +支持MCP + +--- + +## 4. 沙盒代码执行 +沙盒采用现成的解决方案。https://docs.langchain.com/oss/python/integrations/sandboxes/daytona + +https://app.daytona.io/api +dtn_066b83f57f0337c96fae2ef1f5c8456477a39dfbd5fc615456263fd4947108c2 +依然要满足前端输出要求。 + + +## 5. 文档生成 Agent +http://doc-creator-agent-b0d02105-a557fe.taijiagnet.com +sk-t5R8jkEp6IA7_ghJ6Hy1rQ +http://agnetdoc.taijiaicloud.com/node/019cd223-9d13-7566-a2ea-52ee67645463 + + +## 6. 工单系统 + +> gongdan 工单系统,只读集成。 + +### 环境变量(已配置) +``` +GONGDAN_API_BASE=https://gongdan-b5fzbtgteqd5gzfb.eastasia-01.azurewebsites.net +GONGDAN_API_KEY=gd_live_a28b3db84385be75d1d3b6b6023784c27200d045 +``` + +### 请求示例 +```bash +# 工单列表 +curl -X GET "${GONGDAN_API_BASE}/api/tickets?page=1&pageSize=20" \ + -H "X-Api-Key: ${GONGDAN_API_KEY}" + +# 工单详情 +curl -X GET "${GONGDAN_API_BASE}/api/tickets/{ticketId}" \ + -H "X-Api-Key: ${GONGDAN_API_KEY}" +``` + +--- + +## 7. Pgsql数据库 +``` +DATABASE_URL=postgresql://USER:PASSWORD@:5432/yydn?sslmode=require +``` +``` +dataope.postgres.database.azure.com +azuredb:h13nYoFJX6QrfLzB8bdipEUCjsZq2P7W +``` +--- + +### 8.Redis +``` +oper.redis.cache.windows.net:6380,password=bY8ZNwyJX60UwN5NPqnl6HRODfTV0efkDAzCaF1PrOU=,ssl=True,abortConnect=False +``` +--- +### 9.存储账户 +``` +DefaultEndpointsProtocol=https;AccountName=authdatablol;AccountKey=sm3ysR0zAmS9OLtiHVau3Wj122YWQJTuMHAyHO4ReIrpe6+3r1K7oGfFLGCZSZh+1n72gbK1q/+C+AStgrZ7fw==;EndpointSuffix=core.windows.net +``` +--- +### 10.service bus +``` +Endpoint=sb://databus.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=+b7+0KMW1UQt5mbJEkA7uRxds4h0h4VNK+ASbOH5q3E= +``` +--- diff --git a/doc/start/gpthd.md b/doc/start/gpthd.md new file mode 100644 index 0000000..c6771b5 --- /dev/null +++ b/doc/start/gpthd.md @@ -0,0 +1,918 @@ +# SOC 项目后端功能方案(基于 LangChain,按功能拆解) + +> 约束:在未获得明确允许前,不修改前端交互,只补后端能力、编排链路和数据层。 +> 目标:严格围绕当前 `~/go/soc` 这个 Gemini 风格前端,按“每个功能用什么技术完成什么功能”来写,核心框架改为 **LangChain / LangGraph**,不再以 FastAPI 作为方案重点。 + +--- + +## 1. 对话流式回复 + +### 要完成什么功能 +- 用户在当前聊天输入框发送消息 +- 后端实时返回回答内容 +- 支持“思考中 / 检索中 / 生成中”的状态 +- 不改变现有前端交互,只替换当前前端 `simulateAIResponse()` + +### 用什么技术 +- **LangChain**:负责组织提示词、消息上下文、模型调用 +- **LangGraph**:负责整个对话节点编排与状态流转 +- **Azure OpenAI**:生成最终回答 +- **SSE**:把 LangChain/LangGraph 执行过程和回答流式推给前端 +- **PostgreSQL**:保存会话和消息记录 + +### 怎么落地 +- 以 `LangGraph StateGraph` 建立一个对话图: + - `receive_message` + - `load_history` + - `route_tools` + - `call_llm` + - `persist_message` +- 前端发送消息后,后端触发 graph 执行 +- Azure OpenAI 通过 LangChain chat model 调用 +- 生成的 token 和中间状态通过 SSE 返回给前端 + +### 输出结果 +- 前端仍然保持当前 Gemini 风格聊天交互 +- 从 mock 回复升级为真实流式 AI 回复 +- 后续所有工具调用都能接到同一个 graph 里 + +--- + +## 2. 会话管理 + +### 要完成什么功能 +- 左侧历史会话列表从真实数据读取 +- 支持新建、切换、删除会话 +- 支持自动生成标题 +- 会话上下文可在 LangChain 中继续使用 + +### 用什么技术 +- **PostgreSQL**:保存 conversation 和 message +- **LangChain Memory / Message History 抽象**:管理历史消息上下文 +- **SQLAlchemy / SQLModel**:管理数据表 + +### 怎么落地 +- conversations/messages 数据存入 PostgreSQL +- 在 LangChain 层使用 `BaseChatMessageHistory` 风格封装数据库消息 +- 每次进入 graph 时先加载历史消息 +- 标题生成可以由 LLM 在首轮消息后自动归纳 + +### 输出结果 +- 当前左侧 mock 会话可以替换为真实会话记录 +- 后端真正具备多轮上下文记忆能力 + +--- + +## 3. 内部知识库检索 + +### 要完成什么功能 +- 当用户问产品、方案、配置、内部文档时 +- 自动检索内部知识库 +- 再让模型基于检索结果生成回答 +- 回答中附带引用来源 + +### 用什么技术 +- **LangChain Tool**:把 KB_AGENT 封装成知识库工具 +- **KB_AGENT 接口**:作为实际搜索源 +- **Azure OpenAI**:总结检索结果并生成回答 +- **LangGraph**:决定何时调用知识库节点 + +### 怎么落地 +- 编写 `kb_search_tool` +- 工具内部调用 `KB_AGENT_URL + KB_AGENT_SEARCH_PATH` +- 返回统一的文档列表结构 +- graph 中当识别为内部知识类问题时,先进入 `kb_search` 节点,再进入 `llm_answer` + +### 输出结果 +- 产品知识问答不再靠模型空想 +- 回答可基于真实内部资料 +- 更适合售前、售后、研发支持 + +--- + +## 4. 外部 AI 搜索 + +### 要完成什么功能 +- 处理实时互联网问题、行业动态、外部资料调研 +- 支持搜索、网页读取、重排 +- 支持 fast / deep / auto 三种搜索质量 +- 后续支持图片和视频检索结果 + +### 用什么技术 +- **LangChain Tool**:封装外部搜索工具链 +- **Jina Search API**:外部搜索 +- **Jina Reader**:网页正文读取 +- **Rerank 模型**:重排结果 +- **LangGraph**:编排 Search -> Read -> Rerank -> Answer +- **Azure OpenAI**:生成最终总结回答 + +### 怎么落地 +- 分成三个 tool: + - `web_search_tool` + - `web_read_tool` + - `rerank_tool` +- 在 graph 中建立外部搜索链路: + - 搜索候选 + - 读取正文 + - 重排结果 + - 将高质量上下文交给 LLM +- `fast/deep/auto` 可作为 graph state 中的参数 + +### 输出结果 +- 外部信息回答准确度显著提升 +- 满足文档要求里的企业级外部搜索能力 +- 为后续图片、视频结果展示预留结构 + +--- + +## 5. 工单系统只读接入 + +### 要完成什么功能 +- 查询工单列表 +- 查询工单详情 +- 汇总 P0/P1 工单 +- 支持聊天中分析工单趋势、共性问题、故障重点 +- 替换当前前端 mock 工单摘要 + +### 用什么技术 +- **LangChain Tool**:把 gongdan API 封装为工单工具 +- **Gongdan HTTP API**:工单实际数据源 +- **Azure OpenAI**:对工单结果做总结和归纳 +- **PostgreSQL(可选缓存)**:保存查询结果和摘要缓存 + +### 怎么落地 +- 编写工具: + - `ticket_list_tool` + - `ticket_detail_tool` + - `ticket_summary_tool` +- 当用户问题涉及工单时,graph 路由到 ticket 节点 +- 工具取回结果后,再由 LLM 进行总结 +- 当前前端的 TicketSummary 数据以后改成读取真实接口结果,但不改交互样式 + +### 输出结果 +- 工单分析能力可直接在聊天里使用 +- 首页/聊天区的工单摘要可从 mock 变成真实数据 + +--- + +## 6. 文档生成 + +### 要完成什么功能 +- 用户要求生成方案、汇报、纪要、总结文档时 +- 后端自动进入文档生成流程 +- 返回任务状态和结果 +- 生成正式文档链接或结果卡片 + +### 用什么技术 +- **LangChain Tool / Runnable**:封装文档生成能力 +- **Doc Creator Agent HTTP API**:实际生成正式文档 +- **LangGraph**:把“文档生成”作为 graph 的分支节点 +- **PostgreSQL**:保存文档任务记录 +- **Azure OpenAI**:先整理文档结构或提纲 + +### 怎么落地 +- graph 中识别“生成文档”类意图 +- 先用 LLM 生成结构化文档提纲 +- 再调用 doc creator agent +- 把任务状态写入数据库 +- 前端依旧保持聊天式入口,只在消息中显示结果卡片 + +### 输出结果 +- 销售、售前、汇报场景可以直接从聊天进入正式文档输出 +- 文档生成成为对话系统中的标准能力节点 + +--- + +## 7. 沙盒代码执行 + +### 要完成什么功能 +- 分析 CSV、JSON、日志、结构化数据 +- 在安全环境中执行代码 +- 返回分析结果、图表和文件 +- 不改变前端交互,只把结果塞回当前聊天流里 + +### 用什么技术 +- **LangChain Tool**:把沙盒能力封装为可调用工具 +- **Daytona Sandbox**:安全执行环境 +- **Python 数据工具链**:pandas、matplotlib、json、csv +- **LangGraph**:按意图路由到 sandbox 节点 +- **PostgreSQL**:保存执行记录 + +### 怎么落地 +- 第一阶段不开放任意代码执行 +- 只先封装几个固定工具: + - `csv_summary_tool` + - `json_transform_tool` + - `data_analysis_tool` + - `chart_generate_tool` +- graph 根据问题和附件类型决定是否调用 sandbox + +### 输出结果 +- 后端不仅能“回答”,还能“执行”和“计算” +- 数据类问题能返回真正算出来的结果 + +--- + +## 8. 附件上传与解析 + +### 要完成什么功能 +- 接收用户上传的附件 +- 保存附件元数据 +- 提取文本内容进入上下文 +- 为知识问答、文档生成、沙盒分析提供输入 + +### 用什么技术 +- **对象存储/本地存储**:保存附件 +- **LangChain Document Loader**:解析 PDF、DOCX、TXT、CSV 等文件 +- **PostgreSQL**:保存附件元数据 +- **LangGraph**:把附件解析结果接入 graph state + +### 怎么落地 +- 上传后先保存附件和元数据 +- 再用 LangChain loader 抽取文本 +- 将解析结果挂到当前会话 state 中 +- 当用户继续提问时,graph 可以把附件内容作为上下文输入 LLM 或工具 + +### 输出结果 +- 未来可以真正支持“基于附件提问”和“基于附件分析” +- 为文档生成和沙盒执行提供输入材料 + +--- + +## 9. 工具编排层 + +### 要完成什么功能 +- 判断用户当前问题到底需要哪种能力 +- 决定先查 KB、先查工单、还是先查外部搜索 +- 决定是否触发文档生成或沙盒分析 +- 把多个工具结果统一整理给模型 + +### 用什么技术 +- **LangGraph**:整个系统的核心编排框架 +- **LangChain Tools**:封装 KB、Search、Tickets、Docs、Sandbox +- **Azure OpenAI**:辅助做意图判断、结果总结 + +### 怎么落地 +- graph 中至少有这些节点: + - `router` + - `kb_search` + - `web_search` + - `ticket_query` + - `doc_generate` + - `sandbox_run` + - `llm_answer` + - `persist` +- 第一阶段可以先“规则路由 + LLM总结” +- 第二阶段再升级为“LLM路由 + 工具调用决策” + +### 输出结果 +- 后端不再是散乱接口集合,而是统一 Agent 编排系统 +- 前端只保留一个 Gemini 风格聊天入口即可 + +--- + +## 10. 数据持久化与基础设施 + +### 要完成什么功能 +- 保存历史会话 +- 保存消息记录 +- 保存工具调用记录 +- 保存附件记录 +- 保存文档任务记录 +- 保存 graph 执行状态和日志 +- 提升缓存能力、异步任务能力和文件持久化能力 + +### 用什么技术 +- **PostgreSQL**:主数据库,保存会话、消息、任务、工具记录 +- **LangGraph Checkpointer / State Persistence**:保存 graph 执行状态 +- **Redis**:缓存热点结果、会话临时状态、短期上下文、速率控制 +- **Azure Storage Account**:保存附件、图表、导出文件、文档产物 +- **Azure Service Bus**:承载异步任务与解耦长链路处理 + +### 怎么落地 +- PostgreSQL 中至少建立以下表: + - `conversations` + - `messages` + - `tool_runs` + - `attachments` + - `document_tasks` + - `graph_runs` +- Redis 用于: + - 外部搜索结果缓存 + - KB 搜索缓存 + - 工单摘要缓存 + - 正在运行的 graph/session 临时状态 + - SSE 会话短状态同步 +- Azure Storage Account 用于: + - 用户上传附件原始文件 + - Sandbox 输出文件 + - 图表与中间产物 + - 文档生成结果文件 +- Azure Service Bus 用于: + - 文档生成异步任务派发 + - Sandbox 长任务调度 + - 外部搜索深度模式异步并发编排 + - 后续告警/通知类事件扩展 + +### 输出结果 +- 对话、工具、任务都有追踪记录 +- graph 执行链路具备可恢复能力 +- 系统具备缓存、异步任务和文件持久化基础设施 + +--- + +## 11. Redis 缓存层 + +### 要完成什么功能 +- 降低外部接口重复调用成本 +- 提升对话链路响应速度 +- 处理短期状态、热点数据和限流控制 + +### 用什么技术 +- **Azure Redis**:缓存层 +- **LangChain / LangGraph 外围状态管理**:结合缓存保存中间态 + +### 怎么落地 +- 缓存这些内容: + - 相同 query 的 KB 搜索结果 + - 相同 query 的外部搜索与重排结果 + - 工单摘要结果 + - 文档生成任务短状态 + - 会话级短期上下文摘要 +- 为外部搜索和知识库增加 TTL +- 为 Service Bus 异步任务增加状态缓存 + +### 输出结果 +- 系统速度更稳定 +- 外部服务成本更低 +- 可支撑更高并发下的会话请求 + +--- + +## 12. 存储账户(文件与产物存储) + +### 要完成什么功能 +- 持久化用户上传附件 +- 保存文档生成结果 +- 保存 Sandbox 执行生成的图表/文件 +- 为前端提供附件与结果文件访问地址 + +### 用什么技术 +- **Azure Blob Storage**:统一文件对象存储 +- **LangChain Document Loader**:结合存储文件做解析 + +### 怎么落地 +- 上传文件后先保存到 Blob Storage +- 数据库中记录 blob URL、文件类型、所属消息/会话 +- 文档生成与 Sandbox 产物统一落到 Blob Storage +- 前端保持现有交互,仅在消息中附带文件结果卡片或链接 + +### 输出结果 +- 所有附件和中间产物有统一落盘位置 +- 后续分析、下载、追踪都更方便 + +--- + +## 13. Service Bus 异步任务层 + +### 要完成什么功能 +- 处理长耗时任务 +- 解耦即时对话链路和后台异步处理链路 +- 支持重试、失败恢复、延后处理 + +### 用什么技术 +- **Azure Service Bus**:消息队列 / 异步任务总线 +- **LangGraph**:消费任务后继续执行长链路节点 + +### 怎么落地 +- 把这些任务异步化: + - 文档生成 + - Sandbox 长任务 + - 深度外部搜索 + - 未来的大批量分析任务 +- 聊天主链路先返回“任务已受理”状态 +- Worker 从 Service Bus 拉取任务继续执行 +- 执行结果写数据库和存储账户,再回推前端 + +### 输出结果 +- 避免主对话链路阻塞 +- 长任务处理更稳定 +- 适合企业级系统扩展 + +--- + +## 14. MCP 方式接入外部搜索 + +### 要完成什么功能 +- 利用 `https://mcp.jina.ai/sse` 这一类能力,以 MCP 方式接入外部搜索 +- 让外部搜索不只是普通 HTTP API,而是可作为标准工具节点接入 LangChain / LangGraph + +### 用什么技术 +- **MCP(Model Context Protocol)**:统一工具协议 +- **Jina MCP SSE / v1**:外部搜索与读取能力来源 +- **LangChain Tool 封装层**:把 MCP 调用转换成 graph 可调用工具 + +### 怎么落地 +- 优先测试 Jina 提供的 `/sse` 和 `/v1` 两种入口 +- 将 Search 和 Read 分别封装成两个 tool +- 在外部搜索节点中统一走 MCP 接入层,保留将来替换搜索供应商的可能 +- 重排仍保留单独节点,以便保障搜索质量控制 + +### 输出结果 +- 外部搜索链路更标准化 +- 更容易扩展到更多 MCP 服务 +- 对 LangChain / LangGraph 编排更友好 + +--- + +## 15. 基于当前前端代码补充的后端缺口与完善方案 + +> 这一章专门对应当前前端已经存在、但此前后端方案没有完整覆盖的功能点。不含认证和权限,只补业务后端能力。 + +### 15.1 消息反馈(赞 / 踩) + +#### 要完成什么 +- 用户对 assistant 消息进行点赞或点踩 +- 后端记录反馈结果 +- 后续可用于回答质量分析、提示词优化和问题回溯 + +#### 用什么技术 +- **PostgreSQL**:保存反馈记录 +- **LangGraph 旁路记录**:反馈不进入主对话 graph +- **Redis(可选)**:做短期统计缓存 + +#### 怎么落地 +- 新增表:`message_feedback` + - `id` + - `message_id` + - `conversation_id` + - `feedback_type` (`up` / `down`) + - `reason`(可空,后续扩展) + - `created_at` +- 新增接口: + - `POST /api/messages/{id}/feedback` +- 前端点击赞/踩后直接调用该接口 +- 第一阶段先只记录 `up/down`,不做复杂原因分类 + +--- + +### 15.2 模型切换映射 + +#### 要完成什么 +- 前端已有 Flash / Pro 与顶部模型选择入口 +- 第一阶段后端先统一固定使用 **GPT-5.4** +- 但保留字段和映射结构,后续再扩展多模型、多链路 + +#### 用什么技术 +- **LangChain model wrapper**:模型封装 +- **LangGraph state**:保存 `model_profile` +- **PostgreSQL conversation metadata**:记录选择结果 + +#### 怎么落地 +- 前端若传模型字段,第一阶段统一映射为: + - `model_provider = azure_openai` + - `model_name = gpt-5.4` +- 保留 metadata 字段: + - `selected_model` + - `selected_mode` +- 当前只做字段记录与透传,不做真正多模型切换 +- 第二阶段再扩为 flash/pro 对应不同 graph 策略 + +--- + +### 15.3 工具显式开关控制 + +#### 要完成什么 +- 前端工具 chips: + - 搜索 + - 内部知识库 + - 沙盒 + - 文档生成 +- 用户手动启用哪些工具,后端就只允许调用这些工具 +- 用户未选择时,后端才走自动路由 + +#### 用什么技术 +- **LangGraph state**:保存当前消息工具选择 +- **LangChain tools registry**:统一工具注册 +- **tool allowlist / denylist**:工具调用控制 + +#### 怎么落地 +- 前端发消息时附带: +```json +{ + "enabled_tools": ["search", "knowledge"] +} +``` +- graph state 增加: + - `enabled_tools` + - `tool_selection_mode` (`auto` / `manual`) +- router 节点规则: + - `manual` 模式:只能从 allowlist 中路由 + - `auto` 模式:按规则或模型自由决策 +- 工具执行前统一做可用性校验 + +--- + +### 15.4 多文件上传与消息绑定 + +#### 要完成什么 +- 一次上传多个文件 +- 每个文件单独保存 +- 文件和某条消息绑定 +- 文件可参与知识问答、搜索、Sandbox 分析和文档生成 + +#### 用什么技术 +- **Azure Blob Storage**:存储文件 +- **PostgreSQL**:存储附件元数据 +- **LangChain Document Loaders**:解析附件内容 +- **消息-附件关联机制**:支撑多文件场景 + +#### 怎么落地 +- 新增表:`attachments` + - `id` + - `conversation_id` + - `message_id`(允许先空,待消息发送后再绑定) + - `file_name` + - `content_type` + - `storage_url` + - `parse_status` + - `parsed_text` + - `created_at` +- 新增接口: + - `POST /api/attachments` + - `POST /api/messages/{id}/attachments/bind` +- 推荐流程: + 1. 前端先上传多个文件 + 2. 后端返回 attachment ids + 3. 前端发消息时附带 attachment ids + 4. 后端完成消息与附件绑定 +- 解析流程异步化,避免阻塞主聊天链路 + +--- + +### 15.5 扩展程序连接管理 + +#### 要完成什么 +- 支持扩展程序的连接、断开、修改 key、查看状态 +- 页面刷新后仍保留扩展连接状态 +- 扩展状态可被后端 graph 感知 + +#### 用什么技术 +- **PostgreSQL**:保存扩展配置与状态 +- **加密存储机制**:保存敏感配置 +- **extension registry**:统一扩展管理 +- **LangChain tool 注册机制**:根据扩展状态暴露工具 + +#### 怎么落地 +- 新增表:`extensions` + - `id` + - `extension_type` (`ticket` / `sales` / `cloud`) + - `display_name` + - `status` + - `config_encrypted` + - `last_check_at` + - `last_check_status` +- 新增接口: + - `GET /api/extensions` + - `POST /api/extensions/{type}/connect` + - `POST /api/extensions/{type}/disconnect` + - `POST /api/extensions/{type}/validate` +- 第一阶段先完成工单系统全链路,销售和云管先保留扩展框架 + +--- + +### 15.6 销售系统 / 云管系统预留 + +#### 要完成什么 +- 虽然当前两套系统还在开发,但后端要预留统一扩展接入结构 +- 避免未来工单、销售、云管三套系统接入方式不一致 + +#### 用什么技术 +- **统一 extension schema** +- **summary provider 接口** +- **tool provider 接口** +- **connection config schema** + +#### 怎么落地 +- 一期不要求真实接入销售/云管 API +- 但必须预留: + - 扩展类型定义 + - tool 注册入口 + - summary 注册入口 + - 状态位和配置结构 +- 后续新增业务系统时不需要推翻现有后端结构 + +--- + +### 15.7 通用扩展摘要机制 + +#### 要完成什么 +- 不只是工单系统,未来销售、云管系统接入后,也能输出首页/对话页摘要卡片 +- 后端统一提供摘要机制 + +#### 用什么技术 +- **summary provider registry**:每个扩展实现自己的摘要提供者 +- **Redis**:缓存摘要结果 +- **PostgreSQL**:记录摘要生成时间与状态 +- **LangChain summarizer(可选)**:对原始数据做摘要 + +#### 怎么落地 +- 新增统一摘要接口: + - `GET /api/extensions/summaries` +- 返回结构示例: +```json +[ + { + "extension_type": "ticket", + "status": "connected", + "summary_type": "ticket_summary", + "data": {} + } +] +``` +- 第一阶段先实现 ticket summary provider +- 但接口设计按多扩展统一返回 + +--- + +### 15.8 结构化消息块协议 + +#### 要完成什么 +- 后端不能只返回纯文本 +- 需要支持: + - 文本 + - 引用来源 + - 摘要卡片 + - 文件结果 + - 工具状态 + - 错误块 + +#### 用什么技术 +- **LangGraph 标准化事件输出** +- **message block schema** +- **前后端统一 JSON 协议** + +#### 怎么落地 +- 定义统一 block 结构: +```json +{ + "type": "text | citation | summary_card | artifact | tool_status | error", + "payload": {} +} +``` +- assistant message 最终存储结构: +```json +{ + "id": "...", + "blocks": [] +} +``` +- SSE 中间态也复用 block/event 体系 +- 第一阶段前端至少支持: + - `text` + - `tool_status` + - `citation` + - `summary_card` + +--- + +### 15.9 长任务状态回传 + +#### 要完成什么 +- 文档生成、Sandbox 数据分析、深度搜索等任务可能耗时较长 +- 前端需要看到任务状态,而不是一直假 loading + +#### 用什么技术 +- **Azure Service Bus**:异步任务投递 +- **PostgreSQL**:任务状态持久化 +- **Redis**:缓存短状态 +- **SSE / 轮询**:状态回传给前端 + +#### 怎么落地 +- 新增表:`async_tasks` + - `id` + - `task_type` + - `conversation_id` + - `message_id` + - `status` + - `progress_text` + - `result_payload` + - `created_at` + - `updated_at` +- 新增接口: + - `GET /api/tasks/{id}` +- 第一阶段先采用“数据库状态 + 前端轮询” +- 后续再增强为 SSE 任务事件推送 + +--- + +### 15.10 扩展连接状态注入 graph + +#### 要完成什么 +- 某个扩展是否已连接,必须直接决定 graph 中哪些工具可用 +- 未连接扩展不能被调用 +- 已连接扩展才能参与 agent 路由 + +#### 用什么技术 +- **extension registry** +- **LangGraph state injection** +- **tool availability resolver** + +#### 怎么落地 +- graph 执行前先加载当前扩展连接状态 +- 注入 state: +```json +{ + "available_extensions": ["ticket"] +} +``` +- router 节点判断: + - 工单问题 + ticket 已连接 -> 允许调用 + - 工单问题 + ticket 未连接 -> 返回“扩展未连接” +- 销售 / 云管未来直接复用该机制 + +--- + +### 15.11 会话重命名 / 置顶等预留 + +#### 要完成什么 +- 为左侧会话更多操作菜单预留后端能力 +- 支持未来扩展: + - 重命名 + - 置顶 + - 自定义排序 + +#### 用什么技术 +- **PostgreSQL conversation metadata** +- **排序字段 / pinned 字段** + +#### 怎么落地 +- conversations 表补充字段: + - `custom_title` + - `pinned` + - `sort_order` +- 接口统一走: + - `PATCH /api/conversations/{id}` +- 即使前端暂未开放置顶,也建议先预留字段 + +--- + +### 15.12 会话级偏好元数据 + +#### 要完成什么 +- 记录会话偏好信息,例如: + - 当前选中的模型 + - 当前启用工具 + - 默认搜索模式 + - 当前关联扩展 +- 会话恢复时自动延续这些设置 + +#### 用什么技术 +- **PostgreSQL JSON metadata** +- **LangGraph state hydration** + +#### 怎么落地 +- conversations 表增加: + - `metadata_json` +- 典型结构示例: +```json +{ + "selected_model": "gpt-5.4", + "selected_mode": "pro", + "enabled_tools": ["knowledge", "search"], + "preferred_search_mode": "deep" +} +``` +- 会话恢复时把 metadata 注入 graph 初始 state + +--- + +## 16. 接口层总表 + +> 虽然本方案不以 FastAPI 为重点,但前端要接入,仍然需要有 HTTP/SSE 出口。这里把它视为“接入层”,不是方案核心。 + +### 第一阶段建议建设的接口 + +#### 基础接口 +- `GET /health` + +#### 会话接口 +- `GET /api/conversations` +- `POST /api/conversations` +- `GET /api/conversations/{id}` +- `PATCH /api/conversations/{id}` +- `DELETE /api/conversations/{id}` + +#### 聊天接口 +- `POST /api/chat/stream` + +#### 工单接口 +- `GET /api/tickets/summary` +- `GET /api/tickets` +- `GET /api/tickets/{id}` + +#### 搜索接口 +- `POST /api/search/internal` +- `POST /api/search/external` + +#### 文档接口 +- `POST /api/documents/generate` +- `GET /api/documents/{task_id}` + +#### 附件接口 +- `POST /api/attachments` +- `GET /api/attachments/{id}` + +#### 沙盒接口 +- `POST /api/sandbox/run` + +--- + +## 16. 推荐技术组合总结 + +### 核心框架 +- **LangChain**:模型调用、Prompt 组织、Tool 封装、Memory 适配 +- **LangGraph**:对话状态机、工具路由、任务编排、长链路执行 + +### 数据层 +- **PostgreSQL**:会话/消息/工具调用/任务持久化 +- **Redis**:缓存、短状态、限流 +- **Azure Blob Storage**:附件、产物、文档存储 +- **Azure Service Bus**:异步任务编排 +- **SQLAlchemy / SQLModel**:ORM +- **Alembic**:迁移管理 + +### AI 与搜索 +- **Azure OpenAI**:LLM 生成与总结 +- **KB_AGENT**:内部知识库检索 +- **Jina MCP SSE / v1 + Search / Reader / Rerank**:外部搜索链路 + +### 外部业务系统 +- **Gongdan API**:工单只读 +- **Doc Creator Agent**:文档生成 +- **Daytona Sandbox**:受控代码执行 + +### 协议与接入 +- **SSE**:流式输出到前端 +- **HTTP API**:前端接入层 + +--- + +## 17. 第一阶段开发顺序 + +### 第一步 +先完成: +- LangChain + LangGraph 基础工程 +- PostgreSQL 接入 +- conversations/messages 表 +- 基础聊天 graph +- `/api/chat/stream` +- `/api/conversations` + +### 第二步 +接入: +- Azure OpenAI +- KB_AGENT tool +- 工单 tools + +### 第三步 +接入: +- Jina MCP SSE / v1 搜索链路 +- Search / Reader / Rerank tool chain +- 来源引用 +- graph 中间状态流式事件 +- Redis 缓存 + +### 第四步 +接入: +- 文档生成 tool +- 附件解析 loader +- sandbox tools +- Azure Blob Storage +- Azure Service Bus +- graph 持久化和恢复 + +--- + +## 18. 最终结论 + +这个项目当前最合适的后端方案,如果明确要求基于 LangChain 框架来做,那就应该是: + +- 用 **LangChain + LangGraph** 做整个后端核心 +- 用 **Azure OpenAI** 做模型生成和总结 +- 用 **KB_AGENT** 做内部知识检索工具 +- 用 **Jina Search/Reader/Rerank** 做外部搜索工具链 +- 用 **Gongdan API** 做工单查询工具 +- 用 **Doc Creator Agent** 做正式文档生成工具 +- 用 **Daytona Sandbox** 做受控执行工具 +- 用 **PostgreSQL** 做会话、消息、任务和 graph 状态持久化 +- 用 **Redis** 做缓存和短状态管理 +- 用 **Azure Blob Storage** 做附件与产物存储 +- 用 **Azure Service Bus** 做长任务异步编排 +- 用 **MCP 方式** 标准化接入 Jina 外部搜索 + +整个系统本质上是: +**一个基于 LangGraph 编排、具备缓存/存储/异步任务能力的企业级对话 Agent 后端。** + +而且整个过程中: +**前端交互不改,只替换数据来源和后端能力。** diff --git a/doc/start/test.md b/doc/start/test.md new file mode 100644 index 0000000..694c300 --- /dev/null +++ b/doc/start/test.md @@ -0,0 +1,61 @@ +# SOC End-to-End Test Reports + +--- + +## File Upload End-to-End Integration Test + +**Date**: 2026-04-08 +**Tester**: Claude Agent (Opus 4.6) +**Backend**: https://soc-backend.azurewebsites.net +**Frontend**: https://proud-pebble-04db8fd00.2.azurestaticapps.net + +### Pre-test Fixes Applied + +Before testing could succeed, three issues were identified and fixed: + +1. **`delete_attachment` Litestar startup crash** (`ImproperlyConfiguredException`) + - Cause: `@delete(..., status_code=204)` with `-> None` return type triggers Litestar validation error + - Fix: Added `return_dto=None` to the `@delete` decorator in `backend/app/api/attachments.py` + +2. **`create_tables` ExceptionGroup race condition** + - Cause: Two gunicorn workers call `CREATE TABLE` simultaneously; PostgreSQL raises `UniqueViolation`, but anyio wraps it in `ExceptionGroup` (a `BaseException` subclass) which bypasses `except Exception` + - Fix: Changed `except Exception` to `except BaseException` in `backend/app/store/postgres.py` + +3. **ForeignKeyViolationError on upload with non-existent conversation_id** + - Cause: `attachments.conversation_id` has a FK constraint to `conversations.id`; uploading with an arbitrary `conversation_id` that doesn't exist fails + - Status: Not a bug -- expected behavior. Tests adapted to create a conversation first or upload without `conversation_id` + +Commit: `48d7dd7` -- `fix: resolve attachment upload 500 errors (delete_attachment startup crash + create_tables ExceptionGroup)` + +### Test Results + +| # | Test Item | Method | Expected | Actual | Result | +|---|-----------|--------|----------|--------|--------| +| 1 | Health check | `GET /health` | 200 `{"status":"ok"}` | 200 `{"status":"ok"}` | **PASS** | +| 2 | Upload attachment (no conversation) | `POST /api/attachments/upload` multipart | 201 with `{id, filename, blob_url, size_bytes, content_type, created_at}` | 201 -- id=`7c0eba99`, filename=`CLAUDE.md`, blob_url=`https://authdatablol.blob.core.windows.net/soc-files/...`, size_bytes=4966, content_type=`text/markdown` | **PASS** | +| 2b | Upload attachment (with valid conversation_id) | `POST /api/attachments/upload?conversation_id={id}` | 201 with all fields + conversation_id set | 201 -- id=`a0dd9108`, conversation_id=`3d859ce5-...`, all fields present | **PASS** | +| 3 | Get attachment metadata | `GET /api/attachments/{id}` | 200 with attachment JSON | 200 -- all fields match upload response | **PASS** | +| 4 | Download attachment (SAS redirect) | `GET /api/attachments/{id}/download` | 302 with `Location` header containing SAS URL | 302 -- Location: `https://authdatablol.blob.core.windows.net/soc-files/...?se=...&sp=r&sv=...&sr=b&sig=...` | **PASS** | +| 5 | Delete attachment | `DELETE /api/attachments/{id}` | 204 No Content | 204 | **PASS** | +| 6 | Get after delete | `GET /api/attachments/{id}` | 404 | 404 `{"status_code":404,"detail":"Attachment ... not found"}` | **PASS** | +| 7 | Frontend homepage + upload button | `GET /` + source verification | 200 + upload UI code in source | 200 (25464 bytes) + `handleFileUpload`, `uploadAttachment`, file input in `GeminiInput.tsx` | **PASS** | +| 8 | SSE chat regression | `POST /api/chat/stream` | SSE tokens + done event | Received token events ("Hi! How can I help?") + `{"type":"done"}` | **PASS** | +| 9 | Tickets/summary regression | `GET /api/tickets/summary` | 200 with summary JSON | 200 `{"total":3,"by_status":{"pending":0,"processing":1,"resolved":2},"by_priority":{"P0":0,"P1":2,"P2":1,"P3":0}}` | **PASS** | + +### Summary + +**Result: 10/10 PASS** (counting 2 and 2b as separate items = 10 tests total) + +All file upload CRUD operations work correctly end-to-end: +- Upload to Azure Blob Storage succeeds (both with and without conversation_id) +- Metadata persisted in PostgreSQL and retrievable via GET +- Download generates a time-limited SAS URL and returns 302 redirect +- Delete removes both the blob and the database record +- Frontend has upload UI wired to the backend API +- SSE chat and tickets/summary remain functional (no regressions) + +### Note on `debug=True` + +The `debug=True` flag was left enabled in `backend/app/main.py` to aid ongoing development. This should be set back to `debug=False` before production hardening. + +--- diff --git a/doc/start/uigoto-run (1).md b/doc/start/uigoto-run (1).md new file mode 100644 index 0000000..5bd87c2 --- /dev/null +++ b/doc/start/uigoto-run (1).md @@ -0,0 +1,395 @@ +# langgraphjs-gen-ui-examples 实跑与代码核对记录 + +## 1. 这次我实际做了什么 + +这次不是只看 README,我实际做了下面这些事: + +1. clone 仓库 + - 路径:`/Users/gongzhiyong/go/langgraphjs-gen-ui-examples` +2. 安装依赖 + - 执行:`pnpm install` + - 已成功 +3. 读取关键配置与代码 + - `README.md` + - `package.json` + - `.env.example` + - `langgraph.json` + - `src/agent-uis/index.tsx` + - `src/agent-uis/writer/index.tsx` + - `src/agent/writer-agent/index.ts` + - `src/agent/open-code/index.ts` + - `src/agent/pizza-orderer/index.ts` +4. 核对该项目“交互到底由哪里承接” + +## 2. 这次我没有做到什么 + +我还没有把它完整跑到真实可交互演示画面,原因不是偷懒,而是这个仓库本身不是一个“装完直接开网页就能看完整交互”的纯前端 demo。 + +它依赖: +- LangGraph server +- 模型 API key(OpenAI / Google,部分示例还要 Anthropic) +- Agent Chat UI 这一套承接壳 + +所以它的“完整交互形态”不是只靠本仓库单独就能闭环展示的。 + +因此这次能确认的是: +- 代码结构 +- 交互承接机制 +- gen-ui 的真实工作方式 + +但不能假装说: +- 我已经把所有示例一条条真实点过并完整体验完 + +这点必须说明白。 + +--- + +## 3. 关键结论:这个仓库不是独立完整聊天产品,而是“LangGraph + UI 组件映射层” + +从 README 和代码看,`langgraphjs-gen-ui-examples` 的定位很明确: + +> 这是给 Agent Chat UI 使用的一组 LangGraph.js generative UI 示例。 + +也就是说,它不是: +- 一个完整成品聊天应用 + +它更像: +- 一组 graph +- 一组 UI component map +- 一套 graph 向 UI 推送结构化组件的示例实现 + +这点非常关键。 + +### 证据 1:README 直接写明 +README 原文核心意思是: +- `This repository contains a series of agents intended to be used with the Agent Chat UI` + +这已经说明: +- 真正承接聊天壳和交互主框架的是 Agent Chat UI +- 这个 repo 负责的是 agent + generative UI 示例 + +### 证据 2:`langgraph.json` +代码里: + +```json +{ + "graphs": { + "agent": "./src/agent/supervisor/index.ts:graph", + "email_agent": "./src/agent/email-agent/index.ts:agent", + "chat": "./src/agent/chat-agent/index.ts:agent" + }, + "ui": { + "agent": "./src/agent-uis/index.tsx" + } +} +``` + +这说明: +- graph 在后端/agent 侧 +- UI 映射入口在 `src/agent-uis/index.tsx` + +也就是它的核心不是“前端页面布局”,而是: +- graph 运行时能推送什么 UI +- UI 名字如何映射到 React 组件 + +--- + +## 4. 真实的 generative UI 工作方式 + +这是这次最重要的发现。 + +## 4.1 它不是 trace panel 升级版 +它不是: +- tool_start +- tool_end +- 然后前端自己把这些事件渲染成卡片 + +它更接近: +- graph 在执行过程中直接推一个“UI 组件实例” +- 前端按 name + props 渲染这个组件 + +## 4.2 `ComponentMap` 是关键 +`src/agent-uis/index.tsx`: + +```ts +const ComponentMap = { + "stock-price": StockPrice, + portfolio: PortfolioView, + "accommodations-list": AccommodationsList, + "restaurants-list": RestaurantsList, + "buy-stock": BuyStock, + "code-plan": Plan, + "proposed-change": ProposedChange, + writer: Writer, +} as const; +``` + +这个文件明确说明: +- gen-ui 的核心单位不是“通用 trace item” +- 而是“命名组件” +- graph 推什么组件名,前端就渲染什么组件 + +这和我前面只讲“timeline + result card”的说法相比,更接近真实代码。 + +也就是说,如果你要无限接近它: +- 不能只做通用 workspace + 通用卡片系统 +- 还要有“组件注册表 / 组件协议 / 组件 props 约定” + +--- + +## 5. Writer 示例说明了什么 + +`src/agent/writer-agent/index.ts` 和 `src/agent-uis/writer/index.tsx` 是最关键的例子。 + +### 5.1 后端/graph 侧是怎么做的 +writer graph 里用了: + +```ts +import { typedUi } from "@langchain/langgraph-sdk/react-ui/server"; +``` + +然后在 graph 执行过程中: + +```ts +ui.push( + { id, name: "writer", props: { ...tool, isGenerating: true } }, + { message, merge: true }, +); +``` + +后面内容流式生成时继续: + +```ts +ui.push( + { id, name: "writer", props: { content, isGenerating: true } }, + { message: lastMessage, merge: true }, +); +``` + +最后结束时: + +```ts +ui.push( + { id, name: "writer", props: { isGenerating: false } }, + { message: lastMessage, merge: true }, +); +``` + +这说明它的机制不是: +- 前端根据工具结果“猜”出该渲染什么 + +而是: +- graph 明确 push 一个叫 `writer` 的 UI 组件 +- 并且持续 merge 更新它的 props + +### 5.2 前端组件侧是怎么承接的 +`src/agent-uis/writer/index.tsx` 中: +- 组件会根据 `isGenerating` 显示生成中 +- 有 `Artifact` 侧边面板 +- 内容流式写进 textarea +- 生成过程中还能自动打开 artifact panel + +这个交互不是“消息上的 trace 面板”能替代的。 + +它本质上是: +- 聊天消息只是触发器/上下文 +- 真正的内容承接在独立 artifact/workspace + +这对 SOC 的启发非常大。 + +--- + +## 6. Pizza / Open Code / Email 示例分别说明什么 + +## 6.1 Pizza +README 和代码都说明: +- pizza 示例主要展示 tool call/result UI + +这说明它有一条路线是: +- 工具调用本身也能被 UI 组件化展示 + +## 6.2 Open Code +Open Code 是一个假的代码生成 agent,用来演示: +- plan +- proposed changes +- 审批/继续 +- 多步 UI 交互 + +这说明它不是只有“卡片展示”,还有: +- 多步状态机 UI +- 用户确认后继续 graph + +## 6.3 Email Agent +Email agent 用的是 interrupt / HumanInterrupt 标准 schema。 + +说明它还支持: +- graph 中断 +- 前端自动渲染 HITL UI +- 用户处理后恢复 graph + +这比“只展示 trace”又高了一个层级。 + +--- + +## 7. 对 SOC 的真实结论 + +这部分必须收得很实。 + +## 7.1 现在 SOC 最接近的不是它的“完整形态”,而是最外层轮廓 +SOC 现在已有: +- chat +- SSE token +- status +- tool trace +- message 绑定 + +这些只对应到它的最外层轮廓。 + +SOC 现在还没有真正拥有的,是下面这几层: + +### 第一层:UI 组件注册机制 +类似: +- `writer` +- `stock-price` +- `portfolio` +- `code-plan` +- `proposed-change` + +SOC 现在没有这种“组件名 -> React 组件”的标准化注册表。 + +### 第二层:后端主动 push UI 组件实例 +这个 repo 的关键能力是: +- graph 直接 `ui.push({ id, name, props })` + +SOC 当前没有这层。 +SOC 现在还是: +- 发 status/tool/token +- 前端自己猜怎么显示 + +这两者差异很大。 + +### 第三层:artifact/workspace 是一等公民 +writer 示例里,artifact 侧边面板是正式交互层。 +SOC 现在没有真正的一等 workspace / artifact 层。 + +### 第四层:interrupt / HITL / resume 机制 +SOC 目前也没有把这一层做成产品 UI。 + +--- + +## 8. 历史消息 gen-ui 持久化,为什么会变成大问题 + +现在可以更准确回答你前面那个问题。 + +因为在这个例子里,gen-ui 不是简单 trace,而是: +- 一个或多个具名 UI 组件实例 +- 每个实例都有 props +- props 还会增量更新 +- 还可能挂在某条 message 上 merge + +所以历史消息持久化时,最稳的不是只存 trace,而是要存: + +```json +{ + "message_id": "...", + "ui_instances": [ + { + "id": "...", + "name": "writer", + "props": { + "title": "...", + "content": "...", + "isGenerating": false + } + } + ] +} +``` + +也就是: +- 存“组件实例快照” +- 不只是存“工具事件日志” + +否则历史消息重开时,根本无法接近它那个交互。 + +--- + +## 9. 我现在对“能不能无限接近它”的新判断 + +在实际读过这些代码后,答案比之前更明确: + +### 9.1 如果 SOC 只是把现在的 trace panel 升级成右侧栏 +那*不能无限接近*。 + +因为这只是在 UI 布局层面模仿。 +没有触及它真正的核心机制: +- 后端推具名 UI 组件 +- 前端组件注册映射 +- artifact / workspace 一等化 +- message 级别的 UI merge + +### 9.2 如果 SOC 允许新增一层“GenUI 协议 + 组件注册 + workspace 持久化” +那*可以高相似度接近*。 + +但前提不是“小修小补”,而是要补以下能力: + +1. 后端 UI 事件协议 +2. 前端 component registry +3. workspace / artifact 容器 +4. message -> ui instance 绑定 +5. 历史会话快照恢复 + +### 9.3 所以结论不能再说虚的 +最准确的话是: + +- *单靠当前那套 status + trace + answer 方案,不能无限接近 `langgraphjs-gen-ui-examples`* +- *如果把协议层升级为“具名 UI 组件实例流”,再加 workspace 持久化,才有资格说高相似度接近* + +--- + +## 10. 对我前面方案的修正 + +我前面那版 `uigoto.md` 有一个本质问题: + +我把目标抽象成了: +- timeline +- result cards +- workspace + +这没错,但还不够贴这次真实读到的代码。 + +缺的关键一层是: + +> gen-ui 不是“结果卡片集合”这么简单,而是“后端驱动的具名 UI 组件实例系统”。 + +这会直接影响: +- 前后端协议怎么设计 +- 持久化怎么做 +- 组件如何注册 +- 历史消息如何恢复 +- 后续交互复杂度能否提升到 interrupt / HITL / artifact 级别 + +所以如果接下来重写 SOC 方案,必须把这一层补进去。 + +--- + +## 11. 当前可以下的最硬结论 + +### 结论 1 +`langgraphjs-gen-ui-examples` 不是单纯展示 trace 的项目,它的核心是: +- graph 执行时直接推 UI 组件实例 +- 前端按组件注册表渲染 + +### 结论 2 +它的完整交互壳很大程度依赖 Agent Chat UI,不是这个 repo 自己单独包办全部页面壳。 + +### 结论 3 +SOC 如果想无限接近它,不能只升级 trace panel,必须升级成: +- UI 组件协议 +- component registry +- artifact/workspace +- message 绑定持久化 + +### 结论 4 +历史消息 gen-ui 持久化,最稳方案不是只存 trace,而是存“message 级 UI 组件实例快照”。 + diff --git a/doc/start/uigoto-run.md b/doc/start/uigoto-run.md new file mode 100644 index 0000000..5bd87c2 --- /dev/null +++ b/doc/start/uigoto-run.md @@ -0,0 +1,395 @@ +# langgraphjs-gen-ui-examples 实跑与代码核对记录 + +## 1. 这次我实际做了什么 + +这次不是只看 README,我实际做了下面这些事: + +1. clone 仓库 + - 路径:`/Users/gongzhiyong/go/langgraphjs-gen-ui-examples` +2. 安装依赖 + - 执行:`pnpm install` + - 已成功 +3. 读取关键配置与代码 + - `README.md` + - `package.json` + - `.env.example` + - `langgraph.json` + - `src/agent-uis/index.tsx` + - `src/agent-uis/writer/index.tsx` + - `src/agent/writer-agent/index.ts` + - `src/agent/open-code/index.ts` + - `src/agent/pizza-orderer/index.ts` +4. 核对该项目“交互到底由哪里承接” + +## 2. 这次我没有做到什么 + +我还没有把它完整跑到真实可交互演示画面,原因不是偷懒,而是这个仓库本身不是一个“装完直接开网页就能看完整交互”的纯前端 demo。 + +它依赖: +- LangGraph server +- 模型 API key(OpenAI / Google,部分示例还要 Anthropic) +- Agent Chat UI 这一套承接壳 + +所以它的“完整交互形态”不是只靠本仓库单独就能闭环展示的。 + +因此这次能确认的是: +- 代码结构 +- 交互承接机制 +- gen-ui 的真实工作方式 + +但不能假装说: +- 我已经把所有示例一条条真实点过并完整体验完 + +这点必须说明白。 + +--- + +## 3. 关键结论:这个仓库不是独立完整聊天产品,而是“LangGraph + UI 组件映射层” + +从 README 和代码看,`langgraphjs-gen-ui-examples` 的定位很明确: + +> 这是给 Agent Chat UI 使用的一组 LangGraph.js generative UI 示例。 + +也就是说,它不是: +- 一个完整成品聊天应用 + +它更像: +- 一组 graph +- 一组 UI component map +- 一套 graph 向 UI 推送结构化组件的示例实现 + +这点非常关键。 + +### 证据 1:README 直接写明 +README 原文核心意思是: +- `This repository contains a series of agents intended to be used with the Agent Chat UI` + +这已经说明: +- 真正承接聊天壳和交互主框架的是 Agent Chat UI +- 这个 repo 负责的是 agent + generative UI 示例 + +### 证据 2:`langgraph.json` +代码里: + +```json +{ + "graphs": { + "agent": "./src/agent/supervisor/index.ts:graph", + "email_agent": "./src/agent/email-agent/index.ts:agent", + "chat": "./src/agent/chat-agent/index.ts:agent" + }, + "ui": { + "agent": "./src/agent-uis/index.tsx" + } +} +``` + +这说明: +- graph 在后端/agent 侧 +- UI 映射入口在 `src/agent-uis/index.tsx` + +也就是它的核心不是“前端页面布局”,而是: +- graph 运行时能推送什么 UI +- UI 名字如何映射到 React 组件 + +--- + +## 4. 真实的 generative UI 工作方式 + +这是这次最重要的发现。 + +## 4.1 它不是 trace panel 升级版 +它不是: +- tool_start +- tool_end +- 然后前端自己把这些事件渲染成卡片 + +它更接近: +- graph 在执行过程中直接推一个“UI 组件实例” +- 前端按 name + props 渲染这个组件 + +## 4.2 `ComponentMap` 是关键 +`src/agent-uis/index.tsx`: + +```ts +const ComponentMap = { + "stock-price": StockPrice, + portfolio: PortfolioView, + "accommodations-list": AccommodationsList, + "restaurants-list": RestaurantsList, + "buy-stock": BuyStock, + "code-plan": Plan, + "proposed-change": ProposedChange, + writer: Writer, +} as const; +``` + +这个文件明确说明: +- gen-ui 的核心单位不是“通用 trace item” +- 而是“命名组件” +- graph 推什么组件名,前端就渲染什么组件 + +这和我前面只讲“timeline + result card”的说法相比,更接近真实代码。 + +也就是说,如果你要无限接近它: +- 不能只做通用 workspace + 通用卡片系统 +- 还要有“组件注册表 / 组件协议 / 组件 props 约定” + +--- + +## 5. Writer 示例说明了什么 + +`src/agent/writer-agent/index.ts` 和 `src/agent-uis/writer/index.tsx` 是最关键的例子。 + +### 5.1 后端/graph 侧是怎么做的 +writer graph 里用了: + +```ts +import { typedUi } from "@langchain/langgraph-sdk/react-ui/server"; +``` + +然后在 graph 执行过程中: + +```ts +ui.push( + { id, name: "writer", props: { ...tool, isGenerating: true } }, + { message, merge: true }, +); +``` + +后面内容流式生成时继续: + +```ts +ui.push( + { id, name: "writer", props: { content, isGenerating: true } }, + { message: lastMessage, merge: true }, +); +``` + +最后结束时: + +```ts +ui.push( + { id, name: "writer", props: { isGenerating: false } }, + { message: lastMessage, merge: true }, +); +``` + +这说明它的机制不是: +- 前端根据工具结果“猜”出该渲染什么 + +而是: +- graph 明确 push 一个叫 `writer` 的 UI 组件 +- 并且持续 merge 更新它的 props + +### 5.2 前端组件侧是怎么承接的 +`src/agent-uis/writer/index.tsx` 中: +- 组件会根据 `isGenerating` 显示生成中 +- 有 `Artifact` 侧边面板 +- 内容流式写进 textarea +- 生成过程中还能自动打开 artifact panel + +这个交互不是“消息上的 trace 面板”能替代的。 + +它本质上是: +- 聊天消息只是触发器/上下文 +- 真正的内容承接在独立 artifact/workspace + +这对 SOC 的启发非常大。 + +--- + +## 6. Pizza / Open Code / Email 示例分别说明什么 + +## 6.1 Pizza +README 和代码都说明: +- pizza 示例主要展示 tool call/result UI + +这说明它有一条路线是: +- 工具调用本身也能被 UI 组件化展示 + +## 6.2 Open Code +Open Code 是一个假的代码生成 agent,用来演示: +- plan +- proposed changes +- 审批/继续 +- 多步 UI 交互 + +这说明它不是只有“卡片展示”,还有: +- 多步状态机 UI +- 用户确认后继续 graph + +## 6.3 Email Agent +Email agent 用的是 interrupt / HumanInterrupt 标准 schema。 + +说明它还支持: +- graph 中断 +- 前端自动渲染 HITL UI +- 用户处理后恢复 graph + +这比“只展示 trace”又高了一个层级。 + +--- + +## 7. 对 SOC 的真实结论 + +这部分必须收得很实。 + +## 7.1 现在 SOC 最接近的不是它的“完整形态”,而是最外层轮廓 +SOC 现在已有: +- chat +- SSE token +- status +- tool trace +- message 绑定 + +这些只对应到它的最外层轮廓。 + +SOC 现在还没有真正拥有的,是下面这几层: + +### 第一层:UI 组件注册机制 +类似: +- `writer` +- `stock-price` +- `portfolio` +- `code-plan` +- `proposed-change` + +SOC 现在没有这种“组件名 -> React 组件”的标准化注册表。 + +### 第二层:后端主动 push UI 组件实例 +这个 repo 的关键能力是: +- graph 直接 `ui.push({ id, name, props })` + +SOC 当前没有这层。 +SOC 现在还是: +- 发 status/tool/token +- 前端自己猜怎么显示 + +这两者差异很大。 + +### 第三层:artifact/workspace 是一等公民 +writer 示例里,artifact 侧边面板是正式交互层。 +SOC 现在没有真正的一等 workspace / artifact 层。 + +### 第四层:interrupt / HITL / resume 机制 +SOC 目前也没有把这一层做成产品 UI。 + +--- + +## 8. 历史消息 gen-ui 持久化,为什么会变成大问题 + +现在可以更准确回答你前面那个问题。 + +因为在这个例子里,gen-ui 不是简单 trace,而是: +- 一个或多个具名 UI 组件实例 +- 每个实例都有 props +- props 还会增量更新 +- 还可能挂在某条 message 上 merge + +所以历史消息持久化时,最稳的不是只存 trace,而是要存: + +```json +{ + "message_id": "...", + "ui_instances": [ + { + "id": "...", + "name": "writer", + "props": { + "title": "...", + "content": "...", + "isGenerating": false + } + } + ] +} +``` + +也就是: +- 存“组件实例快照” +- 不只是存“工具事件日志” + +否则历史消息重开时,根本无法接近它那个交互。 + +--- + +## 9. 我现在对“能不能无限接近它”的新判断 + +在实际读过这些代码后,答案比之前更明确: + +### 9.1 如果 SOC 只是把现在的 trace panel 升级成右侧栏 +那*不能无限接近*。 + +因为这只是在 UI 布局层面模仿。 +没有触及它真正的核心机制: +- 后端推具名 UI 组件 +- 前端组件注册映射 +- artifact / workspace 一等化 +- message 级别的 UI merge + +### 9.2 如果 SOC 允许新增一层“GenUI 协议 + 组件注册 + workspace 持久化” +那*可以高相似度接近*。 + +但前提不是“小修小补”,而是要补以下能力: + +1. 后端 UI 事件协议 +2. 前端 component registry +3. workspace / artifact 容器 +4. message -> ui instance 绑定 +5. 历史会话快照恢复 + +### 9.3 所以结论不能再说虚的 +最准确的话是: + +- *单靠当前那套 status + trace + answer 方案,不能无限接近 `langgraphjs-gen-ui-examples`* +- *如果把协议层升级为“具名 UI 组件实例流”,再加 workspace 持久化,才有资格说高相似度接近* + +--- + +## 10. 对我前面方案的修正 + +我前面那版 `uigoto.md` 有一个本质问题: + +我把目标抽象成了: +- timeline +- result cards +- workspace + +这没错,但还不够贴这次真实读到的代码。 + +缺的关键一层是: + +> gen-ui 不是“结果卡片集合”这么简单,而是“后端驱动的具名 UI 组件实例系统”。 + +这会直接影响: +- 前后端协议怎么设计 +- 持久化怎么做 +- 组件如何注册 +- 历史消息如何恢复 +- 后续交互复杂度能否提升到 interrupt / HITL / artifact 级别 + +所以如果接下来重写 SOC 方案,必须把这一层补进去。 + +--- + +## 11. 当前可以下的最硬结论 + +### 结论 1 +`langgraphjs-gen-ui-examples` 不是单纯展示 trace 的项目,它的核心是: +- graph 执行时直接推 UI 组件实例 +- 前端按组件注册表渲染 + +### 结论 2 +它的完整交互壳很大程度依赖 Agent Chat UI,不是这个 repo 自己单独包办全部页面壳。 + +### 结论 3 +SOC 如果想无限接近它,不能只升级 trace panel,必须升级成: +- UI 组件协议 +- component registry +- artifact/workspace +- message 绑定持久化 + +### 结论 4 +历史消息 gen-ui 持久化,最稳方案不是只存 trace,而是存“message 级 UI 组件实例快照”。 + diff --git a/doc/start/uigoto.md b/doc/start/uigoto.md new file mode 100644 index 0000000..ede9e87 --- /dev/null +++ b/doc/start/uigoto.md @@ -0,0 +1,715 @@ +# SOC Generative UI 产品方案(uigoto) + +## 1. 目标结论 + +本方案不再把当前前端的 `trace panel` 视为最终形态,而是将其定义为过渡能力。 + +本阶段产品目标明确为: + +- 基于当前 SOC 前端代码继续演进 +- 不切换主工程,不直接迁移到 `langgraphjs-gen-ui-examples` +- 以现有 `GeminiChat` / `GeminiMessage` / `TracePanel` / `api.ts` 为基础重构交互 +- 目标交互不是“消息下方的工具折叠区” +- 而是“聊天主线程 + agent 活动层 + 结构化 UI 承接区” +- 最终要求:在当前项目约束下,无限接近 `langgraphjs-gen-ui-examples` 的交互体验 + +一句话定义: + +> 当前项目从 `chat + trace` 升级为 `chat + activity + generative UI workspace`。 + +--- + +## 2. 基于当前代码的现状判断 + +当前前端已经具备以下基础: + +### 2.1 已有能力 + +- 页面入口极简,`frontend/app/page.tsx` 直接挂 `GeminiChat` +- `GeminiChat.tsx` 已经是主控容器: + - 负责 sidebar / topbar / message list / input / extensions panel + - 已接入流式聊天 `streamChat(...)` + - 已接收 `status / tool_start / tool_end / tool_error / token` +- `GeminiMessage.tsx` 已支持: + - assistant message 渲染 + - `traceItems` 绑定到单条 assistant message + - `TracePanel` 插入正文前 +- `TracePanel.tsx` 已支持: + - 工具摘要 + - 工具列表展开 + - 成功/失败/运行中状态 +- `api.ts` 已有前后端流协议类型: + - `ChatStreamEvent` + - `TraceItem` +- 页面整体骨架已经是聊天产品,不需要推倒重做 + +### 2.2 当前不足 + +当前代码的交互层级仍然偏低,主要问题如下: + +#### 问题 A:trace 仍然附属于 message,而不是独立 UI 层 +当前 `TracePanel` 只是 assistant message 上方的一块折叠区域。 + +这意味着: +- tool activity 只是“消息补充信息” +- 不是可持续存在的 agent workspace +- 用户注意力仍然集中在文本回复,不是结构化交互本身 + +#### 问题 B:status 虽然收到了,但没有成为真正可见 UI +当前 `GeminiChat.tsx` 接了 `status` 事件,但 `TracePanel.tsx` 会过滤掉非 tool 项。 + +这意味着: +- 后端的 status 没有变成产品层交互 +- 用户看不到 agent 当前处于哪个阶段 +- 还没有形成真正的 activity timeline + +#### 问题 C:没有独立的 generative UI 承接区 +当前页面结构只有: +- 左侧 sidebar +- 中间 chat +- 扩展面板 `ExtensionsPanel` + +没有一个与 agent 执行结果强绑定的右侧 workspace / side panel / dynamic card panel。 + +这和 `langgraphjs-gen-ui-examples` 的交互差距最大。 + +#### 问题 D:tool 结果还是“摘要文本”,不是“组件状态” +当前 tool_end/tool_error 最终只是写入: +- `outputSummary` +- `errorSummary` + +也就是说: +- tool 结果只被当成 message 附属文本 +- 不能驱动卡片、详情面板、结果区块、交互组件 + +#### 问题 E:页面是“消息流 UI”,不是“agent 工作台 UI” +现在用户是在看聊天。 +不是在看一个 agent 正在构造、更新、切换多个结果视图。 + +而用户现在明确偏好的是后者。 + +--- + +## 3. 产品目标:必须完成的范围 + +下面是本轮必须完成的范围。这个范围一旦确认,就不能做成“弱化版”。 + +## 3.1 最终交互目标 + +必须完成的目标交互如下: + +### 目标 1:对话区仍保留,但不再是唯一主角 +中间聊天区仍然保留,用于: +- 展示用户输入 +- 展示 assistant 文本回答 +- 展示简洁的 agent 活动摘要 + +但是: +- 详细工具过程 +- 结构化结果 +- 中间态 UI +- 后续可点击视图 + +都不能只塞在 message 里。 + +### 目标 2:新增独立的 Agent Workspace 区域 +必须新增一个独立 UI 区域,位置优先级如下: + +- 首选:右侧固定 workspace panel +- 次选:中间 chat 区下方的持续存在 workspace +- 不接受:仍然只挂在 message 气泡里作为折叠面板 + +这个 workspace 的职责是: +- 承接 agent 的结构化执行过程 +- 承接 tool 返回的可视结果 +- 在同一轮对话中持续更新 +- 在回答结束后保留结果视图 + +### 目标 3:status 必须成为第一层可见交互 +status 不能再只存在于后端事件和前端状态变量中。 + +必须在 UI 里直接可见。 + +必须具备以下阶段表达: +- 已接收问题 +- 正在分析 +- 正在调用某个工具 +- 正在汇总结果 +- 已完成 +- 失败 / 中断 + +要求: +- 用户在不展开任何调试视图的前提下,也能看到 agent 当前阶段 +- status 不得埋在细节抽屉里 + +### 目标 4:tool 调用必须既有摘要,也有实体卡片 +每个重要工具调用,不仅要有 timeline 行,还要能够驱动结果卡片。 + +例如: +- 知识库检索 -> “知识卡片 / 命中条目列表” +- web 搜索 -> “来源列表卡片” +- ticket 工具 -> “工单摘要卡片 / 工单详情卡片” +- 文档生成 -> “文档卡片 / 下载入口” +- sandbox -> “执行结果卡片 / 输出块” + +也就是说: +- tool trace 是“过程层” +- tool result card 是“结果层” +- 两层必须同时存在 + +### 目标 5:每一轮回答要形成完整闭环 +每次一次完整请求,前端必须形成一轮完整可见闭环: + +`user message -> status -> tool activity -> structured result cards -> assistant answer -> completed state` + +不能只剩下: +- 有 token +- 有 trace +- 但没有结果区承接 + +### 目标 6:结果区要持续存在,不随消息滚动立即消失 +用户喜欢的交互,本质上不是“看完就过去”,而是 agent 在右侧/固定区域留下工作结果。 + +因此必须做到: +- 当前轮次结果在回答结束后仍然可见 +- 点击历史消息时,可以重新激活该轮对应 workspace +- workspace 与 conversation / message 建立明确绑定 + +--- + +## 4. 本轮不允许模糊的实现边界 + +为了保证“必须完成”,这里明确哪些属于本轮范围,哪些不属于。 + +## 4.1 本轮必须完成 + +### A. 页面布局升级 +必须把当前页面从: +- sidebar + main chat + extensions panel + +升级为: +- sidebar + chat main + agent workspace panel + +要求: +- `ExtensionsPanel` 不再承担 generative UI 主职责 +- workspace 是主产品结构,不是弹窗附属物 + +### B. activity timeline 升级 +必须把当前 `TracePanel` 升级为真正的 `ActivityPanel`: +- 支持 status 节点 +- 支持 tool 节点 +- 支持完成/失败节点 +- 支持当前轮次高亮 +- 支持点击节点联动结果卡片 + +### C. result cards 体系 +必须定义并实现第一批可交付卡片类型。 +本轮最少要完成以下卡片: + +- SearchResultCard +- KnowledgeResultCard +- TicketSummaryCard +- TicketDetailCard +- DocumentResultCard +- SandboxResultCard +- ErrorCard +- EmptyStateCard + +这些卡片不要求一次做到极复杂,但必须是“真实组件”,不是把 JSON dump 出来。 + +### D. chat 与 workspace 联动 +必须实现: +- 某条 assistant message 对应一个 workspace session +- 点击该 message,可重新展示它那一轮的 activity + result cards +- 正在生成时,workspace 实时更新 +- 完成后,workspace 固化为当前轮结果 + +### E. 事件协议升级 +必须在当前 SSE 事件协议上增加一层 UI 事件语义。 + +后端不一定一开始就一次发完整 UI schema,但前端必须按这个目标设计: +- activity 事件 +- result card 事件 +- workspace patch 事件 + +即使第一版是由前端根据 tool_end 映射卡片,也必须保留后续升级为后端直接下发 UI schema 的位置。 + +## 4.2 本轮明确不做 + +为了保证“无限接近目标交互”而不是分散精力,本轮先不做: + +- 多标签 workspace 管理 +- 拖拽式布局编辑 +- 用户自定义卡片布局 +- 通用低代码 schema 编辑器 +- 全量历史 workspace 持久化版本管理 +- 复杂多人协作 UI + +这些都不是当前用户最核心的“像那个项目的交互”诉求。 + +--- + +## 5. 交互方案:必须实现到的细节 + +这是本方案最核心部分,重点是交互细节,不允许只停留在“有个右侧栏”。 + +## 5.1 总体布局 + +页面结构必须调整为三栏心智模型: + +### 左栏:Conversation / Navigation +保留现有 `GeminiSidebar`,只做轻量微调。 + +职责不变: +- 新建会话 +- 会话切换 +- 会话管理 +- 扩展入口 + +### 中栏:Chat Thread +保留聊天主线程,但要减轻其“承载所有信息”的职责。 + +中栏只负责: +- 用户消息 +- assistant 文本输出 +- 精炼版 activity 摘要 +- 当前轮生成状态 + +中栏不再承担: +- 大量工具明细 +- 详细结构化结果 +- 复杂工具结果交互 + +### 右栏:Agent Workspace(本轮核心) +新增固定 workspace,默认始终可见。 + +必须具备: +- 标题区 +- 当前阶段状态区 +- activity timeline +- 结果卡片区 +- 空状态 / 错误状态 / 完成状态 + +建议结构: + +1. Header + - 当前轮标题 + - 当前状态 badge + - collapse / expand 能力(可选) +2. Activity 区 + - 展示 status + tool 节点 +3. Result 区 + - 展示当前轮生成的 card stack +4. Footer / meta 区 + - 时间、完成状态、重试入口(可选) + +--- + +## 5.2 Chat 区具体交互要求 + +### 交互要求 1:assistant message 上方只保留轻摘要 +当前 `TracePanel` 的详单不应该继续作为主交互。 + +改造后: +- message 上方只保留一行 activity summary +- 文案例如: + - 正在分析问题 + - 已查询知识库与外部信息 + - 已生成工单摘要 + - 已完成文档生成 + +不再默认把完整工具列表塞进 message 气泡区域。 + +### 交互要求 2:点击 assistant message 可聚焦右侧 workspace +每条 assistant message 都必须可触发右侧 workspace 聚焦。 + +行为定义: +- hover 时出现“查看工作区”提示或高亮 +- 点击 assistant message,右侧显示它对应那一轮的 workspace +- 当前活跃 message 在聊天流中有弱高亮 + +### 交互要求 3:生成期间聊天区与 workspace 同步流动 +生成中: +- 中间 message 逐步吐文本 +- 右侧 activity / cards 同时更新 + +用户应感知到: +- 不是“等回答完再展示工具过程” +- 而是“回答和工作区同步生长” + +--- + +## 5.3 Workspace 区具体交互要求 + +### 交互要求 4:workspace 默认固定显示,不是弹窗 +这是硬约束。 + +不接受: +- 抽屉点击后才看见 +- modal 弹窗式承接 +- 工具面板必须展开才看到 + +必须默认可见。 + +### 交互要求 5:workspace 有明确状态头部 +顶部必须显示: +- 当前轮标题(自动从用户问题摘要生成) +- 当前状态 badge +- 当前阶段短文案 + +例如: +- 分析中 +- 调用知识库 +- 汇总工单结果 +- 生成完成 +- 执行失败 + +### 交互要求 6:ActivityTimeline 必须是真正的时间线 +ActivityTimeline 不是现在 `TracePanel` 那种折叠列表。 + +必须具备: +- 顺序节点 +- 节点状态图标 +- 当前节点高亮 +- 完成节点保持可见 +- 错误节点可单独标红 +- 点击节点可联动结果区滚动/高亮 + +节点类型至少包括: +- status node +- tool node +- done node +- error node + +### 交互要求 7:结果卡片区必须有“主卡片”概念 +不是简单 list。 + +要求: +- 最新 / 当前最关键结果卡片优先显示在上方 +- 次级结果按时间或重要性排列 +- 当前高亮的 activity 节点对应的 card 自动高亮 + +### 交互要求 8:无结果时不能空白 +在 agent 正在运行但还没产出 card 时,workspace 结果区必须有占位状态: +- 正在准备结果… +- 正在等待工具返回… +- 暂无结构化结果 + +不能出现大面积空白让用户以为没工作。 + +### 交互要求 9:回答完成后 workspace 要进入“已完成态” +回答完成后: +- timeline 固化 +- 最终状态变为 completed +- 结果卡片保留 +- message summary 从“进行中”切换为“已完成摘要” + +--- + +## 5.4 卡片层交互要求 + +### 交互要求 10:卡片必须像产品组件,不像调试面板 +卡片 UI 原则: +- 标题明确 +- 信息分组清楚 +- 可扫读 +- 有主次层级 +- 不直接暴露原始 JSON + +### 交互要求 11:卡片内容要有“用户可读摘要 + 结构化字段”双层 +例如搜索卡片: +- 顶部:找到 5 条高相关结果 +- 下方:来源列表、标题、摘要、链接 + +例如 ticket 卡片: +- 顶部:共 12 条工单,P0 2 条 +- 下方:按状态/优先级分组 + +### 交互要求 12:失败也要有失败卡片 +如果工具失败: +- timeline 有错误节点 +- 结果区同步出现 ErrorCard +- assistant 文本可继续回答,但 workspace 必须保留失败证据 + +### 交互要求 13:同一轮允许多卡片叠加 +例如一次请求可能触发: +- KnowledgeResultCard +- SearchResultCard +- TicketSummaryCard +- FinalDocumentCard + +这些都必须能在一轮 workspace 中共存。 + +--- + +## 6. 信息架构与前端状态模型 + +为了避免方案空泛,这里直接给前端目标状态模型。 + +## 6.1 当前 message 模型的问题 +当前 `Message` 结构大致是: +- id +- role +- content +- attachments +- traceItems + +这不够支撑 generative UI。 + +## 6.2 目标模型 +前端状态必须增加“workspace session”概念。 + +建议新增: + +```ts +interface WorkspaceSession { + id: string; + conversationId: string; + messageId: string; + title: string; + status: "idle" | "running" | "completed" | "error"; + stageLabel: string; + timeline: ActivityNode[]; + cards: WorkspaceCard[]; + startedAt: number; + finishedAt?: number; +} +``` + +```ts +interface ActivityNode { + id: string; + type: "status" | "tool" | "done" | "error"; + label: string; + detail?: string; + tool?: string; + callId?: string; + status: "running" | "success" | "error" | "info"; + ts: number; + linkedCardIds?: string[]; +} +``` + +```ts +interface WorkspaceCard { + id: string; + kind: + | "search-results" + | "knowledge-results" + | "ticket-summary" + | "ticket-detail" + | "document-result" + | "sandbox-result" + | "error" + | "empty"; + title: string; + priority: number; + data: Record; + sourceCallId?: string; +} +``` + +## 6.3 关键绑定关系 +必须建立以下绑定: + +- `assistant message` -> `workspaceSession` +- `timeline node` -> `workspace card` +- `tool call_id` -> `activity node` -> `card sourceCallId` + +这一步是整个交互能否稳住的关键。 + +--- + +## 7. 事件协议升级要求 + +## 7.1 当前协议可复用部分 +当前已有: +- token +- status +- tool_start +- tool_end +- tool_error +- done +- error + +这个基础可以继续用。 + +## 7.2 必须补的语义层 +虽然第一版可以前端自行从 tool_end 推导卡片,但协议设计必须预留为以下方向: + +### 事件层 1:Activity Event +用于更新 timeline。 + +### 事件层 2:Workspace Card Event +用于插入或更新结果卡片。 + +建议未来协议形态: + +```ts +type ChatStreamEvent = + | TokenEvent + | StatusEvent + | ToolStartEvent + | ToolEndEvent + | ToolErrorEvent + | WorkspaceCardEvent + | DoneEvent + | ErrorEvent +``` + +其中 `WorkspaceCardEvent` 应允许: +- append card +- update card +- mark card done + +### 事件层 3:Workspace Meta Event +用于更新: +- session title +- stage label +- overall status + +第一版即使后端不直接下发,也必须在前端 store 设计上预留。 + +--- + +## 8. 组件改造建议(基于现有文件) + +## 8.1 必改组件 + +### 1. `frontend/components/gemini/GeminiChat.tsx` +这是主改造中心。 + +必须负责: +- 新增 workspace state +- 维护 active workspace session +- 接收 SSE 事件后同时更新: + - message content + - activity timeline + - workspace cards +- 管理 chat 与 workspace 联动 +- 页面布局从双主区变三主区 + +### 2. `frontend/components/gemini/GeminiMessage.tsx` +必须改为: +- 弱化当前 `TracePanel` +- 增加 `message summary bar` +- 增加 “查看工作区”/高亮态 +- assistant message 点击后切换 active workspace + +### 3. `frontend/components/gemini/TracePanel.tsx` +不建议继续保留原职责。 + +建议: +- 要么升级重命名为 `ActivityTimeline.tsx` +- 要么拆成: + - `MessageActivitySummary.tsx` + - `WorkspaceActivityTimeline.tsx` + +当前这个文件的“折叠工具列表”心智不够用了。 + +### 4. `frontend/lib/api.ts` +必须升级类型: +- 增加 workspace card event 类型预留 +- 增加 UI card 数据结构类型 +- 增加 session / activity / card 的类型定义 + +## 8.2 新增组件建议 + +至少新增: + +- `AgentWorkspace.tsx` +- `WorkspaceHeader.tsx` +- `ActivityTimeline.tsx` +- `WorkspaceCardRenderer.tsx` +- `cards/SearchResultCard.tsx` +- `cards/KnowledgeResultCard.tsx` +- `cards/TicketSummaryCard.tsx` +- `cards/TicketDetailCard.tsx` +- `cards/DocumentResultCard.tsx` +- `cards/SandboxResultCard.tsx` +- `cards/ErrorCard.tsx` +- `cards/EmptyStateCard.tsx` +- `MessageActivitySummary.tsx` + +--- + +## 9. 分阶段交付要求(但每阶段都有硬结果) + +为了降低风险,允许分阶段做,但每阶段都必须是完整可见结果,不接受“先埋代码,UI 后补”。 + +## Phase 1:布局与状态层完成 +必须交付: +- 页面三栏结构完成 +- 右侧 workspace 固定出现 +- status 在 workspace 顶部真实可见 +- assistant message 可与 workspace 绑定 +- 当前 trace panel 不再承担主展示职责 + +这是最低可验收版本。 + +## Phase 2:timeline 完成 +必须交付: +- status/tool/done/error 节点都能显示 +- timeline 可高亮当前节点 +- timeline 与 message / workspace 联动 +- 当前轮完整活动闭环可见 + +## Phase 3:第一批结果卡片完成 +必须交付: +- 至少实现 4 类真实结果卡片 +- 正在生成时能增量更新 +- 失败时能显示 ErrorCard +- 完成后 workspace 保留结果 + +## Phase 4:接近目标项目交互的收口 +必须交付: +- message 区与 workspace 的视觉关系收敛 +- summary 文案收敛 +- 动效和状态切换自然 +- 当前交互整体无限接近目标项目的观感与操作路径 + +--- + +## 10. 我的方案选择 + +结合当前 SOC 项目现状,我明确选择: + +- 不直接改 `langgraphjs-gen-ui-examples` 为主工程 +- 以它为交互参考 +- 在当前 SOC 前端基础上完成 generative UI 升级 + +理由: +- 当前项目前端骨架已成型 +- SSE 聊天链路已接通 +- trace / status / tool 事件已具备基础 +- 改当前项目比反向适配示例仓库更稳、更贴当前后端 + +所以这份方案不是概念探索,而是当前项目的明确产品实现方向。 + +--- + +## 11. 最终硬性验收标准 + +只有同时满足以下条件,才算本方案完成: + +### 验收 1 +页面存在固定可见的 agent workspace,不再只是 message 下方 trace 折叠区。 + +### 验收 2 +status / tool / done / error 都能在 UI 中形成可视 timeline。 + +### 验收 3 +至少 4 类结构化结果卡片真实落地,并与时间线/工具调用绑定。 + +### 验收 4 +聊天区与 workspace 双向联动: +- 点击消息可切换 workspace +- 生成中同步更新 +- 完成后保留结果 + +### 验收 5 +最终交互观感明显不再是“chat + 调试 trace”,而是“chat + agent workspace”。 + +### 验收 6 +用户主观体验上,必须明显无限接近 `langgraphjs-gen-ui-examples` 的交互方向,而不是只做一个右侧栏凑数。 + diff --git a/doc/uigoto/GPTui.md b/doc/uigoto/GPTui.md new file mode 100644 index 0000000..d9b2e4c --- /dev/null +++ b/doc/uigoto/GPTui.md @@ -0,0 +1,69 @@ +# GPTui + +## 结论 1:按当前项目继续改,能不能做到接近 `langgraphjs-gen-ui-examples` 的效果? + +能。 + +但前提是目标不能继续停留在当前这版 `trace panel`,而要升级成更接近 LangGraph generative UI 的交互: + +- 不是只在消息里挂一个执行过程折叠区 +- 而是让 agent 输出的事件驱动 UI 渲染 +- tool / state / result 不只是文本,而是驱动卡片、侧边栏、嵌入式交互组件 +- 最终形态应是:`chat + agent activity + generative UI component` + +也就是说,当前项目不是不能做,而是目前只做到轻量 trace MVP,还没有进入 generative UI 这一层。 + +如果继续沿现有项目演进,理论上可以做到接近 `langgraphjs-gen-ui-examples` 的效果,但需要补一层明确的 UI 渲染契约,例如: + +- 后端输出结构化 UI schema / UI event +- 前端根据 schema 决定渲染哪种组件 +- tool 结果不只进消息正文,还能驱动独立的 UI 容器 +- chat 区与右侧面板 / 卡片式区域协同渲染 + +所以结论是: + +- 按当前项目继续改,能做到接近那个效果 +- 但需要把目标从 `status + tool trace + answer` 升级为 `generative UI` +- 现在本地代码还没到那个层级 + +--- + + +## 结论 2:直接去改 `https://github.com/langchain-ai/langgraphjs-gen-ui-examples`,兼容当前后端,能不能改成功? + +可以改成功。 + +但我判断,这条路不如在现有项目里演进划算。 + +原因是这个示例仓库默认假设的基础设施和当前项目并不一致: + +- 它默认是 LangGraph.js +- 配套 Agent Chat UI / LangGraph Server 的交互方式 +- 有自己的一套 graph/server 协议与 generative UI 组件约定 + +而当前项目实际是: + +- Python 后端 +- 自己的 SSE 协议 +- 自己的 React 前端组件结构 +- 当前只实现到文本流 + tool trace + status 的轻量形态 + +所以如果硬把 `langgraphjs-gen-ui-examples` 改造成兼容当前后端,本质上需要改的是: + +- 前端 transport 层 +- 事件协议适配层 +- UI 渲染契约 +- graph/tool 输出结构 + +这不属于“小改兼容”,而是“借它的 UI 思路,重新接你自己的后端协议”。 + +所以更准确的判断是: + +- 技术上可以改成功 +- 但不是最低成本路线 +- 最合理的方式,是参考它的交互设计与生成式 UI 思路,在当前项目里重做一层 generative UI 适配 + +最终结论: + +- 现有项目继续改,能做出接近它的效果,而且这条路更合理 +- 直接改那个示例项目去兼容当前后端,也能成,但会更别扭,成本更高 diff --git a/doc/工单接口.md b/doc/工单接口.md new file mode 100644 index 0000000..f476c4a --- /dev/null +++ b/doc/工单接口.md @@ -0,0 +1,474 @@ +我已经完整阅读了后端的 API 密钥系统代码。下面是完整的接口文档: + +--- + +# 工单系统 — 外部 API 密钥调用接口文档 + +## 一、认证方式 + +所有请求通过 HTTP 请求头 `X-Api-Key` 传递 API 密钥进行认证。 + +``` +X-Api-Key: gd_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +``` + +- 密钥格式:`gd_live_` + 40 位十六进制字符 +- 密钥仅在创建时返回一次明文,之后无法再次获取 +- 密钥支持过期时间和启用/禁用控制 + +## 二、权限模型 + +### 2.1 模块权限 + +每个 API 密钥创建时需指定 `allowedModules`,限制可访问的模块范围。合法模块如下: + +| 模块键 | 路由前缀 | 说明 | +|---|---|---| +| `ticket` | `/api/tickets` | 工单管理 | +| `customer` | `/api/customers` | 客户管理 | +| `engineer` | `/api/engineers` | 工程师/运维管理 | +| `attachment` | `/api/attachments` | 附件上传 | +| `statusMonitor` | `/api/status` | 状态监控 | + +请求路径不在上述模块范围内的,跳过模块权限检查。 + +### 2.2 角色映射 + +API 密钥通过认证后,系统会生成一个**合成 ADMIN 用户**(`id: api-key:`, `role: ADMIN`)。因此 API 密钥只能访问 `@Roles(...)` 中包含 `ADMIN` 的接口。 + +### 2.3 禁止访问的路径 + +以下管理端路径**明确禁止** API 密钥访问(即使密钥有效也返回 `401`): + +- `/api/api-keys/**` — API 密钥管理 +- `/api/api-permissions/**` — API 模块权限管理 + +--- + +## 三、接口列表 + +> 基础路径:`/api` +> 所有请求需携带 `X-Api-Key` 请求头 +> 返回格式:JSON + +--- + +### 3.1 工单模块 (`ticket`) + +需要 `allowedModules` 包含 `"ticket"`。 + +#### 3.1.1 创建工单 + +``` +POST /api/tickets +``` + +**请求体:** + +| 字段 | 类型 | 必填 | 说明 | +|---|---|---|---| +| `platform` | string | ✅ | 平台类型:`taiji` / `xm` / `original` | +| `accountInfo` | string | ✅ | 账号信息 | +| `modelUsed` | string | ✅ | 使用的模型 | +| `description` | string | ✅ | 问题描述 | +| `requestExample` | string | ✅ | 请求示例 | +| `contactInfo` | string | ❌ | 联系方式 | +| `framework` | string | ❌ | 使用框架 | +| `networkEnv` | string | ❌ | 网络环境:`local` / `cloud` | +| `attachmentUrls` | string[] | ❌ | 附件 URL 列表 | +| `requestedLevel` | string | ❌ | 请求工程师等级:`L1` / `L2` / `L3` | + +#### 3.1.2 为指定客户创建工单 + +``` +POST /api/tickets/for-customer/:customerId +``` + +**路径参数:** `customerId` — 客户 ID + +**请求体:** 同 3.1.1 + +#### 3.1.3 查询工单列表 + +``` +GET /api/tickets?page=1&pageSize=20&status=PENDING +``` + +**查询参数:** + +| 参数 | 类型 | 必填 | 默认值 | 说明 | +|---|---|---|---|---| +| `page` | number | ❌ | 1 | 页码 | +| `pageSize` | number | ❌ | 20 | 每页条数 | +| `status` | string | ❌ | — | 筛选状态:`PENDING` / `ACCEPTED` / `IN_PROGRESS` / `PENDING_CLOSE` / `CLOSED` | + +#### 3.1.4 查询单个工单 + +``` +GET /api/tickets/:id +``` + +#### 3.1.5 更新工单状态 + +``` +PUT /api/tickets/:id/status +``` + +**请求体:** + +| 字段 | 类型 | 必填 | 说明 | +|---|---|---|---| +| `status` | string | ✅ | 目标状态 | + +#### 3.1.6 自行接单 + +``` +PUT /api/tickets/:id/self-assign +``` + +#### 3.1.7 分配工程师 + +``` +PUT /api/tickets/:id/assign +``` + +**请求体:** + +| 字段 | 类型 | 必填 | 说明 | +|---|---|---|---| +| `engineerId` | string | ✅ | 工程师 ID | + +#### 3.1.8 客户关闭工单 + +``` +PUT /api/tickets/:id/customer-close +``` + +#### 3.1.9 申请关闭工单 + +``` +PUT /api/tickets/:id/close-request +``` + +#### 3.1.10 审批关闭工单 + +``` +PUT /api/tickets/:id/close-approve +``` + +#### 3.1.11 拒绝关闭工单 + +``` +PUT /api/tickets/:id/close-reject +``` + +#### 3.1.12 催单 + +``` +POST /api/tickets/:id/urge +``` + +**请求体:** + +| 字段 | 类型 | 必填 | 说明 | +|---|---|---|---| +| `note` | string | ❌ | 催单备注 | + +#### 3.1.13 获取工单留言列表 + +``` +GET /api/tickets/:id/messages +``` + +#### 3.1.14 添加工单留言 + +``` +POST /api/tickets/:id/messages +``` + +**请求体:** + +| 字段 | 类型 | 必填 | 说明 | +|---|---|---|---| +| `content` | string | ✅ | 留言内容 | +| `attachmentUrls` | string[] | ❌ | 附件 URL 列表 | + +#### 3.1.15 删除工单留言 + +``` +DELETE /api/tickets/messages/:messageId +``` + +#### ⚠️ 不可访问的接口 + +| 接口 | 原因 | +|---|---| +| `GET /api/tickets/daily-usage/me` | 仅限 `CUSTOMER` 角色,API 密钥为 `ADMIN` 角色,无权限 | + +--- + +### 3.2 客户模块 (`customer`) + +需要 `allowedModules` 包含 `"customer"`。 + +#### 3.2.1 查询客户列表 + +``` +GET /api/customers +``` + +#### 3.2.2 查询单个客户 + +``` +GET /api/customers/:id +``` + +#### ⚠️ 不可访问的接口 + +以下接口仅限 `OPERATOR` 角色,API 密钥(`ADMIN`)无权访问: + +| 接口 | 方法 | +|---|---| +| `POST /api/customers` | 创建客户 | +| `PATCH /api/customers/:id/tier` | 更新客户等级 | +| `PATCH /api/customers/:id/bind-engineer` | 绑定工程师 | + +--- + +### 3.3 工程师/运维模块 (`engineer`) + +需要 `allowedModules` 包含 `"engineer"`。 + +#### 3.3.1 查询工程师列表 + +``` +GET /api/engineers +``` + +#### 3.3.2 创建工程师 + +``` +POST /api/engineers +``` + +**请求体:** + +| 字段 | 类型 | 必填 | 说明 | +|---|---|---|---| +| `username` | string | ✅ | 用户名 | +| `email` | string | ✅ | 邮箱 | +| `password` | string | ✅ | 密码 | +| `level` | string | ✅ | 等级:`L1` / `L2` / `L3` | +| `isAdmin` | boolean | ❌ | 是否管理员 | + +#### 3.3.3 创建运维人员 + +``` +POST /api/engineers/operators +``` + +**请求体:** + +| 字段 | 类型 | 必填 | 说明 | +|---|---|---|---| +| `username` | string | ✅ | 用户名 | +| `email` | string | ✅ | 邮箱 | +| `password` | string | ✅ | 密码 | + +#### 3.3.4 管理端 — 查询工程师列表 + +``` +GET /api/engineers/admin/engineers +``` + +#### 3.3.5 管理端 — 更新工程师信息 + +``` +PATCH /api/engineers/admin/engineers/:id +``` + +**请求体(均可选):** + +| 字段 | 类型 | 说明 | +|---|---|---| +| `username` | string | 用户名 | +| `email` | string | 邮箱 | +| `level` | string | 等级:`L1` / `L2` / `L3` | +| `isAvailable` | boolean | 是否可用 | + +#### 3.3.6 管理端 — 重置工程师密码 + +``` +PATCH /api/engineers/admin/engineers/:id/password +``` + +**请求体:** + +| 字段 | 类型 | 必填 | 说明 | +|---|---|---|---| +| `newPassword` | string | ✅ | 新密码 | + +#### 3.3.7 管理端 — 删除工程师 + +``` +DELETE /api/engineers/admin/engineers/:id +``` + +#### 3.3.8 管理端 — 查询运维人员列表 + +``` +GET /api/engineers/admin/operators +``` + +#### 3.3.9 管理端 — 更新运维人员信息 + +``` +PATCH /api/engineers/admin/operators/:id +``` + +**请求体(均可选):** + +| 字段 | 类型 | 说明 | +|---|---|---| +| `username` | string | 用户名 | +| `email` | string | 邮箱 | + +#### 3.3.10 管理端 — 重置运维人员密码 + +``` +PATCH /api/engineers/admin/operators/:id/password +``` + +**请求体:** + +| 字段 | 类型 | 必填 | 说明 | +|---|---|---|---| +| `newPassword` | string | ✅ | 新密码 | + +#### 3.3.11 管理端 — 删除运维人员 + +``` +DELETE /api/engineers/admin/operators/:id +``` + +#### ⚠️ 注意 — `me` 接口的局限性 + +以下接口虽然角色上允许 `ADMIN` 访问,但 API 密钥的合成用户 ID 为 `api-key:`,不对应真实工程师账号,**调用会在服务层失败**: + +| 接口 | 说明 | +|---|---| +| `PATCH /api/engineers/me/availability` | 更新可用状态 | +| `PATCH /api/engineers/me/email` | 更新邮箱 | +| `PATCH /api/engineers/me/password` | 修改密码 | + +--- + +### 3.4 附件模块 (`attachment`) + +需要 `allowedModules` 包含 `"attachment"`。 + +#### 3.4.1 获取上传 SAS Token + +``` +POST /api/attachments/sas-token +``` + +**请求体:** + +| 字段 | 类型 | 必填 | 说明 | +|---|---|---|---| +| `fileName` | string | ✅ | 文件名(会被安全化处理) | + +**返回:** 包含 Azure Blob Storage 上传所需的 SAS Token。 + +--- + +### 3.5 状态监控模块 (`statusMonitor`) + +需要 `allowedModules` 包含 `"statusMonitor"`。 + +#### 3.5.1 获取外部服务状态 + +``` +GET /api/status/external +``` + +#### 3.5.2 获取运维仪表盘 + +``` +GET /api/status/dashboard +``` + +#### 3.5.3 获取公开仪表盘 + +``` +GET /api/status/public-dashboard +``` + +> 注:此接口本身无需认证即可访问。使用 API 密钥访问时会消耗模块权限检查。 + +--- + +## 四、白名单路径(无需模块检查) + +以下路径不受 API 模块权限限制,但部分仍需 JWT 认证(API 密钥不可替代): + +| 路径 | 说明 | +|---|---| +| `/api/auth/*` | 认证相关 | +| `/api/health` | 健康检查 | +| `/api/public/bing-background` | 必应壁纸 | +| `/api/api-permissions/*` | 权限管理(**禁止**API密钥访问) | +| `/api/api-keys*` | 密钥管理(**禁止**API密钥访问) | + +--- + +## 五、错误码说明 + +| HTTP 状态码 | 说明 | +|---|---| +| `401 Unauthorized` | 密钥无效、已禁用、已过期,或尝试访问禁止路径 | +| `403 Forbidden` | 密钥的 `allowedModules` 不包含请求的模块 / 角色不足 | +| `400 Bad Request` | 请求参数校验失败 | +| `404 Not Found` | 资源不存在 | + +--- + +## 六、示例调用 + +```bash +# 查询工单列表 +curl -H "X-Api-Key: gd_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \ + "https://your-domain/api/tickets?page=1&pageSize=10" + +# 创建工单 +curl -X POST \ + -H "X-Api-Key: gd_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \ + -H "Content-Type: application/json" \ + -d '{ + "platform": "taiji", + "accountInfo": "test-account", + "modelUsed": "gpt-4", + "description": "API调用异常", + "requestExample": "curl https://api.example.com/v1/chat" + }' \ + "https://your-domain/api/tickets" +``` + +--- + +## 七、认证流程总结 + +``` +请求 → [全局 ApiKeyGuard] 读取 X-Api-Key → 验证密钥 → 设置 req.apiClient + → [全局 ApiModulePermissionGuard] 解析路径模块 → 检查 allowedModules + → [控制器 JwtAuthGuard] 检测 apiClient → 合成 ADMIN 用户 → 跳过 JWT 校验 + → [控制器 RolesGuard] 检查 ADMIN 是否在 @Roles() 的允许列表中 + → 执行业务逻辑 +``` + +关键源码参考: +- 密钥验证:api-key.service.ts(`validateKey` 方法) +- 密钥 Guard:api-key.guard.ts +- 模块权限 Guard:api-module-permission.guard.ts +- JWT Guard 中的 API Key 处理:jwt-auth.guard.ts \ No newline at end of file diff --git a/frontend/components/workspace/ActivityTimeline.tsx b/frontend/components/workspace/ActivityTimeline.tsx new file mode 100644 index 0000000..6a1b6cc --- /dev/null +++ b/frontend/components/workspace/ActivityTimeline.tsx @@ -0,0 +1,65 @@ +"use client"; +import { Zap, Wrench, Flag, AlertCircle, Loader2, CheckCircle, XCircle } from "lucide-react"; +import { cn } from "@/lib/utils"; +import type { ActivityNode } from "@/lib/api"; + +interface ActivityTimelineProps { + nodes: ActivityNode[]; +} + +const TYPE_ICON: Record> = { + status: Zap, + tool: Wrench, + done: Flag, + error: AlertCircle, +}; + +function NodeIcon({ node }: { node: ActivityNode }) { + if (node.nodeStatus === "running") { + return ; + } + if (node.nodeStatus === "success") { + return ; + } + if (node.nodeStatus === "error") { + return ; + } + const Icon = TYPE_ICON[node.type] ?? Zap; + return ; +} + +export function ActivityTimeline({ nodes }: ActivityTimelineProps) { + if (nodes.length === 0) return null; + return ( +
+ {nodes.map((node, i) => ( +
+ {i < nodes.length - 1 && ( +
+ )} +
+ +
+
+

+ {node.label} +

+ {node.detail && ( +

{node.detail}

+ )} +
+
+ ))} +
+ ); +} diff --git a/frontend/components/workspace/AgentWorkspace.tsx b/frontend/components/workspace/AgentWorkspace.tsx new file mode 100644 index 0000000..338bef3 --- /dev/null +++ b/frontend/components/workspace/AgentWorkspace.tsx @@ -0,0 +1,100 @@ +"use client"; +import { BrainCircuit, Loader2, CheckCircle, XCircle } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { ActivityTimeline } from "./ActivityTimeline"; +import { WorkspaceCardRenderer } from "./WorkspaceCardRenderer"; +import type { WorkspaceSession } from "@/lib/api"; + +interface AgentWorkspaceProps { + session: WorkspaceSession | null; + isGenerating: boolean; +} + +function StatusBadge({ status }: { status: WorkspaceSession["status"] }) { + if (status === "running") { + return ( + + + 运行中 + + ); + } + if (status === "completed") { + return ( + + + 已完成 + + ); + } + if (status === "error") { + return ( + + + 失败 + + ); + } + return null; +} + +export function AgentWorkspace({ session, isGenerating }: AgentWorkspaceProps) { + if (!session) { + return ( +
+ +

+ 向 Agent 发送消息后
工作区将在此展示执行过程 +

+
+ ); + } + + return ( +
+ {/* Header */} +
+
+ +

{session.title}

+ +
+ {session.stageLabel && ( +

{session.stageLabel}

+ )} +
+ + {/* Body */} +
+ {/* Timeline */} + {session.timeline.length > 0 && ( +
+

执行过程

+ +
+ )} + + {/* Cards */} + {session.cards.length > 0 && ( +
+ {session.timeline.length > 0 && ( +
+ )} +

结果

+ {session.cards.map((card) => ( + + ))} +
+ )} + + {/* Empty state while running */} + {session.status === "running" && session.cards.length === 0 && session.timeline.length === 0 && ( +
+ + 正在准备结果… +
+ )} +
+
+ ); +} diff --git a/frontend/components/workspace/WorkspaceCardRenderer.tsx b/frontend/components/workspace/WorkspaceCardRenderer.tsx new file mode 100644 index 0000000..53eb53d --- /dev/null +++ b/frontend/components/workspace/WorkspaceCardRenderer.tsx @@ -0,0 +1,27 @@ +"use client"; +import { KnowledgeResultCard } from "./cards/KnowledgeResultCard"; +import { TicketSummaryCard } from "./cards/TicketSummaryCard"; +import { SearchResultCard } from "./cards/SearchResultCard"; +import { DocumentResultCard } from "./cards/DocumentResultCard"; +import { SandboxResultCard } from "./cards/SandboxResultCard"; +import { ErrorCard } from "./cards/ErrorCard"; +import type { WorkspaceCard } from "@/lib/api"; + +const COMPONENT_MAP: Record> = { + KnowledgeResultCard, + TicketSummaryCard, + SearchResultCard, + DocumentResultCard, + SandboxResultCard, + ErrorCard, +}; + +export function WorkspaceCardRenderer({ card }: { card: WorkspaceCard }) { + const Component = COMPONENT_MAP[card.name]; + if (!Component) return null; + return ( +
+ +
+ ); +} diff --git a/frontend/components/workspace/cards/DocumentResultCard.tsx b/frontend/components/workspace/cards/DocumentResultCard.tsx new file mode 100644 index 0000000..6104be0 --- /dev/null +++ b/frontend/components/workspace/cards/DocumentResultCard.tsx @@ -0,0 +1,41 @@ +"use client"; +import { FileText, Download } from "lucide-react"; + +interface DocumentResultCardProps { + title: string; + type: string; + file_url: string; + size_hint?: string; +} + +export function DocumentResultCard({ title, type, file_url, size_hint }: DocumentResultCardProps) { + return ( +
+
+ + 文档生成 + + {type} + +
+
+ +
+

{title}

+ {size_hint &&

{size_hint}

} +
+ {file_url && ( + + + 下载 + + )} +
+
+ ); +} diff --git a/frontend/components/workspace/cards/ErrorCard.tsx b/frontend/components/workspace/cards/ErrorCard.tsx new file mode 100644 index 0000000..66c0f19 --- /dev/null +++ b/frontend/components/workspace/cards/ErrorCard.tsx @@ -0,0 +1,20 @@ +"use client"; +import { AlertCircle } from "lucide-react"; + +interface ErrorCardProps { + error: string; + tool?: string; +} + +export function ErrorCard({ error, tool }: ErrorCardProps) { + return ( +
+
+ + 工具执行失败 + {tool && · {tool}} +
+

{error}

+
+ ); +} diff --git a/frontend/components/workspace/cards/KnowledgeResultCard.tsx b/frontend/components/workspace/cards/KnowledgeResultCard.tsx new file mode 100644 index 0000000..9230506 --- /dev/null +++ b/frontend/components/workspace/cards/KnowledgeResultCard.tsx @@ -0,0 +1,46 @@ +"use client"; +import { BookOpen } from "lucide-react"; + +interface KnowledgeResult { + title: string; + category: string; + snippet: string; +} + +interface KnowledgeResultCardProps { + query: string; + total: number; + results: KnowledgeResult[]; +} + +export function KnowledgeResultCard({ query, total, results }: KnowledgeResultCardProps) { + return ( +
+
+ + 知识库检索 + + {total} 条结果 + +
+ {query && ( +

查询:{query}

+ )} +
+ {results.map((r, i) => ( +
+
+ {r.title} + {r.category && ( + {r.category} + )} +
+ {r.snippet && ( +

{r.snippet}

+ )} +
+ ))} +
+
+ ); +} diff --git a/frontend/components/workspace/cards/SandboxResultCard.tsx b/frontend/components/workspace/cards/SandboxResultCard.tsx new file mode 100644 index 0000000..78ef474 --- /dev/null +++ b/frontend/components/workspace/cards/SandboxResultCard.tsx @@ -0,0 +1,43 @@ +"use client"; +import { Terminal, CheckCircle, XCircle } from "lucide-react"; + +interface SandboxResultCardProps { + language: string; + exit_code: number; + stdout: string; + has_more: boolean; + duration_ms?: number; +} + +export function SandboxResultCard({ language, exit_code, stdout, has_more, duration_ms }: SandboxResultCardProps) { + const success = exit_code === 0; + return ( +
+
+ + 代码执行 + + {success + ? + : + } + + {success ? "成功" : `退出码 ${exit_code}`} + + +
+
+ {language} + {duration_ms != null && ( + {duration_ms}ms + )} +
+ {stdout && ( +
+          {stdout}
+          {has_more && "\n… (输出已截断)"}
+        
+ )} +
+ ); +} diff --git a/frontend/components/workspace/cards/SearchResultCard.tsx b/frontend/components/workspace/cards/SearchResultCard.tsx new file mode 100644 index 0000000..512edda --- /dev/null +++ b/frontend/components/workspace/cards/SearchResultCard.tsx @@ -0,0 +1,54 @@ +"use client"; +import { Search, ExternalLink } from "lucide-react"; + +interface SearchResult { + title: string; + url: string; + snippet: string; +} + +interface SearchResultCardProps { + query: string; + total: number; + results: SearchResult[]; +} + +export function SearchResultCard({ query, total, results }: SearchResultCardProps) { + return ( +
+
+ + 网络搜索 + + {total} 条来源 + +
+ {query && ( +

查询:{query}

+ )} +
+ {results.map((r, i) => ( +
+ + {r.url && ( +

{r.url}

+ )} + {r.snippet && ( +

{r.snippet}

+ )} +
+ ))} +
+
+ ); +} diff --git a/frontend/components/workspace/cards/TicketSummaryCard.tsx b/frontend/components/workspace/cards/TicketSummaryCard.tsx new file mode 100644 index 0000000..9f83cd7 --- /dev/null +++ b/frontend/components/workspace/cards/TicketSummaryCard.tsx @@ -0,0 +1,68 @@ +"use client"; +import { Ticket } from "lucide-react"; + +interface TicketItem { + id: string; + title: string; + status: string; + priority: string; + customer: string; + created: string; +} + +interface TicketSummaryCardProps { + total: number; + tickets: TicketItem[]; + stats: Record; +} + +const PRIORITY_COLOR: Record = { + P0: "text-red-400 bg-red-400/10", + P1: "text-orange-400 bg-orange-400/10", + P2: "text-yellow-400 bg-yellow-400/10", + P3: "text-[var(--gem-text-muted)] bg-[var(--gem-surface-2)]", +}; + +const STATUS_LABEL: Record = { + pending: "待处理", + processing: "处理中", + resolved: "已解决", + closed: "已关闭", +}; + +export function TicketSummaryCard({ total, tickets, stats }: TicketSummaryCardProps) { + return ( +
+
+ + 工单查询 + + 共 {total} 条 + +
+ {Object.keys(stats).length > 0 && ( +
+ {Object.entries(stats).map(([status, count]) => ( + + {STATUS_LABEL[status] ?? status} {count} + + ))} +
+ )} +
+ {tickets.slice(0, 8).map((t) => ( +
+ {t.id} + {t.title} + + {t.priority} + +
+ ))} + {tickets.length > 8 && ( +

还有 {tickets.length - 8} 条…

+ )} +
+
+ ); +}