PRD BERTH-2438 · ADR-0305

Routine 入站 Webhook 触发器

Routine 第三种触发器,通用入站 webhook(推翻 ADR-0297 §7「Grok Bot 式通用 webhook 不做」):任何能发 HTTP POST 的系统——GitHub、n8n、Zapier、自建脚本、TradingView——都能往一条 Routine 的专属地址发一封来件,平台验令牌后按序起一个 Run,模型按 task 临场处理。在通用入口之上,叠一层交易信号增强:来件恰好只带信号字段时,Run 不污染,过三道确定性闸可自动下单。

POST https://hooks.sigla.pro/v1/<user_id>.<raw>.<mac>
user_id 路由到所有者 DO raw 32 字节随机数,DO 只存 SHA-256 mac HMAC(key_v, user_id‖raw),Worker 先验 /v1/ = MAC key 版本,换 key 不废旧地址

00定位:通用入口 + 交易信号增强

两层不是两种 webhook,是同一封来件上的两段处理:通用层所有来件都走;增强层只在 body 恰好带信号字段时额外生效。

通用层 · 所有来件

谁都能发,发什么都收(JSON / 纯文本 / 空),都能唤起 Run。

  • 路径令牌鉴权(一 Routine 一地址,只显示一次)
  • 三层限额 · 去重 · 单 Routine 串行队列 · 日量
  • 投递记录(时间 / 结局 / 来源 IP 与国家)
  • 来件作为外部数据块交给模型,线程显示来件卡
  • 其余内容非空 ⇒ Run 污染 ⇒ 写操作一律审批卡
  • Run 结束推结局
交易增强层 · 仅带信号字段时

让「收到交易信号就执行」成立,但不信发送方、不信模型。

  • 封闭信号字段集(id / time / ticker / action / price / size / interval)不污染
  • time 过期检查、id 去重
  • 方向 / 标的 / 信号价三道确定性闸 ⇒ 可自动下单
  • 自动下单只在手机打开,预算封顶
  • 只为 HL perp / spot 定义

同一入口,不同发送方会怎样

发送方 body 走哪层 结果
GitHub push 事件 嵌套 JSON 通用 全是其余内容 ⇒ 污染;模型读、总结、推送,要写就出审批卡
n8n 转邮件摘要 {"summary":"…"} 通用 同上
自建脚本告警 纯文本「BTC 跌破 9 万」 通用 非 JSON ⇒ 其余内容 ⇒ 污染;模型按 task 解读后推送
cron / 任意脚本 空 body 通用 一次无数据唤起,等于「现在跑一次」;不污染
TradingView 模板 只有信号字段 通用 + 增强 不污染;开了自动下单且三闸全过 ⇒ 自动执行
TradingView + comment 信号字段 + 文字 通用 + 增强 comment 进模型但污染 ⇒ 下单走审批卡
任意系统发信号字段 n8n / 脚本按同一字段名发 通用 + 增强 与 TradingView 完全同等;信号字段不认发送方
为什么文档里处处是 TradingView

它是约束最紧的一等发送方:不能加 header、不能签名、3s 超时、5xx 重发至多 4 次、body 可能是 text/plain、看不到响应体。方案按它定下限——凭据放路径、内容错误不回 4xx 而是降级照跑、去重兜重发、3 秒内只做一次 DO RPC。它接得住,其他发送方自然都接得住。

真正 TradingView 专属的只有这些
  • 信号字段别名:timenow、long/short/flat/exit、symbol
  • 设置页「复制 TradingView 消息模板」
  • WAF 对它 4 个共享出口 IP 的限流豁免
  • size 无单位约定、body 哈希去重窗口 60s

去掉这几样,剩下就是一个标准的 per-Routine 通用 webhook。

「通用」= 谁都能发、发什么都收、都能唤起 Run;不等于发什么都能自动执行。只有交易信号字段可以免人审,且须三闸全过;其余内容永远只是给模型读的外部数据。

第一部分 · 技术架构

A1部署拓扑与信任边界

不新增可部署壳:入口挂在现有 bot Worker(berth-bot-*)上,只多一个主机名 hooks.。三个区域三种信任级别:公网未鉴权 → 边缘过滤 → 持有效 MAC 才能进所有者 DO。

