@jsonstudio/rcc 0.89.1562 → 0.89.1803

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. package/README.md +97 -13
  2. package/configsamples/config.json +8 -8
  3. package/configsamples/config.reference.json +1 -1
  4. package/configsamples/provider/crs/config.v1.json +1 -1
  5. package/configsamples/provider/glm/config.v1.json +1 -1
  6. package/configsamples/provider/glm-anthropic/config.v1.json +1 -1
  7. package/configsamples/provider/kimi/config.v1.json +1 -1
  8. package/configsamples/provider/lmstudio/config.v1.json +2 -1
  9. package/configsamples/provider/mimo/config.v1.json +1 -1
  10. package/configsamples/provider/modelscope/config.v1.json +1 -1
  11. package/configsamples/provider/qwen/config.v1.json +1 -1
  12. package/configsamples/provider/tab/config.v1.json +2 -1
  13. package/configsamples/provider/tabglm/config.v1.json +1 -1
  14. package/dist/build-info.js +3 -3
  15. package/dist/build-info.js.map +1 -1
  16. package/dist/cli/commands/config.js +8 -9
  17. package/dist/cli/commands/config.js.map +1 -1
  18. package/dist/cli/commands/restart.d.ts +4 -12
  19. package/dist/cli/commands/restart.js +226 -120
  20. package/dist/cli/commands/restart.js.map +1 -1
  21. package/dist/cli/commands/start.d.ts +1 -0
  22. package/dist/cli/commands/start.js +28 -1
  23. package/dist/cli/commands/start.js.map +1 -1
  24. package/dist/cli/commands/status.js +12 -6
  25. package/dist/cli/commands/status.js.map +1 -1
  26. package/dist/cli/config/init-provider-catalog.js +12 -11
  27. package/dist/cli/config/init-provider-catalog.js.map +1 -1
  28. package/dist/cli.js +3 -14
  29. package/dist/cli.js.map +1 -1
  30. package/dist/client/anthropic/anthropic-protocol-client.d.ts +1 -0
  31. package/dist/client/anthropic/anthropic-protocol-client.js +25 -0
  32. package/dist/client/anthropic/anthropic-protocol-client.js.map +1 -1
  33. package/dist/commands/oauth.js +185 -9
  34. package/dist/commands/oauth.js.map +1 -1
  35. package/dist/commands/token-daemon.js +12 -2
  36. package/dist/commands/token-daemon.js.map +1 -1
  37. package/dist/docs/daemon-admin-ui.html +1242 -234
  38. package/dist/index.js +119 -0
  39. package/dist/index.js.map +1 -1
  40. package/dist/manager/index.d.ts +2 -0
  41. package/dist/manager/index.js +39 -2
  42. package/dist/manager/index.js.map +1 -1
  43. package/dist/manager/modules/quota/antigravity-quota-manager.d.ts +29 -5
  44. package/dist/manager/modules/quota/antigravity-quota-manager.js +369 -113
  45. package/dist/manager/modules/quota/antigravity-quota-manager.js.map +1 -1
  46. package/dist/manager/modules/quota/provider-quota-daemon.cooldown.d.ts +7 -0
  47. package/dist/manager/modules/quota/provider-quota-daemon.cooldown.js +61 -0
  48. package/dist/manager/modules/quota/provider-quota-daemon.cooldown.js.map +1 -1
  49. package/dist/manager/modules/quota/provider-quota-daemon.d.ts +1 -0
  50. package/dist/manager/modules/quota/provider-quota-daemon.events.js +134 -5
  51. package/dist/manager/modules/quota/provider-quota-daemon.events.js.map +1 -1
  52. package/dist/manager/modules/quota/provider-quota-daemon.js +19 -13
  53. package/dist/manager/modules/quota/provider-quota-daemon.js.map +1 -1
  54. package/dist/manager/modules/quota/provider-quota-daemon.model-backoff.d.ts +1 -0
  55. package/dist/manager/modules/quota/provider-quota-daemon.model-backoff.js +8 -3
  56. package/dist/manager/modules/quota/provider-quota-daemon.model-backoff.js.map +1 -1
  57. package/dist/manager/modules/token/index.js +2 -2
  58. package/dist/manager/modules/token/index.js.map +1 -1
  59. package/dist/manager/quota/provider-quota-center.d.ts +9 -0
  60. package/dist/manager/quota/provider-quota-center.js +19 -2
  61. package/dist/manager/quota/provider-quota-center.js.map +1 -1
  62. package/dist/modules/llmswitch/bridge.d.ts +33 -1
  63. package/dist/modules/llmswitch/bridge.js +170 -2
  64. package/dist/modules/llmswitch/bridge.js.map +1 -1
  65. package/dist/modules/llmswitch/core-loader.js +64 -11
  66. package/dist/modules/llmswitch/core-loader.js.map +1 -1
  67. package/dist/modules/pipeline/utils/debug-logger.d.ts +1 -0
  68. package/dist/modules/pipeline/utils/debug-logger.js +50 -3
  69. package/dist/modules/pipeline/utils/debug-logger.js.map +1 -1
  70. package/dist/providers/auth/apikey-auth.js +15 -3
  71. package/dist/providers/auth/apikey-auth.js.map +1 -1
  72. package/dist/providers/auth/oauth-auth.js +26 -2
  73. package/dist/providers/auth/oauth-auth.js.map +1 -1
  74. package/dist/providers/auth/oauth-lifecycle.d.ts +13 -1
  75. package/dist/providers/auth/oauth-lifecycle.js +346 -45
  76. package/dist/providers/auth/oauth-lifecycle.js.map +1 -1
  77. package/dist/providers/auth/oauth-repair-cooldown.d.ts +21 -0
  78. package/dist/providers/auth/oauth-repair-cooldown.js +100 -0
  79. package/dist/providers/auth/oauth-repair-cooldown.js.map +1 -0
  80. package/dist/providers/auth/oauth-repair-env.d.ts +1 -0
  81. package/dist/providers/auth/oauth-repair-env.js +79 -0
  82. package/dist/providers/auth/oauth-repair-env.js.map +1 -0
  83. package/dist/providers/auth/qwen-userinfo-helper.d.ts +2 -0
  84. package/dist/providers/auth/qwen-userinfo-helper.js +72 -40
  85. package/dist/providers/auth/qwen-userinfo-helper.js.map +1 -1
  86. package/dist/providers/auth/tokenfile-auth.d.ts +2 -0
  87. package/dist/providers/auth/tokenfile-auth.js +163 -21
  88. package/dist/providers/auth/tokenfile-auth.js.map +1 -1
  89. package/dist/providers/core/api/provider-types.d.ts +10 -0
  90. package/dist/providers/core/config/camoufox-launcher.d.ts +3 -0
  91. package/dist/providers/core/config/camoufox-launcher.js +190 -3
  92. package/dist/providers/core/config/camoufox-launcher.js.map +1 -1
  93. package/dist/providers/core/config/oauth-flows.js +50 -19
  94. package/dist/providers/core/config/oauth-flows.js.map +1 -1
  95. package/dist/providers/core/config/provider-oauth-configs.js +1 -1
  96. package/dist/providers/core/config/provider-oauth-configs.js.map +1 -1
  97. package/dist/providers/core/runtime/gemini-cli-http-provider.d.ts +5 -0
  98. package/dist/providers/core/runtime/gemini-cli-http-provider.js +172 -15
  99. package/dist/providers/core/runtime/gemini-cli-http-provider.js.map +1 -1
  100. package/dist/providers/core/runtime/gemini-http-provider.d.ts +11 -0
  101. package/dist/providers/core/runtime/gemini-http-provider.js +281 -3
  102. package/dist/providers/core/runtime/gemini-http-provider.js.map +1 -1
  103. package/dist/providers/core/runtime/http-request-executor.js +55 -0
  104. package/dist/providers/core/runtime/http-request-executor.js.map +1 -1
  105. package/dist/providers/core/runtime/http-transport-provider.js +10 -14
  106. package/dist/providers/core/runtime/http-transport-provider.js.map +1 -1
  107. package/dist/providers/core/runtime/provider-factory.d.ts +1 -0
  108. package/dist/providers/core/runtime/provider-factory.js +40 -2
  109. package/dist/providers/core/runtime/provider-factory.js.map +1 -1
  110. package/dist/providers/core/strategies/oauth-auth-code-flow.js +45 -2
  111. package/dist/providers/core/strategies/oauth-auth-code-flow.js.map +1 -1
  112. package/dist/providers/core/strategies/oauth-device-flow.js +13 -2
  113. package/dist/providers/core/strategies/oauth-device-flow.js.map +1 -1
  114. package/dist/providers/core/strategies/oauth-refresh-errors.d.ts +1 -0
  115. package/dist/providers/core/strategies/oauth-refresh-errors.js +26 -0
  116. package/dist/providers/core/strategies/oauth-refresh-errors.js.map +1 -0
  117. package/dist/providers/core/utils/snapshot-writer.d.ts +4 -2
  118. package/dist/providers/core/utils/snapshot-writer.js +86 -23
  119. package/dist/providers/core/utils/snapshot-writer.js.map +1 -1
  120. package/dist/scripts/camoufox/launch-auth.mjs +545 -49
  121. package/dist/server/handlers/chat-handler.js +1 -1
  122. package/dist/server/handlers/chat-handler.js.map +1 -1
  123. package/dist/server/handlers/handler-utils.d.ts +1 -0
  124. package/dist/server/handlers/handler-utils.js +231 -3
  125. package/dist/server/handlers/handler-utils.js.map +1 -1
  126. package/dist/server/handlers/messages-handler.js +1 -1
  127. package/dist/server/handlers/messages-handler.js.map +1 -1
  128. package/dist/server/handlers/responses-handler.js +17 -5
  129. package/dist/server/handlers/responses-handler.js.map +1 -1
  130. package/dist/server/handlers/sse-dispatcher.js +10 -1
  131. package/dist/server/handlers/sse-dispatcher.js.map +1 -1
  132. package/dist/server/runtime/http-server/daemon-admin/control-handler.d.ts +3 -0
  133. package/dist/server/runtime/http-server/daemon-admin/control-handler.js +389 -0
  134. package/dist/server/runtime/http-server/daemon-admin/control-handler.js.map +1 -0
  135. package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js +190 -5
  136. package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js.map +1 -1
  137. package/dist/server/runtime/http-server/daemon-admin/providers-handler.js +2 -1
  138. package/dist/server/runtime/http-server/daemon-admin/providers-handler.js.map +1 -1
  139. package/dist/server/runtime/http-server/daemon-admin/quota-handler.js +116 -14
  140. package/dist/server/runtime/http-server/daemon-admin/quota-handler.js.map +1 -1
  141. package/dist/server/runtime/http-server/daemon-admin/routing-policy.d.ts +30 -0
  142. package/dist/server/runtime/http-server/daemon-admin/routing-policy.js +133 -0
  143. package/dist/server/runtime/http-server/daemon-admin/routing-policy.js.map +1 -0
  144. package/dist/server/runtime/http-server/daemon-admin/status-handler.js +40 -1
  145. package/dist/server/runtime/http-server/daemon-admin/status-handler.js.map +1 -1
  146. package/dist/server/runtime/http-server/daemon-admin-routes.d.ts +5 -0
  147. package/dist/server/runtime/http-server/daemon-admin-routes.js +3 -0
  148. package/dist/server/runtime/http-server/daemon-admin-routes.js.map +1 -1
  149. package/dist/server/runtime/http-server/executor-pipeline.d.ts +10 -0
  150. package/dist/server/runtime/http-server/executor-pipeline.js +6 -0
  151. package/dist/server/runtime/http-server/executor-pipeline.js.map +1 -1
  152. package/dist/server/runtime/http-server/executor-response.js +26 -0
  153. package/dist/server/runtime/http-server/executor-response.js.map +1 -1
  154. package/dist/server/runtime/http-server/hub-shadow-compare.js +41 -3
  155. package/dist/server/runtime/http-server/hub-shadow-compare.js.map +1 -1
  156. package/dist/server/runtime/http-server/index.d.ts +9 -0
  157. package/dist/server/runtime/http-server/index.js +337 -91
  158. package/dist/server/runtime/http-server/index.js.map +1 -1
  159. package/dist/server/runtime/http-server/middleware.js +27 -1
  160. package/dist/server/runtime/http-server/middleware.js.map +1 -1
  161. package/dist/server/runtime/http-server/request-executor.js +159 -24
  162. package/dist/server/runtime/http-server/request-executor.js.map +1 -1
  163. package/dist/server/runtime/http-server/routes.d.ts +1 -0
  164. package/dist/server/runtime/http-server/routes.js +36 -3
  165. package/dist/server/runtime/http-server/routes.js.map +1 -1
  166. package/dist/server/runtime/http-server/server-id.d.ts +1 -0
  167. package/dist/server/runtime/http-server/server-id.js +18 -0
  168. package/dist/server/runtime/http-server/server-id.js.map +1 -0
  169. package/dist/server/runtime/http-server/stats-manager.d.ts +2 -0
  170. package/dist/server/runtime/http-server/stats-manager.js +63 -7
  171. package/dist/server/runtime/http-server/stats-manager.js.map +1 -1
  172. package/dist/server/runtime/http-server/types.d.ts +2 -0
  173. package/dist/server/utils/stage-logger.js +54 -9
  174. package/dist/server/utils/stage-logger.js.map +1 -1
  175. package/dist/token-daemon/history-store.d.ts +8 -3
  176. package/dist/token-daemon/history-store.js +41 -20
  177. package/dist/token-daemon/history-store.js.map +1 -1
  178. package/dist/token-daemon/index.d.ts +5 -1
  179. package/dist/token-daemon/index.js +191 -11
  180. package/dist/token-daemon/index.js.map +1 -1
  181. package/dist/token-daemon/quota-auth-issue.d.ts +7 -0
  182. package/dist/token-daemon/quota-auth-issue.js +231 -0
  183. package/dist/token-daemon/quota-auth-issue.js.map +1 -0
  184. package/dist/token-daemon/token-daemon.d.ts +2 -0
  185. package/dist/token-daemon/token-daemon.js +177 -14
  186. package/dist/token-daemon/token-daemon.js.map +1 -1
  187. package/dist/token-portal/local-token-portal.js +6 -0
  188. package/dist/token-portal/local-token-portal.js.map +1 -1
  189. package/docs/ANTIGRAVITY_IDE_FORWARD_PROXY.md +61 -0
  190. package/docs/ANTIGRAVITY_THOUGHT_SIGNATURE_BOOTSTRAP_429.md +80 -0
  191. package/docs/CLOCK.md +94 -0
  192. package/docs/DAEMON_CONTROL_PLANE.md +34 -0
  193. package/docs/OAUTH.md +172 -0
  194. package/docs/PROVIDERS_BUILTIN.md +5 -3
  195. package/docs/PROVIDER_TYPES.md +6 -4
  196. package/docs/QUOTA_MANAGER_V3.md +54 -0
  197. package/docs/ROUTING_POLICY_SCHEMA.md +47 -0
  198. package/docs/ROUTING_POLICY_UI.md +11 -0
  199. package/docs/SERVERTOOL_CLOCK_DESIGN.md +56 -25
  200. package/docs/antigravity-routing-contract.md +17 -11
  201. package/docs/config-secrets.md +49 -0
  202. package/docs/daemon-admin-ui.html +1242 -234
  203. package/docs/oauth-authentication-guide.md +4 -0
  204. package/docs/oauth-iflow-implementation.md +4 -0
  205. package/docs/provider-quota-design.md +11 -0
  206. package/docs/providers/antigravity-gemini-provider-compat.md +1 -0
  207. package/docs/providers/antigravity-thought-signature.md +127 -0
  208. package/docs/providers/tabglm-claude-code-compat.md +11 -3
  209. package/docs/refactoring/host-sharedmodule-safe-migration-plan.md +164 -0
  210. package/docs/token-daemon-preview.html +2 -2
  211. package/docs/token-refresh-daemon-plan.md +6 -6
  212. package/package.json +4 -3
  213. package/scripts/antigravity-ide-forward-proxy.mjs +362 -0
  214. package/scripts/backfill-apply-patch-exec-errorsamples.mjs +19 -0
  215. package/scripts/camoufox/launch-auth.mjs +545 -49
  216. package/scripts/ci/repo-sanity.mjs +2 -0
  217. package/scripts/install-global.sh +46 -0
  218. package/scripts/migrate-antigravity-session-signatures-alias.mjs +193 -0
  219. package/scripts/migrate-antigravity-session-signatures.mjs +165 -0
  220. package/scripts/tests/blackbox-rcc-vs-routecodex-antigravity.mjs +44 -9
  221. package/scripts/tests/ci-jest.mjs +3 -0
  222. package/scripts/verify-client-headers.mjs +33 -5
  223. package/scripts/virtual-router-dryrun.mjs +333 -0
