@bolloon/bolloon-agent 0.2.9 → 0.2.10

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 (268) hide show
  1. package/README.md +467 -467
  2. package/bin/bolloon-cli.cjs +183 -183
  3. package/bin/bolloon.cjs +0 -0
  4. package/bin/bolloon.js +157 -0
  5. package/dist/agents/agent-manifest-protocol.js +0 -52
  6. package/dist/agents/constraint-layer.js +19 -19
  7. package/dist/agents/pi-sdk.js +287 -1213
  8. package/dist/agents/shell-guard.js +2 -2
  9. package/dist/agents/workflow-pivot-loop.js +2 -81
  10. package/dist/bollharness/src/scripts/deploy-guard.js +8 -8
  11. package/dist/bollharness-integration/context-router-judgment.js +4 -4
  12. package/dist/bollharness-integration/context-router.js +292 -292
  13. package/dist/bollharness-integration/gate-state-machine.js +13 -26
  14. package/dist/bollharness-integration/integration.js +0 -71
  15. package/dist/bollharness-integration/llm-judgment-engine.js +134 -134
  16. package/dist/bollharness-integration/skill-adapter.js +127 -52
  17. package/dist/bootstrap/context-collector.js +3 -6
  18. package/dist/bootstrap/lifecycle-hooks.js +1 -15
  19. package/dist/cli-entry.js +35 -39
  20. package/dist/constraint-runtime/src/_archive_helper.js +9 -0
  21. package/dist/constraint-runtime/src/agent/coordinator.js +48 -0
  22. package/dist/constraint-runtime/src/agent/index.js +1 -0
  23. package/dist/constraint-runtime/src/assistant/index.js +12 -0
  24. package/dist/constraint-runtime/src/bootstrap/index.js +12 -0
  25. package/dist/constraint-runtime/src/bootstrap_graph.js +13 -0
  26. package/dist/constraint-runtime/src/bridge/index.js +12 -0
  27. package/dist/constraint-runtime/src/buddy/index.js +12 -0
  28. package/dist/constraint-runtime/src/cli/index.js +12 -0
  29. package/dist/constraint-runtime/src/command_graph.js +10 -0
  30. package/dist/constraint-runtime/src/commands.js +60 -0
  31. package/dist/constraint-runtime/src/components/index.js +12 -0
  32. package/dist/constraint-runtime/src/constants/index.js +12 -0
  33. package/dist/constraint-runtime/src/constraint/budget.js +22 -0
  34. package/dist/constraint-runtime/src/constraint/index.js +2 -0
  35. package/dist/constraint-runtime/src/constraint/permission.js +20 -0
  36. package/dist/constraint-runtime/src/context.js +30 -0
  37. package/dist/constraint-runtime/src/coordinator/index.js +12 -0
  38. package/dist/constraint-runtime/src/cost_hook.js +4 -0
  39. package/dist/constraint-runtime/src/cost_tracker.js +8 -0
  40. package/dist/constraint-runtime/src/deferred_init.js +10 -0
  41. package/dist/constraint-runtime/src/direct_modes.js +6 -0
  42. package/dist/constraint-runtime/src/dynamic-tool-loader.js +85 -0
  43. package/dist/constraint-runtime/src/entrypoints/index.js +12 -0
  44. package/dist/constraint-runtime/src/execution_registry.js +44 -0
  45. package/dist/constraint-runtime/src/history.js +9 -0
  46. package/dist/constraint-runtime/src/hooks/index.js +12 -0
  47. package/dist/constraint-runtime/src/index.js +26 -0
  48. package/dist/constraint-runtime/src/ink.js +4 -0
  49. package/dist/constraint-runtime/src/keybindings/index.js +12 -0
  50. package/dist/constraint-runtime/src/memdir/index.js +12 -0
  51. package/dist/constraint-runtime/src/migrations/index.js +12 -0
  52. package/dist/constraint-runtime/src/models.js +1 -0
  53. package/dist/constraint-runtime/src/moreright/index.js +12 -0
  54. package/dist/constraint-runtime/src/native_ts/index.js +12 -0
  55. package/dist/constraint-runtime/src/output_styles/index.js +12 -0
  56. package/dist/constraint-runtime/src/parity_audit.js +12 -0
  57. package/dist/constraint-runtime/src/plugins/index.js +12 -0
  58. package/dist/constraint-runtime/src/port_manifest.js +11 -0
  59. package/dist/constraint-runtime/src/prefetch.js +9 -0
  60. package/dist/constraint-runtime/src/query.js +1 -0
  61. package/dist/constraint-runtime/src/remote/index.js +12 -0
  62. package/dist/constraint-runtime/src/remote_runtime.js +9 -0
  63. package/dist/constraint-runtime/src/runtime/index.js +1 -0
  64. package/dist/constraint-runtime/src/runtime/session.js +35 -0
  65. package/dist/constraint-runtime/src/schemas/index.js +12 -0
  66. package/dist/constraint-runtime/src/screens/index.js +12 -0
  67. package/dist/constraint-runtime/src/server/index.js +12 -0
  68. package/dist/constraint-runtime/src/services/index.js +12 -0
  69. package/dist/constraint-runtime/src/session_store.js +22 -0
  70. package/dist/constraint-runtime/src/setup.js +30 -0
  71. package/dist/constraint-runtime/src/skills/index.js +1 -0
  72. package/dist/constraint-runtime/src/skills/skill-registry.js +28 -0
  73. package/dist/constraint-runtime/src/state/index.js +12 -0
  74. package/dist/constraint-runtime/src/system_init.js +20 -0
  75. package/dist/constraint-runtime/src/thinking/engine.js +42 -0
  76. package/dist/constraint-runtime/src/thinking/index.js +1 -0
  77. package/dist/constraint-runtime/src/tool_pool.js +8 -0
  78. package/dist/constraint-runtime/src/tools/OpenCLI/execAdapter.js +7 -0
  79. package/dist/constraint-runtime/src/tools/OpenCLI/listAdapters.js +7 -0
  80. package/dist/constraint-runtime/src/tools/OpenCLI/runCommand.js +7 -0
  81. package/dist/constraint-runtime/src/tools/SafeSDK/confirmTransaction.js +6 -0
  82. package/dist/constraint-runtime/src/tools/SafeSDK/createTransaction.js +8 -0
  83. package/dist/constraint-runtime/src/tools/SafeSDK/executeTransaction.js +6 -0
  84. package/dist/constraint-runtime/src/tools/SafeSDK/getBalance.js +6 -0
  85. package/dist/constraint-runtime/src/tools/SafeSDK/getPendingTransactions.js +6 -0
  86. package/dist/constraint-runtime/src/tools/SafeSDK/proposeTransaction.js +6 -0
  87. package/dist/constraint-runtime/src/tools.js +80 -0
  88. package/dist/constraint-runtime/src/transcript.js +19 -0
  89. package/dist/constraint-runtime/src/types/index.js +12 -0
  90. package/dist/constraint-runtime/src/upstream_proxy/index.js +12 -0
  91. package/dist/constraint-runtime/src/utils/index.js +12 -0
  92. package/dist/constraint-runtime/src/vim/index.js +12 -0
  93. package/dist/constraint-runtime/src/voice/index.js +12 -0
  94. package/dist/constraint-runtime/tests/agent.test.js +16 -0
  95. package/dist/constraint-runtime/tests/constraint.test.js +41 -0
  96. package/dist/constraint-runtime/tests/skill.test.js +19 -0
  97. package/dist/constraint-runtime/tests/thinking.test.js +22 -0
  98. package/dist/context-compaction/auto-compact.js +7 -7
  99. package/dist/electron-preload.js +1 -18
  100. package/dist/electron-preload.js.map +1 -1
  101. package/dist/electron.js +168 -3
  102. package/dist/electron.js.map +1 -1
  103. package/dist/index.js +116 -136
  104. package/dist/llm/config-store.js +8 -24
  105. package/dist/llm/llm-judgment-client.js +102 -102
  106. package/dist/llm/pi-ai.js +85 -126
  107. package/dist/llm/system-prompt/layers/channel/local.md +14 -14
  108. package/dist/llm/system-prompt/layers/channel/p2p-agent.md +18 -18
  109. package/dist/llm/system-prompt/layers/channel/p2p-visitor.md +19 -19
  110. package/dist/llm/system-prompt/layers/core/artifacts_storage.md +89 -89
  111. package/dist/llm/system-prompt/layers/core/evenhandedness.md +21 -21
  112. package/dist/llm/system-prompt/layers/core/hibs_reminders.md +15 -15
  113. package/dist/llm/system-prompt/layers/core/identity.md +37 -37
  114. package/dist/llm/system-prompt/layers/core/knowledge.md +17 -17
  115. package/dist/llm/system-prompt/layers/core/memory_system.md +12 -12
  116. package/dist/llm/system-prompt/layers/core/network_filesystem.md +28 -28
  117. package/dist/llm/system-prompt/layers/core/refusal.md +37 -37
  118. package/dist/llm/system-prompt/layers/core/tone.md +31 -31
  119. package/dist/llm/system-prompt/layers/core/tools.thin.md +13 -13
  120. package/dist/llm/system-prompt/layers/core/wellbeing.md +41 -41
  121. package/dist/llm/system-prompt/layers/role/architect.md +20 -20
  122. package/dist/llm/system-prompt/layers/role/expert.md +19 -19
  123. package/dist/llm/system-prompt/layers/role/implementer.md +15 -15
  124. package/dist/llm/system-prompt/layers/role/security.md +15 -15
  125. package/dist/llm/system-prompt/layers/tool/artifacts.md +72 -72
  126. package/dist/llm/system-prompt/layers/tool/bash.md +25 -25
  127. package/dist/llm/system-prompt/layers/tool/hibs_api.md +171 -171
  128. package/dist/llm/system-prompt/layers/tool/image_search.md +70 -70
  129. package/dist/llm/system-prompt/layers/tool/manifest.md +89 -89
  130. package/dist/llm/system-prompt/layers/tool/mcp_apps.md +53 -53
  131. package/dist/llm/system-prompt/layers/tool/web_search.md +83 -83
  132. package/dist/llm/tool-manifest/ask_user_input.js +1 -1
  133. package/dist/llm/tool-manifest/bash.js +3 -3
  134. package/dist/llm/tool-manifest/create_file.js +4 -4
  135. package/dist/llm/tool-manifest/fetch_sports_data.js +3 -3
  136. package/dist/llm/tool-manifest/image_search.js +2 -2
  137. package/dist/llm/tool-manifest/mcp.js +2 -2
  138. package/dist/llm/tool-manifest/message_compose.js +3 -3
  139. package/dist/llm/tool-manifest/places.js +2 -2
  140. package/dist/llm/tool-manifest/present_files.js +1 -1
  141. package/dist/llm/tool-manifest/recipe.js +2 -2
  142. package/dist/llm/tool-manifest/str_replace.js +5 -5
  143. package/dist/llm/tool-manifest/view.js +3 -3
  144. package/dist/llm/tool-manifest/weather.js +4 -4
  145. package/dist/llm/tool-manifest/web.js +4 -4
  146. package/dist/pi-ecosystem-colony/index.js +365 -0
  147. package/dist/pi-ecosystem-judgment/causal-judge.js +51 -51
  148. package/dist/pi-ecosystem-judgment/distill-prompt.js +42 -42
  149. package/dist/pi-ecosystem-judgment/distillation.js +14 -14
  150. package/dist/pi-ecosystem-judgment/monitor-gate.js +28 -28
  151. package/dist/pi-ecosystem-judgment/value-injection.js +102 -102
  152. package/dist/security/context-router-tool.js +15 -15
  153. package/dist/security/tool-gate.js +0 -11
  154. package/dist/social/ant-colony/AdaptiveHeartbeat.js +101 -0
  155. package/dist/social/ant-colony/PheromoneEngine.js +227 -0
  156. package/dist/social/ant-colony/index.js +6 -0
  157. package/dist/social/ant-colony/types.js +24 -0
  158. package/dist/test/ai-judgment-test.js +80 -0
  159. package/dist/test/bollharness-integration.test.js +318 -0
  160. package/dist/test/channel-agent-multi-dialogue.js +205 -0
  161. package/dist/test/channel-heartbeat-agent-test.js +201 -0
  162. package/dist/test/constraint-layer.test.js +164 -0
  163. package/dist/test/diap-identity-test.js +172 -0
  164. package/dist/test/diap-quick-test.js +62 -0
  165. package/dist/test/global-shared-context.test.js +315 -0
  166. package/dist/test/harness-judgment-injection.test.js +246 -0
  167. package/dist/test/harness-workflow-integrator-test.js +228 -0
  168. package/dist/test/human-value-store.test.js +243 -0
  169. package/dist/test/hybrid-integration-test.js +118 -0
  170. package/dist/test/hybrid-messenger-verify.js +55 -0
  171. package/dist/test/iroh-bistream-debug.js +38 -0
  172. package/dist/test/iroh-communication.test.js +66 -0
  173. package/dist/test/iroh-debug-test.js +57 -0
  174. package/dist/test/iroh-diap-test.js +71 -0
  175. package/dist/test/iroh-direct-connect.js +55 -0
  176. package/dist/test/iroh-e2e-fixed.js +89 -0
  177. package/dist/test/iroh-e2e-same-process.js +63 -0
  178. package/dist/test/iroh-e2e.js +66 -0
  179. package/dist/test/iroh-final-e2e.js +72 -0
  180. package/dist/test/iroh-relay-test.js +37 -0
  181. package/dist/test/iroh-simple-test.js +41 -0
  182. package/dist/test/iroh-transport-verify.js +54 -0
  183. package/dist/test/iroh-transport.test.js +37 -0
  184. package/dist/test/iroh-two-nodes.js +70 -0
  185. package/dist/test/iroh-verify.js +44 -0
  186. package/dist/test/judgment-decision.test.js +219 -0
  187. package/dist/test/llm-judgment-integration.test.js +220 -0
  188. package/dist/test/p2p-agent-complex-dialogue.js +385 -0
  189. package/dist/test/p2p-agent-dialogue.js +341 -0
  190. package/dist/test/p2p-agent-full-bidirectional.js +510 -0
  191. package/dist/test/p2p-agent-harness-flow.js +437 -0
  192. package/dist/test/p2p-agent-harness-single.js +143 -0
  193. package/dist/test/p2p-ai-dialogue-test.js +318 -0
  194. package/dist/test/p2p-cid-connect-test.js +195 -0
  195. package/dist/test/p2p-connect-receiver.js +69 -0
  196. package/dist/test/p2p-doc-transfer.js +110 -0
  197. package/dist/test/p2p-identity-page-test.js +77 -0
  198. package/dist/test/p2p-iroh-test.js +171 -0
  199. package/dist/test/p2p-minimal-test.js +241 -0
  200. package/dist/test/p2p-node-1.js +148 -0
  201. package/dist/test/p2p-node-2.js +148 -0
  202. package/dist/test/p2p-server.js +281 -0
  203. package/dist/test/p2p-two-nodes-test.js +438 -0
  204. package/dist/test/pi-sdk.test.js +44 -0
  205. package/dist/test/set-persona.js +40 -0
  206. package/dist/test/simple.test.js +9 -0
  207. package/dist/test/storage-integration.test.js +150 -0
  208. package/dist/test/subagent-manager.test.js +276 -0
  209. package/dist/test/test-gate-flow.test.js +81 -0
  210. package/dist/test/workflow-engine.test.js +87 -0
  211. package/dist/test/workflow-pivot-loop.test.js +246 -0
  212. package/dist/web/api-config.html +520 -520
  213. package/dist/web/client.js +2917 -3791
  214. package/dist/web/components/p2p/index.js +234 -276
  215. package/dist/web/components/wallet-viem.mjs +118 -118
  216. package/dist/web/index.html +374 -373
  217. package/dist/web/manifest.json +20 -20
  218. package/dist/web/server.js +23 -775
  219. package/dist/web/style.css +4737 -4737
  220. package/dist/web/ui/message-renderer.js +373 -451
  221. package/dist/web/ui/step-timeline.js +255 -351
  222. package/package.json +160 -160
  223. package/scripts/build-cli.js +215 -215
  224. package/scripts/build-web.ts +125 -125
  225. package/scripts/postinstall.js +152 -152
  226. package/bin/bolloon-daemon.sh +0 -207
  227. package/bin/ipfs +0 -0
  228. package/dist/agents/chat-segmenter.js +0 -298
  229. package/dist/agents/judgment-protocol.js +0 -479
  230. package/dist/agents/parse-tool-call.js +0 -304
  231. package/dist/agents/peer-manifest-loader.js +0 -210
  232. package/dist/agents/react-loop.js +0 -120
  233. package/dist/agents/session-store.js +0 -171
  234. package/dist/agents/tool-registry.js +0 -136
  235. package/dist/bootstrap/chat-archiver.js +0 -276
  236. package/dist/bootstrap/memory-compressor.js +0 -170
  237. package/dist/bootstrap/persona-loader.js +0 -94
  238. package/dist/electron/config.js +0 -16
  239. package/dist/electron/config.js.map +0 -1
  240. package/dist/electron/dialogs.js +0 -71
  241. package/dist/electron/dialogs.js.map +0 -1
  242. package/dist/electron/first-run.js +0 -129
  243. package/dist/electron/first-run.js.map +0 -1
  244. package/dist/electron/ipc.js +0 -16
  245. package/dist/electron/ipc.js.map +0 -1
  246. package/dist/electron/logger.js +0 -77
  247. package/dist/electron/logger.js.map +0 -1
  248. package/dist/electron/main.js +0 -60
  249. package/dist/electron/main.js.map +0 -1
  250. package/dist/electron/menu.js +0 -140
  251. package/dist/electron/menu.js.map +0 -1
  252. package/dist/electron/paths.js +0 -33
  253. package/dist/electron/paths.js.map +0 -1
  254. package/dist/electron/server.js +0 -74
  255. package/dist/electron/server.js.map +0 -1
  256. package/dist/electron/tray.js +0 -73
  257. package/dist/electron/tray.js.map +0 -1
  258. package/dist/electron/window.js +0 -69
  259. package/dist/electron/window.js.map +0 -1
  260. package/dist/network/local-inbox-bus.js +0 -73
  261. package/dist/network/p2p-outbox.js +0 -161
  262. package/dist/network/peer-fs.js +0 -420
  263. package/dist/network/peer-resource-bridge.js +0 -216
  264. package/dist/web/components/p2p/P2PModal.js +0 -188
  265. package/dist/web/components/p2p/p2p-modal.js +0 -664
  266. package/dist/web/components/p2p/p2p-tools.js +0 -248
  267. package/dist/web/input-validator.js +0 -103
  268. package/dist/web/util/safe-name.js +0 -32
