# Y-Send 开发文档

Y-Send 只有一个核心接口，所有能发起 HTTP 请求的语言和工具都可以调用。

- 网页版：https://send.yimeijia.cc/docs

## 快速开始

1. 打开控制台 https://send.yimeijia.cc/app ，用懿美嘉账号登录（账号由懿美嘉中央账号服务统一管理，无需在 Y-Send 单独注册），首次登录系统会自动分配一个 SendKey。
2. 在控制台为 SendKey 配置消息通道（企业微信 / 钉钉 / 飞书群机器人 webhook）。
3. 把下面 URL 中的 `<SENDKEY>` 换成你自己的，粘贴到浏览器地址栏即可收到第一条消息：

```
https://send.yimeijia.cc/<SENDKEY>.send?title=你好&desp=这是第一条消息
```

## 发送消息

```
GET 或 POST  https://send.yimeijia.cc/<SENDKEY>.send
```

支持三种传参方式：GET query、POST 表单（`application/x-www-form-urlencoded`）、POST JSON。

| 参数 | 必填 | 说明 |
| --- | --- | --- |
| `title` | 否 | 消息标题，最长 32 字符，超长截断，缺省为「无标题」 |
| `desp` | 否 | 消息正文，最长 32KB，企业微信 / 钉钉通道支持 Markdown |
| `idempotency_key` | 否 | 幂等键（也可用 `Idempotency-Key` 请求头，头优先）。1~128 字符、不含控制字符；同一 SendKey + 同一键的重复请求不会重复投递外部通道 |

> ⚠ URL 中的中文需要百分号编码（浏览器和绝大多数 HTTP 库会自动处理）。

### 投递语义

每条消息带 `delivery` 状态：`success`（全部通道成功或仅站内保存）/ `failed`（结果已落盘，有通道失败）/ `pending`（投递结果未落盘的中间态）/ `unknown`（结果无法确认）。响应 `data.delivery` 可读取该状态。

- **携带幂等键**：同键重试返回首次持久化结果（相同 pushid、相同 code，带 `X-Idempotent-Replay: 1` 头），绝不重复调用外部通道；若首次「外部已投递但结果未落盘」，同键重试返回 `40006 DELIVERY_UNKNOWN` 与原 pushid，同样不会重复投递。幂等记录重启后仍有效，随消息一起保留：在该账号最近 200 条消息（全局最近 2000 条）范围内有效，更早的记录被淘汰后，同一个键会被当作新请求。
- **不携带幂等键**：每次请求独立投递，**至少一次**语义——在结果未知时盲目重试可能产生重复通知，需要去重请务必携带幂等键。

### 成功响应

```json
{"code":0,"message":"","data":{"pushid":"8f3ad2...","readkey":"","error":"SUCCESS","errno":0,"delivery":"success"}}
```

### 通道投递失败响应

只要有一个已配置通道投递失败（网络错误、超时、HTTP 非 2xx、平台错误码、重定向），响应就是非 0 状态，**不会**伪装成 SUCCESS。此时结果已经落盘：**用同一个幂等键重试只会拿回这份结果，不会重新投递**；确需重投请换一个新的幂等键——等于新发一条消息、占用一次每日额度，请由调用方显式决定，不要自动重投：

```json
{"code":40004,"message":"通道投递失败：webhook","data":{
  "pushid":"8f3ad2...","error":"CHANNEL_FAILED","errno":40004,"delivery":"failed",
  "results":{"page":{"ok":true},"webhook":{"ok":false,"status":500,"error":"http_500"}}}}
```

### 投递状态未知响应

外部投递已发生但结果未能落盘（或进程在结果落盘前崩溃）时返回，**不会**伪装成可安全重试的普通错误：

```json
{"code":40006,"message":"投递结果持久化失败……","data":{
  "pushid":"8f3ad2...","error":"DELIVERY_UNKNOWN","errno":40006,"delivery":"unknown"}}
```

## 错误码

| code | 含义 |
| --- | --- |
| `0` | 成功 |
| `40001` | SendKey 不正确或不存在 |
| `40002` | 超过每分钟请求上限 |
| `40003` | 超过每日请求上限（免费版每天 5 条，Pro 每天 200 条；按账号名下所有 SendKey 当日发送量汇总判定，而非单把 SendKey 各自计数） |
| `40004` | 一个或多个通道投递失败，`data.results` 含各通道结果；同一幂等键重试只重放该结果、不会重投，重投需换新键 |
| `40005` | 触发按 IP+SendKey 的限流（默认 60 条/分钟），请求未写入消息、未计数、未投递 |
| `40006` | `DELIVERY_UNKNOWN`：投递状态未知（外部通道可能已收到），携带相同幂等键重试不会重复投递 |
| `40007` | 幂等键非法（超长或含控制字符），请求未执行任何副作用 |
| `400` / `413` | 请求本身不合法：`400` 请求体不是合法 JSON 对象，`413` 请求体超过 64 KB。这两种用真实 HTTP 状态码返回，body 同样是 `{"code":400,...}` / `{"code":413,...}`，请求未执行 |

