175 lines
11 KiB
Markdown
175 lines
11 KiB
Markdown
# 由壹点通知中心
|
||
|
||
由壹点内部项目统一钉钉机器人单聊出口。只有本服务保存钉钉应用密钥和 `RobotCode`;素材库、固定资产、产品开发平台等业务系统使用各自的项目密钥调用本服务。
|
||
|
||
## 业务项目接入
|
||
|
||
新项目不要从本 README 中零散复制接口代码,请按以下规范接入:
|
||
|
||
- [统一通知中心接入操作手册](docs/统一通知中心接入交接说明.md):可直接交给其他项目负责人的完整接入流程、代码入口与验收标准。
|
||
- [通知中心新项目开通操作手册](docs/通知中心新项目开通操作手册.md):仅供通知中心维护人登记项目、生成密钥、安全交付和验收使用。
|
||
- [接入指南](docs/接入指南.md):项目登记、环境变量、签名、发送、多人通知和上线验证。
|
||
- [消息规范](docs/消息规范.md):固定项目抬头、ActionCard / Markdown 模板选择、正文排版、按钮跳转及完整请求示例。
|
||
- [错误与状态规范](docs/错误与状态规范.md):HTTP 返回、投递状态、错误码和重试边界。
|
||
- [新项目接入清单](docs/新项目接入清单.md):从申请到生产验证的逐项门禁。
|
||
- [Node.js 客户端示例](examples/node/README.md):可以直接复制到业务后端的无依赖实现。
|
||
|
||
接入的核心边界:业务项目只保存自己的项目代码和 HMAC 密钥;钉钉 `AppKey`、`AppSecret`、`RobotCode` 始终只由通知中心持有。
|
||
|
||
收件人支持两种方式:业务项目可以直接提交钉钉 `userId`,也可以提交准确花名 `recipientAlias`。花名会由通知中心调用素材平台内部目录接口精确解析为 `userId`;找不到时拒绝发送,不做模糊猜测,也不再要求项目级收件人预登记。
|
||
|
||
## 安全边界
|
||
|
||
- 每个项目使用独立 HMAC 密钥。
|
||
- 请求签名包含时间、nonce、HTTP 方法、路径和正文摘要。
|
||
- nonce 五分钟内只能使用一次。
|
||
- `actionUrl` 按业务项目提交值使用,不限制协议、公网/内网地址或项目域名白名单;只有启用免登且目标本身满足现有 HTTPS 登录流程时,才包装到统一 H5 入口。
|
||
- `projectCode + idempotencyKey` 唯一,防止业务重试造成重复通知。
|
||
- 项目无法读取其它项目的投递记录。
|
||
- 钉钉 `AppSecret` 只保存在通知中心;业务系统只用自己的项目 HMAC 密钥兑换登录票据。
|
||
- 登录票据随机生成、只存 SHA-256 摘要、最长 60 秒、绑定项目和回跳域名并且只能原子消费一次。
|
||
- 免登码和登录票据都只通过 POST 传递,不进入 URL;服务日志不记录二者和任何密钥。
|
||
|
||
## 本地启动
|
||
|
||
1. 复制 `.env.example` 为 `.env` 并填写真实配置;不要提交 `.env`。
|
||
Windows 下项目目录包含空格,`DATABASE_URL` 请保留为完整绝对路径。
|
||
`YYD_NOTIFICATION_ADMIN_API_SECRET`(兼容旧变量 `YYD_NOTIFICATION_ADMIN_TOKEN`)和 `YYD_NOTIFICATION_CONFIG_ENCRYPTION_KEY` 只保存在通知中心服务端。
|
||
2. 执行 `npm install`。
|
||
3. 执行 `npm run prisma:generate`。
|
||
4. 执行 `npm run prisma:migrate`。
|
||
5. 执行 `npm run dev`。
|
||
|
||
健康检查:`GET /health`。
|
||
|
||
`npm test` 不读取本地 `.env`:测试入口会创建唯一临时 SQLite,先执行正式 Prisma 迁移,再串行运行测试,并在成功或失败后清理数据库、journal、WAL 和 SHM 文件。
|
||
|
||
## 项目配置来源与管理 API
|
||
|
||
当 `YYD_NOTIFICATION_ADMIN_API_SECRET`(或兼容旧变量 `YYD_NOTIFICATION_ADMIN_TOKEN`)与 `YYD_NOTIFICATION_CONFIG_ENCRYPTION_KEY` 都已配置时,通知项目配置以数据库为准;`YYD_NOTIFICATION_PROJECTS_JSON` 只承担“首次/补录导入缺失项目”的 bootstrap 角色:
|
||
|
||
- 服务启动时会尝试把 `YYD_NOTIFICATION_PROJECTS_JSON` 中尚未登记的项目导入数据库;
|
||
- 已存在项目不会被覆盖,已轮换密钥、已手工维护的 `recipients` 和登录配置保持不变;
|
||
- 运维可用 `POST /admin/runtime/import-env` 再次补录当前环境变量中的缺失项目;
|
||
- 运维可用 `POST /admin/runtime/refresh` 让运行时缓存重新读取数据库,不必重启进程。
|
||
- 如果管理员密钥或加密密钥缺失,服务会安全回退到 legacy 内存模式:继续直接使用 `YYD_NOTIFICATION_PROJECTS_JSON` 提供旧 `/v1/*` 能力,且不挂载 `/admin/*`。
|
||
|
||
管理 API 使用独立管理员 Bearer Secret,与业务项目 HMAC 密钥隔离;该 Secret 同时复用于内部花名目录调用:
|
||
|
||
- 主变量:`YYD_NOTIFICATION_ADMIN_API_SECRET`
|
||
- 兼容旧变量:`YYD_NOTIFICATION_ADMIN_TOKEN`
|
||
- 请求头:`Authorization: Bearer <YYD_NOTIFICATION_ADMIN_API_SECRET>`
|
||
- 可选 `x-yyd-admin-actor`:写入审计日志的操作人标识
|
||
|
||
当前管理接口:
|
||
|
||
- `GET /admin/projects` / `GET /admin/projects/:projectCode`
|
||
- `POST /admin/projects`
|
||
- `PATCH /admin/projects/:projectCode`
|
||
- `DELETE /admin/projects/:projectCode`
|
||
- `GET|PUT /admin/projects/:projectCode/recipients`
|
||
- `POST /admin/projects/:projectCode/rotate-secret`
|
||
- `POST /admin/projects/:projectCode/test`
|
||
- `GET /admin/deliveries`
|
||
- `GET /admin/audits`
|
||
- `POST /admin/runtime/import-env`
|
||
- `POST /admin/runtime/refresh`
|
||
|
||
稳定响应 contract:
|
||
|
||
- 项目列表:`{ projects: [...] }`,默认包含 `projectCode / name / recipientCount / recipients / deliverySummary`
|
||
- 创建/轮换密钥:`{ project, secret, env }`
|
||
- 投递列表:`{ deliveries, total }`
|
||
- 审计列表:`{ audits, total }`
|
||
- 管理错误:`{ error: { code, message, details? } }`
|
||
|
||
其中 `secret` 与 `env.YYD_NOTIFICATION_CENTER_PROJECT_SECRET` 只会在 `POST /admin/projects` 和 `POST /admin/projects/:projectCode/rotate-secret` 当次响应里出现一次,后续列表、详情和审计都不会回显明文密钥。
|
||
|
||
请求体大小限制:
|
||
|
||
- 业务 `/v1/*`:64 KB
|
||
- 管理 `/admin/*`:256 KB
|
||
|
||
普通业务接入接口 `POST /v1/notifications`、`GET /v1/notifications/:id`、`GET /v1/notifications` 与原签名协议保持兼容。消息字段仍是 `title / content / actionTitle / actionUrl` 四件套:通知中心生成 `【项目名称】业务标题`,并将其作为 Markdown 标题固定显示在钉钉可见正文顶部;业务项目决定其后的正文排版、按钮文案和跳转目标。有 `actionUrl` 时使用原生 ActionCard,无地址时使用无按钮的普通 Markdown 消息。样式示例和总显示长度说明见[消息规范](docs/消息规范.md)。
|
||
|
||
生产环境建议只把容器端口绑定到 `127.0.0.1`,再由 Nginx 提供独立 HTTPS 域名。素材库和固定资产可能位于不同主机,因此不能把业务项目配置成通知中心的容器内网地址。
|
||
|
||
当前生产部署脚本位于:
|
||
|
||
- `scripts/deploy-material-host.sh`:只管理通知中心独立容器、配置和独立 FRP 服务;
|
||
- `scripts/deploy-public-nginx.sh`:备份并扩展素材库专用 FRP 端口白名单,然后为素材库公网域名增加 `/notification-center/` 反向代理;脚本会验证主站、S3 和通知中心,失败时回滚。
|
||
|
||
脚本不包含任何密钥。统一机器人凭证从素材库生产环境安全复制到通知中心独立 `.env`,项目 HMAC 密钥在服务器生成并以 `0600` 权限保存。
|
||
|
||
发布脚本会先一致性备份通知中心 SQLite、环境配置和源码,再从本次解压的源码强制构建镜像并 `force-recreate`,不会因为服务器上存在旧 `latest` 镜像而跳过新代码。素材库生产 NAS 无法稳定访问 Docker Hub,正式发布前应预载 `node:22-alpine` 基础镜像并确认 npm 依赖源可用;构建失败会在重建容器前终止,旧服务继续运行。
|
||
|
||
Git 发布入口会在生产机首次生成、后续稳定保留 `YYD_NOTIFICATION_ADMIN_API_SECRET` 与 `YYD_NOTIFICATION_CONFIG_ENCRYPTION_KEY`,并只把管理地址和管理员密钥同步到素材库后端 `.env`;脚本只打印配置完成状态,不输出密钥原文。
|
||
|
||
## 签名算法
|
||
|
||
请求头:
|
||
|
||
- `x-yyd-project-code`
|
||
- `x-yyd-timestamp`:Unix 秒
|
||
- `x-yyd-nonce`:每次请求生成新的随机字符串
|
||
- `x-yyd-signature`:HMAC-SHA256 十六进制字符串
|
||
|
||
待签名文本:
|
||
|
||
```text
|
||
timestamp\nnonce\nHTTP_METHOD\nPATH\nSHA256(rawBody)
|
||
```
|
||
|
||
发送接口:`POST /v1/notifications`。查询接口:`GET /v1/notifications/:id`。
|
||
|
||
通知中心只接受任务并异步发送,业务系统通过查询接口获取 `pending / sending / sent / unread / read / retrying / failed` 状态。
|
||
|
||
## 钉钉 H5 免登中转
|
||
|
||
项目的 `login` 配置是可选项;不配置时仍按原方式直接跳转,素材库等现有调用不受影响。启用后,通知中心会把业务传入的项目 HTTPS 地址转换为内部 `redirectPath`,并把卡片动作包装到统一入口:
|
||
|
||
```text
|
||
GET /v1/auth/h5-entry?projectCode=fixed_asset&redirectPath=%2Fworkflows%2Fdisposals%2F53
|
||
```
|
||
|
||
入口页面向钉钉 JSAPI 同时传入公开的 `clientId`(即 AppKey)和 `corpId` 获取免登码,再以 JSON POST 调用:
|
||
|
||
```text
|
||
POST /v1/auth/h5-code
|
||
{"projectCode":"fixed_asset","redirectPath":"/workflows/disposals/53","code":"..."}
|
||
```
|
||
|
||
中心换取钉钉 `userId` 后返回 `{ticket, formPostUrl, expiresIn}`,页面立即通过隐藏表单把 `ticket` POST 到项目配置的精确 `callbackUrl`。业务后端收到票据后,使用与通知接口相同的 HMAC 头调用:
|
||
|
||
```text
|
||
POST /v1/auth/tickets/redeem
|
||
{"ticket":"..."}
|
||
```
|
||
|
||
成功响应只包含 `{dingtalkUserId, redirectPath}`。未知、过期、已消费或跨项目票据统一返回 `401 TICKET_INVALID_OR_EXPIRED`,不泄露票据是否存在。业务系统必须在创建 Session 前重新核验本地账号、员工绑定和在职状态。
|
||
|
||
项目配置示例:
|
||
|
||
```json
|
||
{
|
||
"code": "fixed_asset",
|
||
"name": "固定资产",
|
||
"secret": "独立随机项目密钥",
|
||
"allowedHosts": ["asset.jiansun.vip"],
|
||
"login": {
|
||
"enabled": true,
|
||
"callbackUrl": "https://asset.jiansun.vip/api/dingtalk/broker-login",
|
||
"allowedRedirectPathPrefixes": ["/workflows/disposals", "/dashboard", "/inventory"]
|
||
}
|
||
}
|
||
```
|
||
|
||
新增环境变量:
|
||
|
||
- `DINGTALK_CORP_ID`:统一钉钉应用所属企业 CorpId。
|
||
- `YYD_NOTIFICATION_PUBLIC_BASE_URL`:通知中心公网 HTTPS 基地址,生产为反向代理后的 `/notification-center` 地址。
|
||
- `YYD_NOTIFICATION_DIRECTORY_URL`:素材平台内部花名目录解析接口地址;默认可设为 `https://sc.jiansun.vip/api/internal/notification-center/recipients/resolve`。
|
||
- `DINGTALK_LOGIN_TICKET_TTL_SECONDS`:30~60 秒,默认 60 秒。
|
||
|
||
新建任务最多投递 5 次,钉钉 HTTP 429、`QpsLimit` 和其他明确限流统一归一为 `FLOW_CONTROLLED`,按 1、5、15、30 分钟退避;接入方只观察统一状态和错误码,不解析钉钉厂商错误码。中心默认将发送请求启动间隔限制为至少 250ms(`WORKER_SEND_MIN_INTERVAL_MS` 可在 100~5000ms 内调整),避免整点任务集中冲击下游。发送调用若在请求已发出后发生网络断开、超时、非限流 HTTP 408/5xx、钉钉成功响应缺少 `processQueryKey`,或进程在发送中途崩溃并遗留过期租约,会以 `DELIVERY_OUTCOME_UNKNOWN` 结束并停止自动重试,避免不确定结果造成重复单聊。历史任务保留创建时的最大尝试次数,不通过迁移篡改。
|