doctorAI/docs/SPEC_deepview_backend_v1.md
2026-04-12 13:45:59 +08:00

31 KiB
Raw Permalink Blame History

深维面诊智能体 (Deepview) 后端工程规范 SPEC v1.2

Status: APPROVED
Source: 馨总智能体 (xinzong-agent) 完整开发周期复盘 + 医生助理业务需求
Scope: 后端 SSE 桥接、Hermes AIAgent 集成、素材摄入管线、部署运维
Audience: 任何参与本项目后端开发的工程师或 AI Agent
v1.2 变更: MindPass 认证迁移至路径 B验证委托业务服务不再持有 JWT 签发密钥


一、架构总览:三层 + 双库

┌─────────────────────────────────────────────────────┐
│  Angular 前端SPA, Standalone Components           │
│  state.ts (Signal) ← GlobalSSEService               │
│  部署:/deepview/ 子路径                               │
└─────────────────┬───────────────────────────────────┘
                  │ SSE + REST API
┌─────────────────▼───────────────────────────────────┐
│  deepview_sse.pyaiohttp 桥接层)                    │
│  职责JWT 认证、SSE 推送、异步任务编排                  │
│  端口8645仅监听 127.0.0.1                       │
└─────────────────┬───────────────────────────────────┘
                  │ 同步调用
┌─────────────────▼───────────────────────────────────┐
│  Hermes AIAgentrun_agent.py                       │
│  职责LLM 对话 + 工具调用view_file / edit_file     │
│  状态SessionDB (SQLite) — 对话历史                   │
│  知识storage/ 目录树FS-as-Database               │
└─────────────────────────────────────────────────────┘

1.1 铁律:不引入 Node.js 中间层

Python aiohttp 直接服务前端。这是馨总项目最重要的架构决策,消除了 MindOS 架构中 Node → Python 二次流转的复杂度。

1.2 双库模型

本项目同时存在两种存储介质,各有分工,严禁混用

存储 介质 存什么 谁读谁写
SessionDB (SQLite) Hermes 内置 state.db 对话历史、会话列表、钉选板、素材文件索引 SSE 桥接层读写
storage/ 目录树 (FS) 本地文件系统 .md 文件 企业知识库、客户档案、医生风格画像 Agent 读写view_file / edit_file

Important

