Files
u-claw/portable/config-server/public/index.html

801 lines
37 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>U-Claw 配置中心</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
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; }
/* 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; }
/* 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; }
/* 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 { display: block; margin-bottom: 16px; clear: both; }
.form-group label {
display: block;
width: auto;
float: none;
color: #ccc;
margin-bottom: 8px;
font-size: 0.9em;
}
.form-group input,
input[type="text"],
input[type="password"] {
display: block;
width: 100%;
min-width: 0;
min-height: 46px;
padding: 11px 14px;
background: #222;
border: 1px solid #444;
border-radius: 8px;
color: #fff;
font-size: 0.95em;
line-height: 1.3;
outline: none;
transition: border-color 0.2s;
}
input:focus { border-color: #ff6b35; }
.input-wrap { position: relative; display: block; width: 100%; min-width: 0; }
.input-wrap input { padding-right: 70px; }
.toggle-pw {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
min-width: 44px;
height: 30px;
background: #2d2d2d;
border: 1px solid #444;
border-radius: 6px;
color: #ccc;
cursor: pointer;
font-size: 0.82em;
line-height: 28px;
text-align: center;
}
.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; }
/* 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; }
/* 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 class="lobster">🦞</span> U-Claw 配置中心</h1>
<p class="subtitle">选择模型,填入 API Key一键启动</p>
<!-- Connection status -->
<div class="status-bar">
<div class="dot" id="statusDot"></div>
<div class="status-text" id="statusText">检测中...</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">最省心:用「虾盘云」一个 Key 调用国内外全部大模型(中转站)。也可用下面各家自己的官方 Key。</p>
<div class="model-grid">
<div class="model-card" data-provider="uclaw-cloud" data-base="https://api.u-claw.org/v1" data-model="deepseek-v4-flash">
<span class="check"></span>
<h4>虾盘云 <span class="tag hot">首选</span><span class="tag">中转站</span></h4>
<p>一个 Key 用 DeepSeek / Claude / GPT / 通义 等国内外全部大模型,无需翻墙</p>
<a class="buy-link" href="https://u-claw.org/cloud.html" target="_blank" data-action-label="注册 / 充值 / 获取 API Key">→ 注册 / 充值 / 获取 API Key</a>
</div>
<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 class="model-card channel-card" style="grid-column: 1 / -1; border-color: #07c160;" onclick="startWeChatLogin(event)">
<h4>💬 微信 <span class="tag" style="background:rgba(7,193,96,0.2);color:#07c160;">个人微信</span></h4>
<p>扫码接入个人微信,用微信聊天控制 AI</p>
<div class="channel-form" id="form-wechat" onclick="event.stopPropagation()" style="max-height:none;overflow:visible;">
<div id="wechat-qr" style="text-align:center;padding:16px;">
<p style="color:#888;font-size:0.85em;">点击此卡片开始扫码</p>
</div>
<div id="wechat-status" style="text-align:center;color:#888;font-size:0.85em;"></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 class="section active skills-section" style="display:block; margin-top: 8px;">
<h2>🐛 反馈 / 报 Bug</h2>
<p class="desc">U-Claw 是开源项目。遇到问题欢迎到 GitHub 提 Issue —— 我们不会自动收集或上传你的任何数据。</p>
<div class="btn-row">
<a class="btn btn-primary" href="https://github.com/dongsheng123132/u-claw/issues/new" target="_blank">→ 在 GitHub 提交 Issue</a>
</div>
</div>
</div>
<div class="toast" id="toast"></div>
<script>
// --- State ---
let selectedProvider = null;
let selectedBase = '';
let selectedModel = '';
// --- 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;
});
});
document.getElementById('nextStep1').addEventListener('click', () => goStep(2));
// --- 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');
});
if (n === 2) setupStep2();
}
const PROVIDER_NAMES = {
'uclaw-cloud': '虾盘云', minimax: 'MiniMax', kimi: 'Kimi (月之暗面)', deepseek: 'DeepSeek',
zai: '智谱 GLM', qwen: '通义千问', doubao: '豆包',
openai: 'OpenAI', anthropic: 'Anthropic Claude', groq: 'Groq',
siliconflow: '硅基流动', custom: '自定义'
};
function setupStep2() {
var isCustom = selectedProvider === 'custom';
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) {
var action = buyLink.dataset.actionLabel || '获取 API Key';
linksDiv.innerHTML = '<a href="' + buyLink.href + '" target="_blank">🔑 ' + name + ' — ' + action + '</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: { mode: 'local', auth: { mode: 'token', token: 'uclaw' }, remote: { 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 {
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);
}
}
// --- 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);
}
}
// --- WeChat login ---
var wechatSession = null;
var wechatPolling = false;
async function startWeChatLogin(e) {
var form = document.getElementById('form-wechat');
// If form is already open and showing QR, don't restart
if (form.classList.contains('open') && wechatPolling) {
return;
}
// Toggle form open
var card = form.closest('.channel-card');
if (!form.classList.contains('open')) {
form.classList.add('open');
card.classList.add('selected');
}
var qrDiv = document.getElementById('wechat-qr');
var statusDiv = document.getElementById('wechat-status');
qrDiv.innerHTML = '<p style="color:#888;">正在获取二维码...</p>';
statusDiv.textContent = '';
try {
var res = await fetch('/api/wechat/start', { method: 'POST' });
var data = await res.json();
if (data.error) throw new Error(data.error);
wechatSession = data.sessionKey;
qrDiv.innerHTML = '<img src="' + data.qrcodeUrl + '" style="width:200px;height:200px;image-rendering:pixelated;border-radius:8px;">';
statusDiv.textContent = '请用微信扫描上方二维码';
statusDiv.style.color = '#07c160';
// Start polling
wechatPolling = true;
pollWeChatStatus();
} catch (err) {
qrDiv.innerHTML = '<p style="color:#f44336;">获取二维码失败</p>';
statusDiv.textContent = err.message;
statusDiv.style.color = '#f44336';
}
}
async function pollWeChatStatus() {
if (!wechatSession || !wechatPolling) return;
try {
var res = await fetch('/api/wechat/status?session=' + encodeURIComponent(wechatSession));
var data = await res.json();
var qrDiv = document.getElementById('wechat-qr');
var statusDiv = document.getElementById('wechat-status');
if (data.status === 'confirmed') {
wechatPolling = false;
qrDiv.innerHTML = '<div style="font-size:3em;">&#10004;</div>';
// 后端已自动装好插件 + 写好 openclaw.json + 存好账号,剩下只需重启 Gateway 加载插件。
// 不再暴露 pluginInstalled 内部状态——它即使为 false配置也已写好下次启动必加载
// 显示「插件未安装」只会和「连接成功」自相矛盾、吓到客户。
statusDiv.innerHTML = '<span style="color:#4caf50;font-weight:600;">✅ 微信连接成功!</span>' +
'<br><span style="font-size:0.8em;color:#888;">账号 ' + (data.accountId || '') + ' 已绑定</span>' +
'<br><span style="font-size:0.85em;color:#ff9800;font-weight:600;">最后一步:重启 U-Claw关闭后重新打开即可在微信里使用</span>';
statusDiv.style.color = '#4caf50';
showToast('微信连接成功!重启 U-Claw 后生效');
return;
}
if (data.status === 'refreshed') {
qrDiv.innerHTML = '<img src="' + data.qrcodeUrl + '" style="width:200px;height:200px;image-rendering:pixelated;border-radius:8px;">';
statusDiv.textContent = '二维码已刷新,请重新扫码';
statusDiv.style.color = '#ff6b35';
} else if (data.status === 'scaned') {
statusDiv.textContent = '已扫码,请在手机上确认...';
statusDiv.style.color = '#07c160';
} else if (data.status === 'expired') {
wechatPolling = false;
qrDiv.innerHTML = '<p style="color:#888;">二维码已过期,<a href="#" onclick="startWeChatLogin(event);return false;" style="color:#ff6b35;">重新获取</a></p>';
statusDiv.textContent = data.message || '';
statusDiv.style.color = '#f44336';
return;
} else if (data.status === 'error') {
wechatPolling = false;
qrDiv.innerHTML = '<p style="color:#f44336;">连接失败</p>';
statusDiv.textContent = data.message || 'Unknown error';
statusDiv.style.color = '#f44336';
return;
}
// wait -> continue polling
setTimeout(pollWeChatStatus, 1500);
} catch (err) {
setTimeout(pollWeChatStatus, 3000);
}
}
// --- 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, token: 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() : {};
// 清除旧版废弃键,防止 OpenClaw 报 "agent.* was moved" 错误
delete existing.agent;
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(gatewayUrl(), '_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);
}
// --- Find active gateway port (18789-18799) ---
var _gatewayPort = null;
async function findGatewayPort() {
for (var p = 18789; p <= 18799; p++) {
try {
await fetch('http://127.0.0.1:' + p + '/', { mode: 'no-cors' });
_gatewayPort = p;
return p;
} catch(e) {}
}
_gatewayPort = null;
return null;
}
function gatewayUrl() {
var p = _gatewayPort || 18789;
return 'http://127.0.0.1:' + p + '/#token=uclaw';
}
// --- Check gateway status ---
async function checkGateway() {
var dot = document.getElementById('statusDot');
var text = document.getElementById('statusText');
var port = await findGatewayPort();
if (port) {
dot.className = 'dot on';
text.innerHTML = 'Gateway 运行中 (端口 ' + port + ') · <a href="' + gatewayUrl() + '" target="_blank">打开 Dashboard →</a>';
document.getElementById('gatewayUrl').textContent = 'http://127.0.0.1:' + port;
} else {
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>