# 设计

xiangqin 把交友 app 的信息不对称戳破：排序规则公开 + 匹配算法在用户端 + 触达协议透明。

## 三原则

1. **查询权在用户**：服务端只提供受限 WHERE DSL，不做推荐 / 排序算分。
2. **触达透明**：虚拟信封私信，每天免费 3 封，超额 ¥1/封；匿名代理双方 agent gateway URL/token 互不透露。
3. **不做聊天**：xiangqin 只存单次信件（30 天 TTL），不做对话 UI / 线程 / 历史。

## 数据层（sqlite）

以 ULID 为主键、WAL 模式、HMAC 手机号的 sqlite 单库。高层 4 个域：

- **身份**：users / auth_requests / sessions
- **资料**：profiles（对外字段 + 可选 agent gateway 配置）
- **私信**：messages / envelope_* / reports / blocks / notify_state

## 受限 WHERE DSL

- **字段白名单**：`gender` / `age` / `city` / `tags` / `height` / `education`
- **操作符白名单**：`= != >= <= > <` / `IN` / `CONTAINS`（tags 专用）
- **值白名单**：字符串 / 整数 / 逗号列表；禁 `OR` / `JOIN` / 子查询
- **LIMIT** 默认 50，最大 100
- **WHERE 不能为空**

示例：

```
gender=f AND city=hangzhou AND age>=25 AND age<=30
gender IN (f,nb) AND tags CONTAINS 登山
height>=170 AND education IN (bachelor,master)
```

## 虚拟信封模型（M4 触达架构）

**核心：信箱 + 通知分离**

- **信箱**是基础设施，注册即开，所有用户都有；信件落库后 30 天 TTL
- **通知**是可选上层，需要 `agent_gateway_url + agent_hooks_token + notify_on_new_mail=true` 三个字段齐全才触发

**付费规则**：每用户每天免费 3 封信封（UTC 0 点重置），超额 ¥1/封（充值信封池）。投递失败（对方拉黑 / 注销）不扣。

**通知去重**：

```
新信到达 B →
  B 没开通知 / 无 gateway → 不推（notify_status = not_enabled）
  B 开通知 →
    B 当前无未读（除当前）→ 推（pushed）
    B 有未读 + 距上次通知 < 12h → 不推（suppressed_dup）
    B 有未读 + 距上次通知 ≥ 12h → 推（兜底）
```

**推送协议**：OpenClaw `POST /hooks/agent` 标准（任何实现此协议的 agent 框架都可接入，不绑 OpenClaw）：

```
POST {agent_gateway_url}/hooks/agent
Authorization: Bearer {agent_hooks_token}
{
  "message": "你收到一封新 xiangqin 私信...",
  "name": "xiangqin",
  "deliver": true,
  "channel": "last"
}
```

**匿名代理**：A、B 双方互不知对方 `agent_gateway_url` / `hooks_token` / `user_id` → 真实身份的映射，xiangqin 服务端唯一持有，作为中间人转发。

## 信件状态（5 维度）

每条 message 5 维并行：

| 维度 | 可能值 |
|---|---|
| 投递 | `delivered` / `blocked_by_recipient`（退信封）/ `recipient_not_found`（不落库直接抛）|
| 通知 | `not_enabled` / `pushed` / `push_failed` / `suppressed_dup` |
| 阅读 | `unread` → `read`（B 跑 `xq inbox show` 时）|
| 回应 | `no_reply` / `replied`（带 `reply_to_message_id`）|
| 处置 | `none` / `reported` / `deleted_by_recipient`（服务端仍留 30 天）|

## 排序语义

```sql
SELECT ... FROM profiles p
  JOIN users u ON u.id=p.user_id
  WHERE u.deleted_at IS NULL
    AND <user-dsl>
  ORDER BY p.updated_at DESC
  LIMIT <N>;
```

新资料 / 新活跃的人排前。**没有付费置顶**——付费点在私信（主动联系），不在被查看。

## 组件

```
┌─ client ────────────┐      ┌─ server (epsilon) ────┐
│ xq CLI (Click)      │ HTTP │ FastAPI + sqlite       │
│ xiangqin skill      │─────▶│ auth / profile / query │
└─────────────────────┘      │ mail (虚拟信封)        │
                              └────────────────────────┘
                                       │
                                       └─▶ aliyun SMS（注册验证码）

凭证：vault.json 声明 → vault install → .vault/secrets.json → 代码读
```

## 非目标（宪法）

- ❌ 不做推荐算法
- ❌ **不做聊天** —— xiangqin 只转发**单次信件**，不建对话线程 / 不存会话历史 / 不做群组
- ❌ 不做 UGC 社区
- ❌ 不做匹配评分 / 相似度
- ❌ 不存手机号明文
- ❌ 不过滤私信内容（用户意图就是换联系方式，强过滤违反用户意愿；保留 30 天日志用于举报追诉）

## 自治单体实验

- **唯一对外 CLI 依赖**：vault
- **不依赖**：生态里 matchmaker / cashier / backup / membership / host / oss / hitch
- **背景**：跑通 0.1 验证付费意愿后才决定拆不拆；拆不拆看 O2 指标
