Cloudflare Durable Object SQLite(每所有者一实例) · BOT_OWNER → BotOwner(apps/bot/wrangler.jsonc) · contexts/bot

BotOwner DO

一个用户一间「Bot 办公室」:他名下的全部 bot、Routine、运行记录、模型凭证、画像,以及 bot 下单要走的审批 / 执行账本 / 审计和交易委托授权,都锁在这一个 Durable Object 的 SQLite 里。

不管钱:订单 / 仓位 / 成交 / 余额的权威是 Hyperliquid L1,执行账本与审计只记 venue 单号作引用(ADR-0083)。用户身份与会话归 USER_DB(apps/server);线程消息本体在同一 DO 的 Think session 表(cf_agents_*,Agents SDK 自建,不在本迁移里);平台开关 / 配额参数在 KV BOT_CONFIG;KEK 在 Secrets Store。

21 张表 185 个字段 10 个聚合 17 个迁移 语义层已对齐 · sha256:c55d89cfc093d7be

板块颜色贯穿全文

全库约定

业务流程跟着流程看表怎么变

💬 建一个 bot 并和它聊一句

  1. 首次带有效会话访问 → owner.ensure 建 owner 行(DO 名 = user_id)
    之后每次连上刷新 last_connected_at,休眠的顺带唤醒
  2. 建 bot → 插入 bots,session_name = bot-<bot_id>,对应一个 Think session
  3. 客户端要连线程 → 签 ws_tickets(只存哈希,≤ 60 s)→ WS 升级时 DELETE … RETURNING 消费掉
  4. 用户发消息 → runs 插入 running(operation_id = 客户端请求 id);每次模型调用累加 token / 约花费,每次 tool 调用写 run_tool_calls 并改计数
    没有可用凭证就不产生 Run
  5. 消息落进 Think session → thread_versions / thread_heads bump、bots 刷新最后消息预览;Run 收尾记 last_message_id
  6. 客户端按版本号增量同步;打开线程 = bots.last_read_at 推到此刻

📈 价格穿越告警(条件 Routine)命中一次

  1. 用户在 Routine 页新建(或批准 bot 提议的 persistent_change 卡)→ 插入 routines,同一事务给每个条件建 condition_states(待命 + 待定基线)
  2. 第一次检查只记基线,不算命中;检查失败只写 routines.last_error 与 check_failures、不产生 Run
  3. 价格穿越阈值 → 该条件 armed 置 0,发起 source = routine 的 runs(带 hits_json),模型写解读落线程
  4. 推送前看冷却(last_pushed_at 1 h 内不推)、静音、quota_counters 的 push 配额;推了就 markPushed
    被拦下的命中照样在线程里
  5. 价格退回并越过 1% 回差 → 重新待命;改了触发器则整组 condition_states 删掉重建

🧾 bot 在聊天里提议下一笔市价单

  1. hl__place_order 构造写意图 → 现读行情报价 → 策略闸读 trading_policy(kill switch / 档位 / 额度,额度用量按 venue_executions.submitted_at 现算)
  2. 每笔确认档 → 插入 approvals(pending,deadline = 现在 + 5 min,epoch ms),同意图已有待确认卡则复用;每段写一条 audit_events
    闸内自动档额度内则 decided_by = policy 当场放行
  3. 用户在手机批准 → 条件 UPDATE 到 approved → 现价重报、重过闸
  4. venue_executions 登记 pending(cloid 由 approval_id 派生)→ 委托 key 签名 → 发出前一刻置 submitted
  5. 拿到 HL 回执 → 执行账本 settled、审批 executed(resolved_at + 回执锚点 receipt_after_message_id)→ 线程追加回执卡
    DO 若在 submitted 后被驱逐,起来只按 cloid 回读,永不重发

🔑 开通交易委托,90 天后续签

  1. 首次签发:delegation_dek 建行;生成委托 key,密文写进新 delegations 行(requested),同时挂上签发请求 request_json(≤ 10 min)
    部分唯一索引保证全库同时只有一个未完结请求
  2. 手机核验请求、签 approveAgent 交回 → DO 发给 HL → submitted;旧地址还在注销则扣着签名(submission_json + submit_retry_at)退避重发
  3. 链上核到生效 → active,请求列清空、记 activated_at;被顶下来的旧地址写进 delegation_removal_watch
  4. 到期前 7 天推一次续签提醒(expiry_notice_at)→ 续签 = 新建一行新 key;新的生效后旧行 expired(rotated),密文置空
  5. 用户撤销 → revoking(同一条 UPDATE 销毁 key,即刻生效)→ 顶替上链后 revoked;行永远留着,地址不复用

设计要点

🏠 一个所有者一个库DO 名就是 user_id,没有 owner_id 列,也没有跨用户查询;单行表用 CHECK (id = 1)。硬删 = storage.deleteAll(),不逐表删。
🔐 只存密文,销毁 = 置空API key、委托 key 都是信封加密(KEK 在 Secrets Store),AAD 绑所有者防行搬运;委托 key 进 revoking / 终态时同一条 UPDATE 把密文置 NULL,这一刻就是撤销生效点。
🎯 状态机 = domain 判定 + 条件 UPDATE审批 / 委托 / 执行账本都由 next* 纯函数判合法迁移,仓储只做 WHERE status = <旧值> 的乐观并发写;被抢先就回 null,调用方当「已答过」。
🚫 submitted 之后永不重发执行账本在发出前一刻置 submitted,cloid 由审批 ID 派生;DO 被驱逐后只能按 cloid 回读收口,宁可 unresolved 也不双发。
🧮 用量与预算现算,不落累计列交易当日累计、action 预算、Routine 自动下单已花额度都从 venue_executions.submitted_at 现算——账本是唯一事实,避免两份数字对不上。
⏱️ 时间两种口径业务时间是 ISO TEXT(字典序可比);与 HL / 签名请求直接对齐的时刻(deadline、valid_until、request_expires_at、submit_retry_at、since)是 INTEGER epoch ms。读代码时注意别混比。
🔢 线程同步靠版本号Think 存储没有更新序,thread_heads / thread_versions 给每条消息记最近一次被改的版本;客户端拿游标增量拉,早于 reset_version 的游标整线程重拉。