package/docs/CLOCK.md ADDED
@@ -0,0 +1,94 @@
1
+ # `clock`(Time + Alarm)概览
2
+
3
+ 本项目的 `clock` 是 llmswitch-core 的 ServerTool,用于给模型/MCP提供两类能力:
4
+
5
+ 1. **获取时间**:可通过工具调用 `clock(action="get")` 获取机器可解析的当前时间(UTC + 本地时间)与 NTP 校时状态。
6
+ 2. **设置闹钟/提醒**:可通过工具调用 `clock(action="schedule")` 写入持久化的 session 级任务;到点后在后续请求中注入提醒文本,提示模型进行工具调用。
7
+
8
+ 更详细的设计/语义说明见:`docs/SERVERTOOL_CLOCK_DESIGN.md`。
9
+
10
+ ## 1) 时间从哪里来?(System + NTP)
11
+
12
+ `clock` 的“当前时间”以 `nowMs` 为基准:
13
+
14
+ - **系统时间**:`Date.now()`(毫秒)
15
+ - **NTP 校时**(可选):后台通过 SNTP(UDP/123)向 NTP 服务器获取偏移量 `ntpOffsetMs`,并用 `nowMs = Date.now() + ntpOffsetMs` 作为“校正后的当前时间”
16
+
17
+ 默认 NTP server 列表(可通过环境变量覆盖):
18
+
19
+ - `time.google.com`
20
+ - `time.cloudflare.com`
21
+ - `pool.ntp.org`
22
+
23
+ 相关环境变量:
24
+
25
+ - `ROUTECODEX_CLOCK_NTP=0|false|off`:禁用 NTP
26
+ - `ROUTECODEX_CLOCK_NTP_SERVERS=host1,host2,...`:自定义 NTP servers
27
+ - `ROUTECODEX_CLOCK_NTP_STALE_AFTER_MS=<ms>`:NTP 状态多久后视为 `stale`(默认 6h)
28
+
29
+ NTP 状态持久化路径(落在运行时 session 根目录下):
30
+
31
+ - `$ROUTECODEX_SESSION_DIR/clock/ntp-state.json`
32
+
33
+ ## 2) 时间输出格式(Time Tag)
34
+
35
+ 当 `clock` 功能启用后,Hub Pipeline 会在每次请求的 messages 末尾追加一条 **`role=user`** 的时间标签(Time Tag),格式如下:
36
+
37
+ ```
38
+ [Time/Date]: utc=`2026-02-02T21:22:38.229Z` local=`2026-02-02 13:22:38.229 -08:00` tz=`America/Los_Angeles` nowMs=`1770038558229` ntpOffsetMs=`-12`
39
+ ```
40
+
41
+ 字段含义:
42
+
43
+ - `utc`:ISO8601 UTC 时间
44
+ - `local`:本地时间字符串(含毫秒 + 时区偏移)
45
+ - `tz`:服务端 `Intl.DateTimeFormat().resolvedOptions().timeZone`
46
+ - `nowMs`:校正后的毫秒时间戳
47
+ - `ntpOffsetMs`:当前使用的 NTP 偏移(毫秒)
48
+
49
+ 注意:
50
+
51
+ - `Time Tag` 是“提示信息”,不改变系统消息。
52
+ - 内部 servertool followup hops(`serverToolFollowup=true`)会跳过注入,避免死循环/噪声。
53
+
54
+ ## 3) 闹钟/提醒如何工作?(Schedule → Persist → Inject)
55
+
56
+ ### 3.1 `clock` 工具动作
57
+
58
+ 工具名固定为 `clock`,通过 `action` 区分:
59
+
60
+ - `get`:获取当前时间与 NTP 状态(无需 sessionId)
61
+ - `schedule`:创建提醒任务(需要 sessionId)
62
+ - `list`:列出任务(需要 sessionId)
63
+ - `cancel`:取消指定任务(需要 sessionId)
64
+ - `clear`:清空全部任务(需要 sessionId)
65
+
66
+ ### 3.2 `schedule` 入参(核心字段)
67
+
68
+ `schedule` 使用 `items` 数组,每项包含:
69
+
70
+ - `dueAt`:ISO8601(含时区),例如 `2026-01-21T20:30:00-08:00`
71
+ - `task`:提醒文本(建议写清楚“要调用哪个工具、做什么”)
72
+ - `tool`:可选建议工具名(提示用,不强制)
73
+ - `arguments`:可选建议工具参数(JSON 字符串;不用时传 `"{}"`)
74
+
75
+ ### 3.3 持久化位置
76
+
77
+ 每个 session 的任务持久化在:
78
+
79
+ - `$ROUTECODEX_SESSION_DIR/clock/<sessionId>.json`(`sessionId` 会做安全字符清洗)
80
+
81
+ ### 3.4 触发窗口与“避免同请求死循环”
82
+
83
+ - 触发窗口:当 `now >= dueAt - dueWindowMs` 视为“到点需要提醒”(默认 60s)
84
+ - 为避免“在同一次 HTTP 请求链里 schedule 后立刻触发”导致死循环:
85
+ - 若 `schedule` 的 `dueAt` 已经落在触发窗口(`dueAt <= now + dueWindowMs`),系统会为任务写入 `notBeforeRequestId=<当前请求 requestId>`
86
+ - 在同一请求链(例如 `requestId` 或 `requestId:clock_followup`)内不会投递该任务;必须等到下一次独立请求才会投递
87
+
88
+ ## 4) MCP 如何获取时间/设置闹钟?
89
+
90
+ 两种方式:
91
+
92
+ 1. **主动工具调用**:MCP/模型调用 `clock(action="get")` 或 `clock(action="schedule")`
93
+ 2. **被动注入**:每次请求都会看到 `Time Tag`;到点的提醒会被注入为 `role=user` 的提示文本(建议进行工具调用)
94
+
@@ -0,0 +1,34 @@
1
+ # Daemon Control Plane (single WebUI entry)
2
+
3
+ ## Goal
4
+ WebUI connects to **one** RouteCodex server port and can:
5
+ - discover all local servers
6
+ - broadcast restart
7
+ - manage quota (disable / recover / reset / refresh)
8
+ - fetch a unified snapshot (servers + quota + routing hits)
9
+
10
+ ## Endpoints (daemon-admin, localhost-only, password-auth)
11
+ ### `GET /daemon/control/snapshot`
12
+ Returns:
13
+ - discovered local servers (ports + pids + version + ready)
14
+ - quota snapshot (provider states + antigravity raw snapshot)
15
+ - `virtualRouterConfig` (current runtime config snapshot)
16
+ - `policy` + `policyHash` (best-effort read of routing policy from `configPath` on disk; `policy` is a stable, serializable subset of the user config)
17
+ - `antigravityAliasLeases` (best-effort read of `~/.routecodex/state/antigravity-alias-leases.json` when present; used for session lease/binding observability)
18
+ - `llmsStats` (llmswitch-core stats center snapshot; includes routing hits)
19
+
20
+ ### `POST /daemon/control/mutate`
21
+ Body: `{ action: string, ... }`
22
+
23
+ Supported actions:
24
+ - `servers.restart` (broadcast): sends `SIGUSR2` to other servers; self uses runtime reload when available
25
+ - `quota.refresh`
26
+ - `quota.disable` `{ providerKey, mode: "cooldown"|"blacklist", durationMs }`
27
+ - `quota.recover` `{ providerKey }`
28
+ - `quota.reset` `{ providerKey }`
29
+ - `routing.policy.set` `{ policy }` (writes policy into config file and triggers best-effort broadcast restart)
30
+ - `runtime.restart` (reload config from disk for current server)
31
+
32
+ ## Discovery
33
+ - Uses `~/.routecodex/sessions/*_<port>` + env ports (`ROUTECODEX_PORT/RCC_PORT`) + dev default `5555`.
34
+ - Probes `/health` and filters `server=routecodex`.
package/docs/OAUTH.md ADDED
@@ -0,0 +1,172 @@
1
+ # OAuth Guide(认证与刷新)
2
+
3
+ 本页是 RouteCodex / RCC 的 **OAuth 统一说明**:如何认证、如何刷新、以及常见踩坑(尤其是 “我都授权了但服务器不拿 token”)。
4
+
5
+ > 说明:文中用 `${bin}` 表示 CLI 命令名:release 包是 `rcc`,dev 包是 `routecodex`。
6
+
7
+ ## 1) Token 文件与 `tokenFile` 写法
8
+
9
+ ### Token 存放位置
10
+
11
+ 默认目录:`~/.routecodex/auth/`
12
+
13
+ 常见命名:
14
+ - `qwen-oauth-<seq>-<alias>.json`
15
+ - `antigravity-oauth-<seq>-<alias>.json`
16
+ - `gemini-cli-oauth-<seq>-<alias>.json`
17
+ - `iflow-oauth-<seq>-<alias>.json`
18
+
19
+ ### `tokenFile` 支持两种写法
20
+
21
+ 1) **显式路径**(通用,最稳):
22
+
23
+ ```jsonc
24
+ "auth": { "type": "qwen-oauth", "tokenFile": "~/.routecodex/auth/qwen-oauth-1-default.json" }
25
+ ```
26
+
27
+ 2) **alias(仅文件名,不含路径)**:
28
+
29
+ ```jsonc
30
+ "auth": { "type": "qwen-oauth", "tokenFile": "default" }
31
+ ```
32
+
33
+ 当前推荐 Qwen 用 alias:`default`。
34
+
35
+ - 如果存在 `~/.routecodex/auth/qwen-oauth-1-default.json`,会优先使用它(固定文件名,避免“我刚 reauth 但 server 读的是另一个 seq”的坑)
36
+ - 否则会回退到 `~/.routecodex/auth/qwen-oauth-*-default.json` 中 seq 最新的那份
37
+
38
+ ## 2) 认证(授权登录)
39
+
40
+ ### 2.1 手动认证(通用)
41
+
42
+ ```bash
43
+ ${bin} oauth --force qwen-oauth-1-default.json
44
+ ```
45
+
46
+ - `selector` 支持:token 文件 basename、全路径、或 provider id(例如 `qwen` / `iflow` / `antigravity`)
47
+ - `--force`:强制重新授权(一定会走浏览器/portal)
48
+ - 不带 `--force`:如果 token 仍然有效,会直接跳过(避免“每次都让我重新认证”)
49
+ - 如果你希望看到可见窗口(避免 headless 误以为“没弹出来”),加 `--headful`(等价于 `ROUTECODEX_CAMOUFOX_DEV_MODE=1`)
50
+ - 若你的 `oauthBrowser=camoufox`,Qwen 在 `authorize` 页面需要点一次 Confirm;`oauth` 默认会启用 `qwen` auto(等价于 `oauth qwen-auto ...`)
51
+
52
+ ### 2.2 Camoufox 自动化(推荐:Qwen / Gemini / Antigravity / iFlow)
53
+
54
+ 这些命令会用 Camoufox(固定指纹 + profile)帮你自动完成关键点击。
55
+
56
+ ```bash
57
+ ${bin} oauth qwen-auto qwen-oauth-1-default.json
58
+ ${bin} oauth gemini-auto gemini-cli-oauth-1-youralias.json
59
+ ${bin} oauth antigravity-auto antigravity-oauth-1-youralias.json
60
+ ${bin} oauth iflow-auto iflow-oauth-1-youralias.json
61
+ ```
62
+
63
+ Qwen 的自动化会在授权页自动点击:
64
+
65
+ `button.qwen-confirm-btn`(Confirm)
66
+
67
+ > 如果 auto 失败,会自动退到 headed 模式让你手动完成(Qwen 不走 localhost callback,因此不会再“等不到回调一直卡住”)。
68
+
69
+ WebUI(daemon-admin)里点 “Authorize OAuth” 时也会强制走 Camoufox;若未安装,会返回错误 `camoufox_missing` 并提示安装命令。
70
+
71
+ 当 token 被标记为 `verify required`(Google 风控校验)时,daemon-admin 会提供 `open` 链接,点击后会用 Camoufox 打开验证 URL(固定 profile + 指纹),不要再用系统浏览器。
72
+
73
+ CLI `oauth <selector>` 若发现 quota-manager 已将该 alias 标记为 `verify required`,也会打印明确警告与验证 URL(即使 token 文件仍显示 valid)。
74
+
75
+ 同时 CLI 会提供“一步打开”:
76
+
77
+ - 在 TTY 下会询问是否立刻用 Camoufox 打开验证 URL(默认 **Y**,open-only,不等 callback)
78
+ - 若你希望不提示直接打开:设置 `ROUTECODEX_OAUTH_AUTO_OPEN_VERIFY=1`(或 `RCC_OAUTH_AUTO_OPEN_VERIFY=1`),或在命令上加 `--headful`
79
+ - 若你只想打印 URL:回答 `n`
80
+
81
+ > 注:quota-manager 状态文件默认读取 `${ROUTECODEX_HOME:-$HOME}/.routecodex/quota/quota-manager.json`(便于测试/沙盒环境重定向)。
82
+
83
+ Portal 健康检查(`/health`)默认会等待 **300s**(网络慢时避免过早 timeout),可用环境变量调整:
84
+
85
+ - `ROUTECODEX_OAUTH_PORTAL_READY_TIMEOUT_MS`(总等待)
86
+ - `ROUTECODEX_OAUTH_PORTAL_READY_REQUEST_TIMEOUT_MS`(单次请求)
87
+ - `ROUTECODEX_OAUTH_PORTAL_READY_POLL_MS`(轮询间隔)
88
+
89
+ Camoufox 自动化(Google / Antigravity / Qwen)相关等待也有独立超时(默认 **300s**,避免网络慢/跳转慢导致误判):
90
+
91
+ - `ROUTECODEX_CAMOUFOX_GEMINI_TIMEOUT_MS`(Gemini / Antigravity account/confirm/callback 总等待)
92
+ - `ROUTECODEX_CAMOUFOX_PORTAL_BUTTON_TIMEOUT_MS`(token portal `Continue` 按钮等待)
93
+ - `ROUTECODEX_CAMOUFOX_PORTAL_POPUP_TIMEOUT_MS`(portal 点击后 popup 或同页跳转等待)
94
+ - `ROUTECODEX_CAMOUFOX_PAGE_LOAD_TIMEOUT_MS`(portal 后 OAuth 页 `domcontentloaded` 等待)
95
+
96
+ > Antigravity/Gemini 的确认按钮点击不依赖文案(locale/font 变化),按容器 selector 点击 primary action。
97
+
98
+ ## 3) 刷新(refresh)是怎么工作的?
99
+
100
+ ### 3.1 后台刷新(默认:静默,不弹窗)
101
+
102
+ RouteCodex 运行时会启动 token-daemon 做 **后台刷新**(默认提前刷新窗口:到期前 **30 分钟**;可用 `ROUTECODEX_TOKEN_REFRESH_AHEAD_MIN` 覆盖)。
103
+
104
+ 默认情况下后台会先尝试 **静默 refresh**(`openBrowser=false`)。对以下 provider:
105
+
106
+ - `antigravity`
107
+ - `qwen`
108
+ - `iflow`
109
+
110
+ 当静默 refresh 失败且需要交互式修复时,会尝试用 Camoufox 做 **auto OAuth**(headless),并在 auto 失败时退到 headed 让你手动完成。
111
+
112
+ 为避免“无限认证 / 无限弹窗”,如果连续 **3 次**都等不到用户完成(例如 device-code 超时 / callback 超时),token-daemon 会对该 token **自动暂停**后续后台认证,直到 token 文件被你手动更新(例如执行一次 `${bin} oauth --force ...` 成功写回文件)。
113
+
114
+ > 需要交互式修复时,使用上面的 `${bin} oauth ...` 显式触发。
115
+
116
+ ### 3.3 403 账户验证(Antigravity / Gemini)
117
+
118
+ 如果上游返回 `HTTP 403` 且包含以下关键字之一:
119
+
120
+ - `verify your account`
121
+ - `validation_required`
122
+ - `accounts.google.com/signin/continue`
123
+
124
+ 说明账号需要在浏览器里完成验证/风控解除(不是简单的 token 过期)。此时 RouteCodex 会尝试自动拉起 Camoufox 交互式 OAuth/验证流程。
125
+
126
+ - 对于这类 “Google verify” 403,后台非阻塞 repair 会优先 **直接打开验证 URL**(Camoufox open-only,不等 localhost callback),让路由立即 failover,同时用户可以在 Camoufox 里完成验证。
127
+
128
+ 为避免“无限弹窗/无限认证”,同一个 token(provider + tokenFile)在该类 403 下会有 **30 分钟冷却**(可用环境变量覆盖):
129
+
130
+ - `ROUTECODEX_OAUTH_GOOGLE_VERIFY_COOLDOWN_MS`(默认 `1800000`)
131
+
132
+ 另外,所有 **交互式 OAuth repair**(例如持续 401/403 导致的自动 reauth)也有统一冷却,避免在网络抖动或上游风控期内“疯狂弹窗/疯狂认证”:
133
+
134
+ - `ROUTECODEX_OAUTH_INTERACTIVE_COOLDOWN_MS`(默认 `1800000`)
135
+
136
+ ### 3.2 显式刷新/重登(你主动触发)
137
+
138
+ - 刷新(有效就跳过):`${bin} oauth <selector>`
139
+ - 强制重登:`${bin} oauth --force <selector>`
140
+
141
+ 验证 token 状态:
142
+
143
+ ```bash
144
+ ${bin} oauth validate all
145
+ ```
146
+
147
+ > `oauth validate ...` 只做读取/校验,不会拉起浏览器。
148
+
149
+ ## 4) 常见问题(非常重要)
150
+
151
+ ### “我都授权了,但服务器不拿 token / 还是 401/403”
152
+
153
+ 按顺序排查:
154
+
155
+ 1) **你的配置里有没有这个 provider**
156
+ - 例如 Qwen:`virtualrouter.providers.qwen` 必须存在,否则 token 永远不会被使用
157
+
158
+ 2) **`auth.type` 与 `tokenFile` 是否对得上**
159
+ - Qwen:`"type": "qwen-oauth"` + `tokenFile: "default"`(或写全路径)
160
+ - 认证生成的文件是否真的在 `~/.routecodex/auth/` 下
161
+
162
+ 3) **token 文件内容是否具备可用字段**
163
+ - 至少需要 `access_token`(部分 provider 还会有 `api_key`)
164
+
165
+ 4) **你是不是在后台刷新时期待它弹窗**
166
+ - 默认后台不会弹;需要交互请用 `${bin} oauth --force ...` 或 `${bin} oauth qwen-auto ...`
167
+
168
+ ## 5) 相关文档入口
169
+
170
+ - 内置 Provider 配置:`docs/PROVIDERS_BUILTIN.md`
171
+ - Provider 类型:`docs/PROVIDER_TYPES.md`
172
+ - Antigravity 指纹/养号:`docs/providers/antigravity-fingerprint-ua-warmup.md`
@@ -29,9 +29,11 @@
29
29
  ### 2) OAuth(tokenFile)