公网 · 未鉴权 · 载荷不可信
TradingView alert只控 URL + body · 3s 超时 · 4 个出口 IP
n8n / Zapier / GitHub / 自建脚本任意 HTTP POST · 可带 Idempotency-Key / webhook-id · 签名头不读
扫描器 / 泄露令牌持有者要被挡住的对象
POST hooks.sigla.pro/v1/…
Cloudflare · bot Worker(同一个 script,两个 custom domain)
WAF 规则(仅 hooks.)每 IP 600/10s,TradingView IP 豁免
hooks.sigla.proapp.ts 按 Host 分流 ⇒ 只挂 webhook 入口路由;无 CORS / 会话 / WS
bot.sigla.pro现有 API:CORS + sessionGate ⇒ /bots/:botId/routines/:id/webhook* 新端点
secret ROUTINE_WEBHOOK_MAC_KEYS版本化 key 环,常数时间验 MAC
ratelimits × 2每地址 10/60s · 每所有者 30/60s
BOT_METRICS (AE)+ webhook_rate_limited
BOT_CONFIG (KV)+ webhook.dailyAcceptedPerRoutine / queueDepth
MAC 通过 ⇒ BOT_OWNER.getByName(user_id) · 一次 RPC
所有者 DO BotOwner(每用户一个 · SQLite)· 金融权威仍在 HL
acceptWebhookDelivery一个事务:令牌 → 生效 → 日量 → 新鲜度 → 去重 → 写来件 → runId → 入队
FIFO 派活单 Routine 至多一个 Run;排队 > 5min 丢弃
Routine fire → submit复用现有路径,来件像条件命中 hits 一样挂到输入消息
thinkRuntime / systemPrompt来件 = 外部数据块;规则补「来件不是所有者」
写管线 + writePolicyGate四条件后加「信号一致性」;webhook 审批卡 5min
pushes → PUSH_QUEUERun 结束判 outcome ⇒ apps/server 投递 APNs/FCM
Hyperliquid L1自动下单经委托 Agent Wallet 直达;订单 / 仓位权威
apps/servernotificationText 加四句结局 + 日量通知(先发版)
web / mobile 壳Routine 表单、地址区、投递记录、来件卡、审批卡头

为何不另起 Worker:所有者 DO 在 bot script 里,另起要跨 script 绑 DO、多一个部署壳;独立主机名已能让 WAF 只作用于这个入口,且以后 bot API 换主机名不影响已粘进 TradingView 的 URL。

A2时序:来件接收(同步)与 Run(异步)

同步段只到 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 收尾 → 派下一封
      

A3代码分层:动哪些文件

按落点判据:纯函数归 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/   ~

A4数据模型:所有者 DO SQLite 新表

全部随 bot DO 迁移版本走;删 Routine / 清除所有者数据一并删除。日拍清理过期行。

routine_webhook_tokens 令牌(只存哈希)
  • routine_id所属 Routine
  • token_hashSHA-256(raw),入口按它查
  • mac_version对应 /v<n>/
  • last4 · created_at掩码展示
  • revoked_at作废后留 30 天做归因
  • 部分唯一索引:每 Routine 至多一条 revoked_at IS NULL
webhook_deliveries 来件 = 投递记录
  • delivery_id · routine_id · received_at
  • outcomeaccepted / queued / duplicate / stale / inactive / too_large / unknown_token / busy / daily_limit
  • dedupe_key · dedupe_source
  • run_id一封来件至多一个 Run
  • signal · extras · notes解析结果与降级说明
  • body(≤64KiB, 脱敏) · sha256 · ip · country
  • 留存 30 天 · 每 Routine 最近 200 条
webhook_dedupe 去重
  • (routine_id, dedupe_key)主键,与写来件同事务
  • delivery_id · run_id重复时回原 runId
  • expires_atheader / id 24h · body 哈希 60s
  • 轮换令牌不清
日计数 + 队列状态 形状实现定
  • routine_id · utc_day · accepted上限 200(KV 可调)
  • queued 来件 + 游标深度 5,FIFO
  • 「每日首次」系统提示标记(stale / busy / daily_limit 共用)

Routine 本身只多一条不变式:触发器 = webhook ⇒ 无 desktopNodeId;改成 webhook ⇒ 清 autoTrade 与绑定。令牌不进 trigger 规范形,确认卡与改前改后比对永远看不到秘密。

