{"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\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\nexport async function chatJSON(messages, options): Promise\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\nexport async function checkEnergy(supabase: any, userId: string, cost: number): Promise\nexport async function consumeEnergy(supabase: any, userId: string, cost: number, description: string): Promise\nexport async function getCompanionForUser(supabase: any, userId: string): Promise\nexport async function getRecentMessages(supabase: any, companionId: string, limit: number): Promise\nexport async function getLTMMemories(supabase: any, companionId: string, limit: number): Promise\nexport async function getMoodRecord(supabase: any, companionId: string): Promise\nexport async function getAnteriorMemories(supabase: any, companionId: string): Promise\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, key: string): string\nexport function createZpayOrder(params: object): { url: string; sign: string; }\nexport function verifyZpayCallback(params: Record, 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 ` |\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: ` | AI 生成的文本片段(逐字/逐词流式推送) |\n| `data: [DONE]` | 流式传输完成标志 |\n| `data: [ERROR] ` | 发生错误 |\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 ` |\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 ` |\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 ` |\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 ` |\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 ` |\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 `(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 ` |\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 `(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 := '/functions/v1/proactive-scheduler',\n headers := jsonb_build_object('Authorization', 'Bearer ', '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 | `/rest/v1` | 数据库操作 | 全部 |\n| Supabase Functions | `/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 ` 头验证 |\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"}}}}