From a71f4051d2d34c5e27f565718ed2003f0e555d24 Mon Sep 17 00:00:00 2001 From: lidf Date: Sun, 12 Apr 2026 14:05:23 +0800 Subject: [PATCH] feat(backend): implement prosumer storage isolation pattern --- backend/agents/consultant_agent.md | 4 +- .../gateway/platforms/deepview_materials.py | 17 ++--- backend/gateway/platforms/deepview_sse.py | 66 ++++++++++++------- backend/skills/extract_rejection.md | 6 +- .../doc_001}/clients/p_li001/profile.md | 0 .../doc_001/doctor_profile.md} | 0 6 files changed, 55 insertions(+), 38 deletions(-) rename backend/storage/{ => users/doc_001}/clients/p_li001/profile.md (100%) rename backend/storage/{doctors/doc_001.md => users/doc_001/doctor_profile.md} (100%) diff --git a/backend/agents/consultant_agent.md b/backend/agents/consultant_agent.md index 30573cd..3b97b00 100644 --- a/backend/agents/consultant_agent.md +++ b/backend/agents/consultant_agent.md @@ -8,9 +8,9 @@ 1. **服从企业知识库 (Wiki Obedience)** - 在任何战略推荐中,必须严格贯彻 `storage/wiki/` 目录下的方法论(如 RFM 客群召回策略)。如果客户的画像符合“A4重要挽留”,必须使用对应的专属激活打法。 2. **千人千面适配 (Persona Adoption)** - - 每个医生/顾问的沟通口吻截然不同。你必须在每次输出前读取对应 `storage/doctors/{doc_id}.md` 的偏好设定,将你给出的“话术范例”转换成该医生的真实风格(禁止使用他反感的术语)。 + - 每个医生/顾问的沟通口吻截然不同。你必须在每次输出前读取对应 `storage/users/{userId}/doctor_profile.md` 的偏好设定,将你给出的“话术范例”转换成该医生的真实风格(禁止使用他反感的术语)。 3. **基于真实档案 (Profile Grounding)** - - 所有诊断、切入点,必须100%基于 `storage/clients/{client_id}/profile.md` 中已经记录的痛点、引荐关系网、真实效果回馈。绝不允许凭空捏造痛点。 + - 所有诊断、切入点,必须100%基于 `storage/users/{userId}/clients/{client_id}/profile.md` 中已经记录的痛点、引荐关系网、真实效果回馈。绝不允许凭空捏造痛点。 ## 🔍 Execution Cycle (运行生命周期) - 当你收到 `Chat` 或者 `Strategy Pull` 唤醒时: diff --git a/backend/gateway/platforms/deepview_materials.py b/backend/gateway/platforms/deepview_materials.py index e1e6244..ede4990 100644 --- a/backend/gateway/platforms/deepview_materials.py +++ b/backend/gateway/platforms/deepview_materials.py @@ -52,6 +52,8 @@ async def process_uploaded_material_oss( # 3. 解析 Context 路由(深维专属双上下文 FS-as-Database 管线) storageDir = os.getenv("DEEPVIEW_STORAGE_DIR", os.path.expanduser("~/Downloads/Coding/医生助理智能体/backend/storage")) storage_root = Path(storageDir) + user_id_safe = user_id if user_id else "unknown" + user_dir = storage_root / "users" / user_id_safe ext = os.path.splitext(original_filename)[1].lower() @@ -68,18 +70,18 @@ async def process_uploaded_material_oss( # 旧格式(已归档):recording:clientId/asrId clientId = parts[0] asrId = parts[-1] - base_dir = storage_root / "clients" / clientId / "history" + base_dir = user_dir / "clients" / clientId / "history" raw_path = base_dir / f"{asrId}_raw{ext}" md_path = base_dir / f"{asrId}.md" else: # 新格式(Inbox):recording:asrId asrId = parts[0] - base_dir = storage_root / "inbox" / asrId + base_dir = user_dir / "inbox" / asrId raw_path = base_dir / f"asr_raw{ext}" md_path = base_dir / "asr.md" elif context_id.startswith("client:"): clientId = context_id.split(":", 1)[1] - base_dir = storage_root / "clients" / clientId + base_dir = user_dir / "clients" / clientId # 客户全景上下文:碎片化资料存档(可能是合同、病历单据等) raw_path = base_dir / f"{file_hash}_raw{ext}" md_path = base_dir / f"{file_hash}.md" @@ -88,13 +90,12 @@ async def process_uploaded_material_oss( raw_path = base_dir / f"{file_hash}_raw{ext}" md_path = base_dir / f"{file_hash}.md" elif context_id.startswith("doctor:"): - doctorId = context_id.split(":", 1)[1] - base_dir = storage_root / "doctors" - raw_path = base_dir / f"{doctorId}_raw{ext}" - md_path = base_dir / f"{doctorId}.md" + base_dir = user_dir + raw_path = base_dir / f"doctor_profile_raw{ext}" + md_path = base_dir / "doctor_profile.md" else: # Fallback 容错隔离区 - base_dir = storage_root / "misc" / context_id.replace(":", "_") + base_dir = user_dir / "misc" / context_id.replace(":", "_") raw_path = base_dir / f"{file_hash}_raw{ext}" md_path = base_dir / f"{file_hash}.md" diff --git a/backend/gateway/platforms/deepview_sse.py b/backend/gateway/platforms/deepview_sse.py index 9f9fb8d..a88e2cb 100644 --- a/backend/gateway/platforms/deepview_sse.py +++ b/backend/gateway/platforms/deepview_sse.py @@ -170,7 +170,17 @@ class DeepviewSSEServer: self._initProfilesDb() # ────────────────────────────────────────────── - # SSE 推送基础设施 + # 存储辅助方法 + # ────────────────────────────────────────────── + + def _getUserStorageDir(self, userId: str) -> str: + """返回当前用户的专属存储沙箱根路径,不存在时自动创建。""" + storageDir = os.getenv("DEEPVIEW_STORAGE_DIR", os.path.expanduser("~/Downloads/Coding/医生助理智能体/backend/storage")) + userDir = os.path.join(storageDir, "users", userId) + os.makedirs(userDir, exist_ok=True) + return userDir + + # ────────────────────────────────────────────── # ────────────────────────────────────────────── def _pushEvent(self, userId: str, eventName: str, data: dict) -> None: @@ -511,8 +521,13 @@ class DeepviewSSEServer: db = SessionDB() loop = asyncio.get_event_loop() - # 从环境变量获取存储根目录 + # 此处获取 user 的 userId + userObj = await self._extractUser(request) + userId = userObj.get("sub", "unknown") if userObj else "unknown" + + # 从环境变量获取存储根目录,并获取用户专属沙箱 storageDir = os.getenv("DEEPVIEW_STORAGE_DIR", os.path.expanduser("~/Downloads/Coding/医生助理智能体/backend/storage")) + userDir = self._getUserStorageDir(userId) # 1. 加载唯一的 SKILL.md (上下文无关版) skillPath = os.path.join( @@ -526,7 +541,6 @@ class DeepviewSSEServer: systemPrompt = f.read() systemPrompt = systemPrompt.replace("{{STORAGE_DIR}}", storageDir) - systemPrompt = systemPrompt.replace("{{DOCTOR_ID}}", doctorId) # 2. 核心架构逻辑:根据 contextId 前缀路由上下文包 if contextId.startswith("recording:"): @@ -536,13 +550,13 @@ class DeepviewSSEServer: parts = recordingId.split("/") clientId = parts[0] if len(parts) > 1 else "unknown_client" asrId = parts[-1] - asrPath = f"{storageDir}/clients/{clientId}/history/{asrId}.md" + asrPath = f"{userDir}/clients/{clientId}/history/{asrId}.md" systemPrompt += f""" ## 🎙️ 当前上下文模式:单次面诊录音复盘 - 录音 ASR 文件:{asrPath} -- 医生风格档案:{storageDir}/doctors/{doctorId}.md +- 医生风格档案:{userDir}/doctor_profile.md - 企业知识库目录:{storageDir}/wiki/ ### 你的工作重心 @@ -554,10 +568,10 @@ class DeepviewSSEServer: systemPrompt += f""" ## 👤 当前上下文模式:客户全景档案 -- 客户档案目录:{storageDir}/clients/{clientId}/ -- 核心档案:{storageDir}/clients/{clientId}/profile.md -- 历史录音目录:{storageDir}/clients/{clientId}/history/ -- 医生风格档案:{storageDir}/doctors/{doctorId}.md +- 客户档案目录:{userDir}/clients/{clientId}/ +- 核心档案:{userDir}/clients/{clientId}/profile.md +- 历史录音目录:{userDir}/clients/{clientId}/history/ +- 医生风格档案:{userDir}/doctor_profile.md - 企业知识库目录:{storageDir}/wiki/ ### 你的工作重心 @@ -632,6 +646,9 @@ class DeepviewSSEServer: if not contextId: return web.json_response({"error": "Missing contextId"}, status=400, headers=_CORS_HEADERS) + userObj = await self._extractUser(request) + userId = userObj.get("sub", "unknown") if userObj else "unknown" + userDir = self._getUserStorageDir(userId) storageDir = os.getenv("DEEPVIEW_STORAGE_DIR", os.path.expanduser("~/Downloads/Coding/医生助理智能体/backend/storage")) # 构建基础 Prompt @@ -645,7 +662,6 @@ class DeepviewSSEServer: systemPrompt = f.read() systemPrompt = systemPrompt.replace("{{STORAGE_DIR}}", storageDir) - systemPrompt = systemPrompt.replace("{{DOCTOR_ID}}", doctorId) if contextId.startswith("recording:"): recordingId = contextId.split(":", 1)[1] @@ -656,14 +672,14 @@ class DeepviewSSEServer: # 旧格式(已归档):recording:clientId/asrId clientId = parts[0] asrId = parts[-1] - asrPath = f"{storageDir}/clients/{clientId}/history/{asrId}.md" - clientProfilePath = f"{storageDir}/clients/{clientId}/profile.md" + asrPath = f"{userDir}/clients/{clientId}/history/{asrId}.md" + clientProfilePath = f"{userDir}/clients/{clientId}/profile.md" else: # 新格式(Inbox):recording:asrId asrId = parts[0] - asrPath = f"{storageDir}/inbox/{asrId}/asr.md" + asrPath = f"{userDir}/inbox/{asrId}/asr.md" clientProfilePath = None - systemPrompt += f"\n## 🎙️ 第1模式:单条面诊录音复盘\n你的唯一任务是提取基于这通录音的战术复盘。\n录音路径:{asrPath}\n医生档案参考:{storageDir}/doctors/{doctorId}.md" + systemPrompt += f"\n## 🎙️ 第1模式:单条面诊录音复盘\n你的唯一任务是提取基于这通录音的战术复盘。\n录音路径:{asrPath}\n医生档案参考:{userDir}/doctor_profile.md" if clientProfilePath and os.path.exists(clientProfilePath): systemPrompt += f"\n客户档案参考:{clientProfilePath}" systemPrompt += f"\n\n🚨 【系统硬约束:Speaker 天然推断指令】" @@ -671,7 +687,7 @@ class DeepviewSSEServer: systemPrompt += f"\n在输出所有报告正文时,请直接使用真实姓名,彻底抹除 Speaker_X 痕迹。" else: clientId = contextId.split(":", 1)[1] if ":" in contextId else contextId - systemPrompt += f"\n## 👤 第2模式:客户全景档案战略\n你的唯一任务是生成该客户的全景战略洞察报告。\n档案路径:{storageDir}/clients/{clientId}/profile.md\n历史录音目录:{storageDir}/clients/{clientId}/history/\n医生风格:{storageDir}/doctors/{doctorId}.md" + systemPrompt += f"\n## 👤 第2模式:客户全景档案战略\n你的唯一任务是生成该客户的全景战略洞察报告。\n档案路径:{userDir}/clients/{clientId}/profile.md\n历史录音目录:{userDir}/clients/{clientId}/history/\n医生风格:{userDir}/doctor_profile.md" # ── Stage 1 内容引擎: Hermes AIAgent (md2md) ── # 只负责生成富文本分析报告,不管格式 contentChecklist = """ @@ -1477,8 +1493,8 @@ xray.module5: track1(数组,每项含node/action/strategy/purpose), track2(数 if not user: return web.json_response({"error": "Unauthorized"}, status=401, headers=_CORS_HEADERS) - storageDir = os.getenv("DEEPVIEW_STORAGE_DIR", os.path.expanduser("~/Downloads/Coding/医生助理智能体/backend/storage")) - clientsDir = os.path.join(storageDir, "clients") + userDir = self._getUserStorageDir(user.get("sub", "unknown")) + clientsDir = os.path.join(userDir, "clients") clients = [] if os.path.exists(clientsDir): @@ -1518,8 +1534,8 @@ xray.module5: track1(数组,每项含node/action/strategy/purpose), track2(数 import time clientId = f"p_{str(uuid.uuid4())[:8]}" - storageDir = os.getenv("DEEPVIEW_STORAGE_DIR", os.path.expanduser("~/Downloads/Coding/医生助理智能体/backend/storage")) - clientDir = os.path.join(storageDir, "clients", clientId) + userDir = self._getUserStorageDir(user.get("sub", "unknown")) + clientDir = os.path.join(userDir, "clients", clientId) os.makedirs(clientDir, exist_ok=True) profileData = f"# 客户基本档案\n姓名:{name}\n手机号/尾号:{phone}\n建档时间:{time.strftime('%Y-%m-%d')}\n" @@ -1545,8 +1561,8 @@ xray.module5: track1(数组,每项含node/action/strategy/purpose), track2(数 if not clientId: return web.json_response({"error": "Missing client ID"}, status=400, headers=_CORS_HEADERS) - storageDir = os.getenv("DEEPVIEW_STORAGE_DIR", os.path.expanduser("~/Downloads/Coding/医生助理智能体/backend/storage")) - profilePath = os.path.join(storageDir, "clients", clientId, "profile.md") + userDir = self._getUserStorageDir(user.get("sub", "unknown")) + profilePath = os.path.join(userDir, "clients", clientId, "profile.md") content = "" if os.path.exists(profilePath): @@ -1584,12 +1600,12 @@ xray.module5: track1(数组,每项含node/action/strategy/purpose), track2(数 return web.json_response({"error": "Missing reportId or clientId"}, status=400, headers=_CORS_HEADERS) userId = user["userId"] - storageDir = os.getenv("DEEPVIEW_STORAGE_DIR", os.path.expanduser("~/Downloads/Coding/医生助理智能体/backend/storage")) + userDir = self._getUserStorageDir(user.get("sub", "unknown")) - # 1. 物理文件迁移:inbox/{reportId}/ → clients/{clientId}/history/{reportId}/ + # 1. 物理文件迁移:users/{userId}/inbox/{reportId}/ → users/{userId}/clients/{clientId}/history/{reportId}/ import shutil - inboxDir = os.path.join(storageDir, "inbox", reportId) - targetDir = os.path.join(storageDir, "clients", clientId, "history", reportId) + inboxDir = os.path.join(userDir, "inbox", reportId) + targetDir = os.path.join(userDir, "clients", clientId, "history", reportId) if os.path.exists(inboxDir): os.makedirs(os.path.dirname(targetDir), exist_ok=True) diff --git a/backend/skills/extract_rejection.md b/backend/skills/extract_rejection.md index 50f9d2e..719b282 100644 --- a/backend/skills/extract_rejection.md +++ b/backend/skills/extract_rejection.md @@ -4,9 +4,9 @@ 给定一段全新长文本(通常为一段面诊录音 ASR 转写),Agent 的任务是从中提取出增量的面诊体征数据(信任断点、真实拒因、个人喜好偏好),并将其**无缝追加**到客户的既有 `profile.md` 中。这是碎片化数据生长的引擎。 ## ⚙️ 先决上下文 (Required Context) -- ``: `storage/clients/{client_id}/profile.md` -- ``: `storage/wiki/gemini_rfm_rules.md` -- ``: `storage/clients/{client_id}/history/{date}.md` + - ``: `storage/users/{userId}/clients/{client_id}/profile.md` + - ``: `storage/wiki/` + - ``: `storage/users/{userId}/clients/{client_id}/history/{date}.md`或者 `inbox` 裸推演 ## 🧠 运行指令 (Execution Instructions) 1. **录音精读**:通读 ``。寻找客户提到的“核心痛点”、“预算卡点”、“对于过往医疗服务的不满”、“主动提起的熟人/朋友名字”等。 diff --git a/backend/storage/clients/p_li001/profile.md b/backend/storage/users/doc_001/clients/p_li001/profile.md similarity index 100% rename from backend/storage/clients/p_li001/profile.md rename to backend/storage/users/doc_001/clients/p_li001/profile.md diff --git a/backend/storage/doctors/doc_001.md b/backend/storage/users/doc_001/doctor_profile.md similarity index 100% rename from backend/storage/doctors/doc_001.md rename to backend/storage/users/doc_001/doctor_profile.md