coding-tool-x 3.8.4 → 3.8.5

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 (434) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +86 -15
  3. package/dist/web/assets/AgentsDrawer-BG6P3DX-.js +1 -0
  4. package/dist/web/assets/AgentsDrawer-Pw2k-ZAa.css +1 -0
  5. package/dist/web/assets/Analytics--z_FmL5P.js +1 -0
  6. package/dist/web/assets/Analytics-CyHhR126.css +1 -0
  7. package/dist/web/assets/AssetPathField-CrJtRKHy.js +1 -0
  8. package/dist/web/assets/ChatHistoryDrawer-B2qBQs9L.css +1 -0
  9. package/dist/web/assets/ChatHistoryDrawer-BiuEryJj.js +1 -0
  10. package/dist/web/assets/CollapseItem-BAYEsL_Q.js +1 -0
  11. package/dist/web/assets/CommandsDrawer-BZJxlPvV.css +1 -0
  12. package/dist/web/assets/CommandsDrawer-YJ-U7BXN.js +1 -0
  13. package/dist/web/assets/ConfigExportDrawer-Ce_hAknW.js +1 -0
  14. package/dist/web/assets/ConfigExportDrawer-dwruRKWr.css +1 -0
  15. package/dist/web/assets/ConfigTemplates-Bvr1NN6K.js +1 -0
  16. package/dist/web/assets/ConfigTemplatesDrawer-BfDi8yO3.css +1 -0
  17. package/dist/web/assets/ConfigTemplatesDrawer-BjIeHfF-.js +1 -0
  18. package/dist/web/assets/ConfigTemplatesPanel-AV00tm_D.js +1 -0
  19. package/dist/web/assets/ConfigTemplatesPanel-CWyPtk67.css +1 -0
  20. package/dist/web/assets/Divider-D28g1gwj.js +1 -0
  21. package/dist/web/assets/DrawerContent-ChXgZ1Xm.js +1 -0
  22. package/dist/web/assets/FavoritesDrawer-CG9eI9Ay.css +1 -0
  23. package/dist/web/assets/FavoritesDrawer-Cz95o4CD.js +1 -0
  24. package/dist/web/assets/GatewayConvertDrawer-CuNlf7AC.css +1 -0
  25. package/dist/web/assets/GatewayConvertDrawer-DLlbMz27.js +1 -0
  26. package/dist/web/assets/Home-CSuFuJ9Y.css +1 -0
  27. package/dist/web/assets/Home-D7CCOFQC.js +1 -0
  28. package/dist/web/assets/MarkdownEditor-qfVgSre_.css +1 -0
  29. package/dist/web/assets/MarkdownEditor-rEZ-c72L.js +1 -0
  30. package/dist/web/assets/McpDrawer-C0dnsZhQ.css +1 -0
  31. package/dist/web/assets/McpDrawer-DfoCyxU2.js +1 -0
  32. package/dist/web/assets/McpFormDrawer-BRbDcawn.css +1 -0
  33. package/dist/web/assets/McpFormDrawer-SKu8HNUR.js +1 -0
  34. package/dist/web/assets/PluginManager-BbeK-3rV.js +1 -0
  35. package/dist/web/assets/PluginsDrawer-CWVJ7yjS.js +1 -0
  36. package/dist/web/assets/PluginsDrawer-DM3QjprE.css +1 -0
  37. package/dist/web/assets/PluginsPanel-Cm2N3StG.js +1 -0
  38. package/dist/web/assets/PluginsPanel-D6ZqITcR.css +1 -0
  39. package/dist/web/assets/ProjectList-677RLZ8O.css +1 -0
  40. package/dist/web/assets/ProjectList-CsjnsNPv.js +1 -0
  41. package/dist/web/assets/PromptsDrawer-BIgiYCEQ.css +1 -0
  42. package/dist/web/assets/PromptsDrawer-EGd3djFa.js +1 -0
  43. package/dist/web/assets/RecentSessionsDrawer-CKFsOYDM.css +1 -0
  44. package/dist/web/assets/RecentSessionsDrawer-D1vtoLAs.js +1 -0
  45. package/dist/web/assets/SessionCard-BgLg49U3.js +1 -0
  46. package/dist/web/assets/SessionCard-aM2NHu6i.css +1 -0
  47. package/dist/web/assets/SessionList-BpYRz1cX.css +1 -0
  48. package/dist/web/assets/SessionList-TRiTklWk.js +1 -0
  49. package/dist/web/assets/SettingsDrawer-d5liXjHq.css +1 -0
  50. package/dist/web/assets/SettingsDrawer-rz2WejGY.js +1 -0
  51. package/dist/web/assets/SkillManager-BTjTUbHq.js +1 -0
  52. package/dist/web/assets/SkillManager-C0GCT2Kb.css +1 -0
  53. package/dist/web/assets/SkillsDrawer-BCNme-aJ.css +1 -0
  54. package/dist/web/assets/SkillsDrawer-pN0hbkK3.js +1 -0
  55. package/dist/web/assets/SkillsPanel-B3Nm5j32.css +1 -0
  56. package/dist/web/assets/SkillsPanel-gmBWEoiF.js +1 -0
  57. package/dist/web/assets/SpeedTestDrawer-Dc0WToJ_.css +1 -0
  58. package/dist/web/assets/SpeedTestDrawer-DcNzuVJk.js +1 -0
  59. package/dist/web/assets/Upload-v5rV0qTT.js +1 -0
  60. package/dist/web/assets/WorkspaceDrawer-CGnMu9p1.css +1 -0
  61. package/dist/web/assets/WorkspaceDrawer-DxmicM4U.js +1 -0
  62. package/dist/web/assets/WorkspaceManager-CJHCjARN.js +1 -0
  63. package/dist/web/assets/WorkspacePanel-D0-zHTHP.css +1 -0
  64. package/dist/web/assets/WorkspacePanel-D8_Zq-du.js +1 -0
  65. package/dist/web/assets/_plugin-vue_export-helper-BeLZJZij.js +1 -0
  66. package/dist/web/assets/client-UDwXBP9i.js +1 -0
  67. package/dist/web/assets/clipboard-B46yyoz_.js +1 -0
  68. package/dist/web/assets/headers-cf-QapUq.js +1 -0
  69. package/dist/web/assets/icons-B2Vg-IOC.js +1 -0
  70. package/dist/web/assets/index-C0EHRym0.css +1 -0
  71. package/dist/web/assets/index-DnEocV0M.js +2 -0
  72. package/dist/web/assets/light-Cp0t7v32.js +1 -0
  73. package/dist/web/assets/markdown-u082bbtj.js +1 -0
  74. package/dist/web/assets/platforms-CBkB45JP.js +1 -0
  75. package/dist/web/assets/platforms-FTpjxAT6.js +1 -0
  76. package/dist/web/assets/project-config-B2baPzYL.js +1 -0
  77. package/dist/web/assets/sessions-Cmy31slD.js +1 -0
  78. package/dist/web/assets/sessions-yC_oVVh8.js +1 -0
  79. package/dist/web/assets/skill-repo-input-B_fkGlUK.js +1 -0
  80. package/dist/web/assets/use-lock-html-scroll-DXIqmUW4.js +1 -0
  81. package/dist/web/assets/use-message-DoLM8C1C.js +1 -0
  82. package/dist/web/assets/useResponsiveDrawer-B3uojyvV.js +1 -0
  83. package/dist/web/assets/vendors-D0c_0KLP.js +1 -0
  84. package/dist/web/assets/vue-vendor-BqZbbXHA.js +1 -0
  85. package/dist/web/assets/workspaces-mjyPVGS8.js +1 -0
  86. package/dist/web/assets/xml-CgY3RMlX.js +1 -0
  87. package/dist/web/index.html +12 -8
  88. package/docs/native-log-memory-2026-09-16.md +43 -0
  89. package/docs/superpowers/plans/2026-08-26-configuration-driven-cli-platform.md +1578 -0
  90. package/docs/superpowers/plans/2026-08-30-project-scoped-config-control.md +1260 -0
  91. package/docs/superpowers/plans/2026-09-01-omp-skill-refresh-boundary.md +213 -0
  92. package/docs/superpowers/plans/2026-09-01-realtime-logs-panel.md +125 -0
  93. package/docs/superpowers/plans/2026-09-02-config-driven-platform-boundary.md +86 -0
  94. package/docs/superpowers/plans/2026-09-02-platform-implementation-drivers.md +508 -0
  95. package/docs/superpowers/plans/2026-09-03-cli-platform-correctness-boundary.md +760 -0
  96. package/docs/superpowers/plans/2026-09-03-dependency-security-remediation.md +133 -0
  97. package/docs/superpowers/plans/2026-09-04-model-catalog-pricing-sync.md +1078 -0
  98. package/docs/superpowers/plans/2026-09-05-codex-project-scan-500.md +128 -0
  99. package/docs/superpowers/plans/2026-09-06-project-skill-path-fix.md +97 -0
  100. package/docs/superpowers/specs/2026-08-26-configuration-driven-cli-platform-design.md +408 -0
  101. package/docs/superpowers/specs/2026-08-30-project-scoped-config-control-design.md +467 -0
  102. package/docs/superpowers/specs/2026-09-01-claude-session-content-repair-design.md +111 -0
  103. package/docs/superpowers/specs/2026-09-01-omp-skill-refresh-boundary-design.md +83 -0
  104. package/docs/superpowers/specs/2026-09-01-realtime-logs-panel-design.md +27 -0
  105. package/docs/superpowers/specs/2026-09-02-config-driven-platform-boundary-design.md +64 -0
  106. package/docs/superpowers/specs/2026-09-02-platform-implementation-driver-boundary-design.md +123 -0
  107. package/docs/superpowers/specs/2026-09-03-cli-platform-correctness-boundary-design.md +249 -0
  108. package/docs/superpowers/specs/2026-09-03-dependency-security-remediation-design.md +87 -0
  109. package/docs/superpowers/specs/2026-09-04-model-catalog-pricing-sync-design.md +301 -0
  110. package/docs/superpowers/specs/2026-09-05-codex-project-scan-500-design.md +24 -0
  111. package/package.json +8 -4
  112. package/src/commands/channels.js +88 -191
  113. package/src/commands/cli-type.js +19 -11
  114. package/src/commands/daemon.js +94 -32
  115. package/src/commands/doctor.js +16 -9
  116. package/src/commands/export-config.js +23 -5
  117. package/src/commands/list.js +130 -52
  118. package/src/commands/logs.js +44 -27
  119. package/src/commands/platform-command-registry.js +78 -0
  120. package/src/commands/plugin.js +11 -5
  121. package/src/commands/port-config.js +34 -52
  122. package/src/commands/proxy-control.js +207 -145
  123. package/src/commands/proxy.js +2 -2
  124. package/src/commands/resume.js +87 -91
  125. package/src/commands/search.js +137 -67
  126. package/src/commands/stats.js +30 -22
  127. package/src/commands/switch.js +1 -1
  128. package/src/commands/toggle-proxy.js +94 -111
  129. package/src/commands/ui.js +67 -6
  130. package/src/commands/workspace.js +90 -8
  131. package/src/config/default.js +6 -53
  132. package/src/config/loader.js +61 -24
  133. package/src/config/model-metadata.js +49 -59
  134. package/src/config/model-metadata.json +4095 -99
  135. package/src/config/paths.js +181 -8
  136. package/src/index.js +134 -71
  137. package/src/platforms/access.js +196 -0
  138. package/src/{server/services → platforms}/agents-service.js +354 -197
  139. package/src/platforms/channel-auth-registry.js +162 -0
  140. package/src/platforms/channel-auth-service.js +82 -0
  141. package/src/{server/services → platforms}/commands-service.js +195 -115
  142. package/src/platforms/driver-registry.js +59 -0
  143. package/src/platforms/drivers/claude/api-operations.js +62 -0
  144. package/src/{server/services/channels.js → platforms/drivers/claude/channels-implementation.js} +131 -33
  145. package/src/platforms/drivers/claude/channels.js +39 -0
  146. package/src/{server/services/config-sync-service.js → platforms/drivers/claude/config-sync.js} +29 -5
  147. package/src/{server/services → platforms/drivers/claude}/health-check.js +4 -4
  148. package/src/platforms/drivers/claude/health.js +22 -0
  149. package/src/platforms/drivers/claude/mcp.js +9 -0
  150. package/src/{server/services/settings-manager.js → platforms/drivers/claude/native-config-implementation.js} +13 -4
  151. package/src/platforms/drivers/claude/native-config.js +46 -0
  152. package/src/platforms/drivers/claude/native-logs.js +72 -0
  153. package/src/{server/services/claude-openai-gateway.js → platforms/drivers/claude/openai-gateway.js} +8 -65
  154. package/src/platforms/drivers/claude/project-config.js +21 -0
  155. package/src/platforms/drivers/claude/projects.js +62 -0
  156. package/src/platforms/drivers/claude/prompts.js +9 -0
  157. package/src/{server/proxy-server.js → platforms/drivers/claude/proxy-implementation.js} +41 -278
  158. package/src/platforms/drivers/claude/proxy.js +16 -0
  159. package/src/platforms/drivers/claude/resource-sync.js +9 -0
  160. package/src/platforms/drivers/claude/session-history-adapter.js +313 -0
  161. package/src/{server/services/sessions.js → platforms/drivers/claude/sessions-implementation.js} +145 -35
  162. package/src/platforms/drivers/claude/sessions.js +76 -0
  163. package/src/{server/services/claude-statistics-service.js → platforms/drivers/claude/statistics-implementation.js} +2 -2
  164. package/src/platforms/drivers/claude/statistics.js +16 -0
  165. package/src/{server/services/claude-wire.js → platforms/drivers/claude/wire.js} +1 -1
  166. package/src/platforms/drivers/codex/api-operations.js +9 -0
  167. package/src/{server/services/codex-channels.js → platforms/drivers/codex/channels-implementation.js} +149 -14
  168. package/src/platforms/drivers/codex/channels.js +45 -0
  169. package/src/{server/services/codex-config.js → platforms/drivers/codex/config.js} +12 -2
  170. package/src/{server/services/codex-env-manager.js → platforms/drivers/codex/env-manager.js} +1 -1
  171. package/src/platforms/drivers/codex/mcp.js +9 -0
  172. package/src/{server/services/codex-settings-manager.js → platforms/drivers/codex/native-config-implementation.js} +15 -6
  173. package/src/platforms/drivers/codex/native-config.js +47 -0
  174. package/src/platforms/drivers/codex/native-logs.js +166 -0
  175. package/src/platforms/drivers/codex/project-config.js +21 -0
  176. package/src/platforms/drivers/codex/projects.js +26 -0
  177. package/src/platforms/drivers/codex/prompts.js +9 -0
  178. package/src/{server/codex-proxy-server.js → platforms/drivers/codex/proxy-implementation.js} +30 -233
  179. package/src/platforms/drivers/codex/proxy.js +16 -0
  180. package/src/platforms/drivers/codex/resource-sync.js +9 -0
  181. package/src/{server/services/session-history-adapters/codex.js → platforms/drivers/codex/session-history-adapter.js} +37 -23
  182. package/src/{server/services/codex-sessions.js → platforms/drivers/codex/sessions-implementation.js} +81 -75
  183. package/src/platforms/drivers/codex/sessions.js +36 -0
  184. package/src/{server/services/codex-statistics-service.js → platforms/drivers/codex/statistics-implementation.js} +2 -2
  185. package/src/platforms/drivers/codex/statistics.js +16 -0
  186. package/src/platforms/drivers/gemini/api-operations.js +9 -0
  187. package/src/{server/services/gemini-channels.js → platforms/drivers/gemini/channels-implementation.js} +202 -37
  188. package/src/platforms/drivers/gemini/channels.js +39 -0
  189. package/src/{server/services/gemini-config.js → platforms/drivers/gemini/config.js} +12 -2
  190. package/src/platforms/drivers/gemini/mcp.js +9 -0
  191. package/src/{server/services/gemini-settings-manager.js → platforms/drivers/gemini/native-config-implementation.js} +14 -5
  192. package/src/platforms/drivers/gemini/native-config.js +39 -0
  193. package/src/platforms/drivers/gemini/native-logs.js +180 -0
  194. package/src/platforms/drivers/gemini/project-config.js +21 -0
  195. package/src/platforms/drivers/gemini/projects.js +26 -0
  196. package/src/platforms/drivers/gemini/prompts.js +9 -0
  197. package/src/{server/gemini-proxy-server.js → platforms/drivers/gemini/proxy-implementation.js} +36 -267
  198. package/src/platforms/drivers/gemini/proxy.js +16 -0
  199. package/src/platforms/drivers/gemini/resource-sync.js +9 -0
  200. package/src/{server/services/session-history-adapters/gemini.js → platforms/drivers/gemini/session-history-adapter.js} +70 -18
  201. package/src/{server/services/gemini-sessions.js → platforms/drivers/gemini/sessions-implementation.js} +53 -37
  202. package/src/platforms/drivers/gemini/sessions.js +47 -0
  203. package/src/{server/services/gemini-statistics-service.js → platforms/drivers/gemini/statistics-implementation.js} +2 -2
  204. package/src/platforms/drivers/gemini/statistics.js +16 -0
  205. package/src/{server/services/gemini-wire.js → platforms/drivers/gemini/wire.js} +1 -1
  206. package/src/platforms/drivers/generic-filesystem.js +166 -0
  207. package/src/platforms/drivers/generic-jsonl.js +141 -0
  208. package/src/platforms/drivers/generic-mcp.js +44 -0
  209. package/src/platforms/drivers/generic-openai-compatible.js +45 -0
  210. package/src/platforms/drivers/generic-prompt.js +41 -0
  211. package/src/platforms/drivers/legacy.js +677 -0
  212. package/src/platforms/drivers/native-config-snapshot.js +125 -0
  213. package/src/platforms/drivers/native-log-utils.js +1292 -0
  214. package/src/platforms/drivers/notification-hooks.js +578 -0
  215. package/src/{server/api/omp-config.js → platforms/drivers/omp/api-operations.js} +52 -64
  216. package/src/{server/services/omp-auth-provider-worker.js → platforms/drivers/omp/auth-provider-worker.js} +1 -1
  217. package/src/{server/services/omp-auth-providers.js → platforms/drivers/omp/auth-providers.js} +1 -1
  218. package/src/{server/services/omp-channels.js → platforms/drivers/omp/channels-implementation.js} +166 -39
  219. package/src/platforms/drivers/omp/channels.js +58 -0
  220. package/src/{server/services/omp-config.js → platforms/drivers/omp/config.js} +53 -13
  221. package/src/{server/services/omp-gateway-routing.js → platforms/drivers/omp/gateway-routing.js} +14 -7
  222. package/src/{server/services/omp-gateway.js → platforms/drivers/omp/gateway.js} +4 -262
  223. package/src/platforms/drivers/omp/mcp.js +9 -0
  224. package/src/{server/services/omp-settings-manager.js → platforms/drivers/omp/native-config-implementation.js} +75 -10
  225. package/src/platforms/drivers/omp/native-config.js +46 -0
  226. package/src/platforms/drivers/omp/native-logs.js +52 -0
  227. package/src/{server/services/omp-native-plugin-adapter.js → platforms/drivers/omp/native-plugin-adapter.js} +91 -4
  228. package/src/platforms/drivers/omp/project-config.js +29 -0
  229. package/src/platforms/drivers/omp/projects.js +20 -0
  230. package/src/platforms/drivers/omp/prompts.js +9 -0
  231. package/src/{server/omp-proxy-server.js → platforms/drivers/omp/proxy-implementation.js} +6 -15
  232. package/src/platforms/drivers/omp/proxy.js +16 -0
  233. package/src/platforms/drivers/omp/resource-sync.js +9 -0
  234. package/src/{server/services/session-history-adapters/omp.js → platforms/drivers/omp/session-history-adapter.js} +42 -18
  235. package/src/{server/services/omp-sessions.js → platforms/drivers/omp/sessions-implementation.js} +130 -94
  236. package/src/platforms/drivers/omp/sessions.js +36 -0
  237. package/src/{server/services/omp-skill-discovery.js → platforms/drivers/omp/skill-discovery.js} +159 -32
  238. package/src/{server/services/omp-skill-settings-service.js → platforms/drivers/omp/skill-settings.js} +1 -1
  239. package/src/{server/services/omp-statistics-service.js → platforms/drivers/omp/statistics-implementation.js} +3 -3
  240. package/src/platforms/drivers/omp/statistics.js +16 -0
  241. package/src/platforms/drivers/opencode/api-operations.js +9 -0
  242. package/src/{server/services/opencode-channels.js → platforms/drivers/opencode/channels-implementation.js} +28 -9
  243. package/src/platforms/drivers/opencode/channels.js +40 -0
  244. package/src/platforms/drivers/opencode/conversion.js +41 -0
  245. package/src/{server/services/opencode-gateway-converter.js → platforms/drivers/opencode/gateway-converter.js} +1 -1
  246. package/src/platforms/drivers/opencode/mcp.js +9 -0
  247. package/src/{server/services/opencode-settings-manager.js → platforms/drivers/opencode/native-config-implementation.js} +17 -6
  248. package/src/platforms/drivers/opencode/native-config.js +26 -0
  249. package/src/platforms/drivers/opencode/native-logs.js +133 -0
  250. package/src/platforms/drivers/opencode/project-config.js +98 -0
  251. package/src/platforms/drivers/opencode/projects.js +20 -0
  252. package/src/platforms/drivers/opencode/prompts.js +9 -0
  253. package/src/{server/opencode-proxy-server.js → platforms/drivers/opencode/proxy-implementation.js} +27 -362
  254. package/src/platforms/drivers/opencode/proxy.js +16 -0
  255. package/src/platforms/drivers/opencode/resource-sync.js +9 -0
  256. package/src/{server/services/opencode-sessions.js → platforms/drivers/opencode/sessions-implementation.js} +211 -64
  257. package/src/platforms/drivers/opencode/sessions.js +48 -0
  258. package/src/{server/services/opencode-statistics-service.js → platforms/drivers/opencode/statistics-implementation.js} +2 -2
  259. package/src/platforms/drivers/opencode/statistics.js +16 -0
  260. package/src/platforms/drivers/secure-file-driver.js +272 -0
  261. package/src/platforms/drivers/unsupported.js +7 -0
  262. package/src/platforms/manifest-schema.js +398 -0
  263. package/src/platforms/manifests/claude.json +489 -0
  264. package/src/platforms/manifests/codex.json +448 -0
  265. package/src/platforms/manifests/gemini.json +447 -0
  266. package/src/platforms/manifests/omp.json +496 -0
  267. package/src/platforms/manifests/opencode.json +422 -0
  268. package/src/{server/services → platforms}/native-oauth-adapters.js +87 -417
  269. package/src/{server/services → platforms}/notification-hooks.js +140 -1023
  270. package/src/platforms/oauth-credentials-service.js +506 -0
  271. package/src/platforms/path-resolver.js +179 -0
  272. package/src/platforms/platform-path-context.js +305 -0
  273. package/src/platforms/registry.js +248 -0
  274. package/src/platforms/remote-notification-providers.js +678 -0
  275. package/src/platforms/runtime.js +259 -0
  276. package/src/{server/services/session-history-adapters/index.js → platforms/session-history-adapters.js} +8 -1
  277. package/src/plugins/plugin-loader.js +2 -2
  278. package/src/plugins/plugin-manager.js +2 -2
  279. package/src/plugins/registry.js +27 -10
  280. package/src/reset-config.js +21 -5
  281. package/src/server/api/agents.js +46 -72
  282. package/src/server/api/commands.js +16 -6
  283. package/src/server/api/config-registry.js +206 -59
  284. package/src/server/api/config-sync.js +3 -2
  285. package/src/server/api/config-templates.js +20 -4
  286. package/src/server/api/config.js +111 -102
  287. package/src/server/api/convert.js +54 -178
  288. package/src/server/api/dashboard.js +168 -102
  289. package/src/server/api/hooks.js +1 -1
  290. package/src/server/api/mcp.js +224 -84
  291. package/src/server/api/platform-api-config.js +102 -0
  292. package/src/server/api/platform-route-factory.js +215 -0
  293. package/src/server/api/platform-routes.js +26 -0
  294. package/src/server/api/platforms.js +110 -0
  295. package/src/server/api/plugins.js +57 -57
  296. package/src/server/api/project-config.js +168 -0
  297. package/src/server/api/prompts.js +58 -45
  298. package/src/server/api/settings.js +55 -8
  299. package/src/server/api/skills.js +262 -133
  300. package/src/server/api/ui-config.js +2 -2
  301. package/src/server/api/validation-errors.js +18 -4
  302. package/src/server/api/workspaces.js +47 -12
  303. package/src/server/index.js +164 -213
  304. package/src/server/platform-context.js +81 -0
  305. package/src/server/services/channel-balance.js +11 -20
  306. package/src/server/services/channel-health.js +21 -6
  307. package/src/server/services/channel-scheduler.js +29 -39
  308. package/src/server/services/channel-sync-utils.js +23 -13
  309. package/src/server/services/config-export-service.js +350 -319
  310. package/src/server/services/config-registry-service.js +280 -54
  311. package/src/server/services/config-sync-manager.js +172 -944
  312. package/src/server/services/config-templates-service.js +419 -353
  313. package/src/server/services/control-manifest-store.js +206 -0
  314. package/src/server/services/dashboard-snapshot-worker.js +656 -100
  315. package/src/server/services/effective-control-service.js +557 -0
  316. package/src/server/services/local-resource-index.js +314 -0
  317. package/src/server/services/mcp-service.js +607 -977
  318. package/src/server/services/model-detector.js +15 -10
  319. package/src/server/services/native-log-channel-resolver.js +214 -0
  320. package/src/server/services/native-log-observer.js +291 -0
  321. package/src/server/services/network-access.js +1 -1
  322. package/src/server/services/platform-catalog.js +82 -0
  323. package/src/server/services/platform-resolution.js +32 -29
  324. package/src/server/services/plugins-service.js +376 -122
  325. package/src/server/services/project-config-service.js +550 -0
  326. package/src/server/services/project-path-validation.js +31 -0
  327. package/src/server/services/prompts-service.js +267 -233
  328. package/src/server/services/proxy-runtime.js +7 -4
  329. package/src/server/services/remote-credential-cache.js +43 -0
  330. package/src/server/services/repo-scanner-base.js +182 -75
  331. package/src/server/services/request-logger.js +7 -4
  332. package/src/server/services/session-history-index.js +780 -213
  333. package/src/server/services/session-history-worker.js +228 -33
  334. package/src/server/services/session-snapshot-worker.js +124 -34
  335. package/src/server/services/session-snapshots.js +4 -2
  336. package/src/server/services/skill-artifact-store.js +355 -0
  337. package/src/server/services/skill-format-adapters.js +65 -0
  338. package/src/server/services/skill-projection-service.js +323 -0
  339. package/src/server/services/skill-refresh-task-service.js +275 -0
  340. package/src/server/services/skill-service.js +2020 -677
  341. package/src/server/services/snapshot-cache.js +30 -8
  342. package/src/server/services/speed-test.js +8 -8
  343. package/src/server/services/sqlite-connection.js +19 -3
  344. package/src/server/services/statistics-service.js +240 -57
  345. package/src/server/services/ui-config.js +107 -76
  346. package/src/server/services/usage-log-utils.js +98 -0
  347. package/src/server/services/web-build.js +1 -1
  348. package/src/server/services/workspace-service.js +59 -29
  349. package/src/server/utils/pricing.js +10 -19
  350. package/src/server/websocket-server.js +153 -28
  351. package/src/{server/services/base → shared}/base-channel-service.js +156 -28
  352. package/src/shared/capability-driver.js +104 -0
  353. package/src/shared/channel-auth-policy.js +27 -0
  354. package/src/shared/channel-driver.js +161 -0
  355. package/src/shared/driver-factories/api.js +180 -0
  356. package/src/shared/driver-factories/mcp.js +244 -0
  357. package/src/shared/driver-factories/projects.js +121 -0
  358. package/src/shared/driver-factories/prompts.js +58 -0
  359. package/src/shared/driver-factories/resource-sync.js +375 -0
  360. package/src/shared/driver-result.js +78 -0
  361. package/src/shared/mcp-format.js +189 -0
  362. package/src/shared/platforms.js +104 -167
  363. package/src/shared/project-config.js +512 -0
  364. package/src/shared/proxy-driver.js +57 -0
  365. package/src/{server/services/base → shared}/response-usage-parser.js +8 -4
  366. package/src/ui/menu.js +91 -67
  367. package/src/utils/port-helper.js +35 -19
  368. package/src/utils/session.js +1 -1
  369. package/dist/web/assets/Analytics-COEUXmd4.js +0 -25
  370. package/dist/web/assets/Analytics-RNn1BUbG.css +0 -1
  371. package/dist/web/assets/ConfigTemplates-DLLkS53H.js +0 -1
  372. package/dist/web/assets/Home-DCEyLzwX.js +0 -1
  373. package/dist/web/assets/Home-DYVy4cGZ.css +0 -1
  374. package/dist/web/assets/PluginManager-Oe1ZHoMO.js +0 -1
  375. package/dist/web/assets/ProjectList-CWjuAFof.js +0 -1
  376. package/dist/web/assets/ProjectList-VeMNXfgO.css +0 -1
  377. package/dist/web/assets/SessionList-C4chCPxK.css +0 -1
  378. package/dist/web/assets/SessionList-v3ehGg9U.js +0 -1
  379. package/dist/web/assets/SkillManager-BsX8Qpj0.css +0 -1
  380. package/dist/web/assets/SkillManager-DUF_qP5S.js +0 -1
  381. package/dist/web/assets/WorkspaceManager-CjAWBosO.js +0 -1
  382. package/dist/web/assets/icons-nGdSKQn2.js +0 -1
  383. package/dist/web/assets/index-D5hyHPj7.js +0 -2
  384. package/dist/web/assets/index-bJRG3RXH.css +0 -1
  385. package/dist/web/assets/markdown-BNJfEgCA.js +0 -1
  386. package/dist/web/assets/markdown-BfC0goYb.css +0 -10
  387. package/dist/web/assets/naive-ui-BALVpMgd.js +0 -1
  388. package/dist/web/assets/vendors-DQA4Hw9v.js +0 -7
  389. package/dist/web/assets/vue-vendor-B5RqjEjQ.js +0 -45
  390. package/src/config/model-pricing.js +0 -35
  391. package/src/server/api/channels.js +0 -439
  392. package/src/server/api/claude-hooks.js +0 -56
  393. package/src/server/api/claude-statistics.js +0 -57
  394. package/src/server/api/codex-channels.js +0 -491
  395. package/src/server/api/codex-projects.js +0 -117
  396. package/src/server/api/codex-proxy.js +0 -293
  397. package/src/server/api/codex-sessions.js +0 -652
  398. package/src/server/api/codex-statistics.js +0 -57
  399. package/src/server/api/gemini-channels.js +0 -392
  400. package/src/server/api/gemini-projects.js +0 -103
  401. package/src/server/api/gemini-proxy.js +0 -231
  402. package/src/server/api/gemini-sessions.js +0 -550
  403. package/src/server/api/gemini-statistics.js +0 -57
  404. package/src/server/api/health-check.js +0 -31
  405. package/src/server/api/oauth-credentials.js +0 -198
  406. package/src/server/api/omp-channels.js +0 -575
  407. package/src/server/api/omp-projects.js +0 -88
  408. package/src/server/api/omp-proxy.js +0 -125
  409. package/src/server/api/omp-sessions.js +0 -386
  410. package/src/server/api/omp-statistics.js +0 -40
  411. package/src/server/api/opencode-channels.js +0 -471
  412. package/src/server/api/opencode-projects.js +0 -110
  413. package/src/server/api/opencode-proxy.js +0 -354
  414. package/src/server/api/opencode-sessions.js +0 -466
  415. package/src/server/api/opencode-statistics.js +0 -57
  416. package/src/server/api/projects.js +0 -120
  417. package/src/server/api/proxy.js +0 -330
  418. package/src/server/api/sessions.js +0 -810
  419. package/src/server/services/oauth-credentials-service.js +0 -945
  420. package/src/server/services/omp-session-log-observer.js +0 -138
  421. package/src/server/services/opencode-gateway-adapters.js +0 -43
  422. package/src/server/services/project-snapshots.js +0 -59
  423. package/src/server/services/proxy-log-helper.js +0 -298
  424. package/src/server/services/session-cache.js +0 -127
  425. package/src/server/services/session-converter.js +0 -582
  426. package/src/server/services/session-history-adapters/claude.js +0 -235
  427. /package/src/{server/services/claude-token-recovery.js → platforms/drivers/claude/token-recovery.js} +0 -0
  428. /package/src/{server/services/codex-parser.js → platforms/drivers/codex/parser.js} +0 -0
  429. /package/src/{server/services/codex-speed-test-template.json → platforms/drivers/codex/speed-test-template.json} +0 -0
  430. /package/src/{server/services/codex-wire.js → platforms/drivers/codex/wire.js} +0 -0
  431. /package/src/{server/services/omp-auth-gateway-client.js → platforms/drivers/omp/auth-gateway-client.js} +0 -0
  432. /package/src/{server/services/opencode-normalization.js → platforms/drivers/opencode/normalization.js} +0 -0
  433. /package/src/{server/services → shared}/config-artifact-paths.js +0 -0
  434. /package/src/{server/services/base → shared}/proxy-utils.js +0 -0
