Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/ca53411_ic.json
T
OpenCode 68a9071cf1 feat: reconcile local master with origin (exa/hot-platform hunters, leaks ledger, latest results)
Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
2026-08-07 12:53:47 +08:00

1 line
54 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "ca53411/ic", "files": {"PROJECT_DOCS/02-edge-functions.md": {"32a728b8284559abc250fa84f092f0e137811d5b": {"t": 1786068437.3957412, "n": 38586, "h": "e3b81a67d5f4fa4d", "c": "# Corolas | Platonic — Edge Functions 技术文档\n\n> **文档版本**: v1.0 \n> **适用项目**: Platonic AI 伴侣系统 \n> **编写日期**: 2025年6月 \n> **Edge Functions 总数**: 13 个 \n> **共享模块**: 4 个\n\n---\n\n## 目录\n\n- [一、全局概述](#一全局概述)\n- [二、共享模块](#二共享模块)\n- [三、Edge Functions 详细文档](#三edge-functions-详细文档)\n - [1. chat-stream](#1-chat-stream)\n - [2. drama-chat](#2-drama-chat)\n - [3. drama-session](#3-drama-session)\n - [4. energy](#4-energy)\n - [5. payment-create](#5-payment-create)\n - [6. payment-callback](#6-payment-callback)\n - [7. achievement-check](#7-achievement-check)\n - [8. consolidation](#8-consolidation)\n - [9. milestone-adjust](#9-milestone-adjust)\n - [10. proactive](#10-proactive)\n - [11. proactive-scheduler](#11-proactive-scheduler)\n - [12. seed-data](#12-seed-data)\n - [13. setup-proactive](#13-setup-proactive)\n- [四、部署配置汇总](#四部署配置汇总)\n- [五、数据库表关联图](#五数据库表关联图)\n- [六、外部依赖汇总](#六外部依赖汇总)\n- [七、安全策略总览](#七安全策略总览)\n\n---\n\n## 一、全局概述\n\n### 1.1 系统架构\n\nPlatonic 项目的 Edge Functions 构建于 **Supabase Edge Functions** (Deno Runtime) 之上,构成了 AI 伴侣系统的后端服务层。所有函数通过 HTTP 接口对外暴露,内部通过 Supabase JS Client 与 PostgreSQL 数据库交互,通过 REST API 与 DeepSeek 大模型服务交互。\n\n### 1.2 函数分类\n\n| 类别 | 函数 | 说明 |\n|------|------|------|\n| **核心聊天** | `chat-stream`, `drama-chat` | SSE 流式对话(普通聊天 + 剧情模式) |\n| **会话管理** | `drama-session` | 剧情会话的 CRUD 管理 |\n| **能量系统** | `energy` | 能量账户查询与消费(乐观锁) |\n| **支付系统** | `payment-create`, `payment-callback` | ZPay 支付订单创建与回调处理 |\n| **成就系统** | `achievement-check` | 成就检测与解锁 |\n| **记忆系统** | `consolidation` | STM -> LTM -> 远事记忆的 AI 整合 |\n| **亲密度系统** | `milestone-adjust` | 每日亲密度评分调整 |\n| **主动消息** | `proactive`, `proactive-scheduler` | AI 主动发消息 + 定时调度 |\n| **系统工具** | `seed-data`, `setup-proactive` | 种子数据初始化 + 定时任务配置 |\n\n### 1.3 环境变量清单\n\n| 变量名 | 用途 | 哪些函数使用 |\n|--------|------|-------------|\n| `SUPABASE_URL` | Supabase 项目 URL | 全部 |\n| `SUPABASE_SERVICE_ROLE_KEY` | 服务角色密钥(绕过 RLS) | 全部 |\n| `DEEPSEEK_API_KEY` | DeepSeek API 密钥 | chat-stream, drama-chat, consolidation, milestone-adjust, proactive, proactive-scheduler |\n| `ZPAY_PID` | ZPay 商户 ID | payment-create, payment-callback |\n| `ZPAY_KEY` | ZPay 商户密钥 | payment-create, payment-callback |\n| `SCHEDULER_SECRET` | 调度器密钥 | proactive-scheduler |\n\n### 1.4 技术栈\n\n- **运行时**: Deno (via Supabase Edge Functions)\n- **Supabase Client**: `@supabase/supabase-js@2.39.0`\n- **AI 模型**: DeepSeek V4 Flash (`deepseek-v4-flash`)\n- **支付网关**: ZPay (zpayz.cn)\n- **数据库**: PostgreSQL 16 (via Supabase)\n- **定时调度**: pg_cron (PostgreSQL 扩展)\n\n---\n\n## 二、共享模块\n\n### 2.1 `_shared/cors.ts` — CORS 跨域配置\n\n**文件路径**: `supabase/functions/_shared/cors.ts`\n\n**导出内容**:\n\n```typescript\nexport const corsHeaders: Record<string, string>\nexport function handleCors(req: Request): Response | null\n```\n\n**CORS 配置详情**:\n\n| 响应头 | 值 | 说明 |\n|--------|------|------|\n| `Access-Control-Allow-Origin` | `*` | 允许所有来源 |\n| `Access-Control-Allow-Headers` | `authorization, x-client-info, apikey, content-type` | 允许的请求头 |\n| `Access-Control-Allow-Methods` | `POST, GET, OPTIONS` | 允许的 HTTP 方法 |\n\n**handleCors 函数逻辑**:\n1. 检查请求方法是否为 `OPTIONS`(预检请求)\n2. 如果是,返回状态码 200 的 `Response('ok')`\n3. 如果不是,返回 `null`,由调用方继续处理\n\n> **注意**: 生产环境函数(如 chat-stream)通常内联自己的 CORS 配置(限定 `https://platonic.corolas.top`),不使用此共享模块。\n\n---\n\n### 2.2 `_shared/deepseek.ts` — DeepSeek API 调用\n\n**文件路径**: `supabase/functions/_shared/deepseek.ts`\n\n**导出内容**:\n\n```typescript\nexport interface DeepSeekMessage { role: 'system' | 'user' | 'assistant'; content: string; }\nexport async function* streamChat(messages, options): AsyncGenerator<string>\nexport async function chatJSON<T>(messages, options): Promise<T>\nexport function buildSystemPrompt(companion, mood, memories, milestone): string\n```\n\n#### `streamChat()` — SSE 流式对话\n\n| 参数 | 类型 | 必填 | 说明 |\n|------|------|------|------|\n| `messages` | `DeepSeekMessage[]` | 是 | 对话消息数组 |\n| `options.temperature` | `number` | 否 | 温度参数,默认 `0.8` |\n| `options.max_tokens` | `number` | 否 | 最大 token 数,默认 `500` |\n| `options.stream` | `boolean` | 否 | 是否流式输出,默认 `true` |\n\n**处理流程**:\n1. 从环境变量读取 `DEEPSEEK_API_KEY`\n2. 向 `https://api.deepseek.com/v1/chat/completions` 发送 POST 请求\n3. 使用模型 `deepseek-v4-flash`\n4. 通过 `response.body.getReader()` 读取 SSE 流\n5. 使用 `TextDecoder` 解码数据块\n6. 解析 `data:` 开头的 SSE 行,提取 `choices[0].delta.content`\n7. 遇到 `data: [DONE]` 时结束\n\n#### `chatJSON()` — 非流式 JSON 输出\n\n| 参数 | 类型 | 必填 | 说明 |\n|------|------|------|------|\n| `messages` | `DeepSeekMessage[]` | 是 | 对话消息数组 |\n| `options.temperature` | `number` | 否 | 默认 `0.5` |\n| `options.max_tokens` | `number` | 否 | 默认 `2000` |\n\n**特性**: 设置 `response_format: { type: 'json_object' }`,确保输出合法 JSON。\n\n#### `buildSystemPrompt()` — 系统提示构建\n\n**输入参数**:\n\n| 参数 | 类型 | 说明 |\n|------|------|------|\n| `companion` | `object` | 伴侣表完整数据(昵称、性别、年龄、五大人格、背景等) |\n| `mood` | `object | null` | 情绪记录(pleasure, arousal, dominance, valence) |\n| `memories` | `any[]` | LTM 记忆列表 |\n| `milestone` | `object | null` | 亲密度里程碑记录 |\n\n**核心逻辑**:\n1. **五大人格映射**: 将 0-100 的数值映射到 10 级描述文本(如开放性 75 -> \"很开放\")\n2. **情绪状态计算**: 根据 PAD 模型(pleasure/arousal/dominance)映射到中文情绪词\n3. **里程碑映射**: 阶段 1-5 分别对应「初见乍欢」「渐入佳境」「暗生情愫」「情投意合」「心有灵犀」\n4. **记忆格式化**: 将 LTM 记忆按类型和重要性格式化\n5. **规则注入**: 注入角色扮演规则(禁止打破第四面墙、中文回复 300-500 字等)\n\n**输出**: 完整的系统提示字符串,约 1000-2000 字。\n\n---\n\n### 2.3 `_shared/supabase.ts` — Supabase 客户端工具\n\n**文件路径**: `supabase/functions/_shared/supabase.ts`\n\n**导出内容**:\n\n```typescript\nexport function getSupabaseClient(authHeader?: string): SupabaseClient\nexport async function getUser(supabase: any): Promise<User>\nexport async function checkEnergy(supabase: any, userId: string, cost: number): Promise<boolean>\nexport async function consumeEnergy(supabase: any, userId: string, cost: number, description: string): Promise<boolean>\nexport async function getCompanionForUser(supabase: any, userId: string): Promise<any>\nexport async function getRecentMessages(supabase: any, companionId: string, limit: number): Promise<any[]>\nexport async function getLTMMemories(supabase: any, companionId: string, limit: number): Promise<any[]>\nexport async function getMoodRecord(supabase: any, companionId: string): Promise<any | null>\nexport async function getAnteriorMemories(supabase: any, companionId: string): Promise<any[]>\n```\n\n#### `getSupabaseClient()`\n\n| 参数 | 类型 | 必填 | 说明 |\n|------|------|------|------|\n| `authHeader` | `string` | 否 | JWT 授权头,用于传递用户身份 |\n\n**逻辑**: 使用 `SUPABASE_URL` + `SUPABASE_SERVICE_ROLE_KEY` 创建客户端。如果提供 `authHeader`,则在全局 headers 中设置 `Authorization`,使后续的 `supabase.auth.getUser()` 能正确解析用户身份。\n\n#### `consumeEnergy()` — 能量消费(含事务)\n\n**处理流程**:\n1. 查询 `energy_accounts` 表,获取用户账户(`id`, `balance`)\n2. 检查余额是否充足\n3. 更新账户余额(`balance - cost`)\n4. 在 `energy_transactions` 表中插入消费记录\n5. 如果步骤 4 失败,回滚余额(重新写入旧余额)\n6. 返回 `true`(成功)或 `false`(失败)\n\n---\n\n### 2.4 `_shared/zpay.ts` — ZPay 支付集成\n\n**文件路径**: `supabase/functions/_shared/zpay.ts`\n\n**导出内容**:\n\n```typescript\nexport function getZpayConfig(): { pid: string; key: string; }\nexport function generateZpaySign(params: Record<string, string>, key: string): string\nexport function createZpayOrder(params: object): { url: string; sign: string; }\nexport function verifyZpayCallback(params: Record<string, string>, key: string): boolean\nexport function generateOutTradeNo(): string\n```\n\n#### `generateZpaySign()` — 签名生成\n\n**算法**:\n1. 过滤掉 `sign`、`sign_type` 字段和空值字段\n2. 按键名字母升序排序\n3. 用 `&` 拼接为 `key1=value1&key2=value2` 格式\n4. 末尾追加商户密钥 `key`\n5. 返回 Base64 编码(简化版;实际 ZPay 使用 MD5)\n\n#### `createZpayOrder()` — 创建支付订单\n\n**输入参数**:\n\n| 参数 | 类型 | 说明 |\n|------|------|------|\n| `pid` | `string` | 商户 ID |\n| `type` | `string` | 支付类型(如 `alipay`) |\n| `out_trade_no` | `string` | 商户订单号 |\n| `notify_url` | `string` | 异步回调 URL |\n| `return_url` | `string` | 同步返回 URL |\n| `name` | `string` | 商品名称 |\n| `money` | `string` | 金额(元) |\n| `clientip` | `string` | 客户端 IP |\n| `device` | `string` | 设备类型 |\n| `key` | `string` | 商户密钥 |\n\n**输出**: 支付跳转 URL(`https://zpayz.cn/submit.php?...`)和签名。\n\n#### `generateOutTradeNo()` — 订单号生成\n\n**格式**: `PLATONIC_${timestamp}_${random}` \n**示例**: `PLATONIC_1718001234567_A3B9C2`\n\n---\n\n## 三、Edge Functions 详细文档\n\n---\n\n### 1. chat-stream\n\n**函数概述**\n\n| 属性 | 说明 |\n|------|------|\n| **功能描述** | SSE 流式对话(核心聊天功能)。接收用户消息,调用 DeepSeek AI 生成回复,以 SSE 流式返回。同时处理情绪检测、能量消费、消息持久化、亲密度更新、记忆整合触发等。 |\n| **重要性** | **极高** — 系统最核心的功能 |\n| **触发方式** | 前端 HTTP POST 请求 |\n| **调度频率** | 实时(每次用户发送消息时) |\n\n**HTTP 接口定义**\n\n| 属性 | 值 |\n|------|------|\n| 方法 | `POST` |\n| 路径 | `/functions/v1/chat-stream` |\n| Content-Type | `application/json` |\n| 响应类型 | `text/event-stream` (SSE) |\n\n**请求头**:\n\n| 头字段 | 必填 | 说明 |\n|--------|------|------|\n| `Authorization` | 是 | `Bearer <JWT_TOKEN>` |\n| `Content-Type` | 是 | `application/json` |\n\n**请求体格式**:\n\n```json\n{\n \"message\": \"用户输入的消息文本\"\n}\n```\n\n| 字段 | 类型 | 必填 | 说明 | 示例值 |\n|------|------|------|------|--------|\n| `message` | `string` | 是 | 用户发送的消息内容 | `\"今天过得怎么样?\"` |\n\n**响应体格式 (SSE 流)**:\n\n```\ndata: 你\ndata: 好\ndata: 呀\ndata: ~\ndata: 今\ndata: 天\ndata: ...\ndata: [DONE]\n```\n\n| SSE 事件 | 说明 |\n|----------|------|\n| `data: <text>` | AI 生成的文本片段(逐字/逐词流式推送) |\n| `data: [DONE]` | 流式传输完成标志 |\n| `data: [ERROR] <message>` | 发生错误 |\n\n**处理流程**:\n\n1. **CORS 处理**: 响应 OPTIONS 预检请求(204 No Content)\n2. **身份验证**: 从 `Authorization` 头提取 JWT,调用 `supabase.auth.getUser(jwt)` 验证\n3. **请求体解析**: 解析 JSON 请求体,检查 `message` 字段非空\n4. **能量检查与自动创建**:\n - 查询 `energy_accounts` 表获取用户余额\n - 如果账户不存在,**自动创建**(balance=0, version=1)\n - 检查余额是否 >= 50(单次消息消耗),不足返回 402 错误\n5. **获取伴侣数据**: 从 `companions` 表查询用户的 AI 伴侣\n6. **并行获取上下文数据**:\n - `intimacy_records` — 亲密度记录\n - `ltm_memories` — 长期记忆(按重要性降序,最多 20 条)\n - `mood_records` — 情绪记录(最新 1 条)\n - `stm_messages` — 短期记忆消息(最近 15 条)\n - `profiles` — 用户配置(获取 timezone)\n7. **提取情绪上下文**: 从近期用户消息中提取情绪标签,保留最近 5 次\n8. **提取待办事项**: 从记忆中过滤 `pending/todo/reminder` 类型的记忆\n9. **消费能量**: 扣除 50 能量,记录交易流水\n10. **保存用户消息**: 插入 `stm_messages` 表(含 speaker='user')\n11. **更新主动调度计划**(非阻塞): 计算下次触发时间,upsert `proactive_schedule`\n12. **异步情绪检测**(非阻塞): 调用 DeepSeek 检测用户消息情绪,更新消息记录\n13. **构建 DeepSeek 消息**:\n - System: 通过 `buildSystemPrompt()` 构建(含人格、记忆、情绪、时间等)\n - History: 将 STM 消息按时间正序排列,每条前缀加 `[用户/我 X分钟前]`\n - User: 当前消息\n14. **SSE 流式响应**: 调用 `streamChat()` 获取 AI 流式输出,逐块推送给客户端\n15. **AI 情绪检测**: 对完整 AI 回复进行情绪检测\n16. **保存 AI 回复**: 插入 `stm_messages` 表(含情绪标签)\n17. **更新亲密度**: 调用 `update_intimacy` RPC 函数\n18. **触发记忆整合**(非阻塞): 异步调用 `consolidation` Edge Function\n\n**数据库操作**:\n\n| 表名 | 操作 | 说明 |\n|------|------|------|\n| `energy_accounts` | SELECT, UPDATE(自动 INSERT) | 能量余额查询与扣减 |\n| `energy_transactions` | INSERT | 消费流水记录 |\n| `companions` | SELECT | 获取伴侣信息 |\n| `intimacy_records` | SELECT, RPC update_intimacy | 亲密度查询与更新 |\n| `ltm_memories` | SELECT | 获取长期记忆 |\n| `mood_records` | SELECT | 获取情绪记录 |\n| `stm_messages` | INSERT (x2) | 保存用户消息和 AI 回复 |\n| `profiles` | SELECT | 获取用户时区 |\n| `proactive_schedule` | UPSERT | 更新主动消息调度 |\n\n**外部 API 调用**:\n\n| 服务 | 调用次数 | 说明 |\n|------|----------|------|\n| DeepSeek API | 1-2 次 | 主对话流(必调)+ 情绪检测(异步,可能不调) |\n\n**错误处理**:\n\n| 错误码 | 触发条件 | 响应 |\n|--------|----------|------|\n| `401 Unauthorized` | 缺少/无效 JWT | JSON `{ error: 'Unauthorized' }` |\n| `402` | 能量不足 | JSON `{ error: 'Insufficient energy', balance }` |\n| `500` | 内部错误 | SSE 流中返回 `[ERROR] message` |\n\n**安全机制**:\n\n- **JWT 验证**: 通过 `supabase.auth.getUser()` 验证\n- **CORS**: 限定 `https://platonic.corolas.top`\n- **能量校验**: 每次消息消耗 50 能量,余额不足拒绝服务\n\n**代码关键逻辑说明**:\n\n- **指数退避重试**: DeepSeek 调用失败时自动重试 3 次,延迟分别为 1s, 2s, 4s\n- **30 秒超时**: 每次 API 调用设置 30 秒超时\n- **乐观锁**: 能量扣减使用 `version` 字段实现乐观锁\n- **情绪检测超时**: 500ms 超时,失败不影响主流程\n- **时区感知**: 根据用户时区格式化当前时间,生成时段描述(清晨/上午/中午/下午/傍晚/晚上/深夜)\n- **时间感知前缀**: 历史消息前缀包含 `timeSince()` 计算的时间差,AI 能感知对话间隔\n\n---\n\n### 2. drama-chat\n\n**函数概述**\n\n| 属性 | 说明 |\n|------|------|\n| **功能描述** | SSE 流式剧情对话。在剧情空间中,AI 伴侣完全沉浸在预设剧情场景中,以第一人称 RPG 方式与用户互动。 |\n| **重要性** | **高** — 核心剧情功能 |\n| **触发方式** | 前端 HTTP POST 请求 |\n| **调度频率** | 实时 |\n\n**HTTP 接口定义**\n\n| 属性 | 值 |\n|------|------|\n| 方法 | `POST` |\n| 路径 | `/functions/v1/drama-chat` |\n| 响应类型 | `text/event-stream` (SSE) |\n\n**请求头**:\n\n| 头字段 | 必填 | 说明 |\n|--------|------|------|\n| `Authorization` | 是 | `Bearer <JWT_TOKEN>` |\n| `Content-Type` | 是 | `application/json` |\n\n**请求体格式**:\n\n```json\n{\n \"session_id\": \" drama_session_uuid\",\n \"message\": \"用户输入的剧情消息\"\n}\n```\n\n| 字段 | 类型 | 必填 | 说明 | 示例值 |\n|------|------|------|------|--------|\n| `session_id` | `string` | 是 | 剧情会话 UUID | `\"550e8400-e29b-41d4-a716-446655440000\"` |\n| `message` | `string` | 否 | 用户消息(为空则只获取 AI 开场白) | `\"我走进了这个房间...\"` |\n\n**处理流程**:\n\n1. **CORS 处理**: 响应 OPTIONS 预检请求\n2. **身份验证**: JWT 验证\n3. **获取剧情会话**: 从 `drama_sessions` 表查询 session,验证属于当前用户\n4. **获取剧情定义**: 从 `drama_definitions` 表获取剧情信息(名称、场景设定、剧情提示)\n5. **获取伴侣**: 从 `companions` 表获取伴侣的人格数据\n6. **能量检查**: 查询余额,不足返回 402(每次消耗 30 能量)\n7. **获取时区**: 从 `profiles` 表获取用户时区\n8. **获取剧情历史**: 从 `drama_messages` 表获取最近 20 条消息\n9. **构建剧情系统提示**: 包含身份、场景设定、剧情规则、对话历史\n10. **保存用户消息**: 插入 `drama_messages` 表\n11. **SSE 流式响应**: 调用 DeepSeek 生成 AI 回复\n12. **保存 AI 回复**: 插入 `drama_messages` 表\n13. **扣减能量**: 更新余额,记录交易\n14. **更新会话上下文**: 更新 `context_memory` 和 `updated_at`\n\n**数据库操作**:\n\n| 表名 | 操作 | 说明 |\n|------|------|------|\n| `drama_sessions` | SELECT, UPDATE | 会话查询与上下文更新 |\n| `drama_definitions` | SELECT | 剧情定义 |\n| `companions` | SELECT | 伴侣数据 |\n| `drama_messages` | INSERT (x2) | 用户消息和 AI 回复 |\n| `energy_accounts` | SELECT, UPDATE | 能量查询与扣减 |\n| `energy_transactions` | INSERT | 交易流水 |\n| `profiles` | SELECT | 时区 |\n\n**代码关键逻辑说明**:\n\n- **完全沉浸模式**: AI 被严格要求\"忘记自己是 AI\",完全沉浸在剧情角色中\n- **动作与心理描写**: AI 可以用 `*星号*` 标注动作、表情、心理活动\n- **剧情场景驱动**: 系统提示包含 `scene_setting` 和 `drama_prompt`,AI 的回应必须贴合场景\n- **旁白角色**: `speaker='narrator'` 用于场景描述和剧情推进\n\n---\n\n### 3. drama-session\n\n**函数概述**\n\n| 属性 | 说明 |\n|------|------|\n| **功能描述** | 剧情会话管理。提供剧情列表查询、会话创建/获取/完成/重启等操作。 |\n| **重要性** | **高** — 剧情空间入口 |\n| **触发方式** | 前端 HTTP POST 请求 |\n| **调度频率** | 按需调用 |\n\n**HTTP 接口定义**\n\n| 属性 | 值 |\n|------|------|\n| 方法 | `POST` |\n| 路径 | `/functions/v1/drama-session` |\n| 响应类型 | `application/json` |\n\n**请求头**:\n\n| 头字段 | 必填 | 说明 |\n|--------|------|------|\n| `Authorization` | 是 | `Bearer <JWT_TOKEN>` |\n| `Content-Type` | 是 | `application/json` |\n\n**请求体格式**:\n\n```json\n{\n \"action\": \"list|start|get|complete|restart\",\n \"drama_id\": \"uuid\",\n \"session_id\": \"uuid\"\n}\n```\n\n**action 说明**:\n\n| action | 必填参数 | 说明 |\n|--------|----------|------|\n| `list` | 无 | 获取所有可用剧情列表及用户进度 |\n| `start` | `drama_id` | 开始/恢复某个剧情 |\n| `get` | `session_id` | 获取会话详情及消息历史 |\n| `complete` | `session_id` | 完成某个剧情会话 |\n| `restart` | `drama_id` | 重新开始某个剧情(删除旧数据) |\n\n**处理流程(按 action)**:\n\n#### `list` 流程\n\n1. 查询 `drama_definitions` 表中 `is_active=true` 的剧情,按 `sort_order` 排序\n2. 查询 `drama_progress` 表获取用户解锁进度\n3. 查询 `drama_sessions` 表中 `active/paused` 状态的会话\n4. 合并数据:将进度和活跃会话合并到剧情定义中\n5. 返回完整剧情列表(含 `is_unlocked`, `active_session` 等字段)\n\n#### `start` 流程\n\n1. 检查是否已有活跃会话(同一 `user_id + drama_id`,状态为 `active/paused`)\n2. 如果有,直接返回现有会话\n3. 如果没有,获取用户的 `companion_id`\n4. 创建新会话:插入 `drama_sessions`(status='active', context_memory={})\n5. 获取剧情定义,生成开场旁白\n6. 插入开场旁白到 `drama_messages`(speaker='narrator')\n7. 更新 `drama_progress`(标记为已解锁)\n8. 返回会话信息和 `is_new` 标志\n\n#### `get` 流程\n\n1. 验证会话存在且属于当前用户\n2. 查询 `drama_definitions` 获取剧情信息\n3. 查询 `companions` 获取伴侣基本信息\n4. 查询 `drama_messages` 按时间升序获取全部消息\n5. 返回 `{ session, drama, companion, messages }`\n\n#### `complete` 流程\n\n1. 验证会话存在且属于当前用户\n2. 更新会话状态为 `completed`,设置 `ended_at`\n3. 更新 `drama_progress` 标记 `completed_at`\n4. 返回 `{ success: true }`\n\n#### `restart` 流程\n\n1. 查询该用户+剧情的所有旧会话\n2. 循环删除每个会话关联的 `drama_messages`\n3. 删除旧会话记录\n4. 调用 `createOrGetSession` 创建全新会话\n5. 返回新会话信息\n\n**数据库操作**:\n\n| 表名 | 操作 | 说明 |\n|------|------|------|\n| `drama_definitions` | SELECT | 剧情定义查询 |\n| `drama_sessions` | SELECT, INSERT, UPDATE, DELETE | 会话 CRUD |\n| `drama_messages` | SELECT, INSERT, DELETE | 消息操作 |\n| `drama_progress` | SELECT, UPSERT | 进度追踪 |\n| `companions` | SELECT | 获取伴侣 |\n\n**错误处理**:\n\n| 错误码 | 触发条件 |\n|--------|----------|\n| `401` | JWT 无效或缺失 |\n| `500` | 数据库错误或参数缺失 |\n\n---\n\n### 4. energy\n\n**函数概述**\n\n| 属性 | 说明 |\n|------|------|\n| **功能描述** | 能量账户管理。提供余额查询和能量消费功能,使用乐观锁防止并发冲突。 |\n| **重要性** | **高** — 核心经济系统 |\n| **触发方式** | 前端 HTTP GET/POST 请求 |\n| **调度频率** | 按需调用 |\n\n**HTTP 接口定义**\n\n| 属性 | 值 |\n|------|------|\n| 方法 | `GET`, `POST` |\n| 路径 | `/functions/v1/energy` |\n| 响应类型 | `application/json` |\n\n**GET 请求 — 查询余额**\n\n**请求头**:\n\n| 头字段 | 必填 | 说明 |\n|--------|------|------|\n| `Authorization` | 是 | `Bearer <JWT_TOKEN>` |\n\n**响应体**:\n\n```json\n{\n \"balance\": 1250,\n \"total_recharged\": 5000,\n \"total_consumed\": 3750\n}\n```\n\n| 字段 | 类型 | 说明 |\n|------|------|------|\n| `balance` | `number` | 当前余额 |\n| `total_recharged` | `number` | 累计充值 |\n| `total_consumed` | `number` | 累计消费 |\n\n**POST 请求 — 消费能量**\n\n**请求体**:\n\n```json\n{\n \"amount\": 50,\n \"description\": \"chat_message\"\n}\n```\n\n| 字段 | 类型 | 必填 | 说明 | 示例值 |\n|------|------|------|------|--------|\n| `amount` | `number` | 是 | 消费金额(正整数) | `50` |\n| `description` | `string` | 否 | 消费描述 | `\"chat_message\"` |\n\n**成功响应**:\n\n```json\n{\n \"success\": true,\n \"remaining_balance\": 1200\n}\n```\n\n**失败响应(余额不足)**:\n\n```json\n{\n \"success\": false,\n \"reason\": \"insufficient\",\n \"balance\": 30\n}\n```\n\n**失败响应(并发冲突)**:\n\n```json\n{\n \"success\": false,\n \"reason\": \"concurrent_conflict\",\n \"message\": \"Retry needed due to concurrent update\"\n}\n```\n\n**处理流程 (GET)**:\n\n1. 验证 JWT\n2. 查询 `energy_accounts` 表(`SELECT * ... user_id = ?`)\n3. 返回余额信息\n\n**处理流程 (POST)**:\n\n1. 验证 JWT\n2. 解析请求体,验证 `amount > 0`\n3. **乐观锁读取**: `SELECT id, balance, version FROM energy_accounts WHERE user_id = ?`\n4. 检查余额充足性\n5. **条件更新**: `UPDATE energy_accounts SET balance = ?, version = version + 1 WHERE id = ? AND version = ?`\n6. 检查更新行数(`count === 0` 表示并发冲突)\n7. 插入 `energy_transactions` 记录\n8. 返回结果\n\n**数据库操作**:\n\n| 表名 | 操作 | 说明 |\n|------|------|------|\n| `energy_accounts` | SELECT, UPDATE(乐观锁) | 余额查询与更新 |\n| `energy_transactions` | INSERT | 消费流水 |\n\n**乐观锁机制详解**:\n\n```\n读取: balance=1000, version=5\n计算: newBalance=950\n更新: UPDATE ... SET balance=950, version=6 WHERE id=X AND version=5\n ↑ 如果此时 version 已被改为 6,则 count=0,返回并发冲突\n```\n\n---\n\n### 5. payment-create\n\n**函数概述**\n\n| 属性 | 说明 |\n|------|------|\n| **功能描述** | 创建 ZPay 支付订单。根据用户选择的套餐生成支付链接,用户点击后跳转至 ZPay 完成支付。 |\n| **重要性** | **高** — 付费转化入口 |\n| **触发方式** | 前端 HTTP POST 请求 |\n| **调度频率** | 按需调用 |\n\n**HTTP 接口定义**\n\n| 属性 | 值 |\n|------|------|\n| 方法 | `POST` |\n| 路径 | `/functions/v1/payment-create` |\n| 响应类型 | `application/json` |\n\n**请求头**:\n\n| 头字段 | 必填 | 说明 |\n|--------|------|------|\n| `Authorization` | 是 | `Bearer <JWT_TOKEN>` |\n| `Content-Type` | 是 | `application/json` |\n\n**请求体**:\n\n```json\n{\n \"plan_id\": \"uuid_of_pricing_plan\"\n}\n```\n\n| 字段 | 类型 | 必填 | 说明 | 示例值 |\n|------|------|------|------|--------|\n| `plan_id` | `string` | 是 | 套餐 ID(pricing_plans 表主键) | `\"uuid-string\"` |\n\n**响应体**:\n\n```json\n{\n \"order_no\": \"PLATONIC_1718001234567_A3B9\",\n \"payment_url\": \"https://zpayz.cn/submit.php?pid=...&sign=...\",\n \"amount\": \"29.90\",\n \"energy_amount\": 3000\n}\n```\n\n| 字段 | 类型 | 说明 |\n|------|------|------|\n| `order_no` | `string` | 商户订单号 |\n| `payment_url` | `string` | ZPay 支付跳转链接 |\n| `amount` | `string` | 支付金额(元) |\n| `energy_amount` | `number` | 获得的总能量数(含赠送) |\n\n**处理流程**:\n\n1. 验证 JWT\n2. 从请求体获取 `plan_id`\n3. 查询 `pricing_plans` 表验证套餐有效性(`is_active = true`)\n4. 计算金额: `amount = price_cents / 100`(单位:元)\n5. 计算总能量: `totalEnergy = energy_amount + bonus_amount`\n6. 生成订单号: `PLATONIC_${timestamp}_${random}`\n7. 插入 `payment_orders` 表(状态 `pending`,30 分钟过期)\n8. 构建 ZPay 支付参数(含 MD5 签名)\n9. 生成支付 URL\n10. 返回订单信息和支付链接\n\n**数据库操作**:\n\n| 表名 | 操作 | 说明 |\n|------|------|------|\n| `pricing_plans` | SELECT | 套餐信息查询 |\n| `payment_orders` | INSERT | 创建待支付订单 |\n\n**外部 API 调用**:\n\n| 服务 | 说明 |\n|------|------|\n| ZPay (zpayz.cn) | 生成支付跳转 URL,用户浏览器实际跳转到 ZPay |\n\n**安全机制**:\n\n- 使用 CryptoJS.MD5 生成 ZPay 签名\n- 订单号包含时间戳和随机字符串,防止重放攻击\n- `idempotency_key` 确保幂等性\n\n---\n\n### 6. payment-callback\n\n**函数概述**\n\n| 属性 | 说明 |\n|------|------|\n| **功能描述** | ZPay 异步回调处理。接收 ZPay 支付结果通知,验证签名,更新订单状态。**不需要 JWT 验证**(由 ZPay 服务器调用)。 |\n| **重要性** | **极高** — 支付完成的关键链路 |\n| **触发方式** | ZPay 服务器 HTTP GET 回调 |\n| **调度频率** | 支付完成后由 ZPay 触发 |\n\n**HTTP 接口定义**\n\n| 属性 | 值 |\n|------|------|\n| 方法 | `GET` |\n| 路径 | `/functions/v1/payment-callback` |\n| 响应类型 | `text/plain` |\n\n**请求参数 (URL Query String)**:\n\n| 参数 | 类型 | 说明 |\n|------|------|------|\n| `out_trade_no` | `string` | 商户订单号 |\n| `trade_status` | `string` | 交易状态(`TRADE_SUCCESS` 表示成功) |\n| `money` | `string` | 实际支付金额 |\n| `sign` | `string` | ZPay 签名 |\n| `sign_type` | `string` | 签名类型(`MD5`) |\n\n**响应**:\n\n| 响应体 | 说明 |\n|--------|------|\n| `success` | 处理成功(ZPay 要求返回此字符串) |\n| `sign_error` | 签名验证失败 |\n| `error` | 内部错误 |\n\n**处理流程**:\n\n1. 解析 URL 查询参数\n2. 提取 `out_trade_no`、`trade_status`、`money`\n3. 使用 ZPay 密钥重新计算签名\n4. 对比签名(防止篡改)\n5. 查询 `payment_orders` 表获取订单信息\n6. 检查订单是否已处理(`paid` 或 `completed` 状态则跳过)\n7. 如果 `trade_status === 'TRADE_SUCCESS'`:\n - 更新订单状态为 `paid`\n - 设置 `paid_cents`\n - **能量充值由数据库触发器 `trg_payment_orders_status` 自动处理**\n8. 无论结果如何,返回 `success`(ZPay 要求)\n\n**数据库操作**:\n\n| 表名 | 操作 | 说明 |\n|------|------|------|\n| `payment_orders` | SELECT, UPDATE | 订单查询与状态更新 |\n\n**安全机制**:\n\n- **签名验证**: MD5 签名对比,确保请求来自 ZPay\n- **幂等处理**: 已支付订单直接返回 success,防止重复充值\n- **错误静默**: 找不到订单也返回 success(避免 ZPay 重试风暴)\n\n> **关键设计**: 能量充值不在这个函数中直接处理,而是由数据库触发器自动完成。这种设计确保了即使回调处理失败,只要订单状态更新为 paid,能量一定会到账。\n\n---\n\n### 7. achievement-check\n\n**函数概述**\n\n| 属性 | 说明 |\n|------|------|\n| **功能描述** | 成就检测引擎。检查用户行为是否满足成就定义条件,解锁成就并记录进度。支持三种触发类型:一次性、累积型、阈值型。 |\n| **重要性** | **中** — 游戏化系统核心 |\n| **触发方式** | 被其他 Edge Function 调用(chat-stream、consolidation、milestone-adjust 等) |\n| **调度频率** | 事件驱动 |\n\n**HTTP 接口定义**\n\n| 属性 | 值 |\n|------|------|\n| 方法 | `POST` |\n| 路径 | `/functions/v1/achievement-check` |\n| 响应类型 | `application/json` |\n\n**请求头**:\n\n| 头字段 | 必填 | 说明 |\n|--------|------|------|\n| `Authorization` | 是 | `Bearer <JWT_TOKEN>` |\n| `Content-Type` | 是 | `application/json` |\n\n**请求体**:\n\n```json\n{\n \"event_type\": \"chat\",\n \"event_target\": \"send_message\",\n \"event_value\": 1\n}\n```\n\n| 字段 | 类型 | 必填 | 默认值 | 说明 |\n|------|------|------|--------|------|\n| `event_type` | `string` | 否 | `\"chat\"` | 事件类型 |\n| `event_target` | `string` | 否 | `\"send_message\"` | 事件目标 |\n| `event_value` | `number` | 否 | `1` | 事件数值 |\n\n**响应体**:\n\n```json\n{\n \"unlocked\": [\"初次对话\", \"百次交流\"],\n \"errors\": []\n}\n```\n\n**处理流程**:\n\n1. 验证 JWT 获取用户 ID\n2. 从请求体获取 `event_type`、`event_target`、`event_value`\n3. 查询 `achievement_definitions` 表中 `is_active = true` 的成就定义\n4. 过滤匹配的成就定义:\n - `one_time` + `trigger_target` 匹配事件目标\n - `cumulative` + `trigger_target` 匹配\n - `threshold` + `trigger_target` 匹配且 `event_value >= trigger_value`\n5. 对每个匹配成就:\n - 检查 `achievement_progress` 表是否已解锁\n - 如果已解锁则跳过\n - 计算新进度值: `newValue = current_value + event_value`\n - 判断是否满足解锁条件\n - 如果满足: upsert progress 记录(标记 `is_unlocked=true`, `unlocked_at=now`)\n - 如果不满足: 仅更新进度值\n6. 返回解锁的成就列表和错误列表\n\n**数据库操作**:\n\n| 表名 | 操作 | 说明 |\n|------|------|------|\n| `achievement_definitions` | SELECT | 成就定义查询 |\n| `achievement_progress` | SELECT, UPSERT | 成就进度查询与更新 |\n\n**成就触发类型详解**:\n\n| 类型 | 说明 | 示例 |\n|------|------|------|\n| `one_time` | 一次性成就,完成即解锁 | \"初次对话\" |\n| `cumulative` | 累积型,累计达到阈值解锁 | \"百次交流\"(累计 100 次) |\n| `threshold` | 阈值型,单次事件值达到阈值解锁 | \"单次长对话\"(一次 50 条消息) |\n\n---\n\n### 8. consolidation\n\n**函数概述**\n\n| 属性 | 说明 |\n|------|------|\n| **功能描述** | 记忆整合系统(STM -> LTM -> 远事记忆)。对话结束后(最后消息 > 1 小时),由 AI 分析对话内容,提取结构化长期记忆,进行语义去重后合并入库。同时生成日记条目和远事记忆任务。 |\n| **重要性** | **极高** — AI 记忆系统的核心引擎 |\n| **触发方式** | 被 chat-stream 异步调用 / 被 pg_cron 定时调用 |\n| **调度频率** | 每 15 分钟(pg_cron)|\n\n**HTTP 接口定义**\n\n| 属性 | 值 |\n|------|------|\n| 方法 | `POST` |\n| 路径 | `/functions/v1/consolidation` |\n| 响应类型 | `application/json` |\n\n**请求头**:\n\n| 头字段 | 必填 | 说明 |\n|--------|------|------|\n| `Authorization` | 否 | `Bearer <JWT_TOKEN>`(pg_cron 调用时使用 service_key) |\n\n**请求体**: 无(函数扫描所有伴侣)\n\n**响应体**:\n\n```json\n{\n \"processed\": 5,\n \"skipped\": 12,\n \"memories_inserted\": 8,\n \"memories_merged\": 3,\n \"anterior_created\": 4,\n \"companions\": 17,\n \"elapsed_sec\": \"45.2\"\n}\n```\n\n**处理流程**:\n\n1. 获取 Supabase 客户端\n2. 查询所有活跃用户伴侣(排除测试用户 `'00000000-...'`)\n3. 对每个伴侣循环处理:\n 1. **获取最后消息时间**: 查询 `stm_messages` 最新一条\n 2. **判断对话是否结束**: 最后消息 > 1 小时则认为对话已结束\n 3. **获取上次整合时间**: 查询 `companion_consolidations` 表\n 4. **获取待处理消息**: 获取上次整合后(或 24 小时内)的新消息,至少 3 条才处理\n 5. **获取上下文数据**: 现有 LTM(最多 50 条)+ 往日日记(最近 3 篇)\n 6. **AI 提取记忆**: 调用 DeepSeek 分析对话,提取结构化记忆\n 7. **去重与入库**: 逐条检查是否重复,新记忆插入,相似记忆合并\n 8. **创建远事记忆**: 插入 `anterior_memories` 表\n 9. **生成日记**: 插入 `companion_diaries` 表\n 10. **更新整合记录**: upsert `companion_consolidations`\n4. 返回处理统计\n\n**AI 提取的记忆结构**:\n\n```typescript\ninterface ExtractedMemory {\n content: string; // 记忆内容(简洁清晰,第三人称描述)\n type: 'fact' | 'preference' | 'event' | 'emotion' | 'relationship' | 'goal';\n importance: number; // 0.0-1.0\n is_permanent: boolean; // 是否永久保存\n confidence: number; // 0.0-1.0 AI 置信度\n source_stm_ids: string[]; // 来源 STM 消息 ID\n context_quote: string; // 支撑证据的对话原文\n time_anchor?: string; // 时间锚点描述\n}\n```\n\n**去重算法**:\n\n1. **精确匹配**: 内容完全相同的跳过\n2. **子串匹配**: 长度 > 10 且互相包含的跳过\n3. **语义重叠**: 同类型 + 70% 以上词语重叠的跳过\n\n**数据库操作**:\n\n| 表名 | 操作 | 说明 |\n|------|------|------|\n| `companions` | SELECT | 获取所有伴侣 |\n| `stm_messages` | SELECT | 获取待处理消息 |\n| `companion_consolidations` | SELECT, UPSERT | 整合记录 |\n| `ltm_memories` | SELECT, INSERT, UPDATE | 长期记忆 CRUD |\n| `companion_diaries` | SELECT, INSERT | 日记 |\n| `anterior_memories` | INSERT | 远事记忆任务 |\n\n**外部 API 调用**:\n\n| 服务 | 调用次数 | 说明 |\n|------|----------|------|\n| DeepSeek API | 每伴侣 1 次 | 记忆提取(JSON 输出,最多 3 次重试) |\n\n**代码关键逻辑说明**:\n\n- **重试机制**: DeepSeek 调用失败时指数退避重试(1s, 2s, 4s),30 秒超时\n- **置信度过滤**: 置信度 < 0.5 的记忆被丢弃\n- **批量处理**: 每次最多处理所有伴侣,每个伴侣独立失败不影响其他\n- **日记生成**: AI 以伴侣第一人称写 200-400 字的温暖日记\n\n---\n\n### 9. milestone-adjust\n\n**函数概述**\n\n| 属性 | 说明 |\n|------|------|\n| **功能描述** | 亲密度里程碑调整系统。每日凌晨自动评估所有用户与伴侣的关系进展,基于多维度算法调整亲密度分数,检测里程碑跨越事件。 |\n| **重要性** | **高** — 关系系统的核心 |\n| **触发方式** | pg_cron 每日凌晨 3 点调用 |\n| **调度频率** | 每日 1 次 |\n\n**HTTP 接口定义**\n\n| 属性 | 值 |\n|------|------|\n| 方法 | `POST` |\n| 路径 | `/functions/v1/milestone-adjust` |\n| 响应类型 | `application/json` |\n\n**请求头**: 无特殊要求(由 pg_cron 使用 service_key 调用)\n\n**响应体**:\n\n```json\n{\n \"adjusted\": 5,\n \"total_companions\": 12,\n \"duration_ms\": 45200,\n \"timestamp\": \"2025-06-01T03:00:45.200Z\",\n \"details\": [...]\n}\n```\n\n**处理流程**:\n\n1. 获取所有伴侣列表\n2. 批量获取所有亲密度记录\n3. 对每个伴侣执行评估:\n 1. **获取最近 24 小时消息**: 从 `stm_messages` 表\n 2. **获取最近 24 小时 LTM**: 从 `ltm_memories` 表\n 3. **获取最后消息时间**: 计算距今天数\n 4. **LTM 记忆评分**: 根据记忆类型加分\n 5. **情绪质量评分**: 根据正负情绪占比加分/减分\n 6. **对话质量评分**: 根据消息数量和深度加分\n 7. **不活跃惩罚**: 按未对话天数减分\n 8. **DeepSeek 辅助分析**(可选,不阻塞): 分析对话摘要,生成关系评分\n 9. **汇总计算**: 各维度 delta 求和\n 10. **计算新分数**: `newScore = max(0, min(100, currentScore + totalDelta))`\n 11. **检测里程碑事件**: 检查是否跨越阈值(20, 40, 60, 80)\n 12. **保存记录**: upsert `intimacy_records`\n 13. **记录里程碑日记**: 如有跨越则插入 `companion_diaries`\n4. 返回处理统计\n\n**评分维度详解**:\n\n#### LTM 记忆评分\n\n| 记忆类型 | 条件 | 加分 |\n|----------|------|------|\n| `fact` | `is_permanent = true` 且 `importance >= 0.8` | +3 |\n| `preference` | 无条件 | +2 |\n| `emotion` | `importance >= 0.6` | +2 |\n| `event` | 无条件 | +1 |\n| `relationship` | 无条件 | +3 |\n| 其他 | `importance >= 0.7` | +1 |\n\n#### 情绪质量评分\n\n| 条件 | 加分 |\n|------|------|\n| 正向情绪占比 > 60% | +2 |\n| 负向情绪占比 > 60% | -2 |\n\n#### 对话质量评分\n\n| 条件 | 加分 |\n|------|------|\n| 消息数量 >= 50 | +3 |\n| 消息数量 >= 20 | +2 |\n| 平均消息长度 > 50 字 | +1 |\n| 用户主动开启对话 | +1 |\n\n#### 不活跃惩罚\n\n| 未对话天数 | 减分 |\n|------------|------|\n| >= 30 天 | -10 |\n| >= 7 天 | -3 |\n| >= 3 天 | -1 |\n| < 3 天 | 0 |\n\n#### DeepSeek 辅助评分\n\n- 调用 DeepSeek 分析最近 30 条对话\n- 返回 `relationship_score` (0-10)\n- 转换为 delta: `round((score - 5) * 0.5)`,范围 [-2.5, +2.5]\n- 5 秒超时,失败不影响主流程\n\n**里程碑阈值**:\n\n| 阶段 | 分数范围 | 名称 |\n|------|----------|------|\n| 1 | 0-20 | 初见乍欢 |\n| 2 | 21-40 | 渐入佳境 |\n| 3 | 41-60 | 暗生情愫 |\n| 4 | 61-80 | 情投意合 |\n| 5 | 81-100 | 心有灵犀 |\n\n**数据库操作**:\n\n| 表名 | 操作 | 说明 |\n|------|------|------|\n| `companions` | SELECT | 获取伴侣列表 |\n| `intimacy_records` | SELECT, UPSERT | 亲密度记录 |\n| `stm_messages` | SELECT | 获取消息 |\n| `ltm_memories` | SELECT | 获取 LTM |\n| `companion_diaries` | INSERT | 里程碑日记 |\n\n---\n\n### 10. proactive\n\n**函数概述**\n\n| 属性 | 说明 |\n|------|------|\n| **功能描述** | AI 主动消息生成。伴侣在特定时机主动给用户发送消息,营造真实陪伴感。基于时间感知、亲密度、长期记忆和待办事项生成个性化主动消息。 |\n| **重要性** | **中** — 提升沉浸感的关键功能 |\n| **触发方式** | 前端 HTTP POST 请求 / proactive-scheduler 调用 |\n| **调度频率** | 由 scheduler 控制(随机 2 分钟 ~ 24 小时) |\n\n**HTTP 接口定义**\n\n| 属性 | 值 |\n|------|------|\n| 方法 | `POST` |\n| 路径 | `/functions/v1/proactive` |\n| 响应类型 | `application/json` |\n\n**请求头**:\n\n| 头字段 | 必填 | 说明 |\n|--------|------|------|\n| `Authorization` | 是 | `Bearer <JWT_TOKEN>` |\n| `Content-Type` | 是 | `application/json` |\n\n**请求体**:\n\n```json\n{\n \"companion_id\": \"companion_uuid\"\n}\n```\n\n| 字段 | 类型 | 必填 | 说明 |\n|------|------|------|------|\n| `companion_id` | `string` | 否 | 伴侣 ID(不传则自动选择用户的伴侣) |\n\n**响应体**:\n\n```json\n{\n \"success\": true,\n \"message\": \"你今天过得怎么样呀~\",\n \"energy_deducted\": 10,\n \"balance_after\": 1240\n}\n```\n\n**处理流程**:\n\n1. 验证 JWT\n2. 获取伴侣(按 `companion_id` 或按 `user_id` 查找)\n3. **能量检查**: 检查余额是否 >= 10,不足则跳过\n4. **获取用户时区**: 从 `profiles` 表\n5. **获取亲密度**: 从 `intimacy_records` 表\n6. **获取 LTM 记忆**: 按重要性降序,最多 15 条\n7. **获取远事记忆**: `status='active'`,按优先级排序,最多 5 条\n8. **获取 STM 历史**: 最近 10 条消息\n9. **计算距上次回复时间**: 计算小时数\n10. **构建主动消息提示**: 包含时间感知、人格画像、记忆、待办事项\n11. **调用 DeepSeek 生成消息**\n12. **扣减能量**: 10 能量\n13. **保存到 STM**: 插入 `stm_messages`(speaker='companion')\n14. **更新调度记录**: upsert `proactive_schedule`\n\n**时间感知语调调整**:\n\n| 距上次回复 | 语调指令 |\n|------------|----------|\n| < 0.5 小时 | 轻松自然,随口分享 |\n| < 2 小时 | 温暖带一点俏皮 |\n| < 6 小时 | 温柔关心,问问在忙什么 |\n| < 12 小时 | 关切温柔,提醒注意休息 |\n| < 24 小时 | 思念牵挂,表达不习惯 |\n| >= 24 小时 | 淡淡思念和委屈,自言自语式想念 |\n\n**数据库操作**:\n\n| 表名 | 操作 | 说明 |\n|------|------|------|\n| `companions` | SELECT | 伴侣数据 |\n| `energy_accounts` | SELECT, UPDATE | 能量查询与扣减 |\n| `energy_transactions` | INSERT | 交易流水 |\n| `profiles` | SELECT | 时区 |\n| `intimacy_records` | SELECT | 亲密度 |\n| `ltm_memories` | SELECT | 长期记忆 |\n| `anterior_memories` | SELECT | 远事记忆 |\n| `stm_messages` | INSERT | 保存主动消息 |\n| `proactive_schedule` | UPSERT | 调度记录 |\n\n---\n\n### 11. proactive-scheduler\n\n**函数概述**\n\n| 属性 | 说明 |\n|------|------|\n| **功能描述** | 主动消息定时调度器。由 pg_cron 每分钟调用,扫描到期的调度计划,逐个触发主动消息生成。每个用户独立处理,失败不影响其他用户。 |\n| **重要性** | **中** — 主动消息系统的心脏 |\n| **触发方式** | pg_cron 每分钟调用 |\n| **调度频率** | 每分钟 1 次 |\n\n**HTTP 接口定义**\n\n| 属性 | 值 |\n|------|------|\n| 方法 | `POST` |\n| 路径 | `/functions/v1/proactive-scheduler` |\n| 响应类型 | `application/json` |\n\n**请求头**:\n\n| 头字段 | 必填 | 说明 |\n|--------|------|------|\n| `x-scheduler-secret` | 条件 | `SCHEDULER_SECRET` 环境变量的值(如果配置了) |\n\n**请求体**: 无\n\n**响应体**:\n\n```json\n{\n \"processed\": 3,\n \"results\": [\n { \"user_id\": \"uuid\", \"status\": \"success\", \"message\": \"你好呀~\" },\n { \"user_id\": \"uuid\", \"status\": \"skipped\", \"error\": \"insufficient_energy\" }\n ]\n}\n```\n\n**处理流程**:\n\n1. **认证**: 验证 `x-scheduler-secret`(如果配置了 `SCHEDULER_SECRET`)\n2. 获取 Supabase 客户端\n3. **查询到期计划**: 从 `proactive_schedule` 表中查询:\n - `next_trigger_at <= now()`\n - `is_triggered = false`\n - 最多 10 条(防止单次处理过多)\n4. 如果没有到期计划,返回 `processed: 0`\n5. **逐个处理用户**:\n - 调用 `processUser()` 内部函数\n - 包含完整的 proactive 消息生成逻辑(与 proactive function 相同)\n6. **成功处理**:\n - 计算下次触发时间(随机 45-75 分钟,避开深夜 23:00-08:00)\n - upsert `proactive_schedule`(`is_triggered = false`, 设置新 `next_trigger_at`)\n7. **失败处理**:\n - 标记 `is_triggered = true`(防止无限重试)\n - 记录失败原因\n8. 返回处理结果\n\n**下次触发时间计算**:\n\n1. 基础间隔: 45 + random(0~30) 分钟\n2. 检查目标时间是否在用户时区的 23:00-08:00 之间\n3. 如果是,推迟到次日 08:30-09:30 之间\n4. 返回 ISO 格式时间字符串\n\n**数据库操作**:\n\n| 表名 | 操作 | 说明 |\n|------|------|------|\n| `proactive_schedule` | SELECT, UPSERT, UPDATE | 调度计划查询与更新 |\n| (间接通过 processUser) | — | 涉及 energy、stm、ltm、anterior 等全部表 |\n\n---\n\n### 12. seed-data\n\n**函数概述**\n\n| 属性 | 说明 |\n|------|------|\n| **功能描述** | 种子数据初始化。为新环境或测试快速创建完整的数据集:测试用户、能量账户、伴侣、亲密度、情绪记录、测试消息。 |\n| **重要性** | **低** — 仅用于开发和测试 |\n| **触发方式** | 前端/开发者 HTTP POST 调用 |\n| **调度频率** | 一次性/按需 |\n\n**HTTP 接口定义**\n\n| 属性 | 值 |\n|------|------|\n| 方法 | `POST` |\n| 路径 | `/functions/v1/seed-data` |\n| 响应类型 | `application/json` |\n\n**请求头**:\n\n| 头字段 | 必填 | 说明 |\n|--------|------|------|\n| `Authorization` | 是 | `Bearer <JWT_TOKEN>`(service_role) |\n| `Content-Type` | 是 | `application/json` |\n\n**请求体**:\n\n```json\n{\n \"action\": \"full\",\n \"user_id\": \"11111111-1111-1111-1111-111111111111\"\n}\n```\n\n| 字段 | 类型 | 必填 | 默认值 | 说明 |\n|------|------|------|--------|------|\n| `action` | `string` | 否 | `\"full\"` | 操作类型:`full`, `energy`, `companion`, `messages`, `profile` |\n| `user_id` | `string` | 否 | 固定测试 UUID | 目标用户 ID |\n\n**处理流程**:\n\n1. 创建测试账号(调用 `create_test_account` RPC)\n2. 根据 `action` 创建对应数据:\n - `energy`: 创建能量账户(balance=10000)\n - `companion`: 创建伴侣\"小樱\"(含五大人格参数)\n - `messages`: 创建 9 条测试对话消息\n - `profile`: 创建用户配置\n3. 如果创建伴侣成功,同时创建:\n - 亲密度记录(score=53, stage=3 暗生情愫)\n - 情绪记录(pleasure=0.3, arousal=0.2)\n\n**数据库操作**:\n\n| 表名 | 操作 | 说明 |\n|------|------|------|\n| `energy_accounts` | UPSERT | 能量账户 |\n| `companions` | SELECT, INSERT | 伴侣(先查后插) |\n| `intimacy_records` | UPSERT | 亲密度 |\n| `mood_records` | UPSERT | 情绪 |\n| `stm_messages` | DELETE, INSERT | 消息(先删后插) |\n| `profiles` | UPSERT | 用户配置 |\n\n---\n\n### 13. setup-proactive\n\n**函数概述**\n\n| 属性 | 说明 |\n|------|------|\n| **功能描述** | 初始化主动消息系统的数据库结构。创建必要的函数、索引、触发器和 pg_cron 定时任务。一次性执行,用于新环境部署。 |\n| **重要性** | **低** — 仅用于初始化 |\n| **触发方式** | 开发者手动调用 |\n| **调度频率** | 一次性 |\n\n**HTTP 接口定义**\n\n| 属性 | 值 |\n|------|------|\n| 方法 | `POST` |\n| 路径 | `/functions/v1/setup-proactive` |\n| 响应类型 | `application/json` |\n\n**请求头**: 无(`verify_jwt: false`)\n\n**请求体**: 无\n\n**响应体**:\n\n```json\n{\n \"overall\": \"success\",\n \"steps\": [\n { \"step\": 1, \"name\": \"check_table\", \"status\": \"ok\", \"exists\": true },\n { \"step\": 4, \"name\": \"create_exec_sql\", \"status\": \"ok\", \"method\": \"pg_execute_rpc\" },\n { \"step\": 5, \"name\": \"create_index\", \"status\": \"ok\" },\n { \"step\": 6, \"name\": \"pg_cron\", \"status\": \"ok\" }\n ]\n}\n```\n\n**处理流程**:\n\n1. 检查 `proactive_schedule` 表是否存在\n2. 尝试通过 `pg_execute` RPC 创建 `exec_sql` 函数\n3. 如果 `exec_sql` 创建成功,依次执行:\n - 创建索引 `idx_proactive_schedule_trigger`\n - 创建 `update_proactive_schedule_updated_at()` 触发器函数\n - 创建 `trigger_proactive_schedule_updated_at` 触发器\n - 启用 RLS\n4. 配置 pg_cron:\n - 先取消已有任务(`cron.unschedule`)\n - 创建每分钟执行的新任务\n5. 验证 cron 任务是否创建成功\n6. 返回各步骤执行结果\n\n**创建的 pg_cron 任务**:\n\n```sql\nSELECT cron.schedule('proactive-scheduler', '* * * * *', \n $$SELECT net.http_post(\n url := '<SUPABASE_URL>/functions/v1/proactive-scheduler',\n headers := jsonb_build_object('Authorization', 'Bearer <SERVICE_KEY>', 'Content-Type', 'application/json'),\n body := '{}'::jsonb\n )$$);\n```\n\n**数据库操作**:\n\n| 操作 | 说明 |\n|------|------|\n| `CREATE FUNCTION exec_sql` | 动态 SQL 执行函数 |\n| `CREATE INDEX` | 触发时间索引 |\n| `CREATE TRIGGER` | 自动更新 updated_at |\n| `cron.schedule()` | 每分钟调度任务 |\n\n---\n\n## 四、部署配置汇总\n\n### verify_jwt 设置\n\n| Edge Function | verify_jwt | 说明 |\n|---------------|------------|------|\n| `chat-stream` | `true` | 需用户登录 |\n| `drama-chat` | `true` | 需用户登录 |\n| `drama-session` | `true` | 需用户登录 |\n| `energy` | `true` | 需用户登录 |\n| `payment-create` | `true` | 需用户登录 |\n| `payment-callback` | `false` | ZPay 服务器调用,无需 JWT |\n| `achievement-check` | `true` | 需用户登录 |\n| `consolidation` | `true/false` | 可被 pg_cron 调用,支持 service_key |\n| `milestone-adjust` | `false` | 由 pg_cron 调用 |\n| `proactive` | `true` | 需用户登录 |\n| `proactive-scheduler` | `false` | 由 pg_cron 调用,使用 scheduler_secret |\n| `seed-data` | `true` | 需管理员权限 |\n| `setup-proactive` | `false` | 一次性初始化 |\n\n### CORS 设置汇总\n\n| Edge Function | Allow-Origin | 特殊头字段 |\n|---------------|-------------|------------|\n| 大多数 | `https://platonic.corolas.top` | `x-requested-with` |\n| `proactive-scheduler` | `*` | 无 |\n| `setup-proactive` | `*` | 无 |\n\n---\n\n## 五、数据库表关联图\n\n```\n+------------------+ +------------------+ +------------------+\n| auth.users |<---| companions |<---| intimacy_records|\n+------------------+ +------------------+ +------------------+\n | | |\n | | v\n v v +------------------+\n+------------------+ +------------------+ | companion_diaries|\n| profiles | | stm_messages | +------------------+\n+------------------+ +------------------+ |\n | | v\n | v +------------------+\n | +------------------+ |ltm_memories |\n | | mood_records | +------------------+\n | +------------------+ |\n | v\n | +------------------+\n | |anterior_memories |\n | +------------------+\n |\n v\n+------------------+ +------------------+ +------------------+\n| energy_accounts |<--->| energy_transactions| | payment_orders |\n+------------------+ +------------------+ +------------------+\n |\n v\n+------------------+ +------------------+\n|drama_definitions |<--->| drama_sessions |\n+------------------+ +------------------+\n |\n v\n +------------------+\n | drama_messages |\n +------------------+\n |\n v\n +------------------+\n | drama_progress |\n +------------------+\n\n+------------------+ +------------------+\n|achievement_defs |<--->|achievement_progress|\n+------------------+ +------------------+\n\n+---------------------------+\n| companion_consolidations |\n+---------------------------+\n\n+---------------------------+\n| proactive_schedule |\n+---------------------------+\n```\n\n---\n\n## 六、外部依赖汇总\n\n| 服务 | URL | 用途 | 调用函数 |\n|------|-----|------|----------|\n| DeepSeek API | `https://api.deepseek.com/v1/chat/completions` | AI 对话、情绪检测、记忆提取、关系分析 | chat-stream, drama-chat, consolidation, milestone-adjust, proactive, proactive-scheduler |\n| ZPay | `https://zpayz.cn/submit.php` | 支付网关 | payment-create, payment-callback |\n| Supabase REST | `<SUPABASE_URL>/rest/v1` | 数据库操作 | 全部 |\n| Supabase Functions | `<SUPABASE_URL>/functions/v1` | Edge Function 间调用 | chat-stream -> consolidation |\n\n---\n\n## 七、安全策略总览\n\n### 7.1 认证机制\n\n| 层级 | 机制 | 说明 |\n|------|------|------|\n| API Gateway | JWT (Supabase Auth) | 通过 `Authorization: Bearer <token>` 头验证 |\n| Service Role | `SUPABASE_SERVICE_ROLE_KEY` | 绕过 RLS,用于内部服务调用 |\n| Scheduler | `SCHEDULER_SECRET` | `x-scheduler-secret` 头验证 |\n\n### 7.2 能量消费安全\n\n- 每次对话消费 50 能量\n- 剧情对话消费 30 能量\n- 主动消息消费 10 能量\n- 使用乐观锁(`version` 字段)防止并发冲突\n- 余额不足返回 402 状态码\n\n### 7.3 支付安全\n\n- MD5 签名验证(ZPay 回调)\n- 订单号包含时间戳 + 随机数,防止重放\n- 幂等性键确保同一请求不会重复创建订单\n- 支付状态由数据库触发器自动处理能量充值\n\n### 7.4 CORS 安全\n\n- 生产环境限定 `https://platonic.corolas.top`\n- 仅允许 `POST`, `GET`, `OPTIONS` 方法\n- 暴露 `authorization, x-client-info, apikey, content-type` 头\n\n### 7.5 RLS (Row Level Security)\n\n- `proactive_schedule` 表启用 RLS\n- 大多数数据访问通过 service_role_key 绕过 RLS(后端函数内部使用)\n- 前端直接访问数据库需通过 RLS 策略\n\n---\n\n*文档结束 — Corolas | Platonic Edge Functions 技术文档 v1.0*\n"}}}}