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:
hfshfg
2026-03-20 10:47:52 +08:00
parent 15460fef62
commit 6569bcddf6
2 changed files with 601 additions and 191 deletions

View File

@@ -1,5 +1,4 @@
@echo off
setlocal EnableDelayedExpansion
chcp 65001 >nul 2>&1
title U-Claw - Portable AI Agent
@@ -76,10 +75,10 @@ REM Find available port
set PORT=18789
:check_port
netstat -an | findstr ":%PORT% " | findstr "LISTENING" >nul 2>&1
if !errorlevel!==0 (
if %errorlevel%==0 (
echo Port %PORT% in use, trying next...
set /a PORT+=1
if !PORT! gtr 18799 (
if %PORT% gtr 18799 (
echo No available port 18789-18799
pause
exit /b 1
@@ -87,40 +86,34 @@ if !errorlevel!==0 (
goto :check_port
)
REM Read token from config
set "TOKEN=uclaw"
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%...
echo.
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.
cd /d "%CORE_DIR%"
set "OPENCLAW_MJS=%CORE_DIR%\node_modules\openclaw\openclaw.mjs"
echo Log: %DATA_DIR%\logs\gateway.log
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' }"
"%NODE_BIN%" "%OPENCLAW_MJS%" gateway run --allow-unconfigured --force --port %PORT%
echo.
echo OpenClaw stopped.

View File

@@ -6,202 +6,619 @@
<title>U-Claw 配置中心</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, "Microsoft YaHei", sans-serif; background: #0a0a0a; color: #e0e0e0; padding: 20px; }
.container { max-width: 600px; margin: 0 auto; }
h1 { text-align: center; margin-bottom: 10px; }
h1 span { color: #ff6b35; }
.subtitle { text-align: center; color: #888; margin-bottom: 30px; font-size: 0.9em; }
body { font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; background: #0a0a0a; color: #e0e0e0; min-height: 100vh; }
.container { max-width: 720px; margin: 0 auto; padding: 28px 20px 60px; }
h1 { text-align: center; font-size: 1.8em; margin-bottom: 4px; }
h1 .lobster { color: #ff6b35; }
.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; }
.section h2 { color: #ff6b35; font-size: 1.1em; margin-bottom: 10px; }
/* Steps */
.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; }
label { display: block; color: #ccc; margin-bottom: 5px; font-size: 0.9em; }
input, select { width: 100%; padding: 10px; background: #222; border: 1px solid #444; border-radius: 6px; color: #fff; font-size: 0.95em; }
input:focus, select:focus { outline: none; border-color: #ff6b35; }
/* Section */
.section { background: #1a1a1a; border: 1px solid #333; border-radius: 12px; padding: 24px; margin-bottom: 16px; display: none; }
.section.active { display: block; }
.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: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; }
.status.success { display: block; background: rgba(76,175,80,0.12); color: #4caf50; border: 1px solid rgba(76,175,80,0.3); }
.status.error { display: block; background: rgba(244,67,54,0.12); color: #f44336; border: 1px solid rgba(244,67,54,0.3); }
/* Result */
.result { text-align: center; padding: 40px 20px; }
.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; }
.links a { color: #ff6b35; text-decoration: none; margin: 0 10px; }
/* Toast */
.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>
</head>
<body>
<div class="container">
<h1>🦞 <span>U-Claw</span> 配置中心</h1>
<p class="subtitle">简单配置,快速启动</p>
<h1><span class="lobster">🦞</span> U-Claw 配置中心</h1>
<p class="subtitle">选择模型,填入 API Key一键启动</p>
<!-- AI 模型配置 -->
<div class="section">
<h2>AI 模型配置</h2>
<div class="form-group">
<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>
<!-- Connection status -->
<div class="status-bar">
<div class="dot" id="statusDot"></div>
<div class="status-text" id="statusText">检测中...</div>
</div>
<div class="form-group">
<label>API Key</label>
<input type="password" id="apiKey" placeholder="输入您的 API Key">
</div>
<div class="form-group" id="baseUrlGroup" style="display:none">
<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>
<!-- QQ Bot 配置 -->
<div class="section">
<h2>QQ 机器人(可选)</h2>
<div class="form-group">
<label>AppID</label>
<input type="text" id="qqAppId" placeholder="从 q.qq.com 获取">
<!-- 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>
<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 class="links">
<a href="http://127.0.0.1:18789/#token=uclaw" target="_blank">打开 OpenClaw 控制台 →</a>
</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-5zai 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">
<label>API Key</label>
<div class="input-wrap">
<input type="password" id="apiKey" placeholder="sk-...">
<button class="toggle-pw" onclick="togglePw('apiKey')">显示</button>
</div>
<p class="hint">你的 Key 仅保存在本地 U 盘,不会上传到任何服务器</p>
</div>
<div class="btn-row">
<button class="btn btn-secondary" onclick="goStep(1)">← 返回</button>
<button class="btn btn-primary" onclick="saveConfig()">保存并继续 →</button>
</div>
</div>
<!-- Step 3: Done -->
<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 class="toast" id="toast"></div>
<script>
let config = {};
// --- State ---
let selectedProvider = null;
let selectedBase = '';
let selectedModel = '';
// 加载配置
async function loadConfig() {
try {
const res = await fetch('/api/config');
config = await res.json();
// 填充表单
if (config.agent) {
document.getElementById('provider').value = config.agent.provider || 'anthropic';
document.getElementById('apiKey').value = config.agent.apiKey || '';
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';
// --- Model cards ---
document.querySelectorAll('#step1 .model-card').forEach(card => {
card.addEventListener('click', (e) => {
if (e.target.closest('.buy-link')) return;
document.querySelectorAll('#step1 .model-card').forEach(c => c.classList.remove('selected'));
card.classList.add('selected');
selectedProvider = card.dataset.provider;
selectedBase = card.dataset.base;
selectedModel = card.dataset.model;
document.getElementById('nextStep1').disabled = false;
});
});
// 保存 AI 配置
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;
document.getElementById('nextStep1').addEventListener('click', () => goStep(2));
if (!apiKey) {
showStatus('aiStatus', 'error', '请输入 API Key');
return;
}
config.agent = { provider, apiKey };
if (provider === 'custom') {
config.agent.baseUrl = baseUrl;
config.agent.model = modelName;
}
try {
const res = await fetch('/api/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(config)
// --- Steps ---
function goStep(n) {
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) => {
s.classList.remove('active', 'done');
if (i + 1 < n) s.classList.add('done');
if (i + 1 === n) s.classList.add('active');
});
const result = await res.json();
if (result.ok) {
showStatus('aiStatus', 'success', '✓ AI 配置已保存');
} else {
showStatus('aiStatus', 'error', '保存失败: ' + (result.error || 'Unknown error'));
}
} catch (err) {
showStatus('aiStatus', 'error', '保存失败: ' + err.message);
}
if (n === 2) setupStep2();
}
// 保存 QQ 配置
async function saveQQ() {
const appId = document.getElementById('qqAppId').value;
const secret = document.getElementById('qqSecret').value;
const PROVIDER_NAMES = {
minimax: 'MiniMax', kimi: 'Kimi (月之暗面)', deepseek: 'DeepSeek',
zai: '智谱 GLM', qwen: '通义千问', doubao: '豆包',
openai: 'OpenAI', anthropic: 'Anthropic Claude', groq: 'Groq',
siliconflow: '硅基流动', custom: '自定义'
};
if (!appId || !secret) {
showStatus('qqStatus', 'error', '请输入 AppID 和 Secret');
return;
}
function setupStep2() {
var isCustom = selectedProvider === 'custom';
document.getElementById('customFields').style.display = isCustom ? 'block' : 'none';
config.channels = config.channels || {};
config.channels.qqbot = {
token: `${appId}:${secret}`,
enabled: true
};
var name = PROVIDER_NAMES[selectedProvider] || selectedProvider;
document.getElementById('step2desc').textContent = '请填写 ' + name + ' 的 API Key';
try {
const res = await fetch('/api/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(config)
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>';
}
// --- Toggle password ---
function togglePw(id) {
var inp = document.getElementById(id);
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 } } }
});
const result = await res.json();
if (result.ok) {
showStatus('qqStatus', 'success', '✓ QQ 配置已保存,请重启 OpenClaw');
} else {
showStatus('qqStatus', 'error', '保存失败: ' + (result.error || 'Unknown error'));
}
// --- 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 {
var res = await fetch('/api/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(config)
});
if (!res.ok) throw new Error('保存失败');
showToast('配置已保存!');
goStep(3);
} catch (e) {
showToast('保存失败: ' + e.message, true);
}
} catch (err) {
showStatus('qqStatus', 'error', '保存失败: ' + err.message);
}
}
function showStatus(id, type, message) {
const el = document.getElementById(id);
el.className = `status ${type}`;
el.textContent = message;
setTimeout(() => el.style.display = 'none', 3000);
// --- 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 {
form.classList.add('open');
card.classList.add('selected');
var firstInput = form.querySelector('input');
if (firstInput) setTimeout(function() { firstInput.focus(); }, 100);
}
}
// --- Save channels and open dashboard ---
async function saveChannelsAndOpen() {
var channels = {};
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, '&lt;') + '</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();
checkGateway();
setInterval(checkGateway, 10000);
</script>
</body>
</html>