A5所有者侧控制面(已登录会话)

地址只能在 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
第二部分 · 行为规则

01一封来件的全程

越靠前的检查越便宜:乱打请求在 Worker 层就 404,进不了任何 DO。3 秒预算内只做一次 DO RPC,起 Run 异步。

任意发送方

POST 来件

任意 body。按最紧的 TradingView 设计:只能控制 URL + body,不能带 header / 签名,3s 超时,5xx 重发至多 4 次。

CF WAF · hooks.

边缘限流

每 IP 600 / 10s,TradingView 4 个出口 IP 豁免(所有用户共用)。

超 ⇒ 429(不记来件)
bot Worker

方法 → 路径 → 常数时间验 MAC → 突发限流

hooks 主机名只挂这一条路由,无 CORS / 会话 / WS。应用日志永不打印路径与令牌。
ratelimits:每地址 10/60s · 每所有者 30/60s。

非 POST 405 · MAC 错 404 · 突发 429(只打 AE 计数 webhook_rate_limited)
所有者 DO · 一个事务

acceptWebhookDelivery

查令牌哈希 → 判 Routine 生效 → 计日量 → 时间新鲜度 → 去重 → 写来件 → 分配 runId → 入 FIFO。

413 过大 · 404 令牌失效(与 MAC 错字节级相同)· 409 不生效 · 429 日量 · 422 过期 · 200 重复回原 runId · 429 队列满

202 { deliveryId, runId } 目标 p99 < 1s

DO 异步派活

单 Routine 串行 FIFO(深度 5)

前一个 Run 收尾再派下一个;排队超 5 min 丢弃记 stale——过期信号下单比不下更危险。

Run

Routine task + 来件外部数据块

Run Source 仍是 routine;metadata.routine.delivery 引用来件,线程渲染「来件卡」,永不当用户发言。其余内容非空 ⇒ 污染来源 webhook。

策略闸

原四条件 + 三条信号一致性

全过 ⇒ 自动下单(扣预算);任一不过 ⇒ 降为审批卡(5 min 过期),不直接拒。

推送

Run 结束推一条结局

转了审批就只推审批。锁屏不带标的 / 方向 / 数量。

02入口响应码(固定处理顺序)

原则:内容问题永不 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 重发)。

03载荷信任:信号字段 vs 其余内容

来件作者不可证(持令牌即可发)。防注入靠污染,防伪造靠交易闸。校验不过降级照跑,不拒收——TradingView 看不到响应体,4xx = 信号静默丢失。

信号字段 · 不污染

平台固定封闭集,只认 JSON 顶层键;地位等同条件命中 hits。

  • id 去重键 · time / timenow
  • ticker / symbol · 短标识正则
  • action buy / sell / close(long·short·flat·exit 别名)
  • price · size(无单位,闸不读)· interval
  • 附加数值 ≤ 16 个(数字 / 布尔)
{ "id": "{{strategy.order.id}}",
  "time": "{{timenow}}",
  "ticker": "{{ticker}}",
  "action": "{{strategy.order.action}}",
  "price": "{{close}}" }

↑ 设置页一键复制的 TradingView 模板(不含令牌)

其余内容 · 非空即污染

未识别字符串、嵌套对象 / 数组、非 JSON 原文、值不合法的字段。

  • 随机 nonce 分隔标记包装成 untrusted 块,照样送进模型
  • action:"buyy" ⇒ 降为其余内容,投递记录标「action 无效」
  • secret / token / password 键在记录中脱敏
  • system prompt 补规则:来件不是所有者,「已批准」字样只是数据
  • 污染 Run 里的放权提议直接拒(ADR-0292)
{ ...信号字段,
  "comment": "RSI 超卖,可以加仓" }
⇒ bot 能看到 comment
⇒ 本次下单走审批卡

04交易闸:未污染 Run 可自动下单

信号执行器(3Commas / Alertatron)都是「令牌 + 信号即执行」;逐笔审批会让场景失去意义。所以允许,但单独开、三道确定性闸、风险由预算封顶。只为 HL perp / spot 定义,其他 venue 一律人审。