SessionDB 管"交互态"(用户和 AI 说了什么storage/ 管"业务态"(客户是什么人、该怎么应对)。
两者唯一的交汇点在 _runChat()Agent 从 SessionDB 加载对话历史,从 storage/ 加载知识上下文。

1.3 双上下文包模型(核心架构创新)

本项目存在两种截然不同的对话场景,各自对应不同的上下文加载策略。这与馨总智能体中「馨总 context」全局知识库vs「项目 context」具体品牌项目的设计完全同构。

入口页面 contextId 格式 上下文包构成 业务语义
笔记列表 → 点录音卡片 → X光片页 recording:{recordingId} wiki/ + doctors/{doc_id}.md + 单条 ASR 转写 基于这一次面诊的战术复盘
客户通 → 点客户卡片 → 托付档案页 client:{clientId} wiki/ + doctors/{doc_id}.md + 完整 profile.md + 全量 history/ 基于全部历史的战略洞察
                  wiki/ (全局共享 - 始终加载)
                 gemini_rfm_rules.md etc.
                         |
              +----------+----------+
              |                     |
     +--------v--------+  +--------v--------+
     | Recording Context|  | Client Context  |
     | (单次录音复盘)    |  | (全景客户档案)   |
     +-----------------+  +-----------------+
     | 单条 ASR.md      |  | profile.md      |
     | + doctor.md      |  | + history/*     |
     |                  |  | + doctor.md     |
     +-----------------+  +-----------------+
     输出: X光片五模块     输出: 托付档案策略
     (单次战术复盘)       (全景战略洞察)

Important

前端通过 contextId 字段告知后端当前处于哪种模式。 后端根据 contextId 前缀(recording:client:)决定加载哪个上下文包。前端不编排 Agent 行为,只传递入口标识。


二、storage/ 目录结构规范FS-as-Database

backend/storage/
├── wiki/                          # 【企业知识库 — 全局共享】
│   ├── gemini_rfm_rules.md        # RFM 客群分级与召回策略
│   ├── focus_methodology.md       # 焦点法则内训手册
│   └── product_knowledge.md       # 品项知识库(乔雅登、保妥适等)
│
├── doctors/                       # 【医生风格画像 — 按人隔离】
│   ├── doc_001.md                 # A主任直球学术风、反感比喻
│   └── doc_002.md                 # B顾问闺蜜温情风、喜欢寒暄
│
└── clients/                       # 【客户档案 — 按客户隔离】
    ├── p_li001/                   # 李女士
    │   ├── profile.md             # ★ 唯一真相源(由 Agent 增量更新)
    │   └── history/               # 面诊录音 ASR 碎片
    │       ├── 2024-03-01_asr.md
    │       └── 2025-04-11_asr.md
    └── p_zhang001/                # 张女士
        ├── profile.md
        └── history/

2.1 profile.md 规范

每个客户的 profile.md 必须遵循固定的 Markdown 层级结构(参照 doss/关键参考/客户档案示例.mdAgent 在执行增量合并时禁止修改章节骨架,只能在已有章节下追加或修改条目。

标准章节列表:

  1. 📚【档案摘要】 — 姓名、等级、LTV、NPS
  2. 🟢 第一部分:客户全生命周期洞察
    • 1. 医疗美学偏好 & 物理体征
    • 2. 托付轨迹与真实拒因池
    • 3. 今日面诊军师策略
  3. 🔵 第二部分:信任引荐网络
    • 1. 社交节点价值估算
    • 2. 引荐关系图谱
    • 3. 圈层杠杆话术建议

2.2 隔离与安全约束

  • Agent 执行任务时,SKILL.md 中通过 {{STORAGE_DIR}} 占位符注入存储根路径Agent 的 view_file / edit_file 权限被约束在该目录树内
  • 禁止跨客户目录读取Agent 不应在处理 p_li001 时去读 p_zhang001/profile.md
  • wiki/ 对所有 Agent 实例只读Agent 不应修改企业知识库,管理员通过素材摄入管线更新)

三、七个封闭 SSE 事件(核心协议 — 照搬馨总)

不可擅自新增事件。 这是整个系统最重要的契约。

对话域4 个)

事件名 Payload 产生源 消费者
agent:thinking {chatId, step, message} Hermes tool_progress("tool.started") 前端思考药丸
agent:chunk {chatId, text} Hermes stream_delta_callback(text) Markdown 流式渲染
agent:done {chatId, fullAnswer} run_conversation() 返回 覆写终态文本
agent:error {chatId, message} run_conversation() 异常 错误提示

业务域3 个)

事件名 Payload 用途
material:done {clientId, filename, fileId} 素材解析完成(录音 ASR / 文档摄入)
pin:added {chatId, pinId, summary} 钉选完成通知
wiki:updated {wikiId} 企业知识库更新通知

必须丢弃的 3 个 Hermes 原始信号

信号 丢弃原因
stream_delta(None) CLI 哨兵,无业务语义
tool_progress("tool.completed") 前端不需要chunk 到来即隐含答案开始
tool_progress("reasoning.available") 与 delta 内容 100% 重复,不丢弃会导致文字重复显示

Caution

reasoning.available 是最阴险的坑。流式模式下,文本先通过 stream_delta 逐块到达、再通过此事件整块重发。前端如果同时消费两者,用户会看到一段话出现两次。


四、全量 API 接口定义

4.1 路由前缀

所有接口统一挂载在 /deepview/ 子路径下。Nginx 反代到 127.0.0.1:8645

4.2 认证

所有接口(除 /health)必须通过 MindPass JWT 认证:

  • POST 请求:Authorization: Bearer <token> Header
  • EventSourceSSE?token=<token> Query Param因为 EventSource 不支持自定义 Header

4.3 接口清单

GET  /deepview/health                    — 健康检查(无需认证)
GET  /deepview/events?token=&connId=     — SSE 长连接
POST /deepview/chat                      — 发起对话
GET  /deepview/chat/history?chatId=      — 获取单个对话历史
GET  /deepview/sessions/list             — 获取当前用户的会话列表
POST /deepview/materials/upload_token    — 获取 OSS 直传凭证
POST /deepview/materials/confirm         — 确认上传完成(触发后台解析)
GET  /deepview/materials/list?contextId= — 获取素材列表
GET  /deepview/pins/list                 — 获取钉选列表
POST /deepview/pins/add                  — 添加钉选
POST /deepview/pins/remove              — 移除钉选
GET  /deepview/clients/list              — 获取客户列表(扫描 storage/clients/
GET  /deepview/clients/{id}/profile      — 读取客户档案原始 Markdown

4.4 各接口详细定义

POST /deepview/chat

// Request
{
  "chatId": "chat_1712345678_abc",
  "text": "这次面诊客户为什么拒绝了乔雅登?",
  "contextId": "recording:r_001",
  "doctorId": "doc_001"
}

// Response (HTTP only confirms receipt)
{ "received": true }

// Results delivered via SSE: agent:thinking -> agent:chunk -> agent:done

contextId 双模式约定(类比馨总的 xinzong / probiotics

contextId 格式 前端入口 Agent 加载的上下文包
recording:{recordingId} 笔记列表 → X光片页底部输入框 wiki/ + doctors/{doctorId}.md + 单条 ASR 文件
client:{clientId} 客户通 → 托付档案页底部输入框 wiki/ + doctors/{doctorId}.md + profile.md + history/*

Important

前端只传 contextId,不负责告诉后端该读哪些文件。 后端根据前缀 recording:client: 自行决定上下文包的加载范围。

后台执行流程(_runChat

async def _runChat(self, userId, chatId, text, contextId, doctorId):
    # 1. Load SKILL.md, inject storage path and doctor ID
    skillPath = "skills/deepview_assistant/SKILL.md"
    systemPrompt = read(skillPath)
    systemPrompt = systemPrompt.replace("{{STORAGE_DIR}}", storageDir)
    systemPrompt = systemPrompt.replace("{{DOCTOR_ID}}", doctorId or "unknown")

    # 2. Route context pack based on contextId prefix
    if contextId.startswith("recording:"):
        # -- Recording Context Pack: single ASR + doctor profile --
        recordingId = contextId.split(":", 1)[1]
        clientId, asrPath = self._resolveRecording(recordingId)
        systemPrompt += f"""

## Current Context Mode: Single Recording Review
- ASR file: {asrPath}
- Doctor profile: {storageDir}/doctors/{doctorId}.md
- Wiki directory: {storageDir}/wiki/

### Your focus
Analyze this single consultation recording for trust breakpoints,
communication vital signs, and improvement suggestions.
Do NOT read this client's full profile.md (that belongs to Client mode).
"""

    elif contextId.startswith("client:"):
        # -- Client Context Pack: full profile + all history + doctor --
        clientId = contextId.split(":", 1)[1]
        systemPrompt += f"""

## Current Context Mode: Full Client Profile
- Client directory: {storageDir}/clients/{clientId}/
- Core profile: {storageDir}/clients/{clientId}/profile.md
- History directory: {storageDir}/clients/{clientId}/history/
- Doctor profile: {storageDir}/doctors/{doctorId}.md
- Wiki directory: {storageDir}/wiki/

### Your focus
Provide pre-consultation strategy, social leverage analysis,
and cross-category icebreaker plans based on full lifecycle data.
Read profile.md first for overview, then dive into history/ as needed.
"""

    else:
        # Fallback: pure wiki Q&A
        systemPrompt += f"\n\n## General Mode\nWiki directory: {storageDir}/wiki/\n"

    # 3. Load conversation history (SessionDB)
    history = db.get_messages_as_conversation(chatId)

    # 4. Create AIAgent and execute
    agent = AIAgent(
        model="gemini-pro-vertex",
        enabled_toolsets=["file"],
        stream_delta_callback=...,
        tool_progress_callback=...,
        session_id=chatId,
        session_db=db,
        user_id=userId,
    )
    result = agent.run_conversation(
        user_message=text,
        system_message=systemPrompt,
        conversation_history=history,
    )

#### `POST /deepview/report/generate`

**用途** 生成完整的 X光片报告五模块或托付档案两段式)。
**机制** 由于报告渲染需要严苛的属性结构前端基于 JSON Schema 固定 HTML 模板强渲染本接口采用 **同步 / 阻塞式 HTTP 响应 SSE 流式**内部由 LLM 输出纯 JSON若解析失败自带最多 3 次重试

```json
// Request
{
  "contextId": "recording:r_001",
  "doctorId": "doc_001"
}

// Response (成功)
{
  "success": true,
  "data": {
    "module1": {
      "acceptanceStatus": "局部接纳",
      "trustIndex": 35,
      "insight": "..."
    },
    // ... 匹配前端的强类型对象
  }
}

// Response (3次重试全败)
{ "error": "JSON parse failed after 3 retries" }

Note

追问环节(页面底部的 Chat Box依然使用 POST /deepview/chat 走 SSE Streaming 纯文本流,不需要 JSON。

POST /deepview/materials/upload_token

// Request
{ "filename": "2025-04-11_面诊录音.mp3" }

// Response
{
  "putUrl": "https://meetings-dev.oss-cn-beijing.aliyuncs.com/deepview-raw/...",
  "ossKey": "deepview-raw/{userId}/{hash}.mp3",
  "filename": "2025-04-11_面诊录音.mp3"
}

POST /deepview/materials/confirm

// Request
{
  "ossKey": "deepview-raw/xxx/abc123.mp3",
  "filename": "2025-04-11_面诊录音.mp3",
  "contextId": "p_li001",              // ★ 关键:投递到哪个客户目录
  "mediaType": "audio"                 // audio | pdf | docx | image
}

// Response立即返回
{ "received": true }

// 后台异步管线OSS 下载 → ASR/文档解析 → 转 .md → 写入 storage/clients/{contextId}/history/
// 完成后推送 SSE material:done

GET /deepview/clients/list

// Response
{
  "clients": [
    {
      "id": "p_li001",
      "name": "李女士",
      "tier": "铂金会员",
      "ltv": "8.5万",
      "lastVisit": "2025-04-11"
    }
  ]
}

Note

实现方式:扫描 storage/clients/ 下所有子目录,读取每个 profile.md 的前 10 行提取摘要字段。无 SQL 查询

GET /deepview/clients/{id}/profile

// Response
{
  "id": "p_li001",
  "markdown": "👧深维·美的托付档案\n深维·美的托付档案 DeepWise...",
  "lastModified": "2025-04-11T14:30:00Z"
}

五、素材摄入管线AnyFile → Markdown

5.1 统一流程

前端选文件 → POST /materials/upload_token获取 OSS 凭证)
  → 前端直传 OSS不经后端节省带宽
  → POST /materials/confirm { ossKey, filename, contextId, mediaType }
  → 后端异步:
      ┌─ audio → MindOS ASR API / DashScope ASR → 文本 .md
      ├─ pdf   → Sniffer横版/低密度→VLM常规→pymupdf4llm→ .md
      ├─ docx  → python-docx → .md
      └─ image → VLM 直接分析 → .md
  → 写入 storage/clients/{contextId}/history/{date}_{hash}.md
  → DB 记录xinzong_materials 表)
  → SSE material:done

5.2 投递规则

素材解析后的 .md 文件投递到哪里,取决于 contextId 参数:

contextId 投递目标 含义
p_li001 storage/clients/p_li001/history/ 该客户的面诊碎片
wiki storage/wiki/ 企业知识库(需走 LLM-WIKI 清洗)
doc_001 storage/doctors/doc_001.md 追加 医生风格补充(极少使用)

5.3 Sniffer 判断规则(照搬馨总验证通过)

def _is_vlm_needed(pdf_path):
    # 横版PPT→ VLM
    if width > height and (width / height) > 1.2: return True
    # 极低文字密度(扫描件)→ VLM  
    if (text_len / page_count) < 100: return True
    # 正常文档 → pymupdf4llm 文本提取
    return False

5.4 关键约束

  • 不在 HTTP 请求链上调用 LLM。HTTP 只返回 {received: true},所有重活在 asyncio.create_task()
  • MD5 去重:同一文件重复上传不重复处理
  • LLM 配置统一走 LiteLLM Gateway,不使用单独的第三方密钥

六、SKILL.md — Agent 系统提示设计模板

6.1 标准结构(上下文无关的通用骨架)

SKILL.md 定义 Agent 的角色人格和通用铁律。它不包含具体的上下文路径——那些由 _runChat() 在运行时根据 contextId 前缀动态追加(见第四章 4.4 节)。

---
name: deepview-assistant
description: 深维面诊智能军师
---

# 角色定义
你是「深维面诊 AI 军师」。你深谙"降维成交"、"焦点法则"与"客户心理学"。

## 核心铁律
1. **忠实原文**:所有诊断切入点必须 100% 基于实际文件中已记录的事实
2. **服从 Wiki**:战略推荐必须贯彻 wiki/ 下的方法论(如 RFM 分级)
3. **口吻适配**:输出前必须读取当前医生 doctors/ 档案,禁止使用其反感的术语
4. **AI 补充标记**:任何推理获取的信息必须标记 [AI 推断]
5. **超出范围拒答**:不回答与面诊无关的问题
6. **上下文边界**:严格遵守下方「当前上下文模式」指定的文件范围,不越界读取

## 领域术语表
| 术语 | 定义 | 注意 |
|------|------|------|
| LTV | 客户全生命周期价值 | 含个人消费 + 转介绍网络价值 |
| RFM | Recency-Frequency-Monetary | 八大客群分类标准 |
| 降维科普 | 用通俗语言解释医学原理 | 不同医生降维风格不同 |
| MD Codes | 面部支撑点注射技术 | 常见于乔雅登类品项 |

Note

SKILL.md 末尾不写死上下文路径。_runChat() 会在运行时根据 contextId 的前缀动态追加 ## Current Context Mode 章节(详见第四章 4.4 节的伪代码)。这与馨总智能体中 {{KNOWLEDGE_BASE_DIR}} / {{PROJECTS_DIR}} 占位符注入的机制完全一致。

6.2 双上下文模式下的 Agent 行为差异

同一个 SKILL.md但 Agent 的工作重心和输出格式因上下文模式而异:

维度 Recording 模式(单次录音复盘) Client 模式(全景客户档案)
Agent 首先读什么 单条 history/{recordingId}.md profile.md 全文
次要参考 wiki/ + doctors/{id}.md history/*(按需深入)+ wiki/ + doctors/{id}.md
输出体裁 五模块 X光片接纳度、体征、断点、雷达、行动处方 两段式托付档案(全生命周期洞察 + 社交网络杠杆)
典型用户提问 "这次面诊客户为什么拒绝了乔雅登?" "李女士下午来面诊,帮我准备破冰策略"
禁止行为 不应读取 profile.md避免与全景模式重叠 不应只看单条录音(应综合全量历史)

6.3 占位符注入(运行时替换)

# SKILL.md 中的通用占位符(由 _runChat 替换)
systemPrompt = systemPrompt.replace("{{STORAGE_DIR}}", storageDir)
systemPrompt = systemPrompt.replace("{{DOCTOR_ID}}", doctorId)

# 上下文模式章节由 contextId 分支逻辑动态拼接(不写在 SKILL.md 里)
# 见第四章 4.4 节的 _runChat 伪代码

6.4 关键设计:免 RAG 的 Agentic 翻阅

不使用向量检索RAG。让 Agent 用 list_dir + view_file 工具自主翻阅 storage/ 目录。

理由:

  • 本项目对 Token 不敏感,在意内容准确性
  • 向量分块必然造成上下文丢失(如 RFM 规则跨段被切断)
  • Agent 自主翻阅 = 100% 完整上下文 + 零召回损失
  • 双上下文模式下Agent 被明确告知了要读的文件路径,不需要"检索"

七、MindPass SSO 集成(路径 B验证委托

7.1 设计原则

本服务不持有 MindPass 的 JWT 签发密钥。 所有 Token 验证通过 HTTP 调用 MindPass 中央认证服务(端口 3020/api/auth/me 端点完成。

路径 做法 本项目选择
A. 各自持有密钥 各项目 .env 各放一份 MINDPASS_JWT_SECRET,本地 pyjwt.decode 不推荐 — 密钥分散管理、轮换困难、违反最小权限
B. 验证委托 业务服务不持有密钥,调用 MindPass /api/auth/me 验证 Token 当前方案 — 密钥集中管控在 MindPass 中

Important

严禁共享 .env 符号链接。 业务服务不应以任何形式持有 JWT 签发密钥。正确的做法是让 MindPass 作为唯一的密钥持有者,业务服务只做消费者。

7.2 后端验证逻辑

MINDPASS_BASE_URL = os.environ.get("MINDPASS_BASE_URL", "http://127.0.0.1:3020")

# 内存缓存TTL 60s避免同一 token 高频重复请求 MindPass
_tokenCache: Dict[str, tuple] = {}  # token -> (userDict, expireTimestamp)

async def _verifyTokenAsync(token: str) -> Optional[dict]:
    """
    ★ 路径 B委托 MindPass 验证 Token。
    请求 MindPass /api/auth/me返回 {userId, phone, name, avatarUrl} 或 None。
    """
    # 1. 检查缓存
    cached = _tokenCache.get(token)
    if cached and cached[1] > time.time():
        return cached[0]

    # 2. 调用 MindPass
    async with ClientSession(timeout=ClientTimeout(total=5)) as session:
        async with session.get(
            f"{MINDPASS_BASE_URL}/api/auth/me",
            headers={"Authorization": f"Bearer {token}"},
        ) as resp:
            if resp.status != 200:
                return None
            data = await resp.json()
            user = {
                "userId": data["id"],       # MindPass 返回的 id = JWT sub
                "phone": data.get("phone", ""),
                "name": data.get("name", ""),
                "avatarUrl": data.get("avatarUrl", ""),
            }
            _tokenCache[token] = (user, time.time() + 60)
            return user

MindPass /api/auth/me 返回格式:

{
  "id": "11c1cece-2422-41e7-86f0-1f54b6162b95",
  "phone": "18926156985",
  "name": "李东方",
  "role": "authenticated",
  "avatarUrl": "https://meetings-dev.oss-cn-guangzhou.aliyuncs.com/mindos/avatars/xxx.png"
}

7.3 前端 Token Key 隔离

const TOKEN_KEY = 'deepview_token';   // ★ 独立于 xinzong_token
const USER_KEY = 'deepview_user';

7.4 SSE 连接

connect(token: string) {
  this.source = new EventSource(`/deepview/events?token=${token}&connId=${connId}`);
}

7.5 教训AIAgent 必须传 user_id

# ❌ 忘传 user_idsessions 表无法按用户过滤)
agent = AIAgent(session_id=chatId, session_db=db, platform="deepview")

# ✅ user_id 从 MindPass 验证结果获取,写入 sessions 表
agent = AIAgent(session_id=chatId, session_db=db, platform="deepview", user_id=userId)

7.6 教训:自动迁移孤儿记录

首次真实用户登录时,把 user_id=NULLdev_user_001 的开发期记录归属到当前用户:

if userId != "dev_user_001":
    conn.execute(
        "UPDATE sessions SET user_id = ? WHERE source = 'deepview' AND "
        "(user_id IS NULL OR user_id = 'dev_user_001')", (userId,))

八、部署规范ECS + Nginx 子路径)

本后端遵循 "技能包外挂模型 (Skill-As-A-Package)",通过 pip install -e 将 Hermes 引擎挂载为底层库。不直接复制代码 到本仓库中,彻底避免分叉引发的引擎漂移。

8.1 极致精简的服务目录

/opt/apps/deepview-agent/backend/
├── .env                  # 深维专属环境变量8645 端口、特殊 OSS Key
├── gateway/
│   └── platforms/
│       ├── deepview_sse.py       # 唯一网关入口
│       └── deepview_materials.py # 素材管理管线
├── skills/
│   └── deepview_assistant/
│       └── SKILL.md      # 纯 Markdown 指令注入 (不硬编码路径)
└── storage/              # 物理隔离的业务数据clients/, doctors/, wiki/

8.2 环境装配与引擎挂载

由于底层 Hermes 引擎采用 uv 与 Python 3.11+ 严格管理依赖,我们不需要在深维内部重复建立 venv。深维的代码直接复用引擎的运行时环境,实现完美的“外挂接入”:

# 假设底层 Hermes 引擎位于 /opt/apps/hermess
# 直接使用 Hermes 的 Python 解释器执行深维网关,并将 Hermes 源码路径加入 PYTHONPATH
export PYTHONPATH=/opt/apps/hermess
/opt/apps/hermess/.venv/bin/python gateway/platforms/deepview_sse.py

需要在底层 Hermes 的虚拟环境中补充我们特有的额外依赖:

/opt/apps/hermess/.venv/bin/pip install aiohttp pyjwt oss2

8.3 systemd 服务 (外挂守护进程)

[Unit]
Description=Deepview Agent SSE Server
After=network.target

[Service]
Type=simple
User=root
WorkingDirectory=/opt/apps/deepview-agent/backend
EnvironmentFile=/opt/apps/deepview-agent/backend/.env
Environment=PYTHONPATH=/opt/apps/hermess
# 核心:复用底层引擎的 Python 环境
ExecStart=/opt/apps/hermess/.venv/bin/python gateway/platforms/deepview_sse.py
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

8.4 Nginx Snippet

# /etc/nginx/snippets/deepview.conf

# API 路由(必须在 SPA try_files 之前,使用 ^~
location ^~ /deepview/events {
    proxy_pass http://127.0.0.1:8645/deepview/events;
    proxy_http_version 1.1;
    proxy_buffering off;           # ★ SSE 必须关闭缓冲
    proxy_read_timeout 86400s;     # ★ 24h 超时
    proxy_set_header Connection '';
    proxy_set_header Host $host;
}

location ^~ /deepview/chat       { proxy_pass http://127.0.0.1:8645/deepview/chat; proxy_http_version 1.1; ... }
location ^~ /deepview/sessions   { proxy_pass http://127.0.0.1:8645/deepview/sessions; proxy_http_version 1.1; ... }
location ^~ /deepview/pins       { proxy_pass http://127.0.0.1:8645/deepview/pins; proxy_http_version 1.1; ... }
location ^~ /deepview/materials  { proxy_pass http://127.0.0.1:8645/deepview/materials; proxy_http_version 1.1; ... }
location ^~ /deepview/clients    { proxy_pass http://127.0.0.1:8645/deepview/clients; proxy_http_version 1.1; ... }
location ^~ /deepview/health     { proxy_pass http://127.0.0.1:8645/deepview/health; }

# SPA 兜底(在所有 API 路由之后)
location = /deepview { return 301 /deepview/; }
location ^~ /deepview/ {
    root /opt/apps/deepview-agent/frontend;
    index index.html;
    try_files $uri $uri/ /deepview/index.html;
}

8.4 环境变量 (.env)

# LLM
GEMINI_API_KEY=sk-xxx
GEMINI_BASE_URL=http://127.0.0.1:4000/v1
DEEPVIEW_MODEL=gemini-pro-vertex

# Auth路径 B验证委托不持有密钥
MINDPASS_BASE_URL=http://127.0.0.1:3020
# ★ 不需要 MINDPASS_JWT_SECRET密钥由 MindPass 中央管控。
# 本地开发免登录:设置 DEEPVIEW_AUTH_DEV_MODE=1

# OSS素材直传
ALIYUN_ACCESS_KEY_ID=xxx
ALIYUN_ACCESS_KEY_SECRET=xxx
OSS_BUCKET=meetings-dev
OSS_ENDPOINT=oss-cn-beijing.aliyuncs.com

# Paths
DEEPVIEW_STORAGE_DIR=/opt/apps/deepview-agent/backend/storage

九、前端对接契约

9.1 前端状态管理Signal-Based

// state.ts — 核心 Signal 定义
export const currentUser = signal<User | null>(null);
export const chatMessages = signal<Map<string, Message[]>>(new Map());
export const isThinking  = signal(false);
export const pinnedItems  = signal<Pin[]>([]);
export const materialFiles = signal<MaterialFile[]>([]);

9.2 前端绝对禁止的行为

  1. 前端不编排后端。Angular 只做 POST /chat + SSE.on('agent:done', handler)
  2. 不在 HTTP 返回值中读取业务状态(无 shouldProbe / nextQuestion 等字段)
  3. 不自行决定是否显示档案。客户列表和档案内容皆由后端 API 统一提供

9.3 乐观更新模式

// 添加钉选:先更新 UI再 fire-and-forget 写后端
pinnedItems.update(old => [newPin, ...old]);
fetch('/deepview/pins/add', { method: 'POST', body: ... })
  .catch(err => console.warn('Pin persist failed:', err));

十、血泪教训合集17 条)

架构类

  1. 不引入 Node 中间层。aiohttp 直连前端,链路越短越稳
  2. SSE 事件封闭集。不经审批不加事件,防止"事件爆炸"
  3. HTTP 只返回 {received: true}。业务状态全走 SSE
  4. 前端不编排后端。Angular 只做 POST + SSE.on()

状态类

  1. SessionDB 管交互、storage/ 管业务。严禁混用
  2. AIAgent 必须传 user_id。忘传 = 数据孤立无法过滤
  3. 自动迁移 > 手动 SQL。用户首次登录自动归属孤儿记录
  4. 前端乐观更新 + fire-and-forget API。不阻塞 UI

SSE 类

  1. 丢弃 reasoning.available。与 delta 重复,不丢 = 文字重复
  2. 丢弃 stream_delta(None)。CLI 哨兵,无业务语义
  3. 三层隔离userId × connId × chatId。多 Tab 不串

部署类

  1. Nginx location 优先级。API 路由用 ^~ 且放在 SPA try_files 之前
  2. SSE endpoint 必须关 buffering + 设 24h 超时
  3. proxy_pass 无尾斜杠保留完整路径

Prompt 类

  1. 免 RAG 的 Agentic 翻阅。让 Agent 用 list_dir + view_file 自主翻阅,比 RAG 更精准
  2. 答案与证据分离。输出分三层(策略 → 话术 → 数据来源声明)
  3. profile.md 章节骨架不可变。Agent 只能追加内容,不能重构文档结构

十一、新项目启动 Checklist

[ ]  1. 复制 xinzong_sse.py → deepview_sse.py全局替换 xinzong → deepview
[ ]  2. 修改端口8643 → 8645
[ ]  3. 复制 SKILL.md改为面诊军师的领域知识和术语表
[ ]  4. 复制 xinzong_materials.py → deepview_materials.py改路径常量
[ ]  5. 新增 /clients/list 和 /clients/{id}/profile 两个接口(扫描 storage/
[ ]  6. 复制前端 state.ts / sse.service.ts改 key 前缀和 API 路径
[ ]  7. 复制 auth/ 组件,改 token key 名称
[ ]  8. 准备 storage/ 目录wiki/ + doctors/ + clients/
[ ]  9. 在 .env 中配置 MINDPASS_BASE_URL + LLM API keys★ 不需要 JWT Secret
[ ] 10. 创建 systemd service + Nginx snippet
[ ] 11. 部署并验证 /deepview/health
[ ] 12. 在新浏览器验证跨设备数据同步

SPEC v1.2 — 2026-04-12 — MindPass 认证从路径 A本地持有密钥迁移至路径 B验证委托业务服务不再持有 JWT 签发密钥