30
30
 
31
31
  ```jsonc
32
- "auth": { "type": "qwen-oauth", "tokenFile": "~/.routecodex/auth/qwen-oauth.json" }
32
+ "auth": { "type": "qwen-oauth", "tokenFile": "default" }
33
33
  ```
34
34
 
35
+ > OAuth 详细说明(认证 / 刷新 / auto):`docs/OAUTH.md`
36
+
35
37
  ### 3) Cookie(cookieFile)
36
38
 
37
39
  ```jsonc
@@ -76,8 +78,8 @@
76
78
  ### Qwen(OAuth)
77
79
 
78
80
  - 样本:`configsamples/provider/qwen/config.v1.json`
79
- - 关键点:`auth.type: "qwen-oauth"` + `auth.tokenFile` 指向你的 token 文件;`compatibilityProfile: "chat:qwen"`
80
- - 你需要:先完成一次 OAuth 登录生成 tokenFile(或按你自己的路径修改 tokenFile)
81
+ - 关键点:`auth.type: "qwen-oauth"` + `auth.tokenFile: "default"`(或写全路径);`compatibilityProfile: "chat:qwen"`
82
+ - 你需要:先完成一次 OAuth 登录生成 tokenFile(详见 `docs/OAUTH.md`)
81
83
 
82
84
  ### iFlow(OAuth)
83
85
 
@@ -24,7 +24,10 @@
24
24
  "id": "openai",
25
25
  "type": "openai",
26
26
  "baseURL": "https://api.openai.com/v1",
27
- "auth": { "type": "apikey", "apiKey": "YOUR_API_KEY_HERE" }
27
+ // 推荐:用环境变量引用,config 可共享/可进 repo
28
+ "auth": { "type": "apikey", "apiKey": "${OPENAI_API_KEY}" }
29
+ // 兼容:也支持直接明文写入(不推荐)
30
+ // "auth": { "type": "apikey", "apiKey": "sk-..." }
28
31
  }
29
32
  ```
