"""AI references 工具模块。 为 AI 输出(ai_cache.result_json / ai_messages.reference_card) 注入数据来源引用元数据,便于前端渲染可点击引用卡片。 - App2~8:通过 dispatcher._write_cache 统一注入到 result['_references'] - App1:通过 xcx_chat 在 assistant 消息写入时调用 build_app1_reference 生成单卡片 """ from __future__ import annotations from typing import Any def build_app_references(app_type: str, context: dict) -> list[dict]: """为 App2~8 构建 references 列表,供前端消息卡片渲染。 引用结构: { "type": "member" | "task" | "assistant" | "finance", "id": int | str, "label": "卡片上的文字", "link": "/pages/xxx/xxx?param=val"(小程序页面路径), "source_page": 小程序页面 contextType } Args: app_type: 应用名称 context: 传给 build_prompt 的上下文(含 site_id / member_id 等) Returns: refs 数组。无有效上下文时返回空数组。 """ refs: list[dict] = [] site_id = context.get("site_id") member_id = context.get("member_id") assistant_id = context.get("assistant_id") time_dimension = context.get("time_dimension") if member_id is not None: refs.append({ "type": "member", "id": member_id, "label": f"客户 #{member_id}", "link": f"/pages/customer-detail/customer-detail?customerId={member_id}", "source_page": "customer-detail", }) if assistant_id is not None: refs.append({ "type": "assistant", "id": assistant_id, "label": f"助教 #{assistant_id}", "link": f"/pages/coach-detail/coach-detail?coachId={assistant_id}", "source_page": "coach-detail", }) if app_type == "app2_finance" and time_dimension: refs.append({ "type": "finance", "id": time_dimension, "label": f"财务看板:{_label_for_dimension(time_dimension)}", "link": f"/pages/board-finance/board-finance?timeDimension={time_dimension}", "source_page": "board-finance", }) # 保留 site_id 作为兜底上下文(不单独成卡,但用于前端场景判断) if site_id is not None and refs: for r in refs: r.setdefault("site_id", site_id) return refs def attach_references(app_type: str, result: dict | None, context: dict) -> dict | None: """向 AI 输出 result 追加 _references 字段(非破坏性)。 - result 为 None 时原样返回(调用失败不注入) - result 为 dict 时追加 _references 字段;如果 result 已含 _references,保留原值 """ if result is None or not isinstance(result, dict): return result if "_references" in result: return result refs = build_app_references(app_type, context) if refs: result["_references"] = refs return result def build_app1_reference_card(source_page: str | None, context_id: int | str | None) -> dict | None: """为 App1(chat)assistant 消息构建单个 reference_card。 兼容前端 chat.wxml 已有的 {type, title, summary, data, dataList} 渲染结构, 额外携带 link 字段供前端点击跳转详情页。 当用户在特定页面(customer-detail / coach-detail / task-detail)发起对话时, 自动附加对应跳转卡片。普通浮窗对话(source_page='general')返回 None。 与 chat_service.build_reference_card 不同:本函数不查 DB,仅按 source_page 构造链接。 """ if not source_page or not context_id: return None mapping: dict[str, tuple[str, str, str]] = { "customer-detail": ("customer", "客户", "customerId"), "coach-detail": ("assistant", "助教", "coachId"), "task-detail": ("task", "任务", "taskId"), } entry = mapping.get(source_page) if entry is None: return None ref_type, label_prefix, param = entry return { "type": ref_type, "title": f"{label_prefix} #{context_id}", "summary": f"点击查看{label_prefix}详情", "data": {}, "link": f"/pages/{source_page}/{source_page}?{param}={context_id}", "source_page": source_page, } def _label_for_dimension(dimension: str) -> str: """8 个财务维度 → 中文标签。""" mapping = { "this_month": "本月", "last_month": "上月", "this_week": "本周", "last_week": "上周", "this_quarter": "本季度", "last_quarter": "上季度", "last_3_months": "近三个月", "last_6_months": "近六个月", } return mapping.get(dimension, dimension)