Routine 第三种触发器,通用入站 webhook(推翻 ADR-0297 §7「Grok Bot 式通用 webhook 不做」):任何能发 HTTP POST 的系统——GitHub、n8n、Zapier、自建脚本、TradingView——都能往一条 Routine 的专属地址发一封来件,平台验令牌后按序起一个 Run,模型按 task 临场处理。在通用入口之上,叠一层交易信号增强:来件恰好只带信号字段时,Run 不污染,过三道确定性闸可自动下单。
两层不是两种 webhook,是同一封来件上的两段处理:通用层所有来件都走;增强层只在 body 恰好带信号字段时额外生效。
谁都能发,发什么都收(JSON / 纯文本 / 空),都能唤起 Run。
让「收到交易信号就执行」成立,但不信发送方、不信模型。
time 过期检查、id 去重| 发送方 | body | 走哪层 | 结果 |
|---|---|---|---|
| GitHub push 事件 | 嵌套 JSON | 通用 | 全是其余内容 ⇒ 污染;模型读、总结、推送,要写就出审批卡 |
| n8n 转邮件摘要 | {"summary":"…"} |
通用 | 同上 |
| 自建脚本告警 | 纯文本「BTC 跌破 9 万」 | 通用 | 非 JSON ⇒ 其余内容 ⇒ 污染;模型按 task 解读后推送 |
| cron / 任意脚本 | 空 body | 通用 | 一次无数据唤起,等于「现在跑一次」;不污染 |
| TradingView 模板 | 只有信号字段 | 通用 + 增强 | 不污染;开了自动下单且三闸全过 ⇒ 自动执行 |
| TradingView + comment | 信号字段 + 文字 | 通用 + 增强 | comment 进模型但污染 ⇒ 下单走审批卡 |
| 任意系统发信号字段 | n8n / 脚本按同一字段名发 | 通用 + 增强 | 与 TradingView 完全同等;信号字段不认发送方 |
它是约束最紧的一等发送方:不能加 header、不能签名、3s
超时、5xx 重发至多 4 次、body 可能是
text/plain、看不到响应体。方案按它定下限——凭据放路径、内容错误不回 4xx
而是降级照跑、去重兜重发、3 秒内只做一次 DO
RPC。它接得住,其他发送方自然都接得住。
timenow、long/short/flat/exit、symbol
size 无单位约定、body 哈希去重窗口 60s去掉这几样,剩下就是一个标准的 per-Routine 通用 webhook。
「通用」= 谁都能发、发什么都收、都能唤起 Run;不等于发什么都能自动执行。只有交易信号字段可以免人审,且须三闸全过;其余内容永远只是给模型读的外部数据。
不新增可部署壳:入口挂在现有 bot
Worker(berth-bot-*)上,只多一个主机名
hooks.。三个区域三种信任级别:公网未鉴权 → 边缘过滤 →
持有效 MAC 才能进所有者 DO。
Idempotency-Key /
webhook-id · 签名头不读
/bots/:botId/routines/:id/webhook* 新端点
webhook_rate_limited
webhook.dailyAcceptedPerRoutine /
queueDepth
BOT_OWNER.getByName(user_id) · 一次 RPC
BotOwner(每用户一个 · SQLite)· 金融权威仍在
HL
hits 一样挂到输入消息
notificationText 加四句结局 +
日量通知(先发版)
为何不另起 Worker:所有者 DO 在 bot script 里,另起要跨 script 绑 DO、多一个部署壳;独立主机名已能让 WAF 只作用于这个入口,且以后 bot API 换主机名不影响已粘进 TradingView 的 URL。
同步段只到 202 就返回,保住 TradingView 3 秒预算;起 Run 在 DO 内异步。
sequenceDiagram
autonumber
participant S as 发送方(如 TradingView)
participant W as bot Worker (hooks.)
participant D as BotOwner DO
participant R as Run (thinkRuntime)
participant G as 写管线 / 策略闸
participant H as Hyperliquid
participant P as PUSH_QUEUE → server
S->>W: POST /v1/uid.raw.mac + body
W->>W: 405? · 路径 + MAC 常数时间校验 · ratelimits
W->>D: acceptWebhookDelivery(raw 哈希, body, headers, ip)
D->>D: SQLite 事务:令牌/生效/日量/新鲜度/去重/写来件/runId/入队
D-->>W: { deliveryId, runId }
W-->>S: 202(p99 < 1s)
Note over D,R: —— 以下异步,不在请求里 await ——
D->>R: 出队 · 复查 receivedAt ≤ 5min · Routine task + 来件块
R->>R: 来件解析 → 信号字段 / 其余内容(非空即污染)
R->>G: 写意图(方向, coin, reduceOnly)+ 本 Run 信号字段
alt 未污染 且 方向/标的/信号价 全过
G->>H: 自动下单(扣预算)
R->>P: routine_webhook { executed }
else 任一不过 / 已污染
G->>P: routine_approval(5min 过期,本 Run 不再推结局)
end
R->>D: Run 收尾 → 派下一封
按落点判据:纯函数归 contexts/bot domain,DO 装配 / 路由 /
bindings 归 apps/bot。 + 新
~ 改;新文件名为示意,实现定。
contexts/bot/src/ ├── domain/ │ ├── value-objects/routine/ │ │ ~ trigger.ts # TriggerSchema 加 {type:"webhook",v:1} │ ├── ~ value-objects/taintSource.ts # +webhook, ref=来件id │ └── calculators/ │ + webhookDelivery/parse # 来件解析:最深模块 │ + webhookDelivery/signalConsistency │ ~ write/writePolicyGate.ts # 四条件后调一致性 ├── application/ │ ~ PushService.ts # outcome × 冷却 × 审批优先 │ + 来件受理 / 令牌生命周期用例 ├── infrastructure/crypto/ │ + webhookTokenSeal.ts # 照 desktopNodeSeal.ts,独立 key 环 └── server.ts # /server 门:push 模板 schema
apps/bot/ ├── ~ wrangler.jsonc # hooks. 域名 / ratelimits / env var(mainnet 重声明) └── src/ ├── ~ app.ts # 按 Host 分流 ├── routes/ │ + webhookIngress.ts # 公网入口:405/404/429/413 │ ~ bots.ts # 生成/轮换/撤销/投递记录/测试来件 ├── owner/ │ ~ BotOwner.ts # RPC acceptWebhookDelivery + 队列 │ ~ chatGate.ts # 用户消息落库前隐藏地址令牌 │ ~ systemPrompt.ts # 来件不是所有者(重录 eval) │ ~ pushes.ts ├── ~ doMigrations.snapshot.json # v57 之后新表 └── ~ wranglerConfig.test.ts apps/server/src/routes/notifications/ └── ~ notificationText # 每 locale 结局句 apps/web/src/pages/bot/chat/routines/ ~ apps/mobile/src/screens/bot/routines/ ~
全部随 bot DO 迁移版本走;删 Routine / 清除所有者数据一并删除。日拍清理过期行。
routine_id所属 Routinetoken_hashSHA-256(raw),入口按它查mac_version对应 /v<n>/last4 · created_at掩码展示revoked_at作废后留 30 天做归因delivery_id · routine_id · received_atoutcomeaccepted / queued / duplicate / stale / inactive / too_large /
unknown_token / busy / daily_limit
dedupe_key · dedupe_sourcerun_id一封来件至多一个 Runsignal · extras · notes解析结果与降级说明
body(≤64KiB, 脱敏) · sha256 · ip · country(routine_id, dedupe_key)主键,与写来件同事务
delivery_id · run_id重复时回原 runIdexpires_atheader / id 24h · body 哈希 60s
routine_id · utc_day · accepted上限 200(KV 可调)
queued 来件 + 游标深度 5,FIFO
Routine 本身只多一条不变式:触发器 = webhook ⇒ 无
desktopNodeId;改成 webhook ⇒ 清
autoTrade 与绑定。令牌不进 trigger
规范形,确认卡与改前改后比对永远看不到秘密。
地址只能在 UI 里签发——bot 没有生成 / 轮换 / 撤销的 tool,令牌永不进模型。
flowchart LR
U[web / mobile / desktop
Routine 设置] -->|POST …/webhook/rotate| B[bot. API
sessionGate]
B --> D[(BotOwner DO)]
D -->|生成 raw · 存 SHA-256 · 旧令牌 revoked_at| D
D -->|完整 URL 只返回这一次| U
U -->|GET …/webhook/deliveries| B
U -->|POST …/webhook/test
不经公网入口| B
C[对话 propose_routine] -->|批准后只建 Routine
不签地址| D
C -.->|回执按钮「去生成地址」| U
M[mobile 会话] -->|唯一能打开 / 调高
webhook 自动下单| D
越靠前的检查越便宜:乱打请求在 Worker 层就 404,进不了任何 DO。3 秒预算内只做一次 DO RPC,起 Run 异步。
任意 body。按最紧的 TradingView 设计:只能控制 URL + body,不能带 header / 签名,3s 超时,5xx 重发至多 4 次。
每 IP 600 / 10s,TradingView 4 个出口 IP 豁免(所有用户共用)。
hooks 主机名只挂这一条路由,无 CORS / 会话 /
WS。应用日志永不打印路径与令牌。
ratelimits:每地址 10/60s ·
每所有者 30/60s。
webhook_rate_limited)
acceptWebhookDelivery查令牌哈希 → 判 Routine 生效 → 计日量 → 时间新鲜度 → 去重 → 写来件 → 分配 runId → 入 FIFO。
202 { deliveryId, runId } 目标 p99 < 1s
前一个 Run 收尾再派下一个;排队超 5 min 丢弃记
stale——过期信号下单比不下更危险。
Run Source 仍是 routine;metadata.routine.delivery
引用来件,线程渲染「来件卡」,永不当用户发言。其余内容非空 ⇒
污染来源 webhook。
全过 ⇒ 自动下单(扣预算);任一不过 ⇒ 降为审批卡(5 min 过期),不直接拒。
转了审批就只推审批。锁屏不带标的 / 方向 / 数量。
原则:内容问题永不 5xx。5xx 只来自平台故障 ⇒ TradingView 重发,去重兜底;4xx 不重发,正是鉴权 / 过期 / 不生效要的语义。
| 序 | 条件 | 响应 | 记来件 |
|---|---|---|---|
| 1 | 非 POST | 405 + Allow: POST |
否 |
| 2 | 路径形状错 / MAC 不过 | 404 | 否 |
| 3 | 边缘 / 突发限流 | 429 + Retry-After | 否(AE 计数) |
| 4 | body > 64 KiB | 413 | too_large |
| 5 | 令牌查不到 / 已作废 / Routine 删了或不再是 webhook | 404(同第 2 步) | 30 天内旧哈希记 unknown_token |
| 6 | Routine 暂停 / 额度 / 凭证失效 | 409 ROUTINE_INACTIVE + reason |
inactive |
| 7 | 每 Routine 日量 200 顶满 | 429 DAILY_LIMIT(到 UTC 0 点) |
daily_limit |
| 8 | time 早 > 5 min 或晚 > 60 s |
422 SIGNAL_STALE |
stale |
| 9 | 重复 | 200 duplicate:true + 原 runId |
duplicate |
| 10 | 队列满 | 429 ROUTINE_BUSY |
busy |
| 11 | 接受 | 202 { deliveryId, runId } | accepted / queued |
去重键优先级:Idempotency-Key(24h)→
webhook-id(24h)→ body id(24h)→ 原始 body
SHA-256(60s,只吸 TradingView 重发)。
来件作者不可证(持令牌即可发)。防注入靠污染,防伪造靠交易闸。校验不过降级照跑,不拒收——TradingView 看不到响应体,4xx = 信号静默丢失。
平台固定封闭集,只认 JSON 顶层键;地位等同条件命中
hits。
id 去重键 · time / timenowticker / symbol · 短标识正则action buy / sell / close(long·short·flat·exit
别名)
price · size(无单位,闸不读)·
interval
{ "id": "{{strategy.order.id}}",
"time": "{{timenow}}",
"ticker": "{{ticker}}",
"action": "{{strategy.order.action}}",
"price": "{{close}}" }
↑ 设置页一键复制的 TradingView 模板(不含令牌)
未识别字符串、嵌套对象 / 数组、非 JSON 原文、值不合法的字段。
action:"buyy" ⇒ 降为其余内容,投递记录标「action
无效」
secret / token /
password 键在记录中脱敏
{ ...信号字段,
"comment": "RSI 超卖,可以加仓" }
⇒ bot 能看到 comment
⇒ 本次下单走审批卡
信号执行器(3Commas / Alertatron)都是「令牌 + 信号即执行」;逐笔审批会让场景失去意义。所以允许,但单独开、三道确定性闸、风险由预算封顶。只为 HL perp / spot 定义,其他 venue 一律人审。
BINANCE: / .P /
USDT,kPEPE 带不带 k 都认;模型选 coin,规则只校验
开关是更大的放权:webhook Routine 的「允许自动下单」只在手机会话打开 / 调高,web / 桌面只能关 / 调低。触发器改成 webhook 时清掉
autoTrade。伪造攻击最坏损失 = min(剩余预算, 当日累计
notional)。
Routine 建好时没有地址,在设置里按需生成。完整 URL 只显示一次
+ 「复制 TradingView 模板」。之后只剩掩码 ••••末4位 +
生成时间 + 最近来件时间。
旧令牌立即作废,无宽限期(泄露要立刻止血)。找回地址 = 在当前设备重新生成。
只作废不签发,回到「尚未生成」。暂停回 409,撤销回 404。三个操作都是收紧,任意已登录端可做。
不做可再次查看的信封加密——DO 存储或 KEK 任一泄露都会让全部 URL
泄露。作废哈希留 30 天,用来把旧 alert 归因为
unknown_token · 来自 x.x.x.x。
ROUTINE_WEBHOOK_MAC_KEYS,不从凭证 KEK 派生:常规 KEK
轮换不该让几百个 TradingView alert 同时失效。怀疑泄露才签
/v2/ 并通知重新生成。
入站签名验证(须可逆存密钥,TradingView 签不了)、来源 IP 白名单(出口 IP 会变)。签名头收到也照常处理。
| 层 | 位置 | 键 | 数值 | 管什么 |
|---|---|---|---|---|
| 边缘 | CF WAF(仅 hooks.) | 来源 IP | 600 / 10s,TV IP 豁免 | 扫描与洪泛 |
| 突发 | Worker ratelimits,MAC 后进 DO 前 | 每地址 · 每所有者 | 10 · 30 / 60s | 单令牌狂刷 |
| 日量 | 所有者 DO 同事务 | 每 Routine 每 UTC 日 | 200 次接受(托管 = BYOK) | 泄露令牌烧 BYOK key |
不合并、不防抖:每封都是独立指令。额度计入现有 Routine 份额,不新增桶。用户可配的 ticker / action 预检不做——确定性规则归策略脚本。
routine_webhook { outcome }
| executed | 至少一笔挂上 / 成交 | 不受冷却 |
| not_executed | 尝试写但被拒 / 失败 | |
| done | 没尝试写 | 受 1h 冷却 |
| failed | Run 失败 |
收到来件不推;转审批只推审批。stale /
busy
不推(防骚扰通道),每日首次插线程提示。日量顶满推所有者级安全通知
webhook_daily_limit。
list_routines 只给
{ hasAddress, lastDeliveryAt };生成 / 轮换 / 撤销无
tool
全部经曳光弹间接 blocked-by BERTH-2420(Sigla 代码层改名)——berth.pro 的 301 会把 POST 变 GET,任何环境都不在 berth.pro 签地址。