30
33
 
@@ -38,7 +41,7 @@
38
41
  "type": "openai",
39
42
  "baseURL": "https://portal.qwen.ai/v1",
40
43
  "compatibilityProfile": "chat:qwen",
41
- "auth": { "type": "qwen-oauth", "tokenFile": "~/.routecodex/auth/qwen-oauth.json" }
44
+ "auth": { "type": "qwen-oauth", "tokenFile": "default" }
42
45
  }
43
46
  ```
44
47
 
@@ -49,7 +52,6 @@
49
52
  "id": "tab",
50
53
  "type": "responses",
51
54
  "baseURL": "https://api.tabcode.cc/openai",
52
- "auth": { "type": "apikey", "apiKey": "YOUR_API_KEY_HERE" }
55
+ "auth": { "type": "apikey", "apiKey": "${TAB_API_KEY}" }
53
56
  }
54
57
  ```
55
-
@@ -0,0 +1,54 @@
1
+ # Quota Manager V3 (Core-Owned)
2
+
3
+ ## Goal
4
+ Make **quota / cooldown / blacklist / auth-verify gating** a *single source of truth* and stop producing router-local cooldown state.
5
+
6
+ - Routing path remains: `HTTP server → llmswitch-core Hub Pipeline → Provider V2 → upstream`.
7
+ - VirtualRouter **consumes** quota view only; it must not invent cooldown/health decisions in host/server/provider layers.
8
+
9
+ ## Where it lives
10
+ - **llmswitch-core**: `sharedmodule/llmswitch-core/src/quota/*`
11
+ - `QuotaManager` state machine + persistence contract (`QuotaStore`)
12
+ - Outputs `ProviderQuotaView` (consumed by VirtualRouter selection)
13
+ - **routecodex host**: `src/manager/modules/quota/antigravity-quota-manager.ts`
14
+ - Implements `QuotaStore` (I/O only)
15
+ - Subscribes to `providerErrorCenter` + `providerSuccessCenter` and forwards events into core `QuotaManager`
16
+ - Feeds Antigravity external quota snapshot into `QuotaManager.updateProviderPoolState(...)`
17
+
18
+ ## Inputs
19
+ 1) Provider errors (from Provider V2 → `emitProviderError(...)`)
20
+ 2) Provider successes (from HTTP server → `providerSuccessCenter.emit(...)`)
21
+ 3) External quota snapshots (Antigravity quota API refresh)
22
+ 4) Static metadata per providerKey (authType / priorityTier / apikeyDailyResetTime)
23
+
24
+ ## Outputs
25
+ - `quotaView(providerKey) -> ProviderQuotaViewEntry`
26
+ - `inPool`, `cooldownUntil`, `blacklistUntil`
27
+ - `selectionPenalty`, `lastErrorAtMs`, `consecutiveErrorCount`
28
+
29
+ ## Key semantics
30
+ - **HTTP 402** (apikey daily cost limit): blacklist until `resetAt` (prefer upstream `resetAt`, else use `apikeyDailyResetTime`, default `12:00` local).
31
+ - **Antigravity OAuth auth verification required**: blacklist the whole `antigravity.<alias>.*` group with `authIssue=google_account_verification`.
32
+ - **Antigravity thought-signature missing**: cooldown Gemini series keys under `antigravity.<alias>.*` to avoid request storms.
33
+
34
+ ## Config
35
+ You can set the default daily reset time used for **HTTP 402 without upstream `resetAt`**:
36
+
37
+ ```json
38
+ {
39
+ "virtualrouter": {
40
+ "quota": {
41
+ "apikeyDailyResetTime": "16:00Z"
42
+ }
43
+ }
44
+ }
45
+ ```
46
+
47
+ - Format:
48
+ - `"HH:MM"` = local time (server timezone)
49
+ - `"HH:MMZ"` = UTC time
50
+ - Default: `"12:00"` (local)
51
+
52
+ ## Persistence
53
+ - Core `QuotaManager` persists via host `QuotaStore` at `~/.routecodex/quota/quota-manager.json`.
54
+ - Legacy fallback migration reads `~/.routecodex/quota/provider-quota.json` when the new snapshot is missing.
@@ -0,0 +1,47 @@
1
+ # Routing Policy Schema (Control Plane)
2
+
3
+ ## Goal
4
+ Provide a **serializable** routing policy snapshot for WebUI, and a single write path that updates the user config on disk and triggers a controlled restart.
5
+
6
+ This policy is **not** a separate runtime config. It is a stable subset of the user config that maps to llmswitch-core Virtual Router inputs.
7
+
8
+ ## Read: `GET /daemon/control/snapshot`
9
+ The response includes:
10
+ - `routing.policy` (nullable)
11
+ - `routing.policyHash` (nullable; `sha256(stableStringify(policy))`)
12
+
13
+ The policy is read best-effort from the current server `configPath` on disk.
14
+
15
+ ## Write: `POST /daemon/control/mutate`
16
+ Action:
17
+ - `routing.policy.set` `{ policy }`
18
+
19
+ Behavior:
20
+ - Writes the policy fields into `virtualrouter.*` in the config file.
21
+ - Reloads the current server runtime from disk (best-effort) without cutting the HTTP response.
22
+ - Sends `SIGUSR2` to other local servers (best-effort) so they restart and pick up the new config.
23
+
24
+ ## Policy object (V1)
25
+ Canonical snapshot shape:
26
+
27
+ ```json
28
+ {
29
+ "schemaVersion": 1,
30
+ "virtualrouter": {
31
+ "routing": { "default": ["provider.model"] },
32
+ "loadBalancing": { "strategy": "round-robin" },
33
+ "classifier": {},
34
+ "health": {},
35
+ "contextRouting": {},
36
+ "webSearch": {},
37
+ "execCommandGuard": {},
38
+ "clock": {}
39
+ }
40
+ }
41
+ ```
42
+
43
+ Notes:
44
+ - `virtualrouter.routing` is required; other fields are optional.
45
+ - For compatibility, `routing.policy.set` also accepts a flattened input shape (`{ routing, loadBalancing, ... }`) and will normalize it into the canonical form.
46
+ - The control plane does **not** interpret or validate routing semantics; llmswitch-core remains the single source of truth for routing behavior.
47
+
@@ -0,0 +1,11 @@
1
+ # Routing Policy UI (planned)
2
+
3
+ This repo already has a CRUD editor under the daemon-admin “Runtime Routing Pool” tab.
4
+
5
+ The next step (not implemented yet) is a unified **policy editor** in the control plane that:
6
+ - edits `virtualrouter.routing` tiers (targets/mode/priority) and `virtualrouter.loadBalancing` (strategy/healthWeighted/contextWeighted/aliasSelection)
7
+ - writes to the user config file and triggers a controlled restart (`config-driven` rule)
8
+
9
+ This doc exists to reserve the contract and avoid ad-hoc runtime patching.
10
+
11
+ See `docs/ROUTING_POLICY_SCHEMA.md` for the control-plane schema and the `/daemon/control/*` contract.
@@ -1,15 +1,16 @@
1
- # ServerTool: `clock`(定时提醒)详细设计(Draft)
1
+ # ServerTool: `clock`(Time + Alarm)详细设计(Draft)
2
2
 