@@ -4,6 +4,7 @@ const fs = require('fs');
4
4
  const path = require('path');
5
5
  const { loadConfig } = require('../config/loader');
6
6
  const { PATHS } = require('../config/paths');
7
+ const eventBus = require('../plugins/event-bus');
7
8
  const {
8
9
  normalizeAddress,
9
10
  isLoopbackAddress,
@@ -11,19 +12,28 @@ const {
11
12
  } = require('./services/network-access');
12
13
 
13
14
  const MAX_PERSISTED_LOGS = 500;
15
+ const LOG_PERSIST_DEBOUNCE_MS = 250;
16
+
17
+ let maxLogsLimit = 100;
14
18
 
15
19
  function getMaxLogsLimit() {
20
+ return maxLogsLimit;
21
+ }
22
+
23
+ function refreshMaxLogsLimit(config = null) {
16
24
  try {
17
- const config = loadConfig();
18
- const limit = parseInt(config.maxLogs, 10);
25
+ const limit = parseInt((config || loadConfig()).maxLogs, 10);
19
26
  if (!Number.isFinite(limit)) {
20
- return 100;
27
+ maxLogsLimit = 100;
28
+ } else {
29
+ maxLogsLimit = Math.min(Math.max(limit, 50), MAX_PERSISTED_LOGS);
21
30
  }
22
- return Math.min(Math.max(limit, 50), MAX_PERSISTED_LOGS);
23
31
  } catch (err) {
24
32
  console.error('Failed to load log limit from config:', err);
25
- return 100;
33
+ maxLogsLimit = 100;
26
34
  }
35
+ trimLogCache();
36
+ return maxLogsLimit;
27
37
  }
28
38
 
29
39
  let wss = null;
@@ -227,8 +237,7 @@ function filterTodayLogs(logs) {
227
237
  });