**判断成败**：能解析出响应 JSON 就以 `code` 为准（`0` 才是成功）；超时、网络错误等拿不到响应时按「结果未知」处理，用同一个幂等键重试，不会重复通知。

**判定顺序**：SendKey 不存在（40001）→ IP+SendKey 限流（40005）→ 幂等键非法（40007）→ 同一幂等键已有记录则直接重放 → 账号每日额度（40003）→ 单 SendKey 每分钟上限（40002）。被 40001 / 40002 / 40003 / 40005 / 40007 拒绝的请求都不写消息、不计数、不投递；其中 40002 / 40003 / 40005 是限流，不要立即重试。

## 调用示例

### Shell / crontab

```cron
# 每天 9:00 汇报磁盘用量
0 9 * * * curl -s "https://send.yimeijia.cc/<SENDKEY>.send" \
  --data-urlencode "title=磁盘日报" \
  --data-urlencode "desp=$(df -h / | tail -1)"
```

### Python

```python
import requests

requests.post("https://send.yimeijia.cc/<SENDKEY>.send", data={
    "title": "备份完成",
    "desp": "共写入 2.3GB，耗时 14 分钟",
})
```

### JavaScript / Node.js

```javascript
await fetch("https://send.yimeijia.cc/<SENDKEY>.send", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ title: "构建成功", desp: "v2.4.0 已发布" }),
});
```

### 第三方工具

在监控面板、自动化平台、短信转发器等工具的 Webhook / 自定义通知设置中，把请求地址填成 `https://send.yimeijia.cc/<SENDKEY>.send`，用 GET 参数或 POST JSON 传 `title` 和 `desp` 即可。

## 通道配置

在控制台 https://send.yimeijia.cc/app 的 SendKey 卡片中粘贴对应通道的 webhook 地址，保存即生效。一个 SendKey 可配置多个通道，推送时同时投递。通道配置接口严格校验：仅接受 `wecom`/`dingtalk`/`feishu`/`webhook` 四个键，值必须是字符串（空字符串表示移除该通道），未知通道名或非字符串值一律拒绝且不落盘。

### 企业微信群机器人（微信中可收）

- 企业微信群 → 右上角 →「群机器人」→「添加」→ 复制 webhook 地址
- 形如 `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=...`
- 单条消息最长 4096 字节（含标题，企业微信官方限制），超出时 Y-Send 自动截断正文并附「查看全文」链接；每个机器人每分钟最多 20 条

### 钉钉群机器人

- 钉钉群 → 群设置 →「智能群助手」→「添加机器人」→「自定义」→ 复制 webhook
- 形如 `https://oapi.dingtalk.com/robot/send?access_token=...`
- 安全设置必须选一项：推荐「IP 地址（段）」填 `14.103.66.221`，或「自定义关键词」（之后每条消息的标题或正文都得包含这个词）；**不要选「加签」**——Y-Send 推送不带签名，选了会全部失败

### 飞书群机器人

- 飞书群 → 设置 →「群机器人」→「添加机器人」→「Custom Bot」→ 复制 webhook
- 形如 `https://open.feishu.cn/open-apis/bot/v2/hook/...`
- 请求体最大 20 KB（飞书官方限制），超出时 Y-Send 自动截断正文并附「查看全文」链接；每个机器人每分钟 100 次、每秒 5 次
- 安全设置可以不开；要开请选「IP 白名单」填 `14.103.66.221` 或关键词，**不要开「签名校验」**（原因同钉钉）

### 自定义 Webhook

推送时会向该地址发送 `POST application/json`：

```json
{"title":"标题","desp":"正文","time":"2026-01-01T08:00:00.000Z"}
```

可用它桥接邮件、短信、Bark 或任何云服务。

> 安全策略：仅允许 http/https 公网地址——localhost、回环、私有网段、链路本地（含云元数据 169.254.169.254）、文档/保留段（192.0.2.0/24、198.51.100.0/24、203.0.113.0/24 等）、IPv6 等价地址全部拒绝；每次发送前重新解析域名并校验全部解析结果，随后把 TCP/TLS 连接固定到已校验的 IP（不存在第二次解析；TLS SNI 与 Host 头保留原始域名）；不跟随重定向；默认超时 8 秒。确有内网桥接需求时，运维可通过 `WEBHOOK_ALLOWLIST_HOSTS` 精确放行指定主机（不匹配同后缀域名）。