3
- > 目标:在不引入“旁路”执行路径的前提下,为每个 session 提供可持久化的定时提醒能力,并在后续请求中把提醒注入到模型上下文里。
4
- > 本文是**详细设计**,不包含实现。
3
+ > 目标:在不引入“旁路”执行路径的前提下,为每个 session 提供可持久化的 **定时提醒(Alarm)**,并提供可机器读取的 **当前时间(Time)** 能力给 MCP/模型使用。
4
+ > 本文是**详细设计**,用于约束行为与契约(实现细节以源码为准)。
5
5
 
6
6
  ## 0. 背景与约束
7
7
 
8
8
  - 单执行路径:所有模型调用仍必须走 `HTTP server → llmswitch-core Hub Pipeline → Provider V2 → upstream`。
9
9
  - llmswitch-core 拥有工具语义与路由:Host/Provider 不得自行修复 tool calls、重写参数或决定路由,只能注入依赖与 IO。
10
10
  - Provider 层只做 transport(auth/http/retry/compat),不得理解 payload 语义。
11
- - 时钟任务必须 **serverId + sessionId** 作用域隔离,避免跨 server 串读(stopMessage 的坑不能再踩)。
12
- - “过期删除”与“过期也算触发”必须统一:保留期(retention)设为 **20 分钟**(`retentionMs = 20 * 60 * 1000`)。
11
+ - `clock` 的存储根目录由运行时环境变量 `ROUTECODEX_SESSION_DIR` 提供;所有持久化均落在该目录下。
12
+ - “过期删除”与“过期也算触发”必须统一:保留期(retention)默认 **20 分钟**(`retentionMs = 20 * 60 * 1000`)。
13
+ - 所有 “注入(messages/tools)/工具 schema/工具配对” 必须在 llmswitch-core 内完成;Host/Provider 不得做补丁式修复。
13
14
 
