按 U-Claw-海外化改造方案.md 与 范围决策记录.md 实施。这是 fork,不回上游: 海外版删掉的正是上游的中国市场默认值。 阶段 0 地基 - 下载源全部改国际:脚本/CI 61 处 + lockfile 880 条 npmmirror URL 归零 (lockfile 那 880 条是 npm 的 resolved 字段,脚本层参数化根本绕不过它) - 移除 install.ps1 里三个第三方 GitHub 加速代理,bundle 改直连 + SHA256 校验 (原来只检查"文件大于 1MB"就解压运行) - 技能内容与分发分离:skills/manifest.json 单一来源,install.sh 1170→658 行、 install.ps1 721→546 行,两者技能内容归零 实测原来是三份不一致:skills-cn 完整、install.sh 约 40%、install.ps1 约 17%, 且 7 个通用技能只有 U 盘版有 —— 一键安装的用户一个能用的技能都没有 - Node 版本三种(v22.14/16/22.1)统一,新建 NODE_VERSION 单一来源 - Config 页三份合一。portable/Config.html 用根相对路径调 API 却只从 file:// 打开, 保存功能已静默失效两个月;现缩为 120 行重定向壳 - 测试接入 CI(此前 node --test 无人运行,所有断言形同虚设) 阶段 1 双语可用 - 浏览器侧 i18n:JSON 为源、生成经典 script(file:// 下 fetch 本地 JSON 被拦) 语言跟盘走不跟机器走:启动器写 data/.openclaw/locale.js - 8 处硬编码 lang="zh-CN" 归零,data-i18n 覆盖 213 处,词条 en/zh 各 279 条 - B3 单框 Key:12 张模型卡 → 一个输入框,前缀识别 provider, 服务端 /api/test-key 发 1-token 请求实测,错误映射成人话 Key 填错到得知:从"直到对话失败"降到 ≤1 秒 - 区域格式 SG:DD/MM/YYYY、12 小时、S$、Asia/Singapore (ICU 在 en-SG 下把 SGD 渲染成裸 $,与美元无法区分,故自行拼 S$) - README 内容分叉而非翻译,§1.3 证据清单逐条清零 阶段 2 降门槛 - 启动逻辑上移 lib/start.mjs:Windows-Start.bat 220→28 行、 Mac-Start.command 235→33 行 修掉 Mac 侧两个 bug:控制台端口硬编码 18788(回落时打开死页)、 微信插件从未在 Mac 上安装 - U 盘根目录 23 → 3 个可点文件,其余进 advanced/ - 首启向导:语言 → 用途(7 角色,manifest 驱动)→ 密钥,答过不再问 - 三档界面,Simple 档隐藏一切技术名词 - 自动自愈:启动失败先自查自修,修不好导出脱敏诊断包 (Doctor 从"用户要知道去点的工具"变成后台机制) 阶段 3 技能库 - 19 个英文技能,planned 归零。sg-weather / sg-transport 的端点均实测过 - SkillHub 从 56 张手写第三方卡片改为 manifest 生成:703→125 行,中文归零 其他 - origin.json 收拢所有运行时地址,tests/origin.test.mjs 保证迁移不会漏 - portable/ 下用户可见中文归零(由断言保证) - 82 项测试 未验证(本机无 Windows / 无 pwsh): - install.ps1、setup.ps1 约 210 行改动从未经 PowerShell 解析器 - 完整启动路径仅在假 node + 假 openclaw 上冒烟 - 8 个 .bat 的盘根推导仅静态断言 详见 U盘实测清单.md 受阻: - 隐藏黑窗口 —— 需代码签名证书(.vbs 已被 Windows 弃用,替代方案都要签名) - 场景卡 —— OpenClaw 上游 Dashboard 无预填 prompt 接口 - 官网 36 条 —— 上游 2026-04-14 拆到私有仓库,无权限
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>U-Claw 配置中心</title>
|
||||
<title data-i18n="key.page_title">U-Claw Settings</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; }
|
||||
@@ -155,262 +155,250 @@ input:focus { border-color: #ff6b35; }
|
||||
.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>
|
||||
<script src="/locale.js"></script>
|
||||
<script src="/lib/i18n/messages.js"></script>
|
||||
<script src="/lib/i18n/i18n.js"></script>
|
||||
<style>
|
||||
/* Interface tiers. The wizard picks one; Settings offers a switch.
|
||||
Simple hides everything a non-technical user should never have to meet —
|
||||
endpoints, model names, config files, the China-only channel group. */
|
||||
body[data-tier="simple"] [data-tier-min="standard"],
|
||||
body[data-tier="simple"] [data-tier-min="expert"],
|
||||
body[data-tier="standard"] [data-tier-min="expert"] { display: none !important; }
|
||||
.tier-switch { margin-top: 22px; text-align: center; color: #888; font-size: .82em; }
|
||||
.tier-switch select { margin-left: 6px; background: #222; color: #ddd; border: 1px solid #333; border-radius: 6px; padding: 4px 8px; font: inherit; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1><span class="lobster">🦞</span> U-Claw 配置中心</h1>
|
||||
<p class="subtitle">选择模型,填入 API Key,一键启动</p>
|
||||
<h1><span class="lobster">🦞</span> <span data-i18n="key.page_title">U-Claw Settings</span></h1>
|
||||
<p class="subtitle" data-i18n="key.page_subtitle">Paste an API key and you are done.</p>
|
||||
|
||||
<!-- Connection status -->
|
||||
<div class="status-bar">
|
||||
<div class="dot" id="statusDot"></div>
|
||||
<div class="status-text" id="statusText">检测中...</div>
|
||||
<div class="status-text" id="statusText" data-i18n="key.checking">Checking…</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 class="step active" data-step="1" onclick="goStep(1)"><span class="num">1</span><span data-i18n="key.step_key">Your key</span></div>
|
||||
<div class="step" data-step="3" onclick="goStep(3)"><span class="num">2</span><span data-i18n="key.step_done">Done</span></div>
|
||||
</div>
|
||||
|
||||
<!-- Step 1: Select Model -->
|
||||
<!-- Step 1: one field -->
|
||||
<div class="section active" id="step1">
|
||||
<h2>选择 AI 模型</h2>
|
||||
<p class="desc">最省心:用「虾盘云」一个 Key 调用国内外全部大模型(中转站)。也可用下面各家自己的官方 Key。</p>
|
||||
<h2 data-i18n="key.heading">Paste your API key</h2>
|
||||
<p class="desc" data-i18n="key.desc">U-Claw works out the rest by itself. Your key is saved on this drive only — it is never uploaded anywhere.</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-v4-flash">
|
||||
<span class="check">✓</span>
|
||||
<h4>DeepSeek <span class="tag cn">国内</span><span class="tag cheap">便宜</span></h4>
|
||||
<p>deepseek-v4-flash,超低价格</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 class="form-group">
|
||||
<div class="input-wrap">
|
||||
<input type="password" id="apiKey" autocomplete="off" spellcheck="false"
|
||||
data-i18n-attr="placeholder:key.placeholder" placeholder="sk-…">
|
||||
<button class="toggle-pw" onclick="togglePw('apiKey')" data-i18n="key.show">Show</button>
|
||||
</div>
|
||||
<div id="keyFeedback" class="hint" aria-live="polite"></div>
|
||||
</div>
|
||||
|
||||
<!-- Appears once the key checks out. Without it, picking a key would
|
||||
also lock the model, and there would be no way to switch later. -->
|
||||
<div class="form-group" id="modelPicker" style="display:none;" data-tier-min="standard">
|
||||
<label data-i18n="key.which_model">Which model?</label>
|
||||
<select id="modelChoice"></select>
|
||||
</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>
|
||||
<button class="btn btn-primary" id="continueBtn" onclick="saveConfig()" disabled data-i18n="key.continue">Continue →</button>
|
||||
</div>
|
||||
|
||||
<div id="customFields" style="display:none;">
|
||||
<div class="form-group">
|
||||
<label>API 地址 (Base URL)</label>
|
||||
<div class="api-info">
|
||||
<h4 data-i18n="key.no_key_title">Do not have a key yet?</h4>
|
||||
<p class="desc" data-i18n="key.no_key_desc">Google Gemini is the quickest to get: sign in with a Google account, no card needed, and there is a free tier.</p>
|
||||
<div class="provider-links">
|
||||
<a href="https://aistudio.google.com/apikey" target="_blank" rel="noopener" data-i18n="key.get_gemini">🔑 Get a free Gemini key</a>
|
||||
<a href="https://openrouter.ai/keys" target="_blank" rel="noopener" data-i18n="key.get_openrouter">🔑 OpenRouter — one key for many models</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Everything below is for people who already know they need it. -->
|
||||
<details id="advanced" style="margin-top:18px;" data-tier-min="standard">
|
||||
<summary style="cursor:pointer; color:#888; font-size:0.85em;" data-i18n="key.advanced">Advanced — set the endpoint manually</summary>
|
||||
<div class="form-group" style="margin-top:12px;">
|
||||
<label data-i18n="key.base_url">API endpoint</label>
|
||||
<input type="text" id="customBase" placeholder="https://api.example.com/v1">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>模型名称</label>
|
||||
<label data-i18n="key.model_name">Model name</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>
|
||||
<p class="hint" data-i18n="key.advanced_hint">Only needed for a self-hosted or unlisted provider. Leave blank to let U-Claw decide.</p>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<!-- Step 3: Done -->
|
||||
<div class="section" id="step3">
|
||||
<div class="result">
|
||||
<div class="icon">🦞</div>
|
||||
<h2>配置完成!</h2>
|
||||
<p>模型和 API Key 已保存到本地</p>
|
||||
<h2 data-i18n="done.title">You are set up</h2>
|
||||
<p data-i18n="done.subtitle">Your model and key are saved on this drive.</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>
|
||||
<h3 style="color: #ff6b35; font-size: 0.95em; margin-bottom: 12px;" data-i18n="ch.title">📱 Connect a chat app (optional)</h3>
|
||||
<p style="color: #888; font-size: 0.82em; margin-bottom: 12px;" data-i18n="ch.desc">Click a card to fill it in, or skip this entirely.</p>
|
||||
<div class="model-grid">
|
||||
<div class="model-card channel-card" onclick="toggleChannel(this, 'whatsapp')">
|
||||
<h4>💬 WhatsApp</h4>
|
||||
<p data-i18n="ch.whatsapp_d">Talk to U-Claw from WhatsApp</p>
|
||||
<div class="channel-form" id="form-whatsapp" onclick="event.stopPropagation()">
|
||||
<p class="hint" style="color:#ffb84d;" data-i18n="ch.whatsapp_warning">⚠️ This uses an unofficial protocol. Your WhatsApp account can be banned for it. Do not connect a number you rely on.</p>
|
||||
<div class="form-group">
|
||||
<label data-i18n="ch.whatsapp_number">Phone number</label>
|
||||
<input type="text" id="ch-whatsapp-number" placeholder="+65 8123 4567">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="model-card channel-card" onclick="toggleChannel(this, 'telegram')">
|
||||
<h4>✈️ Telegram</h4>
|
||||
<p>接入 Telegram Bot</p>
|
||||
<p data-i18n="ch.telegram_d">Talk to U-Claw from Telegram</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>
|
||||
<p class="hint"><a href="https://t.me/BotFather" target="_blank" rel="noopener" data-i18n="ch.telegram_get">→ Get one from @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="model-card channel-card" onclick="toggleChannel(this, 'slack')">
|
||||
<h4>💼 Slack</h4>
|
||||
<p data-i18n="ch.slack_d">Talk to U-Claw from your workspace</p>
|
||||
<div class="channel-form" id="form-slack" 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>
|
||||
<label>Bot Token</label>
|
||||
<input type="text" id="ch-slack-token" placeholder="xoxb-...">
|
||||
<p class="hint"><a href="https://api.slack.com/apps" target="_blank" rel="noopener" data-i18n="ch.slack_get">→ Create an app at api.slack.com</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="model-card channel-card" onclick="toggleChannel(this, 'discord')">
|
||||
<h4>🎮 Discord</h4>
|
||||
<p data-i18n="ch.discord_d">Talk to U-Claw from Discord</p>
|
||||
<div class="channel-form" id="form-discord" onclick="event.stopPropagation()">
|
||||
<div class="form-group">
|
||||
<label>App ID</label>
|
||||
<input type="text" id="ch-feishu-appid" placeholder="cli_xxx...">
|
||||
<label>Bot Token</label>
|
||||
<input type="text" id="ch-discord-token" placeholder="MTIz...">
|
||||
<p class="hint"><a href="https://discord.com/developers/applications" target="_blank" rel="noopener" data-i18n="ch.discord_get">→ Create an app in the Discord developer portal</a></p>
|
||||
</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>
|
||||
|
||||
<!-- China-only channels. Kept for users in China, folded away by
|
||||
default so they are not noise for everyone else. -->
|
||||
<details style="margin-top:14px;" data-tier-min="standard">
|
||||
<summary style="cursor:pointer; color:#888; font-size:0.85em;" data-i18n="ch.china_group">Chat apps used mainly in China</summary>
|
||||
<div class="model-grid" style="margin-top:12px;">
|
||||
<div class="model-card channel-card" onclick="toggleChannel(this, 'qqbot')">
|
||||
<h4>🐧 QQ Bot</h4>
|
||||
<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="App ID">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Client Secret</label>
|
||||
<input type="text" id="ch-qqbot-secret" placeholder="Client Secret">
|
||||
<p class="hint"><a href="https://q.qq.com" target="_blank" rel="noopener">→ q.qq.com</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="model-card channel-card" onclick="toggleChannel(this, 'feishu')">
|
||||
<h4>📘 Feishu / Lark</h4>
|
||||
<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="App Secret">
|
||||
<p class="hint"><a href="https://open.feishu.cn/app" target="_blank" rel="noopener">→ open.feishu.cn</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="model-card channel-card" onclick="toggleChannel(this, 'wecom')">
|
||||
<h4>🏢 WeCom</h4>
|
||||
<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="Secret">
|
||||
<p class="hint"><a href="https://work.weixin.qq.com" target="_blank" rel="noopener">→ work.weixin.qq.com</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="model-card channel-card" style="grid-column: 1 / -1; border-color: #07c160;" onclick="startWeChatLogin(event)">
|
||||
<h4>💬 WeChat</h4>
|
||||
<p data-i18n="ch.wechat_d">Scan a QR code to link a personal WeChat account</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;" data-i18n="ch.wechat_start">Click this card to show the QR code</p>
|
||||
</div>
|
||||
<div id="wechat-status" style="text-align:center;color:#888;font-size:0.85em;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</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>
|
||||
<button class="btn btn-primary" onclick="saveChannelsAndOpen()" style="font-size: 1.15em; padding: 16px 48px;" data-i18n="done.open">🚀 Start chatting</button>
|
||||
<p style="color: #888; font-size: 0.8em; margin-top: 10px;" data-i18n="done.restart_note">Chat app settings are saved now and take effect after U-Claw restarts.</p>
|
||||
</div>
|
||||
|
||||
<div class="tier-switch">
|
||||
<label for="tierChoice" data-i18n="tier.label">How much do you want to see?</label>
|
||||
<select id="tierChoice" onchange="setTier(this.value)">
|
||||
<option value="simple" data-i18n="tier.simple">Just the essentials</option>
|
||||
<option value="standard" data-i18n="tier.standard">A bit more control</option>
|
||||
<option value="expert" data-i18n="tier.expert">Everything</option>
|
||||
</select>
|
||||
</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>
|
||||
<button class="btn btn-secondary" onclick="goStep(1)" data-i18n="done.change_key">Change model or key</button>
|
||||
<button class="btn btn-secondary" onclick="viewConfig()" data-tier-min="expert" data-i18n="done.view_config">View config file</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="section active skills-section" style="display:block; margin-top: 8px;" data-tier-min="standard">
|
||||
<h2>📦 <span data-i18n="info.layout_title">What is on the drive</span></h2>
|
||||
<p class="desc" data-i18n="info.layout_desc">How the folders are organised</p>
|
||||
<div class="skill-list">
|
||||
<div class="skill-item">
|
||||
<div class="info">
|
||||
<h4>skills-cn/ — 中文技能</h4>
|
||||
<p>放入 .md 技能文件,OpenClaw 自动加载</p>
|
||||
<h4>skills/ — <span data-i18n="info.skills">Skills</span></h4>
|
||||
<p data-i18n="info.skills_d">Drop a SKILL.md in here and it is picked up automatically</p>
|
||||
</div>
|
||||
<span class="status">📁</span>
|
||||
</div>
|
||||
<div class="skill-item">
|
||||
<div class="info">
|
||||
<h4>app/ — 运行时 + 核心</h4>
|
||||
<p>Node.js 运行时、OpenClaw 核心代码</p>
|
||||
<h4>app/ — <span data-i18n="info.app">Runtime</span></h4>
|
||||
<p data-i18n="info.app_d">Node.js and the OpenClaw core</p>
|
||||
</div>
|
||||
<span class="status">⚙️</span>
|
||||
</div>
|
||||
<div class="skill-item">
|
||||
<div class="info">
|
||||
<h4>data/ — 配置和数据</h4>
|
||||
<p>API Key、聊天历史等保存在此(仅本地)</p>
|
||||
<h4>data/ — <span data-i18n="info.data">Your data</span></h4>
|
||||
<p data-i18n="info.data_d">Your key and chat history. Stays on this drive.</p>
|
||||
</div>
|
||||
<span class="status">🔒</span>
|
||||
</div>
|
||||
@@ -418,10 +406,10 @@ input:focus { border-color: #ff6b35; }
|
||||
</div>
|
||||
|
||||
<div class="section active skills-section" style="display:block; margin-top: 8px;">
|
||||
<h2>🐛 反馈 / 报 Bug</h2>
|
||||
<p class="desc">U-Claw 是开源项目。遇到问题欢迎到 GitHub 提 Issue —— 我们不会自动收集或上传你的任何数据。</p>
|
||||
<h2>🐛 <span data-i18n="info.feedback_title">Found a problem?</span></h2>
|
||||
<p class="desc" data-i18n="info.feedback_desc">U-Claw is open source. Report anything that breaks on GitHub — nothing is collected or uploaded from your machine automatically.</p>
|
||||
<div class="btn-row">
|
||||
<a class="btn btn-primary" href="https://github.com/dongsheng123132/u-claw/issues/new" target="_blank">→ 在 GitHub 提交 Issue</a>
|
||||
<a class="btn btn-primary" href="https://gitea.fanghe.it.com/zhenghy/u-claw/issues/new" target="_blank" rel="noopener" data-i18n="info.feedback_link">→ Open an issue on GitHub</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -430,70 +418,116 @@ input:focus { border-color: #ff6b35; }
|
||||
|
||||
<script>
|
||||
// --- State ---
|
||||
let selectedProvider = null;
|
||||
let selectedBase = '';
|
||||
let selectedModel = '';
|
||||
// Filled in by the key check, not by the user. The old flow made people choose a
|
||||
// provider from twelve cards and then type a Base URL and a model name; every
|
||||
// key already announces its provider through its prefix, so we derive all three.
|
||||
let detected = null; // { id, label, baseUrl, model } once a key checks out
|
||||
|
||||
// --- 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));
|
||||
const t = (key, vars) => (window.UClawI18n ? window.UClawI18n.t(key, vars) : key);
|
||||
|
||||
// --- 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) => {
|
||||
document.querySelectorAll('.steps .step').forEach(s => {
|
||||
const step = Number(s.dataset.step);
|
||||
s.classList.remove('active', 'done');
|
||||
if (i + 1 < n) s.classList.add('done');
|
||||
if (i + 1 === n) s.classList.add('active');
|
||||
if (step < n) s.classList.add('done');
|
||||
if (step === 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: '自定义'
|
||||
};
|
||||
// --- Key check ---
|
||||
const keyInput = document.getElementById('apiKey');
|
||||
const feedback = document.getElementById('keyFeedback');
|
||||
const continueBtn = document.getElementById('continueBtn');
|
||||
let checkTimer = null;
|
||||
let checkToken = 0;
|
||||
|
||||
function setupStep2() {
|
||||
var isCustom = selectedProvider === 'custom';
|
||||
document.getElementById('customFields').style.display = isCustom ? 'block' : 'none';
|
||||
function setFeedback(state, text) {
|
||||
feedback.className = 'hint ' + state;
|
||||
feedback.style.color = state === 'ok' ? '#4caf50' : state === 'bad' ? '#ff6b6b' : '#888';
|
||||
feedback.textContent = text;
|
||||
}
|
||||
|
||||
var name = PROVIDER_NAMES[selectedProvider] || selectedProvider;
|
||||
document.getElementById('step2desc').textContent = '请填写 ' + name + ' 的 API Key';
|
||||
async function checkKey() {
|
||||
const apiKey = keyInput.value.trim();
|
||||
detected = null;
|
||||
continueBtn.disabled = true;
|
||||
if (!apiKey) { setFeedback('', ''); return; }
|
||||
|
||||
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>';
|
||||
const mine = ++checkToken;
|
||||
setFeedback('', t('key.checking_key'));
|
||||
|
||||
let result;
|
||||
try {
|
||||
const res = await fetch('/api/test-key', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
apiKey,
|
||||
baseUrl: document.getElementById('customBase').value.trim(),
|
||||
model: document.getElementById('customModel').value.trim(),
|
||||
}),
|
||||
});
|
||||
result = await res.json();
|
||||
} catch (err) {
|
||||
result = { ok: false, reason: 'key.err_offline' };
|
||||
}
|
||||
// A slower earlier check must not overwrite a newer one.
|
||||
if (mine !== checkToken) return;
|
||||
|
||||
if (result.ok) {
|
||||
detected = { id: result.provider ? result.provider.id : 'custom', baseUrl: result.baseUrl, model: result.model };
|
||||
const label = result.provider ? result.provider.label : t('key.custom_provider');
|
||||
setFeedback('ok', t('key.ok', { provider: label, ms: result.latencyMs }));
|
||||
populateModels(result.provider, result.model);
|
||||
continueBtn.disabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Every failure names something to do next, never just a status code.
|
||||
setFeedback('bad', t(result.reason || 'key.err_unknown'));
|
||||
if (result.reason === 'key.err_unrecognised') {
|
||||
document.getElementById('advanced').open = true;
|
||||
}
|
||||
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>';
|
||||
}
|
||||
|
||||
const modelPicker = document.getElementById('modelPicker');
|
||||
const modelChoice = document.getElementById('modelChoice');
|
||||
|
||||
function populateModels(provider, current) {
|
||||
var options = (provider && provider.models) ? provider.models.slice() : [];
|
||||
if (current && options.indexOf(current) === -1) options.unshift(current);
|
||||
if (options.length < 2) { modelPicker.style.display = 'none'; return; }
|
||||
modelChoice.innerHTML = '';
|
||||
options.forEach(function (name) {
|
||||
var opt = document.createElement('option');
|
||||
opt.value = name;
|
||||
opt.textContent = name;
|
||||
if (name === current) opt.selected = true;
|
||||
modelChoice.appendChild(opt);
|
||||
});
|
||||
modelPicker.style.display = 'block';
|
||||
}
|
||||
|
||||
// Check as they paste, but wait for typing to settle first.
|
||||
function scheduleCheck() {
|
||||
clearTimeout(checkTimer);
|
||||
checkTimer = setTimeout(checkKey, 800);
|
||||
}
|
||||
keyInput.addEventListener('input', scheduleCheck);
|
||||
keyInput.addEventListener('paste', () => setTimeout(checkKey, 0));
|
||||
document.getElementById('customBase').addEventListener('input', scheduleCheck);
|
||||
document.getElementById('customModel').addEventListener('input', scheduleCheck);
|
||||
|
||||
// --- 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 = '显示'; }
|
||||
var showing = inp.type !== 'password';
|
||||
inp.type = showing ? 'password' : 'text';
|
||||
btn.textContent = t(showing ? 'key.show' : 'key.hide');
|
||||
}
|
||||
|
||||
// --- Build OpenClaw config ---
|
||||
@@ -537,18 +571,14 @@ function buildConfig(provider, base, model, apiKey) {
|
||||
// --- Save model config ---
|
||||
async function saveConfig() {
|
||||
var apiKey = document.getElementById('apiKey').value.trim();
|
||||
if (!apiKey) { showToast('请填写 API Key', true); return; }
|
||||
// The button is only enabled once a key has actually answered, so reaching
|
||||
// here without `detected` means the check has not finished yet.
|
||||
if (!apiKey || !detected) { showToast(t('key.err_not_checked'), 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);
|
||||
// The picker wins when shown, so returning here later is a real way to
|
||||
// switch model without hunting for the config file.
|
||||
var model = (modelPicker.style.display !== 'none' && modelChoice.value) ? modelChoice.value : detected.model;
|
||||
var config = buildConfig(detected.id, detected.baseUrl, model, apiKey);
|
||||
|
||||
try {
|
||||
var res = await fetch('/api/config', {
|
||||
@@ -556,11 +586,11 @@ async function saveConfig() {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(config)
|
||||
});
|
||||
if (!res.ok) throw new Error('保存失败');
|
||||
showToast('配置已保存!');
|
||||
if (!res.ok) throw new Error(String(res.status));
|
||||
showToast(t('key.saved'));
|
||||
goStep(3);
|
||||
} catch (e) {
|
||||
showToast('保存失败: ' + e.message, true);
|
||||
showToast(t('key.err_save'), true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -599,7 +629,7 @@ async function startWeChatLogin(e) {
|
||||
|
||||
var qrDiv = document.getElementById('wechat-qr');
|
||||
var statusDiv = document.getElementById('wechat-status');
|
||||
qrDiv.innerHTML = '<p style="color:#888;">正在获取二维码...</p>';
|
||||
qrDiv.innerHTML = '<p style="color:#888;">' + t('wx.getting_qr') + '</p>';
|
||||
statusDiv.textContent = '';
|
||||
|
||||
try {
|
||||
@@ -609,14 +639,14 @@ async function startWeChatLogin(e) {
|
||||
|
||||
wechatSession = data.sessionKey;
|
||||
qrDiv.innerHTML = '<img src="' + data.qrcodeUrl + '" style="width:200px;height:200px;image-rendering:pixelated;border-radius:8px;">';
|
||||
statusDiv.textContent = '请用微信扫描上方二维码';
|
||||
statusDiv.textContent = t('wx.scan');
|
||||
statusDiv.style.color = '#07c160';
|
||||
|
||||
// Start polling
|
||||
wechatPolling = true;
|
||||
pollWeChatStatus();
|
||||
} catch (err) {
|
||||
qrDiv.innerHTML = '<p style="color:#f44336;">获取二维码失败</p>';
|
||||
qrDiv.innerHTML = '<p style="color:#f44336;">' + t('wx.qr_failed') + '</p>';
|
||||
statusDiv.textContent = err.message;
|
||||
statusDiv.style.color = '#f44336';
|
||||
}
|
||||
@@ -638,30 +668,30 @@ async function pollWeChatStatus() {
|
||||
// 后端已自动装好插件 + 写好 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.innerHTML = '<span style="color:#4caf50;font-weight:600;">' + t('wx.connected') + '</span>' +
|
||||
'<br><span style="font-size:0.8em;color:#888;">' + t('wx.account') + ' ' + (data.accountId || '') + '</span>' +
|
||||
'<br><span style="font-size:0.85em;color:#ff9800;font-weight:600;">' + t('wx.restart_needed') + '</span>';
|
||||
statusDiv.style.color = '#4caf50';
|
||||
showToast('微信连接成功!重启 U-Claw 后生效');
|
||||
showToast(t('wx.connected_toast'));
|
||||
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.textContent = t('wx.qr_refreshed');
|
||||
statusDiv.style.color = '#ff6b35';
|
||||
} else if (data.status === 'scaned') {
|
||||
statusDiv.textContent = '已扫码,请在手机上确认...';
|
||||
statusDiv.textContent = t('wx.confirm_on_phone');
|
||||
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>';
|
||||
qrDiv.innerHTML = '<p style="color:#888;">' + t('wx.qr_expired') + ' <a href="#" onclick="startWeChatLogin(event);return false;" style="color:#ff6b35;">' + t('wx.qr_retry') + '</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>';
|
||||
qrDiv.innerHTML = '<p style="color:#f44336;">' + t('wx.failed') + '</p>';
|
||||
statusDiv.textContent = data.message || 'Unknown error';
|
||||
statusDiv.style.color = '#f44336';
|
||||
return;
|
||||
@@ -676,29 +706,40 @@ async function pollWeChatStatus() {
|
||||
// --- Save channels and open dashboard ---
|
||||
async function saveChannelsAndOpen() {
|
||||
var channels = {};
|
||||
var val = function (id) {
|
||||
var el = document.getElementById(id);
|
||||
return el ? el.value.trim() : '';
|
||||
};
|
||||
|
||||
var tgToken = document.getElementById('ch-telegram-token').value.trim();
|
||||
// OpenClaw 顶层 telegram 渠道只认 botToken(旧 token 别名仅在 accounts.<id> 里有效)。
|
||||
// 之前写成 token 会被静默忽略 → Telegram 接不上。
|
||||
// OpenClaw's top-level telegram channel only reads botToken; the older
|
||||
// `token` alias works solely under accounts.<id>. Writing `token` here made
|
||||
// Telegram silently fail to connect.
|
||||
var tgToken = val('ch-telegram-token');
|
||||
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();
|
||||
var waNumber = val('ch-whatsapp-number');
|
||||
if (waNumber) channels.whatsapp = { enabled: true, number: waNumber, dmPolicy: 'pairing' };
|
||||
|
||||
var slackToken = val('ch-slack-token');
|
||||
if (slackToken) channels.slack = { enabled: true, botToken: slackToken };
|
||||
|
||||
var discordToken = val('ch-discord-token');
|
||||
if (discordToken) channels.discord = { enabled: true, botToken: discordToken };
|
||||
|
||||
var qqId = val('ch-qqbot-appid'), qqSecret = val('ch-qqbot-secret');
|
||||
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();
|
||||
var fsId = val('ch-feishu-appid'), fsSecret = val('ch-feishu-secret');
|
||||
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();
|
||||
var wcBotId = val('ch-wecom-botid'), wcSecret = val('ch-wecom-secret');
|
||||
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" 错误
|
||||
// Drop the retired key or OpenClaw errors with "agent.* was moved".
|
||||
delete existing.agent;
|
||||
existing.channels = Object.assign(existing.channels || {}, channels);
|
||||
await fetch('/api/config', {
|
||||
@@ -706,8 +747,10 @@ async function saveChannelsAndOpen() {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(existing)
|
||||
});
|
||||
showToast('渠道配置已保存');
|
||||
} catch(e) {}
|
||||
showToast(t('ch.saved'));
|
||||
} catch (e) {
|
||||
showToast(t('ch.err_save'), true);
|
||||
}
|
||||
}
|
||||
|
||||
window.open(gatewayUrl(), '_blank');
|
||||
@@ -723,7 +766,7 @@ async function viewConfig() {
|
||||
w.document.write('<pre style="background:#111;color:#eee;padding:20px;font-size:14px;">' +
|
||||
text.replace(/</g, '<') + '</pre>');
|
||||
} catch (e) {
|
||||
showToast('读取配置失败: ' + e.message, true);
|
||||
showToast(t('info.read_config_failed') + ' ' + e.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -761,37 +804,68 @@ async function checkGateway() {
|
||||
var port = await findGatewayPort();
|
||||
if (port) {
|
||||
dot.className = 'dot on';
|
||||
text.innerHTML = 'Gateway 运行中 (端口 ' + port + ') · <a href="' + gatewayUrl() + '" target="_blank">打开 Dashboard →</a>';
|
||||
text.innerHTML = t('status.running', { port: found }) + ' (' + port + ') · <a href="' + gatewayUrl() + '" target="_blank">' + t('status.open_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';
|
||||
text.innerHTML = t('status.not_running');
|
||||
}
|
||||
}
|
||||
|
||||
// --- Load existing config ---
|
||||
// --- Interface tier ---
|
||||
const TIERS = ['simple', 'standard', 'expert'];
|
||||
|
||||
function applyTier(tier) {
|
||||
var value = TIERS.indexOf(tier) === -1 ? 'simple' : tier;
|
||||
document.body.dataset.tier = value;
|
||||
var picker = document.getElementById('tierChoice');
|
||||
if (picker) picker.value = value;
|
||||
}
|
||||
|
||||
async function setTier(tier) {
|
||||
applyTier(tier);
|
||||
try {
|
||||
var cfg = await (await fetch('/api/config')).json();
|
||||
cfg.uclaw = Object.assign({}, cfg.uclaw, { tier: tier });
|
||||
await fetch('/api/config', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(cfg)
|
||||
});
|
||||
// Say what changed rather than silently revealing new controls.
|
||||
showToast(t('tier.changed_' + tier));
|
||||
} catch (e) { showToast(t('key.err_save'), true); }
|
||||
}
|
||||
|
||||
async function loadConfig() {
|
||||
try {
|
||||
var res = await fetch('/api/config');
|
||||
var cfg = await res.json();
|
||||
var cfg = await (await fetch('/api/config')).json();
|
||||
applyTier((cfg.uclaw && cfg.uclaw.tier) || 'simple');
|
||||
|
||||
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;
|
||||
var key = (cfg.env && cfg.env.ZAI_API_KEY) || null;
|
||||
var model = null;
|
||||
if (!key && cfg.models && cfg.models.providers) {
|
||||
var name = Object.keys(cfg.models.providers)[0];
|
||||
if (name) {
|
||||
key = cfg.models.providers[name].apiKey || null;
|
||||
var primary = cfg.agents && cfg.agents.defaults && cfg.agents.defaults.model
|
||||
? cfg.agents.defaults.model.primary : null;
|
||||
if (primary) model = String(primary).split('/').slice(1).join('/') || null;
|
||||
}
|
||||
}
|
||||
} catch(e) {}
|
||||
if (!key) return;
|
||||
|
||||
// Prefill and re-check, so the page shows what is actually configured and
|
||||
// the model picker comes back populated. Previously this looked for model
|
||||
// cards that no longer exist and silently did nothing.
|
||||
keyInput.value = key;
|
||||
if (model) document.getElementById('customModel').value = model;
|
||||
await checkKey();
|
||||
if (model) {
|
||||
for (var i = 0; i < modelChoice.options.length; i++) {
|
||||
if (modelChoice.options[i].value === model) { modelChoice.selectedIndex = i; break; }
|
||||
}
|
||||
}
|
||||
} catch (e) { /* first run: nothing configured yet */ }
|
||||
}
|
||||
|
||||
loadConfig();
|
||||
|
||||
Reference in New Issue
Block a user