fix: Windows-Start.bat 启动修复 + Config Center 全面升级
Windows-Start.bat: - 去掉 EnableDelayedExpansion,避免感叹号解析问题 - 用 config-server (18788端口) 替代直接打开 Config.html - 去掉有 bug 的 token 读取(引号嵌套问题) - 去掉 powershell 日志管道,简化启动流程 - 对齐 H 盘已验证可用的方案 Config Center (config-server/public/index.html): - 步骤式引导:选模型 → 填 Key → 启动 - 11 个模型:MiniMax/Kimi/DeepSeek/智谱GLM/通义千问/豆包/OpenAI/Claude/Groq/硅基流动/自定义 - 4 个聊天平台:Telegram/QQ/飞书/企业微信 - Gateway 状态实时检测 - 正确的 OpenClaw v3 配置格式 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,4 @@
|
|||||||
@echo off
|
@echo off
|
||||||
setlocal EnableDelayedExpansion
|
|
||||||
chcp 65001 >nul 2>&1
|
chcp 65001 >nul 2>&1
|
||||||
title U-Claw - Portable AI Agent
|
title U-Claw - Portable AI Agent
|
||||||
|
|
||||||
@@ -76,10 +75,10 @@ REM Find available port
|
|||||||
set PORT=18789
|
set PORT=18789
|
||||||
:check_port
|
:check_port
|
||||||
netstat -an | findstr ":%PORT% " | findstr "LISTENING" >nul 2>&1
|
netstat -an | findstr ":%PORT% " | findstr "LISTENING" >nul 2>&1
|
||||||
if !errorlevel!==0 (
|
if %errorlevel%==0 (
|
||||||
echo Port %PORT% in use, trying next...
|
echo Port %PORT% in use, trying next...
|
||||||
set /a PORT+=1
|
set /a PORT+=1
|
||||||
if !PORT! gtr 18799 (
|
if %PORT% gtr 18799 (
|
||||||
echo No available port 18789-18799
|
echo No available port 18789-18799
|
||||||
pause
|
pause
|
||||||
exit /b 1
|
exit /b 1
|
||||||
@@ -87,40 +86,34 @@ if !errorlevel!==0 (
|
|||||||
goto :check_port
|
goto :check_port
|
||||||
)
|
)
|
||||||
|
|
||||||
REM Read token from config
|
echo Starting OpenClaw on port %PORT%...
|
||||||
set "TOKEN=uclaw"
|
echo.
|
||||||
if exist "%STATE_DIR%\openclaw.json" (
|
|
||||||
for /f "tokens=*" %%t in ('"%NODE_BIN%" -e "try{const c=JSON.parse(require('fs').readFileSync(process.argv[1],'utf8'));console.log((c.gateway&&c.gateway.auth&&c.gateway.auth.token)||'uclaw')}catch(e){console.log('uclaw')}" "%STATE_DIR%\openclaw.json"') do set "TOKEN=%%t"
|
|
||||||
)
|
|
||||||
|
|
||||||
echo Starting OpenClaw on port !PORT!...
|
REM Start Config Server in background
|
||||||
|
echo Starting Config Center on port 18788...
|
||||||
|
set "CONFIG_SERVER=%UCLAW_DIR%config-server"
|
||||||
|
start /B "" "%NODE_BIN%" "%CONFIG_SERVER%\server.js" >nul 2>&1
|
||||||
|
|
||||||
|
REM Wait for config server to start
|
||||||
|
timeout /t 2 /nobreak >nul
|
||||||
|
|
||||||
|
REM Open both Dashboard and Config Center
|
||||||
|
echo Opening Dashboard and Config Center...
|
||||||
|
timeout /t 1 /nobreak >nul
|
||||||
|
|
||||||
|
REM Open OpenClaw Dashboard first
|
||||||
|
start "" http://127.0.0.1:%PORT%/#token=uclaw
|
||||||
|
|
||||||
|
REM Open Config Center (Node.js web UI) second
|
||||||
|
start "" http://127.0.0.1:18788/
|
||||||
|
|
||||||
|
echo Browsers opened. Starting OpenClaw Gateway on port %PORT%...
|
||||||
echo DO NOT close this window while using U-Claw!
|
echo DO NOT close this window while using U-Claw!
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
cd /d "%CORE_DIR%"
|
cd /d "%CORE_DIR%"
|
||||||
set "OPENCLAW_MJS=%CORE_DIR%\node_modules\openclaw\openclaw.mjs"
|
set "OPENCLAW_MJS=%CORE_DIR%\node_modules\openclaw\openclaw.mjs"
|
||||||
echo Log: %DATA_DIR%\logs\gateway.log
|
"%NODE_BIN%" "%OPENCLAW_MJS%" gateway run --allow-unconfigured --force --port %PORT%
|
||||||
|
|
||||||
REM Check if model is configured
|
|
||||||
set "HAS_MODEL=no"
|
|
||||||
if exist "%STATE_DIR%\openclaw.json" (
|
|
||||||
findstr /c:"model" "%STATE_DIR%\openclaw.json" >nul 2>&1 && set "HAS_MODEL=yes"
|
|
||||||
)
|
|
||||||
if "!HAS_MODEL!"=="no" if exist "%DATA_DIR%\config.json" (
|
|
||||||
findstr /c:"model" "%DATA_DIR%\config.json" >nul 2>&1 && set "HAS_MODEL=yes"
|
|
||||||
)
|
|
||||||
|
|
||||||
REM Open browser after a short delay (gateway needs ~2-3s to start)
|
|
||||||
if "!HAS_MODEL!"=="yes" (
|
|
||||||
echo Opening dashboard in 3 seconds...
|
|
||||||
start /B "" cmd /c "timeout /t 3 /nobreak >nul && start http://127.0.0.1:!PORT!/#token=!TOKEN!"
|
|
||||||
) else (
|
|
||||||
echo First time setup - opening Config page in 3 seconds...
|
|
||||||
start /B "" cmd /c "timeout /t 3 /nobreak >nul && start %UCLAW_DIR%Config.html?port=!PORT!"
|
|
||||||
)
|
|
||||||
|
|
||||||
REM Start gateway (foreground, blocks until stopped)
|
|
||||||
"%NODE_BIN%" "%OPENCLAW_MJS%" gateway run --allow-unconfigured --force --port !PORT! 2>&1 | powershell -command "& { $input | Tee-Object -Append -FilePath '%DATA_DIR%\logs\gateway.log' }"
|
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo OpenClaw stopped.
|
echo OpenClaw stopped.
|
||||||
|
|||||||
@@ -6,202 +6,619 @@
|
|||||||
<title>U-Claw 配置中心</title>
|
<title>U-Claw 配置中心</title>
|
||||||
<style>
|
<style>
|
||||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
body { font-family: -apple-system, "Microsoft YaHei", sans-serif; background: #0a0a0a; color: #e0e0e0; padding: 20px; }
|
body { font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; background: #0a0a0a; color: #e0e0e0; min-height: 100vh; }
|
||||||
.container { max-width: 600px; margin: 0 auto; }
|
.container { max-width: 720px; margin: 0 auto; padding: 28px 20px 60px; }
|
||||||
h1 { text-align: center; margin-bottom: 10px; }
|
h1 { text-align: center; font-size: 1.8em; margin-bottom: 4px; }
|
||||||
h1 span { color: #ff6b35; }
|
h1 .lobster { color: #ff6b35; }
|
||||||
.subtitle { text-align: center; color: #888; margin-bottom: 30px; font-size: 0.9em; }
|
.subtitle { text-align: center; color: #888; margin-bottom: 24px; font-size: 0.92em; }
|
||||||
|
|
||||||
.section { background: #1a1a1a; border: 1px solid #333; border-radius: 12px; padding: 20px; margin-bottom: 20px; }
|
/* Steps */
|
||||||
.section h2 { color: #ff6b35; font-size: 1.1em; margin-bottom: 10px; }
|
.steps { display: flex; gap: 0; margin-bottom: 24px; background: #1a1a1a; border-radius: 10px; overflow: hidden; border: 1px solid #333; }
|
||||||
|
.step { flex: 1; text-align: center; padding: 12px 8px; font-size: 0.85em; color: #666; cursor: pointer; transition: all 0.2s; }
|
||||||
|
.step.active { color: #ff6b35; background: rgba(255,107,53,0.08); font-weight: 600; }
|
||||||
|
.step.done { color: #4caf50; }
|
||||||
|
.step .num { display: inline-block; width: 22px; height: 22px; line-height: 22px; border-radius: 50%; background: #333; color: #888; font-size: 0.8em; margin-right: 4px; }
|
||||||
|
.step.active .num { background: #ff6b35; color: #fff; }
|
||||||
|
.step.done .num { background: #4caf50; color: #fff; }
|
||||||
|
|
||||||
.form-group { margin-bottom: 15px; }
|
/* Section */
|
||||||
label { display: block; color: #ccc; margin-bottom: 5px; font-size: 0.9em; }
|
.section { background: #1a1a1a; border: 1px solid #333; border-radius: 12px; padding: 24px; margin-bottom: 16px; display: none; }
|
||||||
input, select { width: 100%; padding: 10px; background: #222; border: 1px solid #444; border-radius: 6px; color: #fff; font-size: 0.95em; }
|
.section.active { display: block; }
|
||||||
input:focus, select:focus { outline: none; border-color: #ff6b35; }
|
.section h2 { color: #ff6b35; font-size: 1.15em; margin-bottom: 6px; }
|
||||||
|
.section .desc { color: #888; font-size: 0.88em; margin-bottom: 16px; }
|
||||||
|
|
||||||
.btn { padding: 10px 24px; border: none; border-radius: 8px; font-size: 0.95em; cursor: pointer; }
|
/* Model cards */
|
||||||
|
.model-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 16px; }
|
||||||
|
.model-card { background: #222; border: 2px solid #333; border-radius: 10px; padding: 14px; cursor: pointer; transition: all 0.15s; position: relative; }
|
||||||
|
.model-card:hover { border-color: #555; transform: translateY(-1px); }
|
||||||
|
.model-card.selected { border-color: #ff6b35; background: rgba(255,107,53,0.06); }
|
||||||
|
.model-card h4 { color: #fff; font-size: 0.95em; margin-bottom: 4px; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
|
||||||
|
.model-card p { color: #888; font-size: 0.78em; line-height: 1.4; }
|
||||||
|
.tag { font-size: 0.68em; padding: 2px 7px; border-radius: 10px; font-weight: 500; }
|
||||||
|
.tag.hot { background: rgba(255,107,53,0.2); color: #ff6b35; }
|
||||||
|
.tag.free { background: rgba(76,175,80,0.2); color: #4caf50; }
|
||||||
|
.tag.cn { background: rgba(33,150,243,0.2); color: #2196f3; }
|
||||||
|
.tag.fast { background: rgba(156,39,176,0.2); color: #ce93d8; }
|
||||||
|
.tag.cheap { background: rgba(255,193,7,0.2); color: #ffd54f; }
|
||||||
|
.check { position: absolute; top: 8px; right: 10px; color: #ff6b35; font-size: 1.2em; display: none; }
|
||||||
|
.model-card.selected .check { display: block; }
|
||||||
|
|
||||||
|
/* Buy link */
|
||||||
|
.buy-link { display: inline-flex; align-items: center; gap: 4px; color: #ff6b35; text-decoration: none; font-size: 0.82em; margin-top: 6px; }
|
||||||
|
.buy-link:hover { text-decoration: underline; }
|
||||||
|
|
||||||
|
/* Forms */
|
||||||
|
.form-group { margin-bottom: 16px; }
|
||||||
|
label { display: block; color: #ccc; margin-bottom: 6px; font-size: 0.9em; }
|
||||||
|
input[type="text"], input[type="password"] {
|
||||||
|
width: 100%; padding: 11px 14px; background: #222; border: 1px solid #444; border-radius: 8px;
|
||||||
|
color: #fff; font-size: 0.95em; outline: none; transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
input:focus { border-color: #ff6b35; }
|
||||||
|
.input-wrap { position: relative; }
|
||||||
|
.input-wrap input { padding-right: 44px; }
|
||||||
|
.toggle-pw { position: absolute; right: 12px; top: 50%; transform: translateY(-50%); background: none; border: none; color: #888; cursor: pointer; font-size: 0.85em; }
|
||||||
|
.toggle-pw:hover { color: #ccc; }
|
||||||
|
.hint { color: #888; font-size: 0.8em; margin-top: 6px; line-height: 1.5; }
|
||||||
|
.hint a { color: #ff6b35; text-decoration: none; }
|
||||||
|
.hint a:hover { text-decoration: underline; }
|
||||||
|
|
||||||
|
/* API Key info box */
|
||||||
|
.api-info { background: #1e1e2e; border: 1px solid #333; border-radius: 8px; padding: 14px; margin-bottom: 16px; }
|
||||||
|
.api-info h4 { color: #ff6b35; font-size: 0.9em; margin-bottom: 8px; }
|
||||||
|
.api-info .provider-links { display: flex; flex-direction: column; gap: 6px; }
|
||||||
|
.api-info a { color: #82aaff; text-decoration: none; font-size: 0.85em; display: flex; align-items: center; gap: 6px; }
|
||||||
|
.api-info a:hover { color: #ff6b35; }
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
.btn { display: inline-block; padding: 12px 28px; border: none; border-radius: 8px; font-size: 0.95em; cursor: pointer; transition: all 0.15s; font-weight: 500; }
|
||||||
.btn-primary { background: #ff6b35; color: #fff; }
|
.btn-primary { background: #ff6b35; color: #fff; }
|
||||||
.btn-primary:hover { background: #e55a25; }
|
.btn-primary:hover { background: #e55a25; }
|
||||||
|
.btn-primary:disabled { background: #444; color: #888; cursor: not-allowed; }
|
||||||
|
.btn-secondary { background: #333; color: #ccc; }
|
||||||
|
.btn-secondary:hover { background: #444; }
|
||||||
|
.btn-row { display: flex; gap: 10px; margin-top: 20px; justify-content: flex-end; }
|
||||||
|
|
||||||
.status { padding: 10px; border-radius: 6px; margin-top: 10px; display: none; }
|
/* Result */
|
||||||
.status.success { display: block; background: rgba(76,175,80,0.12); color: #4caf50; border: 1px solid rgba(76,175,80,0.3); }
|
.result { text-align: center; padding: 40px 20px; }
|
||||||
.status.error { display: block; background: rgba(244,67,54,0.12); color: #f44336; border: 1px solid rgba(244,67,54,0.3); }
|
.result .icon { font-size: 3em; margin-bottom: 12px; }
|
||||||
|
.result h2 { color: #4caf50; margin-bottom: 8px; }
|
||||||
|
.result p { color: #888; font-size: 0.92em; line-height: 1.6; }
|
||||||
|
.result .url { color: #ff6b35; font-size: 1.1em; font-weight: 600; margin: 12px 0; }
|
||||||
|
|
||||||
.links { text-align: center; margin-top: 20px; }
|
/* Toast */
|
||||||
.links a { color: #ff6b35; text-decoration: none; margin: 0 10px; }
|
.toast { position: fixed; top: 20px; right: 20px; background: #4caf50; color: #fff; padding: 12px 20px; border-radius: 8px; font-size: 0.9em; opacity: 0; transform: translateY(-10px); transition: all 0.3s; z-index: 999; }
|
||||||
|
.toast.show { opacity: 1; transform: translateY(0); }
|
||||||
|
.toast.error { background: #f44336; }
|
||||||
|
|
||||||
|
/* Channel form */
|
||||||
|
.channel-card { transition: all 0.2s; }
|
||||||
|
.channel-card.selected { border-color: #ff6b35; background: rgba(255,107,53,0.06); }
|
||||||
|
.channel-form { padding-top: 0; margin-top: 10px; border-top: 1px solid #333; max-height: 0; overflow: hidden; transition: max-height 0.3s ease, padding 0.3s ease; }
|
||||||
|
.channel-form.open { max-height: 300px; padding-top: 12px; }
|
||||||
|
.channel-form .form-group { margin-bottom: 10px; }
|
||||||
|
.channel-form label { font-size: 0.82em; color: #aaa; margin-bottom: 4px; }
|
||||||
|
.channel-form input { font-size: 0.88em; padding: 9px 12px; }
|
||||||
|
.channel-form .hint { font-size: 0.75em; margin-top: 4px; }
|
||||||
|
|
||||||
|
/* Status bar */
|
||||||
|
.status-bar {
|
||||||
|
background: #1a1a1a; border: 1px solid #333; border-radius: 8px;
|
||||||
|
padding: 10px 16px; margin-bottom: 16px; display: flex; align-items: center; gap: 10px;
|
||||||
|
}
|
||||||
|
.dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
|
||||||
|
.dot.on { background: #4caf50; animation: pulse 2s infinite; }
|
||||||
|
.dot.off { background: #f44336; }
|
||||||
|
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.5; } }
|
||||||
|
.status-text { flex: 1; font-size: 0.9em; }
|
||||||
|
.status-text a { color: #ff6b35; text-decoration: none; }
|
||||||
|
|
||||||
|
/* Skills section */
|
||||||
|
.skills-section { margin-top: 8px; }
|
||||||
|
.skill-list { display: flex; flex-direction: column; gap: 8px; }
|
||||||
|
.skill-item { background: #222; border: 1px solid #333; border-radius: 8px; padding: 12px; display: flex; align-items: center; justify-content: space-between; }
|
||||||
|
.skill-item .info h4 { color: #fff; font-size: 0.9em; }
|
||||||
|
.skill-item .info p { color: #888; font-size: 0.78em; }
|
||||||
|
.skill-item .status { font-size: 0.8em; color: #4caf50; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>🦞 <span>U-Claw</span> 配置中心</h1>
|
<h1><span class="lobster">🦞</span> U-Claw 配置中心</h1>
|
||||||
<p class="subtitle">简单配置,快速启动</p>
|
<p class="subtitle">选择模型,填入 API Key,一键启动</p>
|
||||||
|
|
||||||
<!-- AI 模型配置 -->
|
<!-- Connection status -->
|
||||||
<div class="section">
|
<div class="status-bar">
|
||||||
<h2>AI 模型配置</h2>
|
<div class="dot" id="statusDot"></div>
|
||||||
<div class="form-group">
|
<div class="status-text" id="statusText">检测中...</div>
|
||||||
<label>Provider</label>
|
|
||||||
<select id="provider">
|
|
||||||
<option value="anthropic">Anthropic (Claude)</option>
|
|
||||||
<option value="openai">OpenAI (GPT)</option>
|
|
||||||
<option value="custom">自定义 (DeepSeek/Kimi/Qwen)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Steps -->
|
||||||
|
<div class="steps">
|
||||||
|
<div class="step active" data-step="1" onclick="goStep(1)"><span class="num">1</span>选模型</div>
|
||||||
|
<div class="step" data-step="2" onclick="goStep(2)"><span class="num">2</span>填 Key</div>
|
||||||
|
<div class="step" data-step="3" onclick="goStep(3)"><span class="num">3</span>启动</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 1: Select Model -->
|
||||||
|
<div class="section active" id="step1">
|
||||||
|
<h2>选择 AI 模型</h2>
|
||||||
|
<p class="desc">推荐国内用户用 MiniMax / Kimi / DeepSeek</p>
|
||||||
|
|
||||||
|
<div class="model-grid">
|
||||||
|
<div class="model-card" data-provider="minimax" data-base="https://api.minimax.chat/v1" data-model="MiniMax-Text-01">
|
||||||
|
<span class="check">✓</span>
|
||||||
|
<h4>MiniMax <span class="tag hot">推荐</span><span class="tag cn">国内</span></h4>
|
||||||
|
<p>MiniMax-Text-01,速度快,性价比高</p>
|
||||||
|
<a class="buy-link" href="https://platform.minimaxi.com/" target="_blank">→ 获取 API Key</a>
|
||||||
|
</div>
|
||||||
|
<div class="model-card" data-provider="kimi" data-base="https://api.moonshot.cn/v1" data-model="moonshot-v1-auto">
|
||||||
|
<span class="check">✓</span>
|
||||||
|
<h4>Kimi (月之暗面) <span class="tag cn">国内</span><span class="tag fast">快</span></h4>
|
||||||
|
<p>moonshot-v1-auto,智能选模型</p>
|
||||||
|
<a class="buy-link" href="https://platform.moonshot.cn/" target="_blank">→ 获取 API Key</a>
|
||||||
|
</div>
|
||||||
|
<div class="model-card" data-provider="deepseek" data-base="https://api.deepseek.com/v1" data-model="deepseek-chat">
|
||||||
|
<span class="check">✓</span>
|
||||||
|
<h4>DeepSeek <span class="tag cn">国内</span><span class="tag cheap">便宜</span></h4>
|
||||||
|
<p>deepseek-chat,超低价格</p>
|
||||||
|
<a class="buy-link" href="https://platform.deepseek.com/" target="_blank">→ 获取 API Key</a>
|
||||||
|
</div>
|
||||||
|
<div class="model-card" data-provider="zai" data-base="" data-model="glm-5">
|
||||||
|
<span class="check">✓</span>
|
||||||
|
<h4>智谱 GLM <span class="tag cn">国内</span><span class="tag free">有免费</span></h4>
|
||||||
|
<p>glm-5(zai provider),免费额度可用</p>
|
||||||
|
<a class="buy-link" href="https://open.bigmodel.cn/" target="_blank">→ 获取 API Key</a>
|
||||||
|
</div>
|
||||||
|
<div class="model-card" data-provider="qwen" data-base="https://dashscope.aliyuncs.com/compatible-mode/v1" data-model="qwen-turbo">
|
||||||
|
<span class="check">✓</span>
|
||||||
|
<h4>通义千问 <span class="tag cn">国内</span><span class="tag free">有免费</span></h4>
|
||||||
|
<p>qwen-turbo,阿里云出品</p>
|
||||||
|
<a class="buy-link" href="https://dashscope.console.aliyun.com/" target="_blank">→ 获取 API Key</a>
|
||||||
|
</div>
|
||||||
|
<div class="model-card" data-provider="doubao" data-base="https://ark.cn-beijing.volces.com/api/v3" data-model="doubao-1.5-pro-32k">
|
||||||
|
<span class="check">✓</span>
|
||||||
|
<h4>豆包 (字节) <span class="tag cn">国内</span><span class="tag fast">快</span></h4>
|
||||||
|
<p>doubao-1.5-pro,字节跳动</p>
|
||||||
|
<a class="buy-link" href="https://console.volcengine.com/ark" target="_blank">→ 获取 API Key</a>
|
||||||
|
</div>
|
||||||
|
<div class="model-card" data-provider="openai" data-base="https://api.openai.com/v1" data-model="gpt-4o">
|
||||||
|
<span class="check">✓</span>
|
||||||
|
<h4>OpenAI <span class="tag hot">强</span></h4>
|
||||||
|
<p>GPT-4o,需翻墙或中转</p>
|
||||||
|
<a class="buy-link" href="https://platform.openai.com/" target="_blank">→ 获取 API Key</a>
|
||||||
|
</div>
|
||||||
|
<div class="model-card" data-provider="anthropic" data-base="https://api.anthropic.com/v1" data-model="claude-sonnet-4-20250514">
|
||||||
|
<span class="check">✓</span>
|
||||||
|
<h4>Claude <span class="tag hot">强</span></h4>
|
||||||
|
<p>Claude Sonnet 4,需翻墙或中转</p>
|
||||||
|
<a class="buy-link" href="https://console.anthropic.com/" target="_blank">→ 获取 API Key</a>
|
||||||
|
</div>
|
||||||
|
<div class="model-card" data-provider="groq" data-base="https://api.groq.com/openai/v1" data-model="llama-3.3-70b-versatile">
|
||||||
|
<span class="check">✓</span>
|
||||||
|
<h4>Groq <span class="tag fast">极快</span><span class="tag free">有免费</span></h4>
|
||||||
|
<p>Llama 3.3 70B,超高速推理</p>
|
||||||
|
<a class="buy-link" href="https://console.groq.com/" target="_blank">→ 获取 API Key</a>
|
||||||
|
</div>
|
||||||
|
<div class="model-card" data-provider="siliconflow" data-base="https://api.siliconflow.cn/v1" data-model="Qwen/Qwen2.5-72B-Instruct">
|
||||||
|
<span class="check">✓</span>
|
||||||
|
<h4>硅基流动 <span class="tag cn">国内</span><span class="tag cheap">便宜</span></h4>
|
||||||
|
<p>多模型聚合,价格低</p>
|
||||||
|
<a class="buy-link" href="https://cloud.siliconflow.cn/" target="_blank">→ 获取 API Key</a>
|
||||||
|
</div>
|
||||||
|
<div class="model-card" data-provider="custom" data-base="" data-model="" style="grid-column: 1 / -1; border-style: dashed;">
|
||||||
|
<span class="check">✓</span>
|
||||||
|
<h4>🔧 自定义 API <span class="tag">OpenAI 兼容</span></h4>
|
||||||
|
<p>填写任意 OpenAI 兼容 API 地址(OpenRouter、API2D、New API、One API 等)</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="btn-row">
|
||||||
|
<button class="btn btn-primary" id="nextStep1" disabled>下一步 →</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 2: API Key -->
|
||||||
|
<div class="section" id="step2">
|
||||||
|
<h2>填写 API Key</h2>
|
||||||
|
<p class="desc" id="step2desc">请填写对应模型的 API Key</p>
|
||||||
|
|
||||||
|
<div class="api-info" id="buyInfo">
|
||||||
|
<h4>📌 如何获取 API Key?</h4>
|
||||||
|
<div class="provider-links" id="providerLinks"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="customFields" style="display:none;">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>API 地址 (Base URL)</label>
|
||||||
|
<input type="text" id="customBase" placeholder="https://api.example.com/v1">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>模型名称</label>
|
||||||
|
<input type="text" id="customModel" placeholder="gpt-4o">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>API Key</label>
|
<label>API Key</label>
|
||||||
<input type="password" id="apiKey" placeholder="输入您的 API Key">
|
<div class="input-wrap">
|
||||||
|
<input type="password" id="apiKey" placeholder="sk-...">
|
||||||
|
<button class="toggle-pw" onclick="togglePw('apiKey')">显示</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" id="baseUrlGroup" style="display:none">
|
<p class="hint">你的 Key 仅保存在本地 U 盘,不会上传到任何服务器</p>
|
||||||
<label>Base URL</label>
|
|
||||||
<input type="text" id="baseUrl" placeholder="https://api.deepseek.com/v1">
|
|
||||||
</div>
|
|
||||||
<div class="form-group" id="modelGroup" style="display:none">
|
|
||||||
<label>Model Name</label>
|
|
||||||
<input type="text" id="modelName" placeholder="deepseek-chat">
|
|
||||||
</div>
|
|
||||||
<button class="btn btn-primary" onclick="saveAI()">保存 AI 配置</button>
|
|
||||||
<div class="status" id="aiStatus"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- QQ Bot 配置 -->
|
<div class="btn-row">
|
||||||
<div class="section">
|
<button class="btn btn-secondary" onclick="goStep(1)">← 返回</button>
|
||||||
<h2>QQ 机器人(可选)</h2>
|
<button class="btn btn-primary" onclick="saveConfig()">保存并继续 →</button>
|
||||||
<div class="form-group">
|
|
||||||
<label>AppID</label>
|
|
||||||
<input type="text" id="qqAppId" placeholder="从 q.qq.com 获取">
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
|
||||||
<label>AppSecret</label>
|
|
||||||
<input type="password" id="qqSecret" placeholder="从 q.qq.com 获取">
|
|
||||||
</div>
|
|
||||||
<button class="btn btn-primary" onclick="saveQQ()">保存 QQ 配置</button>
|
|
||||||
<div class="status" id="qqStatus"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="links">
|
<!-- Step 3: Done -->
|
||||||
<a href="http://127.0.0.1:18789/#token=uclaw" target="_blank">打开 OpenClaw 控制台 →</a>
|
<div class="section" id="step3">
|
||||||
|
<div class="result">
|
||||||
|
<div class="icon">🦞</div>
|
||||||
|
<h2>配置完成!</h2>
|
||||||
|
<p>模型和 API Key 已保存到本地</p>
|
||||||
|
<div class="url" id="gatewayUrl">http://127.0.0.1:18789</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 24px; text-align: left;">
|
||||||
|
<h3 style="color: #ff6b35; font-size: 0.95em; margin-bottom: 12px;">📱 接入聊天平台(可选)</h3>
|
||||||
|
<p style="color: #888; font-size: 0.82em; margin-bottom: 12px;">点击卡片展开填写,不填可直接跳过:</p>
|
||||||
|
<div class="model-grid">
|
||||||
|
<div class="model-card channel-card" onclick="toggleChannel(this, 'telegram')">
|
||||||
|
<h4>✈️ Telegram</h4>
|
||||||
|
<p>接入 Telegram Bot</p>
|
||||||
|
<div class="channel-form" id="form-telegram" onclick="event.stopPropagation()">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Bot Token</label>
|
||||||
|
<input type="text" id="ch-telegram-token" placeholder="123456:ABC-DEF...">
|
||||||
|
<p class="hint"><a href="https://t.me/BotFather" target="_blank">→ 从 @BotFather 获取</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="model-card channel-card" onclick="toggleChannel(this, 'qqbot')">
|
||||||
|
<h4>🐧 QQ Bot</h4>
|
||||||
|
<p>接入 QQ 群/私聊机器人</p>
|
||||||
|
<div class="channel-form" id="form-qqbot" onclick="event.stopPropagation()">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>App ID</label>
|
||||||
|
<input type="text" id="ch-qqbot-appid" placeholder="应用 ID">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Client Secret</label>
|
||||||
|
<input type="text" id="ch-qqbot-secret" placeholder="应用密钥">
|
||||||
|
<p class="hint"><a href="https://q.qq.com" target="_blank">→ 从 QQ 开放平台获取</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="model-card channel-card" onclick="toggleChannel(this, 'feishu')">
|
||||||
|
<h4>📘 飞书</h4>
|
||||||
|
<p>接入飞书机器人</p>
|
||||||
|
<div class="channel-form" id="form-feishu" onclick="event.stopPropagation()">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>App ID</label>
|
||||||
|
<input type="text" id="ch-feishu-appid" placeholder="cli_xxx...">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>App Secret</label>
|
||||||
|
<input type="text" id="ch-feishu-secret" placeholder="应用密钥">
|
||||||
|
<p class="hint"><a href="https://open.feishu.cn/app" target="_blank">→ 从飞书开放平台获取</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="model-card channel-card" onclick="toggleChannel(this, 'wecom')">
|
||||||
|
<h4>🏢 企业微信</h4>
|
||||||
|
<p>接入企业微信机器人</p>
|
||||||
|
<div class="channel-form" id="form-wecom" onclick="event.stopPropagation()">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Bot ID</label>
|
||||||
|
<input type="text" id="ch-wecom-botid" placeholder="aib...">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Secret</label>
|
||||||
|
<input type="text" id="ch-wecom-secret" placeholder="密钥">
|
||||||
|
<p class="hint"><a href="https://work.weixin.qq.com" target="_blank">→ 从企业微信管理后台获取</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 28px; text-align: center;">
|
||||||
|
<button class="btn btn-primary" onclick="saveChannelsAndOpen()" style="font-size: 1.15em; padding: 16px 48px;">🚀 打开 Dashboard</button>
|
||||||
|
<p style="color: #888; font-size: 0.8em; margin-top: 10px;">渠道配置会自动写入,Gateway 需重启生效</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="btn-row" style="justify-content: center; margin-top: 16px;">
|
||||||
|
<button class="btn btn-secondary" onclick="goStep(1)">重新配置模型</button>
|
||||||
|
<button class="btn btn-secondary" onclick="viewConfig()">查看配置文件</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Skills & info (always visible) -->
|
||||||
|
<div class="section active skills-section" style="display:block; margin-top: 8px;">
|
||||||
|
<h2>📦 目录说明</h2>
|
||||||
|
<p class="desc">U 盘内的文件结构</p>
|
||||||
|
<div class="skill-list">
|
||||||
|
<div class="skill-item">
|
||||||
|
<div class="info">
|
||||||
|
<h4>skills-cn/ — 中文技能</h4>
|
||||||
|
<p>放入 .md 技能文件,OpenClaw 自动加载</p>
|
||||||
|
</div>
|
||||||
|
<span class="status">📁</span>
|
||||||
|
</div>
|
||||||
|
<div class="skill-item">
|
||||||
|
<div class="info">
|
||||||
|
<h4>app/ — 运行时 + 核心</h4>
|
||||||
|
<p>Node.js 运行时、OpenClaw 核心代码</p>
|
||||||
|
</div>
|
||||||
|
<span class="status">⚙️</span>
|
||||||
|
</div>
|
||||||
|
<div class="skill-item">
|
||||||
|
<div class="info">
|
||||||
|
<h4>data/ — 配置和数据</h4>
|
||||||
|
<p>API Key、聊天历史等保存在此(仅本地)</p>
|
||||||
|
</div>
|
||||||
|
<span class="status">🔒</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="toast" id="toast"></div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
let config = {};
|
// --- State ---
|
||||||
|
let selectedProvider = null;
|
||||||
|
let selectedBase = '';
|
||||||
|
let selectedModel = '';
|
||||||
|
|
||||||
// 加载配置
|
// --- Model cards ---
|
||||||
async function loadConfig() {
|
document.querySelectorAll('#step1 .model-card').forEach(card => {
|
||||||
try {
|
card.addEventListener('click', (e) => {
|
||||||
const res = await fetch('/api/config');
|
if (e.target.closest('.buy-link')) return;
|
||||||
config = await res.json();
|
document.querySelectorAll('#step1 .model-card').forEach(c => c.classList.remove('selected'));
|
||||||
|
card.classList.add('selected');
|
||||||
// 填充表单
|
selectedProvider = card.dataset.provider;
|
||||||
if (config.agent) {
|
selectedBase = card.dataset.base;
|
||||||
document.getElementById('provider').value = config.agent.provider || 'anthropic';
|
selectedModel = card.dataset.model;
|
||||||
document.getElementById('apiKey').value = config.agent.apiKey || '';
|
document.getElementById('nextStep1').disabled = false;
|
||||||
if (config.agent.baseUrl) {
|
});
|
||||||
document.getElementById('baseUrl').value = config.agent.baseUrl;
|
|
||||||
document.getElementById('baseUrlGroup').style.display = 'block';
|
|
||||||
}
|
|
||||||
if (config.agent.model) {
|
|
||||||
document.getElementById('modelName').value = config.agent.model;
|
|
||||||
document.getElementById('modelGroup').style.display = 'block';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.channels && config.channels.qqbot) {
|
|
||||||
const token = config.channels.qqbot.token || '';
|
|
||||||
const [appId, secret] = token.split(':');
|
|
||||||
document.getElementById('qqAppId').value = appId || '';
|
|
||||||
document.getElementById('qqSecret').value = secret || '';
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to load config:', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Provider 切换
|
|
||||||
document.getElementById('provider').addEventListener('change', (e) => {
|
|
||||||
const custom = e.target.value === 'custom';
|
|
||||||
document.getElementById('baseUrlGroup').style.display = custom ? 'block' : 'none';
|
|
||||||
document.getElementById('modelGroup').style.display = custom ? 'block' : 'none';
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 保存 AI 配置
|
document.getElementById('nextStep1').addEventListener('click', () => goStep(2));
|
||||||
async function saveAI() {
|
|
||||||
const provider = document.getElementById('provider').value;
|
|
||||||
const apiKey = document.getElementById('apiKey').value;
|
|
||||||
const baseUrl = document.getElementById('baseUrl').value;
|
|
||||||
const modelName = document.getElementById('modelName').value;
|
|
||||||
|
|
||||||
if (!apiKey) {
|
// --- Steps ---
|
||||||
showStatus('aiStatus', 'error', '请输入 API Key');
|
function goStep(n) {
|
||||||
return;
|
document.querySelectorAll('.section:not(.skills-section)').forEach(s => s.classList.remove('active'));
|
||||||
}
|
document.getElementById('step' + n).classList.add('active');
|
||||||
|
document.querySelectorAll('.steps .step').forEach((s, i) => {
|
||||||
config.agent = { provider, apiKey };
|
s.classList.remove('active', 'done');
|
||||||
if (provider === 'custom') {
|
if (i + 1 < n) s.classList.add('done');
|
||||||
config.agent.baseUrl = baseUrl;
|
if (i + 1 === n) s.classList.add('active');
|
||||||
config.agent.model = modelName;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await fetch('/api/config', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(config)
|
|
||||||
});
|
});
|
||||||
const result = await res.json();
|
if (n === 2) setupStep2();
|
||||||
if (result.ok) {
|
|
||||||
showStatus('aiStatus', 'success', '✓ AI 配置已保存');
|
|
||||||
} else {
|
|
||||||
showStatus('aiStatus', 'error', '保存失败: ' + (result.error || 'Unknown error'));
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
showStatus('aiStatus', 'error', '保存失败: ' + err.message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存 QQ 配置
|
const PROVIDER_NAMES = {
|
||||||
async function saveQQ() {
|
minimax: 'MiniMax', kimi: 'Kimi (月之暗面)', deepseek: 'DeepSeek',
|
||||||
const appId = document.getElementById('qqAppId').value;
|
zai: '智谱 GLM', qwen: '通义千问', doubao: '豆包',
|
||||||
const secret = document.getElementById('qqSecret').value;
|
openai: 'OpenAI', anthropic: 'Anthropic Claude', groq: 'Groq',
|
||||||
|
siliconflow: '硅基流动', custom: '自定义'
|
||||||
|
};
|
||||||
|
|
||||||
if (!appId || !secret) {
|
function setupStep2() {
|
||||||
showStatus('qqStatus', 'error', '请输入 AppID 和 Secret');
|
var isCustom = selectedProvider === 'custom';
|
||||||
return;
|
document.getElementById('customFields').style.display = isCustom ? 'block' : 'none';
|
||||||
|
|
||||||
|
var name = PROVIDER_NAMES[selectedProvider] || selectedProvider;
|
||||||
|
document.getElementById('step2desc').textContent = '请填写 ' + name + ' 的 API Key';
|
||||||
|
|
||||||
|
var card = document.querySelector('#step1 .model-card[data-provider="' + selectedProvider + '"]');
|
||||||
|
var buyLink = card ? card.querySelector('.buy-link') : null;
|
||||||
|
var linksDiv = document.getElementById('providerLinks');
|
||||||
|
linksDiv.innerHTML = '';
|
||||||
|
if (buyLink) {
|
||||||
|
linksDiv.innerHTML = '<a href="' + buyLink.href + '" target="_blank">🔑 ' + name + ' — 获取 API Key</a>';
|
||||||
}
|
}
|
||||||
|
linksDiv.innerHTML += '<a href="https://platform.minimaxi.com/" target="_blank">🔑 MiniMax — 国内推荐,注册送额度</a>' +
|
||||||
|
'<a href="https://open.bigmodel.cn/" target="_blank">🔑 智谱 GLM — 有免费额度</a>' +
|
||||||
|
'<a href="https://cloud.siliconflow.cn/" target="_blank">🔑 硅基流动 — 多模型聚合,便宜</a>';
|
||||||
|
}
|
||||||
|
|
||||||
config.channels = config.channels || {};
|
// --- Toggle password ---
|
||||||
config.channels.qqbot = {
|
function togglePw(id) {
|
||||||
token: `${appId}:${secret}`,
|
var inp = document.getElementById(id);
|
||||||
enabled: true
|
var btn = inp.parentElement.querySelector('.toggle-pw');
|
||||||
|
if (inp.type === 'password') { inp.type = 'text'; btn.textContent = '隐藏'; }
|
||||||
|
else { inp.type = 'password'; btn.textContent = '显示'; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Build OpenClaw config ---
|
||||||
|
function buildConfig(provider, base, model, apiKey) {
|
||||||
|
var baseConfig = {
|
||||||
|
gateway: { auth: { mode: 'token', token: 'uclaw' } },
|
||||||
|
commands: { native: 'auto', nativeSkills: 'auto', restart: true, ownerDisplay: 'raw' },
|
||||||
|
meta: { lastTouchedVersion: '2026.3.13', lastTouchedAt: new Date().toISOString() }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 智谱 GLM uses built-in zai provider
|
||||||
|
if (provider === 'zai') {
|
||||||
|
return Object.assign({}, baseConfig, {
|
||||||
|
env: { ZAI_API_KEY: apiKey },
|
||||||
|
agents: { defaults: { model: { primary: 'zai/' + model } } }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var providerName = provider || 'myProvider';
|
||||||
|
var providers = {};
|
||||||
|
providers[providerName] = {
|
||||||
|
baseUrl: base,
|
||||||
|
apiKey: apiKey,
|
||||||
|
api: 'openai-completions',
|
||||||
|
models: [{
|
||||||
|
id: model,
|
||||||
|
name: model,
|
||||||
|
reasoning: false,
|
||||||
|
input: ['text'],
|
||||||
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||||
|
contextWindow: 128000,
|
||||||
|
maxTokens: 8192
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
return Object.assign({}, baseConfig, {
|
||||||
|
models: { mode: 'merge', providers: providers },
|
||||||
|
agents: { defaults: { model: { primary: providerName + '/' + model } } }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Save model config ---
|
||||||
|
async function saveConfig() {
|
||||||
|
var apiKey = document.getElementById('apiKey').value.trim();
|
||||||
|
if (!apiKey) { showToast('请填写 API Key', true); return; }
|
||||||
|
|
||||||
|
var base = selectedBase;
|
||||||
|
var model = selectedModel;
|
||||||
|
|
||||||
|
if (selectedProvider === 'custom') {
|
||||||
|
base = document.getElementById('customBase').value.trim();
|
||||||
|
model = document.getElementById('customModel').value.trim();
|
||||||
|
if (!base || !model) { showToast('请填写 API 地址和模型名称', true); return; }
|
||||||
|
}
|
||||||
|
|
||||||
|
var config = buildConfig(selectedProvider, base, model, apiKey);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/config', {
|
var res = await fetch('/api/config', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(config)
|
body: JSON.stringify(config)
|
||||||
});
|
});
|
||||||
const result = await res.json();
|
if (!res.ok) throw new Error('保存失败');
|
||||||
if (result.ok) {
|
showToast('配置已保存!');
|
||||||
showStatus('qqStatus', 'success', '✓ QQ 配置已保存,请重启 OpenClaw');
|
goStep(3);
|
||||||
|
} catch (e) {
|
||||||
|
showToast('保存失败: ' + e.message, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Channel toggle ---
|
||||||
|
function toggleChannel(card, type) {
|
||||||
|
var form = document.getElementById('form-' + type);
|
||||||
|
var isOpen = form.classList.contains('open');
|
||||||
|
if (isOpen) {
|
||||||
|
form.classList.remove('open');
|
||||||
|
card.classList.remove('selected');
|
||||||
} else {
|
} else {
|
||||||
showStatus('qqStatus', 'error', '保存失败: ' + (result.error || 'Unknown error'));
|
form.classList.add('open');
|
||||||
}
|
card.classList.add('selected');
|
||||||
} catch (err) {
|
var firstInput = form.querySelector('input');
|
||||||
showStatus('qqStatus', 'error', '保存失败: ' + err.message);
|
if (firstInput) setTimeout(function() { firstInput.focus(); }, 100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showStatus(id, type, message) {
|
// --- Save channels and open dashboard ---
|
||||||
const el = document.getElementById(id);
|
async function saveChannelsAndOpen() {
|
||||||
el.className = `status ${type}`;
|
var channels = {};
|
||||||
el.textContent = message;
|
|
||||||
setTimeout(() => el.style.display = 'none', 3000);
|
var tgToken = document.getElementById('ch-telegram-token').value.trim();
|
||||||
|
if (tgToken) channels.telegram = { enabled: true, botToken: tgToken, dmPolicy: 'pairing' };
|
||||||
|
|
||||||
|
var qqId = document.getElementById('ch-qqbot-appid').value.trim();
|
||||||
|
var qqSecret = document.getElementById('ch-qqbot-secret').value.trim();
|
||||||
|
if (qqId && qqSecret) channels.qqbot = { enabled: true, appId: qqId, clientSecret: qqSecret };
|
||||||
|
|
||||||
|
var fsId = document.getElementById('ch-feishu-appid').value.trim();
|
||||||
|
var fsSecret = document.getElementById('ch-feishu-secret').value.trim();
|
||||||
|
if (fsId && fsSecret) channels.feishu = { enabled: true, appId: fsId, appSecret: fsSecret };
|
||||||
|
|
||||||
|
var wcBotId = document.getElementById('ch-wecom-botid').value.trim();
|
||||||
|
var wcSecret = document.getElementById('ch-wecom-secret').value.trim();
|
||||||
|
if (wcBotId && wcSecret) channels.wecom = { enabled: true, botId: wcBotId, secret: wcSecret };
|
||||||
|
|
||||||
|
if (Object.keys(channels).length > 0) {
|
||||||
|
try {
|
||||||
|
var res = await fetch('/api/config');
|
||||||
|
var existing = res.ok ? await res.json() : {};
|
||||||
|
existing.channels = Object.assign(existing.channels || {}, channels);
|
||||||
|
await fetch('/api/config', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(existing)
|
||||||
|
});
|
||||||
|
showToast('渠道配置已保存');
|
||||||
|
} catch(e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.open('http://127.0.0.1:18789/#token=uclaw', '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- View config ---
|
||||||
|
async function viewConfig() {
|
||||||
|
try {
|
||||||
|
var res = await fetch('/api/config');
|
||||||
|
var config = await res.json();
|
||||||
|
var text = JSON.stringify(config, null, 2);
|
||||||
|
var w = window.open('', '_blank');
|
||||||
|
w.document.write('<pre style="background:#111;color:#eee;padding:20px;font-size:14px;">' +
|
||||||
|
text.replace(/</g, '<') + '</pre>');
|
||||||
|
} catch (e) {
|
||||||
|
showToast('读取配置失败: ' + e.message, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Toast ---
|
||||||
|
function showToast(msg, isError) {
|
||||||
|
var t = document.getElementById('toast');
|
||||||
|
t.textContent = msg;
|
||||||
|
t.className = 'toast show' + (isError ? ' error' : '');
|
||||||
|
setTimeout(function() { t.className = 'toast'; }, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Check gateway status ---
|
||||||
|
async function checkGateway() {
|
||||||
|
var dot = document.getElementById('statusDot');
|
||||||
|
var text = document.getElementById('statusText');
|
||||||
|
try {
|
||||||
|
await fetch('http://127.0.0.1:18789/', { mode: 'no-cors' });
|
||||||
|
dot.className = 'dot on';
|
||||||
|
text.innerHTML = 'Gateway 运行中 · <a href="http://127.0.0.1:18789/#token=uclaw" target="_blank">打开 Dashboard →</a>';
|
||||||
|
} catch(e) {
|
||||||
|
dot.className = 'dot off';
|
||||||
|
text.innerHTML = 'Gateway 未启动 · 请先运行 Windows-Start.bat 或 Mac-Start.command';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Load existing config ---
|
||||||
|
async function loadConfig() {
|
||||||
|
try {
|
||||||
|
var res = await fetch('/api/config');
|
||||||
|
var cfg = await res.json();
|
||||||
|
|
||||||
|
if (cfg.env && cfg.env.ZAI_API_KEY) {
|
||||||
|
var card = document.querySelector('#step1 .model-card[data-provider="zai"]');
|
||||||
|
if (card) card.click();
|
||||||
|
document.getElementById('apiKey').value = cfg.env.ZAI_API_KEY;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cfg.models && cfg.models.providers) {
|
||||||
|
var providerName = Object.keys(cfg.models.providers)[0];
|
||||||
|
if (providerName) {
|
||||||
|
var p = cfg.models.providers[providerName];
|
||||||
|
var card = document.querySelector('#step1 .model-card[data-provider="' + providerName + '"]');
|
||||||
|
if (card) card.click();
|
||||||
|
if (p.apiKey) document.getElementById('apiKey').value = p.apiKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch(e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化
|
|
||||||
loadConfig();
|
loadConfig();
|
||||||
|
checkGateway();
|
||||||
|
setInterval(checkGateway, 10000);
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user