14
15
  ## 1. 需求澄清(本设计采纳的语义)
15
16
 
@@ -20,7 +21,7 @@
20
21
 
21
22
  ### 1.2 调度与触发窗口
22
23
 
23
- - 模型通过调用 `clock` 工具创建任务,形成 `{dueAt, task}` 列表,写入 daemon(持久化)。
24
+ - 模型通过调用 `clock` 工具创建任务,形成 `{dueAt, task, tool?, arguments?}` 列表,写入持久化(按 session 作用域)。
24
25
  - 每次请求到来时(对该请求的 session)检查任务是否“到达触发阈值”:
25
26
  - 触发窗口定义为:`now >= dueAt - 60s` 即视为到达(“差一分钟到也算,过期也算”)。
26
27
  - 过期任务并不立即删除;仅当 `now > dueAt + retentionMs` 才删除。
@@ -30,10 +31,16 @@
30
31
  **重要现实约束:没有客户端请求就无法把提醒推送给客户端**(系统当前没有反向推送通道)。
31
32
 
32
33
  - 本设计的默认投递语义:提醒以 `"[scheduled task:\"...\"]"` 的形式**注入到下一次该 session 的请求**中。
33
- - “finish_reason=stop 且时间没到就 hold 等待”会把 HTTP 变成长连接并引发代理/超时/资源占用等风险;但在当前需求中**明确要求 hold 且不限制时间**:
34
- - 当响应 `finish_reason=stop` 且存在未到达触发窗口的最近任务 `nextDueAtMs`,server 可以保持连接,直到 `now >= nextDueAtMs - 60s` 再继续执行注入/续轮。
35
- - 若客户端在 hold 期间断开:server 无法把“同一条响应”推送回客户端;此时任务保持在 daemon 持久化中,**在下一次同 session 的请求到来时**(只要未超过 20 分钟 retention)依然会被判定为 due 并注入提醒。
36
- - 若需要“用户完全离线仍能收到提醒”,必须补齐客户端侧机制(例如:客户端定期发起心跳/拉取请求,或新增事件订阅/长轮询 endpoint)。单纯 server 侧 fake 请求给模型不会自动出现在既有客户端 UI 上。
34
+ - 本设计的 best-effort:在 stop/length 场景下允许“短暂 hold + followup”,以便在触发窗口到达时立刻续轮注入提醒(减少“必须等下一次请求”的延迟)。
35
+ - 默认 **stream/SSE** **非流式(JSON)** 都允许 hold(长轮询/阻塞返回),但必须满足 `holdMaxMs` 上限(默认 60000ms),客户端可随时断开连接取消等待。
36
+ - 如需关闭非流式 hold(改回“仅下一次请求注入”语义),可在 `virtualrouter.clock` 中设置:
37
+ - `holdNonStreaming=false`
38
+ - `holdMaxMs=<ms>`(仍用于限制单次 hold 最长等待)
39
+
40
+ 另外,为避免“同一 HTTP 请求内的二跳/三跳 followup”导致的死循环:
41
+
42
+ - 当 `schedule` 的目标时间已经落入触发窗口(`dueAt <= now + dueWindowMs`)时,会给任务写入 `notBeforeRequestId=<当前请求 requestId>`。
43
+ - 注入提醒时会把 `notBeforeRequestId` 视为“请求链前缀屏蔽”,即 `requestId === notBeforeRequestId` 或 `requestId` 以 `notBeforeRequestId + ":"` 开头(例如 `:clock_followup`)时,都不会在该请求链内投递提醒。
37
44
 
