@bolloon/bolloon-agent 0.2.8 → 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.
- package/README.md +467 -467
- package/bin/bolloon-cli.cjs +183 -183
- package/bin/bolloon.cjs +0 -0
- package/bin/bolloon.js +157 -0
- package/dist/agents/agent-manifest-protocol.js +0 -52
- package/dist/agents/constraint-layer.js +19 -19
- package/dist/agents/pi-sdk.js +287 -1213
- package/dist/agents/shell-guard.js +2 -2
- package/dist/agents/workflow-pivot-loop.js +2 -81
- package/dist/bollharness/src/scripts/deploy-guard.js +8 -8
- package/dist/bollharness-integration/context-router-judgment.js +4 -4
- package/dist/bollharness-integration/context-router.js +292 -292
- package/dist/bollharness-integration/gate-state-machine.js +13 -26
- package/dist/bollharness-integration/integration.js +0 -71
- package/dist/bollharness-integration/llm-judgment-engine.js +134 -134
- package/dist/bollharness-integration/skill-adapter.js +127 -52
- package/dist/bootstrap/context-collector.js +3 -6
- package/dist/bootstrap/lifecycle-hooks.js +1 -15
- package/dist/cli-entry.js +35 -39
- package/dist/constraint-runtime/src/_archive_helper.js +9 -0
- package/dist/constraint-runtime/src/agent/coordinator.js +48 -0
- package/dist/constraint-runtime/src/agent/index.js +1 -0
- package/dist/constraint-runtime/src/assistant/index.js +12 -0
- package/dist/constraint-runtime/src/bootstrap/index.js +12 -0
- package/dist/constraint-runtime/src/bootstrap_graph.js +13 -0
- package/dist/constraint-runtime/src/bridge/index.js +12 -0
- package/dist/constraint-runtime/src/buddy/index.js +12 -0
- package/dist/constraint-runtime/src/cli/index.js +12 -0
- package/dist/constraint-runtime/src/command_graph.js +10 -0
- package/dist/constraint-runtime/src/commands.js +60 -0
- package/dist/constraint-runtime/src/components/index.js +12 -0
- package/dist/constraint-runtime/src/constants/index.js +12 -0
- package/dist/constraint-runtime/src/constraint/budget.js +22 -0
- package/dist/constraint-runtime/src/constraint/index.js +2 -0
- package/dist/constraint-runtime/src/constraint/permission.js +20 -0
- package/dist/constraint-runtime/src/context.js +30 -0
- package/dist/constraint-runtime/src/coordinator/index.js +12 -0
- package/dist/constraint-runtime/src/cost_hook.js +4 -0
- package/dist/constraint-runtime/src/cost_tracker.js +8 -0
- package/dist/constraint-runtime/src/deferred_init.js +10 -0
- package/dist/constraint-runtime/src/direct_modes.js +6 -0
- package/dist/constraint-runtime/src/dynamic-tool-loader.js +85 -0
- package/dist/constraint-runtime/src/entrypoints/index.js +12 -0
- package/dist/constraint-runtime/src/execution_registry.js +44 -0
- package/dist/constraint-runtime/src/history.js +9 -0
- package/dist/constraint-runtime/src/hooks/index.js +12 -0
- package/dist/constraint-runtime/src/index.js +26 -0
- package/dist/constraint-runtime/src/ink.js +4 -0
- package/dist/constraint-runtime/src/keybindings/index.js +12 -0
- package/dist/constraint-runtime/src/memdir/index.js +12 -0
- package/dist/constraint-runtime/src/migrations/index.js +12 -0
- package/dist/constraint-runtime/src/models.js +1 -0
- package/dist/constraint-runtime/src/moreright/index.js +12 -0
- package/dist/constraint-runtime/src/native_ts/index.js +12 -0
- package/dist/constraint-runtime/src/output_styles/index.js +12 -0
- package/dist/constraint-runtime/src/parity_audit.js +12 -0
- package/dist/constraint-runtime/src/plugins/index.js +12 -0
- package/dist/constraint-runtime/src/port_manifest.js +11 -0
- package/dist/constraint-runtime/src/prefetch.js +9 -0
- package/dist/constraint-runtime/src/query.js +1 -0
- package/dist/constraint-runtime/src/remote/index.js +12 -0
- package/dist/constraint-runtime/src/remote_runtime.js +9 -0
- package/dist/constraint-runtime/src/runtime/index.js +1 -0
- package/dist/constraint-runtime/src/runtime/session.js +35 -0
- package/dist/constraint-runtime/src/schemas/index.js +12 -0
- package/dist/constraint-runtime/src/screens/index.js +12 -0
- package/dist/constraint-runtime/src/server/index.js +12 -0
- package/dist/constraint-runtime/src/services/index.js +12 -0
- package/dist/constraint-runtime/src/session_store.js +22 -0
- package/dist/constraint-runtime/src/setup.js +30 -0
- package/dist/constraint-runtime/src/skills/index.js +1 -0
- package/dist/constraint-runtime/src/skills/skill-registry.js +28 -0
- package/dist/constraint-runtime/src/state/index.js +12 -0
- package/dist/constraint-runtime/src/system_init.js +20 -0
- package/dist/constraint-runtime/src/thinking/engine.js +42 -0
- package/dist/constraint-runtime/src/thinking/index.js +1 -0
- package/dist/constraint-runtime/src/tool_pool.js +8 -0
- package/dist/constraint-runtime/src/tools/OpenCLI/execAdapter.js +7 -0
- package/dist/constraint-runtime/src/tools/OpenCLI/listAdapters.js +7 -0
- package/dist/constraint-runtime/src/tools/OpenCLI/runCommand.js +7 -0
- package/dist/constraint-runtime/src/tools/SafeSDK/confirmTransaction.js +6 -0
- package/dist/constraint-runtime/src/tools/SafeSDK/createTransaction.js +8 -0
- package/dist/constraint-runtime/src/tools/SafeSDK/executeTransaction.js +6 -0
- package/dist/constraint-runtime/src/tools/SafeSDK/getBalance.js +6 -0
- package/dist/constraint-runtime/src/tools/SafeSDK/getPendingTransactions.js +6 -0
- package/dist/constraint-runtime/src/tools/SafeSDK/proposeTransaction.js +6 -0
- package/dist/constraint-runtime/src/tools.js +80 -0
- package/dist/constraint-runtime/src/transcript.js +19 -0
- package/dist/constraint-runtime/src/types/index.js +12 -0
- package/dist/constraint-runtime/src/upstream_proxy/index.js +12 -0
- package/dist/constraint-runtime/src/utils/index.js +12 -0
- package/dist/constraint-runtime/src/vim/index.js +12 -0
- package/dist/constraint-runtime/src/voice/index.js +12 -0
- package/dist/constraint-runtime/tests/agent.test.js +16 -0
- package/dist/constraint-runtime/tests/constraint.test.js +41 -0
- package/dist/constraint-runtime/tests/skill.test.js +19 -0
- package/dist/constraint-runtime/tests/thinking.test.js +22 -0
- package/dist/context-compaction/auto-compact.js +7 -7
- package/dist/electron-preload.js +1 -18
- package/dist/electron-preload.js.map +1 -1
- package/dist/electron.js +168 -3
- package/dist/electron.js.map +1 -1
- package/dist/index.js +116 -136
- package/dist/llm/config-store.js +8 -24
- package/dist/llm/llm-judgment-client.js +102 -102
- package/dist/llm/pi-ai.js +85 -126
- package/dist/llm/system-prompt/layers/channel/local.md +14 -14
- package/dist/llm/system-prompt/layers/channel/p2p-agent.md +18 -18
- package/dist/llm/system-prompt/layers/channel/p2p-visitor.md +19 -19
- package/dist/llm/system-prompt/layers/core/artifacts_storage.md +89 -89
- package/dist/llm/system-prompt/layers/core/evenhandedness.md +21 -21
- package/dist/llm/system-prompt/layers/core/hibs_reminders.md +15 -15
- package/dist/llm/system-prompt/layers/core/identity.md +37 -37
- package/dist/llm/system-prompt/layers/core/knowledge.md +17 -17
- package/dist/llm/system-prompt/layers/core/memory_system.md +12 -12
- package/dist/llm/system-prompt/layers/core/network_filesystem.md +28 -28
- package/dist/llm/system-prompt/layers/core/refusal.md +37 -37
- package/dist/llm/system-prompt/layers/core/tone.md +31 -31
- package/dist/llm/system-prompt/layers/core/tools.thin.md +13 -13
- package/dist/llm/system-prompt/layers/core/wellbeing.md +41 -41
- package/dist/llm/system-prompt/layers/role/architect.md +20 -20
- package/dist/llm/system-prompt/layers/role/expert.md +19 -19
- package/dist/llm/system-prompt/layers/role/implementer.md +15 -15
- package/dist/llm/system-prompt/layers/role/security.md +15 -15
- package/dist/llm/system-prompt/layers/tool/artifacts.md +72 -72
- package/dist/llm/system-prompt/layers/tool/bash.md +25 -25
- package/dist/llm/system-prompt/layers/tool/hibs_api.md +171 -171
- package/dist/llm/system-prompt/layers/tool/image_search.md +70 -70
- package/dist/llm/system-prompt/layers/tool/manifest.md +89 -89
- package/dist/llm/system-prompt/layers/tool/mcp_apps.md +53 -53
- package/dist/llm/system-prompt/layers/tool/web_search.md +83 -83
- package/dist/llm/tool-manifest/ask_user_input.js +1 -1
- package/dist/llm/tool-manifest/bash.js +3 -3
- package/dist/llm/tool-manifest/create_file.js +4 -4
- package/dist/llm/tool-manifest/fetch_sports_data.js +3 -3
- package/dist/llm/tool-manifest/image_search.js +2 -2
- package/dist/llm/tool-manifest/mcp.js +2 -2
- package/dist/llm/tool-manifest/message_compose.js +3 -3
- package/dist/llm/tool-manifest/places.js +2 -2
- package/dist/llm/tool-manifest/present_files.js +1 -1
- package/dist/llm/tool-manifest/recipe.js +2 -2
- package/dist/llm/tool-manifest/str_replace.js +5 -5
- package/dist/llm/tool-manifest/view.js +3 -3
- package/dist/llm/tool-manifest/weather.js +4 -4
- package/dist/llm/tool-manifest/web.js +4 -4
- package/dist/pi-ecosystem-colony/index.js +365 -0
- package/dist/pi-ecosystem-judgment/causal-judge.js +51 -51
- package/dist/pi-ecosystem-judgment/distill-prompt.js +42 -42
- package/dist/pi-ecosystem-judgment/distillation.js +14 -14
- package/dist/pi-ecosystem-judgment/monitor-gate.js +28 -28
- package/dist/pi-ecosystem-judgment/value-injection.js +102 -102
- package/dist/security/context-router-tool.js +15 -15
- package/dist/security/tool-gate.js +0 -11
- package/dist/social/ant-colony/AdaptiveHeartbeat.js +101 -0
- package/dist/social/ant-colony/PheromoneEngine.js +227 -0
- package/dist/social/ant-colony/index.js +6 -0
- package/dist/social/ant-colony/types.js +24 -0
- package/dist/test/ai-judgment-test.js +80 -0
- package/dist/test/bollharness-integration.test.js +318 -0
- package/dist/test/channel-agent-multi-dialogue.js +205 -0
- package/dist/test/channel-heartbeat-agent-test.js +201 -0
- package/dist/test/constraint-layer.test.js +164 -0
- package/dist/test/diap-identity-test.js +172 -0
- package/dist/test/diap-quick-test.js +62 -0
- package/dist/test/global-shared-context.test.js +315 -0
- package/dist/test/harness-judgment-injection.test.js +246 -0
- package/dist/test/harness-workflow-integrator-test.js +228 -0
- package/dist/test/human-value-store.test.js +243 -0
- package/dist/test/hybrid-integration-test.js +118 -0
- package/dist/test/hybrid-messenger-verify.js +55 -0
- package/dist/test/iroh-bistream-debug.js +38 -0
- package/dist/test/iroh-communication.test.js +66 -0
- package/dist/test/iroh-debug-test.js +57 -0
- package/dist/test/iroh-diap-test.js +71 -0
- package/dist/test/iroh-direct-connect.js +55 -0
- package/dist/test/iroh-e2e-fixed.js +89 -0
- package/dist/test/iroh-e2e-same-process.js +63 -0
- package/dist/test/iroh-e2e.js +66 -0
- package/dist/test/iroh-final-e2e.js +72 -0
- package/dist/test/iroh-relay-test.js +37 -0
- package/dist/test/iroh-simple-test.js +41 -0
- package/dist/test/iroh-transport-verify.js +54 -0
- package/dist/test/iroh-transport.test.js +37 -0
- package/dist/test/iroh-two-nodes.js +70 -0
- package/dist/test/iroh-verify.js +44 -0
- package/dist/test/judgment-decision.test.js +219 -0
- package/dist/test/llm-judgment-integration.test.js +220 -0
- package/dist/test/p2p-agent-complex-dialogue.js +385 -0
- package/dist/test/p2p-agent-dialogue.js +341 -0
- package/dist/test/p2p-agent-full-bidirectional.js +510 -0
- package/dist/test/p2p-agent-harness-flow.js +437 -0
- package/dist/test/p2p-agent-harness-single.js +143 -0
- package/dist/test/p2p-ai-dialogue-test.js +318 -0
- package/dist/test/p2p-cid-connect-test.js +195 -0
- package/dist/test/p2p-connect-receiver.js +69 -0
- package/dist/test/p2p-doc-transfer.js +110 -0
- package/dist/test/p2p-identity-page-test.js +77 -0
- package/dist/test/p2p-iroh-test.js +171 -0
- package/dist/test/p2p-minimal-test.js +241 -0
- package/dist/test/p2p-node-1.js +148 -0
- package/dist/test/p2p-node-2.js +148 -0
- package/dist/test/p2p-server.js +281 -0
- package/dist/test/p2p-two-nodes-test.js +438 -0
- package/dist/test/pi-sdk.test.js +44 -0
- package/dist/test/set-persona.js +40 -0
- package/dist/test/simple.test.js +9 -0
- package/dist/test/storage-integration.test.js +150 -0
- package/dist/test/subagent-manager.test.js +276 -0
- package/dist/test/test-gate-flow.test.js +81 -0
- package/dist/test/workflow-engine.test.js +87 -0
- package/dist/test/workflow-pivot-loop.test.js +246 -0
- package/dist/web/api-config.html +520 -520
- package/dist/web/client.js +2917 -3791
- package/dist/web/components/p2p/index.js +234 -276
- package/dist/web/components/wallet-viem.mjs +118 -118
- package/dist/web/index.html +374 -373
- package/dist/web/manifest.json +20 -20
- package/dist/web/server.js +23 -775
- package/dist/web/style.css +4737 -4737
- package/dist/web/ui/message-renderer.js +373 -451
- package/dist/web/ui/step-timeline.js +255 -351
- package/package.json +160 -157
- package/scripts/build-cli.js +216 -0
- package/scripts/build-web.ts +125 -0
- package/scripts/postinstall.js +153 -0
- package/bin/bolloon-daemon.sh +0 -207
- package/bin/ipfs +0 -0
- package/dist/agents/chat-segmenter.js +0 -298
- package/dist/agents/judgment-protocol.js +0 -479
- package/dist/agents/parse-tool-call.js +0 -304
- package/dist/agents/peer-manifest-loader.js +0 -210
- package/dist/agents/react-loop.js +0 -120
- package/dist/agents/session-store.js +0 -171
- package/dist/agents/tool-registry.js +0 -136
- package/dist/bootstrap/chat-archiver.js +0 -276
- package/dist/bootstrap/memory-compressor.js +0 -170
- package/dist/bootstrap/persona-loader.js +0 -94
- package/dist/electron/config.js +0 -16
- package/dist/electron/config.js.map +0 -1
- package/dist/electron/dialogs.js +0 -71
- package/dist/electron/dialogs.js.map +0 -1
- package/dist/electron/first-run.js +0 -129
- package/dist/electron/first-run.js.map +0 -1
- package/dist/electron/ipc.js +0 -16
- package/dist/electron/ipc.js.map +0 -1
- package/dist/electron/logger.js +0 -77
- package/dist/electron/logger.js.map +0 -1
- package/dist/electron/main.js +0 -60
- package/dist/electron/main.js.map +0 -1
- package/dist/electron/menu.js +0 -140
- package/dist/electron/menu.js.map +0 -1
- package/dist/electron/paths.js +0 -33
- package/dist/electron/paths.js.map +0 -1
- package/dist/electron/server.js +0 -74
- package/dist/electron/server.js.map +0 -1
- package/dist/electron/tray.js +0 -73
- package/dist/electron/tray.js.map +0 -1
- package/dist/electron/window.js +0 -69
- package/dist/electron/window.js.map +0 -1
- package/dist/network/local-inbox-bus.js +0 -73
- package/dist/network/p2p-outbox.js +0 -161
- package/dist/network/peer-fs.js +0 -420
- package/dist/network/peer-resource-bridge.js +0 -216
- package/dist/web/components/p2p/P2PModal.js +0 -188
- package/dist/web/components/p2p/p2p-modal.js +0 -664
- package/dist/web/components/p2p/p2p-tools.js +0 -248
- package/dist/web/input-validator.js +0 -103
- package/dist/web/util/safe-name.js +0 -32
package/dist/web/api-config.html
CHANGED
|
@@ -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>
|