228
238
  }
229
239
 
230
- function enforcePerSourceLimit(logs) {
231
- const limit = getMaxLogsLimit();
240
+ function enforcePerSourceLimit(logs, limit = getMaxLogsLimit()) {
232
241
  if (!limit || limit <= 0) {
233
242
  return logs;
234
243
  }
@@ -263,21 +272,130 @@ function loadPersistedLogs() {
263
272
  return [];
264
273
  }
265
274
 
266
- // 保存日志到文件
267
- function saveLogsToFile(logs) {
275
+ function serializeLogs(logs) {
268
276
  try {
269
- const logsFile = getLogsFilePath();
270
- // 只保留最新的 MAX_PERSISTED_LOGS 条,且仅保存今日日志
271
277
  const todayLogs = enforcePerSourceLimit(filterTodayLogs(logs));
272
278
  const logsToSave = todayLogs.slice(-MAX_PERSISTED_LOGS);
273
- fs.writeFileSync(logsFile, JSON.stringify(logsToSave, null, 2), 'utf8');
279
+ return JSON.stringify(logsToSave, null, 2);
274
280
  } catch (err) {
275
- console.error('Failed to save logs to file:', err);
281
+ console.error('Failed to serialize logs:', err);
282
+ return '[]';
276
283
  }
277
284
  }
278
285
 
279
286
  // 内存中的日志缓存
280
287
  let logsCache = [];
288
+ let logSourceCounts = new Map();
289
+ let cacheDayStartMs = null;
290
+ let persistTimer = null;
291
+ let persistGeneration = 0;
292
+ let persistedGeneration = 0;
293
+ let enqueuedGeneration = 0;
294
+ let persistChain = Promise.resolve();
295
+ let configSavedListener = null;
296
+
297
+ function rebuildLogSourceCounts() {
298
+ logSourceCounts = new Map();
299
+ logsCache.forEach(log => {
300
+ const source = log.source || 'claude';
301
+ logSourceCounts.set(source, (logSourceCounts.get(source) || 0) + 1);
302
+ });
303
+ }
304
+
305
+ function removeLogAt(index) {
306
+ if (index < 0 || index >= logsCache.length) return;
307
+ const [removed] = logsCache.splice(index, 1);
308
+ const source = removed?.source || 'claude';
309
+ const count = (logSourceCounts.get(source) || 1) - 1;
310
+ if (count > 0) logSourceCounts.set(source, count);
311
+ else logSourceCounts.delete(source);
312
+ }
313
+
314
+ function trimLogCache() {
315
+ const { startMs, endMs } = getTodayRange();
316
+ if (cacheDayStartMs !== startMs) {
317
+ logsCache = logsCache.filter(log => {
318
+ let timestamp = typeof log.timestamp === 'number' ? log.timestamp : Date.parse(log.timestamp);
319
+ if (!Number.isFinite(timestamp)) timestamp = Date.now();
320
+ log.timestamp = timestamp;
321
+ log.source = inferSource(log);
322
+ return timestamp >= startMs && timestamp < endMs;
323
+ });
324
+ cacheDayStartMs = startMs;
325
+ rebuildLogSourceCounts();
326
+ }
327
+
328
+ const limit = getMaxLogsLimit();
329
+ if (limit > 0) {
330
+ for (const [source, count] of [...logSourceCounts]) {
331
+ let remaining = count - limit;
332
+ if (remaining <= 0) continue;
333
+ for (let index = 0; index < logsCache.length && remaining > 0; index += 1) {
334
+ if (logsCache[index].source === source) {
335
+ removeLogAt(index);
336
+ index -= 1;
337
+ remaining -= 1;
338
+ }
339
+ }
340
+ }
341
+ }
342
+ while (logsCache.length > MAX_PERSISTED_LOGS) removeLogAt(0);
343
+ }
344
+
345
+ function appendLogToCache(payload) {
346
+ trimLogCache();
347
+ logsCache.push(payload);
348
+ const source = payload.source || 'claude';
349
+ logSourceCounts.set(source, (logSourceCounts.get(source) || 0) + 1);
350
+ trimLogCache();
351
+ }
352
+
353
+ function enqueueLogPersistence(data, generation) {
354
+ enqueuedGeneration = Math.max(enqueuedGeneration, generation);
355
+ persistChain = persistChain
356
+ .catch(() => {})
357
+ .then(async () => {
358
+ await fs.promises.writeFile(getLogsFilePath(), data, 'utf8');
359
+ persistedGeneration = Math.max(persistedGeneration, generation);
360
+ })
361
+ .catch((error) => {
362
+ console.error('Failed to save logs to file:', error);
363
+ });
364
+ return persistChain;
365
+ }
366
+
367
+ function scheduleLogPersistence({ markDirty = true } = {}) {
368
+ if (markDirty) persistGeneration += 1;
369
+ if (persistTimer) return;
370
+ persistTimer = setTimeout(() => {
371
+ persistTimer = null;
372
+ flushPendingLogs();
373
+ }, LOG_PERSIST_DEBOUNCE_MS);
374
+ if (typeof persistTimer.unref === 'function') persistTimer.unref();
375
+ }
376
+
377
+ function flushPendingLogs() {
378
+ if (persistTimer) {
379
+ clearTimeout(persistTimer);
380
+ persistTimer = null;
381
+ }
382
+ if (persistGeneration <= persistedGeneration || persistGeneration <= enqueuedGeneration) {
383
+ return persistChain;
384
+ }
385
+ const generation = persistGeneration;
386
+ const data = serializeLogs(logsCache.slice());
387
+ return enqueueLogPersistence(data, generation).then(() => {
388
+ if (persistGeneration > generation && persistGeneration > enqueuedGeneration) {
389
+ scheduleLogPersistence({ markDirty: false });
390
+ }
391
+ });
392
+ }
393
+
394
+ function ensureConfigListener() {
395
+ if (configSavedListener) return;
396
+ configSavedListener = ({ config } = {}) => refreshMaxLogsLimit(config);
397
+ eventBus.on('config:saved', configSavedListener);
398
+ }
281
399
 
282
400
  // 启动 WebSocket 服务器(附加到现有的 HTTP 服务器)
283
401
  function startWebSocketServer(httpServer, options = {}) {
@@ -290,8 +408,12 @@ function startWebSocketServer(httpServer, options = {}) {
290
408
  host: options.host || '127.0.0.1'
291
409
  };
292
410
 
411
+ ensureConfigListener();
412
+ refreshMaxLogsLimit();
293
413
  // 加载持久化的日志到缓存
294
414
  logsCache = loadPersistedLogs();
415
+ cacheDayStartMs = getTodayRange().startMs;
416
+ rebuildLogSourceCounts();
295
417
  const counts = logsCache.reduce((acc, log) => {
296
418
  const source = log.source || 'unknown';
297
419
  acc[source] = (acc[source] || 0) + 1;
@@ -389,7 +511,7 @@ function startWebSocketServer(httpServer, options = {}) {
389
511
  // 停止 WebSocket 服务器
390
512
  function stopWebSocketServer() {
391
513
  if (!wss) {
392
- return;
514
+ return flushPendingLogs();
393
515
  }
394
516
 
395
517
  // 清除心跳定时器
@@ -410,6 +532,7 @@ function stopWebSocketServer() {
410
532
  });
411
533
 
412
534
  wss = null;
535
+ return flushPendingLogs();
413
536
  }
414
537
 
415
538
  // 广播日志消息
@@ -422,16 +545,9 @@ function broadcastLog(logData) {
422
545
 
423
546
  payload.source = payload.source || inferSource(payload);
424
547
 
425
- // 添加到缓存
426
- logsCache.push(payload);
427
- logsCache = enforcePerSourceLimit(filterTodayLogs(logsCache));
428
-
429
- if (logsCache.length > MAX_PERSISTED_LOGS) {
430
- logsCache = logsCache.slice(-MAX_PERSISTED_LOGS);
431
- }
432
-
433
- // 保存到文件
434
- saveLogsToFile(logsCache);
548
+ // 添加到有界内存缓存,持久化由防抖队列合并处理。
549
+ appendLogToCache(payload);
550
+ scheduleLogPersistence();
435
551
 
436
552
  // 广播给所有连接的客户端
437
553
  if (wss && wsClients.size > 0) {
@@ -448,17 +564,25 @@ function broadcastLog(logData) {
448
564
  // 清空所有日志
449
565
  function clearAllLogs() {
450
566
  logsCache = [];
451
- saveLogsToFile([]);
567
+ logSourceCounts = new Map();
568
+ cacheDayStartMs = getTodayRange().startMs;
569
+ persistGeneration += 1;
570
+ if (persistTimer) {
571
+ clearTimeout(persistTimer);
572
+ persistTimer = null;
573
+ }
574
+ const generation = persistGeneration;
575
+ enqueueLogPersistence('[]', generation);
452
576
  console.log('[OK] All logs cleared');
577
+ return persistChain;
453
578
  }
454
579
 
455
- // 去掉敏感字段
580
+ // 复制渠道状态,保留 API key 供前端展示
456
581
  function sanitizeChannel(channel) {
457
582
  if (!channel || typeof channel !== 'object') {
458
583
  return null;
459
584
  }
460
- const { apiKey, ...rest } = channel;
461
- return rest;
585
+ return { ...channel };
462
586
  }
463
587
 
464
588
  function sanitizeChannels(channels) {
@@ -531,6 +655,7 @@ module.exports = {
531
655
  stopWebSocketServer,
532
656
  broadcastLog,
533
657
  clearAllLogs,
658
+ flushPendingLogs,
534
659
  broadcastProxyState,
535
660
  broadcastSchedulerState,
536
661
  broadcastBrowserNotification
@@ -1,8 +1,7 @@
1
1
  /**
2
2
  * BaseChannelService - 四平台渠道管理的公共基类
3
3
  *
4
- * 提取 channels.js / codex-channels.js / gemini-channels.js / opencode-channels.js
5
- * 中的共享 CRUD、启用/禁用、单渠道强制等逻辑。
4
+ * 为各平台 Driver 实现提供共享 CRUD、启用/禁用、单渠道强制等基础能力。
6
5
  *
7
6
  * 子类通过覆写钩子方法实现平台差异化行为。
8
7
  */
@@ -11,16 +10,25 @@ const fs = require('fs');
11
10
  const path = require('path');
12
11
  const crypto = require('crypto');
13
12
  const { normalizeGatewaySourceType, normalizeNumber } = require('./proxy-utils');
14
- const { resolveChannelWebsiteUrl } = require('../../../config/channel-preset-websites');
15
-
13
+ const { resolveChannelWebsiteUrl } = require('../config/channel-preset-websites');
14
+ const { assertAuthMode, validateEnabledTransition } = require('./channel-auth-policy');
16
15
  function clearChannelBalanceCache(platform, channel) {
17
16
  try {
18
- require('../channel-balance').clearChannelBalanceCache(platform, channel);
17
+ require('../server/services/channel-balance').clearChannelBalanceCache(platform, channel);
19
18
  } catch (_) {
20
19
  // Balance cache invalidation is an optimization; channel updates must still succeed.
21
20
  }
22
21
  }
23
22
 
23
+ function invalidateDashboardSource(platform) {
24
+ try {
25
+ const { invalidateDashboardSourceSnapshot } = require('../server/services/snapshot-cache');
26
+ invalidateDashboardSourceSnapshot(platform);
27
+ } catch (_) {
28
+ // Dashboard snapshots are an optimization; channel mutations still succeed.
29
+ }
30
+ }
31
+
24
32
  class BaseChannelService {
25
33
  /**
26
34
  * @param {object} config
@@ -33,9 +41,16 @@ class BaseChannelService {
33
41
  this.platform = config.platform;
34
42
  this.channelsFilePath = config.channelsFilePath;
35
43
  this.defaultGatewaySource = config.defaultGatewaySource || config.platform;
44
+ this.oauthChannelPolicy = config.oauthChannelPolicy || 'mixed';
36
45
  this._isProxyRunning = config.isProxyRunning || (() => false);
46
+ this._channelCache = { value: null, mtimeMs: 0, invalidated: true };
37
47
  }
38
48
 
49
+ _generateId() {
50
+ return crypto.randomUUID();
51
+ }
52
+
53
+
39
54
  // ── 文件 I/O ──
40
55
 
41
56
  _ensureDir() {
@@ -45,23 +60,59 @@ class BaseChannelService {
45
60
  }
46
61
  }
47
62
 
63
+ _channelFileMtime() {
64
+ try {
65
+ return fs.statSync(this.channelsFilePath).mtimeMs;
66
+ } catch (_) {
67
+ return 0;
68
+ }
69
+ }
70
+
71
+ _cloneChannelData(data) {
72
+ return {
73
+ channels: Array.isArray(data?.channels)
74
+ ? data.channels.map(channel => ({ ...channel }))
75
+ : []
76
+ };
77
+ }
78
+
48
79
  loadChannels() {
49
80
  this._ensureDir();
81
+ const mtimeMs = this._channelFileMtime();
82
+ if (!this._channelCache.invalidated
83
+ && this._channelCache.mtimeMs === mtimeMs
84
+ && this._channelCache.value) {
85
+ return this._cloneChannelData(this._channelCache.value);
86
+ }
87
+
88
+ let value = { channels: [] };
50
89
  try {
51
90
  if (fs.existsSync(this.channelsFilePath)) {
52
91
  const raw = JSON.parse(fs.readFileSync(this.channelsFilePath, 'utf8'));
53
92
  const channels = Array.isArray(raw?.channels) ? raw.channels : [];
54
- return { channels: channels.map(ch => this._applyDefaults(ch)) };
93
+ value = { channels: channels.map(channel => this._applyDefaults(channel)) };
55
94
  }
56
95
  } catch (err) {
57
96
  console.error(`[${this.platform}-channels] Error loading channels:`, err.message);
58
97
  }
59
- return { channels: [] };
98
+ this._channelCache = { value, mtimeMs: this._channelFileMtime(), invalidated: false };
99
+ return this._cloneChannelData(value);
60
100
  }
61
101
 
62
102
  saveChannels(data) {
63
103
  this._ensureDir();
64
104
  fs.writeFileSync(this.channelsFilePath, JSON.stringify(data, null, 2), 'utf8');
105
+ const value = {
106
+ channels: Array.isArray(data?.channels)
107
+ ? data.channels.map(channel => this._applyDefaults(channel))
108
+ : []
109
+ };
110
+ this._channelCache = { value, mtimeMs: this._channelFileMtime(), invalidated: false };
111
+ invalidateDashboardSource(this.platform);
112
+ }
113
+
114
+ invalidate() {
115
+ this._channelCache.invalidated = true;
65
116
  }
66
117
 
67
118
  // ── 查询 ──
@@ -76,23 +127,22 @@ class BaseChannelService {
76
127
  }
77
128
 
78
129
  // ── CRUD ──
79
-
80
- createChannel(fields) {
130
+ createChannel(fields = {}) {
81
131
  const data = this.loadChannels();
132
+ const normalizedFields = this._normalizeAuthFields(fields, data.channels);
82
133
 
83
- // 子类可覆写的唯一性校验
84
- this._validateUniqueness(data.channels, fields);
134
+ this._validateUniqueness(data.channels, normalizedFields);
85
135
 
86
136
  const channel = this._applyDefaults({
87
137
  id: this._generateId(),
88
- ...fields,
138
+ ...normalizedFields,
89
139
  createdAt: Date.now(),
90
140
  updatedAt: Date.now(),
91
141
  });
92
142
 
93
143
  data.channels.push(channel);
94
-
95
- // 单渠道强制:代理未运行时只允许一个渠道启用
144
+ this._validateOAuthEnabledTransition(data.channels, channel, null);
145
+ this._validateBeforeChannelMutation(channel, data.channels, { operation: 'create' });
96
146
  if (channel.enabled && !this._isProxyRunning()) {
97
147
  this._enforceSingleChannel(data.channels, data.channels.length - 1);
98
148
  }
@@ -101,28 +151,23 @@ class BaseChannelService {
101
151
  this._onAfterCreate(channel, data.channels);
102
152
  return channel;
103
153
  }
104
-
105
- updateChannel(channelId, updates) {
154
+ updateChannel(channelId, updates = {}) {
106
155
  const data = this.loadChannels();
107
156
  const index = data.channels.findIndex(ch => ch.id === channelId);
108
- if (index === -1) {
109
- throw new Error('Channel not found');
110
- }
157
+ if (index === -1) throw new Error('Channel not found');
111
158
 
112
159
  const oldChannel = data.channels[index];
113
-
114
- // 子类可覆写的唯一性校验(排除自身)
115
- this._validateUniqueness(data.channels, updates, channelId);
116
-
160
+ const normalizedUpdates = this._normalizeAuthFields(updates, data.channels, oldChannel);
161
+ this._validateUniqueness(data.channels, normalizedUpdates, channelId);
117
162
  const nextChannel = this._applyDefaults({
118
163
  ...oldChannel,
119
- ...updates,
164
+ ...normalizedUpdates,
120
165
  id: channelId,
121
166
  updatedAt: Date.now(),
122
167
  });
168
+ this._validateOAuthEnabledTransition(data.channels, nextChannel, oldChannel);
169
+ this._validateBeforeChannelMutation(nextChannel, data.channels, { operation: 'update', oldChannel });
123
170
  data.channels[index] = nextChannel;
124
-
125
- // 单渠道强制
126
171
  const isProxyRunning = this._isProxyRunning();
127
172
  if (!isProxyRunning && nextChannel.enabled && !oldChannel.enabled) {
128
173
  this._enforceSingleChannel(data.channels, index);
@@ -163,6 +208,7 @@ class BaseChannelService {
163
208
  if (!channel) {
164
209
  throw new Error('Channel not found');
165
210
  }
211
+ this._validateBeforeChannelMutation(channel, data.channels, { operation: 'apply' });
166
212
 
167
213
  // 单渠道模式:只启用目标渠道
168
214
  const wasEnabled = channel.enabled !== false;
@@ -206,11 +252,79 @@ class BaseChannelService {
206
252
  }
207
253
 
208
254
  // ── 内部方法 ──
255
+ _normalizeAuthFields(fields = {}, channels = [], existing = null) {
256
+ const next = { ...fields };
257
+ const currentMode = existing?.authMode || (existing ? 'api_key' : null);
258
+ const authMode = next.authMode || currentMode || 'api_key';
259
+ assertAuthMode(authMode);
260
+ const unsafeField = Object.keys(fields).find(key => {
261
+ if (key === 'balanceToken') {
262
+ return authMode === 'oauth' && String(fields[key] || '').trim() !== '';
263
+ }
264
+ return /token|secret|password|refresh|access/i.test(key);
265
+ });
266
+ if (unsafeField) {
267
+ const error = new Error('Invalid OAuth auth payload');
268
+ error.code = 'invalid_auth_payload';
269
+ error.statusCode = 400;
270
+ throw error;
271
+ }
272
+ if (currentMode && next.authMode && next.authMode !== currentMode) {
273
+ const error = new Error('Channel authMode is immutable');
274
+ error.code = 'auth_mode_immutable';
275
+ error.statusCode = 409;
276
+ throw error;
277
+ }
278
+ if (next.authRef && typeof next.authRef === 'object') {
279
+ const allowed = ['credentialId', 'providerId', 'accountId', 'identityKey', 'accountEmail'];
280
+ const unsafe = Object.keys(next.authRef).find(key => !allowed.includes(key));
281
+ if (unsafe) {
282
+ const error = new Error('Invalid OAuth auth payload');
283
+ error.code = 'invalid_auth_payload';
284
+ error.statusCode = 400;
285
+ throw error;
286
+ }
287
+ next.authRef = Object.fromEntries(allowed.map(key => [key, String(next.authRef[key] || '').trim()]));
288
+ } else if (next.authRef !== undefined && next.authRef !== null) {
289
+ const error = new Error('Invalid OAuth auth payload');
290
+ error.code = 'invalid_auth_payload';
291
+ error.statusCode = 400;
292
+ throw error;
293
+ }
294
+ if (authMode === 'oauth') {
295
+ if ((next.authSource || existing?.authSource) !== 'synced-local') {
296
+ const error = new Error('OAuth channels must use synced-local auth');
297
+ error.code = 'invalid_auth_payload';
298
+ error.statusCode = 400;
299
+ throw error;
300
+ }
301
+ next.authSource = 'synced-local';
302
+ const authRef = next.authRef || existing?.authRef;
303
+ const hasReference = authRef
304
+ && Object.values(authRef).some(value => String(value || '').trim() !== '');
305
+ if (!hasReference) {
306
+ const error = new Error('OAuth reference unavailable');
307
+ error.code = 'oauth_reference_unavailable';
308
+ error.statusCode = 422;
309
+ throw error;
310
+ }
311
+ next.apiKey = '';
312
+ next.baseUrl = next.baseUrl || '';
313
+ } else if (authMode === 'none') {
314
+ next.apiKey = '';
315
+ next.authRef = undefined;
316
+ next.authSource = undefined;
317
+ }
318
+ next.authMode = authMode;
319
+ return next;
320
+ }
209
321
 
210
- _generateId() {
211
- return crypto.randomUUID();
322
+ _validateOAuthEnabledTransition(channels, channel) {
323
+ validateEnabledTransition(channels, channel, this.oauthChannelPolicy);
212
324
  }
213
325
 
326
+ _validateBeforeChannelMutation(_channel, _allChannels, _context) {}
327
+
214
328
  _enforceSingleChannel(channels, enabledIndex) {
215
329
  channels.forEach((ch, i) => {
216
330
  if (i !== enabledIndex && ch.enabled) {
@@ -233,6 +347,20 @@ class BaseChannelService {
233
347
  }
234
348
  normalized.weight = normalizeNumber(normalized.weight, 1, 100);
235
349
  // maxConcurrency: null 表示不限制并发;只有用户显式设置正整数时才生效
350
+ normalized.authMode = ['api_key', 'oauth', 'none'].includes(normalized.authMode)
351
+ ? normalized.authMode
352
+ : 'api_key';
353
+ if (normalized.authMode === 'oauth') {
354
+ normalized.authSource = normalized.authSource || 'synced-local';
355
+ normalized.authRef = {
356
+ credentialId: String(normalized.authRef?.credentialId || '').trim(),
357
+ providerId: String(normalized.authRef?.providerId || '').trim(),
358
+ accountId: String(normalized.authRef?.accountId || '').trim(),
359
+ identityKey: String(normalized.authRef?.identityKey || '').trim(),
360
+ accountEmail: String(normalized.authRef?.accountEmail || '').trim()
361
+ };
362
+ normalized.apiKey = '';
363
+ }
236
364
  const rawConcurrency = normalized.maxConcurrency;
237
365
  normalized.maxConcurrency = (rawConcurrency !== null && rawConcurrency !== undefined && Number(rawConcurrency) > 0)
238
366
  ? Number(rawConcurrency)
@@ -0,0 +1,104 @@
1
+ 'use strict';
2
+
3
+ const { ok, unsupported, failed } = require('./driver-result');
4
+
5
+ function createCapabilityDriver({
6
+ platform,
7
+ capability,
8
+ servicePath,
9
+ localServicePath,
10
+ methods = {},
11
+ customMethods = {},
12
+ adapterPath,
13
+ adapterLocalPath,
14
+ adapterMethods = {},
15
+ preservePayloadUpdatedAt = false,
16
+ parserVersion = 1,
17
+ requireImpl,
18
+ prependPlatform = false,
19
+ context = {},
20
+ ...injected
21
+ } = {}) {
22
+ let service;
23
+ let adapter;
24
+ let configured = false;
25
+ let adapterConfigured = false;
26
+ const configureService = target => {
27
+ if (configured) return;
28
+ if (typeof target?.configure === 'function') {
29
+ target.configure({ ...injected, ...context, platform, capability });
30
+ }
31
+ configured = true;
32
+ };
33
+ const loadAdapter = () => {
34
+ if (!adapter && (adapterPath || adapterLocalPath)) {
35
+ adapter = adapterPath && requireImpl ? requireImpl(adapterPath) : require(adapterLocalPath);
36
+ }
37
+ if (!adapterConfigured && adapter) {
38
+ if (typeof adapter.configure === 'function') {
39
+ adapter.configure({ ...injected, ...context, platform, capability });
40
+ }
41
+ adapterConfigured = true;
42
+ }
43
+ return adapter;
44
+ };
45
+ const load = () => {
46
+ if (!service) {
47
+ service = requireImpl ? requireImpl(servicePath) : require(localServicePath);
48
+ configureService(service);
49
+ }
50
+ return service;
51
+ };
52
+ const driver = { platform, capability, preservePayloadUpdatedAt, parserVersion, ...context };
53
+ for (const [operation, methodName] of Object.entries(methods)) {
54
+ driver[operation] = (...args) => {
55
+ try {
56
+ const target = load();
57
+ if (typeof target?.[methodName] !== 'function') return unsupported(platform, capability, operation);
58
+ const value = target[methodName](...args);
59
+ const wrap = result => {
60
+ if (typeof onSuccess === 'function') onSuccess(operation, result);
61
+ return ok(platform, capability, operation, result);
62
+ };
63
+ return value && typeof value.then === 'function'
64
+ ? value.then(wrap).catch(error => failed(platform, capability, operation, error))
65
+ : wrap(value);
66
+ } catch (error) {
67
+ return failed(platform, capability, operation, error);
68
+ }
69
+ };
70
+ }
71
+ for (const [operation, customMethod] of Object.entries(customMethods)) {
72
+ driver[operation] = (...args) => {
73
+ try {
74
+ const target = load();
75
+ const value = customMethod(target, ...(prependPlatform ? [platform, ...args] : args));
76
+ const wrap = result => ok(platform, capability, operation, result);
77
+ return value && typeof value.then === 'function'
78
+ ? value.then(wrap).catch(error => failed(platform, capability, operation, error))
79
+ : wrap(value);
80
+ } catch (error) {
81
+ return failed(platform, capability, operation, error);
82
+ }
83
+ };
84
+ }
85
+ for (const [operation, methodName] of Object.entries(adapterMethods)) {
86
+ driver[operation] = (...args) => {
87
+ try {
88
+ const target = loadAdapter();
89
+ if (typeof target?.[methodName] !== 'function') return unsupported(platform, capability, operation);
90
+ const value = target[methodName](...args);
91
+ const wrap = result => ok(platform, capability, operation, result);
92
+ return value && typeof value.then === 'function'
93
+ ? value.then(wrap).catch(error => failed(platform, capability, operation, error))
94
+ : wrap(value);
95
+ } catch (error) {
96
+ return failed(platform, capability, operation, error);
97
+ }
98
+ };
99
+ }
100
+ Object.defineProperty(driver, '_service', { value: load, enumerable: false });
101
+ return driver;
102
+ }
103
+
104
+ module.exports = { createCapabilityDriver };
@@ -0,0 +1,27 @@
1
+ 'use strict';
2
+
3
+ const AUTH_MODES = new Set(['api_key', 'oauth', 'none']);
4
+
5
+ function createAuthError(message, code, statusCode) {
6
+ const error = new Error(message);
7
+ error.code = code;
8
+ error.statusCode = statusCode;
9
+ return error;
10
+ }
11
+
12
+ function validateEnabledTransition(channels, channel, policy = 'mixed') {
13
+ if (channel.authMode !== 'oauth' || channel.enabled === false || policy !== 'single-enabled') return;
14
+ const conflict = channels.find(item => item.id !== channel.id && item.authMode === 'oauth' && item.enabled !== false);
15
+ if (!conflict) return;
16
+ const error = createAuthError(`OAuth channel conflicts with ${conflict.name || conflict.id}`, 'oauth_channel_conflict', 409);
17
+ error.conflictingChannelId = conflict.id;
18
+ error.conflictingChannelName = conflict.name || conflict.id;
19
+ throw error;
20
+ }
21
+
22
+ function assertAuthMode(mode) {
23
+ if (!AUTH_MODES.has(mode)) throw createAuthError('Invalid auth mode', 'invalid_auth_payload', 400);
24
+ return mode;
25
+ }
26
+
27
+ module.exports = { AUTH_MODES, assertAuthMode, validateEnabledTransition };