38
45
  ### 1.4 与 `stopMessage` 的优先级与交互(必须)
39
46
 
@@ -53,6 +60,7 @@
53
60
 
54
61
  - 工具名:`clock`
55
62
  - action:
63
+ - `get`:获取当前时间(UTC + local)与 NTP 校时状态(机器可解析;同时作为“clock 激活”信号)
56
64
  - `schedule`:创建/覆盖任务
57
65
  - `list`:列出本 session 未过期任务
58
66
  - `cancel`:取消指定任务
@@ -68,7 +76,7 @@
68
76
  {
69
77
  "type": "object",
70
78
  "properties": {
71
- "action": { "type": "string", "enum": ["schedule", "list", "cancel", "clear"] },
79
+ "action": { "type": "string", "enum": ["get", "schedule", "list", "cancel", "clear"] },
72
80
  "items": {
73
81
  "type": "array",
74
82
  "items": {
@@ -87,24 +95,28 @@
87
95
  "description": "可选:建议要调用的工具名(仅用于提示,不做强制执行)。"
88
96
  },
89
97
  "arguments": {
90
- "type": "object",
91
- "description": "可选:建议工具参数(仅用于提示)。",
92
- "additionalProperties": true
98
+ "type": "string",
99
+ "description": "可选:建议工具参数(仅用于提示)。JSON string(例如 \"{}\"),避免 strict schema 下的任意 object 结构。",
100
+ "default": "{}"
93
101
  }
94
102
  },
95
- "required": ["dueAt", "task"]
103
+ "required": ["dueAt", "task", "tool", "arguments"]
96
104
  }
97
105
  },