@@ -1,520 +1,520 @@
1
- <!DOCTYPE html>
2
- <html lang="zh-CN" data-theme="dark">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>API 配置 - Bolloon</title>
7
- <!-- 2026-06-11: 移除 Google Fonts 外部 CSS — 在大陆/跨公网时经常 30s+ timeout 阻塞首屏
8
- style.css 里只用字体名字符串 (JetBrains Mono / monospace), 浏览器看字面量自动落回系统字体
9
- 2026-06-11 提速: 不再等 Google CDN 即可渲染 -->
10
- <link rel="stylesheet" href="/style.css">
11
- </head>
12
- <body>
13
- <div class="api-config-page">
14
- <!-- 页面标题 -->
15
- <div class="page-header">
16
- <div class="page-title-group">
17
- <h1>API 配置</h1>
18
- <span class="config-count-badge" id="configCount">加载中...</span>
19
- </div>
20
- <a href="/" class="back-link">← 返回主页</a>
21
- </div>
22
-
23
- <!-- 标签切换 -->
24
- <div class="api-tabs">
25
- <button class="api-tab active" data-tab="llm" onclick="switchTab('llm')">
26
- <span class="api-tab-icon">💬</span>
27
- <span>LLM 对话</span>
28
- </button>
29
- <button class="api-tab" data-tab="video" onclick="switchTab('video')">
30
- <span class="api-tab-icon">🎬</span>
31
- <span>视频生成</span>
32
- </button>
33
- <button class="api-tab" data-tab="audio" onclick="switchTab('audio')">
34
- <span class="api-tab-icon">🎵</span>
35
- <span>音频生成</span>
36
- </button>
37
- </div>
38
-
39
- <!-- LLM 面板 -->
40
- <div class="api-panel" id="panel-llm" data-panel="llm">
41
- <div class="provider-list" id="providerList">
42
- <div class="loading-state">加载中...</div>
43
- </div>
44
- </div>
45
-
46
- <!-- 视频生成面板 -->
47
- <div class="api-panel" id="panel-video" data-panel="video" style="display: none;">
48
- <div class="video-intro">
49
- <p>视频生成走异步任务流(提交任务 → 轮询结果),与 LLM 实时对话不同。Seedance 由字节火山方舟 (ARK) 提供,支持文生视频 (t2v) 与图生视频 (i2v);MiniMax Video 为同 tab 下的另一选项。</p>
50
- </div>
51
- <div class="provider-list" id="videoProviderList">
52
- <div class="loading-state">加载中...</div>
53
- </div>
54
- </div>
55
-
56
- <!-- 音频生成面板 -->
57
- <div class="api-panel" id="panel-audio" data-panel="audio" style="display: none;">
58
- <div class="video-intro">
59
- <p>音频生成:TTS(文生语音)/ Music(文生音乐)。MiniMax 提供 speech-01 与 music-01 模型,TTS 默认走 OpenAI 兼容 <code>/audio/speech</code> 端点,Music 走 MiniMax 自有 <code>/music_generation</code> 端点。</p>
60
- </div>
61
- <div class="provider-list" id="audioProviderList">
62
- <div class="loading-state">加载中...</div>
63
- </div>
64
- </div>
65
-
66
- <!-- 配置弹窗 -->
67
- <div class="modal-overlay" id="configModal" style="display: none;">
68
- <div class="modal-box">
69
- <div class="modal-header">
70
- <div class="modal-title-group">
71
- <div class="modal-icon" id="modalIcon">A</div>
72
- <div>
73
- <h2 id="modalTitle">配置</h2>
74
- <p class="modal-subtitle" id="modalSubtitle">描述</p>
75
- </div>
76
- </div>
77
- <button class="modal-close" onclick="closeModal()">×</button>
78
- </div>
79
-
80
- <form class="modal-form" id="configForm">
81
- <div class="form-group">
82
- <label>API Key</label>
83
- <input type="password" id="apiKeyInput" placeholder="输入 API Key">
84
- <p class="form-hint" id="apiKeyHint">已有 Key 会保留,输入新值以更新</p>
85
- </div>
86
-
87
- <div class="form-group">
88
- <label>Base URL</label>
89
- <input type="text" id="baseUrlInput" placeholder="https://api.example.com/v1">
90
- <p class="form-hint">留空使用默认地址</p>
91
- </div>
92
-
93
- <div class="form-group">
94
- <label>模型</label>
95
- <input type="text" id="modelInput" placeholder="如 gpt-4" list="modelSuggestList">
96
- <datalist id="modelSuggestList"></datalist>
97
- <p class="form-hint" id="modelHint"></p>
98
- </div>
99
-
100
- <!-- 视频模型专用字段 -->
101
- <div class="video-only-fields" style="display: none;">
102
- <div class="form-row">
103
- <div class="form-group">
104
- <label>分辨率</label>
105
- <select id="resolutionInput">
106
- <option value="480p">480p</option>
107
- <option value="720p">720p</option>
108
- <option value="1080p">1080p</option>
109
- </select>
110
- </div>
111
- <div class="form-group">
112
- <label>宽高比</label>
113
- <select id="ratioInput">
114
- <option value="16:9">16:9</option>
115
- <option value="9:16">9:16</option>
116
- <option value="1:1">1:1</option>
117
- <option value="4:3">4:3</option>
118
- <option value="3:4">3:4</option>
119
- <option value="21:9">21:9</option>
120
- </select>
121
- </div>
122
- </div>
123
- <div class="form-group">
124
- <label>默认时长(秒)</label>
125
- <input type="number" id="durationInput" min="3" max="12" step="1" value="5">
126
- <p class="form-hint">Seedance 支持 3-12 秒</p>
127
- </div>
128
- </div>
129
-
130
- <!-- 音频模型专用字段 -->
131
- <div class="audio-only-fields" style="display: none;">
132
- <div class="form-group">
133
- <label>音色 / 风格</label>
134
- <input type="text" id="voiceInput" placeholder="male-qn-jingying / female-shaonv ...">
135
- <p class="form-hint" id="voiceHint"></p>
136
- </div>
137
- <div class="form-row">
138
- <div class="form-group">
139
- <label>语速</label>
140
- <input type="number" id="speedInput" min="0.5" max="2.0" step="0.1" value="1.0">
141
- </div>
142
- <div class="form-group">
143
- <label>输出格式</label>
144
- <select id="formatInput">
145
- <option value="mp3">mp3</option>
146
- <option value="pcm">pcm</option>
147
- <option value="wav">wav</option>
148
- </select>
149
- </div>
150
- </div>
151
- <div class="form-group" id="audioModeGroup" style="display: none;">
152
- <label>模式</label>
153
- <select id="modeInput">
154
- <option value="instrumental">纯音乐</option>
155
- <option value="lyrics">带歌词</option>
156
- </select>
157
- </div>
158
- <div class="form-group" id="audioDurationGroup" style="display: none;">
159
- <label>默认时长(秒)</label>
160
- <input type="number" id="audioDurationInput" min="5" max="120" step="1" value="30">
161
- <p class="form-hint">music-01 支持 5-120 秒</p>
162
- </div>
163
- </div>
164
-
165
- <!-- LLM 专用字段 -->
166
- <div class="llm-only-fields">
167
- <div class="form-group form-group-small">
168
- <label>温度</label>
169
- <input type="number" id="temperatureInput" value="0.7" min="0" max="2" step="0.1">
170
- </div>
171
- </div>
172
-
173
- <div class="form-group">
174
- <button type="button" class="test-btn" id="testBtn" onclick="testConnection()">
175
- ⚡ 测试连接
176
- </button>
177
- <div class="test-result" id="testResult" style="display: none;"></div>
178
- </div>
179
-
180
- <div class="form-actions">
181
- <button type="button" class="btn-cancel" onclick="closeModal()">取消</button>
182
- <button type="submit" class="btn-save">保存</button>
183
- </div>
184
- </form>
185
- </div>
186
- </div>
187
- </div>
188
-
189
- <script>
190
- // 当前 tab
191
- let currentTab = 'llm';
192
-
193
- // 缓存三份配置
194
- let llmConfigData = null;
195
- let videoConfigData = null;
196
- let audioConfigData = null;
197
- let currentProvider = null;
198
- let currentProviderType = 'llm'; // 'llm' | 'video' | 'audio'
199
-
200
- // ==================== Tab 切换 ====================
201
- function switchTab(tab) {
202
- currentTab = tab;
203
- document.querySelectorAll('.api-tab').forEach(btn => {
204
- btn.classList.toggle('active', btn.dataset.tab === tab);
205
- });
206
- document.querySelectorAll('.api-panel').forEach(panel => {
207
- panel.style.display = panel.dataset.panel === tab ? 'block' : 'none';
208
- });
209
- // 切换 tab 时更新计数 badge
210
- updateCountBadge();
211
- }
212
-
213
- function updateCountBadge() {
214
- const data = currentTab === 'llm' ? llmConfigData :
215
- currentTab === 'video' ? videoConfigData : audioConfigData;
216
- if (!data) {
217
- document.getElementById('configCount').textContent = '加载中...';
218
- return;
219
- }
220
- let configured = 0, total = 0;
221
- for (const key in data.providers) {
222
- total++;
223
- const p = data.providers[key];
224
- if (currentTab === 'llm') {
225
- if (p.enabled && p.apiKey) configured++;
226
- } else {
227
- const info = data.providerInfo[key] || {};
228
- if (p.enabled && (!info.requiresApiKey || p.apiKey)) configured++;
229
- }
230
- }
231
- const labelMap = { llm: '', video: '视频', audio: '音频' };
232
- const label = labelMap[currentTab] || '';
233
- document.getElementById('configCount').textContent = configured + '/' + total + ' ' + label + '已配置';
234
- }
235
-
236
- // ==================== 加载 ====================
237
- async function loadAll() {
238
- await Promise.all([loadLLMConfig(), loadVideoConfig(), loadAudioConfig()]);
239
- }
240
-
241
- async function loadLLMConfig() {
242
- try {
243
- const resp = await fetch('/api/llm-config');
244
- llmConfigData = await resp.json();
245
- renderProviders('llm');
246
- if (currentTab === 'llm') updateCountBadge();
247
- } catch (err) {
248
- document.getElementById('providerList').innerHTML =
249
- '<div class="error-state">加载失败: ' + err.message + '</div>';
250
- }
251
- }
252
-
253
- async function loadVideoConfig() {
254
- try {
255
- const resp = await fetch('/api/video-config');
256
- videoConfigData = await resp.json();
257
- renderProviders('video');
258
- if (currentTab === 'video') updateCountBadge();
259
- } catch (err) {
260
- document.getElementById('videoProviderList').innerHTML =
261
- '<div class="error-state">加载失败: ' + err.message + '</div>';
262
- }
263
- }
264
-
265
- async function loadAudioConfig() {
266
- try {
267
- const resp = await fetch('/api/audio-config');
268
- audioConfigData = await resp.json();
269
- renderProviders('audio');
270
- if (currentTab === 'audio') updateCountBadge();
271
- } catch (err) {
272
- document.getElementById('audioProviderList').innerHTML =
273
- '<div class="error-state">加载失败: ' + err.message + '</div>';
274
- }
275
- }
276
-
277
- // ==================== 渲染 ====================
278
- function renderProviders(type) {
279
- const data = type === 'llm' ? llmConfigData :
280
- type === 'video' ? videoConfigData : audioConfigData;
281
- const listEl = document.getElementById(
282
- type === 'llm' ? 'providerList' :
283
- type === 'video' ? 'videoProviderList' : 'audioProviderList'
284
- );
285
- if (!data) return;
286
-
287
- const providers = data.providers;
288
- const info = data.providerInfo;
289
- const isActive = (key) => data.activeProvider === key;
290
-
291
- let html = '';
292
- for (const key in providers) {
293
- const p = providers[key];
294
- const i = info[key] || { name: key, description: '供应商', requiresApiKey: true };
295
- const active = isActive(key);
296
-
297
- let statusText, statusClass;
298
- if (active) {
299
- statusText = '使用中';
300
- statusClass = 'status-accent';
301
- } else if (p.enabled && p.apiKey) {
302
- statusText = '已启用';
303
- statusClass = 'status-green';
304
- } else if (p.enabled) {
305
- statusText = '已启用';
306
- statusClass = 'status-yellow';
307
- } else {
308
- statusText = '未启用';
309
- statusClass = 'status-muted';
310
- }
311
-
312
- const isConfigured = p.enabled && p.apiKey;
313
-
314
- html += `
315
- <div class="provider-card ${active ? 'active' : ''}" onclick="openModal('${type}', '${key}')">
316
- <div class="provider-header">
317
- <div class="provider-icon">${i.name.charAt(0).toUpperCase()}</div>
318
- <div class="provider-info">
319
- <h3 class="provider-name">${i.name}</h3>
320
- <p class="provider-desc">${i.description}</p>
321
- ${i.docs ? '<a class="provider-docs" href="' + i.docs + '" target="_blank" onclick="event.stopPropagation()">📖 文档</a>' : ''}
322
- </div>
323
- <div class="status-badge ${statusClass}">
324
- <span class="status-dot"></span>
325
- ${statusText}
326
- </div>
327
- </div>
328
- <div class="provider-footer">
329
- <div class="config-status">
330
- <span class="status-icon ${isConfigured ? 'configured' : ''}">${isConfigured ? '✓' : '○'}</span>
331
- ${isConfigured ? '已配置 API Key' : '未配置 API Key'}
332
- </div>
333
- ${p.model ? '<div class="model-info">模型: ' + p.model + '</div>' : ''}
334
- <div class="arrow-indicator">›</div>
335
- </div>
336
- </div>
337
- `;
338
- }
339
- listEl.innerHTML = html;
340
- }
341
-
342
- // ==================== 弹窗 ====================
343
- function openModal(type, providerKey) {
344
- currentProviderType = type;
345
- currentProvider = providerKey;
346
- const data = type === 'llm' ? llmConfigData :
347
- type === 'video' ? videoConfigData : audioConfigData;
348
- const p = data.providers[providerKey];
349
- const i = data.providerInfo[providerKey] || { name: providerKey, description: '供应商' };
350
-
351
- document.getElementById('modalIcon').textContent = i.name.charAt(0).toUpperCase();
352
- document.getElementById('modalTitle').textContent = '配置 ' + i.name;
353
- document.getElementById('modalSubtitle').textContent = i.description;
354
-
355
- document.getElementById('apiKeyInput').value = '';
356
- document.getElementById('apiKeyHint').textContent = p.apiKey
357
- ? '当前已配置 (***' + (p.apiKey.slice(-4) || '') + '),输入新值以更新'
358
- : '输入 API Key';
359
- document.getElementById('baseUrlInput').value = p.baseUrl || '';
360
- document.getElementById('modelInput').value = p.model || '';
361
-
362
- // 填充模型下拉建议(仅 LLM 类型有 providerInfo.models)
363
- const datalist = document.getElementById('modelSuggestList');
364
- datalist.innerHTML = '';
365
- if (type === 'llm' && i.models && Array.isArray(i.models)) {
366
- for (const m of i.models) {
367
- const opt = document.createElement('option');
368
- opt.value = m;
369
- datalist.appendChild(opt);
370
- }
371
- }
372
-
373
- // 视频/音频/LLM 专用字段显隐
374
- const isVideo = type === 'video';
375
- const isAudio = type === 'audio';
376
- document.querySelector('.video-only-fields').style.display = isVideo ? 'block' : 'none';
377
- document.querySelector('.audio-only-fields').style.display = isAudio ? 'block' : 'none';
378
- document.querySelector('.llm-only-fields').style.display = (!isVideo && !isAudio) ? 'block' : 'none';
379
-
380
- if (isVideo) {
381
- document.getElementById('resolutionInput').value = p.resolution || '720p';
382
- document.getElementById('ratioInput').value = p.ratio || '16:9';
383
- document.getElementById('durationInput').value = p.duration || 5;
384
- document.getElementById('modelHint').textContent = '示例: doubao-seedance-1-0-lite-t2v-250428 (文生视频) 或 ...-i2v-... (图生视频)';
385
- } else if (isAudio) {
386
- const isMusic = (i.kind === 'music');
387
- // TTS 字段
388
- document.getElementById('voiceInput').parentElement.style.display = isMusic ? 'none' : 'block';
389
- document.getElementById('speedInput').parentElement.parentElement.style.display = isMusic ? 'none' : 'flex';
390
- document.getElementById('modeInput').parentElement.style.display = isMusic ? 'block' : 'none';
391
- document.getElementById('audioDurationInput').parentElement.style.display = isMusic ? 'block' : 'none';
392
-
393
- if (!isMusic) {
394
- document.getElementById('voiceInput').value = p.voice || 'male-qn-jingying';
395
- document.getElementById('voiceHint').textContent = '常用音色: male-qn-jingying, female-shaonv, female-yujie, presenter_male, presenter_female';
396
- document.getElementById('speedInput').value = p.speed || 1.0;
397
- document.getElementById('formatInput').value = p.format || 'mp3';
398
- } else {
399
- document.getElementById('modeInput').value = p.mode || 'instrumental';
400
- document.getElementById('audioDurationInput').value = p.duration || 30;
401
- }
402
- document.getElementById('modelHint').textContent = isMusic
403
- ? '示例: music-01'
404
- : '示例: speech-01 (TTS) / asr-01 (语音转写)';
405
- } else {
406
- document.getElementById('temperatureInput').value = p.temperature || 0.7;
407
- }
408
-
409
- document.getElementById('testResult').style.display = 'none';
410
- document.getElementById('configModal').style.display = 'flex';
411
- }
412
-
413
- function closeModal() {
414
- document.getElementById('configModal').style.display = 'none';
415
- currentProvider = null;
416
- }
417
-
418
- // ==================== 测试连接 ====================
419
- async function testConnection() {
420
- const btn = document.getElementById('testBtn');
421
- const result = document.getElementById('testResult');
422
-
423
- btn.disabled = true;
424
- btn.innerHTML = '<span class="spinner"></span> 测试中...';
425
- result.style.display = 'none';
426
-
427
- const endpoint = currentProviderType === 'llm' ? '/api/llm-test' :
428
- currentProviderType === 'video' ? '/api/video-test' :
429
- '/api/audio-test';
430
-
431
- try {
432
- const resp = await fetch(endpoint, {
433
- method: 'POST',
434
- headers: { 'Content-Type': 'application/json' },
435
- body: JSON.stringify({ provider: currentProvider })
436
- });
437
- const data = await resp.json();
438
-
439
- result.className = 'test-result ' + (data.success ? 'test-success' : 'test-error');
440
- result.innerHTML = '<span>' + (data.success ? '✓' : '✗') + '</span> ' +
441
- (data.success ? '连接成功!延迟: ' + data.latency + 'ms' : (data.error || '连接失败'));
442
- result.style.display = 'flex';
443
- } catch (err) {
444
- result.className = 'test-result test-error';
445
- result.innerHTML = '<span>✗</span> ' + err.message;
446
- result.style.display = 'flex';
447
- }
448
-
449
- btn.disabled = false;
450
- btn.innerHTML = '⚡ 测试连接';
451
- }
452
-
453
- // ==================== 保存 ====================
454
- document.getElementById('configForm').onsubmit = async function(e) {
455
- e.preventDefault();
456
-
457
- const data = currentProviderType === 'llm' ? llmConfigData : videoConfigData;
458
- const existing = data.providers[currentProvider];
459
- const apiKeyVal = document.getElementById('apiKeyInput').value;
460
-
461
- const updateData = {
462
- enabled: true,
463
- apiKey: apiKeyVal || existing.apiKey || '',
464
- baseUrl: document.getElementById('baseUrlInput').value || existing.baseUrl || '',
465
- model: document.getElementById('modelInput').value || existing.model || ''
466
- };
467
-
468
- if (currentProviderType === 'video') {
469
- updateData.resolution = document.getElementById('resolutionInput').value;
470
- updateData.ratio = document.getElementById('ratioInput').value;
471
- updateData.duration = parseInt(document.getElementById('durationInput').value) || 5;
472
- } else if (currentProviderType === 'audio') {
473
- const info = audioConfigData.providerInfo[currentProvider] || {};
474
- const isMusic = info.kind === 'music';
475
- if (isMusic) {
476
- updateData.mode = document.getElementById('modeInput').value;
477
- updateData.duration = parseInt(document.getElementById('audioDurationInput').value) || 30;
478
- } else {
479
- updateData.voice = document.getElementById('voiceInput').value;
480
- updateData.speed = parseFloat(document.getElementById('speedInput').value) || 1.0;
481
- updateData.format = document.getElementById('formatInput').value;
482
- }
483
- } else {
484
- updateData.temperature = parseFloat(document.getElementById('temperatureInput').value) || 0.7;
485
- }
486
-
487
- const endpoint = currentProviderType === 'llm' ? '/api/llm-config' :
488
- currentProviderType === 'video' ? '/api/video-config' :
489
- '/api/audio-config';
490
-
491
- try {
492
- await fetch(endpoint, {
493
- method: 'POST',
494
- headers: { 'Content-Type': 'application/json' },
495
- body: JSON.stringify({ provider: currentProvider, config: updateData })
496
- });
497
-
498
- const saveBtn = this.querySelector('.btn-save');
499
- saveBtn.textContent = '✓ 已保存';
500
- setTimeout(() => {
501
- closeModal();
502
- if (currentProviderType === 'llm') loadLLMConfig();
503
- else if (currentProviderType === 'video') loadVideoConfig();
504
- else loadAudioConfig();
505
- }, 500);
506
- } catch (err) {
507
- alert('保存失败: ' + err.message);
508
- }
509
- };
510
-
511
- // ==================== 关闭弹窗 ====================
512
- document.getElementById('configModal').onclick = function(e) {
513
- if (e.target === this) closeModal();
514
- };
515
-
516
- // 启动
517
- loadAll();
518
- </script>
519
- </body>
520
- </html>
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN" data-theme="dark">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>API 配置 - Bolloon</title>
7
+ <!-- 2026-06-11: 移除 Google Fonts 外部 CSS — 在大陆/跨公网时经常 30s+ timeout 阻塞首屏
8
+ style.css 里只用字体名字符串 (JetBrains Mono / monospace), 浏览器看字面量自动落回系统字体
9
+ 2026-06-11 提速: 不再等 Google CDN 即可渲染 -->
10
+ <link rel="stylesheet" href="/style.css">
11
+ </head>
12
+ <body>
13
+ <div class="api-config-page">
14
+ <!-- 页面标题 -->
15
+ <div class="page-header">
16
+ <div class="page-title-group">
17
+ <h1>API 配置</h1>
18
+ <span class="config-count-badge" id="configCount">加载中...</span>
19
+ </div>
20
+ <a href="/" class="back-link">← 返回主页</a>
21
+ </div>
22
+
23
+ <!-- 标签切换 -->
24
+ <div class="api-tabs">
25
+ <button class="api-tab active" data-tab="llm" onclick="switchTab('llm')">
26
+ <span class="api-tab-icon">💬</span>
27
+ <span>LLM 对话</span>
28
+ </button>
29
+ <button class="api-tab" data-tab="video" onclick="switchTab('video')">
30
+ <span class="api-tab-icon">🎬</span>
31
+ <span>视频生成</span>
32
+ </button>
33
+ <button class="api-tab" data-tab="audio" onclick="switchTab('audio')">
34
+ <span class="api-tab-icon">🎵</span>
35
+ <span>音频生成</span>
36
+ </button>
37
+ </div>
38
+
39
+ <!-- LLM 面板 -->
40
+ <div class="api-panel" id="panel-llm" data-panel="llm">
41
+ <div class="provider-list" id="providerList">
42
+ <div class="loading-state">加载中...</div>
43
+ </div>
44
+ </div>
45
+
46
+ <!-- 视频生成面板 -->
47
+ <div class="api-panel" id="panel-video" data-panel="video" style="display: none;">
48
+ <div class="video-intro">
49
+ <p>视频生成走异步任务流(提交任务 → 轮询结果),与 LLM 实时对话不同。Seedance 由字节火山方舟 (ARK) 提供,支持文生视频 (t2v) 与图生视频 (i2v);MiniMax Video 为同 tab 下的另一选项。</p>
50
+ </div>
51
+ <div class="provider-list" id="videoProviderList">
52
+ <div class="loading-state">加载中...</div>
53
+ </div>
54
+ </div>
55
+
56
+ <!-- 音频生成面板 -->
57
+ <div class="api-panel" id="panel-audio" data-panel="audio" style="display: none;">
58
+ <div class="video-intro">
59
+ <p>音频生成:TTS(文生语音)/ Music(文生音乐)。MiniMax 提供 speech-01 与 music-01 模型,TTS 默认走 OpenAI 兼容 <code>/audio/speech</code> 端点,Music 走 MiniMax 自有 <code>/music_generation</code> 端点。</p>
60
+ </div>
61
+ <div class="provider-list" id="audioProviderList">
62
+ <div class="loading-state">加载中...</div>
63
+ </div>
64
+ </div>
65
+
66
+ <!-- 配置弹窗 -->
67
+ <div class="modal-overlay" id="configModal" style="display: none;">
68
+ <div class="modal-box">
69
+ <div class="modal-header">
70
+ <div class="modal-title-group">
71
+ <div class="modal-icon" id="modalIcon">A</div>
72
+ <div>
73
+ <h2 id="modalTitle">配置</h2>
74
+ <p class="modal-subtitle" id="modalSubtitle">描述</p>
75
+ </div>
76
+ </div>
77
+ <button class="modal-close" onclick="closeModal()">×</button>
78
+ </div>
79
+
80
+ <form class="modal-form" id="configForm">
81
+ <div class="form-group">
82
+ <label>API Key</label>
83
+ <input type="password" id="apiKeyInput" placeholder="输入 API Key">
84
+ <p class="form-hint" id="apiKeyHint">已有 Key 会保留,输入新值以更新</p>
85
+ </div>
86
+
87
+ <div class="form-group">
88
+ <label>Base URL</label>
89
+ <input type="text" id="baseUrlInput" placeholder="https://api.example.com/v1">
90
+ <p class="form-hint">留空使用默认地址</p>
91
+ </div>
92
+
93
+ <div class="form-group">
94
+ <label>模型</label>
95
+ <input type="text" id="modelInput" placeholder="如 gpt-4" list="modelSuggestList">
96
+ <datalist id="modelSuggestList"></datalist>
97
+ <p class="form-hint" id="modelHint"></p>
98
+ </div>
99
+
100
+ <!-- 视频模型专用字段 -->
101
+ <div class="video-only-fields" style="display: none;">
102
+ <div class="form-row">
103
+ <div class="form-group">
104
+ <label>分辨率</label>
105
+ <select id="resolutionInput">
106
+ <option value="480p">480p</option>
107
+ <option value="720p">720p</option>
108
+ <option value="1080p">1080p</option>
109
+ </select>
110
+ </div>
111
+ <div class="form-group">
112
+ <label>宽高比</label>
113
+ <select id="ratioInput">
114
+ <option value="16:9">16:9</option>
115
+ <option value="9:16">9:16</option>
116
+ <option value="1:1">1:1</option>
117
+ <option value="4:3">4:3</option>
118
+ <option value="3:4">3:4</option>
119
+ <option value="21:9">21:9</option>
120
+ </select>
121
+ </div>
122
+ </div>
123
+ <div class="form-group">
124
+ <label>默认时长(秒)</label>
125
+ <input type="number" id="durationInput" min="3" max="12" step="1" value="5">
126
+ <p class="form-hint">Seedance 支持 3-12 秒</p>
127
+ </div>
128
+ </div>
129
+
130
+ <!-- 音频模型专用字段 -->
131
+ <div class="audio-only-fields" style="display: none;">
132
+ <div class="form-group">
133
+ <label>音色 / 风格</label>
134
+ <input type="text" id="voiceInput" placeholder="male-qn-jingying / female-shaonv ...">
135
+ <p class="form-hint" id="voiceHint"></p>
136
+ </div>
137
+ <div class="form-row">
138
+ <div class="form-group">
139
+ <label>语速</label>
140
+ <input type="number" id="speedInput" min="0.5" max="2.0" step="0.1" value="1.0">
141
+ </div>
142
+ <div class="form-group">
143
+ <label>输出格式</label>
144
+ <select id="formatInput">
145
+ <option value="mp3">mp3</option>
146
+ <option value="pcm">pcm</option>
147
+ <option value="wav">wav</option>
148
+ </select>
149
+ </div>
150
+ </div>
151
+ <div class="form-group" id="audioModeGroup" style="display: none;">
152
+ <label>模式</label>
153
+ <select id="modeInput">
154
+ <option value="instrumental">纯音乐</option>
155
+ <option value="lyrics">带歌词</option>
156
+ </select>
157
+ </div>
158
+ <div class="form-group" id="audioDurationGroup" style="display: none;">
159
+ <label>默认时长(秒)</label>
160
+ <input type="number" id="audioDurationInput" min="5" max="120" step="1" value="30">
161
+ <p class="form-hint">music-01 支持 5-120 秒</p>
162
+ </div>
163
+ </div>
164
+
165
+ <!-- LLM 专用字段 -->
166
+ <div class="llm-only-fields">
167
+ <div class="form-group form-group-small">
168
+ <label>温度</label>
169
+ <input type="number" id="temperatureInput" value="0.7" min="0" max="2" step="0.1">
170
+ </div>
171
+ </div>
172
+
173
+ <div class="form-group">
174
+ <button type="button" class="test-btn" id="testBtn" onclick="testConnection()">
175
+ ⚡ 测试连接
176
+ </button>
177
+ <div class="test-result" id="testResult" style="display: none;"></div>
178
+ </div>
179
+
180
+ <div class="form-actions">
181
+ <button type="button" class="btn-cancel" onclick="closeModal()">取消</button>
182
+ <button type="submit" class="btn-save">保存</button>
183
+ </div>
184
+ </form>
185
+ </div>
186
+ </div>
187
+ </div>
188
+
189
+ <script>
190
+ // 当前 tab
191
+ let currentTab = 'llm';
192
+
193
+ // 缓存三份配置
194
+ let llmConfigData = null;
195
+ let videoConfigData = null;
196
+ let audioConfigData = null;
197
+ let currentProvider = null;
198
+ let currentProviderType = 'llm'; // 'llm' | 'video' | 'audio'
199
+
200
+ // ==================== Tab 切换 ====================
201
+ function switchTab(tab) {
202
+ currentTab = tab;
203
+ document.querySelectorAll('.api-tab').forEach(btn => {
204
+ btn.classList.toggle('active', btn.dataset.tab === tab);
205
+ });
206
+ document.querySelectorAll('.api-panel').forEach(panel => {
207
+ panel.style.display = panel.dataset.panel === tab ? 'block' : 'none';
208
+ });
209
+ // 切换 tab 时更新计数 badge
210
+ updateCountBadge();
211
+ }
212
+
213
+ function updateCountBadge() {
214
+ const data = currentTab === 'llm' ? llmConfigData :
215
+ currentTab === 'video' ? videoConfigData : audioConfigData;
216
+ if (!data) {
217
+ document.getElementById('configCount').textContent = '加载中...';
218
+ return;
219
+ }
220
+ let configured = 0, total = 0;
221
+ for (const key in data.providers) {
222
+ total++;
223
+ const p = data.providers[key];
224
+ if (currentTab === 'llm') {
225
+ if (p.enabled && p.apiKey) configured++;
226
+ } else {
227
+ const info = data.providerInfo[key] || {};
228
+ if (p.enabled && (!info.requiresApiKey || p.apiKey)) configured++;
229
+ }
230
+ }
231
+ const labelMap = { llm: '', video: '视频', audio: '音频' };
232
+ const label = labelMap[currentTab] || '';
233
+ document.getElementById('configCount').textContent = configured + '/' + total + ' ' + label + '已配置';
234
+ }
235
+
236
+ // ==================== 加载 ====================
237
+ async function loadAll() {
238
+ await Promise.all([loadLLMConfig(), loadVideoConfig(), loadAudioConfig()]);
239
+ }
240
+
241
+ async function loadLLMConfig() {
242
+ try {
243
+ const resp = await fetch('/api/llm-config');
244
+ llmConfigData = await resp.json();
245
+ renderProviders('llm');
246
+ if (currentTab === 'llm') updateCountBadge();
247
+ } catch (err) {
248
+ document.getElementById('providerList').innerHTML =
249
+ '<div class="error-state">加载失败: ' + err.message + '</div>';
250
+ }
251
+ }
252
+
253
+ async function loadVideoConfig() {
254
+ try {
255
+ const resp = await fetch('/api/video-config');
256
+ videoConfigData = await resp.json();
257
+ renderProviders('video');
258
+ if (currentTab === 'video') updateCountBadge();
259
+ } catch (err) {
260
+ document.getElementById('videoProviderList').innerHTML =
261
+ '<div class="error-state">加载失败: ' + err.message + '</div>';
262
+ }
263
+ }
264
+
265
+ async function loadAudioConfig() {
266
+ try {
267
+ const resp = await fetch('/api/audio-config');
268
+ audioConfigData = await resp.json();
269
+ renderProviders('audio');
270
+ if (currentTab === 'audio') updateCountBadge();
271
+ } catch (err) {
272
+ document.getElementById('audioProviderList').innerHTML =
273
+ '<div class="error-state">加载失败: ' + err.message + '</div>';
274
+ }
275
+ }
276
+
277
+ // ==================== 渲染 ====================
278
+ function renderProviders(type) {
279
+ const data = type === 'llm' ? llmConfigData :
280
+ type === 'video' ? videoConfigData : audioConfigData;
281
+ const listEl = document.getElementById(
282
+ type === 'llm' ? 'providerList' :
283
+ type === 'video' ? 'videoProviderList' : 'audioProviderList'
284
+ );
285
+ if (!data) return;
286
+
287
+ const providers = data.providers;
288
+ const info = data.providerInfo;
289
+ const isActive = (key) => data.activeProvider === key;
290
+
291
+ let html = '';
292
+ for (const key in providers) {
293
+ const p = providers[key];
294
+ const i = info[key] || { name: key, description: '供应商', requiresApiKey: true };
295
+ const active = isActive(key);
296
+
297
+ let statusText, statusClass;
298
+ if (active) {
299
+ statusText = '使用中';
300
+ statusClass = 'status-accent';
301
+ } else if (p.enabled && p.apiKey) {
302
+ statusText = '已启用';
303
+ statusClass = 'status-green';
304
+ } else if (p.enabled) {
305
+ statusText = '已启用';
306
+ statusClass = 'status-yellow';
307
+ } else {
308
+ statusText = '未启用';
309
+ statusClass = 'status-muted';
310
+ }
311
+
312
+ const isConfigured = p.enabled && p.apiKey;
313
+
314
+ html += `
315
+ <div class="provider-card ${active ? 'active' : ''}" onclick="openModal('${type}', '${key}')">
316
+ <div class="provider-header">
317
+ <div class="provider-icon">${i.name.charAt(0).toUpperCase()}</div>
318
+ <div class="provider-info">
319
+ <h3 class="provider-name">${i.name}</h3>
320
+ <p class="provider-desc">${i.description}</p>
321
+ ${i.docs ? '<a class="provider-docs" href="' + i.docs + '" target="_blank" onclick="event.stopPropagation()">📖 文档</a>' : ''}
322
+ </div>
323
+ <div class="status-badge ${statusClass}">
324
+ <span class="status-dot"></span>
325
+ ${statusText}
326
+ </div>
327
+ </div>
328
+ <div class="provider-footer">
329
+ <div class="config-status">
330
+ <span class="status-icon ${isConfigured ? 'configured' : ''}">${isConfigured ? '✓' : '○'}</span>
331
+ ${isConfigured ? '已配置 API Key' : '未配置 API Key'}
332
+ </div>
333
+ ${p.model ? '<div class="model-info">模型: ' + p.model + '</div>' : ''}
334
+ <div class="arrow-indicator">›</div>
335
+ </div>
336
+ </div>
337
+ `;
338
+ }
339
+ listEl.innerHTML = html;
340
+ }
341
+
342
+ // ==================== 弹窗 ====================
343
+ function openModal(type, providerKey) {
344
+ currentProviderType = type;
345
+ currentProvider = providerKey;
346
+ const data = type === 'llm' ? llmConfigData :
347
+ type === 'video' ? videoConfigData : audioConfigData;
348
+ const p = data.providers[providerKey];
349
+ const i = data.providerInfo[providerKey] || { name: providerKey, description: '供应商' };
350
+
351
+ document.getElementById('modalIcon').textContent = i.name.charAt(0).toUpperCase();
352
+ document.getElementById('modalTitle').textContent = '配置 ' + i.name;
353
+ document.getElementById('modalSubtitle').textContent = i.description;
354
+
355
+ document.getElementById('apiKeyInput').value = '';
356
+ document.getElementById('apiKeyHint').textContent = p.apiKey
357
+ ? '当前已配置 (***' + (p.apiKey.slice(-4) || '') + '),输入新值以更新'
358
+ : '输入 API Key';
359
+ document.getElementById('baseUrlInput').value = p.baseUrl || '';
360
+ document.getElementById('modelInput').value = p.model || '';
361
+
362
+ // 填充模型下拉建议(仅 LLM 类型有 providerInfo.models)
363
+ const datalist = document.getElementById('modelSuggestList');
364
+ datalist.innerHTML = '';
365
+ if (type === 'llm' && i.models && Array.isArray(i.models)) {
366
+ for (const m of i.models) {
367
+ const opt = document.createElement('option');
368
+ opt.value = m;
369
+ datalist.appendChild(opt);
370
+ }
371
+ }
372
+
373
+ // 视频/音频/LLM 专用字段显隐
374
+ const isVideo = type === 'video';
375
+ const isAudio = type === 'audio';
376
+ document.querySelector('.video-only-fields').style.display = isVideo ? 'block' : 'none';
377
+ document.querySelector('.audio-only-fields').style.display = isAudio ? 'block' : 'none';
378
+ document.querySelector('.llm-only-fields').style.display = (!isVideo && !isAudio) ? 'block' : 'none';
379
+
380
+ if (isVideo) {
381
+ document.getElementById('resolutionInput').value = p.resolution || '720p';
382
+ document.getElementById('ratioInput').value = p.ratio || '16:9';
383
+ document.getElementById('durationInput').value = p.duration || 5;
384
+ document.getElementById('modelHint').textContent = '示例: doubao-seedance-1-0-lite-t2v-250428 (文生视频) 或 ...-i2v-... (图生视频)';
385
+ } else if (isAudio) {
386
+ const isMusic = (i.kind === 'music');
387
+ // TTS 字段
388
+ document.getElementById('voiceInput').parentElement.style.display = isMusic ? 'none' : 'block';
389
+ document.getElementById('speedInput').parentElement.parentElement.style.display = isMusic ? 'none' : 'flex';
390
+ document.getElementById('modeInput').parentElement.style.display = isMusic ? 'block' : 'none';
391
+ document.getElementById('audioDurationInput').parentElement.style.display = isMusic ? 'block' : 'none';
392
+
393
+ if (!isMusic) {
394
+ document.getElementById('voiceInput').value = p.voice || 'male-qn-jingying';
395
+ document.getElementById('voiceHint').textContent = '常用音色: male-qn-jingying, female-shaonv, female-yujie, presenter_male, presenter_female';
396
+ document.getElementById('speedInput').value = p.speed || 1.0;
397
+ document.getElementById('formatInput').value = p.format || 'mp3';
398
+ } else {
399
+ document.getElementById('modeInput').value = p.mode || 'instrumental';
400
+ document.getElementById('audioDurationInput').value = p.duration || 30;
401
+ }
402
+ document.getElementById('modelHint').textContent = isMusic
403
+ ? '示例: music-01'
404
+ : '示例: speech-01 (TTS) / asr-01 (语音转写)';
405
+ } else {
406
+ document.getElementById('temperatureInput').value = p.temperature || 0.7;
407
+ }
408
+
409
+ document.getElementById('testResult').style.display = 'none';
410
+ document.getElementById('configModal').style.display = 'flex';
411
+ }
412
+
413
+ function closeModal() {
414
+ document.getElementById('configModal').style.display = 'none';
415
+ currentProvider = null;
416
+ }
417
+
418
+ // ==================== 测试连接 ====================
419
+ async function testConnection() {
420
+ const btn = document.getElementById('testBtn');
421
+ const result = document.getElementById('testResult');
422
+
423
+ btn.disabled = true;
424
+ btn.innerHTML = '<span class="spinner"></span> 测试中...';
425
+ result.style.display = 'none';
426
+
427
+ const endpoint = currentProviderType === 'llm' ? '/api/llm-test' :
428
+ currentProviderType === 'video' ? '/api/video-test' :
429
+ '/api/audio-test';
430
+
431
+ try {
432
+ const resp = await fetch(endpoint, {
433
+ method: 'POST',
434
+ headers: { 'Content-Type': 'application/json' },
435
+ body: JSON.stringify({ provider: currentProvider })
436
+ });
437
+ const data = await resp.json();
438
+
439
+ result.className = 'test-result ' + (data.success ? 'test-success' : 'test-error');
440
+ result.innerHTML = '<span>' + (data.success ? '✓' : '✗') + '</span> ' +
441
+ (data.success ? '连接成功!延迟: ' + data.latency + 'ms' : (data.error || '连接失败'));
442
+ result.style.display = 'flex';
443
+ } catch (err) {
444
+ result.className = 'test-result test-error';
445
+ result.innerHTML = '<span>✗</span> ' + err.message;
446
+ result.style.display = 'flex';
447
+ }
448
+
449
+ btn.disabled = false;
450
+ btn.innerHTML = '⚡ 测试连接';
451
+ }
452
+
453
+ // ==================== 保存 ====================
454
+ document.getElementById('configForm').onsubmit = async function(e) {
455
+ e.preventDefault();
456
+
457
+ const data = currentProviderType === 'llm' ? llmConfigData : videoConfigData;
458
+ const existing = data.providers[currentProvider];
459
+ const apiKeyVal = document.getElementById('apiKeyInput').value;
460
+
461
+ const updateData = {
462
+ enabled: true,
463
+ apiKey: apiKeyVal || existing.apiKey || '',
464
+ baseUrl: document.getElementById('baseUrlInput').value || existing.baseUrl || '',
465
+ model: document.getElementById('modelInput').value || existing.model || ''
466
+ };
467
+
468
+ if (currentProviderType === 'video') {
469
+ updateData.resolution = document.getElementById('resolutionInput').value;
470
+ updateData.ratio = document.getElementById('ratioInput').value;
471
+ updateData.duration = parseInt(document.getElementById('durationInput').value) || 5;
472
+ } else if (currentProviderType === 'audio') {
473
+ const info = audioConfigData.providerInfo[currentProvider] || {};
474
+ const isMusic = info.kind === 'music';
475
+ if (isMusic) {
476
+ updateData.mode = document.getElementById('modeInput').value;
477
+ updateData.duration = parseInt(document.getElementById('audioDurationInput').value) || 30;
478
+ } else {
479
+ updateData.voice = document.getElementById('voiceInput').value;
480
+ updateData.speed = parseFloat(document.getElementById('speedInput').value) || 1.0;
481
+ updateData.format = document.getElementById('formatInput').value;
482
+ }
483
+ } else {
484
+ updateData.temperature = parseFloat(document.getElementById('temperatureInput').value) || 0.7;
485
+ }
486
+
487
+ const endpoint = currentProviderType === 'llm' ? '/api/llm-config' :
488
+ currentProviderType === 'video' ? '/api/video-config' :
489
+ '/api/audio-config';
490
+
491
+ try {
492
+ await fetch(endpoint, {
493
+ method: 'POST',
494
+ headers: { 'Content-Type': 'application/json' },
495
+ body: JSON.stringify({ provider: currentProvider, config: updateData })
496
+ });
497
+
498
+ const saveBtn = this.querySelector('.btn-save');
499
+ saveBtn.textContent = '✓ 已保存';
500
+ setTimeout(() => {
501
+ closeModal();
502
+ if (currentProviderType === 'llm') loadLLMConfig();
503
+ else if (currentProviderType === 'video') loadVideoConfig();
504
+ else loadAudioConfig();
505
+ }, 500);
506
+ } catch (err) {
507
+ alert('保存失败: ' + err.message);
508
+ }
509
+ };
510
+
511
+ // ==================== 关闭弹窗 ====================
512
+ document.getElementById('configModal').onclick = function(e) {
513
+ if (e.target === this) closeModal();
514
+ };
515
+
516
+ // 启动
517
+ loadAll();
518
+ </script>
519
+ </body>
520
+ </html>