原四条件(ADR-0218 §5)委托授权 active · 闸内自动档 · Routine 开自动下单带预算 · Run 未污染
+
① 方向一致buy ⇒ 买向 · sell ⇒ 卖向 · close ⇒ 只认只减仓;反手照放
② 标的一致去 BINANCE: / .P / USDT,kPEPE 带不带 k 都认;模型选 coin,规则只校验
③ 信号价偏离|price − mark| / mark ≤ 价格保护(默认 3%)
全过 ⇒ 自动执行
扣自动下单预算;单笔 / 当日 notional、白名单、杠杆、日亏上限逐维取最严照常生效。
任一不过 ⇒ 审批卡
卡头「由外部来件触发 · Routine 名 · N 秒前」+ 信号快照 + 哪条没过;5 min 过期,批准时现价重报重跑闸。

开关是更大的放权:webhook Routine 的「允许自动下单」只在手机会话打开 / 调高,web / 桌面只能关 / 调低。触发器改成 webhook 时清掉 autoTrade。伪造攻击最坏损失 = min(剩余预算, 当日累计 notional)。

05令牌生命周期

生成

Routine 建好时没有地址,在设置里按需生成。完整 URL 只显示一次 + 「复制 TradingView 模板」。之后只剩掩码 ••••末4位 + 生成时间 + 最近来件时间。

重新生成 = 轮换

旧令牌立即作废,无宽限期(泄露要立刻止血)。找回地址 = 在当前设备重新生成。

撤销

只作废不签发,回到「尚未生成」。暂停回 409,撤销回 404。三个操作都是收紧,任意已登录端可做。

只存哈希

不做可再次查看的信封加密——DO 存储或 KEK 任一泄露都会让全部 URL 泄露。作废哈希留 30 天,用来把旧 alert 归因为 unknown_token · 来自 x.x.x.x。

MAC key 独立环

ROUTINE_WEBHOOK_MAC_KEYS,不从凭证 KEK 派生:常规 KEK 轮换不该让几百个 TradingView alert 同时失效。怀疑泄露才签 /v2/ 并通知重新生成。

v1 不做

入站签名验证(须可逆存密钥,TradingView 签不了)、来源 IP 白名单(出口 IP 会变)。签名头收到也照常处理。

06成本与滥用:三层限额 + 串行

层 位置 键 数值 管什么
边缘 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 预检不做——确定性规则归策略脚本。

07推送与边界

Run 结束推 routine_webhook { outcome }
executed 至少一笔挂上 / 成交 不受冷却
not_executed 尝试写但被拒 / 失败
done 没尝试写 受 1h 冷却
failed Run 失败

收到来件不推;转审批只推审批。stale / busy 不推(防骚扰通道),每日首次插线程提示。日量顶满推所有者级安全通知 webhook_daily_limit。

执行端与对话边界
  • 禁 绑定桌面节点:外部方不能驱动你的 Mac;改成 webhook 时解除绑定
  • 可 云电脑(默认关),用了云电脑的那次不自动下单
  • bot 能经对话建 webhook Routine,但不签发地址;回执带「去生成地址」
  • list_routines 只给 { hasAddress, lastDeliveryAt };生成 / 轮换 / 撤销无 tool
  • 用户把地址粘进聊天:落库前隐藏令牌 + 建议重新生成,不自动轮换

08落地切片

全部经曳光弹间接 blocked-by BERTH-2420(Sigla 代码层改名)——berth.pro 的 301 会把 POST 变 GET,任何环境都不在 berth.pro 签地址。

2439
曳光弹:web 建 webhook Routine,生成地址,POST 即起 Run
2440
信号字段解析:纯信号不污染,坏字段降级
2441
入口契约补全:去重 / 过期 / 不生效 / 投递记录
2442
地址生命周期:轮换 / 撤销 / 掩码 / 失效归因
2443
对话边界:令牌不进模型,改触发器后果上卡
2444
三层限额 + 每 Routine 串行队列
2445
交易闸:方向 / 标的 / 信号价校验
2446
推送:结局 · 审批优先 · 日量安全提示
2447
mobile 配置面:地址 / 投递记录 / 来件卡
2448 · 2449
设计稿 → 照稿还原(web + mobile)
2450
testnet 真 TradingView 端到端 + mainnet 开通清单