## 使用限制

| 项目 | 限制 |
| --- | --- |
| 单 SendKey 每分钟请求 | 50 次 |
| 账号每日请求（按名下所有 SendKey 汇总） | 免费版 5 条 / Pro 200 条（3 元/月，微信扫码支付后自动开通；支持 1/3/6/12 个月），按 `APP_TIMEZONE` 时区的自然日重置；同一账号下的多把 SendKey 共用同一份每日额度，不会因为多开 SendKey 而叠加，以控制台显示为准 |
| 标题长度 | 32 字符 |
| 正文长度 | 32768 字符（服务端保存上限，超出截断）；推送到企业微信、飞书时按通道上限截断并附「查看全文」链接，详见「通道配置」 |
| 请求体上限 | 64 KB（超限返回 HTTP 413） |
| 消息记录保留 | 每用户最近 200 条（用户间互不影响），全局硬上限 2000 条 |

## 退款政策

Pro 订阅为虚拟服务，一经开通不支持退款；支付或到账如有问题，请联系管理员处理。

## 服务配置

常用环境变量（完整列表与各配置的取值范围见 README——所有数值配置都有最小/最大值，非法值会拒绝启动）。服务按单实例运行，外部投递、管理写操作和过期清扫共用存储写锁，避免响应与数据文件状态分叉；该写锁不覆盖请求体读取，慢速客户端不会阻塞其他请求：

| 变量 | 默认值（范围） | 说明 |
| --- | --- | --- |
| `APP_TIMEZONE` | Asia/Shanghai | 每日限额按此时区的自然日计算；显式设置为空/非法时区名拒绝启动 |
| `DAILY_LIMIT_FREE` / `DAILY_LIMIT_PRO` | 5 / 200 | 账号每日上限（1~100000，按账号名下所有 SendKey 汇总，而非单把 SendKey 各自计数），按账号 `plan` 是否为未过期的 `pro` 二选一；已废弃的单一 `DAILY_LIMIT` 显式设置会拒绝启动 |
| `MINUTE_LIMIT` | 50（1~10000） | 单 SendKey 每分钟上限 |
| `SESSION_TTL_MS` | 30 天（1 秒~365 天） | 登录态有效期，过期自动清理；注销/过期删除均为持久化事务；中央封禁/禁用最迟要到本地 session 过期才生效，生产建议调小到 7 天（604800000） |
| `SERVER_REQUEST_TIMEOUT_MS` | 30000（1000~300000） | HTTP 服务器底线请求超时，兜底回收挂起连接 |
| `SERVER_HEADERS_TIMEOUT_MS` | 15000（1000~300000） | HTTP 服务器接收请求头超时 |
| `AGENTADMIN_BASE_URL` | http://127.0.0.1:3200 | 懿美嘉中央账号服务地址；`http:` 仅允许回环主机，`https:` 允许任意主机 |
| `AGENTADMIN_TIMEOUT_MS` | 5000（500~15000） | 调用中央账号服务的绝对总超时，超时/网络错误/5xx/非 JSON 统一返回 503 |
| `PROFILE_CACHE_MS` | 60000（0~600000） | 中央账号资料缓存时长，0 关闭缓存 |
| `WEBHOOK_TIMEOUT_MS` | 8000（1000~60000） | 绝对总超时（含 DNS 解析阶段） |
| `WEBHOOK_RESPONSE_LIMIT` | 65536（1KB~1MB） | 通道响应体上限，超限立即失败 |
| `SEND_IP_LIMIT` / `SEND_IP_WINDOW_MS` | 60 / 60000 | 按 IP+SendKey 限流，超限返回 code 40005 |
| `WEBHOOK_ALLOWLIST_HOSTS` | 空 | 明确放行的内网 webhook 主机（精确匹配） |
| `TRUSTED_PROXY_IPS` | 空 | 可信反向代理 IP；仅可信来源才解析 X-Forwarded-For（空段/非法项整体回退） |
| `ALLOWED_ORIGINS` | 空 | 可信跨域 Origin，为空不返回 CORS 头；白名单跨域预检允许 Content-Type、Authorization、Idempotency-Key，并允许读取 X-Idempotent-Replay |
| `COOKIE_SECURE` | 0 | 仅接受 0/1：HTTPS 部署时设为 1；其他显式取值（含空串）拒绝启动 |