98
106
  "taskId": { "type": "string", "description": "cancel 时使用" }
99
107
  },
100
- "required": ["action"]
108
+ "required": ["action", "items", "taskId"]
101
109
  }
102
110
  ```
103
111
 
112
+ > 说明:当前实现采用 `strict: true` 的函数工具 schema(对齐 OpenAI Responses 的严格校验),因此 `required` 需要覆盖 `properties` 的全部字段;
113
+ > 对不适用的 action,可使用空数组/空字符串占位(例如 `get` 使用 `items=[]`、`taskId=""`)。
114
+
104
115
  ### 2.3 工具返回(建议)
105
116
 
106
117
  `clock` 工具返回必须可机器解析,便于模型后续自查:
107
118
 
119
+ - `get`:返回 `{ ok, action:"get", active:true, nowMs, utc, local, timezone, ntp:{...} }`
108
120
  - `schedule`:返回 `{ ok, scheduled: [{ taskId, dueAt, task }] }`
109
121
  - `list`:返回 `{ ok, items: [{ taskId, dueAt, task, deliveredAt? }] }`
110
122
  - `cancel`:返回 `{ ok, removed: taskId }`
@@ -117,7 +129,6 @@
117
129
  ```ts
118
130
  type ClockTask = {
119
131
  taskId: string; // uuid
120
- serverId: string; // 当前 server 实例稳定标识(用于路径隔离)
121
132
  sessionId: string;
122
133
  dueAtMs: number; // 毫秒时间戳
123
134
  createdAtMs: number;
@@ -127,6 +138,7 @@ type ClockTask = {
127
138
  arguments?: Record<string, unknown>;
128
139
  deliveredAtMs?: number; // 成功“注入到某次请求并 commit”后写入
129
140
  deliveryCount: number; // 注入/投递计数(至少 1 次)
141
+ notBeforeRequestId?: string; // 防死循环:窗口内设置的任务,本 requestId 不允许触发注入
130
142
  };
131
143
  ```
132
144
 
@@ -135,7 +147,6 @@ type ClockTask = {
135
147
  ```ts
136
148
  type ClockSessionState = {
137
149
  version: 1;
138
- serverId: string;
139
150
  sessionId: string;
140
151
  tasks: ClockTask[];
141
152
  updatedAtMs: number;
@@ -146,10 +157,9 @@ type ClockSessionState = {
146
157
 
147
158
  ### 4.1 存储位置
148
159
 
149
- - 统一放在 server-scoped session dir 下,避免跨 server 串读:
150
- - 根目录:`~/.routecodex/sessions/<serverId>/`
151
- - Clock 子目录建议:
152
- - `~/.routecodex/sessions/<serverId>/clock/<sessionId>.json`
160
+ - 统一放在 `ROUTECODEX_SESSION_DIR` 下:
161
+ - 闹钟任务:`$ROUTECODEX_SESSION_DIR/clock/<sessionId>.json`
162
+ - NTP 校时状态(server-wide):`$ROUTECODEX_SESSION_DIR/clock/ntp-state.json`
153
163
 
154
164
  ### 4.2 加载/写入策略
155
165
 
@@ -164,10 +174,16 @@ type ClockSessionState = {
164
174
 
165
175
  在 Hub Pipeline 的 canonicalization 完成后、路由/上游调用前执行注入(属于 llmswitch-core 的职责)。
166
176
 
167
- 注入格式(最小实现):
177
+ 注入包括两部分:
178
+
179
+ 1) Time tag(每次请求):
180
+ - 在 messages 末尾追加一条新的 `role:user`,内容为 markdown time tag(inline code):
181
+ - `[Time/Date]: utc=\`...\` local=\`...\` tz=\`...\` nowMs=\`...\` ntpOffsetMs=\`...\``
182
+ - 设计意图:避免引入额外 tool-call 语义,减少模型把“时间注入”当作必须响应/执行的工具回合,从而分散注意力或打断会话结构。
168
183
 
169
- - 在请求末尾追加一段系统文本(或 user 文本,二选一,建议 system):
170
- - `"[scheduled task:\"<task>\" tool=<tool?> args=<json?> dueAt=<iso>]"`(字符串格式固定,方便可观测)
184
+ 2) Alarm due reminders(仅当本次有到期任务):
185
+ - 在请求末尾追加一条 `role:user` 提醒文本,包含到期任务列表,并明确提示:
186
+ - “你可以调用 tools 完成这些任务;如果 tools 不全,系统会补齐标准工具列表。”
171
187
 
172
188
  ### 5.2 触发判定
173
189
 
@@ -226,6 +242,21 @@ daemon 启动时扫描 `~/.routecodex/sessions/<serverId>/clock/`:
226
242
  - 写盘失败:工具错误(fail fast)
227
243
  - 同一 session 大量任务:上限策略可后续再加;本设计先不设硬上限(但要有 O(n) 扫描的性能告警/日志)
228
244
 
245
+ ## 10. NTP 校时(Time 校验与偏移)
246
+
247
+ - `clock` 在启用后会进行 best-effort NTP 校时(SNTP/UDP 123):
248
+ - 维护 `ntpOffsetMs`,用于计算 `correctedNowMs = Date.now() + ntpOffsetMs`。
249
+ - 失败不阻断请求/不影响闹钟功能;仅更新 `ntp.status=error` 与 `lastError`。
250
+ - 校时状态持久化到 `$ROUTECODEX_SESSION_DIR/clock/ntp-state.json`,用于进程重启恢复。
251
+
252
+ ## 11. 防死循环规则(窗口内设置不在当前请求激发)
253
+
254
+ 当 `schedule` 设置的 `dueAt` 已经落入触发窗口(例如 `dueAt <= now + dueWindowMs`)时:
255
+
256
+ - 本次请求不注入/不激发提醒(防止同 requestId / followup 路径产生逻辑死循环)。
257
+ - 任务仍会被持久化保存,但写入 `notBeforeRequestId=<currentRequestId>` 作为 guard。
258
+ - 下一次请求(requestId 不同)到来时,会立即满足注入条件并注入一次,然后正常 commit 为 delivered。
259
+
229
260
  ## 9. 与“fake 请求/hold”相关的实验结论(必须先对齐现实)
230
261
 
231
262
  - 单纯由 server 主动发起一条“fake /v1/responses 请求”,其响应只会返回给**发起该 HTTP 请求的客户端连接**。