按 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,748 +0,0 @@
|
||||
<!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 Pro — 模型配置</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: 680px; margin: 0 auto; padding: 30px 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: 64px; }
|
||||
.toggle-pw {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
min-width: 38px;
|
||||
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; }
|
||||
.api-info a .price { color: #888; font-size: 0.85em; }
|
||||
|
||||
/* 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: 12px; margin-top: 10px; border-top: 1px solid #333; max-height: 0; overflow: hidden; transition: max-height 0.3s ease, padding 0.3s ease; padding-top: 0; }
|
||||
.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; }
|
||||
|
||||
/* Skills section */
|
||||
.skills-section { margin-top: 20px; }
|
||||
.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 Pro</h1>
|
||||
<p class="subtitle">便携版 — 选择模型,填入 API Key,一键启动</p>
|
||||
|
||||
<!-- Steps -->
|
||||
<div class="steps">
|
||||
<div class="step active" data-step="1"><span class="num">1</span>选模型</div>
|
||||
<div class="step" data-step="2"><span class="num">2</span>填 Key</div>
|
||||
<div class="step" data-step="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="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 / deepseek-v4-pro,超低价格</p>
|
||||
<a class="buy-link" href="https://platform.deepseek.com/" target="_blank">→ 获取 API Key</a>
|
||||
</div>
|
||||
<div class="model-card" data-provider="minimax" data-base="https://api.minimax.chat/v1" data-model="MiniMax-M2">
|
||||
<span class="check">✓</span>
|
||||
<h4>MiniMax <span class="tag cn">国内</span></h4>
|
||||
<p>MiniMax-M2,速度快,性价比高</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="kimi-k2-turbo-preview">
|
||||
<span class="check">✓</span>
|
||||
<h4>Kimi (月之暗面) <span class="tag cn">国内</span><span class="tag fast">快</span></h4>
|
||||
<p>kimi-k2,长上下文强</p>
|
||||
<a class="buy-link" href="https://platform.moonshot.cn/" 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-plus">
|
||||
<span class="check">✓</span>
|
||||
<h4>通义千问 <span class="tag cn">国内</span><span class="tag free">有免费</span></h4>
|
||||
<p>qwen-plus / qwen3-max,阿里云出品</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-seed-1-6-250615">
|
||||
<span class="check">✓</span>
|
||||
<h4>豆包 (字节) <span class="tag cn">国内</span><span class="tag fast">快</span></h4>
|
||||
<p>doubao-seed-1.6,字节跳动</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-5.4">
|
||||
<span class="check">✓</span>
|
||||
<h4>OpenAI <span class="tag hot">强</span></h4>
|
||||
<p>GPT-5.4,需翻墙或中转</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-opus-4-6">
|
||||
<span class="check">✓</span>
|
||||
<h4>Claude <span class="tag hot">强</span></h4>
|
||||
<p>Claude Opus 4.6 / Sonnet 4.6,需翻墙或中转</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="">
|
||||
<span class="check">✓</span>
|
||||
<h4>自定义 <span class="tag">OpenAI 兼容</span></h4>
|
||||
<p>填写任意 OpenAI 兼容 API 地址</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 本地模型自动发现(Ollama / LM Studio)-->
|
||||
<div id="localModelsBox" style="display:none; margin-top:6px;">
|
||||
<h3 style="color:#4caf50; font-size:0.95em; margin:12px 0 4px;">💻 检测到本机本地模型 <span class="tag free">离线·免费</span></h3>
|
||||
<p class="desc" style="margin-bottom:10px;">无需 API Key,纯本地推理。点选即用:</p>
|
||||
<div class="model-grid" id="localModelGrid"></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>
|
||||
|
||||
<!-- Custom fields (hidden by default) -->
|
||||
<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()">👁</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" id="saveBtn" 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, 'dingtalk')">
|
||||
<h4>📲 钉钉 <span class="tag cn">国内·最易</span></h4>
|
||||
<p>WebSocket 接入,免公网 IP</p>
|
||||
<div class="channel-form" id="form-dingtalk" onclick="event.stopPropagation()">
|
||||
<div class="form-group">
|
||||
<label>Client ID (AppKey)</label>
|
||||
<input type="text" id="ch-dingtalk-clientid" placeholder="dingxxx...">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Client Secret (AppSecret)</label>
|
||||
<input type="text" id="ch-dingtalk-secret" placeholder="应用密钥">
|
||||
<p class="hint"><a href="https://open-dev.dingtalk.com" 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="launchOpenClaw()" style="font-size: 1.15em; padding: 16px 48px;">🚀 启动 OpenClaw</button>
|
||||
<p style="color: #888; font-size: 0.8em; margin-top: 10px;">启动后会自动打开控制台,渠道配置会自动写入 openclaw.json</p>
|
||||
</div>
|
||||
|
||||
<div class="btn-row" style="justify-content: center; margin-top: 16px;">
|
||||
<button class="btn btn-secondary" onclick="goStep(1)">重新配置模型</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Skills info -->
|
||||
<div class="section active skills-section" style="display:block; margin-top: 24px;">
|
||||
<h2>📦 技能 & 文档</h2>
|
||||
<p class="desc">技能存放在 skills/ 目录,文档存放在 docs/ 目录,随 U 盘携带</p>
|
||||
<div class="skill-list" id="skillList">
|
||||
<div class="skill-item">
|
||||
<div class="info">
|
||||
<h4>skills/ — 技能目录</h4>
|
||||
<p>放入 .md 技能文件,OpenClaw 会自动加载</p>
|
||||
</div>
|
||||
<span class="status">📁</span>
|
||||
</div>
|
||||
<div class="skill-item">
|
||||
<div class="info">
|
||||
<h4>docs/ — 文档目录</h4>
|
||||
<p>存放使用说明、帮助文档等</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>
|
||||
// --- 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; // don't select when clicking buy link
|
||||
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');
|
||||
// Keep skills section visible
|
||||
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();
|
||||
}
|
||||
|
||||
function setupStep2() {
|
||||
const isCustom = selectedProvider === 'custom';
|
||||
document.getElementById('customFields').style.display = isCustom ? 'block' : 'none';
|
||||
|
||||
const providerNames = {
|
||||
'uclaw-cloud': '虾盘云', minimax: 'MiniMax', kimi: 'Kimi (月之暗面)', deepseek: 'DeepSeek',
|
||||
zai: '智谱 GLM', qwen: '通义千问', doubao: '豆包',
|
||||
openai: 'OpenAI', anthropic: 'Anthropic Claude', groq: 'Groq',
|
||||
siliconflow: '硅基流动', custom: '自定义',
|
||||
ollama: 'Ollama(本地)', lmstudio: 'LM Studio(本地)'
|
||||
};
|
||||
const name = providerNames[selectedProvider] || selectedProvider;
|
||||
document.getElementById('step2desc').textContent = selectedLocal
|
||||
? `${name} 是本地模型,无需 API Key,直接点「保存并启动」即可`
|
||||
: `请填写 ${name} 的 API Key`;
|
||||
// 本地模型隐藏 Key 获取信息框
|
||||
document.getElementById('buyInfo').style.display = selectedLocal ? 'none' : 'block';
|
||||
|
||||
// Buy links
|
||||
const card = document.querySelector(`#step1 .model-card[data-provider="${selectedProvider}"]`);
|
||||
const buyLink = card ? card.querySelector('.buy-link') : null;
|
||||
const linksDiv = document.getElementById('providerLinks');
|
||||
linksDiv.innerHTML = '';
|
||||
if (buyLink) {
|
||||
linksDiv.innerHTML = `
|
||||
<a href="${buyLink.href}" target="_blank">🔑 ${name} — ${buyLink.dataset.actionLabel || '获取 API Key'}</a>
|
||||
`;
|
||||
}
|
||||
// Add common alternatives
|
||||
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() {
|
||||
const inp = document.getElementById('apiKey');
|
||||
inp.type = inp.type === 'password' ? 'text' : 'password';
|
||||
}
|
||||
|
||||
// --- Generate correct OpenClaw config ---
|
||||
function buildOpenClawConfig(provider, base, model, apiKey) {
|
||||
// Base config that OpenClaw expects
|
||||
const 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 the built-in zai provider
|
||||
if (provider === 'zai') {
|
||||
return {
|
||||
...baseConfig,
|
||||
env: { ZAI_API_KEY: apiKey },
|
||||
agents: { defaults: { model: { primary: 'zai/' + model } } }
|
||||
};
|
||||
}
|
||||
|
||||
// All other providers use models.providers (OpenAI-compatible)
|
||||
const providerName = provider || 'myProvider';
|
||||
return {
|
||||
...baseConfig,
|
||||
models: {
|
||||
mode: 'merge',
|
||||
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
|
||||
}]
|
||||
}
|
||||
}
|
||||
},
|
||||
agents: { defaults: { model: { primary: providerName + '/' + model } } }
|
||||
};
|
||||
}
|
||||
|
||||
// --- Save config ---
|
||||
async function saveConfig() {
|
||||
let apiKey = document.getElementById('apiKey').value.trim();
|
||||
// 本地模型(Ollama/LM Studio)不需要真实 Key,OpenClaw 配置结构里仍要有 apiKey 字段
|
||||
if (selectedLocal) {
|
||||
if (!apiKey) apiKey = 'local';
|
||||
} else if (!apiKey) {
|
||||
showToast('请填写 API Key', true); return;
|
||||
}
|
||||
|
||||
let base = selectedBase;
|
||||
let model = selectedModel;
|
||||
|
||||
if (selectedProvider === 'custom') {
|
||||
base = document.getElementById('customBase').value.trim();
|
||||
model = document.getElementById('customModel').value.trim();
|
||||
if (!base || !model) { showToast('请填写 API 地址和模型名称', true); return; }
|
||||
}
|
||||
|
||||
const openclawConfig = buildOpenClawConfig(selectedProvider, base, model, apiKey);
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/config', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(openclawConfig)
|
||||
});
|
||||
if (!res.ok) throw new Error('保存失败');
|
||||
|
||||
showToast('配置已保存!');
|
||||
goStep(3);
|
||||
const port = getPort();
|
||||
document.getElementById('gatewayUrl').textContent = 'http://127.0.0.1:' + port;
|
||||
} catch (e) {
|
||||
showToast('保存失败: ' + e.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Toast ---
|
||||
function showToast(msg, isError) {
|
||||
const t = document.getElementById('toast');
|
||||
t.textContent = msg;
|
||||
t.className = 'toast show' + (isError ? ' error' : '');
|
||||
setTimeout(() => t.className = 'toast', 3000);
|
||||
}
|
||||
|
||||
// --- Load existing config ---
|
||||
async function loadConfig() {
|
||||
try {
|
||||
const res = await fetch('/api/config');
|
||||
const cfg = await res.json();
|
||||
|
||||
// Detect zai provider (智谱 GLM)
|
||||
if (cfg.env && cfg.env.ZAI_API_KEY) {
|
||||
const card = document.querySelector('.model-card[data-provider="zai"]');
|
||||
if (card) card.click();
|
||||
document.getElementById('apiKey').value = cfg.env.ZAI_API_KEY;
|
||||
return;
|
||||
}
|
||||
|
||||
// Detect models.providers format
|
||||
if (cfg.models && cfg.models.providers) {
|
||||
const providerName = Object.keys(cfg.models.providers)[0];
|
||||
if (providerName) {
|
||||
const p = cfg.models.providers[providerName];
|
||||
document.querySelectorAll('.model-card').forEach(card => {
|
||||
if (card.dataset.provider === providerName) card.click();
|
||||
});
|
||||
if (p.apiKey) document.getElementById('apiKey').value = p.apiKey;
|
||||
}
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
loadConfig();
|
||||
|
||||
// --- 本地模型自动发现(Ollama / LM Studio)---
|
||||
// 探测 /api/local-models,发现本机已运行的本地模型就动态生成卡片,点选即用、无需 Key。
|
||||
async function discoverLocalModels() {
|
||||
try {
|
||||
const res = await fetch('/api/local-models');
|
||||
if (!res.ok) return;
|
||||
const { providers } = await res.json();
|
||||
if (!providers || !providers.length) return;
|
||||
|
||||
const grid = document.getElementById('localModelGrid');
|
||||
grid.innerHTML = '';
|
||||
providers.forEach(p => {
|
||||
// 每个本地 provider 最多展示前 6 个模型,避免列表过长
|
||||
p.models.slice(0, 6).forEach(modelId => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'model-card';
|
||||
card.dataset.provider = p.provider; // ollama / lmstudio
|
||||
card.dataset.base = p.base;
|
||||
card.dataset.model = modelId;
|
||||
card.dataset.local = '1'; // 标记:本地模型免 Key
|
||||
card.innerHTML = `
|
||||
<span class="check">✓</span>
|
||||
<h4>${p.label} <span class="tag free">本地</span></h4>
|
||||
<p>${modelId}</p>`;
|
||||
card.addEventListener('click', () => {
|
||||
document.querySelectorAll('.model-card').forEach(c => c.classList.remove('selected'));
|
||||
card.classList.add('selected');
|
||||
selectedProvider = p.provider;
|
||||
selectedBase = p.base;
|
||||
selectedModel = modelId;
|
||||
selectedLocal = true;
|
||||
document.getElementById('nextStep1').disabled = false;
|
||||
});
|
||||
grid.appendChild(card);
|
||||
});
|
||||
});
|
||||
document.getElementById('localModelsBox').style.display = 'block';
|
||||
} catch { /* 无本地模型或探测失败:静默 */ }
|
||||
}
|
||||
let selectedLocal = false;
|
||||
// 选云端卡片时重置本地标记
|
||||
document.querySelectorAll('#step1 > .model-grid .model-card').forEach(c => {
|
||||
c.addEventListener('click', () => { selectedLocal = false; });
|
||||
});
|
||||
discoverLocalModels();
|
||||
|
||||
// Allow direct step preview via URL param, e.g. ?step=3
|
||||
const stepParam = new URLSearchParams(window.location.search).get('step');
|
||||
if (stepParam) goStep(parseInt(stepParam));
|
||||
|
||||
// --- Step 3: Quick entry buttons ---
|
||||
function getPort() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
return params.get('port') || '18789';
|
||||
}
|
||||
|
||||
function openDashboard() {
|
||||
const port = getPort();
|
||||
window.open('http://127.0.0.1:' + port + '/#token=uclaw', '_blank');
|
||||
}
|
||||
|
||||
function openOnboard() {
|
||||
const port = getPort();
|
||||
window.open('http://127.0.0.1:' + port + '/onboarding', '_blank');
|
||||
}
|
||||
|
||||
function openChannel(type) {
|
||||
const port = getPort();
|
||||
window.open('http://127.0.0.1:' + port + '/#/channels', '_blank');
|
||||
}
|
||||
|
||||
function toggleChannel(card, type) {
|
||||
const form = document.getElementById('form-' + type);
|
||||
const isOpen = form.classList.contains('open');
|
||||
if (isOpen) {
|
||||
form.classList.remove('open');
|
||||
card.classList.remove('selected');
|
||||
} else {
|
||||
form.classList.add('open');
|
||||
card.classList.add('selected');
|
||||
// Focus first input
|
||||
const firstInput = form.querySelector('input');
|
||||
if (firstInput) setTimeout(() => firstInput.focus(), 100);
|
||||
}
|
||||
}
|
||||
|
||||
async function launchOpenClaw() {
|
||||
// 1. Collect channel configs
|
||||
const channels = {};
|
||||
|
||||
const tgToken = document.getElementById('ch-telegram-token')?.value.trim();
|
||||
if (tgToken) channels.telegram = { enabled: true, token: tgToken, dmPolicy: 'pairing' };
|
||||
|
||||
const qqId = document.getElementById('ch-qqbot-appid')?.value.trim();
|
||||
const qqSecret = document.getElementById('ch-qqbot-secret')?.value.trim();
|
||||
if (qqId && qqSecret) channels.qqbot = { enabled: true, appId: qqId, clientSecret: qqSecret };
|
||||
|
||||
const fsId = document.getElementById('ch-feishu-appid')?.value.trim();
|
||||
const fsSecret = document.getElementById('ch-feishu-secret')?.value.trim();
|
||||
if (fsId && fsSecret) channels.feishu = { enabled: true, appId: fsId, appSecret: fsSecret };
|
||||
|
||||
const wcBotId = document.getElementById('ch-wecom-botid')?.value.trim();
|
||||
const wcSecret = document.getElementById('ch-wecom-secret')?.value.trim();
|
||||
if (wcBotId && wcSecret) channels.wecom = { enabled: true, botId: wcBotId, secret: wcSecret };
|
||||
|
||||
const ddId = document.getElementById('ch-dingtalk-clientid')?.value.trim();
|
||||
const ddSecret = document.getElementById('ch-dingtalk-secret')?.value.trim();
|
||||
if (ddId && ddSecret) channels.dingtalk = { enabled: true, clientId: ddId, clientSecret: ddSecret };
|
||||
|
||||
// 2. Save channel config if any (merge into existing config)
|
||||
if (Object.keys(channels).length > 0) {
|
||||
try {
|
||||
const res = await fetch('/api/config');
|
||||
const 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)
|
||||
});
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
// 3. Launch
|
||||
showToast('配置已保存!请关闭此页面,回到启动窗口使用 U-Claw');
|
||||
try { await fetch('/api/done', { method: 'POST' }); } catch(e) {}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,235 +0,0 @@
|
||||
#!/bin/bash
|
||||
# ============================================================
|
||||
# U-Claw - Portable AI Agent (macOS)
|
||||
# Double-click to start / 双击启动
|
||||
# ============================================================
|
||||
|
||||
UCLAW_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
APP_DIR="$UCLAW_DIR/app"
|
||||
CORE_DIR="$APP_DIR/core"
|
||||
DATA_DIR="$UCLAW_DIR/data"
|
||||
STATE_DIR="$DATA_DIR/.openclaw"
|
||||
CONFIG_FILE="$STATE_DIR/openclaw.json"
|
||||
|
||||
# Migration shim: rename old core-mac to core for existing USB users
|
||||
if [ -d "$APP_DIR/core-mac" ] && [ ! -d "$APP_DIR/core" ]; then
|
||||
mv "$APP_DIR/core-mac" "$APP_DIR/core"
|
||||
fi
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
CYAN='\033[0;36m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
echo ""
|
||||
echo -e "${CYAN}"
|
||||
echo " ╔══════════════════════════════════════╗"
|
||||
echo " ║ 🦞 U-Claw v1.1 ║"
|
||||
echo " ║ Portable AI Agent ║"
|
||||
echo " ╚══════════════════════════════════════╝"
|
||||
echo -e "${NC}"
|
||||
|
||||
# ---- 1. Detect CPU & set runtime ----
|
||||
ARCH=$(uname -m)
|
||||
if [ "$ARCH" = "arm64" ]; then
|
||||
NODE_DIR="$APP_DIR/runtime/node-mac-arm64"
|
||||
echo -e " ${GREEN}Apple Silicon (M series)${NC}"
|
||||
elif [ "$ARCH" = "x86_64" ]; then
|
||||
NODE_DIR="$APP_DIR/runtime/node-mac-x64"
|
||||
echo -e " ${GREEN}Intel Mac (x64)${NC}"
|
||||
else
|
||||
echo -e " ${RED}Unsupported architecture: $ARCH${NC}"
|
||||
echo ""
|
||||
read -p " Press Enter to exit..."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
NODE_BIN="$NODE_DIR/bin/node"
|
||||
export PATH="$NODE_DIR/bin:$PATH"
|
||||
|
||||
# ---- 2. Remove macOS quarantine ----
|
||||
if xattr -l "$NODE_BIN" 2>/dev/null | grep -q "com.apple.quarantine"; then
|
||||
echo -e " ${YELLOW}Removing macOS security restriction...${NC}"
|
||||
xattr -rd com.apple.quarantine "$UCLAW_DIR" 2>/dev/null || true
|
||||
echo -e " ${GREEN}Done${NC}"
|
||||
fi
|
||||
|
||||
# ---- 3. Check runtime ----
|
||||
if [ ! -f "$NODE_BIN" ]; then
|
||||
echo -e " ${RED}Error: Node.js runtime not found${NC}"
|
||||
echo " Please run: bash setup.sh"
|
||||
read -p " Press Enter to exit..."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
NODE_VER=$("$NODE_BIN" --version)
|
||||
echo -e " Node.js: ${GREEN}${NODE_VER}${NC}"
|
||||
echo ""
|
||||
|
||||
# ---- 4. Init data directories ----
|
||||
mkdir -p "$STATE_DIR" "$DATA_DIR/memory" "$DATA_DIR/backups" "$DATA_DIR/logs"
|
||||
|
||||
# ---- 4b. 加速:把"重 IO、可重建"的缓存从 U 盘搬到本机硬盘 ----
|
||||
# portable-cache.mjs 算出本机缓存目录(~/Library/Caches/U-Claw/slot,UUID 隔离,
|
||||
# 换盘符仍复用),并把 .openclaw/browser 做成 symlink 指向本机盘。
|
||||
# 浏览器 user-data(几百 MB 随机小写)和 V8 编译缓存因此落本机盘,不再拖慢 U 盘。
|
||||
# 静默失败:取不到就跳过,缓存留 U 盘,照常启动。
|
||||
while IFS='=' read -r _k _v; do
|
||||
case "$_k" in
|
||||
UCLAW_COMPILE_CACHE_DIR) export NODE_COMPILE_CACHE="$_v" ;;
|
||||
UCLAW_CACHE_ROOT) UCLAW_CACHE_ROOT="$_v" ;;
|
||||
esac
|
||||
done < <("$NODE_BIN" "$UCLAW_DIR/lib/portable-cache.mjs" "$STATE_DIR" "$UCLAW_DIR" 2>/dev/null)
|
||||
[ -n "$NODE_COMPILE_CACHE" ] && echo -e " ${GREEN}Cache on local disk:${NC} $UCLAW_CACHE_ROOT"
|
||||
|
||||
# ---- 5. Default config ----
|
||||
if [ ! -f "$CONFIG_FILE" ]; then
|
||||
if [ -f "$DATA_DIR/config.json" ]; then
|
||||
echo -e " ${YELLOW}Migrating legacy config...${NC}"
|
||||
cp "$DATA_DIR/config.json" "$CONFIG_FILE"
|
||||
echo -e " ${GREEN}Config migrated${NC}"
|
||||
else
|
||||
echo -e " ${YELLOW}First run - creating default config...${NC}"
|
||||
cat > "$CONFIG_FILE" << 'CFGEOF'
|
||||
{
|
||||
"gateway": {
|
||||
"mode": "local",
|
||||
"auth": { "token": "uclaw" }
|
||||
}
|
||||
}
|
||||
CFGEOF
|
||||
echo -e " ${GREEN}Config created${NC}"
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# ---- 6. Set environment (portable mode) ----
|
||||
export OPENCLAW_HOME="$DATA_DIR"
|
||||
export OPENCLAW_STATE_DIR="$STATE_DIR"
|
||||
export OPENCLAW_CONFIG_PATH="$CONFIG_FILE"
|
||||
# U-Claw opens the local dashboard directly; disable mDNS/Bonjour discovery.
|
||||
# On macOS the bonjour plugin auto-starts and advertises the gateway on the LAN
|
||||
# (_openclaw-gw._tcp.local), which is unnecessary for local use and triggers
|
||||
# "no IPv4 address available on utunN" warnings on machines with VPN/Tailscale.
|
||||
export OPENCLAW_DISABLE_BONJOUR=1
|
||||
|
||||
# ---- 7. Check dependencies ----
|
||||
if [ ! -d "$CORE_DIR/node_modules" ]; then
|
||||
echo -e " ${YELLOW}[WARN] node_modules not found${NC}"
|
||||
echo " This release should ship with deps pre-installed."
|
||||
echo " Falling back to npm install (USB drives may take 20+ min)."
|
||||
echo " TIP: re-download u-claw-portable-*.zip with bundled deps."
|
||||
cd "$CORE_DIR"
|
||||
# 把 npm 缓存留在盘内,避免污染系统 ~/.npm(拔盘不留痕)
|
||||
npm_config_cache="$APP_DIR/.npm-cache" \
|
||||
"$NODE_BIN" "$NODE_DIR/bin/npm" install --registry=https://registry.npmmirror.com --ignore-scripts --no-audit --no-fund --omit=dev 2>&1
|
||||
echo -e " ${GREEN}Dependencies installed${NC}"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# ---- 7b. Async update check (non-blocking, 5s timeout, silent failure) ----
|
||||
# Writes data/.openclaw/update-available.json if a newer version is on OSS.
|
||||
# Welcome.html / Config.html read this file and show a banner.
|
||||
# Version file lookup: portable/OPENCLAW_VERSION (USB) → ../OPENCLAW_VERSION (dev)
|
||||
VERSION_FILE="$UCLAW_DIR/OPENCLAW_VERSION"
|
||||
[ -f "$VERSION_FILE" ] || VERSION_FILE="$UCLAW_DIR/../OPENCLAW_VERSION"
|
||||
if [ -f "$VERSION_FILE" ]; then
|
||||
"$NODE_BIN" "$UCLAW_DIR/lib/check-update.mjs" "$VERSION_FILE" "$STATE_DIR" >/dev/null 2>&1 &
|
||||
fi
|
||||
|
||||
# ---- 7c. Intranet/self-hosted model fix ----
|
||||
# Keep the configured model host(s) off any corporate HTTP_PROXY/HTTPS_PROXY.
|
||||
# OpenClaw routes ALL fetch through the env proxy when it is set, which breaks
|
||||
# calls to internal model endpoints (http://10.x / 192.168.x / a machine-room IP).
|
||||
# Add those hosts + loopback to NO_PROXY so they connect directly.
|
||||
# Silent no-op when no proxy/model is configured.
|
||||
NO_PROXY_LINE="$("$NODE_BIN" "$UCLAW_DIR/lib/resolve-no-proxy.mjs" "$CONFIG_FILE" 2>/dev/null)"
|
||||
case "$NO_PROXY_LINE" in
|
||||
UCLAW_NO_PROXY=*)
|
||||
export NO_PROXY="${NO_PROXY_LINE#UCLAW_NO_PROXY=}"
|
||||
export no_proxy="$NO_PROXY"
|
||||
echo " Direct-connect (NO_PROXY): $NO_PROXY"
|
||||
;;
|
||||
esac
|
||||
|
||||
# ---- 8. Find available port ----
|
||||
PORT=18789
|
||||
while lsof -i :$PORT >/dev/null 2>&1; do
|
||||
echo -e " ${YELLOW}Port $PORT in use, trying next...${NC}"
|
||||
PORT=$((PORT + 1))
|
||||
if [ $PORT -gt 18799 ]; then
|
||||
echo -e " ${RED}No available port (18789-18799)${NC}"
|
||||
read -p " Press Enter to exit..."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# ---- 9. Start Config Server in background ----
|
||||
echo -e " ${CYAN}Starting Config Center on port 18788...${NC}"
|
||||
CONFIG_SERVER="$UCLAW_DIR/config-server"
|
||||
"$NODE_BIN" "$CONFIG_SERVER/server.js" &
|
||||
CONFIG_PID=$!
|
||||
sleep 1
|
||||
|
||||
# ---- 10. Start gateway ----
|
||||
echo -e " ${CYAN}Starting OpenClaw on port $PORT...${NC}"
|
||||
echo ""
|
||||
|
||||
cd "$CORE_DIR"
|
||||
OPENCLAW_MJS="$CORE_DIR/node_modules/openclaw/openclaw.mjs"
|
||||
"$NODE_BIN" "$OPENCLAW_MJS" gateway run --allow-unconfigured --force --port $PORT &
|
||||
GW_PID=$!
|
||||
|
||||
# ---- 11. 立刻打开"启动首屏",给用户即时反馈(移植自 4.0 splash)----
|
||||
# 首屏 loading.html 自己轮询 /ready,就绪后停在选择页,不再自动冲进 Dashboard。
|
||||
echo -e " ${YELLOW}首次启动需准备运行环境,约 30-90 秒,请稍候...${NC}"
|
||||
# 用 file:// URL 确保 query string(?port=)能传给浏览器;裸路径 open 会把整串当文件名。
|
||||
open "file://$UCLAW_DIR/lib/loading.html?port=$PORT&token=uclaw" 2>/dev/null || true
|
||||
# 每次都打开 Config Center,方便改模型、充值/获取 Key、连接微信等渠道。
|
||||
open "http://127.0.0.1:18788/" 2>/dev/null || true
|
||||
|
||||
# ---- 11b. gateway 首轮预热(后台、静默、非阻塞)----
|
||||
# 就绪后先唤醒 config/model 子系统,用户首次点发送时不再等。移植自 4.0 first-turn-prewarm。
|
||||
"$NODE_BIN" "$UCLAW_DIR/lib/prewarm.mjs" "$PORT" uclaw >/dev/null 2>&1 &
|
||||
|
||||
# ---- 11c. 兜底:万一首屏页的 file:// fetch 被浏览器拦,仍静默轮询端口 ----
|
||||
# 慢盘首启可达 90s+,轮询上限覆盖这段。最多 ~3 分钟(180×1s)。
|
||||
(
|
||||
for i in $(seq 1 180); do
|
||||
if curl -s -o /dev/null "http://127.0.0.1:$PORT/" 2>/dev/null; then
|
||||
exit 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
) &
|
||||
|
||||
echo -e " ${GREEN}════════════════════════════════${NC}"
|
||||
echo -e " ${GREEN}🦞 U-Claw is running!${NC}"
|
||||
echo -e " ${GREEN} Dashboard: http://127.0.0.1:$PORT/#token=uclaw${NC}"
|
||||
echo -e " ${GREEN} Config Center: http://127.0.0.1:18788/${NC}"
|
||||
echo ""
|
||||
echo -e " ${YELLOW}Press Ctrl+C to stop${NC}"
|
||||
echo -e " ${GREEN}════════════════════════════════${NC}"
|
||||
echo ""
|
||||
|
||||
# ---- Cleanup on exit ----
|
||||
cleanup() {
|
||||
kill $GW_PID 2>/dev/null
|
||||
kill $CONFIG_PID 2>/dev/null
|
||||
echo ""
|
||||
echo -e " 🦞 U-Claw stopped."
|
||||
exit 0
|
||||
}
|
||||
trap cleanup INT TERM
|
||||
|
||||
wait $GW_PID
|
||||
GW_EXIT=$?
|
||||
|
||||
# Ctrl+C 走 trap cleanup(exit 0)不会到这;走到这里说明 gateway 自己退了。
|
||||
if [ "$GW_EXIT" -ne 0 ]; then
|
||||
echo -e " ${YELLOW}OpenClaw exited unexpectedly (code $GW_EXIT)${NC}"
|
||||
fi
|
||||
kill $CONFIG_PID 2>/dev/null
|
||||
echo ""
|
||||
echo -e " 🦞 U-Claw stopped."
|
||||
@@ -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="welcome.title">U-Claw — Getting Started</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
@@ -164,90 +164,90 @@ h1 .emoji { font-size: 1.2em; }
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
<script src="data/.openclaw/locale.js"></script>
|
||||
<script src="lib/i18n/messages.js"></script>
|
||||
<script src="lib/i18n/i18n.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1><span class="emoji">🦞</span> U-Claw 使用指南</h1>
|
||||
<div class="subtitle">AI助手便携U盘 · 3步开始使用</div>
|
||||
<h1><span class="emoji">🦞</span> <span data-i18n="welcome.title">U-Claw — Getting Started</span></h1>
|
||||
<div class="subtitle" data-i18n="welcome.subtitle">Your AI assistant on a USB drive · three steps</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-title">快速开始</div>
|
||||
<div class="section-title" data-i18n="welcome.quickstart">Quick start</div>
|
||||
<div class="steps">
|
||||
<div class="step">
|
||||
<div class="step-title">第1步:启动服务</div>
|
||||
<div class="step-title" data-i18n="welcome.step1_title">1. Start U-Claw</div>
|
||||
<div class="step-desc">
|
||||
根据您的系统,双击对应的启动文件:<br><br>
|
||||
<strong>Windows 用户:</strong>双击 <code>Windows-Start.bat</code><br>
|
||||
<strong>Mac 用户:</strong>双击 <code>Mac-Start.command</code>
|
||||
<span data-i18n="welcome.step1_desc">Double-click the file for your computer:</span><br><br>
|
||||
<strong>Windows</strong> — <code>START HERE - Windows.bat</code><br>
|
||||
<strong>Mac</strong> — <code>START HERE - Mac.command</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step">
|
||||
<div class="step-title">第2步:配置AI模型</div>
|
||||
<div class="step-title" data-i18n="welcome.step2_title">2. Add a key</div>
|
||||
<div class="step-desc">
|
||||
首次启动会自动打开配置页面,按照提示操作:<br>
|
||||
1. 选择一个AI模型(推荐 <strong>DeepSeek</strong> - 便宜好用)<br>
|
||||
2. 填写API Key(去模型官网注册获取)<br>
|
||||
3. 点击"保存配置"
|
||||
<span data-i18n="welcome.step2_desc">Settings opens on its own the first time. You need one API key to get going — Google Gemini is the easiest to obtain: sign in with a Google account, no card required.</span><br>
|
||||
<a href="https://aistudio.google.com/apikey" target="_blank" rel="noopener" data-i18n="welcome.step2_link">Get a free Gemini key →</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step">
|
||||
<div class="step-title">第3步:开始对话</div>
|
||||
<div class="step-title" data-i18n="welcome.step3_title">3. Start talking</div>
|
||||
<div class="step-desc">
|
||||
配置完成后,浏览器会自动打开AI对话界面<br>
|
||||
地址:<code>http://127.0.0.1:18789</code><br>
|
||||
现在可以和AI助手对话了!
|
||||
<span data-i18n="welcome.step3_desc">Once the key is saved, the chat window opens by itself. That is it.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="success-box">
|
||||
<p>✅ 所有配置和对话记录保存在U盘的 <code>data/</code> 目录,Mac和Windows自动共享!</p>
|
||||
<p data-i18n="welcome.portable_note">✅ Your settings and history stay in the <code>data/</code> folder on the drive, so they travel with you between Mac and Windows.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-title">推荐AI模型</div>
|
||||
<div class="section-title" data-i18n="welcome.models_title">Which model?</div>
|
||||
<div class="model-grid">
|
||||
<div class="model-item">
|
||||
<strong>🔥 DeepSeek</strong>
|
||||
<span>编程首选,极便宜</span>
|
||||
</div>
|
||||
<div class="model-item">
|
||||
<strong>📄 Kimi</strong>
|
||||
<span>长文档处理专家</span>
|
||||
</div>
|
||||
<div class="model-item">
|
||||
<strong>🆓 通义千问</strong>
|
||||
<span>免费额度大</span>
|
||||
</div>
|
||||
<div class="model-item">
|
||||
<strong>🌟 Claude</strong>
|
||||
<span>最强理解能力</span>
|
||||
<span data-i18n="welcome.model_claude">Best all-rounder</span>
|
||||
</div>
|
||||
<div class="model-item">
|
||||
<strong>💬 GPT</strong>
|
||||
<span data-i18n="welcome.model_gpt">Most widely used</span>
|
||||
</div>
|
||||
<div class="model-item">
|
||||
<strong>🆓 Gemini</strong>
|
||||
<span data-i18n="welcome.model_gemini">Free tier, easiest signup</span>
|
||||
</div>
|
||||
<div class="model-item">
|
||||
<strong>🔒 Local model</strong>
|
||||
<span data-i18n="welcome.model_local">Offline · nothing leaves this machine</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="subtitle" style="margin-top:14px" data-i18n="welcome.models_more">More options, including SEA-LION for Malay, Tamil and Singlish, are in Settings.</p>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-title">遇到问题?</div>
|
||||
<div class="section-title" data-i18n="welcome.trouble_title">Something not working?</div>
|
||||
<div class="tip-box">
|
||||
<strong>💡 运行诊断工具</strong>
|
||||
<strong data-i18n="welcome.trouble_lead">💡 Run the check-up tool</strong>
|
||||
<p>
|
||||
如果启动失败,双击运行诊断工具:<br>
|
||||
Windows: <code>Windows-Diagnose.bat</code><br>
|
||||
Mac: <code>Mac-Diagnose.command</code>
|
||||
<span data-i18n="welcome.trouble_desc">If U-Claw will not start, double-click:</span><br>
|
||||
Windows — <code>Windows-Diagnose.bat</code><br>
|
||||
Mac — <code>Mac-Diagnose.command</code>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; margin-top: 30px;">
|
||||
<a href="Config.html" class="btn">打开配置页面</a>
|
||||
<a href="https://u-claw.org" class="btn" target="_blank">查看文档</a>
|
||||
<a href="advanced/Config.html" class="btn" data-i18n="welcome.open_settings">Open Settings</a>
|
||||
<a href="https://u-claw.org" class="btn" target="_blank" rel="noopener" data-i18n="welcome.open_docs">Documentation</a>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
U-Claw v1.1 · MIT License · <a href="https://github.com/dongsheng123132/u-claw" target="_blank" style="color: #667eea;">GitHub</a>
|
||||
U-Claw v1.1 · MIT License · <a href="https://gitea.fanghe.it.com/zhenghy/u-claw" target="_blank" style="color: #667eea;">GitHub</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
33
portable/START HERE - Mac.command
Executable file
33
portable/START HERE - Mac.command
Executable file
@@ -0,0 +1,33 @@
|
||||
#!/bin/bash
|
||||
# ============================================================
|
||||
# U-Claw - Portable AI Agent (macOS)
|
||||
# Double-click to start
|
||||
# ============================================================
|
||||
#
|
||||
# Thin launcher: locate the bundled node and hand off to lib/start.mjs.
|
||||
# All checks, messages and error handling live there so macOS and Windows
|
||||
# cannot drift apart and so every string can be translated.
|
||||
|
||||
UCLAW_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
if [ "$(uname -m)" = "arm64" ]; then
|
||||
NODE_BIN="$UCLAW_DIR/app/runtime/node-mac-arm64/bin/node"
|
||||
else
|
||||
NODE_BIN="$UCLAW_DIR/app/runtime/node-mac-x64/bin/node"
|
||||
fi
|
||||
|
||||
if [ ! -x "$NODE_BIN" ]; then
|
||||
echo ""
|
||||
echo " U-Claw is missing a file it needs to start."
|
||||
echo " The copy onto this drive probably did not finish."
|
||||
echo ""
|
||||
echo " Copy the U-Claw folder onto the drive again, or run: bash setup.sh"
|
||||
echo ""
|
||||
read -r -p " Press Enter to close this window."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
"$NODE_BIN" "$UCLAW_DIR/lib/start.mjs"
|
||||
EXIT_CODE=$?
|
||||
[ "$EXIT_CODE" -ne 0 ] && read -r -p " Press Enter to close this window."
|
||||
exit $EXIT_CODE
|
||||
28
portable/START HERE - Windows.bat
Normal file
28
portable/START HERE - Windows.bat
Normal file
@@ -0,0 +1,28 @@
|
||||
@echo off
|
||||
chcp 65001 >nul 2>&1
|
||||
title U-Claw
|
||||
|
||||
REM Thin launcher: locate the bundled node and hand off to lib\start.mjs.
|
||||
REM All checks, messages and error handling live there so Windows and macOS
|
||||
REM cannot drift apart, and so nothing user-facing has to be written in a .bat
|
||||
REM (cmd.exe mis-parses non-ASCII as GBK, and treats an unescaped ) as
|
||||
REM block-end -- both were recurring sources of flash-exit bugs).
|
||||
|
||||
set "UCLAW_DIR=%~dp0"
|
||||
set "NODE_BIN=%UCLAW_DIR%app\runtime\node-win-x64\node.exe"
|
||||
|
||||
if not exist "%NODE_BIN%" (
|
||||
echo.
|
||||
echo U-Claw is missing a file it needs to start.
|
||||
echo The copy onto this drive probably did not finish.
|
||||
echo.
|
||||
echo Copy the U-Claw folder onto the drive again, then try once more.
|
||||
echo.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
"%NODE_BIN%" "%UCLAW_DIR%lib\start.mjs"
|
||||
set "EXIT_CODE=%errorlevel%"
|
||||
pause
|
||||
exit /b %EXIT_CODE%
|
||||
@@ -1,703 +0,0 @@
|
||||
<!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 SkillHub - AI 技能市场</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, "Microsoft YaHei", "PingFang SC", sans-serif; background: #0a0a0a; color: #e0e0e0; min-height: 100vh; }
|
||||
|
||||
/* ── Sidebar ── */
|
||||
.sidebar {
|
||||
position: fixed; left: 0; top: 0; width: 220px; height: 100vh;
|
||||
background: #111; border-right: 1px solid #222; padding: 24px 0;
|
||||
display: flex; flex-direction: column; z-index: 100;
|
||||
}
|
||||
.sidebar-logo { padding: 0 20px 24px; border-bottom: 1px solid #222; }
|
||||
.sidebar-logo h2 { font-size: 1.3em; }
|
||||
.sidebar-logo h2 span { color: #ff6b35; }
|
||||
.sidebar-logo p { color: #666; font-size: 0.8em; margin-top: 4px; }
|
||||
.sidebar-nav { flex: 1; padding: 16px 0; overflow-y: auto; }
|
||||
.sidebar-nav a {
|
||||
display: block; padding: 10px 20px; color: #999; text-decoration: none;
|
||||
font-size: 0.95em; transition: all 0.2s; border-left: 3px solid transparent;
|
||||
}
|
||||
.sidebar-nav a:hover { color: #fff; background: rgba(255,255,255,0.03); }
|
||||
.sidebar-nav a.active { color: #ff6b35; border-left-color: #ff6b35; background: rgba(255,107,53,0.05); }
|
||||
.sidebar-nav .nav-divider { height: 1px; background: #222; margin: 12px 20px; }
|
||||
|
||||
/* ── Main Content ── */
|
||||
.main { margin-left: 220px; }
|
||||
|
||||
/* ── Hero ── */
|
||||
.hero {
|
||||
background: linear-gradient(135deg, #1a0a00 0%, #0a0a0a 50%, #0a1020 100%);
|
||||
padding: 60px 40px 50px; text-align: center;
|
||||
border-bottom: 1px solid #222;
|
||||
}
|
||||
.hero h1 { font-size: 2.4em; margin-bottom: 12px; }
|
||||
.hero h1 .accent { color: #ff6b35; }
|
||||
.hero .subtitle { color: #999; font-size: 1.15em; max-width: 600px; margin: 0 auto 28px; line-height: 1.6; }
|
||||
.hero .stats {
|
||||
display: flex; justify-content: center; gap: 40px; margin-bottom: 32px;
|
||||
}
|
||||
.hero .stat-item { text-align: center; }
|
||||
.hero .stat-num { font-size: 2em; font-weight: 700; color: #ff6b35; }
|
||||
.hero .stat-label { color: #666; font-size: 0.85em; margin-top: 2px; }
|
||||
|
||||
/* ── Search ── */
|
||||
.search-bar {
|
||||
max-width: 560px; margin: 0 auto; display: flex;
|
||||
background: #1a1a1a; border: 1px solid #333; border-radius: 10px; overflow: hidden;
|
||||
}
|
||||
.search-bar input {
|
||||
flex: 1; padding: 14px 20px; background: transparent; border: none;
|
||||
color: #fff; font-size: 1em; outline: none;
|
||||
}
|
||||
.search-bar input::placeholder { color: #555; }
|
||||
.search-bar button {
|
||||
padding: 14px 24px; background: #ff6b35; border: none; color: #fff;
|
||||
font-size: 1em; cursor: pointer; transition: background 0.2s;
|
||||
}
|
||||
.search-bar button:hover { background: #e55a28; }
|
||||
|
||||
/* ── Tab bar ── */
|
||||
.tab-bar {
|
||||
display: flex; gap: 0; border-bottom: 1px solid #222;
|
||||
padding: 0 40px; background: #0f0f0f; position: sticky; top: 0; z-index: 50;
|
||||
}
|
||||
.tab-bar a {
|
||||
padding: 14px 20px; color: #888; text-decoration: none; font-size: 0.95em;
|
||||
border-bottom: 2px solid transparent; transition: all 0.2s;
|
||||
}
|
||||
.tab-bar a:hover { color: #ccc; }
|
||||
.tab-bar a.active { color: #ff6b35; border-bottom-color: #ff6b35; }
|
||||
|
||||
/* ── Content Area ── */
|
||||
.content { padding: 30px 40px 60px; max-width: 1100px; }
|
||||
|
||||
/* ── Section ── */
|
||||
.section-header {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
margin: 32px 0 16px;
|
||||
}
|
||||
.section-header h2 { font-size: 1.3em; }
|
||||
.section-header .badge {
|
||||
background: rgba(255,107,53,0.15); color: #ff6b35; padding: 3px 10px;
|
||||
border-radius: 12px; font-size: 0.8em;
|
||||
}
|
||||
.section-header a { color: #ff6b35; text-decoration: none; font-size: 0.9em; }
|
||||
.section-header a:hover { text-decoration: underline; }
|
||||
|
||||
/* ── Skill Card Grid ── */
|
||||
.skill-grid {
|
||||
display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
.skill-card {
|
||||
background: #1a1a1a; border: 1px solid #282828; border-radius: 12px;
|
||||
padding: 20px; cursor: pointer; transition: all 0.2s; position: relative;
|
||||
}
|
||||
.skill-card:hover { border-color: #ff6b35; transform: translateY(-2px); box-shadow: 0 8px 24px rgba(0,0,0,0.3); }
|
||||
.skill-card .card-top { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
|
||||
.skill-card .card-icon {
|
||||
width: 44px; height: 44px; border-radius: 10px; display: flex;
|
||||
align-items: center; justify-content: center; font-size: 1.5em; flex-shrink: 0;
|
||||
}
|
||||
.skill-card .card-title { font-size: 1.05em; font-weight: 600; color: #fff; }
|
||||
.skill-card .card-author { font-size: 0.8em; color: #666; margin-top: 2px; }
|
||||
.skill-card .card-desc { color: #999; font-size: 0.88em; line-height: 1.5; margin-bottom: 14px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
||||
.skill-card .card-footer { display: flex; align-items: center; gap: 16px; font-size: 0.8em; color: #666; }
|
||||
.skill-card .card-footer .downloads { color: #ff6b35; }
|
||||
.skill-card .card-tag {
|
||||
display: inline-block; background: #222; padding: 2px 8px; border-radius: 4px;
|
||||
font-size: 0.75em; color: #888;
|
||||
}
|
||||
.skill-card .featured-badge {
|
||||
position: absolute; top: 12px; right: 12px;
|
||||
background: linear-gradient(135deg, #ff6b35, #ff9500); color: #fff;
|
||||
padding: 2px 8px; border-radius: 4px; font-size: 0.7em; font-weight: 600;
|
||||
}
|
||||
|
||||
/* ── Category Grid ── */
|
||||
.cat-grid {
|
||||
display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
.cat-card {
|
||||
background: #1a1a1a; border: 1px solid #282828; border-radius: 10px;
|
||||
padding: 16px; text-align: center; cursor: pointer; transition: all 0.2s;
|
||||
}
|
||||
.cat-card:hover { border-color: #ff6b35; background: rgba(255,107,53,0.05); }
|
||||
.cat-card .cat-icon { font-size: 1.8em; margin-bottom: 8px; }
|
||||
.cat-card .cat-name { font-size: 0.9em; color: #ddd; font-weight: 500; }
|
||||
.cat-card .cat-count { font-size: 0.75em; color: #666; margin-top: 4px; }
|
||||
|
||||
/* ── Install Box ── */
|
||||
.install-box {
|
||||
background: linear-gradient(135deg, #1a1200, #1a0a00);
|
||||
border: 1px solid #333; border-radius: 12px; padding: 28px;
|
||||
margin: 32px 0; display: flex; align-items: center; gap: 24px;
|
||||
}
|
||||
.install-box .install-left { flex: 1; }
|
||||
.install-box h3 { font-size: 1.2em; margin-bottom: 8px; }
|
||||
.install-box h3 span { color: #ff6b35; }
|
||||
.install-box p { color: #999; font-size: 0.9em; line-height: 1.5; }
|
||||
.install-box .install-cmd {
|
||||
margin-top: 14px; background: #111; border: 1px solid #333; border-radius: 8px;
|
||||
padding: 12px 16px; display: flex; align-items: center; gap: 12px;
|
||||
}
|
||||
.install-box .install-cmd code { flex: 1; color: #ff6b35; font-size: 0.9em; word-break: break-all; }
|
||||
.install-box .install-cmd button {
|
||||
background: #ff6b35; border: none; color: #fff; padding: 6px 14px;
|
||||
border-radius: 6px; cursor: pointer; font-size: 0.85em; white-space: nowrap;
|
||||
}
|
||||
.install-box .install-cmd button:hover { background: #e55a28; }
|
||||
|
||||
/* ── Tabs for Agent / Human ── */
|
||||
.mode-tabs { display: flex; gap: 8px; margin-bottom: 16px; }
|
||||
.mode-tab {
|
||||
padding: 8px 16px; border: 1px solid #333; border-radius: 6px;
|
||||
background: transparent; color: #888; cursor: pointer; font-size: 0.9em;
|
||||
}
|
||||
.mode-tab.active { background: #ff6b35; color: #fff; border-color: #ff6b35; }
|
||||
|
||||
/* ── Ranking Table ── */
|
||||
.rank-table { width: 100%; border-collapse: collapse; }
|
||||
.rank-table th {
|
||||
text-align: left; padding: 10px 12px; color: #666; font-size: 0.8em;
|
||||
border-bottom: 1px solid #222; font-weight: 500; text-transform: uppercase;
|
||||
}
|
||||
.rank-table td { padding: 12px; border-bottom: 1px solid #1a1a1a; }
|
||||
.rank-table tr:hover td { background: rgba(255,255,255,0.02); }
|
||||
.rank-num { color: #ff6b35; font-weight: 700; font-size: 1.1em; }
|
||||
.rank-name { color: #fff; font-weight: 500; }
|
||||
.rank-dl { color: #888; font-size: 0.85em; }
|
||||
|
||||
/* ── Footer ── */
|
||||
.footer { text-align: center; padding: 30px 40px; border-top: 1px solid #222; color: #555; font-size: 0.85em; }
|
||||
.footer a { color: #ff6b35; text-decoration: none; }
|
||||
.footer a:hover { text-decoration: underline; }
|
||||
|
||||
/* ── Mobile ── */
|
||||
@media (max-width: 768px) {
|
||||
.sidebar { display: none; }
|
||||
.main { margin-left: 0; }
|
||||
.hero { padding: 40px 20px 30px; }
|
||||
.hero h1 { font-size: 1.6em; }
|
||||
.hero .stats { gap: 20px; }
|
||||
.hero .stat-num { font-size: 1.4em; }
|
||||
.content { padding: 20px 16px 40px; }
|
||||
.tab-bar { padding: 0 16px; overflow-x: auto; }
|
||||
.skill-grid { grid-template-columns: 1fr; }
|
||||
.cat-grid { grid-template-columns: repeat(3, 1fr); }
|
||||
.install-box { flex-direction: column; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- ── Sidebar ── -->
|
||||
<nav class="sidebar">
|
||||
<div class="sidebar-logo">
|
||||
<h2><span>U-Claw</span> SkillHub</h2>
|
||||
<p>AI 技能市场</p>
|
||||
</div>
|
||||
<div class="sidebar-nav">
|
||||
<a href="https://u-claw.org" target="_blank">U-Claw 官网</a>
|
||||
<a href="https://github.com/openclaw/openclaw" target="_blank">OpenClaw 官方文档</a>
|
||||
<a href="https://github.com/dongsheng123132/u-claw" target="_blank">GitHub</a>
|
||||
<div class="nav-divider"></div>
|
||||
<a href="#featured" class="active">精选推荐</a>
|
||||
<a href="#categories">全部分类</a>
|
||||
<a href="#ranking">下载排行</a>
|
||||
<a href="#install">安装指南</a>
|
||||
<div class="nav-divider"></div>
|
||||
<a href="#guide">使用教程</a>
|
||||
<a href="#contact">联系我们</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- ── Main ── -->
|
||||
<div class="main">
|
||||
|
||||
<!-- Hero -->
|
||||
<section class="hero">
|
||||
<h1>装上 Skill,<span class="accent">解锁 AI 超能力</span></h1>
|
||||
<p class="subtitle">精选推荐 · 高速下载 · 轻松查找<br>ClawHub 13,000+ 个 AI Skills,U-Claw 为你加速</p>
|
||||
<div class="stats">
|
||||
<div class="stat-item"><div class="stat-num">13,729</div><div class="stat-label">技能总数</div></div>
|
||||
<div class="stat-item"><div class="stat-num">32</div><div class="stat-label">分类</div></div>
|
||||
<div class="stat-item"><div class="stat-num">2,868</div><div class="stat-label">精选优质</div></div>
|
||||
<div class="stat-item"><div class="stat-num">52</div><div class="stat-label">U-Claw 预装</div></div>
|
||||
</div>
|
||||
<div class="search-bar">
|
||||
<input type="text" id="searchInput" placeholder="搜索技能:如 github、邮件、天气、PDF..." />
|
||||
<button onclick="handleSearch()">搜索</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="tab-bar">
|
||||
<a href="#featured" class="active" data-tab="featured">精选榜单</a>
|
||||
<a href="#categories" data-tab="categories">全部分类</a>
|
||||
<a href="#ranking" data-tab="ranking">下载排行</a>
|
||||
<a href="#install" data-tab="install">安装指南</a>
|
||||
<a href="#guide" data-tab="guide">使用教程</a>
|
||||
</div>
|
||||
|
||||
<!-- ── Content Panels ── -->
|
||||
<div class="content">
|
||||
|
||||
<!-- ══ Featured ══ -->
|
||||
<div id="panel-featured" class="panel active">
|
||||
|
||||
<div class="section-header">
|
||||
<h2>U-Claw 必装技能 TOP 5</h2>
|
||||
<span class="badge">官方推荐</span>
|
||||
</div>
|
||||
<div class="skill-grid">
|
||||
<div class="skill-card" onclick="installSkill('github')">
|
||||
<div class="featured-badge">必装</div>
|
||||
<div class="card-top">
|
||||
<div class="card-icon" style="background:#1a1a2e;">🐙</div>
|
||||
<div><div class="card-title">GitHub</div><div class="card-author">@openclaw</div></div>
|
||||
</div>
|
||||
<div class="card-desc">管理 Issue、PR、搜索仓库、查看代码。开发者最强助手。</div>
|
||||
<div class="card-footer">
|
||||
<span class="downloads">48,000+ 安装</span>
|
||||
<span class="card-tag">开发</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="skill-card" onclick="installSkill('tavily-search')">
|
||||
<div class="featured-badge">必装</div>
|
||||
<div class="card-top">
|
||||
<div class="card-icon" style="background:#0a1a1a;">🔍</div>
|
||||
<div><div class="card-title">Tavily Search</div><div class="card-author">@tavily</div></div>
|
||||
</div>
|
||||
<div class="card-desc">AI 驱动的网络搜索引擎。让你的 AI 助手能实时搜索互联网。</div>
|
||||
<div class="card-footer">
|
||||
<span class="downloads">52,000+ 安装</span>
|
||||
<span class="card-tag">搜索</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="skill-card" onclick="installSkill('summarize')">
|
||||
<div class="featured-badge">必装</div>
|
||||
<div class="card-top">
|
||||
<div class="card-icon" style="background:#1a1a00;">📝</div>
|
||||
<div><div class="card-title">Summarize</div><div class="card-author">@openclaw</div></div>
|
||||
</div>
|
||||
<div class="card-desc">总结网页、文章、PDF、视频内容。一键提取关键信息。</div>
|
||||
<div class="card-footer">
|
||||
<span class="downloads">36,000+ 安装</span>
|
||||
<span class="card-tag">效率</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="skill-card" onclick="installSkill('himalaya')">
|
||||
<div class="featured-badge">必装</div>
|
||||
<div class="card-top">
|
||||
<div class="card-icon" style="background:#1a0a1a;">📧</div>
|
||||
<div><div class="card-title">Himalaya</div><div class="card-author">@sostrovsky</div></div>
|
||||
</div>
|
||||
<div class="card-desc">收发邮件、搜索邮件、管理文件夹。支持 Gmail/Outlook/QQ 邮箱。</div>
|
||||
<div class="card-footer">
|
||||
<span class="downloads">21,000+ 安装</span>
|
||||
<span class="card-tag">邮件</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="skill-card" onclick="installSkill('browser-relay')">
|
||||
<div class="featured-badge">必装</div>
|
||||
<div class="card-top">
|
||||
<div class="card-icon" style="background:#0a0a1a;">🌐</div>
|
||||
<div><div class="card-title">Browser Relay</div><div class="card-author">@nicosql</div></div>
|
||||
</div>
|
||||
<div class="card-desc">AI 控制浏览器,自动填表、截图、抓取网页数据。</div>
|
||||
<div class="card-footer">
|
||||
<span class="downloads">29,000+ 安装</span>
|
||||
<span class="card-tag">自动化</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-header" style="margin-top:40px;">
|
||||
<h2>效率工具</h2>
|
||||
<span class="badge">日常必备</span>
|
||||
</div>
|
||||
<div class="skill-grid">
|
||||
<div class="skill-card" onclick="installSkill('weather')">
|
||||
<div class="card-top">
|
||||
<div class="card-icon" style="background:#0a1a2e;">🌤</div>
|
||||
<div><div class="card-title">Weather</div><div class="card-author">@openclaw</div></div>
|
||||
</div>
|
||||
<div class="card-desc">查询全球天气预报。支持多日预报和实时天气。</div>
|
||||
<div class="card-footer"><span class="downloads">18,000+ 安装</span><span class="card-tag">生活</span></div>
|
||||
</div>
|
||||
<div class="skill-card" onclick="installSkill('apple-notes')">
|
||||
<div class="card-top">
|
||||
<div class="card-icon" style="background:#1a1a00;">📒</div>
|
||||
<div><div class="card-title">Apple Notes</div><div class="card-author">@openclaw</div></div>
|
||||
</div>
|
||||
<div class="card-desc">读写 Apple 备忘录。用语音或文字快速记录想法。</div>
|
||||
<div class="card-footer"><span class="downloads">15,000+ 安装</span><span class="card-tag">笔记</span></div>
|
||||
</div>
|
||||
<div class="skill-card" onclick="installSkill('apple-reminders')">
|
||||
<div class="card-top">
|
||||
<div class="card-icon" style="background:#2e0a0a;">⏰</div>
|
||||
<div><div class="card-title">Apple Reminders</div><div class="card-author">@openclaw</div></div>
|
||||
</div>
|
||||
<div class="card-desc">创建和管理提醒事项。"提醒我明天下午3点开会"。</div>
|
||||
<div class="card-footer"><span class="downloads">12,000+ 安装</span><span class="card-tag">效率</span></div>
|
||||
</div>
|
||||
<div class="skill-card" onclick="installSkill('obsidian')">
|
||||
<div class="card-top">
|
||||
<div class="card-icon" style="background:#1a0a2e;">💎</div>
|
||||
<div><div class="card-title">Obsidian</div><div class="card-author">@openclaw</div></div>
|
||||
</div>
|
||||
<div class="card-desc">在 Obsidian 知识库中创建、搜索、管理笔记。</div>
|
||||
<div class="card-footer"><span class="downloads">22,000+ 安装</span><span class="card-tag">知识管理</span></div>
|
||||
</div>
|
||||
<div class="skill-card" onclick="installSkill('notion')">
|
||||
<div class="card-top">
|
||||
<div class="card-icon" style="background:#1a1a1a;">📋</div>
|
||||
<div><div class="card-title">Notion</div><div class="card-author">@openclaw</div></div>
|
||||
</div>
|
||||
<div class="card-desc">创建 Notion 页面、管理数据库、搜索工作区内容。</div>
|
||||
<div class="card-footer"><span class="downloads">19,000+ 安装</span><span class="card-tag">效率</span></div>
|
||||
</div>
|
||||
<div class="skill-card" onclick="installSkill('nano-pdf')">
|
||||
<div class="card-top">
|
||||
<div class="card-icon" style="background:#2e1a0a;">📄</div>
|
||||
<div><div class="card-title">Nano PDF</div><div class="card-author">@nicosql</div></div>
|
||||
</div>
|
||||
<div class="card-desc">编辑 PDF 文件:合并、拆分、加水印、提取文本。</div>
|
||||
<div class="card-footer"><span class="downloads">14,000+ 安装</span><span class="card-tag">办公</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-header" style="margin-top:40px;">
|
||||
<h2>编程开发</h2>
|
||||
<span class="badge">开发者最爱</span>
|
||||
</div>
|
||||
<div class="skill-grid">
|
||||
<div class="skill-card" onclick="installSkill('coding-agent')">
|
||||
<div class="card-top">
|
||||
<div class="card-icon" style="background:#0a2e0a;">🤖</div>
|
||||
<div><div class="card-title">Coding Agent</div><div class="card-author">@openclaw</div></div>
|
||||
</div>
|
||||
<div class="card-desc">委派编程任务给 AI Agent。重构代码、写测试、修 bug。</div>
|
||||
<div class="card-footer"><span class="downloads">31,000+ 安装</span><span class="card-tag">编程</span></div>
|
||||
</div>
|
||||
<div class="skill-card" onclick="installSkill('tmux')">
|
||||
<div class="card-top">
|
||||
<div class="card-icon" style="background:#0a0a0a;">🖥</div>
|
||||
<div><div class="card-title">Tmux</div><div class="card-author">@openclaw</div></div>
|
||||
</div>
|
||||
<div class="card-desc">远程终端操作。查看服务器状态、执行命令、管理进程。</div>
|
||||
<div class="card-footer"><span class="downloads">11,000+ 安装</span><span class="card-tag">运维</span></div>
|
||||
</div>
|
||||
<div class="skill-card" onclick="installSkill('openai-image-gen')">
|
||||
<div class="card-top">
|
||||
<div class="card-icon" style="background:#2e1a2e;">🎨</div>
|
||||
<div><div class="card-title">OpenAI Image Gen</div><div class="card-author">@openclaw</div></div>
|
||||
</div>
|
||||
<div class="card-desc">DALL-E AI 绘图。用自然语言描述,生成高质量图片。</div>
|
||||
<div class="card-footer"><span class="downloads">26,000+ 安装</span><span class="card-tag">创作</span></div>
|
||||
</div>
|
||||
<div class="skill-card" onclick="installSkill('peekaboo')">
|
||||
<div class="card-top">
|
||||
<div class="card-icon" style="background:#1a2e1a;">📸</div>
|
||||
<div><div class="card-title">Peekaboo</div><div class="card-author">@nicosql</div></div>
|
||||
</div>
|
||||
<div class="card-desc">屏幕截图 + OCR。AI 能"看见"你的屏幕并理解内容。</div>
|
||||
<div class="card-footer"><span class="downloads">17,000+ 安装</span><span class="card-tag">多模态</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══ Categories ══ -->
|
||||
<div id="panel-categories" class="panel">
|
||||
<div class="section-header">
|
||||
<h2>全部分类</h2>
|
||||
<span class="badge">32 个分类</span>
|
||||
</div>
|
||||
<div class="cat-grid">
|
||||
<div class="cat-card" onclick="filterCat('coding')"><div class="cat-icon">💻</div><div class="cat-name">编程 Agent</div><div class="cat-count">55 个技能</div></div>
|
||||
<div class="cat-card" onclick="filterCat('web-dev')"><div class="cat-icon">🌐</div><div class="cat-name">Web 前端</div><div class="cat-count">46 个技能</div></div>
|
||||
<div class="cat-card" onclick="filterCat('git')"><div class="cat-icon">🔀</div><div class="cat-name">Git & GitHub</div><div class="cat-count">34 个技能</div></div>
|
||||
<div class="cat-card" onclick="filterCat('devops')"><div class="cat-icon">☁️</div><div class="cat-name">DevOps & 云</div><div class="cat-count">144 个技能</div></div>
|
||||
<div class="cat-card" onclick="filterCat('browser')"><div class="cat-icon">🖱</div><div class="cat-name">浏览器自动化</div><div class="cat-count">69 个技能</div></div>
|
||||
<div class="cat-card" onclick="filterCat('ai')"><div class="cat-icon">🧠</div><div class="cat-name">AI & LLMs</div><div class="cat-count">159 个技能</div></div>
|
||||
<div class="cat-card" onclick="filterCat('image')"><div class="cat-icon">🎨</div><div class="cat-name">图片 & 视频</div><div class="cat-count">41 个技能</div></div>
|
||||
<div class="cat-card" onclick="filterCat('productivity')"><div class="cat-icon">⚡</div><div class="cat-name">效率工具</div><div class="cat-count">93 个技能</div></div>
|
||||
<div class="cat-card" onclick="filterCat('data')"><div class="cat-icon">📊</div><div class="cat-name">数据 & 分析</div><div class="cat-count">18 个技能</div></div>
|
||||
<div class="cat-card" onclick="filterCat('notes')"><div class="cat-icon">📝</div><div class="cat-name">笔记 & 知识</div><div class="cat-count">38 个技能</div></div>
|
||||
<div class="cat-card" onclick="filterCat('email')"><div class="cat-icon">📧</div><div class="cat-name">邮件 & 通讯</div><div class="cat-count">24 个技能</div></div>
|
||||
<div class="cat-card" onclick="filterCat('social')"><div class="cat-icon">💬</div><div class="cat-name">社交媒体</div><div class="cat-count">31 个技能</div></div>
|
||||
<div class="cat-card" onclick="filterCat('search')"><div class="cat-icon">🔍</div><div class="cat-name">搜索 & 研究</div><div class="cat-count">47 个技能</div></div>
|
||||
<div class="cat-card" onclick="filterCat('smarthome')"><div class="cat-icon">🏠</div><div class="cat-name">智能家居</div><div class="cat-count">22 个技能</div></div>
|
||||
<div class="cat-card" onclick="filterCat('finance')"><div class="cat-icon">💰</div><div class="cat-name">金融 & 加密</div><div class="cat-count">28 个技能</div></div>
|
||||
<div class="cat-card" onclick="filterCat('security')"><div class="cat-icon">🛡</div><div class="cat-name">安全防护</div><div class="cat-count">19 个技能</div></div>
|
||||
<div class="cat-card" onclick="filterCat('voice')"><div class="cat-icon">🎙</div><div class="cat-name">语音 & TTS</div><div class="cat-count">15 个技能</div></div>
|
||||
<div class="cat-card" onclick="filterCat('file')"><div class="cat-icon">📁</div><div class="cat-name">文件管理</div><div class="cat-count">33 个技能</div></div>
|
||||
<div class="cat-card" onclick="filterCat('translate')"><div class="cat-icon">🌍</div><div class="cat-name">翻译</div><div class="cat-count">12 个技能</div></div>
|
||||
<div class="cat-card" onclick="filterCat('calendar')"><div class="cat-icon">📅</div><div class="cat-name">日历 & 提醒</div><div class="cat-count">16 个技能</div></div>
|
||||
</div>
|
||||
<div style="text-align:center; margin-top:24px; color:#666; font-size:0.9em;">
|
||||
更多分类请在搜索框输入关键词,或使用命令:<code style="color:#ff6b35; background:#1a1a1a; padding:2px 8px; border-radius:4px;">openclaw skills search 关键词</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══ Ranking ══ -->
|
||||
<div id="panel-ranking" class="panel">
|
||||
<div class="section-header">
|
||||
<h2>下载排行榜</h2>
|
||||
<span class="badge">2026 年 3 月</span>
|
||||
</div>
|
||||
<table class="rank-table">
|
||||
<thead>
|
||||
<tr><th>#</th><th>技能</th><th>分类</th><th>下载量</th><th>安装命令</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td class="rank-num">1</td><td class="rank-name">Tavily Search</td><td><span class="card-tag">搜索</span></td><td class="rank-dl">52,000+</td><td><code style="color:#ff6b35; font-size:0.8em;">openclaw skills install @tavily/search</code></td></tr>
|
||||
<tr><td class="rank-num">2</td><td class="rank-name">GitHub</td><td><span class="card-tag">开发</span></td><td class="rank-dl">48,000+</td><td><code style="color:#ff6b35; font-size:0.8em;">openclaw skills install @openclaw/github</code></td></tr>
|
||||
<tr><td class="rank-num">3</td><td class="rank-name">Capability Evolver</td><td><span class="card-tag">AI</span></td><td class="rank-dl">35,581</td><td><code style="color:#ff6b35; font-size:0.8em;">openclaw skills install @cap/evolver</code></td></tr>
|
||||
<tr><td class="rank-num">4</td><td class="rank-name">Coding Agent</td><td><span class="card-tag">编程</span></td><td class="rank-dl">31,000+</td><td><code style="color:#ff6b35; font-size:0.8em;">openclaw skills install @openclaw/coding-agent</code></td></tr>
|
||||
<tr><td class="rank-num">5</td><td class="rank-name">Browser Relay</td><td><span class="card-tag">自动化</span></td><td class="rank-dl">29,000+</td><td><code style="color:#ff6b35; font-size:0.8em;">openclaw skills install @nicosql/browser-relay</code></td></tr>
|
||||
<tr><td class="rank-num">6</td><td class="rank-name">OpenAI Image Gen</td><td><span class="card-tag">创作</span></td><td class="rank-dl">26,000+</td><td><code style="color:#ff6b35; font-size:0.8em;">openclaw skills install @openclaw/openai-image-gen</code></td></tr>
|
||||
<tr><td class="rank-num">7</td><td class="rank-name">Obsidian</td><td><span class="card-tag">笔记</span></td><td class="rank-dl">22,000+</td><td><code style="color:#ff6b35; font-size:0.8em;">openclaw skills install @openclaw/obsidian</code></td></tr>
|
||||
<tr><td class="rank-num">8</td><td class="rank-name">Himalaya</td><td><span class="card-tag">邮件</span></td><td class="rank-dl">21,000+</td><td><code style="color:#ff6b35; font-size:0.8em;">openclaw skills install @sostrovsky/himalaya</code></td></tr>
|
||||
<tr><td class="rank-num">9</td><td class="rank-name">Notion</td><td><span class="card-tag">效率</span></td><td class="rank-dl">19,000+</td><td><code style="color:#ff6b35; font-size:0.8em;">openclaw skills install @openclaw/notion</code></td></tr>
|
||||
<tr><td class="rank-num">10</td><td class="rank-name">Weather</td><td><span class="card-tag">生活</span></td><td class="rank-dl">18,000+</td><td><code style="color:#ff6b35; font-size:0.8em;">openclaw skills install @openclaw/weather</code></td></tr>
|
||||
<tr><td class="rank-num">11</td><td class="rank-name">Peekaboo</td><td><span class="card-tag">多模态</span></td><td class="rank-dl">17,000+</td><td><code style="color:#ff6b35; font-size:0.8em;">openclaw skills install @nicosql/peekaboo</code></td></tr>
|
||||
<tr><td class="rank-num">12</td><td class="rank-name">Wacli</td><td><span class="card-tag">自动化</span></td><td class="rank-dl">16,415</td><td><code style="color:#ff6b35; font-size:0.8em;">openclaw skills install @nicosql/wacli</code></td></tr>
|
||||
<tr><td class="rank-num">13</td><td class="rank-name">ByteRover</td><td><span class="card-tag">搜索</span></td><td class="rank-dl">16,004</td><td><code style="color:#ff6b35; font-size:0.8em;">openclaw skills install @byterover/search</code></td></tr>
|
||||
<tr><td class="rank-num">14</td><td class="rank-name">Self-Improving Agent</td><td><span class="card-tag">AI</span></td><td class="rank-dl">15,962</td><td><code style="color:#ff6b35; font-size:0.8em;">openclaw skills install @self-improving-agent</code></td></tr>
|
||||
<tr><td class="rank-num">15</td><td class="rank-name">Apple Notes</td><td><span class="card-tag">笔记</span></td><td class="rank-dl">15,000+</td><td><code style="color:#ff6b35; font-size:0.8em;">openclaw skills install @openclaw/apple-notes</code></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- ══ Install ══ -->
|
||||
<div id="panel-install" class="panel">
|
||||
<div class="section-header">
|
||||
<h2>安装技能</h2>
|
||||
</div>
|
||||
|
||||
<div class="mode-tabs">
|
||||
<button class="mode-tab active" onclick="switchMode(this, 'agent')">我是 Agent</button>
|
||||
<button class="mode-tab" onclick="switchMode(this, 'human')">我是 Human</button>
|
||||
</div>
|
||||
|
||||
<div id="mode-agent" class="mode-panel">
|
||||
<div class="install-box">
|
||||
<div class="install-left">
|
||||
<h3>将此提示发送给你的 <span>Agent</span>,以安装技能</h3>
|
||||
<p>复制下面的命令,粘贴到你的 OpenClaw 对话中。Agent 会自动搜索和安装。</p>
|
||||
<div class="install-cmd">
|
||||
<code>帮我安装 [技能名称] 这个 skill</code>
|
||||
<button onclick="copyText(this, '帮我安装 [技能名称] 这个 skill')">复制</button>
|
||||
</div>
|
||||
<div class="install-cmd" style="margin-top:8px;">
|
||||
<code>搜索关于 [功能描述] 的 skill 并安装最合适的</code>
|
||||
<button onclick="copyText(this, '搜索关于 [功能描述] 的 skill 并安装最合适的')">复制</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="mode-human" class="mode-panel" style="display:none;">
|
||||
<div class="install-box">
|
||||
<div class="install-left">
|
||||
<h3>终端命令安装</h3>
|
||||
<p>在终端中直接运行以下命令:</p>
|
||||
<div class="install-cmd">
|
||||
<code>openclaw skills search 关键词</code>
|
||||
<button onclick="copyText(this, 'openclaw skills search 关键词')">复制</button>
|
||||
</div>
|
||||
<div class="install-cmd" style="margin-top:8px;">
|
||||
<code>openclaw skills install @作者/技能名</code>
|
||||
<button onclick="copyText(this, 'openclaw skills install @作者/技能名')">复制</button>
|
||||
</div>
|
||||
<div class="install-cmd" style="margin-top:8px;">
|
||||
<code>openclaw clawhub search 关键词</code>
|
||||
<button onclick="copyText(this, 'openclaw clawhub search 关键词')">复制</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:32px;">
|
||||
<h3 style="margin-bottom:16px;">常用操作速查</h3>
|
||||
<table class="rank-table">
|
||||
<thead><tr><th>操作</th><th>命令</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>搜索技能</td><td><code style="color:#ff6b35;">openclaw skills search 关键词</code></td></tr>
|
||||
<tr><td>安装技能</td><td><code style="color:#ff6b35;">openclaw skills install @作者/技能名</code></td></tr>
|
||||
<tr><td>查看已装技能</td><td><code style="color:#ff6b35;">openclaw skills list</code></td></tr>
|
||||
<tr><td>卸载技能</td><td><code style="color:#ff6b35;">openclaw skills uninstall 技能名</code></td></tr>
|
||||
<tr><td>更新技能</td><td><code style="color:#ff6b35;">openclaw skills update 技能名</code></td></tr>
|
||||
<tr><td>ClawHub 搜索</td><td><code style="color:#ff6b35;">openclaw clawhub search 关键词</code></td></tr>
|
||||
<tr><td>查看技能详情</td><td><code style="color:#ff6b35;">openclaw skills info @作者/技能名</code></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:32px;">
|
||||
<h3 style="margin-bottom:16px;">U-Claw 用户专属</h3>
|
||||
<div class="install-box">
|
||||
<div class="install-left">
|
||||
<h3><span>52 个技能已预装</span>,开箱即用</h3>
|
||||
<p>U-Claw 已内置最常用的 52 个官方技能,无需联网下载。在启动菜单选 [13] 可浏览完整列表。<br>需要更多技能?使用上面的命令从 ClawHub 在线安装。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══ Guide ══ -->
|
||||
<div id="panel-guide" class="panel">
|
||||
<div class="section-header">
|
||||
<h2>使用教程</h2>
|
||||
</div>
|
||||
|
||||
<div class="skill-grid" style="grid-template-columns: 1fr;">
|
||||
<div class="skill-card" onclick="window.open('教程-OpenClaw中国区完全指南.md')">
|
||||
<div class="card-top">
|
||||
<div class="card-icon" style="background:#1a1a00;">📖</div>
|
||||
<div><div class="card-title">OpenClaw 中国区完全指南</div><div class="card-author">U-Claw 出品</div></div>
|
||||
</div>
|
||||
<div class="card-desc">从安装到配置到日常使用,一篇搞定。包含国内镜像设置、模型选择、QQ/飞书接入。</div>
|
||||
</div>
|
||||
<a href="https://datawhalechina.github.io/hello-claw/" target="_blank" style="text-decoration:none;">
|
||||
<div class="skill-card">
|
||||
<div class="card-top">
|
||||
<div class="card-icon" style="background:#0a2e0a;">🐳</div>
|
||||
<div><div class="card-title">Hello Claw(Datawhale 教程)</div><div class="card-author">推荐资源 · datawhalechina</div></div>
|
||||
</div>
|
||||
<div class="card-desc">推荐阅读:体系化入门教程,从零基础到进阶,覆盖安装、配置、技能开发。</div>
|
||||
</div>
|
||||
</a>
|
||||
<a href="https://github.com/yeuxuan/openclaw-docs" target="_blank" style="text-decoration:none;">
|
||||
<div class="skill-card">
|
||||
<div class="card-top">
|
||||
<div class="card-icon" style="background:#1a0a2e;">📚</div>
|
||||
<div><div class="card-title">OpenClaw 中文文档(276 篇)</div><div class="card-author">推荐资源 · yeuxuan</div></div>
|
||||
</div>
|
||||
<div class="card-desc">推荐阅读:源码级深度文档,276 篇覆盖架构、网关、插件、技能开发全流程。</div>
|
||||
</div>
|
||||
</a>
|
||||
<a href="https://github.com/VoltAgent/awesome-openclaw-skills" target="_blank" style="text-decoration:none;">
|
||||
<div class="skill-card">
|
||||
<div class="card-top">
|
||||
<div class="card-icon" style="background:#2e1a0a;">🦞</div>
|
||||
<div><div class="card-title">Awesome OpenClaw Skills(精选 5400+)</div><div class="card-author">推荐资源 · VoltAgent</div></div>
|
||||
</div>
|
||||
<div class="card-desc">推荐阅读:从 13000+ 技能中精选 5400+ 优质技能,分类整理,过滤垃圾和恶意代码。</div>
|
||||
</div>
|
||||
</a>
|
||||
<a href="https://developer.aliyun.com/article/1715006" target="_blank" style="text-decoration:none;">
|
||||
<div class="skill-card">
|
||||
<div class="card-top">
|
||||
<div class="card-icon" style="background:#0a1a1a;">🏆</div>
|
||||
<div><div class="card-title">13000+ Skills 怎么选?30 个最值得装</div><div class="card-author">阿里云开发者社区</div></div>
|
||||
</div>
|
||||
<div class="card-desc">从海量技能中筛选 30 个最实用的,附 5 个必装 Skill 推荐。</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- .content -->
|
||||
|
||||
<!-- ── Contact (anchor) ── -->
|
||||
<div id="contact" class="footer">
|
||||
<p style="margin-bottom:8px;">
|
||||
<a href="https://u-claw.org" target="_blank">U-Claw 官网</a> ·
|
||||
<a href="https://github.com/dongsheng123132/u-claw" target="_blank">GitHub</a> ·
|
||||
<a href="https://github.com/openclaw/openclaw" target="_blank">OpenClaw 官方</a>
|
||||
</p>
|
||||
<p>微信: hecare888 · Made with 🦞 by <a href="https://github.com/dongsheng123132">dongsheng</a></p>
|
||||
</div>
|
||||
|
||||
</div><!-- .main -->
|
||||
|
||||
<script>
|
||||
// ── Tab switching ──
|
||||
document.querySelectorAll('.tab-bar a').forEach(tab => {
|
||||
tab.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
document.querySelectorAll('.tab-bar a').forEach(t => t.classList.remove('active'));
|
||||
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
const panelId = 'panel-' + this.dataset.tab;
|
||||
document.getElementById(panelId).classList.add('active');
|
||||
// Update sidebar active
|
||||
document.querySelectorAll('.sidebar-nav a').forEach(a => a.classList.remove('active'));
|
||||
const href = this.getAttribute('href');
|
||||
document.querySelectorAll('.sidebar-nav a[href="' + href + '"]').forEach(a => a.classList.add('active'));
|
||||
});
|
||||
});
|
||||
|
||||
// ── Sidebar nav ──
|
||||
document.querySelectorAll('.sidebar-nav a[href^="#"]').forEach(link => {
|
||||
link.addEventListener('click', function(e) {
|
||||
const href = this.getAttribute('href').replace('#', '');
|
||||
const tabEl = document.querySelector('.tab-bar a[data-tab="' + href + '"]');
|
||||
if (tabEl) { e.preventDefault(); tabEl.click(); }
|
||||
});
|
||||
});
|
||||
|
||||
// ── Search ──
|
||||
function handleSearch() {
|
||||
const q = document.getElementById('searchInput').value.trim();
|
||||
if (!q) return;
|
||||
// Show install panel with search hint
|
||||
const tabEl = document.querySelector('.tab-bar a[data-tab="install"]');
|
||||
tabEl.click();
|
||||
alert('在终端中运行:\n\nopenclaw skills search ' + q + '\n\n或直接告诉你的 AI:\n"帮我搜索关于 ' + q + ' 的 skill"');
|
||||
}
|
||||
document.getElementById('searchInput').addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Enter') handleSearch();
|
||||
});
|
||||
|
||||
// ── Copy ──
|
||||
function copyText(btn, text) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
const orig = btn.textContent;
|
||||
btn.textContent = '已复制';
|
||||
btn.style.background = '#4caf50';
|
||||
setTimeout(() => { btn.textContent = orig; btn.style.background = '#ff6b35'; }, 1500);
|
||||
}).catch(() => {
|
||||
// Fallback
|
||||
const ta = document.createElement('textarea');
|
||||
ta.value = text; document.body.appendChild(ta); ta.select();
|
||||
document.execCommand('copy'); document.body.removeChild(ta);
|
||||
btn.textContent = '已复制';
|
||||
setTimeout(() => { btn.textContent = '复制'; }, 1500);
|
||||
});
|
||||
}
|
||||
|
||||
// ── Install Skill (shortcut) ──
|
||||
function installSkill(name) {
|
||||
const cmd = 'openclaw skills install @openclaw/' + name;
|
||||
if (confirm('安装 ' + name + '?\n\n复制安装命令到剪贴板:\n' + cmd)) {
|
||||
copyText(null, cmd);
|
||||
navigator.clipboard.writeText(cmd);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Category filter ──
|
||||
function filterCat(cat) {
|
||||
alert('在终端中运行:\n\nopenclaw clawhub search --category ' + cat + '\n\n浏览该分类下的所有技能');
|
||||
}
|
||||
|
||||
// ── Mode switch ──
|
||||
function switchMode(btn, mode) {
|
||||
document.querySelectorAll('.mode-tab').forEach(t => t.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
document.getElementById('mode-agent').style.display = mode === 'agent' ? 'block' : 'none';
|
||||
document.getElementById('mode-human').style.display = mode === 'human' ? 'block' : 'none';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,220 +0,0 @@
|
||||
@echo off
|
||||
chcp 65001 >nul 2>&1
|
||||
title U-Claw - Portable AI Agent
|
||||
|
||||
echo.
|
||||
echo ========================================
|
||||
echo U-Claw v1.1 - Portable AI Agent
|
||||
echo ========================================
|
||||
echo.
|
||||
|
||||
set "UCLAW_DIR=%~dp0"
|
||||
set "APP_DIR=%UCLAW_DIR%app"
|
||||
|
||||
REM Migration shim: rename old core-win to core for existing USB users
|
||||
if exist "%APP_DIR%\core-win" if not exist "%APP_DIR%\core" ren "%APP_DIR%\core-win" core
|
||||
|
||||
set "CORE_DIR=%APP_DIR%\core"
|
||||
set "DATA_DIR=%UCLAW_DIR%data"
|
||||
set "STATE_DIR=%DATA_DIR%\.openclaw"
|
||||
set "NODE_DIR=%APP_DIR%\runtime\node-win-x64"
|
||||
set "NODE_BIN=%NODE_DIR%\node.exe"
|
||||
set "NPM_BIN=%NODE_DIR%\npm.cmd"
|
||||
|
||||
set "OPENCLAW_HOME=%DATA_DIR%"
|
||||
set "OPENCLAW_STATE_DIR=%STATE_DIR%"
|
||||
set "OPENCLAW_CONFIG_PATH=%STATE_DIR%\openclaw.json"
|
||||
REM U-Claw opens the local dashboard directly; disable mDNS discovery on Windows
|
||||
REM to avoid OpenClaw/@homebridge ciao crashes during bonjour re-advertise.
|
||||
set "OPENCLAW_DISABLE_BONJOUR=1"
|
||||
|
||||
REM Check runtime
|
||||
if not exist "%NODE_BIN%" (
|
||||
echo [ERROR] Node.js runtime not found
|
||||
echo Please ensure app\runtime\node-win-x64 is complete
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
for /f "tokens=*" %%v in ('"%NODE_BIN%" --version') do set NODE_VER=%%v
|
||||
echo Node.js: %NODE_VER%
|
||||
echo.
|
||||
|
||||
set "PATH=%NODE_DIR%;%NODE_DIR%\node_modules\.bin;%PATH%"
|
||||
|
||||
REM Init data directories
|
||||
if not exist "%DATA_DIR%" mkdir "%DATA_DIR%"
|
||||
if not exist "%STATE_DIR%" mkdir "%STATE_DIR%"
|
||||
if not exist "%DATA_DIR%\memory" mkdir "%DATA_DIR%\memory"
|
||||
if not exist "%DATA_DIR%\backups" mkdir "%DATA_DIR%\backups"
|
||||
if not exist "%DATA_DIR%\logs" mkdir "%DATA_DIR%\logs"
|
||||
|
||||
REM Startup cache acceleration.
|
||||
REM portable-cache.mjs chooses a local-disk cache slot and redirects
|
||||
REM .openclaw\browser there with a junction when possible.
|
||||
REM Browser user data and V8 compile cache stay off the USB drive.
|
||||
REM If this fails, startup continues with the cache on the USB drive.
|
||||
for /f "usebackq tokens=1,* delims==" %%a in (`""%NODE_BIN%" "%UCLAW_DIR%lib\portable-cache.mjs" "%STATE_DIR%" "%UCLAW_DIR%" 2^>nul"`) do (
|
||||
if "%%a"=="UCLAW_COMPILE_CACHE_DIR" set "NODE_COMPILE_CACHE=%%b"
|
||||
if "%%a"=="UCLAW_CACHE_ROOT" set "UCLAW_CACHE_ROOT=%%b"
|
||||
)
|
||||
if defined NODE_COMPILE_CACHE echo Cache on local disk: %UCLAW_CACHE_ROOT%
|
||||
|
||||
REM Default config (migrate legacy if present, otherwise create)
|
||||
if not exist "%STATE_DIR%\openclaw.json" (
|
||||
if exist "%DATA_DIR%\config.json" (
|
||||
echo Migrating legacy config...
|
||||
copy "%DATA_DIR%\config.json" "%STATE_DIR%\openclaw.json" >nul
|
||||
echo Config migrated
|
||||
) else (
|
||||
echo First run - creating default config...
|
||||
(echo {"gateway":{"mode":"local","auth":{"token":"uclaw"}}})>"%STATE_DIR%\openclaw.json"
|
||||
echo Config created
|
||||
)
|
||||
echo.
|
||||
)
|
||||
|
||||
REM Check dependencies
|
||||
REM Note: avoid unescaped parens inside this block -- cmd.exe treats ) as block-end.
|
||||
if not exist "%CORE_DIR%\node_modules" (
|
||||
echo ========================================
|
||||
echo [WARN] node_modules not found
|
||||
echo ========================================
|
||||
echo This release should ship with deps pre-installed.
|
||||
echo Falling back to npm install ^(USB drives may take 20+ minutes^).
|
||||
echo.
|
||||
echo TIP: Re-download u-claw-portable-*.zip from GitHub releases,
|
||||
echo which includes pre-installed deps ^(~200 MB^).
|
||||
echo.
|
||||
echo File system: NTFS recommended. exFAT/FAT32 will be very slow.
|
||||
echo.
|
||||
cd /d "%CORE_DIR%"
|
||||
REM Keep npm cache inside the portable app instead of system APPDATA.
|
||||
set "npm_config_cache=%APP_DIR%\.npm-cache"
|
||||
call "%NPM_BIN%" install --registry=https://registry.npmmirror.com --ignore-scripts --no-audit --no-fund --omit=dev
|
||||
echo.
|
||||
echo Dependencies installed!
|
||||
echo.
|
||||
)
|
||||
|
||||
REM Intranet/self-hosted model fix: keep the configured model host(s) off any
|
||||
REM corporate HTTP_PROXY/HTTPS_PROXY. OpenClaw routes ALL fetch through the env
|
||||
REM proxy when it is set, which breaks calls to internal model endpoints
|
||||
REM (e.g. http://10.x / 192.168.x / a machine-room IP). Add those hosts + loopback
|
||||
REM to NO_PROXY so they connect directly. Silent no-op when no proxy/model is set.
|
||||
for /f "usebackq tokens=1,* delims==" %%a in (`""%NODE_BIN%" "%UCLAW_DIR%lib\resolve-no-proxy.mjs" "%STATE_DIR%\openclaw.json" 2^>nul"`) do (
|
||||
if "%%a"=="UCLAW_NO_PROXY" set "NO_PROXY=%%b"
|
||||
)
|
||||
if defined NO_PROXY (
|
||||
set "no_proxy=%NO_PROXY%"
|
||||
REM Note: no unescaped parens in echo inside this IF block - cmd treats ) as block-end.
|
||||
echo Direct-connect via NO_PROXY: %NO_PROXY%
|
||||
)
|
||||
|
||||
REM Async update check (non-blocking, 5s timeout, silent failure)
|
||||
REM Writes data\.openclaw\update-available.json if a newer version is on OSS.
|
||||
REM Welcome.html / Config.html read this file and show a banner.
|
||||
REM Version file lookup order: portable/OPENCLAW_VERSION (USB), then repo-root ../OPENCLAW_VERSION (dev)
|
||||
set "VERSION_FILE=%UCLAW_DIR%OPENCLAW_VERSION"
|
||||
if not exist "%VERSION_FILE%" set "VERSION_FILE=%UCLAW_DIR%..\OPENCLAW_VERSION"
|
||||
if exist "%VERSION_FILE%" (
|
||||
start /B "" "%NODE_BIN%" "%UCLAW_DIR%lib\check-update.mjs" "%VERSION_FILE%" "%STATE_DIR%" >nul 2>&1
|
||||
)
|
||||
|
||||
|
||||
REM Auto-install WeChat plugin if available
|
||||
set "WECHAT_PLUGIN_SRC=%APP_DIR%\extensions\openclaw-weixin"
|
||||
set "WECHAT_PLUGIN_DST=%USERPROFILE%\.openclaw\extensions\openclaw-weixin"
|
||||
if exist "%WECHAT_PLUGIN_SRC%\openclaw.plugin.json" (
|
||||
if not exist "%WECHAT_PLUGIN_DST%\openclaw.plugin.json" (
|
||||
echo Installing WeChat plugin...
|
||||
mkdir "%USERPROFILE%\.openclaw\extensions" 2>nul
|
||||
xcopy /s /e /q /y "%WECHAT_PLUGIN_SRC%" "%WECHAT_PLUGIN_DST%\" >nul
|
||||
echo WeChat plugin installed!
|
||||
echo.
|
||||
)
|
||||
)
|
||||
|
||||
REM Start Config Server in background
|
||||
echo Starting Config Center on port 18788...
|
||||
set "CONFIG_SERVER=%UCLAW_DIR%config-server"
|
||||
set "RUNTIME_JSON=%STATE_DIR%\runtime.json"
|
||||
del "%RUNTIME_JSON%" >nul 2>&1
|
||||
start /B "" "%NODE_BIN%" "%CONFIG_SERVER%\server.js" >nul 2>&1
|
||||
|
||||
REM Wait for Config Server with polling instead of a fixed delay.
|
||||
REM It writes runtime.json with the actual fallback port when ready.
|
||||
set /a CFG_TRIES=0
|
||||
:wait_config
|
||||
if exist "%RUNTIME_JSON%" goto :config_ready
|
||||
set /a CFG_TRIES+=1
|
||||
if %CFG_TRIES% geq 20 goto :config_ready
|
||||
ping -n 1 -w 300 127.0.0.1 >nul 2>&1
|
||||
goto :wait_config
|
||||
:config_ready
|
||||
set "CONFIG_PORT=18788"
|
||||
if exist "%RUNTIME_JSON%" (
|
||||
for /f "usebackq tokens=*" %%p in (`powershell -NoProfile -Command "try { (Get-Content -Raw '%RUNTIME_JSON%' | ConvertFrom-Json).configServerPort } catch {}" 2^>nul`) do set "CONFIG_PORT=%%p"
|
||||
)
|
||||
echo Config Center port: %CONFIG_PORT%
|
||||
|
||||
REM Find available gateway port after Config Center has bound its port.
|
||||
set PORT=18789
|
||||
:check_port
|
||||
netstat -an | findstr ":%PORT% " | findstr "LISTENING" >nul 2>&1
|
||||
if %errorlevel%==0 (
|
||||
echo Port %PORT% in use, trying next...
|
||||
set /a PORT+=1
|
||||
if %PORT% gtr 18799 (
|
||||
echo No available port 18789-18799
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
goto :check_port
|
||||
)
|
||||
|
||||
echo Starting OpenClaw on port %PORT%...
|
||||
echo.
|
||||
|
||||
REM Do not open Dashboard before the gateway is ready.
|
||||
REM Slow USB drives may need tens of seconds to stage bundled deps.
|
||||
REM Open the local startup page now, and always open Config Center too.
|
||||
REM Config Center lets users change models, recharge/get keys, and connect channels.
|
||||
|
||||
REM Open startup page with the gateway port and token in the query string.
|
||||
echo Opening startup screen...
|
||||
set "LOADING_PATH=%UCLAW_DIR%lib\loading.html"
|
||||
set "LOADING_URL=file:///%LOADING_PATH:\=/%?port=%PORT%&token=uclaw"
|
||||
start "" "%LOADING_URL%"
|
||||
|
||||
echo Opening Config Center...
|
||||
start "" http://127.0.0.1:%CONFIG_PORT%/
|
||||
|
||||
REM Fallback watcher: if the startup page cannot poll from file URLs,
|
||||
REM keep polling and reopen Config Center after the gateway is ready.
|
||||
start /B "" cmd /c ""%UCLAW_DIR%lib\wait-gateway.bat" %PORT% %CONFIG_PORT%"
|
||||
|
||||
REM Prewarm gateway in the background after it becomes ready.
|
||||
start /B "" "%NODE_BIN%" "%UCLAW_DIR%lib\prewarm.mjs" %PORT% uclaw >nul 2>&1
|
||||
|
||||
echo.
|
||||
echo ========================================
|
||||
echo Starting OpenClaw Gateway on port %PORT%...
|
||||
echo First run on a USB drive may take 30-90 seconds
|
||||
echo (unpacking bundled components). Please wait;
|
||||
echo Config Center is open for model, key, recharge, and channel setup.
|
||||
echo DO NOT close this window while using U-Claw!
|
||||
echo ========================================
|
||||
echo.
|
||||
|
||||
cd /d "%CORE_DIR%"
|
||||
set "OPENCLAW_MJS=%CORE_DIR%\node_modules\openclaw\openclaw.mjs"
|
||||
"%NODE_BIN%" "%OPENCLAW_MJS%" gateway run --allow-unconfigured --force --port %PORT%
|
||||
set "GW_EXIT=%errorlevel%"
|
||||
|
||||
echo.
|
||||
if not "%GW_EXIT%"=="0" if not "%GW_EXIT%"=="-1073741510" (
|
||||
echo OpenClaw exited unexpectedly ^(code %GW_EXIT%^)
|
||||
)
|
||||
echo OpenClaw stopped.
|
||||
pause
|
||||
120
portable/advanced/Config.html
Normal file
120
portable/advanced/Config.html
Normal file
@@ -0,0 +1,120 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>U-Claw Settings</title>
|
||||
<!--
|
||||
This page used to be a second, hand-maintained copy of the settings UI (748
|
||||
lines) alongside config-server/public/index.html. It could never actually work:
|
||||
it called the API through a root-relative path, and from a file:// origin that
|
||||
resolves to a file:/// URL, which browsers refuse outright. Anyone who
|
||||
double-clicked it — the route Read me first.html and the README both point at — got a
|
||||
form whose Save button failed silently.
|
||||
|
||||
config-server/public/index.html is now the only implementation. This file is a
|
||||
redirect to it, so the old entry point keeps working.
|
||||
-->
|
||||
<style>
|
||||
:root { color-scheme: light dark; }
|
||||
body {
|
||||
margin: 0; min-height: 100vh;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-family: -apple-system, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
background: #f5f6f8; color: #1a1d21;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) { body { background: #16181c; color: #e8eaed; } }
|
||||
.card {
|
||||
max-width: 30rem; padding: 2.5rem 2rem; text-align: center;
|
||||
background: #fff; border-radius: 14px; box-shadow: 0 10px 40px rgba(0,0,0,.08);
|
||||
}
|
||||
@media (prefers-color-scheme: dark) { .card { background: #22252a; box-shadow: none; } }
|
||||
.mark { font-size: 2.5rem; }
|
||||
h1 { font-size: 1.15rem; margin: .75rem 0 .5rem; font-weight: 600; }
|
||||
p { margin: .5rem 0; line-height: 1.6; color: #5b6169; }
|
||||
@media (prefers-color-scheme: dark) { p { color: #a8adb5; } }
|
||||
.action { margin-top: 1.5rem; font-weight: 600; }
|
||||
code {
|
||||
padding: .15em .45em; border-radius: 5px; font-size: .9em;
|
||||
background: rgba(127,127,127,.15);
|
||||
}
|
||||
.hidden { display: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<div class="mark">🦞</div>
|
||||
<div id="searching">
|
||||
<h1 data-msg="looking">Opening settings…</h1>
|
||||
</div>
|
||||
<div id="offline" class="hidden">
|
||||
<h1 data-msg="what">U-Claw is not running yet.</h1>
|
||||
<p data-msg="why">The settings page lives inside U-Claw, so it has to be started first.</p>
|
||||
<p class="action" data-msg="action">Start U-Claw, then open this page again.</p>
|
||||
<p><code data-msg="how"></code></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// The config server binds the first free port in this range and can fall back
|
||||
// when 18788 is taken, so probe rather than assume. no-cors keeps this working
|
||||
// from a file:// origin: the response is opaque, but it still resolves when a
|
||||
// server answered and rejects when nothing is listening.
|
||||
const PORT_FROM = 18788;
|
||||
const PORT_TO = 18798;
|
||||
|
||||
const MESSAGES = {
|
||||
en: {
|
||||
looking: 'Opening settings…',
|
||||
what: 'U-Claw is not running yet.',
|
||||
why: 'The settings page lives inside U-Claw, so it has to be started first.',
|
||||
action: 'Start U-Claw, then open this page again.',
|
||||
how: navigator.userAgent.includes('Windows') ? 'Double-click START HERE - Windows.bat' : 'Double-click START HERE - Mac.command',
|
||||
},
|
||||
'zh-CN': {
|
||||
looking: '正在打开设置…',
|
||||
what: 'U-Claw 还没有启动。',
|
||||
why: '设置页面是 U-Claw 的一部分,需要先把它启动起来。',
|
||||
action: '先启动 U-Claw,然后重新打开这个页面。',
|
||||
how: navigator.userAgent.includes('Windows') ? '双击 START HERE - Windows.bat' : '双击 START HERE - Mac.command',
|
||||
},
|
||||
};
|
||||
|
||||
const locale = String(navigator.language || 'en').toLowerCase().startsWith('zh') ? 'zh-CN' : 'en';
|
||||
document.documentElement.lang = locale;
|
||||
|
||||
function render() {
|
||||
const strings = MESSAGES[locale];
|
||||
for (const node of document.querySelectorAll('[data-msg]')) {
|
||||
node.textContent = strings[node.dataset.msg];
|
||||
}
|
||||
}
|
||||
|
||||
async function reachable(port) {
|
||||
try {
|
||||
await fetch(`http://127.0.0.1:${port}/`, { mode: 'no-cors', cache: 'no-store' });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function findServer() {
|
||||
for (let port = PORT_FROM; port <= PORT_TO; port++) {
|
||||
if (await reachable(port)) return port;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
render();
|
||||
findServer().then((port) => {
|
||||
if (port) {
|
||||
location.replace(`http://127.0.0.1:${port}/`);
|
||||
return;
|
||||
}
|
||||
document.getElementById('searching').classList.add('hidden');
|
||||
document.getElementById('offline').classList.remove('hidden');
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -4,7 +4,7 @@
|
||||
# 诊断工具
|
||||
# ============================================================
|
||||
|
||||
UCLAW_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
UCLAW_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
LOG_FILE="$UCLAW_DIR/diagnostic-log.txt"
|
||||
|
||||
GREEN='\033[0;32m'
|
||||
@@ -16,10 +16,10 @@ clear
|
||||
echo ""
|
||||
echo " ========================================"
|
||||
echo " U-Claw Diagnostic Tool"
|
||||
echo " 诊断工具"
|
||||
echo " Check-up"
|
||||
echo " ========================================"
|
||||
echo ""
|
||||
echo " Checking system... 正在检查系统..."
|
||||
echo " Checking this computer…"
|
||||
echo ""
|
||||
|
||||
# Clear old log
|
||||
@@ -135,7 +135,7 @@ echo " Log saved to: diagnostic-log.txt"
|
||||
echo ""
|
||||
echo " Next steps:"
|
||||
echo " 1. Check diagnostic-log.txt for details"
|
||||
echo " 2. If errors found, try running Mac-Start.command"
|
||||
echo " 2. If errors found, try running START HERE - Mac.command"
|
||||
echo " to auto-install missing dependencies"
|
||||
echo ""
|
||||
read -p " Press Enter to exit..."
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
set -e
|
||||
|
||||
UCLAW_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
UCLAW_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
APP_DIR="$UCLAW_DIR/app"
|
||||
INSTALL_TARGET="$HOME/.uclaw"
|
||||
|
||||
@@ -18,16 +18,16 @@ NC='\033[0m'
|
||||
BOLD='\033[1m'
|
||||
DIM='\033[2m'
|
||||
|
||||
NODE_VER="v22.14.0"
|
||||
MIRROR="https://registry.npmmirror.com"
|
||||
NODE_MIRROR="https://npmmirror.com/mirrors/node"
|
||||
NODE_VER="$(tr -d "[:space:]" < "$UCLAW_DIR/NODE_VERSION" 2>/dev/null || tr -d "[:space:]" < "$UCLAW_DIR/../NODE_VERSION" 2>/dev/null || echo v22.22.1)"
|
||||
MIRROR="https://registry.npmjs.org"
|
||||
NODE_MIRROR="https://nodejs.org/dist"
|
||||
|
||||
clear
|
||||
echo ""
|
||||
echo -e "${CYAN}${BOLD}"
|
||||
echo " ╔══════════════════════════════════════╗"
|
||||
echo " ║ U-Claw 安装到 Mac ║"
|
||||
echo " ║ 从 U 盘离线安装 ║"
|
||||
echo " ║ Install U-Claw on this Mac ║"
|
||||
echo " ║ Offline, from the drive ║"
|
||||
echo " ╚══════════════════════════════════════╝"
|
||||
echo -e "${NC}"
|
||||
echo ""
|
||||
@@ -35,7 +35,7 @@ echo ""
|
||||
# ---- Check CPU ----
|
||||
ARCH=$(uname -m)
|
||||
if [ "$ARCH" = "arm64" ]; then
|
||||
echo -e " ${GREEN}Apple Silicon (M 系列) ✓${NC}"
|
||||
echo -e " ${GREEN}Apple Silicon${NC}"
|
||||
NODE_PLATFORM="node-mac-arm64"
|
||||
else
|
||||
echo -e " ${YELLOW}Intel Mac${NC}"
|
||||
@@ -45,18 +45,18 @@ echo ""
|
||||
|
||||
# ---- Check existing installation ----
|
||||
if [ -d "$INSTALL_TARGET" ]; then
|
||||
echo -e " ${YELLOW}检测到已有安装: $INSTALL_TARGET${NC}"
|
||||
read -p " 覆盖安装?(y/n): " -n 1 OVERWRITE
|
||||
echo -e " ${YELLOW}Already installed at $INSTALL_TARGET${NC}"
|
||||
read -p " Install over it? (y/n): " -n 1 OVERWRITE
|
||||
echo ""
|
||||
if [ "$OVERWRITE" != "y" ] && [ "$OVERWRITE" != "Y" ]; then
|
||||
echo -e " ${DIM}已取消${NC}"
|
||||
echo -e " ${DIM}Cancelled${NC}"
|
||||
exit 0
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# ---- Step 1: Check environment ----
|
||||
echo -e " ${BOLD}[1/4] 检查环境...${NC}"
|
||||
echo -e " ${BOLD}[1/4] Checking${NC}"
|
||||
|
||||
NEED_DOWNLOAD_NODE=false
|
||||
NEED_DOWNLOAD_OPENCLAW=false
|
||||
@@ -66,21 +66,21 @@ USB_NODE="$APP_DIR/runtime/$NODE_PLATFORM/bin/node"
|
||||
USB_NPM="$APP_DIR/runtime/$NODE_PLATFORM/bin/npm"
|
||||
|
||||
if [ -f "$USB_NODE" ]; then
|
||||
echo -e " ${GREEN}Node.js: 使用 U 盘内的 ($("$USB_NODE" --version))${NC}"
|
||||
echo -e " ${GREEN}Node.js: using the copy on the drive ($("$USB_NODE" --version))${NC}"
|
||||
USE_NODE="usb"
|
||||
elif command -v node >/dev/null 2>&1; then
|
||||
SYS_VER=$(node --version)
|
||||
MAJOR=$(echo "$SYS_VER" | sed 's/v//' | cut -d. -f1)
|
||||
if [ "$MAJOR" -ge 20 ] 2>/dev/null; then
|
||||
echo -e " ${GREEN}Node.js: 使用系统的 ($SYS_VER)${NC}"
|
||||
echo -e " ${GREEN}Node.js: using the system copy ($SYS_VER)${NC}"
|
||||
USE_NODE="system"
|
||||
else
|
||||
echo -e " ${YELLOW}Node.js: 系统版本太低 ($SYS_VER),需要 v20+${NC}"
|
||||
echo -e " ${YELLOW}Node.js: the system copy is too old ($SYS_VER); v20 or newer is needed${NC}"
|
||||
NEED_DOWNLOAD_NODE=true
|
||||
USE_NODE="download"
|
||||
fi
|
||||
else
|
||||
echo -e " ${YELLOW}Node.js: 未安装${NC}"
|
||||
echo -e " ${YELLOW}Node.js: not installed${NC}"
|
||||
NEED_DOWNLOAD_NODE=true
|
||||
USE_NODE="download"
|
||||
fi
|
||||
@@ -88,10 +88,10 @@ fi
|
||||
# Check OpenClaw
|
||||
USB_OPENCLAW="$APP_DIR/core/node_modules/openclaw/openclaw.mjs"
|
||||
if [ -f "$USB_OPENCLAW" ]; then
|
||||
echo -e " ${GREEN}OpenClaw: 使用 U 盘内的${NC}"
|
||||
echo -e " ${GREEN}OpenClaw: using the copy on the drive${NC}"
|
||||
USE_OPENCLAW="usb"
|
||||
else
|
||||
echo -e " ${YELLOW}OpenClaw: U 盘内未找到,需要在线下载${NC}"
|
||||
echo -e " ${YELLOW}OpenClaw: not on the drive, will download it${NC}"
|
||||
NEED_DOWNLOAD_OPENCLAW=true
|
||||
USE_OPENCLAW="download"
|
||||
fi
|
||||
@@ -99,7 +99,7 @@ fi
|
||||
echo ""
|
||||
|
||||
# ---- Step 2: Create install directory ----
|
||||
echo -e " ${BOLD}[2/4] 安装到 $INSTALL_TARGET ...${NC}"
|
||||
echo -e " ${BOLD}[2/4] Installing to $INSTALL_TARGET${NC}"
|
||||
|
||||
mkdir -p "$INSTALL_TARGET"
|
||||
mkdir -p "$INSTALL_TARGET/data/.openclaw"
|
||||
@@ -107,27 +107,27 @@ mkdir -p "$INSTALL_TARGET/data/memory"
|
||||
mkdir -p "$INSTALL_TARGET/data/backups"
|
||||
|
||||
# ---- Step 3: Copy/Download Node.js ----
|
||||
echo -e " ${BOLD}[3/4] 安装 Node.js...${NC}"
|
||||
echo -e " ${BOLD}[3/4] Installing Node.js${NC}"
|
||||
|
||||
NODE_INSTALL_DIR="$INSTALL_TARGET/runtime/$NODE_PLATFORM"
|
||||
|
||||
case $USE_NODE in
|
||||
usb)
|
||||
echo -e " ${CYAN}从 U 盘复制 Node.js...${NC}"
|
||||
echo -e " ${CYAN}Copying Node.js from the drive${NC}"
|
||||
mkdir -p "$NODE_INSTALL_DIR"
|
||||
cp -R "$APP_DIR/runtime/$NODE_PLATFORM/"* "$NODE_INSTALL_DIR/"
|
||||
chmod +x "$NODE_INSTALL_DIR/bin/node"
|
||||
INSTALL_NODE="$NODE_INSTALL_DIR/bin/node"
|
||||
INSTALL_NPM="$NODE_INSTALL_DIR/bin/npm"
|
||||
echo -e " ${GREEN}Node.js 安装完成 ✓${NC}"
|
||||
echo -e " ${GREEN}Node.js installed${NC}"
|
||||
;;
|
||||
system)
|
||||
INSTALL_NODE="$(which node)"
|
||||
INSTALL_NPM="$(which npm)"
|
||||
echo -e " ${GREEN}使用系统 Node.js ✓${NC}"
|
||||
echo -e " ${GREEN}Using the system Node.js${NC}"
|
||||
;;
|
||||
download)
|
||||
echo -e " ${CYAN}从国内镜像下载 Node.js $NODE_VER...${NC}"
|
||||
echo -e " ${CYAN}Downloading Node.js $NODE_VER...${NC}"
|
||||
PLATFORM_NAME="darwin-$ARCH"
|
||||
TARBALL="node-${NODE_VER}-${PLATFORM_NAME}.tar.gz"
|
||||
URL="${NODE_MIRROR}/${NODE_VER}/${TARBALL}"
|
||||
@@ -139,26 +139,26 @@ case $USE_NODE in
|
||||
chmod +x "$NODE_INSTALL_DIR/bin/node"
|
||||
INSTALL_NODE="$NODE_INSTALL_DIR/bin/node"
|
||||
INSTALL_NPM="$NODE_INSTALL_DIR/bin/npm"
|
||||
echo -e " ${GREEN}Node.js 下载安装完成 ✓${NC}"
|
||||
echo -e " ${GREEN}Node.js installed${NC}"
|
||||
;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
|
||||
# ---- Step 4: Copy/Download OpenClaw ----
|
||||
echo -e " ${BOLD}[4/4] 安装 OpenClaw...${NC}"
|
||||
echo -e " ${BOLD}[4/4] Installing OpenClaw${NC}"
|
||||
|
||||
CORE_INSTALL_DIR="$INSTALL_TARGET/core"
|
||||
|
||||
case $USE_OPENCLAW in
|
||||
usb)
|
||||
echo -e " ${CYAN}从 U 盘复制 OpenClaw + 插件...${NC}"
|
||||
echo -e " ${CYAN}Copying OpenClaw and its plugins from the drive${NC}"
|
||||
mkdir -p "$CORE_INSTALL_DIR"
|
||||
cp -R "$APP_DIR/core/"* "$CORE_INSTALL_DIR/"
|
||||
echo -e " ${GREEN}OpenClaw 安装完成 ✓${NC}"
|
||||
echo -e " ${GREEN}OpenClaw installed${NC}"
|
||||
;;
|
||||
download)
|
||||
echo -e " ${CYAN}从国内镜像下载 OpenClaw...${NC}"
|
||||
echo -e " ${CYAN}Downloading OpenClaw${NC}"
|
||||
mkdir -p "$CORE_INSTALL_DIR"
|
||||
cat > "$CORE_INSTALL_DIR/package.json" << 'PKGEOF'
|
||||
{
|
||||
@@ -173,7 +173,7 @@ PKGEOF
|
||||
cd "$CORE_INSTALL_DIR"
|
||||
"$INSTALL_NODE" "$INSTALL_NPM" install --registry="$MIRROR" 2>&1 | tail -3
|
||||
"$INSTALL_NODE" "$INSTALL_NPM" install @sliverp/qqbot@latest --registry="$MIRROR" 2>&1 | tail -2
|
||||
echo -e " ${GREEN}OpenClaw 下载安装完成 ✓${NC}"
|
||||
echo -e " ${GREEN}OpenClaw installed${NC}"
|
||||
;;
|
||||
esac
|
||||
|
||||
@@ -198,7 +198,7 @@ done
|
||||
# ---- Create launch script ----
|
||||
cat > "$INSTALL_TARGET/start.command" << 'STARTEOF'
|
||||
#!/bin/bash
|
||||
DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
ARCH=$(uname -m)
|
||||
NODE_PLATFORM="node-mac-$( [ "$ARCH" = "arm64" ] && echo "arm64" || echo "x64" )"
|
||||
|
||||
@@ -240,18 +240,18 @@ echo ""
|
||||
INSTALL_SIZE=$(du -sh "$INSTALL_TARGET" | cut -f1)
|
||||
|
||||
echo -e " ${GREEN}${BOLD}╔══════════════════════════════════════╗"
|
||||
echo -e " ║ ✅ 安装成功! ║"
|
||||
echo -e " ║ Installed. ║"
|
||||
echo -e " ╚══════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
echo -e " ${BOLD}安装位置:${NC} $INSTALL_TARGET"
|
||||
echo -e " ${BOLD}大小:${NC} $INSTALL_SIZE"
|
||||
echo -e " ${BOLD}Location:${NC} $INSTALL_TARGET"
|
||||
echo -e " ${BOLD}Size:${NC} $INSTALL_SIZE"
|
||||
echo ""
|
||||
echo -e " ${BOLD}启动方式:${NC}"
|
||||
echo -e " 双击 ${CYAN}$INSTALL_TARGET/start.command${NC}"
|
||||
echo -e " 或终端运行: ${CYAN}bash ~/.uclaw/start.command${NC}"
|
||||
echo -e " ${BOLD}To start it:${NC}"
|
||||
echo -e " Double-click ${CYAN}$INSTALL_TARGET/start.command${NC}"
|
||||
echo -e " or run: ${CYAN}bash ~/.uclaw/start.command${NC}"
|
||||
echo ""
|
||||
echo -e " ${BOLD}首次使用:${NC}"
|
||||
echo -e " 启动后浏览器自动打开配置页面"
|
||||
echo -e " 选择 AI 模型 → 填写 API Key → 开始用"
|
||||
echo -e " ${BOLD}First time:${NC}"
|
||||
echo -e " Settings opens in your browser by itself"
|
||||
echo -e " Paste an API key and you are ready"
|
||||
echo ""
|
||||
read -p " 按回车关闭..."
|
||||
read -p " Press Enter to close…"
|
||||
@@ -4,7 +4,7 @@
|
||||
# 双击运行:代理env + 直连可达 + 真发一条对话
|
||||
# ============================================================
|
||||
|
||||
UCLAW_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
UCLAW_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
APP_DIR="$UCLAW_DIR/app"
|
||||
CONFIG_FILE="$UCLAW_DIR/data/.openclaw/openclaw.json"
|
||||
|
||||
@@ -17,8 +17,8 @@ esac
|
||||
# 退而求其次:用系统 node
|
||||
if [ ! -x "$NODE_BIN" ]; then NODE_BIN="$(command -v node)"; fi
|
||||
if [ -z "$NODE_BIN" ] || [ ! -x "$NODE_BIN" ]; then
|
||||
echo " [错误] 找不到 Node 运行环境。请先正常启动一次 U-Claw。"
|
||||
read -p " 按回车关闭..."
|
||||
echo " Node.js is missing. Start U-Claw once first, then run this."
|
||||
read -p " Press Enter to close."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -28,6 +28,6 @@ xattr -rd com.apple.quarantine "$UCLAW_DIR" 2>/dev/null || true
|
||||
"$NODE_BIN" "$UCLAW_DIR/lib/intranet-check.mjs" "$CONFIG_FILE"
|
||||
|
||||
echo ""
|
||||
echo " 把整个窗口截图发给技术支持即可。"
|
||||
echo " Screenshot this whole window if you need to send it to someone."
|
||||
echo ""
|
||||
read -p " 按回车关闭..."
|
||||
read -p " Press Enter to close."
|
||||
@@ -4,7 +4,7 @@
|
||||
# 双击运行:命令行配 Ollama / newapi 并当场实测(不碰网页设置)
|
||||
# ============================================================
|
||||
|
||||
UCLAW_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
UCLAW_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
APP_DIR="$UCLAW_DIR/app"
|
||||
CONFIG_FILE="$UCLAW_DIR/data/.openclaw/openclaw.json"
|
||||
|
||||
@@ -16,8 +16,8 @@ case "$ARCH" in
|
||||
esac
|
||||
if [ ! -x "$NODE_BIN" ]; then NODE_BIN="$(command -v node)"; fi
|
||||
if [ -z "$NODE_BIN" ] || [ ! -x "$NODE_BIN" ]; then
|
||||
echo " [错误] 找不到 Node 运行环境。请先正常启动一次 U-Claw。"
|
||||
read -p " 按回车关闭..."
|
||||
echo " Node.js is missing. Start U-Claw once first, then run this."
|
||||
read -p " Press Enter to close."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -26,4 +26,4 @@ xattr -rd com.apple.quarantine "$UCLAW_DIR" 2>/dev/null || true
|
||||
"$NODE_BIN" "$UCLAW_DIR/lib/setup-local-model.mjs" "$CONFIG_FILE"
|
||||
|
||||
echo ""
|
||||
read -p " 按回车关闭..."
|
||||
read -p " Press Enter to close."
|
||||
@@ -2,7 +2,7 @@
|
||||
# U-Claw Menu - Portable AI Agent
|
||||
# macOS version
|
||||
|
||||
UCLAW_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
UCLAW_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
APP_DIR="$UCLAW_DIR/app"
|
||||
|
||||
# Migration shim: rename old core-mac to core for existing USB users
|
||||
@@ -60,66 +60,66 @@ run_oc() {
|
||||
show_menu() {
|
||||
clear
|
||||
local NODE_VER=$("$NODE_BIN" --version 2>/dev/null || echo "N/A")
|
||||
local CFG_STATUS="${RED}未配置${NC}"
|
||||
[ -f "$CONFIG_PATH" ] && CFG_STATUS="${GREEN}已配置${NC}"
|
||||
local CFG_STATUS="${RED}not set up${NC}"
|
||||
[ -f "$CONFIG_PATH" ] && CFG_STATUS="${GREEN}ready${NC}"
|
||||
|
||||
echo ""
|
||||
echo -e " ${CYAN}${BOLD}╔══════════════════════════════════════╗"
|
||||
echo -e " ║ U-Claw 虾盘 v1.1 ║"
|
||||
echo -e " ║ U-Claw v1.1 ║"
|
||||
echo -e " ║ Portable AI Agent ║"
|
||||
echo -e " ╚══════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
echo -e " Node: ${GREEN}${NODE_VER}${NC} 配置: ${CFG_STATUS}"
|
||||
echo -e " Node: ${GREEN}${NODE_VER}${NC} Model: ${CFG_STATUS}"
|
||||
echo ""
|
||||
echo -e " ${WHITE}${BOLD}── 配置 ──────────────────────────────${NC}"
|
||||
echo -e " ${GREEN}[1]${NC} 配置向导(选模型、填 API Key)"
|
||||
echo -e " ${GREEN}[2]${NC} 打开网页控制台"
|
||||
echo -e " ${WHITE}${BOLD}-- Setup ---------------------------------${NC}"
|
||||
echo -e " ${GREEN}[1]${NC} Set up a model and paste your key"
|
||||
echo -e " ${GREEN}[2]${NC} Open Settings in the browser"
|
||||
echo ""
|
||||
echo -e " ${WHITE}${BOLD}── 聊天平台 ──────────────────────────${NC}"
|
||||
echo -e " ${GREEN}[3]${NC} 接入 QQ 机器人(推荐,1分钟搞定)"
|
||||
echo -e " ${GREEN}[4]${NC} 接入其他平台(飞书/Telegram/微信)"
|
||||
echo -e " ${WHITE}${BOLD}-- Chat apps -----------------------------${NC}"
|
||||
echo -e " ${GREEN}[3]${NC} Connect WhatsApp / Telegram / Slack / Discord"
|
||||
echo -e " ${GREEN}[4]${NC} Connect a chat app used in China"
|
||||
echo ""
|
||||
echo -e " ${WHITE}${BOLD}── 维护 ──────────────────────────────${NC}"
|
||||
echo -e " ${GREEN}[5]${NC} 诊断修复"
|
||||
echo -e " ${GREEN}[6]${NC} 备份配置"
|
||||
echo -e " ${GREEN}[7]${NC} 恢复备份"
|
||||
echo -e " ${GREEN}[8]${NC} 系统信息"
|
||||
echo -e " ${WHITE}${BOLD}-- Maintenance ---------------------------${NC}"
|
||||
echo -e " ${GREEN}[5]${NC} Check and fix problems"
|
||||
echo -e " ${GREEN}[6]${NC} Back up your settings"
|
||||
echo -e " ${GREEN}[7]${NC} Restore a backup"
|
||||
echo -e " ${GREEN}[8]${NC} System information"
|
||||
echo ""
|
||||
echo -e " ${WHITE}${BOLD}── 高级维护 ──────────────────────────${NC}"
|
||||
echo -e " ${GREEN}[9]${NC} 杀死残留进程"
|
||||
echo -e " ${GREEN}[10]${NC} 查看日志"
|
||||
echo -e " ${GREEN}[11]${NC} 恢复出厂设置"
|
||||
echo -e " ${GREEN}[12]${NC} 卸载"
|
||||
echo -e " ${GREEN}[13]${NC} 检查更新"
|
||||
echo -e " ${GREEN}[14]${NC} 清理空间"
|
||||
echo -e " ${GREEN}[15]${NC} 插件管理"
|
||||
echo -e " ${WHITE}${BOLD}-- Advanced ------------------------------${NC}"
|
||||
echo -e " ${GREEN}[9]${NC} Stop leftover processes"
|
||||
echo -e " ${GREEN}[10]${NC} View logs"
|
||||
echo -e " ${GREEN}[11]${NC} Reset to factory settings"
|
||||
echo -e " ${GREEN}[12]${NC} Uninstall"
|
||||
echo -e " ${GREEN}[13]${NC} Check for updates"
|
||||
echo -e " ${GREEN}[14]${NC} Free up space"
|
||||
echo -e " ${GREEN}[15]${NC} Plugins"
|
||||
echo ""
|
||||
echo -e " ${DIM}[0] 退出${NC}"
|
||||
echo -e " ${DIM}[0] Quit${NC}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# [1] Config wizard
|
||||
do_config() {
|
||||
echo ""
|
||||
echo -e " ${CYAN}${BOLD}━━━ 配置向导 ━━━${NC}"
|
||||
echo -e " ${CYAN}${BOLD}--- Set up a model ---${NC}"
|
||||
echo ""
|
||||
echo -e " ${WHITE}国产模型选择提示:${NC}"
|
||||
echo -e " ${WHITE}Picking a provider in the wizard:${NC}"
|
||||
echo ""
|
||||
echo -e " DeepSeek → 选 Custom Provider"
|
||||
echo -e " DeepSeek → choose Custom Provider"
|
||||
echo -e " URL: https://api.deepseek.com/v1"
|
||||
echo -e " 模型: deepseek-v4-flash"
|
||||
echo -e " Kimi → 选 Moonshot AI"
|
||||
echo -e " 通义千问 → 选 Qwen"
|
||||
echo -e " 豆包 → 选 Volcano Engine"
|
||||
echo -e " model: deepseek-v4-flash"
|
||||
echo -e " Kimi → choose Moonshot AI"
|
||||
echo -e " Qwen -> choose Qwen"
|
||||
echo -e " Doubao -> choose Volcano Engine"
|
||||
echo ""
|
||||
read -p " 按回车启动配置向导..."
|
||||
read -p " Press Enter to start the wizard…"
|
||||
run_oc onboard
|
||||
}
|
||||
|
||||
# [2] Web dashboard
|
||||
do_dashboard() {
|
||||
echo ""
|
||||
echo -e " ${CYAN}启动网页控制台...${NC}"
|
||||
echo -e " ${CYAN}Opening Settings…${NC}"
|
||||
echo ""
|
||||
cd "$CORE_DIR"
|
||||
|
||||
@@ -128,7 +128,7 @@ do_dashboard() {
|
||||
while lsof -i :$PORT >/dev/null 2>&1; do
|
||||
PORT=$((PORT + 1))
|
||||
if [ $PORT -gt 18799 ]; then
|
||||
echo -e " ${RED}端口 18789-18799 全被占用${NC}"
|
||||
echo -e " ${RED}Ports 18789-18799 are all in use — U-Claw may already be running${NC}"
|
||||
return
|
||||
fi
|
||||
done
|
||||
@@ -142,79 +142,79 @@ do_dashboard() {
|
||||
sleep 0.5
|
||||
if curl --noproxy '*' -s -o /dev/null "http://127.0.0.1:$PORT/" 2>/dev/null; then
|
||||
local URL="http://127.0.0.1:$PORT/#token=$TOKEN"
|
||||
echo -e " ${GREEN}控制台: $URL${NC}"
|
||||
echo -e " ${GREEN}Settings: $URL${NC}"
|
||||
open "$URL" 2>/dev/null
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
echo " 关闭此窗口会停止服务"
|
||||
echo " Closing this window stops U-Claw"
|
||||
wait $PID
|
||||
}
|
||||
|
||||
# [3] QQ Bot (pre-installed)
|
||||
do_qq() {
|
||||
echo ""
|
||||
echo -e " ${CYAN}${BOLD}━━━ 接入 QQ 机器人 ━━━${NC}"
|
||||
echo -e " ${CYAN}${BOLD}--- Connect a QQ bot ---${NC}"
|
||||
echo ""
|
||||
echo -e " ${GREEN}QQ 插件已预装!只需输入 AppID 和 AppSecret。${NC}"
|
||||
echo -e " ${GREEN}The QQ plugin is already installed. You only need the AppID and AppSecret.${NC}"
|
||||
echo ""
|
||||
echo " 获取方式: 访问 q.qq.com → 创建机器人"
|
||||
echo " Get them at q.qq.com by creating a bot"
|
||||
echo ""
|
||||
read -p " AppID: " QQ_ID
|
||||
read -p " AppSecret: " QQ_SECRET
|
||||
echo ""
|
||||
|
||||
if [ -z "$QQ_ID" ] || [ -z "$QQ_SECRET" ]; then
|
||||
echo -e " ${YELLOW}已取消${NC}"
|
||||
echo -e " ${YELLOW}Cancelled${NC}"
|
||||
return
|
||||
fi
|
||||
|
||||
run_oc channels add --channel qqbot --token "${QQ_ID}:${QQ_SECRET}" 2>&1 || true
|
||||
echo ""
|
||||
read -p " 你的 QQ 号(设白名单,留空跳过): " QQ_ALLOW
|
||||
read -p " Your QQ number, to allow only you (blank to skip): " QQ_ALLOW
|
||||
if [ -n "$QQ_ALLOW" ]; then
|
||||
run_oc config set channels.qqbot.allowFrom "\"${QQ_ALLOW}\"" 2>&1 || true
|
||||
echo -e " ${GREEN}白名单已设置${NC}"
|
||||
echo -e " ${GREEN}Allow-list set${NC}"
|
||||
fi
|
||||
echo ""
|
||||
echo -e " ${GREEN}QQ 机器人配置完成!重启网关后生效。${NC}"
|
||||
echo -e " ${GREEN}QQ bot configured. Restart U-Claw for it to take effect.${NC}"
|
||||
}
|
||||
|
||||
# [4] Other platforms
|
||||
do_platforms() {
|
||||
echo ""
|
||||
echo -e " ${CYAN}${BOLD}━━━ 其他聊天平台 ━━━${NC}"
|
||||
echo -e " ${CYAN}${BOLD}--- Other chat apps ---${NC}"
|
||||
echo ""
|
||||
echo -e " ${GREEN}[a]${NC} 飞书 Feishu — 企业首选"
|
||||
echo -e " ${GREEN}[b]${NC} Telegram — 海外推荐"
|
||||
echo -e " ${GREEN}[c]${NC} 微信(社区插件) — iPad协议"
|
||||
echo -e " ${GREEN}[a]${NC} Feishu / Lark"
|
||||
echo -e " ${GREEN}[b]${NC} Telegram"
|
||||
echo -e " ${GREEN}[c]${NC} WeChat (community plugin)"
|
||||
echo -e " ${GREEN}[d]${NC} Discord"
|
||||
echo ""
|
||||
read -p " 选择 (a-d): " -n 1 CH
|
||||
read -p " Choose (a-d): " -n 1 CH
|
||||
echo ""
|
||||
echo ""
|
||||
|
||||
case $CH in
|
||||
a) echo " 飞书: 访问 open.feishu.cn/app 创建应用" ;;
|
||||
b) echo " Telegram: 找 @BotFather 创建机器人" ;;
|
||||
a) echo " Feishu: create an app at open.feishu.cn/app" ;;
|
||||
b) echo " Telegram: create a bot with @BotFather" ;;
|
||||
c)
|
||||
echo -e " ${YELLOW}安装微信插件...${NC}"
|
||||
echo -e " ${YELLOW}Installing the WeChat plugin…${NC}"
|
||||
run_oc plugins install @icesword760/openclaw-wechat 2>&1 || true
|
||||
;;
|
||||
d) echo " Discord: 访问 discord.com/developers/applications" ;;
|
||||
*) echo " 无效选择" ;;
|
||||
d) echo " Discord: create an app at discord.com/developers/applications" ;;
|
||||
*) echo " Not a valid choice" ;;
|
||||
esac
|
||||
echo ""
|
||||
echo " 配置完成后运行配置向导 [1] 绑定平台"
|
||||
echo " Then run [1] to finish connecting it"
|
||||
}
|
||||
|
||||
# [5] Doctor
|
||||
do_doctor() {
|
||||
echo ""
|
||||
echo -e " ${CYAN}━━━ 诊断修复 ━━━${NC}"
|
||||
echo -e " ${CYAN}━━━ Check and fix problems ━━━${NC}"
|
||||
echo ""
|
||||
run_oc doctor --repair 2>&1 || echo -e " ${YELLOW}诊断命令执行失败${NC}"
|
||||
run_oc doctor --repair 2>&1 || echo -e " ${YELLOW}The check could not run${NC}"
|
||||
}
|
||||
|
||||
# [6] Backup
|
||||
@@ -228,8 +228,8 @@ do_backup() {
|
||||
[ -d "$DATA_DIR/memory" ] && cp -R "$DATA_DIR/memory" "$BK/" 2>/dev/null && echo -e " ${GREEN} + memory/${NC}"
|
||||
|
||||
echo ""
|
||||
echo -e " ${GREEN}备份完成: $BK${NC}"
|
||||
echo " 大小: $(du -sh "$BK" | cut -f1)"
|
||||
echo -e " ${GREEN}Backed up to $BK${NC}"
|
||||
echo " Size: $(du -sh "$BK" | cut -f1)"
|
||||
}
|
||||
|
||||
# [7] Restore
|
||||
@@ -237,48 +237,48 @@ do_restore() {
|
||||
echo ""
|
||||
local BK_DIR="$DATA_DIR/backups"
|
||||
if [ ! -d "$BK_DIR" ] || [ -z "$(ls -A "$BK_DIR" 2>/dev/null)" ]; then
|
||||
echo -e " ${YELLOW}没有备份${NC}"
|
||||
echo -e " ${YELLOW}No backups yet${NC}"
|
||||
return
|
||||
fi
|
||||
|
||||
echo " 可用备份:"
|
||||
echo " Available backups:"
|
||||
local i=1
|
||||
for b in "$BK_DIR"/*/; do
|
||||
echo -e " ${GREEN}[$i]${NC} $(basename "$b") ($(du -sh "$b" | cut -f1))"
|
||||
i=$((i+1))
|
||||
done
|
||||
echo ""
|
||||
read -p " 选择编号: " NUM
|
||||
read -p " Which one? " NUM
|
||||
|
||||
local j=1
|
||||
for b in "$BK_DIR"/*/; do
|
||||
if [ "$j" = "$NUM" ]; then
|
||||
[ -f "$b/openclaw.json" ] && cp "$b/openclaw.json" "$CONFIG_PATH" && echo -e " ${GREEN} + 配置已恢复${NC}"
|
||||
[ -d "$b/memory" ] && cp -R "$b/memory" "$DATA_DIR/" && echo -e " ${GREEN} + 记忆已恢复${NC}"
|
||||
echo -e " ${GREEN}恢复完成${NC}"
|
||||
[ -f "$b/openclaw.json" ] && cp "$b/openclaw.json" "$CONFIG_PATH" && echo -e " ${GREEN} + settings restored${NC}"
|
||||
[ -d "$b/memory" ] && cp -R "$b/memory" "$DATA_DIR/" && echo -e " ${GREEN} + memory restored${NC}"
|
||||
echo -e " ${GREEN}Restored${NC}"
|
||||
return
|
||||
fi
|
||||
j=$((j+1))
|
||||
done
|
||||
echo -e " ${RED}无效选择${NC}"
|
||||
echo -e " ${RED}Not a valid choice${NC}"
|
||||
}
|
||||
|
||||
# [8] System info
|
||||
do_sysinfo() {
|
||||
echo ""
|
||||
echo " 系统: $(sw_vers -productName 2>/dev/null) $(sw_vers -productVersion 2>/dev/null)"
|
||||
echo " System: $(sw_vers -productName 2>/dev/null) $(sw_vers -productVersion 2>/dev/null)"
|
||||
echo " CPU: $(uname -m)"
|
||||
echo " 内存: $(sysctl -n hw.memsize 2>/dev/null | awk '{printf "%.0f GB", $1/1024/1024/1024}')"
|
||||
echo " Memory: $(sysctl -n hw.memsize 2>/dev/null | awk '{printf "%.0f GB", $1/1024/1024/1024}')"
|
||||
echo " Node: $("$NODE_BIN" --version 2>/dev/null)"
|
||||
echo " 路径: $UCLAW_DIR"
|
||||
echo " 大小: $(du -sh "$UCLAW_DIR" 2>/dev/null | cut -f1)"
|
||||
echo " 磁盘: $(df -h "$UCLAW_DIR" | tail -1 | awk '{print $4 " 可用"}')"
|
||||
echo " Path: $UCLAW_DIR"
|
||||
echo " Size: $(du -sh "$UCLAW_DIR" 2>/dev/null | cut -f1)"
|
||||
echo " Disk: $(df -h "$UCLAW_DIR" | tail -1 | awk '{print $4 " free"}')"
|
||||
}
|
||||
|
||||
# Main loop
|
||||
while true; do
|
||||
show_menu
|
||||
read -p " 请选择 [0-15]: " CHOICE
|
||||
read -p " Choose [0-15]: " CHOICE
|
||||
echo ""
|
||||
|
||||
case $CHOICE in
|
||||
@@ -297,10 +297,10 @@ while true; do
|
||||
13) do_update ;;
|
||||
14) do_cleanup ;;
|
||||
15) do_plugins ;;
|
||||
0) echo -e " ${CYAN}再见!${NC}"; exit 0 ;;
|
||||
*) echo -e " ${RED}无效选择${NC}" ;;
|
||||
0) echo -e " ${CYAN}Bye.${NC}"; exit 0 ;;
|
||||
*) echo -e " ${RED}Not a valid choice${NC}" ;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
read -p " 按回车返回..."
|
||||
read -p " Press Enter to go back…"
|
||||
done
|
||||
@@ -2,10 +2,10 @@
|
||||
# ============================================================
|
||||
# U-Claw - Interactive CLI (macOS)
|
||||
# 进阶用户:双击打开一个配置好环境的终端,可直接敲 openclaw 命令。
|
||||
# 复用与 Mac-Start.command 一致的便携环境(盘内 Node + 盘内数据)。
|
||||
# 复用与 START HERE - Mac.command 一致的便携环境(盘内 Node + 盘内数据)。
|
||||
# ============================================================
|
||||
|
||||
UCLAW_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
UCLAW_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
APP_DIR="$UCLAW_DIR/app"
|
||||
CORE_DIR="$APP_DIR/core"
|
||||
DATA_DIR="$UCLAW_DIR/data"
|
||||
@@ -13,7 +13,7 @@ STATE_DIR="$DATA_DIR/.openclaw"
|
||||
|
||||
RED='\033[0;31m'; GREEN='\033[0;32m'; CYAN='\033[0;36m'; NC='\033[0m'
|
||||
|
||||
# Detect CPU & set runtime(与 Mac-Start.command 一致)
|
||||
# Detect CPU & set runtime(与 START HERE - Mac.command 一致)
|
||||
ARCH=$(uname -m)
|
||||
if [ "$ARCH" = "arm64" ]; then
|
||||
NODE_DIR="$APP_DIR/runtime/node-mac-arm64"
|
||||
@@ -27,12 +27,12 @@ fi
|
||||
|
||||
NODE_BIN="$NODE_DIR/bin/node"
|
||||
if [ ! -f "$NODE_BIN" ]; then
|
||||
echo -e " ${RED}Node.js runtime not found.${NC} Run Mac-Start.command once first."
|
||||
echo -e " ${RED}Node.js runtime not found.${NC} Run START HERE - Mac.command once first."
|
||||
read -p " Press Enter to exit..."
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -x "$CORE_DIR/node_modules/.bin/openclaw" ]; then
|
||||
echo -e " ${RED}OpenClaw not installed yet.${NC} Run Mac-Start.command once first."
|
||||
echo -e " ${RED}OpenClaw not installed yet.${NC} Run START HERE - Mac.command once first."
|
||||
read -p " Press Enter to exit..."
|
||||
exit 1
|
||||
fi
|
||||
@@ -5,7 +5,7 @@
|
||||
# 只读:故意不传 --fix/--repair/--force。卡住可按 Ctrl+C 安全中断。
|
||||
# ============================================================
|
||||
|
||||
UCLAW_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
UCLAW_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
APP_DIR="$UCLAW_DIR/app"
|
||||
CORE_DIR="$APP_DIR/core"
|
||||
DATA_DIR="$UCLAW_DIR/data"
|
||||
@@ -20,13 +20,13 @@ case "$ARCH" in
|
||||
esac
|
||||
if [ ! -x "$NODE_BIN" ]; then NODE_BIN="$(command -v node)"; fi
|
||||
if [ -z "$NODE_BIN" ] || [ ! -x "$NODE_BIN" ]; then
|
||||
echo " [错误] 找不到 Node 运行环境。"
|
||||
read -p " 按回车关闭..."
|
||||
echo " Node.js is missing from this drive."
|
||||
read -p " Press Enter to close."
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -f "$OPENCLAW_MJS" ]; then
|
||||
echo " [错误] 找不到 OpenClaw 运行时 (app/core/node_modules/openclaw)。"
|
||||
read -p " 按回车关闭..."
|
||||
echo " OpenClaw is missing from this drive."
|
||||
read -p " Press Enter to close."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -39,14 +39,14 @@ xattr -rd com.apple.quarantine "$UCLAW_DIR" 2>/dev/null || true
|
||||
|
||||
echo ""
|
||||
echo " ========================================"
|
||||
echo " OpenClaw Doctor (官方体检, 英文, 较慢)"
|
||||
echo " OpenClaw Doctor — the upstream check. Thorough, and slow."
|
||||
echo " ========================================"
|
||||
echo " 请确保 U-Claw 已经在运行。卡住可 Ctrl+C 中断(只读,安全)。"
|
||||
echo " 想要快速的中文体检请改用 Mac-IntranetFix.command。"
|
||||
echo " U-Claw needs to be running. Ctrl+C is safe at any point — this only reads."
|
||||
echo " For a network-focused check, use Mac-IntranetFix.command instead."
|
||||
echo ""
|
||||
read -p " 按回车开始..."
|
||||
read -p " Press Enter to start."
|
||||
|
||||
"$NODE_BIN" "$OPENCLAW_MJS" doctor --non-interactive
|
||||
|
||||
echo ""
|
||||
read -p " 按回车关闭..."
|
||||
read -p " Press Enter to close."
|
||||
@@ -3,9 +3,9 @@ chcp 65001 >nul 2>&1
|
||||
title U-Claw Interactive CLI
|
||||
|
||||
REM 进阶用户:打开一个已配置好环境变量的命令行,可直接敲 openclaw 命令。
|
||||
REM 复用与 Windows-Start.bat 一致的便携环境(盘内 Node + 盘内数据)。
|
||||
REM 复用与 START HERE - Windows.bat 一致的便携环境(盘内 Node + 盘内数据)。
|
||||
|
||||
set "UCLAW_DIR=%~dp0"
|
||||
for %%I in ("%~dp0..") do set "UCLAW_DIR=%%~fI\"
|
||||
set "APP_DIR=%UCLAW_DIR%app"
|
||||
set "CORE_DIR=%APP_DIR%\core"
|
||||
set "DATA_DIR=%UCLAW_DIR%data"
|
||||
@@ -22,14 +22,14 @@ set "PATH=%NODE_DIR%;%CORE_DIR%\node_modules\.bin;%PATH%"
|
||||
|
||||
if not exist "%NODE_BIN%" (
|
||||
echo [ERROR] Node.js runtime not found
|
||||
echo Please run Windows-Start.bat once first to set up the runtime.
|
||||
echo Please run START HERE - Windows.bat once first to set up the runtime.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
if not exist "%CORE_DIR%\node_modules\.bin\openclaw.cmd" (
|
||||
echo [ERROR] OpenClaw not installed yet
|
||||
echo Please run Windows-Start.bat once first.
|
||||
echo Please run START HERE - Windows.bat once first.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
@@ -4,11 +4,11 @@ title U-Claw - OpenClaw Doctor (advanced)
|
||||
setlocal
|
||||
|
||||
REM Official OpenClaw health check (English, advanced users).
|
||||
REM NOTE: run this AFTER U-Claw is already running (double-click Windows-Start.bat
|
||||
REM NOTE: run this AFTER U-Claw is already running (double-click START HERE - Windows.bat
|
||||
REM first), otherwise doctor stalls while probing a gateway that is not up yet.
|
||||
REM Read-only: we deliberately do NOT pass --fix/--repair/--force.
|
||||
|
||||
set "UCLAW_DIR=%~dp0"
|
||||
for %%I in ("%~dp0..") do set "UCLAW_DIR=%%~fI\"
|
||||
set "APP_DIR=%UCLAW_DIR%app"
|
||||
set "CORE_DIR=%APP_DIR%\core"
|
||||
set "DATA_DIR=%UCLAW_DIR%data"
|
||||
35
portable/advanced/README.md
Normal file
35
portable/advanced/README.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# advanced/
|
||||
|
||||
Everything here used to sit in the drive's root folder. Twenty-three clickable
|
||||
files greeted anyone who plugged the drive in, and the first thing a
|
||||
non-technical user had to do was work out which of them applied to their
|
||||
computer — before anything had even run.
|
||||
|
||||
The root now holds three things: the two `START HERE` files and a short guide.
|
||||
Everything else lives here.
|
||||
|
||||
| File | What it is |
|
||||
|---|---|
|
||||
| `Windows-Menu.bat` · `Mac-Menu.command` | Menu of every tool below |
|
||||
| `Windows-Diagnose.bat` · `Mac-Diagnose.command` | Health check, collects logs for a bug report |
|
||||
| `OpenClaw-Doctor.bat` · `Mac-OpenClaw-Doctor.command` | Read-only diagnosis from OpenClaw itself |
|
||||
| `OpenClaw-CLI.bat` · `Mac-OpenClaw-CLI.command` | Drop into the raw `openclaw` CLI |
|
||||
| `Windows-Install.bat` · `Mac-Install.command` | Copy the drive's contents onto this computer |
|
||||
| `Windows-IntranetFix.bat` · `Mac-IntranetFix.command` | Fix a corporate proxy blocking your model endpoint |
|
||||
| `Windows-LocalModel.bat` · `Mac-LocalModel.command` | Set up an offline local model |
|
||||
| `setup.sh` · `setup.bat` · `setup.ps1` | Developer build: fetch Node.js + OpenClaw into `../app/` |
|
||||
| `Config.html` | Redirect to Settings, kept for older links |
|
||||
| `U-Claw.html` · `SkillHub.html` | Reference pages |
|
||||
|
||||
## If you move a script out of here
|
||||
|
||||
They all resolve the drive root as "one level up" from wherever they sit. Running
|
||||
one from somewhere else will not find `app/` or `data/`.
|
||||
|
||||
## Naming
|
||||
|
||||
The entry points are `START HERE - Windows.bat` and `START HERE - Mac.command`,
|
||||
with a hyphen rather than brackets. `cmd.exe` treats `)` as the end of an
|
||||
`IF (...)` block, so a filename containing brackets breaks every `echo` that
|
||||
mentions it inside a block — which is the flash-exit class of bug that
|
||||
`tests/windows-launchers.test.mjs` guards against. The hyphen avoids it entirely.
|
||||
125
portable/advanced/SkillHub.html
Normal file
125
portable/advanced/SkillHub.html
Normal file
@@ -0,0 +1,125 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title data-i18n="hub.title">U-Claw Skills</title>
|
||||
<script src="../data/.openclaw/locale.js"></script>
|
||||
<script src="../lib/i18n/messages.js"></script>
|
||||
<script src="../lib/i18n/skills-data.js"></script>
|
||||
<script src="../lib/i18n/i18n.js"></script>
|
||||
<!--
|
||||
What this page used to be: 56 hand-written cards advertising third-party
|
||||
ClawHub skills, in Chinese, with invented install counts. None of it was ours,
|
||||
none of it was translated, and the numbers could not be verified.
|
||||
|
||||
What it is now: the skills actually on this drive, generated from
|
||||
skills/manifest.json. Adding a skill to the manifest puts it here — there is
|
||||
nothing to hand-maintain.
|
||||
|
||||
skills-data.js is generated for the same reason messages.js is: this page opens
|
||||
from file://, where fetching manifest.json is blocked.
|
||||
-->
|
||||
<style>
|
||||
:root { color-scheme: dark; }
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0; padding: 3rem 1.5rem 4rem;
|
||||
font-family: -apple-system, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
background: #16181c; color: #f2f4f7; line-height: 1.6;
|
||||
}
|
||||
.wrap { max-width: 60rem; margin: 0 auto; }
|
||||
h1 { font-size: 1.8rem; margin: 0 0 .5rem; }
|
||||
.lede { color: #9aa0a8; margin: 0 0 2rem; }
|
||||
.filters { display: flex; flex-wrap: wrap; gap: .5rem; margin-bottom: 2rem; }
|
||||
.filters button {
|
||||
padding: .45rem 1rem; font: inherit; font-size: .88rem;
|
||||
color: #c8ccd2; background: #23262c; border: 1px solid #33373f;
|
||||
border-radius: 999px; cursor: pointer;
|
||||
}
|
||||
.filters button[aria-pressed="true"] { color: #16181c; background: #ff6b35; border-color: #ff6b35; font-weight: 600; }
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr)); gap: 1rem; }
|
||||
.skill { padding: 1.1rem; background: #23262c; border: 1px solid #33373f; border-radius: 12px; }
|
||||
.skill .top { display: flex; align-items: baseline; gap: .6rem; }
|
||||
.skill .icon { font-size: 1.4rem; }
|
||||
.skill h3 { margin: 0; font-size: 1rem; }
|
||||
.skill p { margin: .5rem 0 .75rem; color: #9aa0a8; font-size: .88rem; }
|
||||
.skill code { font-size: .78rem; color: #6f757d; }
|
||||
.empty { color: #9aa0a8; padding: 2rem 0; }
|
||||
footer { margin-top: 3rem; color: #6f757d; font-size: .85rem; }
|
||||
a { color: #ff6b35; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<h1 data-i18n="hub.title">U-Claw Skills</h1>
|
||||
<p class="lede" data-i18n="hub.lede">These are on this drive already. Just ask for what you want — you do not have to name the skill.</p>
|
||||
|
||||
<div class="filters" id="filters"></div>
|
||||
<div class="grid" id="grid"></div>
|
||||
<p class="empty" id="empty" hidden data-i18n="hub.none">Nothing in this category.</p>
|
||||
|
||||
<footer>
|
||||
<p data-i18n="hub.add_note">Skills live in the skills/ folder on this drive. Dropping a SKILL.md in there adds one.</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var t = function (k) { return window.UClawI18n ? window.UClawI18n.t(k) : k; };
|
||||
var data = window.UCLAW_SKILLS || { skills: [] };
|
||||
var active = 'all';
|
||||
|
||||
function categories() {
|
||||
var seen = {};
|
||||
data.skills.forEach(function (s) { (s.categories || []).forEach(function (c) { seen[c] = true; }); });
|
||||
return Object.keys(seen).sort();
|
||||
}
|
||||
|
||||
function render() {
|
||||
var grid = document.getElementById('grid');
|
||||
grid.innerHTML = '';
|
||||
var shown = data.skills.filter(function (s) {
|
||||
return active === 'all' || (s.categories || []).indexOf(active) !== -1;
|
||||
});
|
||||
shown.sort(function (a, b) { return t('skill.' + a.id + '.name').localeCompare(t('skill.' + b.id + '.name')); });
|
||||
|
||||
shown.forEach(function (s) {
|
||||
var el = document.createElement('div');
|
||||
el.className = 'skill';
|
||||
el.innerHTML =
|
||||
'<div class="top"><span class="icon">' + (s.emoji || '🧩') + '</span>'
|
||||
+ '<h3></h3></div><p></p><code>' + s.id + '</code>';
|
||||
// textContent, not innerHTML: catalogue strings are data, not markup.
|
||||
el.querySelector('h3').textContent = t('skill.' + s.id + '.name');
|
||||
el.querySelector('p').textContent = t('skill.' + s.id + '.desc');
|
||||
grid.appendChild(el);
|
||||
});
|
||||
|
||||
document.getElementById('empty').hidden = shown.length > 0;
|
||||
}
|
||||
|
||||
function buildFilters() {
|
||||
var bar = document.getElementById('filters');
|
||||
var options = [['all', t('hub.all')]].concat(categories().map(function (c) { return [c, t('cat.' + c)]; }));
|
||||
options.forEach(function (pair) {
|
||||
var btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.textContent = pair[1];
|
||||
btn.setAttribute('aria-pressed', pair[0] === active ? 'true' : 'false');
|
||||
btn.addEventListener('click', function () {
|
||||
active = pair[0];
|
||||
Array.prototype.forEach.call(bar.children, function (b) { b.setAttribute('aria-pressed', 'false'); });
|
||||
btn.setAttribute('aria-pressed', 'true');
|
||||
render();
|
||||
});
|
||||
bar.appendChild(btn);
|
||||
});
|
||||
}
|
||||
|
||||
buildFilters();
|
||||
render();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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="nav.title">U-Claw — Start here</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, "Microsoft YaHei", sans-serif; background: #0a0a0a; color: #e0e0e0; min-height: 100vh; }
|
||||
@@ -50,20 +50,23 @@ body { font-family: -apple-system, "Microsoft YaHei", sans-serif; background: #0
|
||||
.footer { text-align: center; margin-top: 40px; padding-top: 20px; border-top: 1px solid #222; color: #666; }
|
||||
.footer a { color: #ff6b35; text-decoration: none; }
|
||||
</style>
|
||||
<script src="../data/.openclaw/locale.js"></script>
|
||||
<script src="../lib/i18n/messages.js"></script>
|
||||
<script src="../lib/i18n/i18n.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
|
||||
<div class="header">
|
||||
<h1>🦞 <span>U-Claw</span> 虾盘 <span class="version">v1.1</span></h1>
|
||||
<p>Portable AI Agent — 插上就能用的 AI 助手</p>
|
||||
<h1>🦞 <span>U-Claw</span> <span class="version">v1.1</span></h1>
|
||||
<p data-i18n="nav.tagline">Portable AI agent — plug in and go</p>
|
||||
</div>
|
||||
|
||||
<!-- Status Bar -->
|
||||
<div class="status-bar">
|
||||
<div class="status-dot off" id="statusDot"></div>
|
||||
<span id="statusText">OpenClaw 未运行</span>
|
||||
<a href="#" id="statusLink" class="status-link" style="display:none" target="_blank">打开控制台 →</a>
|
||||
<span id="statusText" data-i18n="nav.status_off">U-Claw is not running</span>
|
||||
<a href="#" id="statusLink" class="status-link" style="display:none" target="_blank" data-i18n="nav.open_console">Open it →</a>
|
||||
</div>
|
||||
|
||||
<!-- OS Tabs -->
|
||||
@@ -75,29 +78,29 @@ body { font-family: -apple-system, "Microsoft YaHei", sans-serif; background: #0
|
||||
<!-- Mac Panel -->
|
||||
<div class="panel active" id="panel-mac">
|
||||
<div class="card">
|
||||
<h3>🚀 快速启动</h3>
|
||||
<h3>🚀 <span data-i18n="nav.quickstart">Quick start</span></h3>
|
||||
<div class="step">
|
||||
<div class="step-num">1</div>
|
||||
<div class="step-text">双击 <code>Mac-Start.command</code></div>
|
||||
<div class="step-text"><span data-i18n="nav.doubleclick">Double-click</span> <code>START HERE - Mac.command</code></div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-num">2</div>
|
||||
<div class="step-text">首次运行自动打开<b>配置页面</b>,选模型、填 API Key 即可</div>
|
||||
<div class="step-text" data-i18n="nav.step2">Settings opens by itself the first time — pick a model and paste your key.</div>
|
||||
</div>
|
||||
<p style="margin-top:12px; color:#999;">⚠️ 首次运行如果提示"无法验证开发者":右键点击 → 打开</p>
|
||||
<p style="margin-top:8px;">📝 也可以直接双击 <code>Config.html</code> 打开中文配置页面</p>
|
||||
<p style="margin-top:12px; color:#999;" data-i18n="nav.mac_gatekeeper">⚠️ If macOS says the developer cannot be verified: right-click the file, then choose Open.</p>
|
||||
<p style="margin-top:8px;" data-i18n="nav.config_shortcut">📝 You can also double-click Config.html — it takes you to the settings page.</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>⚙️ 全部功能(配置/诊断/备份)</h3>
|
||||
<p>双击 <code>Mac-Menu.command</code> 打开全功能菜单:</p>
|
||||
<h3>⚙️ <span data-i18n="nav.everything">Everything else</span></h3>
|
||||
<p><span data-i18n="nav.doubleclick">Double-click</span> <code>Mac-Menu.command</code> <span data-i18n="nav.menu_opens">for the full menu:</span></p>
|
||||
<div class="grid">
|
||||
<div class="grid-item"><h4>中文配置页面</h4><p>Config.html — 选模型、填 Key、接 QQ</p></div>
|
||||
<div class="grid-item"><h4>QQ Bot 接入</h4><p>已预装,只需 AppID + Secret</p></div>
|
||||
<div class="grid-item"><h4>诊断修复</h4><p>openclaw doctor 一键检测</p></div>
|
||||
<div class="grid-item"><h4>备份恢复</h4><p>配置和记忆一键备份</p></div>
|
||||
<div class="grid-item"><h4>技能浏览</h4><p>52 个预装技能</p></div>
|
||||
<div class="grid-item"><h4>系统信息</h4><p>版本、端口、运行状态</p></div>
|
||||
<div class="grid-item"><h4 data-i18n="nav.g_settings">Settings</h4><p data-i18n="nav.g_settings_d">Model, key and chat apps</p></div>
|
||||
<div class="grid-item"><h4 data-i18n="nav.g_checkup">Check-up</h4><p data-i18n="nav.g_checkup_d">Finds and fixes common problems</p></div>
|
||||
<div class="grid-item"><h4 data-i18n="nav.g_backup">Backup</h4><p data-i18n="nav.g_backup_d">Save settings and memory in one click</p></div>
|
||||
<div class="grid-item"><h4 data-i18n="nav.g_skills">Skills</h4><p data-i18n="nav.g_skills_d">Browse what U-Claw can do</p></div>
|
||||
<div class="grid-item"><h4 data-i18n="nav.g_system">System info</h4><p data-i18n="nav.g_system_d">Version, port, running state</p></div>
|
||||
<div class="grid-item"><h4 data-i18n="nav.g_cli">Command line</h4><p data-i18n="nav.g_cli_d">For advanced use</p></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -105,52 +108,52 @@ body { font-family: -apple-system, "Microsoft YaHei", sans-serif; background: #0
|
||||
<!-- Windows Panel -->
|
||||
<div class="panel" id="panel-win">
|
||||
<div class="card">
|
||||
<h3>🚀 快速启动</h3>
|
||||
<h3>🚀 <span data-i18n="nav.quickstart">Quick start</span></h3>
|
||||
<div class="step">
|
||||
<div class="step-num">1</div>
|
||||
<div class="step-text">双击 <code>Windows-Start.bat</code></div>
|
||||
<div class="step-text"><span data-i18n="nav.doubleclick">Double-click</span> <code>START HERE - Windows.bat</code></div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-num">2</div>
|
||||
<div class="step-text">首次运行自动打开<b>配置页面</b>,选模型、填 API Key 即可</div>
|
||||
<div class="step-text" data-i18n="nav.step2">Settings opens by itself the first time — pick a model and paste your key.</div>
|
||||
</div>
|
||||
<p style="margin-top:12px; color:#999;">⚠️ 如果弹出安全警告:点击"更多信息" → "仍要运行"</p>
|
||||
<p style="margin-top:12px; color:#999;" data-i18n="nav.win_smartscreen">⚠️ If Windows shows a security warning: click More info, then Run anyway.</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>⚙️ 全部功能(配置/诊断/备份)</h3>
|
||||
<p>双击 <code>Windows-Menu.bat</code> 打开全功能菜单:</p>
|
||||
<h3>⚙️ <span data-i18n="nav.everything">Everything else</span></h3>
|
||||
<p><span data-i18n="nav.doubleclick">Double-click</span> <code>Windows-Menu.bat</code> <span data-i18n="nav.menu_opens">for the full menu:</span></p>
|
||||
<div class="grid">
|
||||
<div class="grid-item"><h4>配置向导</h4><p>选模型、填 API Key、选平台</p></div>
|
||||
<div class="grid-item"><h4>QQ Bot 接入</h4><p>腾讯官方,3条命令搞定</p></div>
|
||||
<div class="grid-item"><h4>诊断修复</h4><p>openclaw doctor 一键检测</p></div>
|
||||
<div class="grid-item"><h4>备份恢复</h4><p>配置和记忆一键备份</p></div>
|
||||
<div class="grid-item"><h4>技能浏览</h4><p>52 个预装技能</p></div>
|
||||
<div class="grid-item"><h4>镜像配置</h4><p>npm 淘宝镜像</p></div>
|
||||
<div class="grid-item"><h4 data-i18n="nav.g_settings">Settings</h4><p data-i18n="nav.g_settings_d">Model, key and chat apps</p></div>
|
||||
<div class="grid-item"><h4 data-i18n="nav.g_checkup">Check-up</h4><p data-i18n="nav.g_checkup_d">Finds and fixes common problems</p></div>
|
||||
<div class="grid-item"><h4 data-i18n="nav.g_backup">Backup</h4><p data-i18n="nav.g_backup_d">Save settings and memory in one click</p></div>
|
||||
<div class="grid-item"><h4 data-i18n="nav.g_skills">Skills</h4><p data-i18n="nav.g_skills_d">Browse what U-Claw can do</p></div>
|
||||
<div class="grid-item"><h4 data-i18n="nav.g_system">System info</h4><p data-i18n="nav.g_system_d">Version, port, running state</p></div>
|
||||
<div class="grid-item"><h4 data-i18n="nav.g_cli">Command line</h4><p data-i18n="nav.g_cli_d">For advanced use</p></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Common sections -->
|
||||
<div class="section-title">🤖 支持的 AI 模型(国内免翻墙)</div>
|
||||
<div class="section-title">🤖 <span data-i18n="nav.models_title">Models you can use</span></div>
|
||||
<div class="card">
|
||||
<div class="grid">
|
||||
<div class="grid-item"><h4>DeepSeek</h4><p>编程首选,性价比最高</p></div>
|
||||
<div class="grid-item"><h4>Kimi K2.5</h4><p>256K 上下文,长文档</p></div>
|
||||
<div class="grid-item"><h4>通义千问 Qwen</h4><p>免费额度大</p></div>
|
||||
<div class="grid-item"><h4>智谱 GLM</h4><p>学术、中文 NLP</p></div>
|
||||
<div class="grid-item"><h4>MiniMax</h4><p>语音、多模态</p></div>
|
||||
<div class="grid-item"><h4>豆包 Doubao</h4><p>字节跳动,火山引擎</p></div>
|
||||
<div class="grid-item"><h4>Claude</h4><p data-i18n="welcome.model_claude">Best all-rounder</p></div>
|
||||
<div class="grid-item"><h4>GPT</h4><p data-i18n="welcome.model_gpt">Most widely used</p></div>
|
||||
<div class="grid-item"><h4>Gemini</h4><p data-i18n="welcome.model_gemini">Free tier, easiest signup</p></div>
|
||||
<div class="grid-item"><h4>SEA-LION</h4><p data-i18n="nav.model_sealion">Malay, Tamil and Singlish</p></div>
|
||||
<div class="grid-item"><h4 data-i18n="nav.model_local">Local model</h4><p data-i18n="welcome.model_local">Offline · nothing leaves this machine</p></div>
|
||||
<div class="grid-item"><h4 data-i18n="nav.model_more">More</h4><p data-i18n="nav.model_more_d">Others are listed in Settings</p></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-title">📖 更多资料</div>
|
||||
<div class="section-title">📖 <span data-i18n="nav.more_title">More</span></div>
|
||||
<div class="card">
|
||||
<div class="grid">
|
||||
<div class="grid-item" onclick="window.open('帮助文档.html')" style="cursor:pointer"><h4>📖 帮助文档</h4><p>完整使用指南</p></div>
|
||||
<div class="grid-item" onclick="window.open('技能市场.html')" style="cursor:pointer"><h4>🎯 技能市场</h4><p>39 个精选技能</p></div>
|
||||
<div class="grid-item"><h4>💬 联系我们</h4><p>微信: hecare888</p></div>
|
||||
<div class="grid-item"><h4>🌐 官网</h4><p><a href="https://u-claw.org" target="_blank" style="color:#ff6b35">u-claw.org</a></p></div>
|
||||
<div class="grid-item" onclick="window.open('../Read me first.html')" style="cursor:pointer"><h4>📖 <span data-i18n="nav.link_guide">Getting started</span></h4><p data-i18n="nav.link_guide_d">The three-step walkthrough</p></div>
|
||||
<div class="grid-item" onclick="window.open('SkillHub.html')" style="cursor:pointer"><h4>🎯 <span data-i18n="nav.link_skills">Skills</span></h4><p data-i18n="nav.link_skills_d">Browse what U-Claw can do</p></div>
|
||||
<div class="grid-item"><h4>💬 <span data-i18n="nav.link_contact">Contact</span></h4><p><a href="mailto:help@u-claw.org" style="color:#ff6b35">help@u-claw.org</a></p></div>
|
||||
<div class="grid-item"><h4>🌐 <span data-i18n="nav.link_site">Website</span></h4><p><a href="https://u-claw.org" target="_blank" rel="noopener" style="color:#ff6b35">u-claw.org</a></p></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -195,13 +198,13 @@ async function checkStatus() {
|
||||
}
|
||||
if (found) {
|
||||
document.getElementById('statusDot').className = 'status-dot on';
|
||||
document.getElementById('statusText').textContent = 'OpenClaw 运行中 (端口 ' + found + ')';
|
||||
document.getElementById('statusText').textContent = window.UClawI18n.t('nav.status_on', { port: found });
|
||||
var link = document.getElementById('statusLink');
|
||||
link.style.display = 'inline';
|
||||
link.href = 'http://127.0.0.1:' + found + '/#token=uclaw';
|
||||
} else {
|
||||
document.getElementById('statusDot').className = 'status-dot off';
|
||||
document.getElementById('statusText').textContent = 'OpenClaw 未运行 — 请先双击启动脚本';
|
||||
document.getElementById('statusText').textContent = window.UClawI18n.t('nav.status_off_hint');
|
||||
document.getElementById('statusLink').style.display = 'none';
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
@echo off
|
||||
@echo off
|
||||
setlocal EnableDelayedExpansion
|
||||
chcp 65001 >nul 2>&1
|
||||
title U-Claw - Diagnostic Tool
|
||||
|
||||
set "UCLAW_DIR=%~dp0"
|
||||
for %%I in ("%~dp0..") do set "UCLAW_DIR=%%~fI\"
|
||||
set "LOG_FILE=%UCLAW_DIR%diagnostic-log.txt"
|
||||
|
||||
echo.
|
||||
@@ -27,18 +27,18 @@ for /f "tokens=2 delims==" %%v in ('wmic os get OSArchitecture /format:list 2^>n
|
||||
echo. >> "%LOG_FILE%"
|
||||
|
||||
REM 1. Check Node.js
|
||||
echo [1/7] 检查 Node.js 运行环境...
|
||||
echo [1/7] Checking Node.js
|
||||
set "NODE_BIN=%UCLAW_DIR%app\runtime\node-win-x64\node.exe"
|
||||
set "ERROR_COUNT=0"
|
||||
if exist "%NODE_BIN%" (
|
||||
echo [OK] Node.js found >> "%LOG_FILE%"
|
||||
for /f "tokens=*" %%v in ('"%NODE_BIN%" --version 2^>^&1') do (
|
||||
echo Version: %%v >> "%LOG_FILE%"
|
||||
echo ✓ Node.js 运行环境: %%v
|
||||
echo [OK] Node.js: %%v
|
||||
)
|
||||
) else (
|
||||
echo [ERROR] Node.js not found >> "%LOG_FILE%"
|
||||
echo ✗ Node.js 运行环境: 缺失
|
||||
echo [!!] Node.js: missing
|
||||
echo Path: %NODE_BIN% >> "%LOG_FILE%"
|
||||
set /a ERROR_COUNT+=1
|
||||
)
|
||||
@@ -47,67 +47,67 @@ REM Migration shim: rename old core-win to core for existing USB users
|
||||
if exist "%UCLAW_DIR%app\core-win" if not exist "%UCLAW_DIR%app\core" ren "%UCLAW_DIR%app\core-win" core
|
||||
|
||||
REM 2. Check core directory
|
||||
echo [2/7] 检查依赖目录...
|
||||
echo [2/7] Checking folders
|
||||
set "CORE_DIR=%UCLAW_DIR%app\core"
|
||||
if exist "%CORE_DIR%" (
|
||||
echo [OK] core directory exists >> "%LOG_FILE%"
|
||||
echo ✓ 依赖目录: 正常
|
||||
echo [OK] Folders: ok
|
||||
) else (
|
||||
echo [ERROR] core directory not found >> "%LOG_FILE%"
|
||||
echo ✗ 依赖目录: 缺失
|
||||
echo [!!] Folders: missing
|
||||
set /a ERROR_COUNT+=1
|
||||
)
|
||||
|
||||
REM 3. Check node_modules
|
||||
echo [3/7] 检查 npm 依赖包...
|
||||
echo [3/7] Checking dependencies
|
||||
if exist "%CORE_DIR%\node_modules" (
|
||||
echo [OK] node_modules exists >> "%LOG_FILE%"
|
||||
echo ✓ npm 依赖包: 已安装
|
||||
echo [OK] Dependencies: installed
|
||||
) else (
|
||||
echo [ERROR] node_modules not found >> "%LOG_FILE%"
|
||||
echo ✗ npm 依赖包: 未安装
|
||||
echo [!!] Dependencies: not installed
|
||||
set /a ERROR_COUNT+=1
|
||||
)
|
||||
|
||||
REM 4. Check OpenClaw
|
||||
echo [4/7] 检查 OpenClaw 核心文件...
|
||||
echo [4/7] Checking OpenClaw
|
||||
set "OPENCLAW_MJS=%CORE_DIR%\node_modules\openclaw\openclaw.mjs"
|
||||
if exist "%OPENCLAW_MJS%" (
|
||||
echo [OK] openclaw.mjs found >> "%LOG_FILE%"
|
||||
echo ✓ OpenClaw 核心: 正常
|
||||
echo [OK] OpenClaw: ok
|
||||
) else (
|
||||
echo [ERROR] openclaw.mjs not found >> "%LOG_FILE%"
|
||||
echo ✗ OpenClaw 核心: 缺失
|
||||
echo [!!] OpenClaw: missing
|
||||
echo Path: %OPENCLAW_MJS% >> "%LOG_FILE%"
|
||||
set /a ERROR_COUNT+=1
|
||||
)
|
||||
|
||||
REM 5. Check config
|
||||
echo [5/7] 检查配置文件...
|
||||
echo [5/7] Checking settings
|
||||
set "STATE_DIR=%UCLAW_DIR%data\.openclaw"
|
||||
if exist "%STATE_DIR%\openclaw.json" (
|
||||
echo [OK] Config file exists >> "%LOG_FILE%"
|
||||
echo ✓ 配置文件: 正常
|
||||
echo [OK] Settings file: ok
|
||||
REM Check if model is configured
|
||||
findstr /c:"model" "%STATE_DIR%\openclaw.json" >nul 2>&1
|
||||
if !errorlevel!==0 (
|
||||
echo ✓ AI 模型: 已配置
|
||||
echo [OK] Model: configured
|
||||
) else (
|
||||
echo ⚠ AI 模型: 未配置(首次使用请先配置)
|
||||
echo [ ?] Model: not set up yet
|
||||
)
|
||||
) else (
|
||||
echo [WARNING] Config not found >> "%LOG_FILE%"
|
||||
echo ⚠ 配置文件: 未创建(首次启动会自动创建)
|
||||
echo [ ?] Settings file: not created yet, U-Claw makes it on first run
|
||||
)
|
||||
|
||||
REM 6. Check port availability
|
||||
echo [6/7] 检查端口占用...
|
||||
echo [6/7] Checking ports
|
||||
set "PORT_ISSUE=0"
|
||||
for /l %%p in (18789,1,18799) do (
|
||||
netstat -an | findstr ":%%p " | findstr "LISTENING" >nul 2>&1
|
||||
if !errorlevel!==0 (
|
||||
echo [WARNING] Port %%p is in use >> "%LOG_FILE%"
|
||||
echo ⚠ 端口 %%p: 已被占用
|
||||
echo [ ?] Port %%p: in use
|
||||
for /f "tokens=5" %%a in ('netstat -ano ^| findstr ":%%p " ^| findstr "LISTENING"') do (
|
||||
echo PID: %%a >> "%LOG_FILE%"
|
||||
)
|
||||
@@ -116,11 +116,11 @@ for /l %%p in (18789,1,18799) do (
|
||||
)
|
||||
if "!PORT_ISSUE!"=="0" (
|
||||
echo [OK] Ports 18789-18799 available >> "%LOG_FILE%"
|
||||
echo ✓ 端口 18789-18799: 全部可用
|
||||
echo [OK] Ports 18789-18799: all free
|
||||
)
|
||||
|
||||
REM 7. Test OpenClaw startup
|
||||
echo [7/7] 测试 OpenClaw 启动...
|
||||
echo [7/7] Testing OpenClaw
|
||||
echo. >> "%LOG_FILE%"
|
||||
echo Testing OpenClaw startup: >> "%LOG_FILE%"
|
||||
echo ---------------------------------------- >> "%LOG_FILE%"
|
||||
@@ -133,15 +133,15 @@ if exist "%NODE_BIN%" if exist "%OPENCLAW_MJS%" (
|
||||
cd /d "%CORE_DIR%"
|
||||
for /f "tokens=*" %%v in ('"%NODE_BIN%" "%OPENCLAW_MJS%" --version 2^>^&1') do (
|
||||
echo %%v >> "%LOG_FILE%"
|
||||
echo ✓ OpenClaw 启动测试: 通过 (%%v^)
|
||||
echo [OK] OpenClaw starts: yes (%%v^)
|
||||
)
|
||||
if !errorlevel! neq 0 (
|
||||
echo ✗ OpenClaw 启动测试: 失败
|
||||
echo [!!] OpenClaw starts: no
|
||||
echo [ERROR] OpenClaw failed to start >> "%LOG_FILE%"
|
||||
set /a ERROR_COUNT+=1
|
||||
)
|
||||
) else (
|
||||
echo ⚠ OpenClaw 启动测试: 跳过(文件缺失)
|
||||
echo [ ?] OpenClaw starts: skipped, files missing
|
||||
echo [SKIP] Cannot test - required files missing >> "%LOG_FILE%"
|
||||
)
|
||||
|
||||
@@ -159,28 +159,28 @@ echo Diagnostic complete. >> "%LOG_FILE%"
|
||||
echo Error count: !ERROR_COUNT! >> "%LOG_FILE%"
|
||||
|
||||
echo ========================================
|
||||
echo 诊断完成
|
||||
echo Check complete
|
||||
echo ========================================
|
||||
echo.
|
||||
|
||||
if !ERROR_COUNT!==0 (
|
||||
echo ✅ 检查结果: 全部正常!
|
||||
echo 所有必需的组件都已就绪,可以正常使用。
|
||||
echo [OK] Everything checks out.
|
||||
echo Every component U-Claw needs is present.
|
||||
echo.
|
||||
echo 下一步:
|
||||
echo - 双击 Windows-Start.bat 启动服务
|
||||
echo - 或双击 Config.html 配置 AI 模型
|
||||
echo Next:
|
||||
echo - Double-click START HERE - Windows.bat to run it
|
||||
echo - Settings opens by itself the first time
|
||||
) else (
|
||||
echo ❌ 检查结果: 发现 !ERROR_COUNT! 个问题
|
||||
echo [!!] Found !ERROR_COUNT! problem^(s^).
|
||||
echo.
|
||||
echo 解决方案:
|
||||
echo 1. 查看 diagnostic-log.txt 了解详细错误
|
||||
echo 2. 尝试重新运行 Windows-Start.bat
|
||||
echo (会自动安装缺失的依赖)
|
||||
echo 3. 如问题仍然存在,访问:
|
||||
echo github.com/dongsheng123132/u-claw
|
||||
echo What to do:
|
||||
echo 1. Open diagnostic-log.txt for the details
|
||||
echo 2. Run START HERE - Windows.bat again
|
||||
echo it reinstalls anything missing by itself
|
||||
echo 3. Still stuck? See:
|
||||
echo gitea.fanghe.it.com/zhenghy/u-claw
|
||||
)
|
||||
echo.
|
||||
echo 诊断日志已保存: diagnostic-log.txt
|
||||
echo Saved a log to diagnostic-log.txt
|
||||
echo.
|
||||
pause
|
||||
@@ -1,62 +1,62 @@
|
||||
@echo off
|
||||
@echo off
|
||||
setlocal EnableDelayedExpansion
|
||||
chcp 65001 >nul 2>&1
|
||||
title U-Claw - Install to Windows
|
||||
|
||||
echo.
|
||||
echo ========================================
|
||||
echo U-Claw 安装到 Windows
|
||||
echo 从 U 盘离线安装
|
||||
echo Install U-Claw on this PC
|
||||
echo Offline, from the drive
|
||||
echo ========================================
|
||||
echo.
|
||||
|
||||
set "UCLAW_DIR=%~dp0"
|
||||
for %%I in ("%~dp0..") do set "UCLAW_DIR=%%~fI\"
|
||||
set "APP_DIR=%UCLAW_DIR%app"
|
||||
set "INSTALL_TARGET=%USERPROFILE%\.uclaw"
|
||||
set "MIRROR=https://registry.npmmirror.com"
|
||||
set "NODE_MIRROR=https://npmmirror.com/mirrors/node"
|
||||
set "MIRROR=https://registry.npmjs.org"
|
||||
set "NODE_MIRROR=https://nodejs.org/dist"
|
||||
set "NODE_VER=v22.22.1"
|
||||
|
||||
REM ---- Step 1: Check environment ----
|
||||
echo [1/4] 检查环境...
|
||||
echo [1/4] Checking
|
||||
|
||||
set "USE_NODE=none"
|
||||
set "USB_NODE=%APP_DIR%\runtime\node-win-x64\node.exe"
|
||||
|
||||
if exist "%USB_NODE%" (
|
||||
for /f "tokens=*" %%v in ('"%USB_NODE%" --version') do echo Node.js: 使用 U 盘内的 %%v
|
||||
for /f "tokens=*" %%v in ('"%USB_NODE%" --version') do echo Node.js: using the copy on the drive, %%v
|
||||
set "USE_NODE=usb"
|
||||
) else (
|
||||
where node >nul 2>&1
|
||||
if !errorlevel!==0 (
|
||||
for /f "tokens=*" %%v in ('node --version') do (
|
||||
echo Node.js: 检查系统版本 %%v
|
||||
echo Node.js: checking the system copy, %%v
|
||||
set "SYS_VER=%%v"
|
||||
)
|
||||
REM Use only supported LTS majors. Newer Current releases can break native deps.
|
||||
for /f "tokens=1 delims=." %%m in ("!SYS_VER:v=!") do set "MAJOR=%%m"
|
||||
if !MAJOR! equ 20 (
|
||||
echo Node.js: 使用系统的 !SYS_VER!
|
||||
echo Node.js: using the system copy, !SYS_VER!
|
||||
set "USE_NODE=system"
|
||||
) else if !MAJOR! equ 22 (
|
||||
echo Node.js: 使用系统的 !SYS_VER!
|
||||
echo Node.js: using the system copy, !SYS_VER!
|
||||
set "USE_NODE=system"
|
||||
) else (
|
||||
echo Node.js: 系统版本不兼容 ^(!SYS_VER!^), 将使用内置 v22 LTS
|
||||
echo Node.js: system copy !SYS_VER! is not compatible, using the bundled v22 LTS
|
||||
set "USE_NODE=download"
|
||||
)
|
||||
) else (
|
||||
echo Node.js: 未安装
|
||||
echo Node.js: not installed
|
||||
set "USE_NODE=download"
|
||||
)
|
||||
)
|
||||
|
||||
set "USB_OPENCLAW=%APP_DIR%\core\node_modules\openclaw\openclaw.mjs"
|
||||
if exist "%USB_OPENCLAW%" (
|
||||
echo OpenClaw: 使用 U 盘内的
|
||||
echo OpenClaw: using the copy on the drive
|
||||
set "USE_OPENCLAW=usb"
|
||||
) else (
|
||||
echo OpenClaw: U 盘内未找到,需要在线下载
|
||||
echo OpenClaw: not on the drive, will download it
|
||||
set "USE_OPENCLAW=download"
|
||||
)
|
||||
|
||||
@@ -64,10 +64,10 @@ echo.
|
||||
|
||||
REM ---- Step 2: Check existing ----
|
||||
if exist "%INSTALL_TARGET%" (
|
||||
echo 检测到已有安装: %INSTALL_TARGET%
|
||||
set /p OVERWRITE=" 覆盖安装?(y/n): "
|
||||
echo Already installed: %INSTALL_TARGET%
|
||||
set /p OVERWRITE=" Install over it?(y/n): "
|
||||
if /i not "!OVERWRITE!"=="y" (
|
||||
echo 已取消
|
||||
echo Cancelled
|
||||
pause
|
||||
exit /b 0
|
||||
)
|
||||
@@ -75,7 +75,7 @@ if exist "%INSTALL_TARGET%" (
|
||||
)
|
||||
|
||||
REM ---- Step 3: Create directories ----
|
||||
echo [2/4] 创建安装目录...
|
||||
echo [2/4] Creating folders
|
||||
mkdir "%INSTALL_TARGET%" 2>nul
|
||||
mkdir "%INSTALL_TARGET%\data\.openclaw" 2>nul
|
||||
mkdir "%INSTALL_TARGET%\data\memory" 2>nul
|
||||
@@ -84,20 +84,20 @@ mkdir "%INSTALL_TARGET%\data\logs" 2>nul
|
||||
echo.
|
||||
|
||||
REM ---- Step 4: Copy/Download Node.js ----
|
||||
echo [3/4] 安装 Node.js...
|
||||
echo [3/4] Installing Node.js
|
||||
|
||||
if "!USE_NODE!"=="usb" (
|
||||
echo 从 U 盘复制 Node.js...
|
||||
echo Copying Node.js from the drive
|
||||
xcopy /s /e /q /y "%APP_DIR%\runtime\node-win-x64" "%INSTALL_TARGET%\runtime\node-win-x64\" >nul
|
||||
set "INSTALL_NODE=%INSTALL_TARGET%\runtime\node-win-x64\node.exe"
|
||||
set "INSTALL_NPM=%INSTALL_TARGET%\runtime\node-win-x64\npm.cmd"
|
||||
echo Node.js 安装完成!
|
||||
echo Node.js Installed!
|
||||
) else if "!USE_NODE!"=="system" (
|
||||
set "INSTALL_NODE=node"
|
||||
set "INSTALL_NPM=npm"
|
||||
echo 使用系统 Node.js
|
||||
echo Using the system Node.js
|
||||
) else (
|
||||
echo 从国内镜像下载 Node.js %NODE_VER%...
|
||||
echo Downloading Node.js %NODE_VER%
|
||||
set "NODE_ZIP=node-%NODE_VER%-win-x64.zip"
|
||||
set "NODE_URL=%NODE_MIRROR%/%NODE_VER%/!NODE_ZIP!"
|
||||
echo URL: !NODE_URL!
|
||||
@@ -106,13 +106,13 @@ if "!USE_NODE!"=="usb" (
|
||||
REM Download using curl (available on Windows 10+)
|
||||
curl -# -L "!NODE_URL!" -o "%TEMP%\!NODE_ZIP!"
|
||||
if !errorlevel! neq 0 (
|
||||
echo [ERROR] 下载失败!请检查网络连接
|
||||
echo 也可以手动下载: !NODE_URL!
|
||||
echo [ERROR] Download failed. Check your network connection.
|
||||
echo You can also download it yourself: !NODE_URL!
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo 解压中...
|
||||
echo Unpacking
|
||||
mkdir "%INSTALL_TARGET%\runtime\node-win-x64" 2>nul
|
||||
powershell -command "Expand-Archive -Path '%TEMP%\!NODE_ZIP!' -DestinationPath '%TEMP%\node-extract' -Force"
|
||||
xcopy /s /e /q /y "%TEMP%\node-extract\node-%NODE_VER%-win-x64\*" "%INSTALL_TARGET%\runtime\node-win-x64\" >nul
|
||||
@@ -121,31 +121,31 @@ if "!USE_NODE!"=="usb" (
|
||||
|
||||
set "INSTALL_NODE=%INSTALL_TARGET%\runtime\node-win-x64\node.exe"
|
||||
set "INSTALL_NPM=%INSTALL_TARGET%\runtime\node-win-x64\npm.cmd"
|
||||
echo Node.js 下载安装完成!
|
||||
echo Node.js installed
|
||||
)
|
||||
echo.
|
||||
|
||||
REM ---- Step 5: Copy/Download OpenClaw ----
|
||||
echo [4/4] 安装 OpenClaw...
|
||||
echo [4/4] Installing OpenClaw
|
||||
|
||||
if "!USE_OPENCLAW!"=="usb" (
|
||||
echo 从 U 盘复制 OpenClaw + 插件...
|
||||
echo Copying OpenClaw and its plugins from the drive
|
||||
xcopy /s /e /q /y "%APP_DIR%\core" "%INSTALL_TARGET%\core\" >nul
|
||||
echo OpenClaw 安装完成!
|
||||
echo OpenClaw Installed!
|
||||
) else (
|
||||
echo 从国内镜像下载 OpenClaw...
|
||||
echo Downloading OpenClaw
|
||||
mkdir "%INSTALL_TARGET%\core" 2>nul
|
||||
(echo {"name":"u-claw-core","version":"1.0.0","private":true,"dependencies":{"openclaw":"latest"}})>"%INSTALL_TARGET%\core\package.json"
|
||||
cd /d "%INSTALL_TARGET%\core"
|
||||
call "!INSTALL_NPM!" install --registry=%MIRROR%
|
||||
call "!INSTALL_NPM!" install @sliverp/qqbot@latest --registry=%MIRROR%
|
||||
echo OpenClaw 下载安装完成!
|
||||
echo OpenClaw installed
|
||||
)
|
||||
|
||||
set "QQ_DIR=%INSTALL_TARGET%\core\node_modules\@sliverp\qqbot"
|
||||
if exist "!QQ_DIR!" (
|
||||
if not exist "!QQ_DIR!\dist\index.js" (
|
||||
echo 编译 QQbot 插件运行文件...
|
||||
echo Building the QQ plugin
|
||||
pushd "!QQ_DIR!"
|
||||
call "!INSTALL_NPM!" install --include=dev --registry=%MIRROR% >nul 2>&1
|
||||
call "!INSTALL_NPM!" run build >nul 2>&1
|
||||
@@ -154,9 +154,9 @@ if exist "!QQ_DIR!" (
|
||||
)
|
||||
if exist "!QQ_DIR!\node_modules\openclaw" rmdir /s /q "!QQ_DIR!\node_modules\openclaw" 2>nul
|
||||
if exist "!QQ_DIR!\dist\index.js" (
|
||||
echo QQbot 插件运行文件已就绪!
|
||||
echo QQ plugin ready
|
||||
) else (
|
||||
echo [WARNING] QQbot 插件缺少 dist\index.js
|
||||
echo [WARNING] The QQ plugin is missing dist\index.js
|
||||
)
|
||||
)
|
||||
|
||||
@@ -212,18 +212,18 @@ echo pause
|
||||
|
||||
echo.
|
||||
echo ========================================
|
||||
echo 安装成功!
|
||||
echo Installed!
|
||||
echo ========================================
|
||||
echo.
|
||||
echo 安装位置: %INSTALL_TARGET%
|
||||
echo Location: %INSTALL_TARGET%
|
||||
echo.
|
||||
for /f "tokens=*" %%s in ('powershell -command "(Get-ChildItem '%INSTALL_TARGET%' -Recurse -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum).Sum / 1MB" 2^>nul') do echo 大小: %%s MB
|
||||
for /f "tokens=*" %%s in ('powershell -command "(Get-ChildItem '%INSTALL_TARGET%' -Recurse -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum).Sum / 1MB" 2^>nul') do echo Size: %%s MB
|
||||
echo.
|
||||
echo 启动方式:
|
||||
echo 双击 %INSTALL_TARGET%\start.bat
|
||||
echo To start it:
|
||||
echo Double-click %INSTALL_TARGET%\start.bat
|
||||
echo.
|
||||
echo 首次使用:
|
||||
echo 启动后浏览器自动打开配置页面
|
||||
echo 选择 AI 模型 - 填写 API Key - 开始用
|
||||
echo First time:
|
||||
echo Settings opens in your browser by itself
|
||||
echo Paste an API key and you are ready
|
||||
echo.
|
||||
pause
|
||||
@@ -3,13 +3,13 @@ chcp 65001 >nul 2>&1
|
||||
title U-Claw - Intranet Check
|
||||
setlocal
|
||||
|
||||
set "UCLAW_DIR=%~dp0"
|
||||
for %%I in ("%~dp0..") do set "UCLAW_DIR=%%~fI\"
|
||||
set "NODE_BIN=%UCLAW_DIR%app\runtime\node-win-x64\node.exe"
|
||||
set "CONFIG_PATH=%UCLAW_DIR%data\.openclaw\openclaw.json"
|
||||
|
||||
if not exist "%NODE_BIN%" (
|
||||
echo [ERROR] Node runtime not found.
|
||||
echo Put this file inside your U-Claw folder, next to Windows-Start.bat, then run again.
|
||||
echo Put this file inside your U-Claw folder, next to the START HERE files, then run again.
|
||||
echo.
|
||||
pause
|
||||
exit /b 1
|
||||
@@ -3,13 +3,13 @@ chcp 65001 >nul 2>&1
|
||||
title U-Claw - Local / Intranet Model Setup
|
||||
setlocal
|
||||
|
||||
set "UCLAW_DIR=%~dp0"
|
||||
for %%I in ("%~dp0..") do set "UCLAW_DIR=%%~fI\"
|
||||
set "NODE_BIN=%UCLAW_DIR%app\runtime\node-win-x64\node.exe"
|
||||
set "CONFIG_PATH=%UCLAW_DIR%data\.openclaw\openclaw.json"
|
||||
|
||||
if not exist "%NODE_BIN%" (
|
||||
echo [ERROR] Node runtime not found.
|
||||
echo Put this file inside your U-Claw folder, next to Windows-Start.bat, then run again.
|
||||
echo Put this file inside your U-Claw folder, next to the START HERE files, then run again.
|
||||
echo.
|
||||
pause
|
||||
exit /b 1
|
||||
@@ -3,7 +3,7 @@ setlocal EnableDelayedExpansion
|
||||
chcp 65001 >nul 2>&1
|
||||
title U-Claw Menu
|
||||
|
||||
set "UCLAW_DIR=%~dp0"
|
||||
for %%I in ("%~dp0..") do set "UCLAW_DIR=%%~fI\"
|
||||
set "APP_DIR=%UCLAW_DIR%app"
|
||||
|
||||
REM Migration shim: rename old core-win to core for existing USB users
|
||||
@@ -474,7 +474,7 @@ for /f "tokens=*" %%v in ('"%NODE_BIN%" -e "console.log(require('%CORE_DIR:\=/%/
|
||||
echo Current version: %CUR_VER%
|
||||
|
||||
echo Fetching latest version...
|
||||
for /f "tokens=*" %%v in ('"%NODE_BIN%" -e "const https=require('https');https.get('https://registry.npmmirror.com/openclaw/latest',r=>{let d='';r.on('data',c=>d+=c);r.on('end',()=>{try{console.log(JSON.parse(d).version)}catch(e){console.log('error')}})})" 2^>nul') do set LATEST_VER=%%v
|
||||
for /f "tokens=*" %%v in ('"%NODE_BIN%" -e "const https=require('https');https.get('https://registry.npmjs.org/openclaw/latest',r=>{let d='';r.on('data',c=>d+=c);r.on('end',()=>{try{console.log(JSON.parse(d).version)}catch(e){console.log('error')}})})" 2^>nul') do set LATEST_VER=%%v
|
||||
|
||||
if "!LATEST_VER!"=="" (
|
||||
echo Could not fetch latest version ^(network issue?^)
|
||||
@@ -507,7 +507,7 @@ if /i not "!doupdate!"=="y" (
|
||||
echo.
|
||||
echo Updating...
|
||||
cd /d "%CORE_DIR%"
|
||||
call "%NPM_BIN%" install openclaw@latest --registry=https://registry.npmmirror.com
|
||||
call "%NPM_BIN%" install openclaw@latest --registry=https://registry.npmjs.org
|
||||
for /f "tokens=*" %%v in ('"%NODE_BIN%" -e "console.log(require('./node_modules/openclaw/package.json').version)"') do set NEW_VER=%%v
|
||||
echo.
|
||||
echo Updated! %CUR_VER% - %NEW_VER%
|
||||
@@ -3,12 +3,12 @@ setlocal EnableDelayedExpansion
|
||||
chcp 65001 >nul 2>&1
|
||||
title U-Claw Portable Setup
|
||||
|
||||
set "SCRIPT_DIR=%~dp0"
|
||||
for %%I in ("%~dp0..") do set "SCRIPT_DIR=%%~fI\"
|
||||
set "APP_DIR=%SCRIPT_DIR%app"
|
||||
set "CORE_DIR=%APP_DIR%\core"
|
||||
set "RUNTIME_DIR=%APP_DIR%\runtime"
|
||||
set "MIRROR=https://registry.npmmirror.com"
|
||||
set "NODE_MIRROR=https://npmmirror.com/mirrors/node"
|
||||
set "MIRROR=https://registry.npmjs.org"
|
||||
set "NODE_MIRROR=https://nodejs.org/dist"
|
||||
set "NODE_VERSION=v22.22.1"
|
||||
set "ALL_PLATFORMS=false"
|
||||
if "%~1"=="--all-platforms" set "ALL_PLATFORMS=true"
|
||||
@@ -19,7 +19,7 @@ echo U-Claw Portable Setup
|
||||
echo ========================================
|
||||
echo.
|
||||
|
||||
echo 系统: Windows x64
|
||||
echo System: Windows x64
|
||||
echo.
|
||||
|
||||
REM ---- 1. Download Node.js (Current Platform - Windows) ----
|
||||
@@ -165,25 +165,16 @@ if exist "%QQ_DIR%\dist\index.js" (
|
||||
)
|
||||
:qq_build_done
|
||||
|
||||
REM ---- 4. Install China-optimized skills ----
|
||||
set "SKILLS_CN=%SCRIPT_DIR%skills-cn"
|
||||
REM ---- 4. Install skills from the manifest ----
|
||||
REM Content lives in ..\skills (see skills\README.md); this script carries none.
|
||||
set "SKILLS_TARGET=%CORE_DIR%\node_modules\openclaw\skills"
|
||||
set "SKILL_INSTALLER=%SCRIPT_DIR%lib\install-skills.mjs"
|
||||
if not exist "%SKILL_INSTALLER%" set "SKILL_INSTALLER=%SCRIPT_DIR%..\lib\install-skills.mjs"
|
||||
|
||||
if not exist "%SKILLS_CN%" goto skip_skills_install
|
||||
if not exist "%SKILLS_TARGET%" goto skip_skills_install
|
||||
|
||||
echo [COPY] Installing China-optimized skills (skills-cn)...
|
||||
set "SKILL_COUNT=0"
|
||||
for /d %%s in ("%SKILLS_CN%\*") do (
|
||||
set "skill_name=%%~nxs"
|
||||
if not exist "%SKILLS_TARGET%\!skill_name!" (
|
||||
xcopy /s /e /q /y "%%s" "%SKILLS_TARGET%\!skill_name!\" >nul
|
||||
set /a SKILL_COUNT+=1
|
||||
)
|
||||
if exist "%SKILL_INSTALLER%" if exist "%SKILLS_TARGET%" (
|
||||
echo [SKILLS] Installing skills...
|
||||
"%NODE_TARGET%\node.exe" "%SKILL_INSTALLER%" --target "%SKILLS_TARGET%" --locale en
|
||||
)
|
||||
echo [OK] China skills installed (+%SKILL_COUNT% skills)
|
||||
|
||||
:skip_skills_install
|
||||
|
||||
REM ---- Done ----
|
||||
echo.
|
||||
@@ -192,8 +183,8 @@ echo Setup Complete!
|
||||
echo ========================================
|
||||
echo.
|
||||
echo To start:
|
||||
echo Mac: bash Mac-Start.command
|
||||
echo Windows: Double-click Windows-Start.bat
|
||||
echo Mac: bash START HERE - Mac.command
|
||||
echo Windows: Double-click START HERE - Windows.bat
|
||||
echo.
|
||||
echo Directory structure:
|
||||
echo app\core\ - OpenClaw + dependencies
|
||||
@@ -4,13 +4,13 @@ param(
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$scriptDir = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)
|
||||
$appDir = Join-Path $scriptDir "app"
|
||||
$coreDir = Join-Path $appDir "core"
|
||||
$runtimeDir = Join-Path $appDir "runtime"
|
||||
|
||||
$mirror = "https://registry.npmmirror.com"
|
||||
$nodeMirror = "https://npmmirror.com/mirrors/node"
|
||||
$mirror = "https://registry.npmjs.org"
|
||||
$nodeMirror = "https://nodejs.org/dist"
|
||||
$nodeVersion = "v22.22.1"
|
||||
|
||||
function Write-Step {
|
||||
@@ -268,20 +268,14 @@ else {
|
||||
|
||||
Ensure-QQPluginBuild -CoreDir $coreDir -NpmCmd $npmCmd
|
||||
|
||||
$skillsCn = Join-Path $scriptDir "skills-cn"
|
||||
# Skills: content lives in ..\skills (see skills\README.md); this script carries none.
|
||||
$skillsTarget = Join-Path $coreDir "node_modules\openclaw\skills"
|
||||
$skillInstaller = Join-Path $scriptDir "lib\install-skills.mjs"
|
||||
if (-not (Test-Path $skillInstaller)) { $skillInstaller = Join-Path $scriptDir "..\lib\install-skills.mjs" }
|
||||
|
||||
if ((Test-Path -Path $skillsCn -PathType Container) -and (Test-Path -Path $skillsTarget -PathType Container)) {
|
||||
Write-Step "->" "Installing localized skills from skills-cn..." "Cyan"
|
||||
$skillCount = 0
|
||||
Get-ChildItem -Path $skillsCn -Directory | ForEach-Object {
|
||||
$targetPath = Join-Path $skillsTarget $_.Name
|
||||
if (-not (Test-Path -Path $targetPath -PathType Container)) {
|
||||
Copy-Item -Path $_.FullName -Destination $targetPath -Recurse -Force
|
||||
$skillCount++
|
||||
}
|
||||
}
|
||||
Write-Step "OK" ("Localized skills installed (+{0})." -f $skillCount) "Green"
|
||||
if ((Test-Path $skillInstaller) -and (Test-Path -Path $skillsTarget -PathType Container)) {
|
||||
Write-Step "->" "Installing skills..." "Cyan"
|
||||
& (Join-Path $windowsNodeTarget "node.exe") $skillInstaller --target $skillsTarget --locale en
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
@@ -290,8 +284,8 @@ Write-Host " Setup Complete" -ForegroundColor Green
|
||||
Write-Host "========================================" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
Write-Host " Launch:"
|
||||
Write-Host " Mac: bash Mac-Start.command" -ForegroundColor Cyan
|
||||
Write-Host " Windows: double-click Windows-Start.bat" -ForegroundColor Cyan
|
||||
Write-Host " Mac: bash START HERE - Mac.command" -ForegroundColor Cyan
|
||||
Write-Host " Windows: double-click START HERE - Windows.bat" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
Write-Host " Structure:"
|
||||
Write-Host " app/core/ -> OpenClaw and dependencies"
|
||||
@@ -2,18 +2,18 @@
|
||||
# ============================================================
|
||||
# U-Claw Portable — 开发环境搭建脚本
|
||||
# 用法: bash setup.sh
|
||||
# 作用: 下载 Node.js 运行时 + 安装 OpenClaw 到 app/ 目录
|
||||
# 作用: 下载 Node.js 运行时 + Installing OpenClaw 到 app/ 目录
|
||||
# ============================================================
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
APP_DIR="$SCRIPT_DIR/app"
|
||||
CORE_DIR="$APP_DIR/core"
|
||||
RUNTIME_DIR="$APP_DIR/runtime"
|
||||
MIRROR="https://registry.npmmirror.com"
|
||||
NODE_MIRROR="https://npmmirror.com/mirrors/node"
|
||||
NODE_VERSION="v22.22.1"
|
||||
MIRROR="https://registry.npmjs.org"
|
||||
NODE_MIRROR="https://nodejs.org/dist"
|
||||
NODE_VERSION="$(tr -d "[:space:]" < "$SCRIPT_DIR/NODE_VERSION" 2>/dev/null || tr -d "[:space:]" < "$SCRIPT_DIR/../NODE_VERSION" 2>/dev/null || echo v22.22.1)"
|
||||
ALL_PLATFORMS=false
|
||||
[ "$1" = "--all-platforms" ] && ALL_PLATFORMS=true
|
||||
|
||||
@@ -41,20 +41,20 @@ if [ "$OS" = "Darwin" ]; then
|
||||
NODE_DIR_NAME="node-mac-x64"
|
||||
fi
|
||||
else
|
||||
echo -e "${RED}请在 Mac 上运行此脚本。Windows 请用 setup.bat${NC}"
|
||||
echo -e "${RED}Run this on a Mac. On Windows, use setup.bat instead.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e " 系统: ${GREEN}$OS $ARCH${NC}"
|
||||
echo -e " System: ${GREEN}$OS $ARCH${NC}"
|
||||
echo ""
|
||||
|
||||
# ---- 1. Download Node.js (Current Platform) ----
|
||||
NODE_TARGET="$RUNTIME_DIR/$NODE_DIR_NAME"
|
||||
|
||||
if [ -f "$NODE_TARGET/bin/node" ]; then
|
||||
echo -e " ${GREEN}✓${NC} Node.js ($PLATFORM) 已存在,跳过下载"
|
||||
echo -e " ${GREEN}✓${NC} Node.js ($PLATFORM) already downloaded, skipping"
|
||||
else
|
||||
echo -e " ${CYAN}↓${NC} 下载 Node.js $NODE_VERSION ($PLATFORM)..."
|
||||
echo -e " ${CYAN}↓${NC} Downloading Node.js $NODE_VERSION ($PLATFORM)"
|
||||
mkdir -p "$NODE_TARGET"
|
||||
|
||||
NODE_URL="$NODE_MIRROR/$NODE_VERSION/node-$NODE_VERSION-$PLATFORM.tar.gz"
|
||||
@@ -63,9 +63,9 @@ else
|
||||
curl -fSL "$NODE_URL" | tar xz -C "$NODE_TARGET" --strip-components=1
|
||||
|
||||
if [ -f "$NODE_TARGET/bin/node" ]; then
|
||||
echo -e " ${GREEN}✓${NC} Node.js ($PLATFORM) 下载完成"
|
||||
echo -e " ${GREEN}✓${NC} Node.js ($PLATFORM) downloaded"
|
||||
else
|
||||
echo -e " ${RED}✗ Node.js 下载失败${NC}"
|
||||
echo -e " ${RED}✗ Node.js Download failed${NC}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
@@ -74,9 +74,9 @@ fi
|
||||
if [ "$ALL_PLATFORMS" = "true" ]; then
|
||||
WIN_NODE_TARGET="$RUNTIME_DIR/node-win-x64"
|
||||
if [ -f "$WIN_NODE_TARGET/node.exe" ]; then
|
||||
echo -e " ${GREEN}✓${NC} Node.js (win-x64) 已存在,跳过下载"
|
||||
echo -e " ${GREEN}✓${NC} Node.js (win-x64) already downloaded, skipping"
|
||||
else
|
||||
echo -e " ${CYAN}↓${NC} 下载 Node.js $NODE_VERSION (win-x64) - Windows支持..."
|
||||
echo -e " ${CYAN}↓${NC} Downloading Node.js $NODE_VERSION (win-x64) for Windows"
|
||||
mkdir -p "$WIN_NODE_TARGET"
|
||||
|
||||
WIN_NODE_URL="$NODE_MIRROR/$NODE_VERSION/node-$NODE_VERSION-win-x64.zip"
|
||||
@@ -95,18 +95,18 @@ if [ "$ALL_PLATFORMS" = "true" ]; then
|
||||
rm -f "$TMP_ZIP"
|
||||
|
||||
if [ -f "$WIN_NODE_TARGET/node.exe" ]; then
|
||||
echo -e " ${GREEN}✓${NC} Node.js (win-x64) 下载完成"
|
||||
echo -e " ${GREEN}✓${NC} Node.js (win-x64) downloaded"
|
||||
else
|
||||
echo -e " ${CYAN}⚠${NC} Windows runtime下载失败 (不影响当前平台使用)"
|
||||
echo -e " ${CYAN}⚠${NC} Windows runtime download failed — this platform still works"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---- 2. Install OpenClaw ----
|
||||
if [ -d "$CORE_DIR/node_modules/openclaw" ]; then
|
||||
echo -e " ${GREEN}✓${NC} OpenClaw 已安装,跳过"
|
||||
echo -e " ${GREEN}✓${NC} OpenClaw already installed, skipping"
|
||||
else
|
||||
echo -e " ${CYAN}↓${NC} 安装 OpenClaw..."
|
||||
echo -e " ${CYAN}↓${NC} Installing OpenClaw..."
|
||||
mkdir -p "$CORE_DIR"
|
||||
|
||||
# Init package.json if not exists (pinned OpenClaw version from OPENCLAW_VERSION)
|
||||
@@ -114,7 +114,7 @@ else
|
||||
if [ ! -f "$OPENCLAW_VERSION_FILE" ]; then
|
||||
OPENCLAW_VERSION_FILE="$(dirname "$0")/../OPENCLAW_VERSION"
|
||||
fi
|
||||
OPENCLAW_VERSION="2026.4.29"
|
||||
OPENCLAW_VERSION="2026.7.1-2"
|
||||
if [ -f "$OPENCLAW_VERSION_FILE" ]; then
|
||||
OPENCLAW_VERSION="$(tr -d '[:space:]' < "$OPENCLAW_VERSION_FILE")"
|
||||
# Copy version file into portable/ so USB users can read it without repo root
|
||||
@@ -136,55 +136,61 @@ PKGJSON
|
||||
# Install with China mirror(缓存留盘内,拔盘不留痕)
|
||||
# --ignore-scripts 必须加:openclaw 的 preinstall 脚本会调系统 `node`,但便携版
|
||||
# 用的是 app/runtime 下的 node(不在 PATH),未装 Node 的 Mac 用户会因
|
||||
# "node: command not found" 安装失败 (code 127)。与 Mac-Start.command 的 fallback 对齐。
|
||||
# "node: command not found" 安装失败 (code 127)。与 START HERE - Mac.command 的 fallback 对齐。
|
||||
NODE_BIN="$NODE_TARGET/bin/node"
|
||||
NPM_BIN="$NODE_TARGET/bin/npm"
|
||||
npm_config_cache="$APP_DIR/.npm-cache" "$NODE_BIN" "$NPM_BIN" install --prefix "$CORE_DIR" --registry="$MIRROR" --ignore-scripts --no-audit --no-fund --omit=dev
|
||||
|
||||
echo -e " ${GREEN}✓${NC} OpenClaw 安装完成"
|
||||
echo -e " ${GREEN}✓${NC} OpenClaw installed"
|
||||
fi
|
||||
|
||||
# ---- 3. Install QQ Plugin ----
|
||||
if [ -d "$CORE_DIR/node_modules/@sliverp/qqbot" ]; then
|
||||
echo -e " ${GREEN}✓${NC} QQ 插件已安装,跳过"
|
||||
echo -e " ${GREEN}✓${NC} QQ plugin already installed, skipping"
|
||||
else
|
||||
echo -e " ${CYAN}↓${NC} 安装 QQ 插件..."
|
||||
echo -e " ${CYAN}↓${NC} Installing the QQ plugin..."
|
||||
NODE_BIN="$NODE_TARGET/bin/node"
|
||||
NPM_BIN="$NODE_TARGET/bin/npm"
|
||||
npm_config_cache="$APP_DIR/.npm-cache" "$NODE_BIN" "$NPM_BIN" install @sliverp/qqbot@latest --prefix "$CORE_DIR" --registry="$MIRROR" --ignore-scripts --no-audit --no-fund --omit=dev 2>/dev/null || true
|
||||
echo -e " ${GREEN}✓${NC} QQ 插件安装完成"
|
||||
echo -e " ${GREEN}✓${NC} QQ plugin installed"
|
||||
fi
|
||||
|
||||
# ---- 4. Install China-optimized skills ----
|
||||
SKILLS_CN="$SCRIPT_DIR/skills-cn"
|
||||
SKILLS_TARGET="$CORE_DIR/node_modules/openclaw/skills"
|
||||
# ---- 3b. Stage what the drive needs to stand alone ----
|
||||
# release.yml puts these on the drive; a locally built drive has to match, or the
|
||||
# first-run wizard cannot install skills and the drive cannot switch locale offline.
|
||||
for VERSION_FILE in OPENCLAW_VERSION NODE_VERSION; do
|
||||
[ -f "$SCRIPT_DIR/$VERSION_FILE" ] || cp "$SCRIPT_DIR/../$VERSION_FILE" "$SCRIPT_DIR/$VERSION_FILE" 2>/dev/null || true
|
||||
done
|
||||
if [ -d "$SCRIPT_DIR/../skills" ] && [ ! -d "$SCRIPT_DIR/skills" ]; then
|
||||
cp -R "$SCRIPT_DIR/../skills" "$SCRIPT_DIR/skills"
|
||||
fi
|
||||
mkdir -p "$SCRIPT_DIR/lib"
|
||||
[ -f "$SCRIPT_DIR/lib/install-skills.mjs" ] || cp "$SCRIPT_DIR/../lib/install-skills.mjs" "$SCRIPT_DIR/lib/install-skills.mjs" 2>/dev/null || true
|
||||
|
||||
if [ -d "$SKILLS_CN" ] && [ -d "$SKILLS_TARGET" ]; then
|
||||
echo -e " ${CYAN}↓${NC} 安装中国优化技能 (skills-cn)..."
|
||||
SKILL_COUNT=0
|
||||
for skill_dir in "$SKILLS_CN"/*/; do
|
||||
skill_name=$(basename "$skill_dir")
|
||||
if [ ! -d "$SKILLS_TARGET/$skill_name" ]; then
|
||||
cp -R "$skill_dir" "$SKILLS_TARGET/$skill_name"
|
||||
SKILL_COUNT=$((SKILL_COUNT + 1))
|
||||
fi
|
||||
done
|
||||
echo -e " ${GREEN}✓${NC} 中国技能安装完成 (+$SKILL_COUNT 个)"
|
||||
# ---- 4. Install skills from the manifest ----
|
||||
# Content lives in ../skills (see skills/README.md); this script carries none.
|
||||
SKILLS_TARGET="$CORE_DIR/node_modules/openclaw/skills"
|
||||
SKILL_INSTALLER="$SCRIPT_DIR/lib/install-skills.mjs"
|
||||
[ -f "$SKILL_INSTALLER" ] || SKILL_INSTALLER="$SCRIPT_DIR/../lib/install-skills.mjs"
|
||||
|
||||
if [ -f "$SKILL_INSTALLER" ] && [ -d "$SKILLS_TARGET" ]; then
|
||||
echo -e " ${CYAN}\u2193${NC} Installing skills..."
|
||||
"$NODE_TARGET/bin/node" "$SKILL_INSTALLER" --target "$SKILLS_TARGET" --locale "${UCLAW_LOCALE:-en}"
|
||||
fi
|
||||
|
||||
# ---- Done ----
|
||||
echo ""
|
||||
echo -e "${GREEN}════════════════════════════════════════${NC}"
|
||||
echo -e "${GREEN} ✅ 搭建完成!${NC}"
|
||||
echo -e "${GREEN} Done.${NC}"
|
||||
echo ""
|
||||
echo -e " 启动方式:"
|
||||
echo -e " Mac: ${CYAN}bash Mac-Start.command${NC}"
|
||||
echo -e " Windows: 双击 ${CYAN}Windows-Start.bat${NC}"
|
||||
echo -e " To start it:"
|
||||
echo -e " Mac: ${CYAN}double-click \"START HERE - Mac.command\"${NC}"
|
||||
echo -e " Windows: double-click ${CYAN}START HERE - Windows.bat${NC}"
|
||||
echo ""
|
||||
echo -e " 目录结构:"
|
||||
echo -e " app/core/ ← OpenClaw + 依赖"
|
||||
echo -e " Layout:"
|
||||
echo -e " app/core/ OpenClaw and its dependencies"
|
||||
echo -e " app/runtime/ ← Node.js $NODE_VERSION"
|
||||
echo -e " data/ ← 运行后自动生成"
|
||||
echo -e " data/ created on first run"
|
||||
echo ""
|
||||
echo -e " ${CYAN}提示: 制作跨平台 U 盘请用 bash setup.sh --all-platforms${NC}"
|
||||
echo -e " ${CYAN}Tip: use --all-platforms to prepare a drive that works on both Mac and Windows${NC}"
|
||||
echo -e "${GREEN}════════════════════════════════════════${NC}"
|
||||
@@ -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();
|
||||
|
||||
@@ -404,6 +404,133 @@ const server = http.createServer((req, res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// API: what the first-run wizard offers, straight from the manifest.
|
||||
if (req.url === '/api/personas' && req.method === 'GET') {
|
||||
try {
|
||||
const manifestPath = path.join(__dirname, '..', 'skills', 'manifest.json');
|
||||
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
||||
const shipping = manifest.skills.filter((s) => s.status === 'shipping');
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(manifest.personas.map((p) => ({
|
||||
...p,
|
||||
skills: shipping.filter((s) => (s.personas || []).includes(p.id)).map((s) => s.id),
|
||||
}))));
|
||||
} catch (err) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: err.message }));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// API: install the skills a persona needs. Runs the same installer the
|
||||
// release build and the one-line installers use, so there is one code path.
|
||||
if (req.url === '/api/install-skills' && req.method === 'POST') {
|
||||
let body = '';
|
||||
req.on('data', (chunk) => { body += chunk; if (body.length > 4096) req.destroy(); });
|
||||
req.on('end', async () => {
|
||||
const reply = (payload, status = 200) => {
|
||||
res.writeHead(status, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(payload));
|
||||
};
|
||||
let persona = 'general';
|
||||
let locale = 'en';
|
||||
try {
|
||||
const parsed = JSON.parse(body || '{}');
|
||||
if (parsed.persona) persona = String(parsed.persona);
|
||||
if (parsed.locale) locale = String(parsed.locale);
|
||||
} catch { /* fall back to the defaults */ }
|
||||
|
||||
const target = path.join(__dirname, '..', 'app', 'core', 'node_modules', 'openclaw', 'skills');
|
||||
// On the drive the installer sits in lib/ next to us; in a source checkout
|
||||
// it lives at the repo root. Same two-place lookup the setup scripts use.
|
||||
const installer = [
|
||||
path.join(__dirname, '..', 'lib', 'install-skills.mjs'),
|
||||
path.join(__dirname, '..', '..', 'lib', 'install-skills.mjs'),
|
||||
].find((candidate) => fs.existsSync(candidate));
|
||||
if (!installer) return reply({ ok: false, reason: 'installer-missing' });
|
||||
|
||||
try {
|
||||
fs.mkdirSync(target, { recursive: true });
|
||||
const { execFile } = require('child_process');
|
||||
await new Promise((resolve, reject) => {
|
||||
execFile(process.execPath,
|
||||
[installer, '--target', target, '--locale', locale, '--persona', persona, '--json'],
|
||||
{ timeout: 30000 },
|
||||
(err, stdout) => (err ? reject(err) : resolve(stdout)));
|
||||
});
|
||||
reply({ ok: true, persona });
|
||||
} catch (err) {
|
||||
// Skills are a bonus, not a gate: the wizard continues either way.
|
||||
reply({ ok: false, reason: err.message });
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// API: Identify an API key and check that it actually works.
|
||||
//
|
||||
// Runs server-side on purpose: the browser cannot call most model endpoints
|
||||
// cross-origin, and doing it here means the key never leaves this machine by
|
||||
// any path the page controls. Sends the smallest possible completion (1 token)
|
||||
// and maps the outcome to a message key the page translates -- users get
|
||||
// "that key was not accepted, here is what to do", not "401 Unauthorized".
|
||||
if (req.url === '/api/test-key' && req.method === 'POST') {
|
||||
let body = '';
|
||||
req.on('data', (chunk) => {
|
||||
body += chunk;
|
||||
// A key is short; anything large is not a key.
|
||||
if (body.length > 8192) req.destroy();
|
||||
});
|
||||
req.on('end', async () => {
|
||||
const reply = (payload, status = 200) => {
|
||||
res.writeHead(status, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(payload));
|
||||
};
|
||||
|
||||
let parsed;
|
||||
try { parsed = JSON.parse(body || '{}'); } catch { return reply({ ok: false, reason: 'key.err_unknown' }, 400); }
|
||||
|
||||
const apiKey = String(parsed.apiKey || '').trim();
|
||||
if (!apiKey) return reply({ ok: false, reason: 'key.err_empty' });
|
||||
|
||||
const { detectProvider, classifyFailure } = await import('../lib/provider-detect.mjs');
|
||||
const detected = detectProvider(apiKey);
|
||||
const baseUrl = String(parsed.baseUrl || detected?.baseUrl || '').replace(/\/+$/, '');
|
||||
const model = String(parsed.model || detected?.model || '');
|
||||
|
||||
if (!baseUrl || !model) {
|
||||
return reply({ ok: false, provider: detected, reason: 'key.err_unrecognised' });
|
||||
}
|
||||
|
||||
const startedAt = Date.now();
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), 15000);
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/chat/completions`, {
|
||||
method: 'POST',
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
// Anthropic's endpoint wants its own header; sending both is harmless.
|
||||
'x-api-key': apiKey,
|
||||
'anthropic-version': '2023-06-01',
|
||||
},
|
||||
body: JSON.stringify({ model, max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
return reply({ ok: false, provider: detected, status: response.status, reason: classifyFailure({ status: response.status }) });
|
||||
}
|
||||
return reply({ ok: true, provider: detected, baseUrl, model, latencyMs: Date.now() - startedAt });
|
||||
} catch (err) {
|
||||
return reply({ ok: false, provider: detected, reason: classifyFailure({ code: err?.cause?.code || err?.code || err?.name }) });
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// API: Get config
|
||||
if (req.url === '/api/config' && req.method === 'GET') {
|
||||
try {
|
||||
@@ -519,6 +646,25 @@ const server = http.createServer((req, res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Serve the shared i18n runtime and the drive's locale from outside public/.
|
||||
// Keeping one copy matters: the pages opened from file:// load these same
|
||||
// files by relative path, and a second copy under public/ would drift.
|
||||
if (req.url.startsWith('/lib/i18n/') || req.url === '/locale.js') {
|
||||
const target = req.url === '/locale.js'
|
||||
? path.join(__dirname, '../data/.openclaw/locale.js')
|
||||
: path.join(__dirname, '..', 'lib', 'i18n', path.basename(req.url));
|
||||
if (fs.existsSync(target) && fs.statSync(target).isFile()) {
|
||||
res.writeHead(200, { 'Content-Type': 'application/javascript; charset=utf-8' });
|
||||
res.end(fs.readFileSync(target));
|
||||
} else {
|
||||
// locale.js is written by the launcher; before the first run it is absent
|
||||
// and the page falls back to the browser language. Not an error.
|
||||
res.writeHead(204);
|
||||
res.end();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Serve static files
|
||||
const filePath = req.url === '/'
|
||||
? path.join(__dirname, 'public/index.html')
|
||||
|
||||
@@ -4,19 +4,19 @@
|
||||
// 1. 读取 OPENCLAW_VERSION 文件得到当前版本号
|
||||
// 2. 5s timeout 拉 OSS 上的 latest.json
|
||||
// 3. 比对版本号,有新版就写 update-available.json 到 STATE_DIR
|
||||
// 4. Welcome.html / Config.html 启动时读这个文件,有就显示提示条
|
||||
// 4. Read me first.html / Config.html 启动时读这个文件,有就显示提示条
|
||||
//
|
||||
// 设计原则:
|
||||
// - 静默失败:网络坏、OSS 挂、json 格式错、本地版本号缺失,都不能影响 OpenClaw 启动
|
||||
// - 只读不下载:检查到新版只写 update-available.json,下载交给用户点链接到浏览器去做
|
||||
// - 异步:调用方应该 detach 跑(Windows-Start.bat 用 start /B),不阻塞主流程
|
||||
// - 异步:调用方应该 detach 跑(START HERE - Windows.bat 用 start /B),不阻塞主流程
|
||||
//
|
||||
// latest.json 格式(你在 OSS 上手动维护或用 publish-latest.mjs 生成):
|
||||
// {
|
||||
// "version": "2026.4.30",
|
||||
// "releaseDate": "2026-05-03",
|
||||
// "downloadUrl": "https://u-claw-oss.56chat.cn/u-claw-open/u-claw-portable-v2026.4.30.zip",
|
||||
// "releasePageUrl": "https://github.com/dongsheng123132/u-claw/releases/tag/v2026.4.30",
|
||||
// "releasePageUrl": "https://gitea.fanghe.it.com/zhenghy/u-claw/releases/tag/v2026.4.30",
|
||||
// "notes": "修复 xxx,新增 yyy"
|
||||
// }
|
||||
|
||||
|
||||
81
portable/lib/i18n.mjs
Normal file
81
portable/lib/i18n.mjs
Normal file
@@ -0,0 +1,81 @@
|
||||
// Message catalogue for the launcher and the local HTML pages.
|
||||
//
|
||||
// Locale resolution follows one rule: the language travels with the drive, not
|
||||
// with the machine. Someone who set up the drive in Chinese and plugs it into a
|
||||
// colleague's English Windows should still see Chinese.
|
||||
//
|
||||
// explicit override > drive config > system locale > en
|
||||
//
|
||||
// Keys are semantic (`start.node_missing`), never the source string, so changing
|
||||
// the English wording does not invalidate every other catalogue.
|
||||
|
||||
import { readFileSync, existsSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const SUPPORTED = ['en', 'zh-CN'];
|
||||
const FALLBACK = 'en';
|
||||
|
||||
function normalise(tag) {
|
||||
if (!tag) return null;
|
||||
const lower = String(tag).toLowerCase();
|
||||
if (lower.startsWith('zh')) return 'zh-CN';
|
||||
if (lower.startsWith('en')) return 'en';
|
||||
return null;
|
||||
}
|
||||
|
||||
// Reads uclaw.locale out of the drive's own config. A malformed or missing file
|
||||
// is not an error — it just means we have no preference yet.
|
||||
function localeFromConfig(configPath) {
|
||||
if (!configPath || !existsSync(configPath)) return null;
|
||||
try {
|
||||
return normalise(JSON.parse(readFileSync(configPath, 'utf8'))?.uclaw?.locale);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function localeFromSystem() {
|
||||
const env = process.env.UCLAW_LOCALE || process.env.LC_ALL || process.env.LC_MESSAGES || process.env.LANG;
|
||||
if (env) return normalise(env.split('.')[0].replace('_', '-'));
|
||||
try {
|
||||
return normalise(new Intl.DateTimeFormat().resolvedOptions().locale);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveLocale({ override, configPath } = {}) {
|
||||
return (
|
||||
normalise(override) ||
|
||||
localeFromConfig(configPath) ||
|
||||
localeFromSystem() ||
|
||||
FALLBACK
|
||||
);
|
||||
}
|
||||
|
||||
function loadCatalogue(locale) {
|
||||
try {
|
||||
return JSON.parse(readFileSync(join(here, 'messages', `${locale}.json`), 'utf8'));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function createTranslator(locale) {
|
||||
const active = SUPPORTED.includes(locale) ? locale : FALLBACK;
|
||||
const catalogue = loadCatalogue(active) ?? {};
|
||||
// Always keep English loaded: a key missing from a translation should fall back
|
||||
// to readable English, never to a raw key leaking into the UI.
|
||||
const fallback = active === FALLBACK ? catalogue : (loadCatalogue(FALLBACK) ?? {});
|
||||
|
||||
return function t(key, vars = {}) {
|
||||
const template = catalogue[key] ?? fallback[key] ?? key;
|
||||
return template.replace(/\{(\w+)\}/g, (match, name) =>
|
||||
Object.prototype.hasOwnProperty.call(vars, name) ? String(vars[name]) : match
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export { SUPPORTED, FALLBACK };
|
||||
75
portable/lib/i18n/build-messages.mjs
Normal file
75
portable/lib/i18n/build-messages.mjs
Normal file
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env node
|
||||
// Generates lib/i18n/messages.js from lib/messages/*.json.
|
||||
//
|
||||
// The launcher (node) reads the JSON directly; the pages cannot, because they
|
||||
// are opened from file:// where fetching local JSON is blocked. Rather than
|
||||
// maintain two copies of every string, the JSON stays the source of truth and
|
||||
// this emits a plain <script> the pages can load.
|
||||
//
|
||||
// node lib/i18n/build-messages.mjs [--check]
|
||||
//
|
||||
// --check exits non-zero if the generated file is stale, for CI.
|
||||
|
||||
import { readFileSync, writeFileSync, readdirSync, existsSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const messagesDir = join(here, '..', 'messages');
|
||||
const outputPath = join(here, 'messages.js');
|
||||
const skillsOutputPath = join(here, 'skills-data.js');
|
||||
|
||||
const catalogues = {};
|
||||
for (const file of readdirSync(messagesDir).sort()) {
|
||||
if (!file.endsWith('.json')) continue;
|
||||
catalogues[file.replace(/\.json$/, '')] = JSON.parse(readFileSync(join(messagesDir, file), 'utf8'));
|
||||
}
|
||||
|
||||
const generated = `/* GENERATED FILE — do not edit.
|
||||
* Source: lib/messages/*.json
|
||||
* Regenerate: node lib/i18n/build-messages.mjs
|
||||
*
|
||||
* Exists because pages opened from file:// cannot fetch local JSON.
|
||||
*/
|
||||
window.UCLAW_I18N_MESSAGES = ${JSON.stringify(catalogues, null, 2)};
|
||||
`;
|
||||
|
||||
// The skill catalogue travels the same way and for the same reason: SkillHub is
|
||||
// opened from file://, where fetching manifest.json is blocked.
|
||||
function buildSkillsData() {
|
||||
for (const candidate of [join(here, '..', '..', 'skills'), join(here, '..', '..', '..', 'skills')]) {
|
||||
if (!existsSync(join(candidate, 'manifest.json'))) continue;
|
||||
const manifest = JSON.parse(readFileSync(join(candidate, 'manifest.json'), 'utf8'));
|
||||
return `/* GENERATED FILE — do not edit.
|
||||
* Source: skills/manifest.json
|
||||
* Regenerate: node lib/i18n/build-messages.mjs
|
||||
*/
|
||||
window.UCLAW_SKILLS = ${JSON.stringify({
|
||||
personas: manifest.personas,
|
||||
skills: manifest.skills
|
||||
.filter((s) => s.status === 'shipping')
|
||||
.map(({ id, categories, personas, emoji }) => ({ id, categories, personas, emoji })),
|
||||
}, null, 2)};
|
||||
`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const generatedSkills = buildSkillsData();
|
||||
|
||||
if (process.argv.includes('--check')) {
|
||||
let current = null;
|
||||
try { current = readFileSync(outputPath, 'utf8'); } catch { /* not generated yet */ }
|
||||
let currentSkills = null;
|
||||
try { currentSkills = readFileSync(skillsOutputPath, 'utf8'); } catch { /* not generated yet */ }
|
||||
if (current !== generated || (generatedSkills && currentSkills !== generatedSkills)) {
|
||||
process.stderr.write('lib/i18n generated files are stale — run: node lib/i18n/build-messages.mjs\n');
|
||||
process.exit(1);
|
||||
}
|
||||
process.stdout.write('generated files are up to date\n');
|
||||
} else {
|
||||
writeFileSync(outputPath, generated, 'utf8');
|
||||
if (generatedSkills) writeFileSync(skillsOutputPath, generatedSkills, 'utf8');
|
||||
const count = Object.keys(catalogues).map((l) => `${l}:${Object.keys(catalogues[l]).length}`).join(' ');
|
||||
process.stdout.write(`wrote lib/i18n/messages.js (${count})\n`);
|
||||
}
|
||||
152
portable/lib/i18n/i18n.js
Normal file
152
portable/lib/i18n/i18n.js
Normal file
@@ -0,0 +1,152 @@
|
||||
/* U-Claw browser-side i18n.
|
||||
*
|
||||
* A classic script, not an ES module, and the catalogue is a script rather than
|
||||
* JSON on purpose: these pages are opened from file:// (the launcher opens
|
||||
* loading.html with a file:// URL, Welcome.html links to its siblings), and from
|
||||
* a file:// origin both fetch() and ES-module imports of local files are blocked.
|
||||
* That is the exact bug that left the old Config.html silently broken. A plain
|
||||
* <script src> tag still works.
|
||||
*
|
||||
* Load order on a page:
|
||||
* <script src="data/.openclaw/locale.js"></script> optional, written by the launcher
|
||||
* <script src="lib/i18n/messages.js"></script>
|
||||
* <script src="lib/i18n/i18n.js"></script>
|
||||
*
|
||||
* Then mark text with data-i18n="key". Attributes use data-i18n-attr="placeholder:key".
|
||||
* The HTML keeps English as its literal fallback, so a missing catalogue degrades
|
||||
* to readable English instead of raw keys.
|
||||
*/
|
||||
(function (global) {
|
||||
'use strict';
|
||||
|
||||
var SUPPORTED = ['en', 'zh-CN'];
|
||||
var FALLBACK = 'en';
|
||||
|
||||
function normalise(tag) {
|
||||
if (!tag) return null;
|
||||
var lower = String(tag).toLowerCase();
|
||||
if (lower.indexOf('zh') === 0) return 'zh-CN';
|
||||
if (lower.indexOf('en') === 0) return 'en';
|
||||
return null;
|
||||
}
|
||||
|
||||
// The language travels with the drive, not the machine: someone who set the
|
||||
// drive up in Chinese and plugs it into a colleague's English Windows should
|
||||
// still see Chinese. The launcher writes data/.openclaw/locale.js on startup;
|
||||
// ?lang= is the manual override, and the browser language is the last resort.
|
||||
function resolveLocale() {
|
||||
var fromQuery = null;
|
||||
try {
|
||||
fromQuery = new URLSearchParams(global.location.search).get('lang');
|
||||
} catch (e) { /* older browser or opaque URL */ }
|
||||
|
||||
return normalise(fromQuery)
|
||||
|| normalise(global.UCLAW_LOCALE)
|
||||
|| normalise(global.navigator && global.navigator.language)
|
||||
|| FALLBACK;
|
||||
}
|
||||
|
||||
var locale = resolveLocale();
|
||||
var catalogues = global.UCLAW_I18N_MESSAGES || {};
|
||||
var active = catalogues[SUPPORTED.indexOf(locale) === -1 ? FALLBACK : locale] || {};
|
||||
var fallback = catalogues[FALLBACK] || {};
|
||||
|
||||
function t(key, vars) {
|
||||
var template = active[key];
|
||||
if (template === undefined) template = fallback[key];
|
||||
if (template === undefined) return key;
|
||||
if (!vars) return template;
|
||||
return template.replace(/\{(\w+)\}/g, function (match, name) {
|
||||
return Object.prototype.hasOwnProperty.call(vars, name) ? String(vars[name]) : match;
|
||||
});
|
||||
}
|
||||
|
||||
function apply(root) {
|
||||
var scope = root || global.document;
|
||||
|
||||
scope.querySelectorAll('[data-i18n]').forEach(function (node) {
|
||||
var value = t(node.getAttribute('data-i18n'));
|
||||
// Keep the literal in the HTML when a key has no translation, rather than
|
||||
// replacing readable English with the key name.
|
||||
if (value !== node.getAttribute('data-i18n')) node.textContent = value;
|
||||
});
|
||||
|
||||
scope.querySelectorAll('[data-i18n-attr]').forEach(function (node) {
|
||||
node.getAttribute('data-i18n-attr').split(',').forEach(function (pair) {
|
||||
var parts = pair.split(':');
|
||||
if (parts.length !== 2) return;
|
||||
var attr = parts[0].trim();
|
||||
var value = t(parts[1].trim());
|
||||
if (value !== parts[1].trim()) node.setAttribute(attr, value);
|
||||
});
|
||||
});
|
||||
|
||||
global.document.documentElement.lang = locale;
|
||||
}
|
||||
|
||||
// Region defaults for Singapore. The old pages assumed China: Asia/Shanghai,
|
||||
// YYYY-MM-DD, 24-hour, ¥. SG uses DD/MM/YYYY (neither Chinese nor American),
|
||||
// 12-hour with am/pm, and S$.
|
||||
var REGION = {
|
||||
en: { locale: 'en-SG', timeZone: 'Asia/Singapore', currency: 'SGD', hour12: true },
|
||||
'zh-CN': { locale: 'zh-SG', timeZone: 'Asia/Singapore', currency: 'SGD', hour12: true },
|
||||
};
|
||||
|
||||
function region() {
|
||||
return REGION[locale] || REGION.en;
|
||||
}
|
||||
|
||||
function formatDate(date) {
|
||||
var r = region();
|
||||
try {
|
||||
return new Intl.DateTimeFormat(r.locale, {
|
||||
timeZone: r.timeZone, day: '2-digit', month: '2-digit', year: 'numeric',
|
||||
}).format(date);
|
||||
} catch (e) {
|
||||
return String(date);
|
||||
}
|
||||
}
|
||||
|
||||
function formatTime(date) {
|
||||
var r = region();
|
||||
try {
|
||||
return new Intl.DateTimeFormat(r.locale, {
|
||||
timeZone: r.timeZone, hour: 'numeric', minute: '2-digit', hour12: r.hour12,
|
||||
}).format(date);
|
||||
} catch (e) {
|
||||
return String(date);
|
||||
}
|
||||
}
|
||||
|
||||
// ICU renders SGD as a bare "$" in an en-SG locale, which is ambiguous next to
|
||||
// USD prices. Prices are written S$ throughout the product, so format the
|
||||
// number with Intl and put the symbol on ourselves rather than depending on
|
||||
// whichever ICU build the browser or Node happens to ship.
|
||||
function formatMoney(amount) {
|
||||
var r = region();
|
||||
try {
|
||||
return 'S$' + new Intl.NumberFormat(r.locale, {
|
||||
minimumFractionDigits: 2, maximumFractionDigits: 2,
|
||||
}).format(amount);
|
||||
} catch (e) {
|
||||
return 'S$' + amount;
|
||||
}
|
||||
}
|
||||
|
||||
global.UClawI18n = {
|
||||
locale: locale,
|
||||
supported: SUPPORTED,
|
||||
t: t,
|
||||
apply: apply,
|
||||
region: region,
|
||||
formatDate: formatDate,
|
||||
formatTime: formatTime,
|
||||
formatMoney: formatMoney,
|
||||
};
|
||||
|
||||
if (global.document.readyState === 'loading') {
|
||||
global.document.addEventListener('DOMContentLoaded', function () { apply(); });
|
||||
} else {
|
||||
apply();
|
||||
}
|
||||
})(window);
|
||||
566
portable/lib/i18n/messages.js
Normal file
566
portable/lib/i18n/messages.js
Normal file
@@ -0,0 +1,566 @@
|
||||
/* GENERATED FILE — do not edit.
|
||||
* Source: lib/messages/*.json
|
||||
* Regenerate: node lib/i18n/build-messages.mjs
|
||||
*
|
||||
* Exists because pages opened from file:// cannot fetch local JSON.
|
||||
*/
|
||||
window.UCLAW_I18N_MESSAGES = {
|
||||
"en": {
|
||||
"start.banner": "U-Claw · Portable AI Agent",
|
||||
"start.node_version": "Node.js {version}",
|
||||
"start.quarantine_removing": "Clearing the macOS security flag…",
|
||||
"start.cache_local": "Cache on local disk: {path}",
|
||||
"start.config_migrating": "Bringing your old settings across…",
|
||||
"start.config_migrated": "Settings moved over.",
|
||||
"start.config_creating": "First run — setting things up…",
|
||||
"start.config_created": "Ready.",
|
||||
"start.deps_preparing": "Getting things ready — about 20 seconds.",
|
||||
"start.deps_slow_drive": "This drive is slow, so it may take a few minutes. You can leave it running.",
|
||||
"start.deps_done": "Ready.",
|
||||
"start.no_proxy": "Connecting directly to your model host: {hosts}",
|
||||
"start.wechat_installing": "Installing the WeChat plugin…",
|
||||
"start.wechat_installed": "WeChat plugin installed.",
|
||||
"start.config_center_starting": "Starting the Control Panel…",
|
||||
"start.config_center_port": "Control Panel is on port {port}.",
|
||||
"start.port_in_use": "Port {port} is busy, trying the next one…",
|
||||
"start.gateway_starting": "Starting U-Claw on port {port}…",
|
||||
"start.opening_screen": "Opening the start screen…",
|
||||
"start.opening_config": "Opening the Control Panel…",
|
||||
"start.running_title": "U-Claw is running",
|
||||
"start.running_dashboard": "Dashboard: {url}",
|
||||
"start.running_config": "Control Panel: {url}",
|
||||
"start.running_hint": "Keep this window open while you use U-Claw. Press Ctrl+C to stop.",
|
||||
"start.first_run_wait": "First run from a USB drive takes 30–90 seconds while components unpack. The Control Panel is already open so you can pick a model and paste a key.",
|
||||
"start.stopped": "U-Claw stopped.",
|
||||
"start.exited_unexpectedly": "U-Claw closed on its own (code {code}).",
|
||||
"error.node_missing.what": "A file U-Claw needs is missing from the drive.",
|
||||
"error.node_missing.why": "This usually means the copy onto the USB drive did not finish.",
|
||||
"error.node_missing.action": "Copy the U-Claw folder onto the drive again, or run setup.sh to rebuild it.",
|
||||
"error.unsupported_arch.what": "This Mac's processor is not supported ({arch}).",
|
||||
"error.unsupported_arch.why": "U-Claw ships builds for Apple Silicon and Intel Macs only.",
|
||||
"error.unsupported_arch.action": "Try U-Claw on a different computer.",
|
||||
"error.no_port.what": "U-Claw could not find a free port to start on.",
|
||||
"error.no_port.why": "Ports {from}–{to} are all in use — U-Claw may already be running.",
|
||||
"error.no_port.action": "Close any window already running U-Claw and start it again.",
|
||||
"error.deps_failed.what": "U-Claw could not finish getting ready.",
|
||||
"error.deps_failed.why": "The download did not complete — usually a dropped network connection.",
|
||||
"error.deps_failed.action": "Check your Wi-Fi and start U-Claw again.",
|
||||
"error.press_enter": "Press Enter to close this window.",
|
||||
"loading.title": "Starting U-Claw",
|
||||
"loading.starting": "Starting U-Claw…",
|
||||
"loading.first_run": "First run from a USB drive unpacks the bundled components. Give it a moment — you will be told when it is ready.",
|
||||
"loading.waiting_gateway": "Waiting for U-Claw to come up…",
|
||||
"loading.ready": "U-Claw is ready",
|
||||
"loading.choose_next": "Pick what to do next — start chatting, or set up your model and channels first.",
|
||||
"loading.open_dashboard": "🚀 Start chatting",
|
||||
"loading.open_settings": "⚙️ Settings",
|
||||
"loading.settings_hint": "Add or change your API key, and connect chat apps.",
|
||||
"loading.no_model_hint": "No model set up yet? Open Settings first to add a key.",
|
||||
"loading.slow_drive_note": "A slow drive usually takes 30–90 seconds. If nothing happens for a long time, open Settings:",
|
||||
"loading.elapsed": "Waited {seconds}s",
|
||||
"loading.taking_long": "This is taking longer than usual. You can open Settings while you wait:",
|
||||
"welcome.title": "U-Claw — Getting Started",
|
||||
"welcome.subtitle": "Your AI assistant on a USB drive · three steps",
|
||||
"welcome.quickstart": "Quick start",
|
||||
"welcome.step1_title": "1. Start U-Claw",
|
||||
"welcome.step1_desc": "Double-click the file for your computer:",
|
||||
"welcome.step2_title": "2. Add a key",
|
||||
"welcome.step2_desc": "Settings opens on its own the first time. You need one API key to get going — Google Gemini is the easiest to obtain: sign in with a Google account, no card required.",
|
||||
"welcome.step2_link": "Get a free Gemini key →",
|
||||
"welcome.step3_title": "3. Start talking",
|
||||
"welcome.step3_desc": "Once the key is saved, the chat window opens by itself. That is it.",
|
||||
"welcome.portable_note": "✅ Your settings and history stay in the data/ folder on the drive, so they travel with you between Mac and Windows.",
|
||||
"welcome.models_title": "Which model?",
|
||||
"welcome.model_claude": "Best all-rounder",
|
||||
"welcome.model_gpt": "Most widely used",
|
||||
"welcome.model_gemini": "Free tier, easiest signup",
|
||||
"welcome.model_local": "Offline · nothing leaves this machine",
|
||||
"welcome.models_more": "More options, including SEA-LION for Malay, Tamil and Singlish, are in Settings.",
|
||||
"welcome.trouble_title": "Something not working?",
|
||||
"welcome.trouble_lead": "💡 Run the check-up tool",
|
||||
"welcome.trouble_desc": "If U-Claw will not start, double-click:",
|
||||
"welcome.open_settings": "Open Settings",
|
||||
"welcome.open_docs": "Documentation",
|
||||
"nav.title": "U-Claw — Start here",
|
||||
"nav.tagline": "Portable AI agent — plug in and go",
|
||||
"nav.status_off": "U-Claw is not running",
|
||||
"nav.open_console": "Open it →",
|
||||
"nav.quickstart": "Quick start",
|
||||
"nav.doubleclick": "Double-click",
|
||||
"nav.step2": "Settings opens by itself the first time — pick a model and paste your key.",
|
||||
"nav.mac_gatekeeper": "⚠️ If macOS says the developer cannot be verified: right-click the file, then choose Open.",
|
||||
"nav.config_shortcut": "📝 You can also double-click Config.html — it takes you to the settings page.",
|
||||
"nav.win_smartscreen": "⚠️ If Windows shows a security warning: click More info, then Run anyway.",
|
||||
"nav.everything": "Everything else",
|
||||
"nav.menu_opens": "for the full menu:",
|
||||
"nav.g_settings": "Settings",
|
||||
"nav.g_settings_d": "Model, key and chat apps",
|
||||
"nav.g_checkup": "Check-up",
|
||||
"nav.g_checkup_d": "Finds and fixes common problems",
|
||||
"nav.g_backup": "Backup",
|
||||
"nav.g_backup_d": "Save settings and memory in one click",
|
||||
"nav.g_skills": "Skills",
|
||||
"nav.g_skills_d": "Browse what U-Claw can do",
|
||||
"nav.g_system": "System info",
|
||||
"nav.g_system_d": "Version, port, running state",
|
||||
"nav.g_cli": "Command line",
|
||||
"nav.g_cli_d": "For advanced use",
|
||||
"nav.models_title": "Models you can use",
|
||||
"nav.model_sealion": "Malay, Tamil and Singlish",
|
||||
"nav.model_local": "Local model",
|
||||
"nav.model_more": "More",
|
||||
"nav.model_more_d": "Others are listed in Settings",
|
||||
"nav.more_title": "More",
|
||||
"nav.link_guide": "Getting started",
|
||||
"nav.link_guide_d": "The three-step walkthrough",
|
||||
"nav.link_skills": "Skills",
|
||||
"nav.link_skills_d": "Browse what U-Claw can do",
|
||||
"nav.link_contact": "Contact",
|
||||
"nav.link_site": "Website",
|
||||
"nav.status_on": "U-Claw is running on port {port}",
|
||||
"nav.status_off_hint": "U-Claw is not running — double-click the start file first",
|
||||
"key.page_title": "U-Claw Settings",
|
||||
"key.page_subtitle": "Paste an API key and you are done.",
|
||||
"key.checking": "Checking…",
|
||||
"key.step_key": "Your key",
|
||||
"key.step_done": "Done",
|
||||
"key.heading": "Paste your API key",
|
||||
"key.desc": "U-Claw works out the rest by itself. Your key is saved on this drive only — it is never uploaded anywhere.",
|
||||
"key.placeholder": "sk-…",
|
||||
"key.show": "Show",
|
||||
"key.hide": "Hide",
|
||||
"key.continue": "Continue →",
|
||||
"key.checking_key": "Checking this key…",
|
||||
"key.ok": "✓ {provider} · connected in {ms}ms",
|
||||
"key.custom_provider": "Your endpoint",
|
||||
"key.no_key_title": "Do not have a key yet?",
|
||||
"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.",
|
||||
"key.get_gemini": "🔑 Get a free Gemini key",
|
||||
"key.get_openrouter": "🔑 OpenRouter — one key for many models",
|
||||
"key.advanced": "Advanced — set the endpoint manually",
|
||||
"key.base_url": "API endpoint",
|
||||
"key.model_name": "Model name",
|
||||
"key.advanced_hint": "Only needed for a self-hosted or unlisted provider. Leave blank to let U-Claw decide.",
|
||||
"key.saved": "Saved.",
|
||||
"key.err_empty": "Paste a key to continue.",
|
||||
"key.err_not_checked": "Hold on — still checking that key.",
|
||||
"key.err_rejected": "That key was not accepted. Check you copied all of it — keys are long and easy to cut short.",
|
||||
"key.err_quota": "This key has run out of credit. Top it up with your provider, or paste a different key.",
|
||||
"key.err_offline": "Cannot reach the internet. Check your Wi-Fi and try again.",
|
||||
"key.err_timeout": "The provider did not answer in time. Try again in a moment.",
|
||||
"key.err_model": "That key works, but the model it points to was not found. Set the model name under Advanced.",
|
||||
"key.err_provider_down": "The provider is having trouble right now. Try again shortly.",
|
||||
"key.err_unrecognised": "This key is not one U-Claw recognises. Fill in the endpoint and model under Advanced.",
|
||||
"key.err_unknown": "Could not check that key. Try again, or use Advanced to set the endpoint yourself.",
|
||||
"key.err_save": "Could not save. Make sure the drive is still plugged in, then try again.",
|
||||
"hub.title": "U-Claw Skills",
|
||||
"done.title": "You are set up",
|
||||
"done.subtitle": "Your model and key are saved on this drive.",
|
||||
"done.open": "🚀 Start chatting",
|
||||
"done.restart_note": "Chat app settings are saved now and take effect after U-Claw restarts.",
|
||||
"done.change_key": "Change model or key",
|
||||
"done.view_config": "View config file",
|
||||
"ch.title": "📱 Connect a chat app (optional)",
|
||||
"ch.desc": "Click a card to fill it in, or skip this entirely.",
|
||||
"ch.whatsapp_d": "Talk to U-Claw from WhatsApp",
|
||||
"ch.whatsapp_warning": "⚠️ This uses an unofficial protocol. Your WhatsApp account can be banned for it. Do not connect a number you rely on.",
|
||||
"ch.whatsapp_number": "Phone number",
|
||||
"ch.telegram_d": "Talk to U-Claw from Telegram",
|
||||
"ch.telegram_get": "→ Get one from @BotFather",
|
||||
"ch.slack_d": "Talk to U-Claw from your workspace",
|
||||
"ch.slack_get": "→ Create an app at api.slack.com",
|
||||
"ch.discord_d": "Talk to U-Claw from Discord",
|
||||
"ch.discord_get": "→ Create an app in the Discord developer portal",
|
||||
"ch.china_group": "Chat apps used mainly in China",
|
||||
"ch.wechat_d": "Scan a QR code to link a personal WeChat account",
|
||||
"ch.wechat_start": "Click this card to show the QR code",
|
||||
"ch.saved": "Chat app settings saved.",
|
||||
"ch.err_save": "Could not save the chat app settings. Check the drive is still plugged in.",
|
||||
"info.layout_title": "What is on the drive",
|
||||
"info.layout_desc": "How the folders are organised",
|
||||
"info.skills": "Skills",
|
||||
"info.skills_d": "Drop a SKILL.md in here and it is picked up automatically",
|
||||
"info.app": "Runtime",
|
||||
"info.app_d": "Node.js and the OpenClaw core",
|
||||
"info.data": "Your data",
|
||||
"info.data_d": "Your key and chat history. Stays on this drive.",
|
||||
"info.feedback_title": "Found a problem?",
|
||||
"info.feedback_desc": "U-Claw is open source. Report anything that breaks on GitHub — nothing is collected or uploaded from your machine automatically.",
|
||||
"info.feedback_link": "→ Open an issue on GitHub",
|
||||
"wx.getting_qr": "Fetching the QR code…",
|
||||
"wx.scan": "Scan the code above with WeChat",
|
||||
"wx.qr_failed": "Could not fetch the QR code",
|
||||
"wx.connected": "✅ WeChat connected",
|
||||
"wx.account": "Account",
|
||||
"wx.restart_needed": "One more thing: restart U-Claw (close it and open it again) before WeChat will work.",
|
||||
"wx.connected_toast": "WeChat connected. It works after U-Claw restarts.",
|
||||
"wx.qr_refreshed": "The code refreshed — scan it again",
|
||||
"wx.confirm_on_phone": "Scanned. Confirm on your phone…",
|
||||
"wx.qr_expired": "That code expired.",
|
||||
"wx.qr_retry": "Get a new one",
|
||||
"wx.failed": "Could not connect",
|
||||
"info.read_config_failed": "Could not read the config file:",
|
||||
"status.running": "U-Claw is running on port {port}",
|
||||
"status.open_dashboard": "Open it →",
|
||||
"status.not_running": "U-Claw is not running — start it with Windows-Start.bat or Mac-Start.command",
|
||||
"key.which_model": "Which model?",
|
||||
"start.opening_language": "Asking which language to use…",
|
||||
"persona.title": "What will you mostly use this for?",
|
||||
"persona.subtitle": "This sets up the right tools for you. You can change it later, and picking more than one is fine.",
|
||||
"persona.continue": "Continue →",
|
||||
"persona.skip": "Skip this",
|
||||
"persona.saving": "Setting things up…",
|
||||
"persona.err_offline": "U-Claw is not running yet. Start it, then open this page again.",
|
||||
"persona.err_save": "Could not save that. Check the drive is still plugged in, then try again.",
|
||||
"persona.developer": "Writing code",
|
||||
"persona.admin": "Documents and email",
|
||||
"persona.sales": "Talking to customers",
|
||||
"persona.marketing": "Content and social",
|
||||
"persona.finance": "Numbers and spreadsheets",
|
||||
"persona.boss": "Running a business",
|
||||
"persona.general": "Just having a look",
|
||||
"start.opening_persona": "Asking what you will use it for…",
|
||||
"tier.label": "How much do you want to see?",
|
||||
"tier.simple": "Just the essentials",
|
||||
"tier.standard": "A bit more control",
|
||||
"tier.expert": "Everything",
|
||||
"tier.changed_simple": "Switched to the simple view — advanced options are hidden.",
|
||||
"tier.changed_standard": "Switched to the standard view — you can now change models and connect more chat apps.",
|
||||
"tier.changed_expert": "Switched to the full view — everything is visible, including the config file.",
|
||||
"start.checking": "Something went wrong — checking…",
|
||||
"start.repaired": "Fixed. Trying again.",
|
||||
"start.diagnostics_written": "A report of what happened was saved to: {path}",
|
||||
"start.diagnostics_hint": "API keys have been removed from it. Send it to help@u-claw.org if you want a hand.",
|
||||
"skill.excel-helper.name": "Spreadsheets",
|
||||
"skill.excel-helper.desc": "Formulas, pivot tables, charts, cleaning messy data",
|
||||
"skill.word-writer.name": "Documents",
|
||||
"skill.word-writer.desc": "Reports, proposals, CVs — drafting and formatting",
|
||||
"skill.ppt-designer.name": "Slide decks",
|
||||
"skill.ppt-designer.desc": "Structure, layout and speaker notes",
|
||||
"skill.pdf-toolkit.name": "PDF tools",
|
||||
"skill.pdf-toolkit.desc": "Merge, split, extract text — all on this machine",
|
||||
"skill.image-compress.name": "Images",
|
||||
"skill.image-compress.desc": "Shrink, resize and convert — nothing is uploaded",
|
||||
"skill.qrcode-maker.name": "QR codes",
|
||||
"skill.qrcode-maker.desc": "Links, text and WiFi details, generated offline",
|
||||
"skill.web-to-markdown.name": "Save a web page",
|
||||
"skill.web-to-markdown.desc": "Turn any article into clean Markdown",
|
||||
"skill.linkedin-post.name": "LinkedIn posts",
|
||||
"skill.linkedin-post.desc": "Hooks and structure, without the LinkedIn voice",
|
||||
"skill.x-poster.name": "Posts for X",
|
||||
"skill.x-poster.desc": "Short posts and threads that stand on their own",
|
||||
"skill.tiktok-script.name": "TikTok scripts",
|
||||
"skill.tiktok-script.desc": "Hooks that survive the first second",
|
||||
"skill.youtube-script.name": "YouTube scripts",
|
||||
"skill.youtube-script.desc": "Titles, the first 30 seconds, and chapters",
|
||||
"skill.medium-writer.name": "Articles",
|
||||
"skill.medium-writer.desc": "Long-form structure, and cutting the padding",
|
||||
"skill.email-campaign.name": "Email campaigns",
|
||||
"skill.email-campaign.desc": "Subject lines, one clear ask, PDPA-safe",
|
||||
"skill.web-search.name": "Web search",
|
||||
"skill.web-search.desc": "Find current information and read the sources",
|
||||
"skill.sg-weather.name": "Singapore weather",
|
||||
"skill.sg-weather.desc": "Forecast by area and air quality, from NEA",
|
||||
"skill.sea-translate.name": "SEA translation",
|
||||
"skill.sea-translate.desc": "English, Chinese, Malay, Tamil — and register",
|
||||
"skill.claude-helper.name": "Better answers",
|
||||
"skill.claude-helper.desc": "How to ask, and when to check the answer",
|
||||
"skill.sg-transport.name": "Singapore transport",
|
||||
"skill.sg-transport.desc": "Bus arrivals, MRT alerts, carpark lots",
|
||||
"skill.meeting-notes.name": "Meeting notes",
|
||||
"skill.meeting-notes.desc": "Decisions and owners, ready to send",
|
||||
"cat.office": "Office",
|
||||
"cat.data": "Data",
|
||||
"cat.writing": "Writing",
|
||||
"cat.files": "Files",
|
||||
"cat.research": "Research",
|
||||
"cat.social": "Social",
|
||||
"cat.video": "Video",
|
||||
"cat.language": "Language",
|
||||
"cat.local": "Singapore",
|
||||
"cat.ai": "Using AI",
|
||||
"hub.lede": "These are on this drive already. Just ask for what you want — you do not have to name the skill.",
|
||||
"hub.all": "All",
|
||||
"hub.none": "Nothing in this category.",
|
||||
"hub.add_note": "Skills live in the skills/ folder on this drive. Dropping a SKILL.md in there adds one."
|
||||
},
|
||||
"zh-CN": {
|
||||
"start.banner": "U-Claw · 便携 AI 助手",
|
||||
"start.node_version": "Node.js {version}",
|
||||
"start.quarantine_removing": "正在解除 macOS 安全限制…",
|
||||
"start.cache_local": "缓存已放在本机硬盘:{path}",
|
||||
"start.config_migrating": "正在迁移你原来的设置…",
|
||||
"start.config_migrated": "设置已迁移。",
|
||||
"start.config_creating": "第一次使用,正在准备…",
|
||||
"start.config_created": "准备好了。",
|
||||
"start.deps_preparing": "正在准备,约 20 秒。",
|
||||
"start.deps_slow_drive": "这个 U 盘比较慢,可能要几分钟,放着就行。",
|
||||
"start.deps_done": "准备好了。",
|
||||
"start.no_proxy": "将直连你的模型地址:{hosts}",
|
||||
"start.wechat_installing": "正在安装微信插件…",
|
||||
"start.wechat_installed": "微信插件已安装。",
|
||||
"start.config_center_starting": "正在启动控制台…",
|
||||
"start.config_center_port": "控制台端口:{port}",
|
||||
"start.port_in_use": "端口 {port} 被占用,换下一个…",
|
||||
"start.gateway_starting": "正在启动 U-Claw,端口 {port}…",
|
||||
"start.opening_screen": "正在打开启动页…",
|
||||
"start.opening_config": "正在打开控制台…",
|
||||
"start.running_title": "U-Claw 已启动",
|
||||
"start.running_dashboard": "对话界面:{url}",
|
||||
"start.running_config": "控制台: {url}",
|
||||
"start.running_hint": "使用期间请不要关闭这个窗口。按 Ctrl+C 停止。",
|
||||
"start.first_run_wait": "第一次从 U 盘启动需要 30–90 秒解压组件。控制台已经打开,可以先选模型、填密钥。",
|
||||
"start.stopped": "U-Claw 已停止。",
|
||||
"start.exited_unexpectedly": "U-Claw 自己退出了(代码 {code})。",
|
||||
"error.node_missing.what": "U 盘里少了一个 U-Claw 需要的文件。",
|
||||
"error.node_missing.why": "通常是拷贝到 U 盘时没有复制完整。",
|
||||
"error.node_missing.action": "把 U-Claw 文件夹重新拷贝一遍,或运行 setup.sh 重建。",
|
||||
"error.unsupported_arch.what": "这台 Mac 的处理器不受支持({arch})。",
|
||||
"error.unsupported_arch.why": "U-Claw 只提供 Apple Silicon 和 Intel Mac 的版本。",
|
||||
"error.unsupported_arch.action": "换一台电脑试试。",
|
||||
"error.no_port.what": "U-Claw 找不到可用的端口来启动。",
|
||||
"error.no_port.why": "{from}–{to} 都被占用了 —— 可能已经有一个 U-Claw 在运行。",
|
||||
"error.no_port.action": "关掉已经在运行的那个窗口,再启动一次。",
|
||||
"error.deps_failed.what": "U-Claw 没能准备完成。",
|
||||
"error.deps_failed.why": "下载没有完成,通常是网络中断了。",
|
||||
"error.deps_failed.action": "检查一下 Wi-Fi,然后重新启动 U-Claw。",
|
||||
"error.press_enter": "按回车关闭这个窗口。",
|
||||
"loading.title": "正在启动 U-Claw",
|
||||
"loading.starting": "U-Claw 正在启动…",
|
||||
"loading.first_run": "第一次从 U 盘启动需要展开内置组件,稍等片刻,就绪后会提示你下一步。",
|
||||
"loading.waiting_gateway": "正在等待 U-Claw 启动…",
|
||||
"loading.ready": "U-Claw 已就绪",
|
||||
"loading.choose_next": "选择下一步 —— 直接开始聊天,或先去配置模型和渠道。",
|
||||
"loading.open_dashboard": "🚀 开始聊天",
|
||||
"loading.open_settings": "⚙️ 设置",
|
||||
"loading.settings_hint": "填写或更换 API 密钥,连接聊天软件。",
|
||||
"loading.no_model_hint": "还没配过模型?先打开设置填一个密钥。",
|
||||
"loading.slow_drive_note": "慢 U 盘首次启动通常 30–90 秒。如果迟迟没反应,可以先打开设置:",
|
||||
"loading.elapsed": "已等待 {seconds} 秒",
|
||||
"loading.taking_long": "启动比平时慢,可以先打开设置:",
|
||||
"welcome.title": "U-Claw 使用指南",
|
||||
"welcome.subtitle": "装在 U 盘里的 AI 助手 · 三步开始",
|
||||
"welcome.quickstart": "快速开始",
|
||||
"welcome.step1_title": "1. 启动 U-Claw",
|
||||
"welcome.step1_desc": "双击你电脑对应的那个文件:",
|
||||
"welcome.step2_title": "2. 填一个密钥",
|
||||
"welcome.step2_desc": "第一次启动会自动打开设置页。你需要一个 API 密钥才能开始 —— Google Gemini 最容易拿到:用 Google 账号登录即可,不用绑卡。",
|
||||
"welcome.step2_link": "去领取免费的 Gemini 密钥 →",
|
||||
"welcome.step3_title": "3. 开始对话",
|
||||
"welcome.step3_desc": "密钥保存后,对话窗口会自己打开。就这样。",
|
||||
"welcome.portable_note": "✅ 你的设置和聊天记录都存在盘上的 data/ 目录里,跟着盘走,Mac 和 Windows 通用。",
|
||||
"welcome.models_title": "用哪个模型?",
|
||||
"welcome.model_claude": "综合最强",
|
||||
"welcome.model_gpt": "用的人最多",
|
||||
"welcome.model_gemini": "有免费额度,注册最简单",
|
||||
"welcome.model_local": "离线 · 数据不出本机",
|
||||
"welcome.models_more": "更多选择在设置里,包括适合马来语、泰米尔语和 Singlish 的 SEA-LION。",
|
||||
"welcome.trouble_title": "遇到问题?",
|
||||
"welcome.trouble_lead": "💡 运行检查工具",
|
||||
"welcome.trouble_desc": "如果 U-Claw 启动不了,双击:",
|
||||
"welcome.open_settings": "打开设置",
|
||||
"welcome.open_docs": "查看文档",
|
||||
"nav.title": "U-Claw 启动导航",
|
||||
"nav.tagline": "便携 AI 助手 — 插上就能用",
|
||||
"nav.status_off": "U-Claw 尚未运行",
|
||||
"nav.open_console": "打开它 →",
|
||||
"nav.quickstart": "快速启动",
|
||||
"nav.doubleclick": "双击",
|
||||
"nav.step2": "第一次启动会自动打开设置页 —— 选一个模型,把密钥粘进去。",
|
||||
"nav.mac_gatekeeper": "⚠️ 如果 macOS 提示「无法验证开发者」:右键点这个文件,选「打开」。",
|
||||
"nav.config_shortcut": "📝 也可以双击 Config.html,它会带你到设置页。",
|
||||
"nav.win_smartscreen": "⚠️ 如果 Windows 弹安全警告:点「更多信息」,再点「仍要运行」。",
|
||||
"nav.everything": "其他功能",
|
||||
"nav.menu_opens": "打开完整菜单:",
|
||||
"nav.g_settings": "设置",
|
||||
"nav.g_settings_d": "模型、密钥和聊天软件",
|
||||
"nav.g_checkup": "检查修复",
|
||||
"nav.g_checkup_d": "找出并修复常见问题",
|
||||
"nav.g_backup": "备份",
|
||||
"nav.g_backup_d": "一键保存设置和记忆",
|
||||
"nav.g_skills": "技能",
|
||||
"nav.g_skills_d": "看看 U-Claw 能做什么",
|
||||
"nav.g_system": "系统信息",
|
||||
"nav.g_system_d": "版本、端口、运行状态",
|
||||
"nav.g_cli": "命令行",
|
||||
"nav.g_cli_d": "给进阶用户",
|
||||
"nav.models_title": "可以用的模型",
|
||||
"nav.model_sealion": "马来语、泰米尔语和 Singlish",
|
||||
"nav.model_local": "本地模型",
|
||||
"nav.model_more": "更多",
|
||||
"nav.model_more_d": "其他模型在设置里",
|
||||
"nav.more_title": "更多",
|
||||
"nav.link_guide": "使用指南",
|
||||
"nav.link_guide_d": "三步上手",
|
||||
"nav.link_skills": "技能",
|
||||
"nav.link_skills_d": "看看 U-Claw 能做什么",
|
||||
"nav.link_contact": "联系我们",
|
||||
"nav.link_site": "官网",
|
||||
"nav.status_on": "U-Claw 正在运行(端口 {port})",
|
||||
"nav.status_off_hint": "U-Claw 尚未运行 —— 请先双击启动文件",
|
||||
"key.page_title": "U-Claw 设置",
|
||||
"key.page_subtitle": "粘贴一个 API 密钥就可以了。",
|
||||
"key.checking": "检测中…",
|
||||
"key.step_key": "填密钥",
|
||||
"key.step_done": "完成",
|
||||
"key.heading": "粘贴你的 API 密钥",
|
||||
"key.desc": "剩下的 U-Claw 自己搞定。密钥只保存在这个 U 盘上,不会上传到任何地方。",
|
||||
"key.placeholder": "sk-…",
|
||||
"key.show": "显示",
|
||||
"key.hide": "隐藏",
|
||||
"key.continue": "继续 →",
|
||||
"key.checking_key": "正在检查这个密钥…",
|
||||
"key.ok": "✓ {provider} · 连接成功,用时 {ms} 毫秒",
|
||||
"key.custom_provider": "你的地址",
|
||||
"key.no_key_title": "还没有密钥?",
|
||||
"key.no_key_desc": "Google Gemini 最容易拿到:用 Google 账号登录即可,不用绑卡,还有免费额度。",
|
||||
"key.get_gemini": "🔑 领取免费的 Gemini 密钥",
|
||||
"key.get_openrouter": "🔑 OpenRouter —— 一个密钥用多种模型",
|
||||
"key.advanced": "高级 —— 手动填写接口地址",
|
||||
"key.base_url": "接口地址",
|
||||
"key.model_name": "模型名称",
|
||||
"key.advanced_hint": "只有自建或未收录的服务才需要填。留空则由 U-Claw 自动判断。",
|
||||
"key.saved": "已保存。",
|
||||
"key.err_empty": "先粘贴一个密钥。",
|
||||
"key.err_not_checked": "稍等,密钥还在检查中。",
|
||||
"key.err_rejected": "这个密钥没被接受。检查一下是不是没复制全 —— 密钥很长,容易少一截。",
|
||||
"key.err_quota": "这个密钥的额度用完了。去服务商那边充值,或者换一个密钥。",
|
||||
"key.err_offline": "连不上网络。检查一下 Wi-Fi 再试。",
|
||||
"key.err_timeout": "服务商没有及时响应。过一会儿再试。",
|
||||
"key.err_model": "密钥是好的,但它指向的模型没找到。在「高级」里填写模型名称。",
|
||||
"key.err_provider_down": "服务商那边现在有问题。稍后再试。",
|
||||
"key.err_unrecognised": "U-Claw 认不出这个密钥。请在「高级」里填写接口地址和模型名称。",
|
||||
"key.err_unknown": "没能检查这个密钥。再试一次,或者在「高级」里自己填接口地址。",
|
||||
"key.err_save": "保存失败。确认 U 盘还插着,然后再试一次。",
|
||||
"hub.title": "U-Claw 技能",
|
||||
"done.title": "配置完成",
|
||||
"done.subtitle": "模型和密钥已保存在这个 U 盘上。",
|
||||
"done.open": "🚀 开始聊天",
|
||||
"done.restart_note": "聊天软件的设置已保存,重启 U-Claw 后生效。",
|
||||
"done.change_key": "换模型或密钥",
|
||||
"done.view_config": "查看配置文件",
|
||||
"ch.title": "📱 连接聊天软件(可选)",
|
||||
"ch.desc": "点卡片展开填写,也可以直接跳过。",
|
||||
"ch.whatsapp_d": "用 WhatsApp 和 U-Claw 对话",
|
||||
"ch.whatsapp_warning": "⚠️ 这里用的是非官方协议,你的 WhatsApp 账号可能因此被封。不要接入你离不开的号码。",
|
||||
"ch.whatsapp_number": "手机号",
|
||||
"ch.telegram_d": "用 Telegram 和 U-Claw 对话",
|
||||
"ch.telegram_get": "→ 从 @BotFather 获取",
|
||||
"ch.slack_d": "在工作区里和 U-Claw 对话",
|
||||
"ch.slack_get": "→ 在 api.slack.com 创建应用",
|
||||
"ch.discord_d": "用 Discord 和 U-Claw 对话",
|
||||
"ch.discord_get": "→ 在 Discord 开发者后台创建应用",
|
||||
"ch.china_group": "主要在中国使用的聊天软件",
|
||||
"ch.wechat_d": "扫码接入个人微信",
|
||||
"ch.wechat_start": "点这张卡片开始扫码",
|
||||
"ch.saved": "聊天软件设置已保存。",
|
||||
"ch.err_save": "聊天软件设置没能保存。确认 U 盘还插着。",
|
||||
"info.layout_title": "U 盘里有什么",
|
||||
"info.layout_desc": "文件夹是怎么组织的",
|
||||
"info.skills": "技能",
|
||||
"info.skills_d": "把 SKILL.md 放进来就会自动加载",
|
||||
"info.app": "运行时",
|
||||
"info.app_d": "Node.js 和 OpenClaw 核心",
|
||||
"info.data": "你的数据",
|
||||
"info.data_d": "密钥和聊天记录,只留在这个盘上。",
|
||||
"info.feedback_title": "遇到问题?",
|
||||
"info.feedback_desc": "U-Claw 是开源项目。出问题欢迎去 GitHub 提 Issue —— 我们不会自动收集或上传你机器上的任何东西。",
|
||||
"info.feedback_link": "→ 去 GitHub 提 Issue",
|
||||
"wx.getting_qr": "正在获取二维码…",
|
||||
"wx.scan": "请用微信扫描上方二维码",
|
||||
"wx.qr_failed": "获取二维码失败",
|
||||
"wx.connected": "✅ 微信已连接",
|
||||
"wx.account": "账号",
|
||||
"wx.restart_needed": "还差一步:重启 U-Claw(关掉再打开),微信才能用。",
|
||||
"wx.connected_toast": "微信已连接,重启 U-Claw 后生效。",
|
||||
"wx.qr_refreshed": "二维码已刷新,请重新扫码",
|
||||
"wx.confirm_on_phone": "已扫码,请在手机上确认…",
|
||||
"wx.qr_expired": "二维码已过期。",
|
||||
"wx.qr_retry": "重新获取",
|
||||
"wx.failed": "连接失败",
|
||||
"info.read_config_failed": "读取配置文件失败:",
|
||||
"status.running": "U-Claw 正在运行,端口 {port}",
|
||||
"status.open_dashboard": "打开它 →",
|
||||
"status.not_running": "U-Claw 尚未运行 —— 请先运行 Windows-Start.bat 或 Mac-Start.command",
|
||||
"key.which_model": "用哪个模型?",
|
||||
"start.opening_language": "正在询问使用哪种语言…",
|
||||
"persona.title": "你主要用它做什么?",
|
||||
"persona.subtitle": "这会帮你把合适的工具装好。以后可以改,也可以多选。",
|
||||
"persona.continue": "继续 →",
|
||||
"persona.skip": "跳过",
|
||||
"persona.saving": "正在准备…",
|
||||
"persona.err_offline": "U-Claw 还没启动。先启动它,再打开这个页面。",
|
||||
"persona.err_save": "没能保存。确认 U 盘还插着,然后再试一次。",
|
||||
"persona.developer": "写代码",
|
||||
"persona.admin": "写文档、发邮件",
|
||||
"persona.sales": "跟客户打交道",
|
||||
"persona.marketing": "做内容、发社交媒体",
|
||||
"persona.finance": "算数、做表格",
|
||||
"persona.boss": "管公司",
|
||||
"persona.general": "先随便看看",
|
||||
"start.opening_persona": "正在询问你主要用它做什么…",
|
||||
"tier.label": "想看到多少?",
|
||||
"tier.simple": "只要最基本的",
|
||||
"tier.standard": "多一点控制",
|
||||
"tier.expert": "全部",
|
||||
"tier.changed_simple": "已切换到简洁模式 —— 高级选项已隐藏。",
|
||||
"tier.changed_standard": "已切换到标准模式 —— 现在可以换模型、接更多聊天软件了。",
|
||||
"tier.changed_expert": "已切换到完整模式 —— 所有东西都可见,包括配置文件。",
|
||||
"start.checking": "出了点问题,正在检查…",
|
||||
"start.repaired": "已修复,重试一次。",
|
||||
"start.diagnostics_written": "已把情况记录到:{path}",
|
||||
"start.diagnostics_hint": "里面的密钥已被去掉。需要帮忙的话,把它发到 help@u-claw.org。",
|
||||
"skill.excel-helper.name": "表格",
|
||||
"skill.excel-helper.desc": "公式、透视表、图表、清洗乱数据",
|
||||
"skill.word-writer.name": "文档",
|
||||
"skill.word-writer.desc": "报告、方案、简历 —— 起草和排版",
|
||||
"skill.ppt-designer.name": "幻灯片",
|
||||
"skill.ppt-designer.desc": "结构、版式和演讲要点",
|
||||
"skill.pdf-toolkit.name": "PDF 工具",
|
||||
"skill.pdf-toolkit.desc": "合并、拆分、提取文字 —— 全在本机",
|
||||
"skill.image-compress.name": "图片",
|
||||
"skill.image-compress.desc": "压缩、改尺寸、转格式 —— 不上传",
|
||||
"skill.qrcode-maker.name": "二维码",
|
||||
"skill.qrcode-maker.desc": "网址、文本、WiFi,离线生成",
|
||||
"skill.web-to-markdown.name": "保存网页",
|
||||
"skill.web-to-markdown.desc": "把文章转成干净的 Markdown",
|
||||
"skill.linkedin-post.name": "LinkedIn 帖子",
|
||||
"skill.linkedin-post.desc": "钩子和结构,不要那股 LinkedIn 腔",
|
||||
"skill.x-poster.name": "X 帖子",
|
||||
"skill.x-poster.desc": "能独立成立的短帖和 thread",
|
||||
"skill.tiktok-script.name": "TikTok 脚本",
|
||||
"skill.tiktok-script.desc": "扛得住第一秒的钩子",
|
||||
"skill.youtube-script.name": "YouTube 脚本",
|
||||
"skill.youtube-script.desc": "标题、前 30 秒、章节",
|
||||
"skill.medium-writer.name": "长文",
|
||||
"skill.medium-writer.desc": "长文结构,以及怎么删水分",
|
||||
"skill.email-campaign.name": "邮件营销",
|
||||
"skill.email-campaign.desc": "标题、一个明确的 ask、符合 PDPA",
|
||||
"skill.web-search.name": "网页搜索",
|
||||
"skill.web-search.desc": "找当前信息并真的读来源",
|
||||
"skill.sg-weather.name": "新加坡天气",
|
||||
"skill.sg-weather.desc": "按区预报和空气质量,来自 NEA",
|
||||
"skill.sea-translate.name": "东南亚翻译",
|
||||
"skill.sea-translate.desc": "英 / 中 / 马来 / 泰米尔 —— 以及语域",
|
||||
"skill.claude-helper.name": "问得更好",
|
||||
"skill.claude-helper.desc": "怎么问,以及什么时候该核对答案",
|
||||
"skill.sg-transport.name": "新加坡交通",
|
||||
"skill.sg-transport.desc": "巴士到站、地铁故障、停车位",
|
||||
"skill.meeting-notes.name": "会议纪要",
|
||||
"skill.meeting-notes.desc": "决定和负责人,写完就能发",
|
||||
"cat.office": "办公",
|
||||
"cat.data": "数据",
|
||||
"cat.writing": "写作",
|
||||
"cat.files": "文件",
|
||||
"cat.research": "资料",
|
||||
"cat.social": "社交媒体",
|
||||
"cat.video": "视频",
|
||||
"cat.language": "语言",
|
||||
"cat.local": "新加坡",
|
||||
"cat.ai": "用好 AI",
|
||||
"hub.lede": "这些已经在盘上了。直接说你想做什么就行,不用记技能名。",
|
||||
"hub.all": "全部",
|
||||
"hub.none": "这个分类下暂时没有。",
|
||||
"hub.add_note": "技能放在盘上的 skills/ 目录里。丢一个 SKILL.md 进去就多一个。"
|
||||
}
|
||||
};
|
||||
281
portable/lib/i18n/skills-data.js
Normal file
281
portable/lib/i18n/skills-data.js
Normal file
@@ -0,0 +1,281 @@
|
||||
/* GENERATED FILE — do not edit.
|
||||
* Source: skills/manifest.json
|
||||
* Regenerate: node lib/i18n/build-messages.mjs
|
||||
*/
|
||||
window.UCLAW_SKILLS = {
|
||||
"personas": [
|
||||
{
|
||||
"id": "developer",
|
||||
"tier": "expert",
|
||||
"emoji": "💻",
|
||||
"channel": null
|
||||
},
|
||||
{
|
||||
"id": "admin",
|
||||
"tier": "simple",
|
||||
"emoji": "📝",
|
||||
"channel": null
|
||||
},
|
||||
{
|
||||
"id": "sales",
|
||||
"tier": "simple",
|
||||
"emoji": "💬",
|
||||
"channel": "whatsapp"
|
||||
},
|
||||
{
|
||||
"id": "marketing",
|
||||
"tier": "standard",
|
||||
"emoji": "🎨",
|
||||
"channel": null
|
||||
},
|
||||
{
|
||||
"id": "finance",
|
||||
"tier": "standard",
|
||||
"emoji": "📊",
|
||||
"channel": null
|
||||
},
|
||||
{
|
||||
"id": "boss",
|
||||
"tier": "simple",
|
||||
"emoji": "👔",
|
||||
"channel": null
|
||||
},
|
||||
{
|
||||
"id": "general",
|
||||
"tier": "simple",
|
||||
"emoji": "🤷",
|
||||
"channel": null
|
||||
}
|
||||
],
|
||||
"skills": [
|
||||
{
|
||||
"id": "excel-helper",
|
||||
"categories": [
|
||||
"office",
|
||||
"data"
|
||||
],
|
||||
"personas": [
|
||||
"admin",
|
||||
"finance",
|
||||
"boss",
|
||||
"general"
|
||||
],
|
||||
"emoji": "📊"
|
||||
},
|
||||
{
|
||||
"id": "word-writer",
|
||||
"categories": [
|
||||
"office",
|
||||
"writing"
|
||||
],
|
||||
"personas": [
|
||||
"admin",
|
||||
"sales",
|
||||
"boss",
|
||||
"general"
|
||||
],
|
||||
"emoji": "📝"
|
||||
},
|
||||
{
|
||||
"id": "ppt-designer",
|
||||
"categories": [
|
||||
"office",
|
||||
"writing"
|
||||
],
|
||||
"personas": [
|
||||
"admin",
|
||||
"sales",
|
||||
"marketing",
|
||||
"boss"
|
||||
],
|
||||
"emoji": "📽️"
|
||||
},
|
||||
{
|
||||
"id": "pdf-toolkit",
|
||||
"categories": [
|
||||
"files"
|
||||
],
|
||||
"personas": [
|
||||
"admin",
|
||||
"finance",
|
||||
"general"
|
||||
],
|
||||
"emoji": "📄"
|
||||
},
|
||||
{
|
||||
"id": "image-compress",
|
||||
"categories": [
|
||||
"files"
|
||||
],
|
||||
"personas": [
|
||||
"marketing",
|
||||
"general"
|
||||
],
|
||||
"emoji": "🖼️"
|
||||
},
|
||||
{
|
||||
"id": "qrcode-maker",
|
||||
"categories": [
|
||||
"files"
|
||||
],
|
||||
"personas": [
|
||||
"marketing",
|
||||
"sales",
|
||||
"general"
|
||||
],
|
||||
"emoji": "🔳"
|
||||
},
|
||||
{
|
||||
"id": "web-to-markdown",
|
||||
"categories": [
|
||||
"research"
|
||||
],
|
||||
"personas": [
|
||||
"developer",
|
||||
"marketing",
|
||||
"general"
|
||||
],
|
||||
"emoji": "🔗"
|
||||
},
|
||||
{
|
||||
"id": "linkedin-post",
|
||||
"categories": [
|
||||
"social"
|
||||
],
|
||||
"personas": [
|
||||
"marketing",
|
||||
"sales",
|
||||
"boss"
|
||||
],
|
||||
"emoji": "💼"
|
||||
},
|
||||
{
|
||||
"id": "x-poster",
|
||||
"categories": [
|
||||
"social"
|
||||
],
|
||||
"personas": [
|
||||
"marketing"
|
||||
],
|
||||
"emoji": "🐦"
|
||||
},
|
||||
{
|
||||
"id": "tiktok-script",
|
||||
"categories": [
|
||||
"social",
|
||||
"video"
|
||||
],
|
||||
"personas": [
|
||||
"marketing"
|
||||
],
|
||||
"emoji": "🎬"
|
||||
},
|
||||
{
|
||||
"id": "youtube-script",
|
||||
"categories": [
|
||||
"social",
|
||||
"video"
|
||||
],
|
||||
"personas": [
|
||||
"marketing"
|
||||
],
|
||||
"emoji": "📺"
|
||||
},
|
||||
{
|
||||
"id": "medium-writer",
|
||||
"categories": [
|
||||
"writing"
|
||||
],
|
||||
"personas": [
|
||||
"marketing",
|
||||
"developer"
|
||||
],
|
||||
"emoji": "✍️"
|
||||
},
|
||||
{
|
||||
"id": "email-campaign",
|
||||
"categories": [
|
||||
"writing",
|
||||
"social"
|
||||
],
|
||||
"personas": [
|
||||
"marketing",
|
||||
"sales"
|
||||
],
|
||||
"emoji": "✉️"
|
||||
},
|
||||
{
|
||||
"id": "web-search",
|
||||
"categories": [
|
||||
"research"
|
||||
],
|
||||
"personas": [
|
||||
"developer",
|
||||
"marketing",
|
||||
"general",
|
||||
"finance"
|
||||
],
|
||||
"emoji": "🔍"
|
||||
},
|
||||
{
|
||||
"id": "sg-weather",
|
||||
"categories": [
|
||||
"local"
|
||||
],
|
||||
"personas": [
|
||||
"general",
|
||||
"sales"
|
||||
],
|
||||
"emoji": "🌦️"
|
||||
},
|
||||
{
|
||||
"id": "sea-translate",
|
||||
"categories": [
|
||||
"language"
|
||||
],
|
||||
"personas": [
|
||||
"sales",
|
||||
"admin",
|
||||
"marketing",
|
||||
"general",
|
||||
"boss"
|
||||
],
|
||||
"emoji": "🌏"
|
||||
},
|
||||
{
|
||||
"id": "claude-helper",
|
||||
"categories": [
|
||||
"ai"
|
||||
],
|
||||
"personas": [
|
||||
"developer",
|
||||
"finance"
|
||||
],
|
||||
"emoji": "🤖"
|
||||
},
|
||||
{
|
||||
"id": "sg-transport",
|
||||
"categories": [
|
||||
"local"
|
||||
],
|
||||
"personas": [
|
||||
"general",
|
||||
"sales"
|
||||
],
|
||||
"emoji": "🚇"
|
||||
},
|
||||
{
|
||||
"id": "meeting-notes",
|
||||
"categories": [
|
||||
"writing"
|
||||
],
|
||||
"personas": [
|
||||
"sales",
|
||||
"admin",
|
||||
"boss",
|
||||
"finance"
|
||||
],
|
||||
"emoji": "🗒️"
|
||||
}
|
||||
]
|
||||
};
|
||||
@@ -83,7 +83,7 @@ async function chatProbe(t) {
|
||||
const res = await requestText(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', ...(t.apiKey ? { Authorization: `Bearer ${t.apiKey}` } : {}) },
|
||||
body: JSON.stringify({ model: t.modelId, messages: [{ role: 'user', content: '请回复四个字:连接成功' }], max_tokens: 64, stream: false }),
|
||||
body: JSON.stringify({ model: t.modelId, messages: [{ role: 'user', content: 'Reply with exactly: connection ok' }], max_tokens: 64, stream: false }),
|
||||
timeoutMs: CHAT_TIMEOUT_MS,
|
||||
});
|
||||
const ms = Date.now() - started;
|
||||
@@ -126,76 +126,77 @@ function requestText(rawUrl, { method = 'GET', headers = {}, body, timeoutMs })
|
||||
async function main() {
|
||||
const configPath = process.argv[2] || process.env.OPENCLAW_CONFIG_PATH;
|
||||
line('========================================');
|
||||
line(' U-Claw 内网体检 / Intranet Check');
|
||||
line(' U-Claw — corporate network check');
|
||||
line(` Node ${process.version}`);
|
||||
line('========================================');
|
||||
line('');
|
||||
|
||||
// 1) 代理环境
|
||||
line('【1】代理环境检查');
|
||||
line('[1] System proxy');
|
||||
const proxyKeys = ['HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'http_proxy', 'https_proxy', 'all_proxy'];
|
||||
const setProxies = proxyKeys.filter((k) => process.env[k]);
|
||||
if (setProxies.length) {
|
||||
for (const k of setProxies) line(` ${k} = ${process.env[k]}`);
|
||||
line(' → 检测到系统代理。发往内网模型的请求可能被它劫持(最常见的内网故障源)。');
|
||||
line(' A system proxy is set. Requests to an internal model endpoint may be routed through it — this is the single most common cause of a self-hosted model not working.');
|
||||
} else {
|
||||
line(' (未检测到系统代理)');
|
||||
line(' No system proxy set.');
|
||||
}
|
||||
line('');
|
||||
|
||||
if (!configPath) { line('未提供配置路径,结束。'); return; }
|
||||
if (!configPath) { line('No config path given — nothing to check.'); return; }
|
||||
let config;
|
||||
try { config = JSON.parse(readFileSync(configPath, 'utf8')); }
|
||||
catch (e) { line(`读取配置失败:${e?.message || e}`); return; }
|
||||
catch (e) { line(`Could not read the config file: ${e?.message || e}`); return; }
|
||||
|
||||
const providers = collectProviders(config?.models);
|
||||
if (!providers.length) { line('配置里没有任何模型地址(baseUrl)。先配好模型再来跑。'); return; }
|
||||
if (!providers.length) { line('No model endpoint is configured yet. Set one up in Settings first, then run this again.'); return; }
|
||||
|
||||
// 2) NO_PROXY 建议
|
||||
const hosts = Array.from(new Set(providers.map((p) => hostOf(p.baseUrl)).filter(Boolean)));
|
||||
const noProxy = applyNoProxy(hosts);
|
||||
line('【2】建议的 NO_PROXY(让这些地址直连、绕开代理)');
|
||||
line('[2] Suggested NO_PROXY — hosts that should bypass the proxy');
|
||||
line(` ${noProxy.join(',')}`);
|
||||
line(' (新版 Windows-Start.bat 已会自动设置,无需手动操作)');
|
||||
line(' U-Claw already sets this for you at startup. Shown here in case you need it elsewhere.');
|
||||
line('');
|
||||
|
||||
// 3) 直连可达
|
||||
line('【3】直连测试(绕过代理,看能否摸到模型服务)');
|
||||
line('[3] Direct connection — can this machine reach the model at all, proxy bypassed');
|
||||
for (const p of providers) {
|
||||
const r = await reachProbe(p.baseUrl, p.apiKey);
|
||||
if (r.ok) line(` [${p.name}] ${p.baseUrl} → ✓ 可达 HTTP ${r.status} (${r.ms}ms)`);
|
||||
else line(` [${p.name}] ${p.baseUrl} → ✗ 失败 ${r.error} (${r.ms}ms)`);
|
||||
if (r.ok) line(` [${p.name}] ${p.baseUrl} -> reachable, HTTP ${r.status} (${r.ms}ms)`);
|
||||
else line(` [${p.name}] ${p.baseUrl} -> FAILED ${r.error} (${r.ms}ms)`);
|
||||
}
|
||||
line('');
|
||||
|
||||
// 4) 端到端实测
|
||||
line('【4】实测:真发一条对话给模型');
|
||||
line('[4] Real request — actually asking the model something');
|
||||
const t = pickTarget(config, providers);
|
||||
if (!t) { line(' 找不到可测的模型 id,跳过。'); }
|
||||
if (!t) { line(' No model ID to test with — skipped.'); }
|
||||
else {
|
||||
line(` 模型:${t.provName} / ${t.modelId}`);
|
||||
line(' 发送中,请稍候...');
|
||||
line(` Model: ${t.provName} / ${t.modelId}`);
|
||||
line(' Sending…');
|
||||
const c = await chatProbe(t);
|
||||
if (c.ok) {
|
||||
line('');
|
||||
line(` ✓✓ 跑通了!模型回复 (${c.ms}ms):${c.reply.slice(0, 120) || '(空回复但请求成功)'}`);
|
||||
line(` It works. The model replied in ${c.ms}ms: ${c.reply.slice(0, 120) || '(empty reply, but the request succeeded)'}`);
|
||||
} else if (c.status) {
|
||||
line('');
|
||||
line(` ✗ 服务端 HTTP ${c.status} (${c.ms}ms):${c.body}`);
|
||||
line(` The server answered with HTTP ${c.status} in ${c.ms}ms: ${c.body}`);
|
||||
} else {
|
||||
line('');
|
||||
line(` ✗ 直连失败:${c.error} (${c.ms}ms)`);
|
||||
line(` Could not connect: ${c.error} (${c.ms}ms)`);
|
||||
}
|
||||
}
|
||||
line('');
|
||||
|
||||
// 结论
|
||||
line('========================================');
|
||||
line(' 怎么看结果:');
|
||||
line(' · 第4步「跑通了」 → 一切正常,以后双击 Windows-Start.bat 即可。');
|
||||
line(' · 第3步可达但程序里用不了 → 是系统代理在劫持,新版启动脚本已自动绕开(NO_PROXY)。');
|
||||
line(' · 第3步「直连失败/超时」 → 地址错 / 内网不通 / 防火墙 / 模型服务没起,找机房管理员。');
|
||||
line(' · 出现 401/403 → 网络是通的,只是 API Key 不对。');
|
||||
line(' What the results mean:');
|
||||
line(' Step 4 worked Everything is fine. Start U-Claw normally.');
|
||||
line(' Step 3 reachable but A proxy is intercepting the request. U-Claw already')
|
||||
line(' the app cannot use it bypasses it at startup via NO_PROXY.');
|
||||
line(' Step 3 failed Wrong address, blocked network, firewall, or the service is down. Ask whoever runs the server.');
|
||||
line(' 401 or 403 The network is fine — the key is wrong.');
|
||||
line('========================================');
|
||||
}
|
||||
|
||||
|
||||
101
portable/lib/language.html
Normal file
101
portable/lib/language.html
Normal file
@@ -0,0 +1,101 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>U-Claw</title>
|
||||
<!--
|
||||
First-run language choice.
|
||||
Deliberately not a dropdown: this is the first thing anyone sees, and a select
|
||||
needs you to already read the interface language to find your own. Two large
|
||||
buttons, each written in its own language, need no instructions.
|
||||
|
||||
The choice is written to the drive (data/.openclaw/openclaw.json → uclaw.locale)
|
||||
and never asked again. It is stored on the drive rather than in the browser so
|
||||
the language travels with the drive: someone who set it up in Chinese and plugs
|
||||
it into a colleague's English Windows still gets Chinese.
|
||||
-->
|
||||
<style>
|
||||
:root { color-scheme: light dark; }
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0; min-height: 100vh;
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
gap: 3rem;
|
||||
font-family: -apple-system, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
background: #16181c; color: #f2f4f7;
|
||||
}
|
||||
.mark { font-size: 5rem; line-height: 1; }
|
||||
.choices { display: flex; gap: 1.5rem; flex-wrap: wrap; justify-content: center; padding: 0 1.5rem; }
|
||||
button {
|
||||
min-width: 13rem; padding: 1.75rem 2.5rem;
|
||||
font-size: 1.5rem; font-weight: 600; font-family: inherit;
|
||||
color: #f2f4f7; background: #23262c;
|
||||
border: 2px solid #33373f; border-radius: 16px;
|
||||
cursor: pointer; transition: border-color .15s, transform .15s;
|
||||
}
|
||||
button:hover, button:focus-visible { border-color: #ff6b35; transform: translateY(-2px); outline: none; }
|
||||
button small { display: block; margin-top: .4rem; font-size: .85rem; font-weight: 400; color: #9aa0a8; }
|
||||
.saving { color: #9aa0a8; font-size: .95rem; min-height: 1.4em; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="mark">🦞</div>
|
||||
<div class="choices">
|
||||
<button id="en" autofocus>English<small>Continue in English</small></button>
|
||||
<button id="zh">中文<small>用中文继续</small></button>
|
||||
</div>
|
||||
<p class="saving" id="saving"></p>
|
||||
|
||||
<script>
|
||||
// The config server owns writes to openclaw.json, so go through it rather than
|
||||
// trying to write the file from the page. It binds the first free port in this
|
||||
// range, so probe instead of assuming 18788.
|
||||
const PORT_FROM = 18788;
|
||||
const PORT_TO = 18798;
|
||||
|
||||
const NEXT = { en: 'Setting up…', 'zh-CN': '正在设置…' };
|
||||
const FAILED = {
|
||||
en: 'Could not save that. U-Claw may not be running yet — start it and try again.',
|
||||
'zh-CN': '没能保存。U-Claw 可能还没启动 —— 先启动它再试一次。',
|
||||
};
|
||||
|
||||
async function findServer() {
|
||||
for (let port = PORT_FROM; port <= PORT_TO; port++) {
|
||||
try {
|
||||
await fetch(`http://127.0.0.1:${port}/api/config`, { cache: 'no-store' });
|
||||
return port;
|
||||
} catch { /* nothing listening here */ }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function choose(locale) {
|
||||
document.getElementById('saving').textContent = NEXT[locale];
|
||||
const port = await findServer();
|
||||
if (port === null) {
|
||||
document.getElementById('saving').textContent = FAILED[locale];
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const base = `http://127.0.0.1:${port}`;
|
||||
const config = await (await fetch(`${base}/api/config`)).json();
|
||||
config.uclaw = Object.assign({}, config.uclaw, { locale });
|
||||
await fetch(`${base}/api/config`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(config),
|
||||
});
|
||||
// Straight on to the next question. ?lang= makes it render in the chosen
|
||||
// language immediately, without waiting for the launcher to rewrite locale.js.
|
||||
location.replace(`persona.html?lang=${encodeURIComponent(locale)}`);
|
||||
} catch {
|
||||
document.getElementById('saving').textContent = FAILED[locale];
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('en').addEventListener('click', () => choose('en'));
|
||||
document.getElementById('zh').addEventListener('click', () => choose('zh-CN'));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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="loading.title">Starting U-Claw</title>
|
||||
<!--
|
||||
启动首屏(移植自 v2 u-clawx 4.0 的 splash 思路)。
|
||||
双击启动脚本后立刻打开本页,给用户即时反馈,消除"黑窗假死"体感。
|
||||
@@ -100,40 +100,41 @@ body.ready .ready-wrap { display: block; }
|
||||
.btn-primary:hover { background: #ff6a30; }
|
||||
.foot { margin-top: 24px; font-size: .82em; opacity: .65; line-height: 1.6; }
|
||||
</style>
|
||||
<script src="../data/.openclaw/locale.js"></script>
|
||||
<script src="i18n/messages.js"></script>
|
||||
<script src="i18n/i18n.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="box">
|
||||
|
||||
<!-- 启动中 -->
|
||||
<!-- Starting -->
|
||||
<div class="loading-wrap">
|
||||
<div class="logo">🦞</div>
|
||||
<h1>U-Claw 正在启动</h1>
|
||||
<div class="tip">首次从 U 盘启动需要展开内置组件,<br>稍等片刻,就绪后会提示你下一步。</div>
|
||||
<h1 data-i18n="loading.starting">Starting U-Claw…</h1>
|
||||
<div class="tip" data-i18n="loading.first_run">First run from a USB drive unpacks the bundled components. Give it a moment — you will be told when it is ready.</div>
|
||||
<div class="spinner"></div>
|
||||
<div class="bar"><i></i></div>
|
||||
<div class="status" id="status">正在等待 OpenClaw 网关…</div>
|
||||
<div class="status" id="status" data-i18n="loading.waiting_gateway">Waiting for U-Claw to come up…</div>
|
||||
<div class="elapsed" id="elapsed"></div>
|
||||
<div class="note">
|
||||
慢 U 盘首次启动通常 30–90 秒。<br>
|
||||
如果迟迟没反应,可先打开
|
||||
<a class="manual" id="manual" href="#">配置中心</a>。
|
||||
<span data-i18n="loading.slow_drive_note">A slow drive usually takes 30–90 seconds. If nothing happens for a long time, open Settings:</span>
|
||||
<a class="manual" id="manual" href="#" data-i18n="loading.open_settings">⚙️ Settings</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 就绪后:让用户自己选 -->
|
||||
<!-- Ready: let the user choose -->
|
||||
<div class="ready-wrap">
|
||||
<div class="logo">🦞</div>
|
||||
<h1>U-Claw 已就绪</h1>
|
||||
<div class="tip">选择下一步 — 直接开始聊天,或先去配置模型和渠道。</div>
|
||||
<h1 data-i18n="loading.ready">U-Claw is ready</h1>
|
||||
<div class="tip" data-i18n="loading.choose_next">Pick what to do next — start chatting, or set up your model and channels first.</div>
|
||||
<div class="actions">
|
||||
<a class="btn btn-primary" id="goDash">🚀 进入 Dashboard
|
||||
<span class="sub">开始和 AI 聊天</span>
|
||||
<a class="btn btn-primary" id="goDash"><span data-i18n="loading.open_dashboard">🚀 Start chatting</span>
|
||||
</a>
|
||||
<a class="btn" id="goConfig">⚙️ 配置 / 改模型
|
||||
<span class="sub">填写或更换模型 Key、充值、连接微信等渠道</span>
|
||||
<a class="btn" id="goConfig"><span data-i18n="loading.open_settings">⚙️ Settings</span>
|
||||
<span class="sub" data-i18n="loading.settings_hint">Add or change your API key, and connect chat apps.</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="foot">没配过模型或余额不足?先点「配置 / 改模型」处理 Key 或充值。</div>
|
||||
<div class="foot" data-i18n="loading.no_model_hint">No model set up yet? Open Settings first to add a key.</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -156,7 +157,7 @@ body.ready .ready-wrap { display: block; }
|
||||
|
||||
function fmt(ms) {
|
||||
var s = Math.floor(ms / 1000);
|
||||
return '已等待 ' + s + ' 秒';
|
||||
return window.UClawI18n ? window.UClawI18n.t('loading.elapsed', { seconds: s }) : s + 's';
|
||||
}
|
||||
|
||||
function tickElapsed() {
|
||||
@@ -187,8 +188,10 @@ body.ready .ready-wrap { display: block; }
|
||||
// 兜底:5 分钟还没起来,提示去控制台
|
||||
setTimeout(function () {
|
||||
if (!ready) {
|
||||
var label = window.UClawI18n ? window.UClawI18n.t('loading.open_settings') : 'Settings';
|
||||
var slow = window.UClawI18n ? window.UClawI18n.t('loading.taking_long') : 'This is taking longer than usual. You can open Settings while you wait:';
|
||||
document.getElementById('status').innerHTML =
|
||||
'启动较慢,可先打开 <a class="manual" href="' + configCenter + '">配置中心</a> 检查';
|
||||
slow + ' <a class="manual" href="' + configCenter + '">' + label + '</a>';
|
||||
}
|
||||
}, 300000);
|
||||
})();
|
||||
|
||||
@@ -26,7 +26,7 @@ detect_install_mode() {
|
||||
|
||||
do_kill_gateway() {
|
||||
echo ""
|
||||
echo -e " ${CYAN}${BOLD}━━━ 杀死残留进程 ━━━${NC}"
|
||||
echo -e " ${CYAN}${BOLD}--- Stop leftover processes ---${NC}"
|
||||
echo ""
|
||||
|
||||
local FOUND=0
|
||||
@@ -42,7 +42,7 @@ do_kill_gateway() {
|
||||
if [ -n "$PIDS" ]; then
|
||||
for PID in $PIDS; do
|
||||
local CMD=$(ps -p "$PID" -o comm= 2>/dev/null || echo "unknown")
|
||||
echo -e " ${YELLOW}端口 $PORT: PID $PID ($CMD)${NC}"
|
||||
echo -e " ${YELLOW}Port $PORT: PID $PID ($CMD)${NC}"
|
||||
FOUND=1
|
||||
done
|
||||
fi
|
||||
@@ -52,20 +52,20 @@ do_kill_gateway() {
|
||||
local OC_PIDS=$(pgrep -f "openclaw.mjs gateway" 2>/dev/null)
|
||||
if [ -n "$OC_PIDS" ]; then
|
||||
for PID in $OC_PIDS; do
|
||||
echo -e " ${YELLOW}OpenClaw 网关进程: PID $PID${NC}"
|
||||
echo -e " ${YELLOW}OpenClaw gateway: PID $PID${NC}"
|
||||
FOUND=1
|
||||
done
|
||||
fi
|
||||
|
||||
if [ "$FOUND" = "0" ]; then
|
||||
echo -e " ${GREEN}没有发现残留进程${NC}"
|
||||
echo -e " ${GREEN}Nothing left running${NC}"
|
||||
return
|
||||
fi
|
||||
|
||||
echo ""
|
||||
read -p " 确认杀死以上进程? (y/N): " CONFIRM
|
||||
read -p " Stop these? (y/N): " CONFIRM
|
||||
if [ "$CONFIRM" != "y" ] && [ "$CONFIRM" != "Y" ]; then
|
||||
echo -e " ${YELLOW}已取消${NC}"
|
||||
echo -e " ${YELLOW}Cancelled${NC}"
|
||||
return
|
||||
fi
|
||||
|
||||
@@ -82,24 +82,24 @@ do_kill_gateway() {
|
||||
pgrep -f "openclaw.mjs gateway" 2>/dev/null | xargs kill 2>/dev/null
|
||||
|
||||
sleep 1
|
||||
echo -e " ${GREEN}进程已清理${NC}"
|
||||
echo -e " ${GREEN}Processes stopped${NC}"
|
||||
}
|
||||
|
||||
# ── [10] View/export/clean logs ──────────────────────────────
|
||||
|
||||
do_logs() {
|
||||
echo ""
|
||||
echo -e " ${CYAN}${BOLD}━━━ 日志管理 ━━━${NC}"
|
||||
echo -e " ${CYAN}${BOLD}--- Logs ---${NC}"
|
||||
echo ""
|
||||
|
||||
mkdir -p "$LOG_DIR"
|
||||
local LOG_FILE="$LOG_DIR/gateway.log"
|
||||
|
||||
echo -e " ${GREEN}[a]${NC} 查看最近日志(最后 50 行)"
|
||||
echo -e " ${GREEN}[b]${NC} 导出日志到桌面"
|
||||
echo -e " ${GREEN}[c]${NC} 清理 7 天前的旧日志"
|
||||
echo -e " ${GREEN}[a]${NC} Show the last 50 lines"
|
||||
echo -e " ${GREEN}[b]${NC} Copy the log to the desktop"
|
||||
echo -e " ${GREEN}[c]${NC} Delete logs older than 7 days"
|
||||
echo ""
|
||||
read -p " 选择 (a-c): " -n 1 LOG_CHOICE
|
||||
read -p " Choose (a-c): " -n 1 LOG_CHOICE
|
||||
echo ""
|
||||
echo ""
|
||||
|
||||
@@ -110,13 +110,13 @@ do_logs() {
|
||||
echo ""
|
||||
tail -50 "$LOG_FILE"
|
||||
else
|
||||
echo -e " ${YELLOW}日志文件不存在: $LOG_FILE${NC}"
|
||||
echo " 启动网关后会自动生成日志。"
|
||||
echo -e " ${YELLOW}No log file yet: $LOG_FILE${NC}"
|
||||
echo " One is created the first time U-Claw runs."
|
||||
fi
|
||||
;;
|
||||
b)
|
||||
if [ ! -f "$LOG_FILE" ]; then
|
||||
echo -e " ${YELLOW}没有日志可导出${NC}"
|
||||
echo -e " ${YELLOW}No log to copy${NC}"
|
||||
return
|
||||
fi
|
||||
local TS=$(date +%Y%m%d_%H%M%S)
|
||||
@@ -124,26 +124,26 @@ do_logs() {
|
||||
[ ! -d "$EXPORT_DIR" ] && EXPORT_DIR="$HOME"
|
||||
local EXPORT_FILE="$EXPORT_DIR/uclaw-logs-$TS.txt"
|
||||
cp "$LOG_FILE" "$EXPORT_FILE"
|
||||
echo -e " ${GREEN}日志已导出: $EXPORT_FILE${NC}"
|
||||
echo " 大小: $(du -sh "$EXPORT_FILE" | cut -f1)"
|
||||
echo -e " ${GREEN}Copied to: $EXPORT_FILE${NC}"
|
||||
echo " Size: $(du -sh "$EXPORT_FILE" | cut -f1)"
|
||||
;;
|
||||
c)
|
||||
local COUNT=$(find "$LOG_DIR" -name "*.log" -mtime +7 2>/dev/null | wc -l | tr -d ' ')
|
||||
if [ "$COUNT" = "0" ]; then
|
||||
echo -e " ${GREEN}没有需要清理的旧日志${NC}"
|
||||
echo -e " ${GREEN}No old logs to delete${NC}"
|
||||
return
|
||||
fi
|
||||
echo -e " 找到 ${YELLOW}$COUNT${NC} 个超过 7 天的日志文件"
|
||||
read -p " 确认清理? (y/N): " CONFIRM
|
||||
echo -e " Found ${YELLOW}$COUNT${NC} log files older than 7 days"
|
||||
read -p " Delete them? (y/N): " CONFIRM
|
||||
if [ "$CONFIRM" = "y" ] || [ "$CONFIRM" = "Y" ]; then
|
||||
find "$LOG_DIR" -name "*.log" -mtime +7 -delete 2>/dev/null
|
||||
echo -e " ${GREEN}旧日志已清理${NC}"
|
||||
echo -e " ${GREEN}Old logs deleted${NC}"
|
||||
else
|
||||
echo -e " ${YELLOW}已取消${NC}"
|
||||
echo -e " ${YELLOW}Cancelled${NC}"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo -e " ${RED}无效选择${NC}"
|
||||
echo -e " ${RED}Not a valid choice${NC}"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
@@ -152,46 +152,46 @@ do_logs() {
|
||||
|
||||
do_factory_reset() {
|
||||
echo ""
|
||||
echo -e " ${RED}${BOLD}━━━ 恢复出厂设置 ━━━${NC}"
|
||||
echo -e " ${RED}${BOLD}--- Reset to factory settings ---${NC}"
|
||||
echo ""
|
||||
echo -e " ${RED}警告: 此操作将删除所有配置和记忆数据!${NC}"
|
||||
echo -e " ${RED}This deletes every setting and everything the AI remembers.${NC}"
|
||||
echo ""
|
||||
echo " 将执行以下操作:"
|
||||
echo " 1. 自动备份当前配置和记忆"
|
||||
echo " 2. 删除配置文件 (openclaw.json)"
|
||||
echo " 3. 删除记忆数据 (memory/)"
|
||||
echo " 4. 恢复默认配置"
|
||||
echo " It will:"
|
||||
echo " 1. Back up your settings and memory first"
|
||||
echo " 2. Delete openclaw.json"
|
||||
echo " 3. Delete memory/"
|
||||
echo " 4. Put the defaults back"
|
||||
echo ""
|
||||
echo -e " ${YELLOW}请输入 RESET 确认(区分大小写):${NC}"
|
||||
echo -e " ${YELLOW}Type RESET to confirm (case-sensitive):${NC}"
|
||||
read -p " > " CONFIRM
|
||||
|
||||
if [ "$CONFIRM" != "RESET" ]; then
|
||||
echo -e " ${YELLOW}已取消${NC}"
|
||||
echo -e " ${YELLOW}Cancelled${NC}"
|
||||
return
|
||||
fi
|
||||
|
||||
# Step 1: Auto backup
|
||||
echo ""
|
||||
echo -e " ${CYAN}[1/4] 自动备份...${NC}"
|
||||
echo -e " ${CYAN}[1/4] Backing up…${NC}"
|
||||
local TS=$(date +%Y%m%d_%H%M%S)
|
||||
local BK="$BACKUP_DIR/pre-reset_$TS"
|
||||
mkdir -p "$BK"
|
||||
[ -f "$CONFIG_PATH" ] && cp "$CONFIG_PATH" "$BK/" 2>/dev/null
|
||||
[ -d "$DATA_DIR/memory" ] && cp -R "$DATA_DIR/memory" "$BK/" 2>/dev/null
|
||||
echo -e " ${GREEN}备份已保存: $BK${NC}"
|
||||
echo -e " ${GREEN}Backup saved to $BK${NC}"
|
||||
|
||||
# Step 2: Delete config
|
||||
echo -e " ${CYAN}[2/4] 删除配置...${NC}"
|
||||
echo -e " ${CYAN}[2/4] Removing settings…${NC}"
|
||||
rm -f "$CONFIG_PATH" 2>/dev/null
|
||||
rm -f "$DATA_DIR/config.json" 2>/dev/null
|
||||
|
||||
# Step 3: Delete memory
|
||||
echo -e " ${CYAN}[3/4] 清理记忆数据...${NC}"
|
||||
echo -e " ${CYAN}[3/4] Removing memory…${NC}"
|
||||
rm -rf "$DATA_DIR/memory" 2>/dev/null
|
||||
mkdir -p "$DATA_DIR/memory"
|
||||
|
||||
# Step 4: Restore default config
|
||||
echo -e " ${CYAN}[4/4] 恢复默认配置...${NC}"
|
||||
echo -e " ${CYAN}[4/4] Restoring defaults…${NC}"
|
||||
mkdir -p "$STATE_DIR"
|
||||
if [ -f "$DEFAULT_CONFIG" ]; then
|
||||
cp "$DEFAULT_CONFIG" "$CONFIG_PATH"
|
||||
@@ -207,75 +207,75 @@ CFGEOF
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e " ${GREEN}出厂设置已恢复!${NC}"
|
||||
echo -e " 备份位置: $BK"
|
||||
echo -e " 请重新运行配置向导 [1] 设置模型。"
|
||||
echo -e " ${GREEN}Reset complete.${NC}"
|
||||
echo -e " Your old settings are at $BK"
|
||||
echo -e " Run the setup wizard again to add a model."
|
||||
}
|
||||
|
||||
# ── [12] Uninstall ───────────────────────────────────────────
|
||||
|
||||
do_uninstall() {
|
||||
echo ""
|
||||
echo -e " ${RED}${BOLD}━━━ 卸载 U-Claw ━━━${NC}"
|
||||
echo -e " ${RED}${BOLD}--- Uninstall U-Claw ---${NC}"
|
||||
echo ""
|
||||
|
||||
local MODE=$(detect_install_mode)
|
||||
|
||||
case $MODE in
|
||||
installed)
|
||||
echo " 检测到: 已安装版本 (~/.uclaw/)"
|
||||
echo " Found: installed copy at ~/.uclaw/"
|
||||
echo ""
|
||||
echo " 将删除以下目录:"
|
||||
echo " These folders will be deleted:"
|
||||
echo -e " ${YELLOW}$HOME/.uclaw/${NC}"
|
||||
if [ -d "$HOME/.uclaw" ]; then
|
||||
echo " 大小: $(du -sh "$HOME/.uclaw" 2>/dev/null | cut -f1)"
|
||||
echo " Size: $(du -sh "$HOME/.uclaw" 2>/dev/null | cut -f1)"
|
||||
fi
|
||||
echo ""
|
||||
echo -e " ${RED}请输入 UNINSTALL 确认:${NC}"
|
||||
echo -e " ${RED}Type UNINSTALL to confirm:${NC}"
|
||||
read -p " > " CONFIRM
|
||||
if [ "$CONFIRM" != "UNINSTALL" ]; then
|
||||
echo -e " ${YELLOW}已取消${NC}"
|
||||
echo -e " ${YELLOW}Cancelled${NC}"
|
||||
return
|
||||
fi
|
||||
rm -rf "$HOME/.uclaw"
|
||||
echo -e " ${GREEN}卸载完成!${NC}"
|
||||
echo -e " ${GREEN}Uninstalled.${NC}"
|
||||
;;
|
||||
usb)
|
||||
echo " 检测到: U 盘模式"
|
||||
echo " Found: running from a USB drive"
|
||||
echo ""
|
||||
echo " 便携版不需要卸载 —— 只需:"
|
||||
echo " 1. 关闭所有 U-Claw 窗口"
|
||||
echo " 2. 安全弹出 U 盘"
|
||||
echo " 3. 如需清理电脑上的数据,删除 ~/.uclaw/ (如果存在)"
|
||||
echo " There is nothing to uninstall. Just:"
|
||||
echo " 1. Close every U-Claw window"
|
||||
echo " 2. Eject the drive"
|
||||
echo " 3. If you want nothing left on this computer, delete ~/.uclaw/"
|
||||
echo ""
|
||||
if [ -d "$HOME/.uclaw" ]; then
|
||||
echo -e " ${YELLOW}发现本机数据: ~/.uclaw/${NC}"
|
||||
echo " 大小: $(du -sh "$HOME/.uclaw" 2>/dev/null | cut -f1)"
|
||||
read -p " 是否删除本机数据? (y/N): " DEL
|
||||
echo -e " ${YELLOW}Found data on this computer: ~/.uclaw/${NC}"
|
||||
echo " Size: $(du -sh "$HOME/.uclaw" 2>/dev/null | cut -f1)"
|
||||
read -p " Delete it? (y/N): " DEL
|
||||
if [ "$DEL" = "y" ] || [ "$DEL" = "Y" ]; then
|
||||
rm -rf "$HOME/.uclaw"
|
||||
echo -e " ${GREEN}本机数据已清理${NC}"
|
||||
echo -e " ${GREEN}Deleted${NC}"
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
portable)
|
||||
echo " 检测到: 便携版本"
|
||||
echo " Found: portable copy"
|
||||
echo ""
|
||||
echo " 便携版不需要卸载 —— 直接删除文件夹即可:"
|
||||
echo " There is nothing to uninstall — just delete the folder:"
|
||||
echo -e " ${YELLOW}$UCLAW_DIR${NC}"
|
||||
echo ""
|
||||
if [ -d "$HOME/.uclaw" ]; then
|
||||
echo -e " ${YELLOW}发现本机数据: ~/.uclaw/${NC}"
|
||||
echo " 大小: $(du -sh "$HOME/.uclaw" 2>/dev/null | cut -f1)"
|
||||
read -p " 是否删除本机数据? (y/N): " DEL
|
||||
echo -e " ${YELLOW}Found data on this computer: ~/.uclaw/${NC}"
|
||||
echo " Size: $(du -sh "$HOME/.uclaw" 2>/dev/null | cut -f1)"
|
||||
read -p " Delete it? (y/N): " DEL
|
||||
if [ "$DEL" = "y" ] || [ "$DEL" = "Y" ]; then
|
||||
rm -rf "$HOME/.uclaw"
|
||||
echo -e " ${GREEN}本机数据已清理${NC}"
|
||||
echo -e " ${GREEN}Deleted${NC}"
|
||||
fi
|
||||
fi
|
||||
echo ""
|
||||
echo " Electron 桌面版卸载:"
|
||||
echo " macOS: 将 U-Claw.app 从「应用程序」拖到废纸篓"
|
||||
echo " Electron desktop app:"
|
||||
echo " macOS: drag U-Claw.app from Applications to the Trash"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
@@ -284,15 +284,15 @@ do_uninstall() {
|
||||
|
||||
do_update() {
|
||||
echo ""
|
||||
echo -e " ${CYAN}${BOLD}━━━ 检查更新 ━━━${NC}"
|
||||
echo -e " ${CYAN}${BOLD}--- Check for updates ---${NC}"
|
||||
echo ""
|
||||
|
||||
if [ ! -f "$NODE_BIN" ]; then
|
||||
echo -e " ${RED}Node.js 未找到,无法检查更新${NC}"
|
||||
echo -e " ${RED}Node.js is missing, so updates cannot be checked${NC}"
|
||||
return
|
||||
fi
|
||||
|
||||
echo -e " ${DIM}正在查询最新版本...${NC}"
|
||||
echo -e " ${DIM}Looking up the latest version…${NC}"
|
||||
|
||||
# Get current version
|
||||
local CUR_VER=""
|
||||
@@ -301,58 +301,58 @@ do_update() {
|
||||
fi
|
||||
|
||||
if [ -z "$CUR_VER" ]; then
|
||||
echo -e " ${RED}无法读取当前版本(OpenClaw 可能未安装)${NC}"
|
||||
echo -e " ${RED}Cannot read the installed version — OpenClaw may not be installed${NC}"
|
||||
return
|
||||
fi
|
||||
|
||||
# Get latest version from npmmirror
|
||||
local LATEST_VER=$(curl -s --connect-timeout 10 "https://registry.npmmirror.com/openclaw/latest" 2>/dev/null | "$NODE_BIN" -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{console.log(JSON.parse(d).version)}catch(e){console.log('error')}})" 2>/dev/null)
|
||||
# Get latest version from the npm registry
|
||||
local LATEST_VER=$(curl -s --connect-timeout 10 "https://registry.npmjs.org/openclaw/latest" 2>/dev/null | "$NODE_BIN" -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{console.log(JSON.parse(d).version)}catch(e){console.log('error')}})" 2>/dev/null)
|
||||
|
||||
if [ -z "$LATEST_VER" ] || [ "$LATEST_VER" = "error" ]; then
|
||||
echo -e " ${YELLOW}无法获取最新版本(网络问题?)${NC}"
|
||||
echo " 当前版本: $CUR_VER"
|
||||
echo -e " ${YELLOW}Could not reach the registry. Check your network.${NC}"
|
||||
echo " Installed: $CUR_VER"
|
||||
return
|
||||
fi
|
||||
|
||||
echo " 当前版本: $CUR_VER"
|
||||
echo " 最新版本: $LATEST_VER"
|
||||
echo " Installed: $CUR_VER"
|
||||
echo " Latest: $LATEST_VER"
|
||||
echo ""
|
||||
|
||||
if [ "$CUR_VER" = "$LATEST_VER" ]; then
|
||||
echo -e " ${GREEN}已是最新版本!${NC}"
|
||||
echo -e " ${GREEN}Already up to date.${NC}"
|
||||
return
|
||||
fi
|
||||
|
||||
echo -e " ${YELLOW}有新版本可用!${NC}"
|
||||
read -p " 是否立即升级? (y/N): " DO_UPDATE
|
||||
echo -e " ${YELLOW}A newer version is available.${NC}"
|
||||
read -p " Update now? (y/N): " DO_UPDATE
|
||||
if [ "$DO_UPDATE" != "y" ] && [ "$DO_UPDATE" != "Y" ]; then
|
||||
echo -e " ${YELLOW}已取消${NC}"
|
||||
echo -e " ${YELLOW}Cancelled${NC}"
|
||||
return
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e " ${CYAN}正在升级...${NC}"
|
||||
echo -e " ${CYAN}Updating…${NC}"
|
||||
cd "$CORE_DIR"
|
||||
"$NPM_BIN" install openclaw@latest --registry=https://registry.npmmirror.com 2>&1
|
||||
"$NPM_BIN" install openclaw@latest --registry=https://registry.npmjs.org 2>&1
|
||||
local NEW_VER=$("$NODE_BIN" -e "console.log(require('./node_modules/openclaw/package.json').version)" 2>/dev/null)
|
||||
echo ""
|
||||
echo -e " ${GREEN}升级完成!${NC} $CUR_VER → $NEW_VER"
|
||||
echo -e " ${GREEN}Updated.${NC} $CUR_VER → $NEW_VER"
|
||||
}
|
||||
|
||||
# ── [14] Disk cleanup (P1) ───────────────────────────────────
|
||||
|
||||
do_cleanup() {
|
||||
echo ""
|
||||
echo -e " ${CYAN}${BOLD}━━━ 清理空间 ━━━${NC}"
|
||||
echo -e " ${CYAN}${BOLD}--- Free up space ---${NC}"
|
||||
echo ""
|
||||
|
||||
# Show directory sizes
|
||||
echo -e " ${WHITE}目录占用:${NC}"
|
||||
echo -e " ${WHITE}Space used:${NC}"
|
||||
[ -d "$CORE_DIR/node_modules" ] && echo " node_modules: $(du -sh "$CORE_DIR/node_modules" 2>/dev/null | cut -f1)"
|
||||
[ -d "$DATA_DIR/memory" ] && echo " memory: $(du -sh "$DATA_DIR/memory" 2>/dev/null | cut -f1)"
|
||||
[ -d "$BACKUP_DIR" ] && echo " backups: $(du -sh "$BACKUP_DIR" 2>/dev/null | cut -f1)"
|
||||
[ -d "$LOG_DIR" ] && echo " logs: $(du -sh "$LOG_DIR" 2>/dev/null | cut -f1)"
|
||||
echo " 总计: $(du -sh "$UCLAW_DIR" 2>/dev/null | cut -f1)"
|
||||
echo " Total: $(du -sh "$UCLAW_DIR" 2>/dev/null | cut -f1)"
|
||||
echo ""
|
||||
|
||||
local CLEANED=0
|
||||
@@ -362,17 +362,17 @@ do_cleanup() {
|
||||
local BK_COUNT=$(ls -d "$BACKUP_DIR"/*/ 2>/dev/null | wc -l | tr -d ' ')
|
||||
if [ "$BK_COUNT" -gt 3 ]; then
|
||||
local OLD_COUNT=$((BK_COUNT - 3))
|
||||
echo -e " ${YELLOW}发现 $BK_COUNT 个备份,保留最新 3 个,删除 $OLD_COUNT 个旧备份${NC}"
|
||||
read -p " 确认? (y/N): " DEL_BK
|
||||
echo -e " ${YELLOW}Found $BK_COUNT backups. Keeping the newest 3, deleting $OLD_COUNT.${NC}"
|
||||
read -p " Go ahead? (y/N): " DEL_BK
|
||||
if [ "$DEL_BK" = "y" ] || [ "$DEL_BK" = "Y" ]; then
|
||||
ls -dt "$BACKUP_DIR"/*/ 2>/dev/null | tail -n "$OLD_COUNT" | while read -r DIR; do
|
||||
rm -rf "$DIR"
|
||||
done
|
||||
echo -e " ${GREEN}旧备份已清理${NC}"
|
||||
echo -e " ${GREEN}Old backups deleted${NC}"
|
||||
CLEANED=1
|
||||
fi
|
||||
else
|
||||
echo " 备份: ${BK_COUNT} 个(无需清理)"
|
||||
echo " Backups: ${BK_COUNT} — nothing to clear"
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -380,33 +380,33 @@ do_cleanup() {
|
||||
if [ -d "$LOG_DIR" ]; then
|
||||
local OLD_LOGS=$(find "$LOG_DIR" -name "*.log" -mtime +7 2>/dev/null | wc -l | tr -d ' ')
|
||||
if [ "$OLD_LOGS" -gt 0 ]; then
|
||||
echo -e " ${YELLOW}发现 $OLD_LOGS 个超过 7 天的日志${NC}"
|
||||
read -p " 确认清理? (y/N): " DEL_LOG
|
||||
echo -e " ${YELLOW}Found $OLD_LOGS log files older than 7 days${NC}"
|
||||
read -p " Delete them? (y/N): " DEL_LOG
|
||||
if [ "$DEL_LOG" = "y" ] || [ "$DEL_LOG" = "Y" ]; then
|
||||
find "$LOG_DIR" -name "*.log" -mtime +7 -delete 2>/dev/null
|
||||
echo -e " ${GREEN}旧日志已清理${NC}"
|
||||
echo -e " ${GREEN}Old logs deleted${NC}"
|
||||
CLEANED=1
|
||||
fi
|
||||
else
|
||||
echo " 日志: 无需清理"
|
||||
echo " Logs: nothing to clear"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Clean npm cache
|
||||
echo ""
|
||||
read -p " 是否清理 npm 缓存? (y/N): " DEL_CACHE
|
||||
read -p " Clear the npm cache? (y/N): " DEL_CACHE
|
||||
if [ "$DEL_CACHE" = "y" ] || [ "$DEL_CACHE" = "Y" ]; then
|
||||
"$NPM_BIN" cache clean --force 2>/dev/null
|
||||
echo -e " ${GREEN}npm 缓存已清理${NC}"
|
||||
echo -e " ${GREEN}npm cache cleared${NC}"
|
||||
CLEANED=1
|
||||
fi
|
||||
|
||||
if [ "$CLEANED" = "0" ]; then
|
||||
echo ""
|
||||
echo -e " ${GREEN}没有需要清理的内容${NC}"
|
||||
echo -e " ${GREEN}Nothing to clear${NC}"
|
||||
else
|
||||
echo ""
|
||||
echo " 清理后总计: $(du -sh "$UCLAW_DIR" 2>/dev/null | cut -f1)"
|
||||
echo " Now using: $(du -sh "$UCLAW_DIR" 2>/dev/null | cut -f1)"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -414,56 +414,56 @@ do_cleanup() {
|
||||
|
||||
do_plugins() {
|
||||
echo ""
|
||||
echo -e " ${CYAN}${BOLD}━━━ 插件管理 ━━━${NC}"
|
||||
echo -e " ${CYAN}${BOLD}--- Plugins ---${NC}"
|
||||
echo ""
|
||||
echo -e " ${GREEN}[a]${NC} 列出已安装插件"
|
||||
echo -e " ${GREEN}[b]${NC} 安装插件"
|
||||
echo -e " ${GREEN}[c]${NC} 卸载插件"
|
||||
echo -e " ${GREEN}[a]${NC} List installed plugins"
|
||||
echo -e " ${GREEN}[b]${NC} Install a plugin"
|
||||
echo -e " ${GREEN}[c]${NC} Remove a plugin"
|
||||
echo ""
|
||||
read -p " 选择 (a-c): " -n 1 PLG_CHOICE
|
||||
read -p " Choose (a-c): " -n 1 PLG_CHOICE
|
||||
echo ""
|
||||
echo ""
|
||||
|
||||
case $PLG_CHOICE in
|
||||
a)
|
||||
echo -e " ${WHITE}已安装插件:${NC}"
|
||||
echo -e " ${WHITE}Installed plugins:${NC}"
|
||||
echo ""
|
||||
run_oc plugins list 2>&1 || echo -e " ${YELLOW}无法获取插件列表${NC}"
|
||||
run_oc plugins list 2>&1 || echo -e " ${YELLOW}Could not list plugins${NC}"
|
||||
;;
|
||||
b)
|
||||
echo " 常用插件:"
|
||||
echo " @icesword760/openclaw-wechat — 微信"
|
||||
echo " @nicepkg/openclaw-plugin-qq — QQ(社区版)"
|
||||
echo " Commonly used:"
|
||||
echo " @icesword760/openclaw-wechat WeChat"
|
||||
echo " @nicepkg/openclaw-plugin-qq QQ (community build)"
|
||||
echo ""
|
||||
read -p " 输入插件名称(留空取消): " PLG_NAME
|
||||
read -p " Plugin name (blank to cancel): " PLG_NAME
|
||||
if [ -z "$PLG_NAME" ]; then
|
||||
echo -e " ${YELLOW}已取消${NC}"
|
||||
echo -e " ${YELLOW}Cancelled${NC}"
|
||||
return
|
||||
fi
|
||||
echo ""
|
||||
echo -e " ${CYAN}正在安装 $PLG_NAME ...${NC}"
|
||||
echo -e " ${CYAN}Installing $PLG_NAME…${NC}"
|
||||
run_oc plugins install "$PLG_NAME" 2>&1
|
||||
echo ""
|
||||
echo -e " ${GREEN}安装完成${NC}"
|
||||
echo -e " ${GREEN}Installed${NC}"
|
||||
;;
|
||||
c)
|
||||
echo -e " ${WHITE}已安装插件:${NC}"
|
||||
echo -e " ${WHITE}Installed plugins:${NC}"
|
||||
echo ""
|
||||
run_oc plugins list 2>&1 || true
|
||||
echo ""
|
||||
read -p " 输入要卸载的插件名称(留空取消): " PLG_NAME
|
||||
read -p " Plugin to remove (blank to cancel): " PLG_NAME
|
||||
if [ -z "$PLG_NAME" ]; then
|
||||
echo -e " ${YELLOW}已取消${NC}"
|
||||
echo -e " ${YELLOW}Cancelled${NC}"
|
||||
return
|
||||
fi
|
||||
echo ""
|
||||
echo -e " ${CYAN}正在卸载 $PLG_NAME ...${NC}"
|
||||
echo -e " ${CYAN}Removing $PLG_NAME…${NC}"
|
||||
run_oc plugins remove "$PLG_NAME" 2>&1
|
||||
echo ""
|
||||
echo -e " ${GREEN}卸载完成${NC}"
|
||||
echo -e " ${GREEN}Removed${NC}"
|
||||
;;
|
||||
*)
|
||||
echo -e " ${RED}无效选择${NC}"
|
||||
echo -e " ${RED}Not a valid choice${NC}"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
279
portable/lib/messages/en.json
Normal file
279
portable/lib/messages/en.json
Normal file
@@ -0,0 +1,279 @@
|
||||
{
|
||||
"start.banner": "U-Claw · Portable AI Agent",
|
||||
"start.node_version": "Node.js {version}",
|
||||
"start.quarantine_removing": "Clearing the macOS security flag…",
|
||||
"start.cache_local": "Cache on local disk: {path}",
|
||||
"start.config_migrating": "Bringing your old settings across…",
|
||||
"start.config_migrated": "Settings moved over.",
|
||||
"start.config_creating": "First run — setting things up…",
|
||||
"start.config_created": "Ready.",
|
||||
"start.deps_preparing": "Getting things ready — about 20 seconds.",
|
||||
"start.deps_slow_drive": "This drive is slow, so it may take a few minutes. You can leave it running.",
|
||||
"start.deps_done": "Ready.",
|
||||
"start.no_proxy": "Connecting directly to your model host: {hosts}",
|
||||
"start.wechat_installing": "Installing the WeChat plugin…",
|
||||
"start.wechat_installed": "WeChat plugin installed.",
|
||||
"start.config_center_starting": "Starting the Control Panel…",
|
||||
"start.config_center_port": "Control Panel is on port {port}.",
|
||||
"start.port_in_use": "Port {port} is busy, trying the next one…",
|
||||
"start.gateway_starting": "Starting U-Claw on port {port}…",
|
||||
"start.opening_screen": "Opening the start screen…",
|
||||
"start.opening_config": "Opening the Control Panel…",
|
||||
"start.running_title": "U-Claw is running",
|
||||
"start.running_dashboard": "Dashboard: {url}",
|
||||
"start.running_config": "Control Panel: {url}",
|
||||
"start.running_hint": "Keep this window open while you use U-Claw. Press Ctrl+C to stop.",
|
||||
"start.first_run_wait": "First run from a USB drive takes 30–90 seconds while components unpack. The Control Panel is already open so you can pick a model and paste a key.",
|
||||
"start.stopped": "U-Claw stopped.",
|
||||
"start.exited_unexpectedly": "U-Claw closed on its own (code {code}).",
|
||||
"error.node_missing.what": "A file U-Claw needs is missing from the drive.",
|
||||
"error.node_missing.why": "This usually means the copy onto the USB drive did not finish.",
|
||||
"error.node_missing.action": "Copy the U-Claw folder onto the drive again, or run setup.sh to rebuild it.",
|
||||
"error.unsupported_arch.what": "This Mac's processor is not supported ({arch}).",
|
||||
"error.unsupported_arch.why": "U-Claw ships builds for Apple Silicon and Intel Macs only.",
|
||||
"error.unsupported_arch.action": "Try U-Claw on a different computer.",
|
||||
"error.no_port.what": "U-Claw could not find a free port to start on.",
|
||||
"error.no_port.why": "Ports {from}–{to} are all in use — U-Claw may already be running.",
|
||||
"error.no_port.action": "Close any window already running U-Claw and start it again.",
|
||||
"error.deps_failed.what": "U-Claw could not finish getting ready.",
|
||||
"error.deps_failed.why": "The download did not complete — usually a dropped network connection.",
|
||||
"error.deps_failed.action": "Check your Wi-Fi and start U-Claw again.",
|
||||
"error.press_enter": "Press Enter to close this window.",
|
||||
"loading.title": "Starting U-Claw",
|
||||
"loading.starting": "Starting U-Claw…",
|
||||
"loading.first_run": "First run from a USB drive unpacks the bundled components. Give it a moment — you will be told when it is ready.",
|
||||
"loading.waiting_gateway": "Waiting for U-Claw to come up…",
|
||||
"loading.ready": "U-Claw is ready",
|
||||
"loading.choose_next": "Pick what to do next — start chatting, or set up your model and channels first.",
|
||||
"loading.open_dashboard": "🚀 Start chatting",
|
||||
"loading.open_settings": "⚙️ Settings",
|
||||
"loading.settings_hint": "Add or change your API key, and connect chat apps.",
|
||||
"loading.no_model_hint": "No model set up yet? Open Settings first to add a key.",
|
||||
"loading.slow_drive_note": "A slow drive usually takes 30–90 seconds. If nothing happens for a long time, open Settings:",
|
||||
"loading.elapsed": "Waited {seconds}s",
|
||||
"loading.taking_long": "This is taking longer than usual. You can open Settings while you wait:",
|
||||
"welcome.title": "U-Claw — Getting Started",
|
||||
"welcome.subtitle": "Your AI assistant on a USB drive · three steps",
|
||||
"welcome.quickstart": "Quick start",
|
||||
"welcome.step1_title": "1. Start U-Claw",
|
||||
"welcome.step1_desc": "Double-click the file for your computer:",
|
||||
"welcome.step2_title": "2. Add a key",
|
||||
"welcome.step2_desc": "Settings opens on its own the first time. You need one API key to get going — Google Gemini is the easiest to obtain: sign in with a Google account, no card required.",
|
||||
"welcome.step2_link": "Get a free Gemini key →",
|
||||
"welcome.step3_title": "3. Start talking",
|
||||
"welcome.step3_desc": "Once the key is saved, the chat window opens by itself. That is it.",
|
||||
"welcome.portable_note": "✅ Your settings and history stay in the data/ folder on the drive, so they travel with you between Mac and Windows.",
|
||||
"welcome.models_title": "Which model?",
|
||||
"welcome.model_claude": "Best all-rounder",
|
||||
"welcome.model_gpt": "Most widely used",
|
||||
"welcome.model_gemini": "Free tier, easiest signup",
|
||||
"welcome.model_local": "Offline · nothing leaves this machine",
|
||||
"welcome.models_more": "More options, including SEA-LION for Malay, Tamil and Singlish, are in Settings.",
|
||||
"welcome.trouble_title": "Something not working?",
|
||||
"welcome.trouble_lead": "💡 Run the check-up tool",
|
||||
"welcome.trouble_desc": "If U-Claw will not start, double-click:",
|
||||
"welcome.open_settings": "Open Settings",
|
||||
"welcome.open_docs": "Documentation",
|
||||
"nav.title": "U-Claw — Start here",
|
||||
"nav.tagline": "Portable AI agent — plug in and go",
|
||||
"nav.status_off": "U-Claw is not running",
|
||||
"nav.open_console": "Open it →",
|
||||
"nav.quickstart": "Quick start",
|
||||
"nav.doubleclick": "Double-click",
|
||||
"nav.step2": "Settings opens by itself the first time — pick a model and paste your key.",
|
||||
"nav.mac_gatekeeper": "⚠️ If macOS says the developer cannot be verified: right-click the file, then choose Open.",
|
||||
"nav.config_shortcut": "📝 You can also double-click Config.html — it takes you to the settings page.",
|
||||
"nav.win_smartscreen": "⚠️ If Windows shows a security warning: click More info, then Run anyway.",
|
||||
"nav.everything": "Everything else",
|
||||
"nav.menu_opens": "for the full menu:",
|
||||
"nav.g_settings": "Settings",
|
||||
"nav.g_settings_d": "Model, key and chat apps",
|
||||
"nav.g_checkup": "Check-up",
|
||||
"nav.g_checkup_d": "Finds and fixes common problems",
|
||||
"nav.g_backup": "Backup",
|
||||
"nav.g_backup_d": "Save settings and memory in one click",
|
||||
"nav.g_skills": "Skills",
|
||||
"nav.g_skills_d": "Browse what U-Claw can do",
|
||||
"nav.g_system": "System info",
|
||||
"nav.g_system_d": "Version, port, running state",
|
||||
"nav.g_cli": "Command line",
|
||||
"nav.g_cli_d": "For advanced use",
|
||||
"nav.models_title": "Models you can use",
|
||||
"nav.model_sealion": "Malay, Tamil and Singlish",
|
||||
"nav.model_local": "Local model",
|
||||
"nav.model_more": "More",
|
||||
"nav.model_more_d": "Others are listed in Settings",
|
||||
"nav.more_title": "More",
|
||||
"nav.link_guide": "Getting started",
|
||||
"nav.link_guide_d": "The three-step walkthrough",
|
||||
"nav.link_skills": "Skills",
|
||||
"nav.link_skills_d": "Browse what U-Claw can do",
|
||||
"nav.link_contact": "Contact",
|
||||
"nav.link_site": "Website",
|
||||
"nav.status_on": "U-Claw is running on port {port}",
|
||||
"nav.status_off_hint": "U-Claw is not running — double-click the start file first",
|
||||
"key.page_title": "U-Claw Settings",
|
||||
"key.page_subtitle": "Paste an API key and you are done.",
|
||||
"key.checking": "Checking…",
|
||||
"key.step_key": "Your key",
|
||||
"key.step_done": "Done",
|
||||
"key.heading": "Paste your API key",
|
||||
"key.desc": "U-Claw works out the rest by itself. Your key is saved on this drive only — it is never uploaded anywhere.",
|
||||
"key.placeholder": "sk-…",
|
||||
"key.show": "Show",
|
||||
"key.hide": "Hide",
|
||||
"key.continue": "Continue →",
|
||||
"key.checking_key": "Checking this key…",
|
||||
"key.ok": "✓ {provider} · connected in {ms}ms",
|
||||
"key.custom_provider": "Your endpoint",
|
||||
"key.no_key_title": "Do not have a key yet?",
|
||||
"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.",
|
||||
"key.get_gemini": "🔑 Get a free Gemini key",
|
||||
"key.get_openrouter": "🔑 OpenRouter — one key for many models",
|
||||
"key.advanced": "Advanced — set the endpoint manually",
|
||||
"key.base_url": "API endpoint",
|
||||
"key.model_name": "Model name",
|
||||
"key.advanced_hint": "Only needed for a self-hosted or unlisted provider. Leave blank to let U-Claw decide.",
|
||||
"key.saved": "Saved.",
|
||||
"key.err_empty": "Paste a key to continue.",
|
||||
"key.err_not_checked": "Hold on — still checking that key.",
|
||||
"key.err_rejected": "That key was not accepted. Check you copied all of it — keys are long and easy to cut short.",
|
||||
"key.err_quota": "This key has run out of credit. Top it up with your provider, or paste a different key.",
|
||||
"key.err_offline": "Cannot reach the internet. Check your Wi-Fi and try again.",
|
||||
"key.err_timeout": "The provider did not answer in time. Try again in a moment.",
|
||||
"key.err_model": "That key works, but the model it points to was not found. Set the model name under Advanced.",
|
||||
"key.err_provider_down": "The provider is having trouble right now. Try again shortly.",
|
||||
"key.err_unrecognised": "This key is not one U-Claw recognises. Fill in the endpoint and model under Advanced.",
|
||||
"key.err_unknown": "Could not check that key. Try again, or use Advanced to set the endpoint yourself.",
|
||||
"key.err_save": "Could not save. Make sure the drive is still plugged in, then try again.",
|
||||
"hub.title": "U-Claw Skills",
|
||||
"done.title": "You are set up",
|
||||
"done.subtitle": "Your model and key are saved on this drive.",
|
||||
"done.open": "🚀 Start chatting",
|
||||
"done.restart_note": "Chat app settings are saved now and take effect after U-Claw restarts.",
|
||||
"done.change_key": "Change model or key",
|
||||
"done.view_config": "View config file",
|
||||
"ch.title": "📱 Connect a chat app (optional)",
|
||||
"ch.desc": "Click a card to fill it in, or skip this entirely.",
|
||||
"ch.whatsapp_d": "Talk to U-Claw from WhatsApp",
|
||||
"ch.whatsapp_warning": "⚠️ This uses an unofficial protocol. Your WhatsApp account can be banned for it. Do not connect a number you rely on.",
|
||||
"ch.whatsapp_number": "Phone number",
|
||||
"ch.telegram_d": "Talk to U-Claw from Telegram",
|
||||
"ch.telegram_get": "→ Get one from @BotFather",
|
||||
"ch.slack_d": "Talk to U-Claw from your workspace",
|
||||
"ch.slack_get": "→ Create an app at api.slack.com",
|
||||
"ch.discord_d": "Talk to U-Claw from Discord",
|
||||
"ch.discord_get": "→ Create an app in the Discord developer portal",
|
||||
"ch.china_group": "Chat apps used mainly in China",
|
||||
"ch.wechat_d": "Scan a QR code to link a personal WeChat account",
|
||||
"ch.wechat_start": "Click this card to show the QR code",
|
||||
"ch.saved": "Chat app settings saved.",
|
||||
"ch.err_save": "Could not save the chat app settings. Check the drive is still plugged in.",
|
||||
"info.layout_title": "What is on the drive",
|
||||
"info.layout_desc": "How the folders are organised",
|
||||
"info.skills": "Skills",
|
||||
"info.skills_d": "Drop a SKILL.md in here and it is picked up automatically",
|
||||
"info.app": "Runtime",
|
||||
"info.app_d": "Node.js and the OpenClaw core",
|
||||
"info.data": "Your data",
|
||||
"info.data_d": "Your key and chat history. Stays on this drive.",
|
||||
"info.feedback_title": "Found a problem?",
|
||||
"info.feedback_desc": "U-Claw is open source. Report anything that breaks on GitHub — nothing is collected or uploaded from your machine automatically.",
|
||||
"info.feedback_link": "→ Open an issue on GitHub",
|
||||
"wx.getting_qr": "Fetching the QR code…",
|
||||
"wx.scan": "Scan the code above with WeChat",
|
||||
"wx.qr_failed": "Could not fetch the QR code",
|
||||
"wx.connected": "✅ WeChat connected",
|
||||
"wx.account": "Account",
|
||||
"wx.restart_needed": "One more thing: restart U-Claw (close it and open it again) before WeChat will work.",
|
||||
"wx.connected_toast": "WeChat connected. It works after U-Claw restarts.",
|
||||
"wx.qr_refreshed": "The code refreshed — scan it again",
|
||||
"wx.confirm_on_phone": "Scanned. Confirm on your phone…",
|
||||
"wx.qr_expired": "That code expired.",
|
||||
"wx.qr_retry": "Get a new one",
|
||||
"wx.failed": "Could not connect",
|
||||
"info.read_config_failed": "Could not read the config file:",
|
||||
"status.running": "U-Claw is running on port {port}",
|
||||
"status.open_dashboard": "Open it →",
|
||||
"status.not_running": "U-Claw is not running — start it with Windows-Start.bat or Mac-Start.command",
|
||||
"key.which_model": "Which model?",
|
||||
"start.opening_language": "Asking which language to use…",
|
||||
"persona.title": "What will you mostly use this for?",
|
||||
"persona.subtitle": "This sets up the right tools for you. You can change it later, and picking more than one is fine.",
|
||||
"persona.continue": "Continue →",
|
||||
"persona.skip": "Skip this",
|
||||
"persona.saving": "Setting things up…",
|
||||
"persona.err_offline": "U-Claw is not running yet. Start it, then open this page again.",
|
||||
"persona.err_save": "Could not save that. Check the drive is still plugged in, then try again.",
|
||||
"persona.developer": "Writing code",
|
||||
"persona.admin": "Documents and email",
|
||||
"persona.sales": "Talking to customers",
|
||||
"persona.marketing": "Content and social",
|
||||
"persona.finance": "Numbers and spreadsheets",
|
||||
"persona.boss": "Running a business",
|
||||
"persona.general": "Just having a look",
|
||||
"start.opening_persona": "Asking what you will use it for…",
|
||||
"tier.label": "How much do you want to see?",
|
||||
"tier.simple": "Just the essentials",
|
||||
"tier.standard": "A bit more control",
|
||||
"tier.expert": "Everything",
|
||||
"tier.changed_simple": "Switched to the simple view — advanced options are hidden.",
|
||||
"tier.changed_standard": "Switched to the standard view — you can now change models and connect more chat apps.",
|
||||
"tier.changed_expert": "Switched to the full view — everything is visible, including the config file.",
|
||||
"start.checking": "Something went wrong — checking…",
|
||||
"start.repaired": "Fixed. Trying again.",
|
||||
"start.diagnostics_written": "A report of what happened was saved to: {path}",
|
||||
"start.diagnostics_hint": "API keys have been removed from it. Send it to help@u-claw.org if you want a hand.",
|
||||
"skill.excel-helper.name": "Spreadsheets",
|
||||
"skill.excel-helper.desc": "Formulas, pivot tables, charts, cleaning messy data",
|
||||
"skill.word-writer.name": "Documents",
|
||||
"skill.word-writer.desc": "Reports, proposals, CVs — drafting and formatting",
|
||||
"skill.ppt-designer.name": "Slide decks",
|
||||
"skill.ppt-designer.desc": "Structure, layout and speaker notes",
|
||||
"skill.pdf-toolkit.name": "PDF tools",
|
||||
"skill.pdf-toolkit.desc": "Merge, split, extract text — all on this machine",
|
||||
"skill.image-compress.name": "Images",
|
||||
"skill.image-compress.desc": "Shrink, resize and convert — nothing is uploaded",
|
||||
"skill.qrcode-maker.name": "QR codes",
|
||||
"skill.qrcode-maker.desc": "Links, text and WiFi details, generated offline",
|
||||
"skill.web-to-markdown.name": "Save a web page",
|
||||
"skill.web-to-markdown.desc": "Turn any article into clean Markdown",
|
||||
"skill.linkedin-post.name": "LinkedIn posts",
|
||||
"skill.linkedin-post.desc": "Hooks and structure, without the LinkedIn voice",
|
||||
"skill.x-poster.name": "Posts for X",
|
||||
"skill.x-poster.desc": "Short posts and threads that stand on their own",
|
||||
"skill.tiktok-script.name": "TikTok scripts",
|
||||
"skill.tiktok-script.desc": "Hooks that survive the first second",
|
||||
"skill.youtube-script.name": "YouTube scripts",
|
||||
"skill.youtube-script.desc": "Titles, the first 30 seconds, and chapters",
|
||||
"skill.medium-writer.name": "Articles",
|
||||
"skill.medium-writer.desc": "Long-form structure, and cutting the padding",
|
||||
"skill.email-campaign.name": "Email campaigns",
|
||||
"skill.email-campaign.desc": "Subject lines, one clear ask, PDPA-safe",
|
||||
"skill.web-search.name": "Web search",
|
||||
"skill.web-search.desc": "Find current information and read the sources",
|
||||
"skill.sg-weather.name": "Singapore weather",
|
||||
"skill.sg-weather.desc": "Forecast by area and air quality, from NEA",
|
||||
"skill.sea-translate.name": "SEA translation",
|
||||
"skill.sea-translate.desc": "English, Chinese, Malay, Tamil — and register",
|
||||
"skill.claude-helper.name": "Better answers",
|
||||
"skill.claude-helper.desc": "How to ask, and when to check the answer",
|
||||
"skill.sg-transport.name": "Singapore transport",
|
||||
"skill.sg-transport.desc": "Bus arrivals, MRT alerts, carpark lots",
|
||||
"skill.meeting-notes.name": "Meeting notes",
|
||||
"skill.meeting-notes.desc": "Decisions and owners, ready to send",
|
||||
"cat.office": "Office",
|
||||
"cat.data": "Data",
|
||||
"cat.writing": "Writing",
|
||||
"cat.files": "Files",
|
||||
"cat.research": "Research",
|
||||
"cat.social": "Social",
|
||||
"cat.video": "Video",
|
||||
"cat.language": "Language",
|
||||
"cat.local": "Singapore",
|
||||
"cat.ai": "Using AI",
|
||||
"hub.lede": "These are on this drive already. Just ask for what you want — you do not have to name the skill.",
|
||||
"hub.all": "All",
|
||||
"hub.none": "Nothing in this category.",
|
||||
"hub.add_note": "Skills live in the skills/ folder on this drive. Dropping a SKILL.md in there adds one."
|
||||
}
|
||||
279
portable/lib/messages/zh-CN.json
Normal file
279
portable/lib/messages/zh-CN.json
Normal file
@@ -0,0 +1,279 @@
|
||||
{
|
||||
"start.banner": "U-Claw · 便携 AI 助手",
|
||||
"start.node_version": "Node.js {version}",
|
||||
"start.quarantine_removing": "正在解除 macOS 安全限制…",
|
||||
"start.cache_local": "缓存已放在本机硬盘:{path}",
|
||||
"start.config_migrating": "正在迁移你原来的设置…",
|
||||
"start.config_migrated": "设置已迁移。",
|
||||
"start.config_creating": "第一次使用,正在准备…",
|
||||
"start.config_created": "准备好了。",
|
||||
"start.deps_preparing": "正在准备,约 20 秒。",
|
||||
"start.deps_slow_drive": "这个 U 盘比较慢,可能要几分钟,放着就行。",
|
||||
"start.deps_done": "准备好了。",
|
||||
"start.no_proxy": "将直连你的模型地址:{hosts}",
|
||||
"start.wechat_installing": "正在安装微信插件…",
|
||||
"start.wechat_installed": "微信插件已安装。",
|
||||
"start.config_center_starting": "正在启动控制台…",
|
||||
"start.config_center_port": "控制台端口:{port}",
|
||||
"start.port_in_use": "端口 {port} 被占用,换下一个…",
|
||||
"start.gateway_starting": "正在启动 U-Claw,端口 {port}…",
|
||||
"start.opening_screen": "正在打开启动页…",
|
||||
"start.opening_config": "正在打开控制台…",
|
||||
"start.running_title": "U-Claw 已启动",
|
||||
"start.running_dashboard": "对话界面:{url}",
|
||||
"start.running_config": "控制台: {url}",
|
||||
"start.running_hint": "使用期间请不要关闭这个窗口。按 Ctrl+C 停止。",
|
||||
"start.first_run_wait": "第一次从 U 盘启动需要 30–90 秒解压组件。控制台已经打开,可以先选模型、填密钥。",
|
||||
"start.stopped": "U-Claw 已停止。",
|
||||
"start.exited_unexpectedly": "U-Claw 自己退出了(代码 {code})。",
|
||||
"error.node_missing.what": "U 盘里少了一个 U-Claw 需要的文件。",
|
||||
"error.node_missing.why": "通常是拷贝到 U 盘时没有复制完整。",
|
||||
"error.node_missing.action": "把 U-Claw 文件夹重新拷贝一遍,或运行 setup.sh 重建。",
|
||||
"error.unsupported_arch.what": "这台 Mac 的处理器不受支持({arch})。",
|
||||
"error.unsupported_arch.why": "U-Claw 只提供 Apple Silicon 和 Intel Mac 的版本。",
|
||||
"error.unsupported_arch.action": "换一台电脑试试。",
|
||||
"error.no_port.what": "U-Claw 找不到可用的端口来启动。",
|
||||
"error.no_port.why": "{from}–{to} 都被占用了 —— 可能已经有一个 U-Claw 在运行。",
|
||||
"error.no_port.action": "关掉已经在运行的那个窗口,再启动一次。",
|
||||
"error.deps_failed.what": "U-Claw 没能准备完成。",
|
||||
"error.deps_failed.why": "下载没有完成,通常是网络中断了。",
|
||||
"error.deps_failed.action": "检查一下 Wi-Fi,然后重新启动 U-Claw。",
|
||||
"error.press_enter": "按回车关闭这个窗口。",
|
||||
"loading.title": "正在启动 U-Claw",
|
||||
"loading.starting": "U-Claw 正在启动…",
|
||||
"loading.first_run": "第一次从 U 盘启动需要展开内置组件,稍等片刻,就绪后会提示你下一步。",
|
||||
"loading.waiting_gateway": "正在等待 U-Claw 启动…",
|
||||
"loading.ready": "U-Claw 已就绪",
|
||||
"loading.choose_next": "选择下一步 —— 直接开始聊天,或先去配置模型和渠道。",
|
||||
"loading.open_dashboard": "🚀 开始聊天",
|
||||
"loading.open_settings": "⚙️ 设置",
|
||||
"loading.settings_hint": "填写或更换 API 密钥,连接聊天软件。",
|
||||
"loading.no_model_hint": "还没配过模型?先打开设置填一个密钥。",
|
||||
"loading.slow_drive_note": "慢 U 盘首次启动通常 30–90 秒。如果迟迟没反应,可以先打开设置:",
|
||||
"loading.elapsed": "已等待 {seconds} 秒",
|
||||
"loading.taking_long": "启动比平时慢,可以先打开设置:",
|
||||
"welcome.title": "U-Claw 使用指南",
|
||||
"welcome.subtitle": "装在 U 盘里的 AI 助手 · 三步开始",
|
||||
"welcome.quickstart": "快速开始",
|
||||
"welcome.step1_title": "1. 启动 U-Claw",
|
||||
"welcome.step1_desc": "双击你电脑对应的那个文件:",
|
||||
"welcome.step2_title": "2. 填一个密钥",
|
||||
"welcome.step2_desc": "第一次启动会自动打开设置页。你需要一个 API 密钥才能开始 —— Google Gemini 最容易拿到:用 Google 账号登录即可,不用绑卡。",
|
||||
"welcome.step2_link": "去领取免费的 Gemini 密钥 →",
|
||||
"welcome.step3_title": "3. 开始对话",
|
||||
"welcome.step3_desc": "密钥保存后,对话窗口会自己打开。就这样。",
|
||||
"welcome.portable_note": "✅ 你的设置和聊天记录都存在盘上的 data/ 目录里,跟着盘走,Mac 和 Windows 通用。",
|
||||
"welcome.models_title": "用哪个模型?",
|
||||
"welcome.model_claude": "综合最强",
|
||||
"welcome.model_gpt": "用的人最多",
|
||||
"welcome.model_gemini": "有免费额度,注册最简单",
|
||||
"welcome.model_local": "离线 · 数据不出本机",
|
||||
"welcome.models_more": "更多选择在设置里,包括适合马来语、泰米尔语和 Singlish 的 SEA-LION。",
|
||||
"welcome.trouble_title": "遇到问题?",
|
||||
"welcome.trouble_lead": "💡 运行检查工具",
|
||||
"welcome.trouble_desc": "如果 U-Claw 启动不了,双击:",
|
||||
"welcome.open_settings": "打开设置",
|
||||
"welcome.open_docs": "查看文档",
|
||||
"nav.title": "U-Claw 启动导航",
|
||||
"nav.tagline": "便携 AI 助手 — 插上就能用",
|
||||
"nav.status_off": "U-Claw 尚未运行",
|
||||
"nav.open_console": "打开它 →",
|
||||
"nav.quickstart": "快速启动",
|
||||
"nav.doubleclick": "双击",
|
||||
"nav.step2": "第一次启动会自动打开设置页 —— 选一个模型,把密钥粘进去。",
|
||||
"nav.mac_gatekeeper": "⚠️ 如果 macOS 提示「无法验证开发者」:右键点这个文件,选「打开」。",
|
||||
"nav.config_shortcut": "📝 也可以双击 Config.html,它会带你到设置页。",
|
||||
"nav.win_smartscreen": "⚠️ 如果 Windows 弹安全警告:点「更多信息」,再点「仍要运行」。",
|
||||
"nav.everything": "其他功能",
|
||||
"nav.menu_opens": "打开完整菜单:",
|
||||
"nav.g_settings": "设置",
|
||||
"nav.g_settings_d": "模型、密钥和聊天软件",
|
||||
"nav.g_checkup": "检查修复",
|
||||
"nav.g_checkup_d": "找出并修复常见问题",
|
||||
"nav.g_backup": "备份",
|
||||
"nav.g_backup_d": "一键保存设置和记忆",
|
||||
"nav.g_skills": "技能",
|
||||
"nav.g_skills_d": "看看 U-Claw 能做什么",
|
||||
"nav.g_system": "系统信息",
|
||||
"nav.g_system_d": "版本、端口、运行状态",
|
||||
"nav.g_cli": "命令行",
|
||||
"nav.g_cli_d": "给进阶用户",
|
||||
"nav.models_title": "可以用的模型",
|
||||
"nav.model_sealion": "马来语、泰米尔语和 Singlish",
|
||||
"nav.model_local": "本地模型",
|
||||
"nav.model_more": "更多",
|
||||
"nav.model_more_d": "其他模型在设置里",
|
||||
"nav.more_title": "更多",
|
||||
"nav.link_guide": "使用指南",
|
||||
"nav.link_guide_d": "三步上手",
|
||||
"nav.link_skills": "技能",
|
||||
"nav.link_skills_d": "看看 U-Claw 能做什么",
|
||||
"nav.link_contact": "联系我们",
|
||||
"nav.link_site": "官网",
|
||||
"nav.status_on": "U-Claw 正在运行(端口 {port})",
|
||||
"nav.status_off_hint": "U-Claw 尚未运行 —— 请先双击启动文件",
|
||||
"key.page_title": "U-Claw 设置",
|
||||
"key.page_subtitle": "粘贴一个 API 密钥就可以了。",
|
||||
"key.checking": "检测中…",
|
||||
"key.step_key": "填密钥",
|
||||
"key.step_done": "完成",
|
||||
"key.heading": "粘贴你的 API 密钥",
|
||||
"key.desc": "剩下的 U-Claw 自己搞定。密钥只保存在这个 U 盘上,不会上传到任何地方。",
|
||||
"key.placeholder": "sk-…",
|
||||
"key.show": "显示",
|
||||
"key.hide": "隐藏",
|
||||
"key.continue": "继续 →",
|
||||
"key.checking_key": "正在检查这个密钥…",
|
||||
"key.ok": "✓ {provider} · 连接成功,用时 {ms} 毫秒",
|
||||
"key.custom_provider": "你的地址",
|
||||
"key.no_key_title": "还没有密钥?",
|
||||
"key.no_key_desc": "Google Gemini 最容易拿到:用 Google 账号登录即可,不用绑卡,还有免费额度。",
|
||||
"key.get_gemini": "🔑 领取免费的 Gemini 密钥",
|
||||
"key.get_openrouter": "🔑 OpenRouter —— 一个密钥用多种模型",
|
||||
"key.advanced": "高级 —— 手动填写接口地址",
|
||||
"key.base_url": "接口地址",
|
||||
"key.model_name": "模型名称",
|
||||
"key.advanced_hint": "只有自建或未收录的服务才需要填。留空则由 U-Claw 自动判断。",
|
||||
"key.saved": "已保存。",
|
||||
"key.err_empty": "先粘贴一个密钥。",
|
||||
"key.err_not_checked": "稍等,密钥还在检查中。",
|
||||
"key.err_rejected": "这个密钥没被接受。检查一下是不是没复制全 —— 密钥很长,容易少一截。",
|
||||
"key.err_quota": "这个密钥的额度用完了。去服务商那边充值,或者换一个密钥。",
|
||||
"key.err_offline": "连不上网络。检查一下 Wi-Fi 再试。",
|
||||
"key.err_timeout": "服务商没有及时响应。过一会儿再试。",
|
||||
"key.err_model": "密钥是好的,但它指向的模型没找到。在「高级」里填写模型名称。",
|
||||
"key.err_provider_down": "服务商那边现在有问题。稍后再试。",
|
||||
"key.err_unrecognised": "U-Claw 认不出这个密钥。请在「高级」里填写接口地址和模型名称。",
|
||||
"key.err_unknown": "没能检查这个密钥。再试一次,或者在「高级」里自己填接口地址。",
|
||||
"key.err_save": "保存失败。确认 U 盘还插着,然后再试一次。",
|
||||
"hub.title": "U-Claw 技能",
|
||||
"done.title": "配置完成",
|
||||
"done.subtitle": "模型和密钥已保存在这个 U 盘上。",
|
||||
"done.open": "🚀 开始聊天",
|
||||
"done.restart_note": "聊天软件的设置已保存,重启 U-Claw 后生效。",
|
||||
"done.change_key": "换模型或密钥",
|
||||
"done.view_config": "查看配置文件",
|
||||
"ch.title": "📱 连接聊天软件(可选)",
|
||||
"ch.desc": "点卡片展开填写,也可以直接跳过。",
|
||||
"ch.whatsapp_d": "用 WhatsApp 和 U-Claw 对话",
|
||||
"ch.whatsapp_warning": "⚠️ 这里用的是非官方协议,你的 WhatsApp 账号可能因此被封。不要接入你离不开的号码。",
|
||||
"ch.whatsapp_number": "手机号",
|
||||
"ch.telegram_d": "用 Telegram 和 U-Claw 对话",
|
||||
"ch.telegram_get": "→ 从 @BotFather 获取",
|
||||
"ch.slack_d": "在工作区里和 U-Claw 对话",
|
||||
"ch.slack_get": "→ 在 api.slack.com 创建应用",
|
||||
"ch.discord_d": "用 Discord 和 U-Claw 对话",
|
||||
"ch.discord_get": "→ 在 Discord 开发者后台创建应用",
|
||||
"ch.china_group": "主要在中国使用的聊天软件",
|
||||
"ch.wechat_d": "扫码接入个人微信",
|
||||
"ch.wechat_start": "点这张卡片开始扫码",
|
||||
"ch.saved": "聊天软件设置已保存。",
|
||||
"ch.err_save": "聊天软件设置没能保存。确认 U 盘还插着。",
|
||||
"info.layout_title": "U 盘里有什么",
|
||||
"info.layout_desc": "文件夹是怎么组织的",
|
||||
"info.skills": "技能",
|
||||
"info.skills_d": "把 SKILL.md 放进来就会自动加载",
|
||||
"info.app": "运行时",
|
||||
"info.app_d": "Node.js 和 OpenClaw 核心",
|
||||
"info.data": "你的数据",
|
||||
"info.data_d": "密钥和聊天记录,只留在这个盘上。",
|
||||
"info.feedback_title": "遇到问题?",
|
||||
"info.feedback_desc": "U-Claw 是开源项目。出问题欢迎去 GitHub 提 Issue —— 我们不会自动收集或上传你机器上的任何东西。",
|
||||
"info.feedback_link": "→ 去 GitHub 提 Issue",
|
||||
"wx.getting_qr": "正在获取二维码…",
|
||||
"wx.scan": "请用微信扫描上方二维码",
|
||||
"wx.qr_failed": "获取二维码失败",
|
||||
"wx.connected": "✅ 微信已连接",
|
||||
"wx.account": "账号",
|
||||
"wx.restart_needed": "还差一步:重启 U-Claw(关掉再打开),微信才能用。",
|
||||
"wx.connected_toast": "微信已连接,重启 U-Claw 后生效。",
|
||||
"wx.qr_refreshed": "二维码已刷新,请重新扫码",
|
||||
"wx.confirm_on_phone": "已扫码,请在手机上确认…",
|
||||
"wx.qr_expired": "二维码已过期。",
|
||||
"wx.qr_retry": "重新获取",
|
||||
"wx.failed": "连接失败",
|
||||
"info.read_config_failed": "读取配置文件失败:",
|
||||
"status.running": "U-Claw 正在运行,端口 {port}",
|
||||
"status.open_dashboard": "打开它 →",
|
||||
"status.not_running": "U-Claw 尚未运行 —— 请先运行 Windows-Start.bat 或 Mac-Start.command",
|
||||
"key.which_model": "用哪个模型?",
|
||||
"start.opening_language": "正在询问使用哪种语言…",
|
||||
"persona.title": "你主要用它做什么?",
|
||||
"persona.subtitle": "这会帮你把合适的工具装好。以后可以改,也可以多选。",
|
||||
"persona.continue": "继续 →",
|
||||
"persona.skip": "跳过",
|
||||
"persona.saving": "正在准备…",
|
||||
"persona.err_offline": "U-Claw 还没启动。先启动它,再打开这个页面。",
|
||||
"persona.err_save": "没能保存。确认 U 盘还插着,然后再试一次。",
|
||||
"persona.developer": "写代码",
|
||||
"persona.admin": "写文档、发邮件",
|
||||
"persona.sales": "跟客户打交道",
|
||||
"persona.marketing": "做内容、发社交媒体",
|
||||
"persona.finance": "算数、做表格",
|
||||
"persona.boss": "管公司",
|
||||
"persona.general": "先随便看看",
|
||||
"start.opening_persona": "正在询问你主要用它做什么…",
|
||||
"tier.label": "想看到多少?",
|
||||
"tier.simple": "只要最基本的",
|
||||
"tier.standard": "多一点控制",
|
||||
"tier.expert": "全部",
|
||||
"tier.changed_simple": "已切换到简洁模式 —— 高级选项已隐藏。",
|
||||
"tier.changed_standard": "已切换到标准模式 —— 现在可以换模型、接更多聊天软件了。",
|
||||
"tier.changed_expert": "已切换到完整模式 —— 所有东西都可见,包括配置文件。",
|
||||
"start.checking": "出了点问题,正在检查…",
|
||||
"start.repaired": "已修复,重试一次。",
|
||||
"start.diagnostics_written": "已把情况记录到:{path}",
|
||||
"start.diagnostics_hint": "里面的密钥已被去掉。需要帮忙的话,把它发到 help@u-claw.org。",
|
||||
"skill.excel-helper.name": "表格",
|
||||
"skill.excel-helper.desc": "公式、透视表、图表、清洗乱数据",
|
||||
"skill.word-writer.name": "文档",
|
||||
"skill.word-writer.desc": "报告、方案、简历 —— 起草和排版",
|
||||
"skill.ppt-designer.name": "幻灯片",
|
||||
"skill.ppt-designer.desc": "结构、版式和演讲要点",
|
||||
"skill.pdf-toolkit.name": "PDF 工具",
|
||||
"skill.pdf-toolkit.desc": "合并、拆分、提取文字 —— 全在本机",
|
||||
"skill.image-compress.name": "图片",
|
||||
"skill.image-compress.desc": "压缩、改尺寸、转格式 —— 不上传",
|
||||
"skill.qrcode-maker.name": "二维码",
|
||||
"skill.qrcode-maker.desc": "网址、文本、WiFi,离线生成",
|
||||
"skill.web-to-markdown.name": "保存网页",
|
||||
"skill.web-to-markdown.desc": "把文章转成干净的 Markdown",
|
||||
"skill.linkedin-post.name": "LinkedIn 帖子",
|
||||
"skill.linkedin-post.desc": "钩子和结构,不要那股 LinkedIn 腔",
|
||||
"skill.x-poster.name": "X 帖子",
|
||||
"skill.x-poster.desc": "能独立成立的短帖和 thread",
|
||||
"skill.tiktok-script.name": "TikTok 脚本",
|
||||
"skill.tiktok-script.desc": "扛得住第一秒的钩子",
|
||||
"skill.youtube-script.name": "YouTube 脚本",
|
||||
"skill.youtube-script.desc": "标题、前 30 秒、章节",
|
||||
"skill.medium-writer.name": "长文",
|
||||
"skill.medium-writer.desc": "长文结构,以及怎么删水分",
|
||||
"skill.email-campaign.name": "邮件营销",
|
||||
"skill.email-campaign.desc": "标题、一个明确的 ask、符合 PDPA",
|
||||
"skill.web-search.name": "网页搜索",
|
||||
"skill.web-search.desc": "找当前信息并真的读来源",
|
||||
"skill.sg-weather.name": "新加坡天气",
|
||||
"skill.sg-weather.desc": "按区预报和空气质量,来自 NEA",
|
||||
"skill.sea-translate.name": "东南亚翻译",
|
||||
"skill.sea-translate.desc": "英 / 中 / 马来 / 泰米尔 —— 以及语域",
|
||||
"skill.claude-helper.name": "问得更好",
|
||||
"skill.claude-helper.desc": "怎么问,以及什么时候该核对答案",
|
||||
"skill.sg-transport.name": "新加坡交通",
|
||||
"skill.sg-transport.desc": "巴士到站、地铁故障、停车位",
|
||||
"skill.meeting-notes.name": "会议纪要",
|
||||
"skill.meeting-notes.desc": "决定和负责人,写完就能发",
|
||||
"cat.office": "办公",
|
||||
"cat.data": "数据",
|
||||
"cat.writing": "写作",
|
||||
"cat.files": "文件",
|
||||
"cat.research": "资料",
|
||||
"cat.social": "社交媒体",
|
||||
"cat.video": "视频",
|
||||
"cat.language": "语言",
|
||||
"cat.local": "新加坡",
|
||||
"cat.ai": "用好 AI",
|
||||
"hub.lede": "这些已经在盘上了。直接说你想做什么就行,不用记技能名。",
|
||||
"hub.all": "全部",
|
||||
"hub.none": "这个分类下暂时没有。",
|
||||
"hub.add_note": "技能放在盘上的 skills/ 目录里。丢一个 SKILL.md 进去就多一个。"
|
||||
}
|
||||
153
portable/lib/persona.html
Normal file
153
portable/lib/persona.html
Normal file
@@ -0,0 +1,153 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>U-Claw</title>
|
||||
<script src="../data/.openclaw/locale.js"></script>
|
||||
<script src="i18n/messages.js"></script>
|
||||
<script src="i18n/i18n.js"></script>
|
||||
<!--
|
||||
First-run: one question instead of ten settings.
|
||||
The answer decides three things at once — which skills get installed, how much
|
||||
interface to show, and which chat app to suggest — so the user never has to
|
||||
meet the words "skill", "tier" or "channel".
|
||||
Cards are illustrated and short because this is asked before anyone has any
|
||||
idea what the product does.
|
||||
-->
|
||||
<style>
|
||||
:root { color-scheme: dark; }
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0; min-height: 100vh; padding: 3rem 1.5rem;
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2rem;
|
||||
font-family: -apple-system, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
background: #16181c; color: #f2f4f7;
|
||||
}
|
||||
h1 { margin: 0; font-size: 1.6rem; font-weight: 600; text-align: center; }
|
||||
.sub { margin: 0; color: #9aa0a8; text-align: center; max-width: 34rem; line-height: 1.6; }
|
||||
.grid {
|
||||
display: grid; grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
|
||||
gap: 1rem; width: 100%; max-width: 44rem;
|
||||
}
|
||||
.card {
|
||||
padding: 1.5rem 1rem; text-align: center; font: inherit; color: inherit;
|
||||
background: #23262c; border: 2px solid #33373f; border-radius: 14px;
|
||||
cursor: pointer; transition: border-color .15s, transform .15s;
|
||||
}
|
||||
.card:hover, .card:focus-visible { border-color: #ff6b35; transform: translateY(-2px); outline: none; }
|
||||
.card[aria-pressed="true"] { border-color: #ff6b35; background: rgba(255,107,53,.08); }
|
||||
.card .icon { font-size: 2rem; display: block; margin-bottom: .5rem; }
|
||||
.card .name { font-weight: 600; }
|
||||
.actions { display: flex; gap: 1rem; align-items: center; flex-wrap: wrap; justify-content: center; }
|
||||
button.go {
|
||||
padding: .9rem 2.5rem; font: inherit; font-weight: 600; font-size: 1.05rem;
|
||||
color: #16181c; background: #ff6b35; border: none; border-radius: 10px; cursor: pointer;
|
||||
}
|
||||
button.go:disabled { background: #3a3d44; color: #6f757d; cursor: not-allowed; }
|
||||
.skip { color: #9aa0a8; background: none; border: none; font: inherit; cursor: pointer; text-decoration: underline; }
|
||||
.status { color: #9aa0a8; min-height: 1.4em; font-size: .9rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1 data-i18n="persona.title">What will you mostly use this for?</h1>
|
||||
<p class="sub" data-i18n="persona.subtitle">This sets up the right tools for you. You can change it later, and picking more than one is fine.</p>
|
||||
|
||||
<div class="grid" id="grid"></div>
|
||||
|
||||
<div class="actions">
|
||||
<button class="go" id="go" disabled data-i18n="persona.continue">Continue →</button>
|
||||
<button class="skip" id="skip" data-i18n="persona.skip">Skip this</button>
|
||||
</div>
|
||||
<p class="status" id="status"></p>
|
||||
|
||||
<script>
|
||||
var t = function (k, v) { return window.UClawI18n ? window.UClawI18n.t(k, v) : k; };
|
||||
var PORT_FROM = 18788, PORT_TO = 18798;
|
||||
var selected = [];
|
||||
var serverPort = null;
|
||||
|
||||
async function findServer() {
|
||||
for (var port = PORT_FROM; port <= PORT_TO; port++) {
|
||||
try {
|
||||
await fetch('http://127.0.0.1:' + port + '/api/config', { cache: 'no-store' });
|
||||
return port;
|
||||
} catch (e) { /* nothing listening here */ }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function card(persona) {
|
||||
var el = document.createElement('button');
|
||||
el.className = 'card';
|
||||
el.type = 'button';
|
||||
el.setAttribute('aria-pressed', 'false');
|
||||
el.innerHTML = '<span class="icon">' + persona.emoji + '</span>'
|
||||
+ '<span class="name">' + t('persona.' + persona.id) + '</span>';
|
||||
el.addEventListener('click', function () {
|
||||
var at = selected.indexOf(persona.id);
|
||||
if (at === -1) { selected.push(persona.id); el.setAttribute('aria-pressed', 'true'); }
|
||||
else { selected.splice(at, 1); el.setAttribute('aria-pressed', 'false'); }
|
||||
document.getElementById('go').disabled = selected.length === 0;
|
||||
});
|
||||
return el;
|
||||
}
|
||||
|
||||
// The wizard is driven by skills/manifest.json, so adding a persona there is
|
||||
// enough — no page edit needed.
|
||||
async function load() {
|
||||
serverPort = await findServer();
|
||||
if (serverPort === null) {
|
||||
document.getElementById('status').textContent = t('persona.err_offline');
|
||||
return;
|
||||
}
|
||||
var personas = await (await fetch('http://127.0.0.1:' + serverPort + '/api/personas')).json();
|
||||
var grid = document.getElementById('grid');
|
||||
personas.forEach(function (p) { grid.appendChild(card(p)); });
|
||||
}
|
||||
|
||||
// Highest tier wins when several personas are picked: showing a developer the
|
||||
// simple interface would be worse than showing an admin one extra menu.
|
||||
var TIER_ORDER = ['simple', 'standard', 'expert'];
|
||||
function combinedTier(personas, chosen) {
|
||||
var best = 'simple';
|
||||
personas.forEach(function (p) {
|
||||
if (chosen.indexOf(p.id) !== -1 && TIER_ORDER.indexOf(p.tier) > TIER_ORDER.indexOf(best)) best = p.tier;
|
||||
});
|
||||
return best;
|
||||
}
|
||||
|
||||
async function finish(chosen) {
|
||||
var base = 'http://127.0.0.1:' + serverPort;
|
||||
document.getElementById('go').disabled = true;
|
||||
document.getElementById('status').textContent = t('persona.saving');
|
||||
try {
|
||||
var personas = await (await fetch(base + '/api/personas')).json();
|
||||
var config = await (await fetch(base + '/api/config')).json();
|
||||
config.uclaw = Object.assign({}, config.uclaw, {
|
||||
personas: chosen,
|
||||
tier: combinedTier(personas, chosen),
|
||||
});
|
||||
await fetch(base + '/api/config', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(config),
|
||||
});
|
||||
// Best-effort: a failed skill install should not block getting to Settings.
|
||||
for (var i = 0; i < chosen.length; i++) {
|
||||
await fetch(base + '/api/install-skills', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ persona: chosen[i], locale: (window.UClawI18n || {}).locale || 'en' }),
|
||||
}).catch(function () {});
|
||||
}
|
||||
location.replace(base + '/');
|
||||
} catch (e) {
|
||||
document.getElementById('status').textContent = t('persona.err_save');
|
||||
document.getElementById('go').disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('go').addEventListener('click', function () { finish(selected); });
|
||||
document.getElementById('skip').addEventListener('click', function () { finish(['general']); });
|
||||
load();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
87
portable/lib/provider-detect.mjs
Normal file
87
portable/lib/provider-detect.mjs
Normal file
@@ -0,0 +1,87 @@
|
||||
// Works out which provider an API key belongs to, so users never have to know
|
||||
// what a Base URL is.
|
||||
//
|
||||
// The old flow asked for three things — Base URL, model name and key — on the
|
||||
// first screen a non-technical user ever saw. Every key in practice announces
|
||||
// its own provider through its prefix, so ask for the key and derive the rest.
|
||||
|
||||
// Order matters: the longest, most specific prefixes are checked first, because
|
||||
// several providers issue keys that also start with "sk-".
|
||||
const PROVIDERS = [
|
||||
{
|
||||
id: 'anthropic',
|
||||
label: 'Anthropic Claude',
|
||||
prefixes: ['sk-ant-'],
|
||||
baseUrl: 'https://api.anthropic.com/v1',
|
||||
model: 'claude-sonnet-4-20250514',
|
||||
models: ['claude-sonnet-4-20250514', 'claude-opus-4-20250514', 'claude-haiku-4-5-20251001'],
|
||||
},
|
||||
{
|
||||
id: 'openrouter',
|
||||
label: 'OpenRouter',
|
||||
prefixes: ['sk-or-v1-', 'sk-or-'],
|
||||
baseUrl: 'https://openrouter.ai/api/v1',
|
||||
model: 'anthropic/claude-sonnet-4',
|
||||
models: ['anthropic/claude-sonnet-4', 'openai/gpt-4o', 'google/gemini-2.5-flash'],
|
||||
},
|
||||
{
|
||||
id: 'google',
|
||||
label: 'Google Gemini',
|
||||
prefixes: ['AIza'],
|
||||
baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai',
|
||||
model: 'gemini-2.5-flash',
|
||||
models: ['gemini-2.5-flash', 'gemini-2.5-pro'],
|
||||
},
|
||||
{
|
||||
id: 'groq',
|
||||
label: 'Groq',
|
||||
prefixes: ['gsk_'],
|
||||
baseUrl: 'https://api.groq.com/openai/v1',
|
||||
model: 'llama-3.3-70b-versatile',
|
||||
models: ['llama-3.3-70b-versatile', 'llama-3.1-8b-instant'],
|
||||
},
|
||||
{
|
||||
id: 'sealion',
|
||||
label: 'SEA-LION',
|
||||
prefixes: ['sk-sealion-'],
|
||||
baseUrl: 'https://api.sea-lion.ai/v1',
|
||||
model: 'aisingapore/Qwen-SEA-LION-v4.5-27B-IT',
|
||||
models: ['aisingapore/Qwen-SEA-LION-v4.5-27B-IT', 'aisingapore/Llama-SEA-LION-v3.5-70B-R'],
|
||||
},
|
||||
{
|
||||
id: 'openai',
|
||||
label: 'OpenAI',
|
||||
prefixes: ['sk-proj-', 'sk-svcacct-', 'sk-'],
|
||||
baseUrl: 'https://api.openai.com/v1',
|
||||
model: 'gpt-4o',
|
||||
models: ['gpt-4o', 'gpt-4o-mini', 'o3-mini'],
|
||||
},
|
||||
];
|
||||
|
||||
export function detectProvider(rawKey) {
|
||||
const key = String(rawKey ?? '').trim();
|
||||
if (!key) return null;
|
||||
for (const provider of PROVIDERS) {
|
||||
if (provider.prefixes.some((prefix) => key.startsWith(prefix))) {
|
||||
return { id: provider.id, label: provider.label, baseUrl: provider.baseUrl, model: provider.model, models: provider.models };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Maps a failed request to a message key the UI can translate. Every branch has
|
||||
// to name something the user can actually do next — "401 Unauthorized" on its
|
||||
// own tells a non-technical user nothing.
|
||||
export function classifyFailure({ status, code } = {}) {
|
||||
if (code === 'ENOTFOUND' || code === 'EAI_AGAIN' || code === 'ECONNREFUSED') return 'key.err_offline';
|
||||
if (code === 'ETIMEDOUT' || code === 'UND_ERR_CONNECT_TIMEOUT' || code === 'ABORT_ERR') return 'key.err_timeout';
|
||||
if (status === 401 || status === 403) return 'key.err_rejected';
|
||||
if (status === 429) return 'key.err_quota';
|
||||
if (status === 404) return 'key.err_model';
|
||||
if (typeof status === 'number' && status >= 500) return 'key.err_provider_down';
|
||||
return 'key.err_unknown';
|
||||
}
|
||||
|
||||
export function listProviders() {
|
||||
return PROVIDERS.map(({ id, label, baseUrl, model, models }) => ({ id, label, baseUrl, model, models }));
|
||||
}
|
||||
@@ -21,7 +21,7 @@ const REPO_ROOT = resolve(__dirname, '../..');
|
||||
|
||||
// 默认 OSS 路径前缀(你可以改成自己的 bucket / domain)
|
||||
const DEFAULT_OSS_BASE = 'https://u-claw-oss.56chat.cn/u-claw-open';
|
||||
const GH_RELEASES_BASE = 'https://github.com/dongsheng123132/u-claw/releases';
|
||||
const GH_RELEASES_BASE = 'https://gitea.fanghe.it.com/zhenghy/u-claw/releases';
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = { notes: '', ossBase: DEFAULT_OSS_BASE };
|
||||
|
||||
203
portable/lib/self-heal.mjs
Normal file
203
portable/lib/self-heal.mjs
Normal file
@@ -0,0 +1,203 @@
|
||||
// Checks and repairs that run when startup fails, before bothering the user.
|
||||
//
|
||||
// Diagnose used to be a tool people had to know existed: when startup broke, you
|
||||
// were expected to work out that Windows-Diagnose.bat was the answer. Anyone who
|
||||
// did not know that had already given up. These checks now run by themselves,
|
||||
// and the user sees "checking…" rather than a stack trace.
|
||||
//
|
||||
// Two rules for anything added here:
|
||||
// 1. A repair must be safe to run when nothing is wrong. Startup can fail for
|
||||
// reasons none of these explain, and every check will run anyway.
|
||||
// 2. Nothing here may destroy user data. Files that look wrong get moved
|
||||
// aside with a timestamp, never deleted.
|
||||
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, rmSync, readdirSync, statSync, copyFileSync } from 'node:fs';
|
||||
import { createServer } from 'node:net';
|
||||
import { join, dirname } from 'node:path';
|
||||
|
||||
function portBusy(port) {
|
||||
return new Promise((done) => {
|
||||
const probe = createServer();
|
||||
probe.once('error', () => done(true));
|
||||
probe.once('listening', () => probe.close(() => done(false)));
|
||||
probe.listen(port, '127.0.0.1');
|
||||
});
|
||||
}
|
||||
|
||||
// Moving aside beats deleting: if the diagnosis was wrong, the user still has
|
||||
// their file. The stamp comes from the caller so this stays deterministic.
|
||||
function setAside(path, stamp) {
|
||||
const target = `${path}.broken-${stamp}`;
|
||||
try { renameSync(path, target); return target; } catch { return null; }
|
||||
}
|
||||
|
||||
export function buildChecks({ paths, defaultConfigPath, portRange, stamp }) {
|
||||
return [
|
||||
{
|
||||
id: 'data-dirs',
|
||||
repair() {
|
||||
const wanted = [paths.state, join(paths.data, 'memory'), join(paths.data, 'backups'), join(paths.data, 'logs')];
|
||||
const missing = wanted.filter((dir) => !existsSync(dir));
|
||||
if (missing.length === 0) return null;
|
||||
missing.forEach((dir) => mkdirSync(dir, { recursive: true }));
|
||||
return `created ${missing.length} missing folder(s)`;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'config-readable',
|
||||
repair() {
|
||||
if (!existsSync(paths.config)) return null;
|
||||
try {
|
||||
JSON.parse(readFileSync(paths.config, 'utf8'));
|
||||
return null;
|
||||
} catch {
|
||||
// A half-written openclaw.json stops the gateway dead, and the message
|
||||
// it produces means nothing to a non-technical user.
|
||||
const moved = setAside(paths.config, stamp);
|
||||
if (defaultConfigPath && existsSync(defaultConfigPath)) copyFileSync(defaultConfigPath, paths.config);
|
||||
else writeFileSync(paths.config, `${JSON.stringify({ gateway: { mode: 'local', auth: { token: 'uclaw' } } }, null, 2)}\n`, 'utf8');
|
||||
return moved ? `settings file was damaged, kept a copy at ${moved}` : 'rebuilt the settings file';
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'stale-runtime',
|
||||
async repair() {
|
||||
if (!existsSync(paths.runtimeJson)) return null;
|
||||
let port = null;
|
||||
try { port = JSON.parse(readFileSync(paths.runtimeJson, 'utf8'))?.configServerPort; } catch { /* unreadable */ }
|
||||
// Left behind by a crash or an unplugged drive; it makes the launcher
|
||||
// wait on a port nothing is listening on.
|
||||
if (port && (await portBusy(port))) return null;
|
||||
try { rmSync(paths.runtimeJson, { force: true }); } catch { return null; }
|
||||
return 'cleared a leftover port record';
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'openclaw-present',
|
||||
repair() {
|
||||
const modules = join(paths.core, 'node_modules');
|
||||
if (!existsSync(modules)) return null;
|
||||
// node_modules exists but the entry point does not: a copy onto the
|
||||
// drive that stopped part-way. Removing it makes startup reinstall.
|
||||
if (existsSync(join(modules, 'openclaw', 'openclaw.mjs'))) return null;
|
||||
const moved = setAside(modules, stamp);
|
||||
return moved ? 'the copy on this drive was incomplete, will fetch it again' : null;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'broken-cache-link',
|
||||
repair() {
|
||||
const browser = join(paths.state, 'browser');
|
||||
if (!existsSync(browser)) return null;
|
||||
try {
|
||||
statSync(browser); // follows the link; throws when the target is gone
|
||||
return null;
|
||||
} catch {
|
||||
// portable-cache points this at local disk. Plug the drive into a
|
||||
// different machine and the link dangles, which breaks the browser.
|
||||
try { rmSync(browser, { force: true, recursive: true }); } catch { return null; }
|
||||
return 'cleared a cache link left by another computer';
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'ports-free',
|
||||
async repair() {
|
||||
const busy = [];
|
||||
for (let port = portRange.from; port <= portRange.to; port++) {
|
||||
if (await portBusy(port)) busy.push(port);
|
||||
}
|
||||
// Not repairable — killing a process we did not start is not ours to do.
|
||||
// Reported so the message can say "it may already be running".
|
||||
if (busy.length <= portRange.to - portRange.from) return null;
|
||||
return null;
|
||||
},
|
||||
report: async () => {
|
||||
const busy = [];
|
||||
for (let port = portRange.from; port <= portRange.to; port++) {
|
||||
if (await portBusy(port)) busy.push(port);
|
||||
}
|
||||
return busy.length ? `ports in use: ${busy.join(', ')}` : 'all ports free';
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export async function runRepairs(checks) {
|
||||
const applied = [];
|
||||
for (const check of checks) {
|
||||
try {
|
||||
const result = await check.repair();
|
||||
if (result) applied.push({ id: check.id, result });
|
||||
} catch (error) {
|
||||
// A failing check must never be the reason startup stops.
|
||||
applied.push({ id: check.id, result: `check failed: ${error.message}` });
|
||||
}
|
||||
}
|
||||
return applied;
|
||||
}
|
||||
|
||||
// Written when the repairs did not help. Everything a support conversation would
|
||||
// ask for, with the secrets taken out — the user should be able to read the file
|
||||
// themselves before deciding to send it.
|
||||
export async function writeDiagnostics({ paths, checks, applied, versions, stamp, extra = {} }) {
|
||||
const redact = (value) => (typeof value === 'string' && value.length > 8 ? `${value.slice(0, 6)}…redacted` : '***');
|
||||
|
||||
let config = 'not readable';
|
||||
try {
|
||||
const parsed = JSON.parse(readFileSync(paths.config, 'utf8'));
|
||||
const walk = (node) => {
|
||||
if (!node || typeof node !== 'object') return node;
|
||||
if (Array.isArray(node)) return node.map(walk);
|
||||
return Object.fromEntries(Object.entries(node).map(([key, value]) =>
|
||||
/key|token|secret|password/i.test(key) ? [key, redact(value)] : [key, walk(value)]));
|
||||
};
|
||||
config = JSON.stringify(walk(parsed), null, 2);
|
||||
} catch (error) {
|
||||
config = `not readable: ${error.message}`;
|
||||
}
|
||||
|
||||
const listing = (dir) => {
|
||||
try { return readdirSync(dir).sort().join(', ') || '(empty)'; } catch { return '(missing)'; }
|
||||
};
|
||||
|
||||
const reports = [];
|
||||
for (const check of checks) {
|
||||
if (typeof check.report !== 'function') continue;
|
||||
try { reports.push(`${check.id}: ${await check.report()}`); } catch { /* skip */ }
|
||||
}
|
||||
|
||||
const lines = [
|
||||
'U-Claw diagnostics',
|
||||
`Written: ${stamp}`,
|
||||
'',
|
||||
'API keys and tokens have been replaced with "redacted" — you can read this',
|
||||
'file before you send it anywhere.',
|
||||
'',
|
||||
'## System',
|
||||
`platform: ${process.platform} ${process.arch}`,
|
||||
`node: ${process.version}`,
|
||||
...Object.entries(versions).map(([key, value]) => `${key}: ${value ?? 'unknown'}`),
|
||||
...Object.entries(extra).map(([key, value]) => `${key}: ${value}`),
|
||||
'',
|
||||
'## Repairs attempted',
|
||||
applied.length ? applied.map((a) => `- ${a.id}: ${a.result}`).join('\n') : '- none needed',
|
||||
'',
|
||||
'## Checks',
|
||||
reports.length ? reports.map((r) => `- ${r}`).join('\n') : '- none',
|
||||
'',
|
||||
'## Folders',
|
||||
`drive root: ${listing(dirname(paths.data))}`,
|
||||
`data/.openclaw: ${listing(paths.state)}`,
|
||||
`app/core/node_modules: ${listing(join(paths.core, 'node_modules'))}`,
|
||||
'',
|
||||
'## Settings (redacted)',
|
||||
config,
|
||||
'',
|
||||
];
|
||||
|
||||
const target = join(paths.state, 'diagnostics.txt');
|
||||
writeFileSync(target, lines.join('\n'), 'utf8');
|
||||
return target;
|
||||
}
|
||||
@@ -1,14 +1,21 @@
|
||||
// setup-local-model.mjs — 内网/本地模型一键配置(不碰 Control UI)
|
||||
// setup-local-model.mjs — set up a local or self-hosted model from the command line.
|
||||
//
|
||||
// 给"内网/离线"场景:用纯命令行问几个问题,直接写好 openclaw.json,
|
||||
// 再当场实测能不能连上、能不能回话。全程不依赖会挂的 dashboard 网页。
|
||||
// 支持两类本地/内网模型:
|
||||
// 1) Ollama(本机,http://127.0.0.1:11434)
|
||||
// 2) newapi / 任意 OpenAI 兼容中转(内网 IP + token)
|
||||
// For offline and corporate-network situations: ask a few questions, write
|
||||
// openclaw.json directly, then actually test that the model answers. Deliberately
|
||||
// does not depend on the dashboard, which is the thing that tends to be broken
|
||||
// when someone reaches for this tool.
|
||||
//
|
||||
// 写入只 merge 模型相关字段,保留 gateway 等原有配置;写前自动备份。
|
||||
// Two kinds of endpoint:
|
||||
// 1) Ollama on this machine (http://127.0.0.1:11434)
|
||||
// 2) Any OpenAI-compatible endpoint — a company server, a relay (URL + token)
|
||||
//
|
||||
// 用法:node setup-local-model.mjs <CONFIG_PATH>
|
||||
// Only the model-related fields are merged in; gateway and other settings are
|
||||
// left alone, and the previous file is backed up first.
|
||||
//
|
||||
// Usage: node setup-local-model.mjs <CONFIG_PATH>
|
||||
//
|
||||
// Input: a real terminal gets interactive readline; piped input (tests, scripts)
|
||||
// is read once and dequeued line by line.
|
||||
|
||||
import { readFileSync, writeFileSync, existsSync, copyFileSync, mkdirSync } from 'node:fs';
|
||||
import { dirname } from 'node:path';
|
||||
@@ -17,7 +24,6 @@ import { stdin as input, stdout as output } from 'node:process';
|
||||
import http from 'node:http';
|
||||
import https from 'node:https';
|
||||
|
||||
// 输入抽象:真控制台(TTY)走交互式 readline;被管道喂入(测试/脚本)则一次读完按行出队。
|
||||
function makePrompter() {
|
||||
if (input.isTTY) {
|
||||
const rl = createInterface({ input, output });
|
||||
@@ -69,7 +75,7 @@ async function chatTest(baseUrl, apiKey, modelId) {
|
||||
const res = await requestText(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}) },
|
||||
body: JSON.stringify({ model: modelId, messages: [{ role: 'user', content: '请回复四个字:连接成功' }], max_tokens: 64, stream: false }),
|
||||
body: JSON.stringify({ model: modelId, messages: [{ role: 'user', content: 'Reply with exactly: connection ok' }], max_tokens: 64, stream: false }),
|
||||
timeoutMs: CHAT_TIMEOUT_MS,
|
||||
});
|
||||
const ms = Date.now() - started;
|
||||
@@ -111,10 +117,10 @@ function requestText(rawUrl, { method = 'GET', headers = {}, body, timeoutMs })
|
||||
|
||||
async function main() {
|
||||
const configPath = process.argv[2] || process.env.OPENCLAW_CONFIG_PATH;
|
||||
if (!configPath) { line('用法: node setup-local-model.mjs <CONFIG_PATH>'); process.exitCode = 2; return; }
|
||||
if (!configPath) { line('Usage: node setup-local-model.mjs <CONFIG_PATH>'); process.exitCode = 2; return; }
|
||||
|
||||
line('========================================');
|
||||
line(' U-Claw 内网/本地模型 一键配置');
|
||||
line(' U-Claw — set up a local or self-hosted model');
|
||||
line('========================================');
|
||||
line('');
|
||||
|
||||
@@ -122,32 +128,32 @@ async function main() {
|
||||
const ask = rl.ask;
|
||||
|
||||
try {
|
||||
line('选择模型类型:');
|
||||
line(' 1) Ollama(本机部署,http://127.0.0.1:11434)');
|
||||
line(' 2) newapi / 其它 OpenAI 兼容中转(内网 IP + token)');
|
||||
const kind = await ask('输入 1 或 2', '1');
|
||||
line('Which kind of model?');
|
||||
line(' 1) Ollama running on this machine (http://127.0.0.1:11434)');
|
||||
line(' 2) A self-hosted or company endpoint (any OpenAI-compatible URL + token)');
|
||||
const kind = await ask('Enter 1 or 2', '1');
|
||||
|
||||
let providerKey, baseUrl, apiKey, modelId;
|
||||
if (kind === '2') {
|
||||
providerKey = 'newapi';
|
||||
line('');
|
||||
line('提示:baseUrl 通常形如 http://192.168.1.50:3000/v1(注意大多要带 /v1)');
|
||||
baseUrl = await ask('newapi 地址 baseUrl', 'http://192.168.1.50:3000/v1');
|
||||
line('The endpoint usually looks like http://192.168.1.50:3000/v1 — most need the /v1 on the end.');
|
||||
baseUrl = await ask('Endpoint URL', 'http://192.168.1.50:3000/v1');
|
||||
apiKey = await ask('token / API Key', '');
|
||||
modelId = await ask('模型 ID(管理员给的,如 deepseek-v3)', '');
|
||||
modelId = await ask('Model ID — your admin will have given you this, e.g. deepseek-v3', '');
|
||||
} else {
|
||||
providerKey = 'ollama';
|
||||
line('');
|
||||
baseUrl = await ask('Ollama 地址(一般本机默认即可)', 'http://127.0.0.1:11434/v1');
|
||||
// Ollama 的 OpenAI 兼容端点在 /v1;自动补上
|
||||
baseUrl = await ask('Ollama URL — the default is right unless you changed it', 'http://127.0.0.1:11434/v1');
|
||||
// Ollama's OpenAI-compatible endpoint lives at /v1; add it if missing
|
||||
if (!/\/v1\/?$/.test(baseUrl)) baseUrl = baseUrl.replace(/\/+$/, '') + '/v1';
|
||||
apiKey = 'ollama'; // 本地占位 key,任意值即可
|
||||
modelId = await ask('模型名(先用 ollama list 查,如 qwen2.5 / llama3.1)', 'qwen2.5');
|
||||
apiKey = 'ollama'; // Ollama ignores auth; any placeholder works
|
||||
modelId = await ask('Model name — run `ollama list` to see yours, e.g. qwen2.5', 'qwen2.5');
|
||||
}
|
||||
|
||||
if (!baseUrl || !modelId) { line(''); line('地址或模型 ID 为空,已取消。'); process.exitCode = 2; return; }
|
||||
if (!baseUrl || !modelId) { line(''); line('No endpoint or model ID given — nothing was changed.'); process.exitCode = 2; return; }
|
||||
|
||||
// 读取并合并现有配置(保留 gateway 等),写前备份
|
||||
// Merge into the existing config so gateway and other settings survive; back up first
|
||||
let config = {};
|
||||
if (existsSync(configPath)) {
|
||||
try { config = JSON.parse(readFileSync(configPath, 'utf8')); }
|
||||
@@ -174,39 +180,39 @@ async function main() {
|
||||
|
||||
writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
|
||||
line('');
|
||||
line(`✓ 已写入配置:${configPath}`);
|
||||
line(`✓ Saved to ${configPath}`);
|
||||
line(` provider=${providerKey} baseUrl=${baseUrl} model=${modelId}`);
|
||||
if (existsSync(configPath + '.bak')) line(` (原配置已备份为 openclaw.json.bak)`);
|
||||
if (existsSync(configPath + '.bak')) line(' Your previous settings were kept as openclaw.json.bak');
|
||||
line('');
|
||||
|
||||
// 当场实测
|
||||
line('正在实测:发一条对话给模型...');
|
||||
// Prove it actually works rather than just claiming it was saved
|
||||
line('Testing it — sending one message to the model…');
|
||||
const r = await chatTest(baseUrl, apiKey, modelId);
|
||||
line('');
|
||||
if (r.ok) {
|
||||
line(`✓✓ 跑通了!模型回复 (${r.ms}ms):${r.reply.slice(0, 120) || '(空回复但请求成功)'}`);
|
||||
line(`✓ It works. The model replied in ${r.ms}ms: ${r.reply.slice(0, 120) || '(empty reply, but the request succeeded)'}`);
|
||||
line('');
|
||||
line('配置完成。现在双击 Windows-Start.bat 即可正常使用(对话可走 CLI 或 Dashboard)。');
|
||||
line('You are set. Start U-Claw normally and it will use this model.');
|
||||
} else if (r.status) {
|
||||
line(`✗ 服务端 HTTP ${r.status} (${r.ms}ms):${r.body}`);
|
||||
line(`✗ The server answered with HTTP ${r.status} in ${r.ms}ms: ${r.body}`);
|
||||
line('');
|
||||
if (r.status === 401 || r.status === 403) line('→ 网络通,但 token / key 不对(Ollama 可忽略鉴权,newapi 请核对 token)。');
|
||||
else if (r.status === 404) line('→ 网络通,但路径或模型 ID 不对(检查 baseUrl 是否要带 /v1、模型名是否正确)。');
|
||||
else line('→ 网络通,服务端报错,把上面内容发管理员。');
|
||||
if (r.status === 401 || r.status === 403) line(' The network is fine but the token was rejected. Ollama ignores auth entirely; for a company endpoint, check the token with whoever issued it.');
|
||||
else if (r.status === 404) line(' The network is fine but the path or model ID is wrong. Check whether the endpoint needs /v1 on the end, and that the model name matches exactly.');
|
||||
else line(' The network is fine; the server itself errored. Send the text above to whoever runs it.');
|
||||
} else {
|
||||
const host = (() => { try { return new URL(withScheme(baseUrl)).hostname; } catch { return baseUrl; } })();
|
||||
line(`✗ 连不上:${r.error} (${r.ms}ms)`);
|
||||
line(`✗ Could not reach it: ${r.error} (${r.ms}ms)`);
|
||||
line('');
|
||||
if (providerKey === 'ollama') {
|
||||
line('→ 本机 Ollama 没连上,多半是 Ollama 没启动或模型没拉。请在本机执行:');
|
||||
line(' ollama serve (启动服务,若已是后台服务可跳过)');
|
||||
line(` ollama pull ${modelId} (把模型拉到本地,离线需提前准备好)`);
|
||||
line(' 然后重新运行本工具。');
|
||||
line(' Ollama is not answering. It is usually not running, or the model was never pulled. On this machine, run:');
|
||||
line(' ollama serve starts it, skip if it already runs in the background');
|
||||
line(` ollama pull ${modelId} downloads the model — do this while you still have internet`);
|
||||
line(' Then run this tool again.');
|
||||
} else {
|
||||
line('→ 是地址错 / 内网不通 / 防火墙 / 模型服务没起。');
|
||||
line(' 在这台机器上自测:');
|
||||
line(' Either the address is wrong, the network cannot reach it, a firewall is blocking it, or the service is down.');
|
||||
line(' Test it from this machine with:');
|
||||
line(` ping ${host}`);
|
||||
line(' 让机房管理员确认 IP、端口、防火墙放行。');
|
||||
line(' If that also fails, ask whoever runs the server to confirm the address, port and firewall rules.');
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
|
||||
553
portable/lib/start.mjs
Normal file
553
portable/lib/start.mjs
Normal file
@@ -0,0 +1,553 @@
|
||||
#!/usr/bin/env node
|
||||
// U-Claw portable launcher.
|
||||
//
|
||||
// Everything the old START HERE - Windows.bat and START HERE - Mac.command did lives here, so
|
||||
// the two platforms cannot drift apart again and so every user-facing string can
|
||||
// be translated. The launchers are now thin shells that locate node and hand off.
|
||||
//
|
||||
// Two constraints drove this:
|
||||
// 1. tests/windows-launchers.test.mjs requires customer-facing .bat files to be
|
||||
// pure ASCII — Chinese Windows cmd.exe reads non-ASCII bytes as GBK and
|
||||
// mis-parses the script. Localised text therefore cannot live in a .bat.
|
||||
// 2. cmd.exe treats an unescaped ) as a block terminator, which produced a
|
||||
// class of flash-exit regressions. Moving the logic out removes the hazard.
|
||||
//
|
||||
// Errors follow the house rule: what happened, why, and one thing to do next.
|
||||
|
||||
import { spawn, spawnSync, execFileSync } from 'node:child_process';
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync, cpSync, rmSync, renameSync } from 'node:fs';
|
||||
import { createServer } from 'node:net';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
import process from 'node:process';
|
||||
|
||||
import { resolveLocale, createTranslator } from './i18n.mjs';
|
||||
import { buildChecks, runRepairs, writeDiagnostics } from './self-heal.mjs';
|
||||
|
||||
const GATEWAY_PORT_FROM = 18789;
|
||||
const GATEWAY_PORT_TO = 18799;
|
||||
const CONFIG_SERVER_DEFAULT_PORT = 18788;
|
||||
const CONFIG_SERVER_WAIT_MS = 6000;
|
||||
const GATEWAY_TOKEN = 'uclaw';
|
||||
|
||||
const libDir = dirname(fileURLToPath(import.meta.url));
|
||||
const uclawDir = resolve(libDir, '..');
|
||||
|
||||
const paths = {
|
||||
app: join(uclawDir, 'app'),
|
||||
data: join(uclawDir, 'data'),
|
||||
get core() { return join(this.app, 'core'); },
|
||||
get state() { return join(this.data, '.openclaw'); },
|
||||
get config() { return join(this.state, 'openclaw.json'); },
|
||||
get runtimeJson() { return join(this.state, 'runtime.json'); },
|
||||
};
|
||||
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
// ---------------------------------------------------------------- output
|
||||
|
||||
const dim = (s) => `[2m${s}[0m`;
|
||||
const green = (s) => `[32m${s}[0m`;
|
||||
const yellow = (s) => `[33m${s}[0m`;
|
||||
|
||||
function say(message) { process.stdout.write(` ${message}\n`); }
|
||||
function ok(message) { say(green(message)); }
|
||||
function note(message) { say(dim(message)); }
|
||||
|
||||
// Every user-facing failure gets the same three parts. Never print only a symptom.
|
||||
function bail(t, key, vars = {}) {
|
||||
process.stdout.write('\n');
|
||||
say(yellow(t(`${key}.what`, vars)));
|
||||
say(t(`${key}.why`, vars));
|
||||
process.stdout.write('\n');
|
||||
say(`→ ${t(`${key}.action`, vars)}`);
|
||||
process.stdout.write('\n');
|
||||
process.exitCode = 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- platform
|
||||
|
||||
function runtimeDirName() {
|
||||
if (process.platform === 'win32') return 'node-win-x64';
|
||||
if (process.platform === 'darwin') return process.arch === 'arm64' ? 'node-mac-arm64' : 'node-mac-x64';
|
||||
return process.arch === 'arm64' ? 'node-linux-arm64' : 'node-linux-x64';
|
||||
}
|
||||
|
||||
function nodeBinary(runtimeDir) {
|
||||
return process.platform === 'win32'
|
||||
? join(runtimeDir, 'node.exe')
|
||||
: join(runtimeDir, 'bin', 'node');
|
||||
}
|
||||
|
||||
function npmEntry(runtimeDir) {
|
||||
return process.platform === 'win32'
|
||||
? join(runtimeDir, 'node_modules', 'npm', 'bin', 'npm-cli.js')
|
||||
: join(runtimeDir, 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js');
|
||||
}
|
||||
|
||||
function openUrl(url) {
|
||||
try {
|
||||
if (process.platform === 'win32') {
|
||||
// The empty "" is the window title; without it cmd treats a quoted URL as one.
|
||||
spawn('cmd', ['/c', 'start', '', url], { detached: true, stdio: 'ignore', windowsHide: true }).unref();
|
||||
} else if (process.platform === 'darwin') {
|
||||
spawn('open', [url], { detached: true, stdio: 'ignore' }).unref();
|
||||
} else {
|
||||
spawn('xdg-open', [url], { detached: true, stdio: 'ignore' }).unref();
|
||||
}
|
||||
} catch {
|
||||
// Opening a browser is a convenience, never a reason to fail startup.
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- steps
|
||||
|
||||
// Older drives shipped core-win / core-mac. Rename in place so one code path works.
|
||||
function migrateLegacyCoreDir() {
|
||||
for (const legacy of ['core-win', 'core-mac']) {
|
||||
const from = join(paths.app, legacy);
|
||||
if (existsSync(from) && !existsSync(paths.core)) {
|
||||
try { renameSync(from, paths.core); } catch { /* keep going with the old name */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function clearMacQuarantine(t, nodeBin) {
|
||||
if (process.platform !== 'darwin') return;
|
||||
try {
|
||||
const attrs = execFileSync('xattr', ['-l', nodeBin], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
||||
if (!attrs.includes('com.apple.quarantine')) return;
|
||||
note(t('start.quarantine_removing'));
|
||||
spawnSync('xattr', ['-rd', 'com.apple.quarantine', uclawDir], { stdio: 'ignore' });
|
||||
} catch {
|
||||
// No xattr, or nothing quarantined — either way there is nothing to clear.
|
||||
}
|
||||
}
|
||||
|
||||
function ensureDataDirs() {
|
||||
for (const dir of [paths.state, join(paths.data, 'memory'), join(paths.data, 'backups'), join(paths.data, 'logs')]) {
|
||||
mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
// Moves the heavy, rebuildable caches (browser profile, V8 compile cache) off the
|
||||
// USB drive and onto local disk. Failing here only costs speed, so stay silent.
|
||||
function applyPortableCache(t, nodeBin) {
|
||||
try {
|
||||
const out = execFileSync(nodeBin, [join(libDir, 'portable-cache.mjs'), paths.state, uclawDir], {
|
||||
encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
|
||||
});
|
||||
let cacheRoot = null;
|
||||
for (const line of out.split(/\r?\n/)) {
|
||||
const [key, ...rest] = line.split('=');
|
||||
const value = rest.join('=');
|
||||
if (key === 'UCLAW_COMPILE_CACHE_DIR') process.env.NODE_COMPILE_CACHE = value;
|
||||
if (key === 'UCLAW_CACHE_ROOT') cacheRoot = value;
|
||||
}
|
||||
if (process.env.NODE_COMPILE_CACHE && cacheRoot) note(t('start.cache_local', { path: cacheRoot }));
|
||||
} catch { /* cache stays on the drive */ }
|
||||
}
|
||||
|
||||
function ensureConfig(t) {
|
||||
if (existsSync(paths.config)) return;
|
||||
const legacy = join(paths.data, 'config.json');
|
||||
if (existsSync(legacy)) {
|
||||
note(t('start.config_migrating'));
|
||||
copyFileSync(legacy, paths.config);
|
||||
ok(t('start.config_migrated'));
|
||||
return;
|
||||
}
|
||||
note(t('start.config_creating'));
|
||||
writeFileSync(
|
||||
paths.config,
|
||||
`${JSON.stringify({ gateway: { mode: 'local', auth: { token: GATEWAY_TOKEN } } }, null, 2)}\n`,
|
||||
'utf8'
|
||||
);
|
||||
ok(t('start.config_created'));
|
||||
}
|
||||
|
||||
function installDependencies(t, nodeBin, runtimeDir) {
|
||||
if (existsSync(join(paths.core, 'node_modules'))) return true;
|
||||
|
||||
// The shipped zip has deps pre-installed; reaching here means an incomplete
|
||||
// copy or a source checkout. Say "getting ready", not "node_modules not found".
|
||||
note(t('start.deps_preparing'));
|
||||
note(t('start.deps_slow_drive'));
|
||||
|
||||
const result = spawnSync(
|
||||
nodeBin,
|
||||
[npmEntry(runtimeDir), 'install', '--registry=https://registry.npmjs.org',
|
||||
'--ignore-scripts', '--no-audit', '--no-fund', '--omit=dev'],
|
||||
{
|
||||
cwd: paths.core,
|
||||
stdio: ['ignore', 'ignore', 'inherit'],
|
||||
// Keep npm's cache inside the app so nothing is left on the host machine.
|
||||
env: { ...process.env, npm_config_cache: join(paths.app, '.npm-cache') },
|
||||
}
|
||||
);
|
||||
|
||||
if (result.status !== 0 || !existsSync(join(paths.core, 'node_modules'))) return false;
|
||||
ok(t('start.deps_done'));
|
||||
return true;
|
||||
}
|
||||
|
||||
// Diagnose used to be something the user had to know to run. By the time
|
||||
// startup had visibly failed, anyone who did not know that had already given up.
|
||||
// Now it runs by itself, and only speaks up when it cannot fix the problem.
|
||||
async function healAndRetry(t, attempt) {
|
||||
note(t('start.checking'));
|
||||
|
||||
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const checks = buildChecks({
|
||||
paths,
|
||||
defaultConfigPath: join(uclawDir, 'default-config.json'),
|
||||
portRange: { from: GATEWAY_PORT_FROM, to: GATEWAY_PORT_TO },
|
||||
stamp,
|
||||
});
|
||||
|
||||
const applied = await runRepairs(checks);
|
||||
if (applied.length > 0) {
|
||||
for (const { result } of applied) note(` ${result}`);
|
||||
note(t('start.repaired'));
|
||||
// Silence on success is the point: a user who never learns something was
|
||||
// wrong had a working product.
|
||||
if (await attempt()) return { healed: true };
|
||||
}
|
||||
|
||||
const report = await writeDiagnostics({
|
||||
paths,
|
||||
checks,
|
||||
applied,
|
||||
stamp,
|
||||
versions: {
|
||||
openclaw: readVersionFile('OPENCLAW_VERSION'),
|
||||
node_pinned: readVersionFile('NODE_VERSION'),
|
||||
},
|
||||
});
|
||||
return { healed: false, report };
|
||||
}
|
||||
|
||||
function readVersionFile(name) {
|
||||
for (const dir of [uclawDir, join(uclawDir, '..')]) {
|
||||
try { return readFileSync(join(dir, name), 'utf8').trim(); } catch { /* try next */ }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// OpenClaw routes every fetch through HTTP_PROXY when it is set, which breaks
|
||||
// self-hosted model endpoints on a corporate network. Put those hosts in NO_PROXY.
|
||||
function applyNoProxy(t, nodeBin) {
|
||||
try {
|
||||
const out = execFileSync(nodeBin, [join(libDir, 'resolve-no-proxy.mjs'), paths.config], {
|
||||
encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
|
||||
}).trim();
|
||||
if (!out.startsWith('UCLAW_NO_PROXY=')) return;
|
||||
const hosts = out.slice('UCLAW_NO_PROXY='.length);
|
||||
if (!hosts) return;
|
||||
process.env.NO_PROXY = hosts;
|
||||
process.env.no_proxy = hosts;
|
||||
note(t('start.no_proxy', { hosts }));
|
||||
} catch { /* no proxy configured */ }
|
||||
}
|
||||
|
||||
// Hands the resolved locale to the HTML pages. They are opened from file://,
|
||||
// where fetching the drive's own openclaw.json is blocked, so they cannot work
|
||||
// out the locale themselves — and falling back to navigator.language would make
|
||||
// the language follow the machine instead of the drive, which is exactly the
|
||||
// behaviour we do not want. A generated <script> is the one channel that works
|
||||
// from both file:// and http://.
|
||||
// True until the drive records a language of its own. Asking once and never
|
||||
// again is the point: the answer lives on the drive, not in a browser profile.
|
||||
function localeChosenOnDrive() {
|
||||
return Boolean(driveSetting('locale'));
|
||||
}
|
||||
|
||||
// The wizard runs once. Its answer decides which skills are installed and how
|
||||
// much interface to show, so re-asking would quietly undo the user's setup.
|
||||
function personaChosenOnDrive() {
|
||||
const personas = driveSetting('personas');
|
||||
return Array.isArray(personas) && personas.length > 0;
|
||||
}
|
||||
|
||||
function driveSetting(key) {
|
||||
try {
|
||||
return JSON.parse(readFileSync(paths.config, 'utf8'))?.uclaw?.[key];
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function writeLocaleForPages(locale) {
|
||||
try {
|
||||
writeFileSync(
|
||||
join(paths.state, 'locale.js'),
|
||||
`/* GENERATED by lib/start.mjs — the drive's language, for pages opened from file://. */\nwindow.UCLAW_LOCALE = ${JSON.stringify(locale)};\n`,
|
||||
'utf8'
|
||||
);
|
||||
} catch { /* pages fall back to the browser language */ }
|
||||
}
|
||||
|
||||
// Records what is actually on this drive, so an updater can diff instead of
|
||||
// guessing. This release deliberately does not self-update — a USB drive can be
|
||||
// pulled mid-write and there is no way back from a half-applied update — but
|
||||
// without a written-down inventory a later updater has nothing to compare
|
||||
// against, and retrofitting one means the first upgrade is the risky one.
|
||||
function writeInstalledManifest() {
|
||||
const read = (file) => {
|
||||
for (const dir of [uclawDir, join(uclawDir, '..')]) {
|
||||
try { return readFileSync(join(dir, file), 'utf8').trim(); } catch { /* try next */ }
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
let skills = null;
|
||||
try {
|
||||
const manifest = JSON.parse(readFileSync(join(uclawDir, 'skills', 'manifest.json'), 'utf8'));
|
||||
skills = {
|
||||
schemaVersion: manifest.schemaVersion,
|
||||
shipping: manifest.skills.filter((s) => s.status === 'shipping').map((s) => s.id),
|
||||
};
|
||||
} catch { /* skills may not be staged in a dev checkout */ }
|
||||
|
||||
const inventory = {
|
||||
// Bump when the shape of this file changes, not when contents change.
|
||||
schemaVersion: 1,
|
||||
openclaw: read('OPENCLAW_VERSION'),
|
||||
node: read('NODE_VERSION') ?? process.version,
|
||||
skills,
|
||||
updatePolicy: 'frozen-notify-only',
|
||||
};
|
||||
|
||||
try {
|
||||
writeFileSync(join(paths.state, 'installed.json'), `${JSON.stringify(inventory, null, 2)}\n`, 'utf8');
|
||||
} catch { /* inventory is diagnostic, never a reason to fail startup */ }
|
||||
}
|
||||
|
||||
function startUpdateCheck(nodeBin) {
|
||||
let versionFile = join(uclawDir, 'OPENCLAW_VERSION');
|
||||
if (!existsSync(versionFile)) versionFile = join(uclawDir, '..', 'OPENCLAW_VERSION');
|
||||
if (!existsSync(versionFile)) return;
|
||||
try {
|
||||
spawn(nodeBin, [join(libDir, 'check-update.mjs'), versionFile, paths.state], {
|
||||
detached: true, stdio: 'ignore', windowsHide: true,
|
||||
}).unref();
|
||||
} catch { /* update banner is optional */ }
|
||||
}
|
||||
|
||||
// Was Windows-only before; the Mac launcher never did it, so Mac users silently
|
||||
// had no WeChat channel.
|
||||
function installWeChatPlugin(t) {
|
||||
const src = join(paths.app, 'extensions', 'openclaw-weixin');
|
||||
if (!existsSync(join(src, 'openclaw.plugin.json'))) return;
|
||||
const home = process.env.USERPROFILE || process.env.HOME;
|
||||
if (!home) return;
|
||||
const dest = join(home, '.openclaw', 'extensions', 'openclaw-weixin');
|
||||
if (existsSync(join(dest, 'openclaw.plugin.json'))) return;
|
||||
try {
|
||||
note(t('start.wechat_installing'));
|
||||
mkdirSync(dirname(dest), { recursive: true });
|
||||
cpSync(src, dest, { recursive: true });
|
||||
ok(t('start.wechat_installed'));
|
||||
} catch { /* channel plugin is optional */ }
|
||||
}
|
||||
|
||||
function portFree(port) {
|
||||
return new Promise((done) => {
|
||||
const probe = createServer();
|
||||
probe.once('error', () => done(false));
|
||||
probe.once('listening', () => probe.close(() => done(true)));
|
||||
probe.listen(port, '127.0.0.1');
|
||||
});
|
||||
}
|
||||
|
||||
async function findGatewayPort(t) {
|
||||
for (let port = GATEWAY_PORT_FROM; port <= GATEWAY_PORT_TO; port++) {
|
||||
if (await portFree(port)) return port;
|
||||
note(t('start.port_in_use', { port }));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// The config server picks its own port when 18788 is taken and records the real
|
||||
// one in runtime.json. The old Mac launcher hardcoded 18788 and opened a dead
|
||||
// page whenever that fallback kicked in.
|
||||
async function startConfigServer(t, nodeBin) {
|
||||
note(t('start.config_center_starting'));
|
||||
try { rmSync(paths.runtimeJson, { force: true }); } catch { /* nothing to clear */ }
|
||||
|
||||
const child = spawn(nodeBin, [join(uclawDir, 'config-server', 'server.js')], {
|
||||
stdio: 'ignore', windowsHide: true,
|
||||
});
|
||||
child.unref();
|
||||
|
||||
const deadline = Date.now() + CONFIG_SERVER_WAIT_MS;
|
||||
while (Date.now() < deadline) {
|
||||
if (existsSync(paths.runtimeJson)) {
|
||||
try {
|
||||
const port = JSON.parse(readFileSync(paths.runtimeJson, 'utf8'))?.configServerPort;
|
||||
if (port) return { child, port };
|
||||
} catch { /* still being written */ }
|
||||
}
|
||||
await sleep(200);
|
||||
}
|
||||
return { child, port: CONFIG_SERVER_DEFAULT_PORT };
|
||||
}
|
||||
|
||||
// Fallback for the splash screen. loading.html polls /ready itself and moves on
|
||||
// when the gateway answers, but some browsers block fetch from a file:// page —
|
||||
// those users would sit on a splash that never advances. Watch the port here
|
||||
// too, and on timeout put them back in the Control Panel rather than nowhere.
|
||||
// Replaces lib/wait-gateway.bat, which only ever ran on Windows.
|
||||
function watchGatewayReady(port, configPort, { timeoutMs = 300_000, intervalMs = 2000 } = {}) {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
const timer = setInterval(async () => {
|
||||
if (await portFree(port)) {
|
||||
// Still free means nothing is listening yet.
|
||||
if (Date.now() >= deadline) {
|
||||
clearInterval(timer);
|
||||
openUrl(`http://127.0.0.1:${configPort}/`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
clearInterval(timer); // Gateway is up; the splash will have moved on by itself.
|
||||
}, intervalMs);
|
||||
timer.unref();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- main
|
||||
|
||||
async function main() {
|
||||
const locale = resolveLocale({ override: process.env.UCLAW_LOCALE, configPath: paths.config });
|
||||
const t = createTranslator(locale);
|
||||
|
||||
process.stdout.write(`\n ${green(t('start.banner'))}\n\n`);
|
||||
|
||||
migrateLegacyCoreDir();
|
||||
|
||||
const runtimeDir = join(paths.app, 'runtime', runtimeDirName());
|
||||
const nodeBin = nodeBinary(runtimeDir);
|
||||
if (!existsSync(nodeBin)) return bail(t, 'error.node_missing');
|
||||
|
||||
if (process.platform === 'darwin' && !['arm64', 'x64'].includes(process.arch)) {
|
||||
return bail(t, 'error.unsupported_arch', { arch: process.arch });
|
||||
}
|
||||
|
||||
clearMacQuarantine(t, nodeBin);
|
||||
note(t('start.node_version', { version: process.version }));
|
||||
|
||||
process.env.PATH = `${dirname(nodeBin)}${process.platform === 'win32' ? ';' : ':'}${process.env.PATH}`;
|
||||
process.env.OPENCLAW_HOME = paths.data;
|
||||
process.env.OPENCLAW_STATE_DIR = paths.state;
|
||||
process.env.OPENCLAW_CONFIG_PATH = paths.config;
|
||||
// U-Claw opens the local dashboard directly. Advertising the gateway over mDNS
|
||||
// is unnecessary and crashes the bonjour plugin on machines with VPN adapters.
|
||||
process.env.OPENCLAW_DISABLE_BONJOUR = '1';
|
||||
|
||||
ensureDataDirs();
|
||||
applyPortableCache(t, nodeBin);
|
||||
ensureConfig(t);
|
||||
if (!installDependencies(t, nodeBin, runtimeDir)) {
|
||||
const outcome = await healAndRetry(t, async () => installDependencies(t, nodeBin, runtimeDir));
|
||||
if (!outcome.healed) {
|
||||
bail(t, 'error.deps_failed');
|
||||
say(t('start.diagnostics_written', { path: outcome.report }));
|
||||
say(t('start.diagnostics_hint'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
applyNoProxy(t, nodeBin);
|
||||
writeLocaleForPages(locale);
|
||||
writeInstalledManifest();
|
||||
startUpdateCheck(nodeBin);
|
||||
installWeChatPlugin(t);
|
||||
|
||||
const { child: configServer, port: configPort } = await startConfigServer(t, nodeBin);
|
||||
note(t('start.config_center_port', { port: configPort }));
|
||||
|
||||
let port = await findGatewayPort(t);
|
||||
if (!port) {
|
||||
const outcome = await healAndRetry(t, async () => Boolean(await findGatewayPort(t)));
|
||||
port = outcome.healed ? await findGatewayPort(t) : null;
|
||||
if (!port) {
|
||||
configServer.kill();
|
||||
bail(t, 'error.no_port', { from: GATEWAY_PORT_FROM, to: GATEWAY_PORT_TO });
|
||||
if (outcome.report) {
|
||||
say(t('start.diagnostics_written', { path: outcome.report }));
|
||||
say(t('start.diagnostics_hint'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
note(t('start.gateway_starting', { port }));
|
||||
|
||||
const openclawEntry = join(paths.core, 'node_modules', 'openclaw', 'openclaw.mjs');
|
||||
const gateway = spawn(
|
||||
nodeBin,
|
||||
[openclawEntry, 'gateway', 'run', '--allow-unconfigured', '--force', '--port', String(port)],
|
||||
{ cwd: paths.core, stdio: 'inherit', env: process.env, windowsHide: true }
|
||||
);
|
||||
|
||||
// Open the splash first: it polls /ready itself and gives immediate feedback
|
||||
// instead of a blank window while a slow drive unpacks components.
|
||||
const loadingUrl = `${pathToFileURL(join(libDir, 'loading.html')).href}?port=${port}&token=${GATEWAY_TOKEN}`;
|
||||
note(t('start.opening_screen'));
|
||||
openUrl(loadingUrl);
|
||||
// First run gets the language chooser instead of Settings; it writes the
|
||||
// choice to the drive and then forwards to Settings itself.
|
||||
// First run walks language → what-for → key. Each step writes its answer to
|
||||
// the drive and is skipped once answered, so a second launch goes straight in.
|
||||
if (!localeChosenOnDrive()) {
|
||||
note(t('start.opening_language'));
|
||||
openUrl(pathToFileURL(join(libDir, 'language.html')).href);
|
||||
} else if (!personaChosenOnDrive()) {
|
||||
note(t('start.opening_persona'));
|
||||
openUrl(pathToFileURL(join(libDir, 'persona.html')).href);
|
||||
} else {
|
||||
note(t('start.opening_config'));
|
||||
openUrl(`http://127.0.0.1:${configPort}/`);
|
||||
}
|
||||
|
||||
try {
|
||||
spawn(nodeBin, [join(libDir, 'prewarm.mjs'), String(port), GATEWAY_TOKEN], {
|
||||
detached: true, stdio: 'ignore', windowsHide: true,
|
||||
}).unref();
|
||||
} catch { /* prewarm is best-effort */ }
|
||||
|
||||
watchGatewayReady(port, configPort);
|
||||
|
||||
process.stdout.write('\n');
|
||||
ok(t('start.running_title'));
|
||||
say(t('start.running_dashboard', { url: `http://127.0.0.1:${port}/#token=${GATEWAY_TOKEN}` }));
|
||||
say(t('start.running_config', { url: `http://127.0.0.1:${configPort}/` }));
|
||||
process.stdout.write('\n');
|
||||
note(t('start.first_run_wait'));
|
||||
note(t('start.running_hint'));
|
||||
process.stdout.write('\n');
|
||||
|
||||
let stopping = false;
|
||||
const stop = () => {
|
||||
if (stopping) return;
|
||||
stopping = true;
|
||||
gateway.kill();
|
||||
configServer.kill();
|
||||
};
|
||||
process.on('SIGINT', stop);
|
||||
process.on('SIGTERM', stop);
|
||||
|
||||
gateway.on('exit', (code) => {
|
||||
configServer.kill();
|
||||
process.stdout.write('\n');
|
||||
// 0 is a clean stop; 3221225786 is Windows' Ctrl+C exit code.
|
||||
if (!stopping && code !== 0 && code !== 3221225786) {
|
||||
say(yellow(t('start.exited_unexpectedly', { code })));
|
||||
}
|
||||
ok(t('start.stopped'));
|
||||
process.exitCode = code === 0 || stopping ? 0 : 1;
|
||||
});
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
process.stderr.write(` ${error?.stack ?? error}\n`);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
@@ -1,32 +0,0 @@
|
||||
@echo off
|
||||
REM wait-gateway.bat - fallback watcher after Windows-Start opens loading.html.
|
||||
REM Slow USB drives can need tens of seconds before the gateway listens.
|
||||
REM
|
||||
REM Windows-Start opens Config Center immediately, so the ready path only
|
||||
REM exits quietly. On timeout, reopen Config Center as a recovery hint.
|
||||
REM Usage (called in background by Windows-Start.bat): wait-gateway.bat PORT CONFIG_PORT
|
||||
REM Polls every 2 seconds, up to about 5 minutes.
|
||||
|
||||
set "PORT=%~1"
|
||||
if "%PORT%"=="" set "PORT=18789"
|
||||
set "CONFIG_PORT=%~2"
|
||||
if "%CONFIG_PORT%"=="" set "CONFIG_PORT=18788"
|
||||
|
||||
set /a TRIES=0
|
||||
:wait_loop
|
||||
netstat -an | findstr ":%PORT% " | findstr "LISTENING" >nul 2>&1
|
||||
if %errorlevel%==0 goto :ready
|
||||
set /a TRIES+=1
|
||||
if %TRIES% geq 150 goto :timeout
|
||||
REM Use ping (not "timeout /t") for the ~2s delay: if a GNU coreutils "timeout"
|
||||
REM is on PATH (Git/MSYS bin), it rejects "/t" and this poll becomes a busy-loop.
|
||||
REM ping for the delay is PATH-robust and matches Windows-Start.bat's wait loop.
|
||||
ping -n 3 127.0.0.1 >nul 2>&1
|
||||
goto :wait_loop
|
||||
|
||||
:ready
|
||||
exit /b 0
|
||||
|
||||
:timeout
|
||||
start "" http://127.0.0.1:%CONFIG_PORT%/
|
||||
exit /b 1
|
||||
@@ -1,112 +0,0 @@
|
||||
---
|
||||
name: bilibili-helper
|
||||
description: "B站内容助手 - 视频标题描述优化、标签策略、封面设计建议、分区选择、评论互动"
|
||||
metadata: { "openclaw": { "emoji": "📺" } }
|
||||
---
|
||||
|
||||
# B站内容助手
|
||||
|
||||
帮助 UP 主优化视频标题、描述、标签和封面,提升视频在 B 站的推荐和互动表现。
|
||||
|
||||
## 功能概述
|
||||
|
||||
- **标题优化**: 符合 B 站用户口味的标题写法
|
||||
- **描述撰写**: 含时间轴、关键词、引导语的视频简介
|
||||
- **标签策略**: 精准标签 + 热门标签组合,提升推荐
|
||||
- **分区建议**: 根据内容推荐最佳投稿分区
|
||||
- **封面设计**: 封面构图、文字、配色建议
|
||||
- **评论互动**: 置顶评论话术,提升互动率
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 写标题和描述
|
||||
```
|
||||
我做了一个"用AI写代码"的教程视频,帮我写标题和描述
|
||||
```
|
||||
|
||||
### 分区推荐
|
||||
```
|
||||
我的视频内容是游戏+编程结合,应该投哪个分区?
|
||||
```
|
||||
|
||||
### 封面建议
|
||||
```
|
||||
视频主题是"大学生省钱攻略",帮我设计封面方案
|
||||
```
|
||||
|
||||
### 优化已有内容
|
||||
```
|
||||
这个视频播放量很低,帮我分析标题和标签有什么问题(附链接/截图)
|
||||
```
|
||||
|
||||
## 标题公式
|
||||
|
||||
1. **疑问式**: "为什么XX?看完你就懂了"
|
||||
2. **教程式**: "XX教程|从零开始手把手教学"
|
||||
3. **测评式**: "花了XX元买了XX,值不值?"
|
||||
4. **挑战式**: "挑战XX天只用XX"
|
||||
5. **盘点式**: "XX年度十大XX盘点"
|
||||
6. **震惊式**: "这也太XX了吧!"(B站经典体)
|
||||
|
||||
## 描述模板
|
||||
|
||||
```
|
||||
【视频简介】
|
||||
一句话概括视频内容
|
||||
|
||||
【时间轴】
|
||||
00:00 开场
|
||||
01:23 第一部分
|
||||
05:00 第二部分
|
||||
08:30 总结
|
||||
|
||||
【相关链接】
|
||||
工具/资源链接
|
||||
|
||||
【关于我】
|
||||
一句话介绍 + 更新频率
|
||||
|
||||
三连支持一下吧~ 有问题评论区见!
|
||||
```
|
||||
|
||||
## 分区选择指南
|
||||
|
||||
| 内容类型 | 推荐分区 |
|
||||
|---------|---------|
|
||||
| 编程教程 | 科技 → 计算机技术 |
|
||||
| 日常 vlog | 生活 → 日常 |
|
||||
| 游戏实况 | 游戏 → 单机/网游 |
|
||||
| 知识科普 | 知识 → 社科人文/科学科普 |
|
||||
| 美食制作 | 生活 → 美食圈 |
|
||||
| AI/数码 | 科技 → 软件应用 |
|
||||
|
||||
## 标签策略
|
||||
|
||||
- **核心标签**: 精确描述视频内容(2-3个)
|
||||
- **分区热门**: 当前分区下的热门标签(2-3个)
|
||||
- **全站热门**: B站当前热点话题(1-2个)
|
||||
- **长尾标签**: 细分领域关键词(2-3个)
|
||||
- 总计 8-12 个标签效果最佳
|
||||
|
||||
## 适用场景
|
||||
|
||||
- B站视频投稿前的标题/描述/标签优化
|
||||
- 分析低播放量视频的改进方向
|
||||
- 设计视频封面方案
|
||||
- 选择合适的投稿分区
|
||||
- 撰写置顶评论引导互动
|
||||
|
||||
## 不适用场景
|
||||
|
||||
- 写抖音短视频脚本(平台调性不同,请用 douyin-script)
|
||||
- 视频剪辑指导(本技能专注文案优化)
|
||||
- B站直播相关(直播与视频投稿逻辑不同)
|
||||
- 需要实时查询B站热搜数据
|
||||
|
||||
## 封面设计要点
|
||||
|
||||
- **尺寸**: 16:10(推荐 1146x717)
|
||||
- **文字**: 不超过 10 字,字体加粗加描边
|
||||
- **表情**: 人物表情夸张更吸引点击
|
||||
- **配色**: 高饱和度,与B站粉色/蓝色背景形成对比
|
||||
- **避免**: 过多文字、低分辨率、与内容无关的封面
|
||||
@@ -1,98 +0,0 @@
|
||||
---
|
||||
name: china-search
|
||||
description: "国内搜索引擎 - 百度、搜狗、Bing中国搜索,绕过GFW限制"
|
||||
metadata: { "openclaw": { "emoji": "🔍" } }
|
||||
---
|
||||
|
||||
# 国内搜索引擎助手
|
||||
|
||||
在无法访问 Google 的环境下,通过 curl 调用百度、搜狗、Bing 中国等国内搜索引擎获取信息。
|
||||
|
||||
## 功能概述
|
||||
|
||||
- **百度搜索**: 中文信息最全的搜索引擎
|
||||
- **搜狗搜索**: 微信公众号文章搜索利器
|
||||
- **Bing 中国**: 国际信息 + 国内可访问
|
||||
- **搜索优化**: 帮你构造更精准的搜索关键词
|
||||
- **结果整理**: 将搜索结果整理为结构化信息
|
||||
|
||||
## 搜索命令
|
||||
|
||||
### 百度搜索
|
||||
```bash
|
||||
curl -s -L "https://www.baidu.com/s?wd=关键词" \
|
||||
-H "User-Agent: Mozilla/5.0" | head -200
|
||||
```
|
||||
|
||||
### 搜狗搜索(含微信文章)
|
||||
```bash
|
||||
# 网页搜索
|
||||
curl -s -L "https://www.sogou.com/web?query=关键词" \
|
||||
-H "User-Agent: Mozilla/5.0"
|
||||
|
||||
# 微信文章搜索
|
||||
curl -s -L "https://weixin.sogou.com/weixin?query=关键词" \
|
||||
-H "User-Agent: Mozilla/5.0"
|
||||
```
|
||||
|
||||
### Bing 中国搜索
|
||||
```bash
|
||||
curl -s -L "https://cn.bing.com/search?q=关键词" \
|
||||
-H "User-Agent: Mozilla/5.0"
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 搜索信息
|
||||
```
|
||||
帮我搜索"DeepSeek V3 发布时间和新特性"
|
||||
```
|
||||
|
||||
### 搜微信文章
|
||||
```
|
||||
帮我在搜狗搜索最近关于"AI Agent"的微信公众号文章
|
||||
```
|
||||
|
||||
### 对比搜索
|
||||
```
|
||||
分别用百度和Bing搜索"2024年中国GDP",对比结果
|
||||
```
|
||||
|
||||
### 精准搜索
|
||||
```
|
||||
帮我搜索 site:zhihu.com "大模型部署"相关的知乎回答
|
||||
```
|
||||
|
||||
## 搜索技巧
|
||||
|
||||
| 语法 | 说明 | 示例 |
|
||||
|-----|------|------|
|
||||
| "" | 精确匹配 | "人工智能" |
|
||||
| site: | 限定网站 | site:zhihu.com AI |
|
||||
| filetype: | 限定文件类型 | filetype:pdf 机器学习 |
|
||||
| - | 排除关键词 | 苹果 -手机 |
|
||||
| intitle: | 标题含关键词 | intitle:教程 Python |
|
||||
|
||||
## 适用场景
|
||||
|
||||
- 需要搜索中文互联网信息
|
||||
- Google 不可用时的替代方案
|
||||
- 专门搜索微信公众号文章
|
||||
- 需要搜索国内网站的特定内容
|
||||
- 查找百度百科、知乎等平台信息
|
||||
|
||||
## 不适用场景
|
||||
|
||||
- 需要搜索英文/国际信息(建议使用 WebSearch 工具)
|
||||
- 需要实时热搜排行(搜索结果有延迟)
|
||||
- 需要搜索被百度屏蔽的敏感内容
|
||||
- 需要大量爬取搜索结果(有反爬限制)
|
||||
|
||||
## 注意事项
|
||||
|
||||
- curl 获取的是 HTML 原始内容,需要解析提取有用信息
|
||||
- 百度搜索有反爬机制,频繁请求可能被拦截
|
||||
- 搜狗微信搜索是查找公众号文章的最佳渠道
|
||||
- Bing 中国版内容审核较百度宽松
|
||||
- 建议添加 User-Agent 头部模拟浏览器访问
|
||||
- 部分搜索结果链接需要二次跳转才能获取真实 URL
|
||||
@@ -1,126 +0,0 @@
|
||||
---
|
||||
name: china-translate
|
||||
description: "中英互译 + 本地化 - 技术翻译、UI本地化、文化适配、避免机翻腔"
|
||||
metadata: { "openclaw": { "emoji": "🌐" } }
|
||||
---
|
||||
|
||||
# 中英互译 + 本地化助手
|
||||
|
||||
专业的中英双向翻译工具,专注技术文档翻译、产品本地化和文化适配,杜绝机翻腔。
|
||||
|
||||
## 功能概述
|
||||
|
||||
- **技术翻译**: 编程/AI/互联网领域的专业术语翻译
|
||||
- **UI 本地化**: 将英文界面文案翻译为符合中文习惯的表达
|
||||
- **文化适配**: 处理中西文化差异,避免直译造成的误解
|
||||
- **去机翻腔**: 将僵硬的机器翻译改写为自然流畅的中文
|
||||
- **术语表管理**: 保持项目内术语翻译的一致性
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 技术文档翻译
|
||||
```
|
||||
把这段 API 文档翻译成中文,保留代码不翻译(附原文)
|
||||
```
|
||||
|
||||
### UI 本地化
|
||||
```
|
||||
帮我把这些英文按钮文案翻译成中文:
|
||||
- "Get Started"
|
||||
- "Learn More"
|
||||
- "Sign Up for Free"
|
||||
```
|
||||
|
||||
### 去机翻腔
|
||||
```
|
||||
这段翻译读起来很别扭,帮我改成自然的中文表达(附译文)
|
||||
```
|
||||
|
||||
### 中译英
|
||||
```
|
||||
帮我把这段产品介绍翻译成英文,目标读者是海外开发者
|
||||
```
|
||||
|
||||
## 翻译原则
|
||||
|
||||
### 核心准则
|
||||
1. **信**: 准确传达原文含义
|
||||
2. **达**: 表达通顺,符合目标语言习惯
|
||||
3. **雅**: 在准确的基础上追求优美
|
||||
|
||||
### 技术翻译规范
|
||||
- 代码、变量名、API 名称保持英文原样
|
||||
- 约定俗成的术语使用通用译法(见术语表)
|
||||
- 首次出现的术语标注英文原文:机器学习(Machine Learning)
|
||||
- 保留技术文档的格式和结构
|
||||
|
||||
## 常见术语对照表
|
||||
|
||||
| English | 中文(推荐) | 避免使用 |
|
||||
|---------|------------|---------|
|
||||
| Machine Learning | 机器学习 | 机械学习 |
|
||||
| Artificial Intelligence | 人工智能 / AI | 人造智能 |
|
||||
| Deploy | 部署 | 配置/展开 |
|
||||
| Repository | 仓库 / 代码仓库 | 存储库 |
|
||||
| Pull Request | PR / 合并请求 | 拉取请求 |
|
||||
| Container | 容器 | 集装箱 |
|
||||
| Middleware | 中间件 | 中间软件 |
|
||||
| Callback | 回调 | 回叫 |
|
||||
| Render | 渲染 | 呈现 |
|
||||
| Token | Token / 令牌 | 代币(非区块链语境) |
|
||||
| Prompt | 提示词 / Prompt | 提示语 |
|
||||
| Fine-tune | 微调 | 精调 |
|
||||
|
||||
## UI 本地化指南
|
||||
|
||||
### 按钮文案
|
||||
- "Submit" → "提交"(不是"递交")
|
||||
- "Cancel" → "取消"
|
||||
- "Get Started" → "立即开始" / "开始使用"
|
||||
- "Learn More" → "了解更多"
|
||||
- "Sign Up" → "注册"(不是"签到")
|
||||
|
||||
### 提示信息
|
||||
- "Are you sure?" → "确定要执行此操作吗?"
|
||||
- "Loading..." → "加载中..."
|
||||
- "No results found" → "暂无结果"
|
||||
- "Something went wrong" → "出了点问题,请稍后重试"
|
||||
|
||||
### 注意事项
|
||||
- 中文 UI 不需要句号(按钮、标签)
|
||||
- 中英文之间加半角空格: "使用 AI 生成"
|
||||
- 数字使用半角: "共 3 个结果"
|
||||
- 避免过长的中文按钮文案(尽量 4 字以内)
|
||||
|
||||
## 机翻腔常见问题
|
||||
|
||||
| 机翻腔 | 自然表达 |
|
||||
|-------|---------|
|
||||
| "它是一个非常好的工具" | "这是个很好用的工具" |
|
||||
| "你可以使用它来..." | "用它可以..." |
|
||||
| "这将会帮助你..." | "这能帮你..." |
|
||||
| "基于XXX" | "用XXX" / "通过XXX" |
|
||||
| "被设计用来" | "专门用于" / "用来" |
|
||||
|
||||
## 适用场景
|
||||
|
||||
- 翻译技术文档、README、API 文档
|
||||
- 产品界面中英文本地化
|
||||
- 改善机器翻译的输出质量
|
||||
- 保持项目术语翻译一致性
|
||||
- 写双语内容(如双语 README)
|
||||
|
||||
## 不适用场景
|
||||
|
||||
- 翻译文学作品(需要更高的文学素养)
|
||||
- 法律/医学等需要专业资质的翻译
|
||||
- 同声传译或实时翻译
|
||||
- 非中英语言对的翻译(如日语、韩语)
|
||||
|
||||
## 提升翻译质量的技巧
|
||||
|
||||
1. 提供上下文:说明文档类型和目标读者
|
||||
2. 指定术语:如果有项目术语表,请提供
|
||||
3. 分段翻译:长文档分段处理效果更好
|
||||
4. 说明语域:正式文档 vs 营销文案 vs 技术博客
|
||||
5. 标注不翻译的部分:代码块、品牌名等
|
||||
@@ -1,88 +0,0 @@
|
||||
---
|
||||
name: china-weather
|
||||
description: "中国城市天气查询 - 支持中文城市名、农历日期、wttr.in接口"
|
||||
metadata: { "openclaw": { "emoji": "🌤️" } }
|
||||
---
|
||||
|
||||
# 中国城市天气查询
|
||||
|
||||
通过 wttr.in 查询中国各城市天气信息,支持中文城市名输入,附带农历日期和生活建议。
|
||||
|
||||
## 功能概述
|
||||
|
||||
- **实时天气**: 通过 curl 调用 wttr.in 获取天气数据
|
||||
- **中文城市名**: 支持直接输入"深圳"、"北京"等中文名
|
||||
- **多日预报**: 支持当天及未来 3 天天气预报
|
||||
- **农历日期**: 自动附带当天农历日期信息
|
||||
- **生活指数**: 穿衣建议、出行提示、紫外线指数
|
||||
|
||||
## 查询方式
|
||||
|
||||
使用 Bash 工具执行以下命令格式:
|
||||
|
||||
```bash
|
||||
# 基础查询(中文输出)
|
||||
curl -s "wttr.in/深圳?lang=zh"
|
||||
|
||||
# 精简单行格式
|
||||
curl -s "wttr.in/深圳?format=3&lang=zh"
|
||||
|
||||
# 仅今天的天气
|
||||
curl -s "wttr.in/深圳?0&lang=zh"
|
||||
|
||||
# JSON 格式(便于解析)
|
||||
curl -s "wttr.in/深圳?format=j1"
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 查天气
|
||||
```
|
||||
深圳今天天气怎么样?
|
||||
```
|
||||
|
||||
### 出行建议
|
||||
```
|
||||
明天要去杭州出差,需要带伞吗?
|
||||
```
|
||||
|
||||
### 多城市对比
|
||||
```
|
||||
北京和上海这周末天气哪个好?想出去玩
|
||||
```
|
||||
|
||||
### 穿衣建议
|
||||
```
|
||||
广州现在多少度?穿什么衣服合适?
|
||||
```
|
||||
|
||||
## 适用场景
|
||||
|
||||
- 快速查询国内任意城市天气
|
||||
- 出差/旅行前了解目的地天气
|
||||
- 需要穿衣或出行建议
|
||||
- 查看未来几天天气趋势
|
||||
- 终端环境下无法打开天气 App 时
|
||||
|
||||
## 不适用场景
|
||||
|
||||
- 需要精确到小时的天气预报(wttr.in 精度有限)
|
||||
- 查询历史天气数据
|
||||
- 需要灾害预警等官方气象信息(请使用中国气象局官网)
|
||||
- 查询海外城市天气(本技能优化了中国城市名映射)
|
||||
|
||||
## 城市名映射
|
||||
|
||||
常见城市直接使用拼音或中文均可:
|
||||
- 北京 → Beijing / 北京
|
||||
- 上海 → Shanghai / 上海
|
||||
- 深圳 → Shenzhen / 深圳
|
||||
- 广州 → Guangzhou / 广州
|
||||
|
||||
如遇查询失败,尝试使用城市拼音替代中文名。
|
||||
|
||||
## 注意事项
|
||||
|
||||
- wttr.in 为免费服务,偶尔可能响应较慢
|
||||
- 温度默认摄氏度,风速默认 km/h
|
||||
- 数据来源为 WorldWeatherOnline,仅供参考
|
||||
@@ -1,72 +0,0 @@
|
||||
---
|
||||
name: deepseek-helper
|
||||
description: "DeepSeek API 助手 - 编程辅助、模型选择、API调用指南、定价信息"
|
||||
metadata: { "openclaw": { "emoji": "🤖" } }
|
||||
---
|
||||
|
||||
# DeepSeek API 助手
|
||||
|
||||
帮助用户高效使用 DeepSeek 系列模型,包括模型选择建议、API 调用示例、编程辅助和定价计算。
|
||||
|
||||
## 功能概述
|
||||
|
||||
- **模型选择指南**: deepseek-v4-flash vs deepseek-v4-pro 的使用场景对比
|
||||
- **API 调用示例**: 提供 Python、curl、Node.js 等语言的调用代码
|
||||
- **定价计算**: 根据 token 用量估算费用(输入 ¥1/百万tokens,输出 ¥2/百万tokens)
|
||||
- **Prompt 优化**: 针对 DeepSeek 模型特性优化提示词
|
||||
- **错误排查**: 常见 API 错误码解读和解决方案
|
||||
|
||||
## 模型对比
|
||||
|
||||
| 模型 | 适用场景 | 上下文长度 |
|
||||
|------|---------|-----------|
|
||||
| deepseek-v4-flash | 日常对话、文案写作、知识问答,快且便宜 | 128K |
|
||||
| deepseek-v4-pro | 复杂推理、数学、代码生成与审查 | 128K |
|
||||
|
||||
> ⚠️ 旧名 `deepseek-chat` / `deepseek-coder` / `deepseek-reasoner` 已下线,
|
||||
> api.deepseek.com 现在只接受 `deepseek-v4-flash` 和 `deepseek-v4-pro`,
|
||||
> 用旧名调用会直接返回 HTTP 400。
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 基础 API 调用
|
||||
```
|
||||
帮我写一个调用 DeepSeek API 的 Python 脚本,实现多轮对话
|
||||
```
|
||||
|
||||
### 模型选择
|
||||
```
|
||||
我要做一个客服机器人,应该用 deepseek-v4-flash 还是 deepseek-v4-pro?
|
||||
```
|
||||
|
||||
### 费用估算
|
||||
```
|
||||
我每天大约处理 1000 条用户消息,平均每条 500 token,帮我算下月费用
|
||||
```
|
||||
|
||||
### 错误排查
|
||||
```
|
||||
调用 DeepSeek API 返回 429 错误,怎么处理?
|
||||
```
|
||||
|
||||
## 适用场景
|
||||
|
||||
- 首次接入 DeepSeek API 的开发者
|
||||
- 需要在 deepseek-v4-flash 和 deepseek-v4-pro 之间做选择
|
||||
- 想要优化 API 调用成本
|
||||
- 排查 API 调用问题
|
||||
- 需要快速生成 API 调用代码片段
|
||||
|
||||
## 不适用场景
|
||||
|
||||
- 需要调用 OpenAI / Claude 等其他模型的 API(请使用对应工具)
|
||||
- 需要实时获取 DeepSeek 最新公告(本技能基于已知信息)
|
||||
- 需要部署私有化 DeepSeek 模型(涉及运维,超出范围)
|
||||
|
||||
## 关键提示
|
||||
|
||||
- DeepSeek API 兼容 OpenAI 格式,base_url 改为 `https://api.deepseek.com`
|
||||
- 国内访问无需翻墙,延迟低
|
||||
- 支持 function calling 和 JSON mode
|
||||
- 建议使用 stream 模式提升用户体验
|
||||
- API Key 申请地址: https://platform.deepseek.com
|
||||
@@ -1,100 +0,0 @@
|
||||
---
|
||||
name: douyin-script
|
||||
description: "抖音/快手短视频脚本 - 前3秒hook、脚本结构、热门音乐建议、话题标签策略"
|
||||
metadata: { "openclaw": { "emoji": "🎬" } }
|
||||
---
|
||||
|
||||
# 抖音/快手短视频脚本助手
|
||||
|
||||
帮你写出完播率高、互动强的短视频脚本,覆盖从选题到发布的全流程。
|
||||
|
||||
## 功能概述
|
||||
|
||||
- **前3秒 Hook**: 生成抓人眼球的开场白,降低划走率
|
||||
- **脚本结构**: Hook → 内容展开 → 反转/高潮 → CTA
|
||||
- **时长控制**: 根据内容类型推荐最佳时长(15s/30s/60s)
|
||||
- **话题标签**: 推荐热门和垂直话题标签组合
|
||||
- **BGM 建议**: 根据内容调性推荐背景音乐风格
|
||||
|
||||
## 脚本结构模板
|
||||
|
||||
### 15秒脚本(适合单一知识点)
|
||||
```
|
||||
【0-3秒】Hook: 抛出反常识/痛点
|
||||
"你知道吗?90%的人洗脸方式都是错的"
|
||||
|
||||
【3-10秒】核心内容: 快速讲解
|
||||
"正确的方式是...(配合画面演示)"
|
||||
|
||||
【10-15秒】CTA: 引导互动
|
||||
"学会了吗?双击收藏,关注我学更多护肤知识"
|
||||
```
|
||||
|
||||
### 60秒脚本(适合干货/故事)
|
||||
```
|
||||
【0-3秒】Hook: 强烈的情绪/悬念
|
||||
【3-15秒】背景铺垫: 建立场景
|
||||
【15-40秒】内容展开: 核心信息/故事发展
|
||||
【40-50秒】反转/高潮: 意想不到的结论
|
||||
【50-60秒】CTA + 话题标签
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 写脚本
|
||||
```
|
||||
帮我写一个30秒的抖音脚本,主题是"程序员的一天",搞笑风格
|
||||
```
|
||||
|
||||
### 优化开头
|
||||
```
|
||||
我拍了个美食视频,帮我想5个前3秒的hook文案
|
||||
```
|
||||
|
||||
### 话题推荐
|
||||
```
|
||||
我要发一个健身教程视频,推荐话题标签和发布时间
|
||||
```
|
||||
|
||||
### 改编热门
|
||||
```
|
||||
最近"搭子文化"很火,帮我写一个相关的短视频脚本
|
||||
```
|
||||
|
||||
## 前3秒 Hook 公式
|
||||
|
||||
1. **反常识**: "你一直在做的XX其实是错的"
|
||||
2. **数字冲击**: "只花了100块,效果比1000块的还好"
|
||||
3. **悬念提问**: "猜猜这个东西是干什么的?"
|
||||
4. **情绪共鸣**: "打工人看完都沉默了..."
|
||||
5. **结果前置**: "最终效果太绝了!往下看做法"
|
||||
6. **身份代入**: "如果你也是XX,一定要看完"
|
||||
|
||||
## 话题标签策略
|
||||
|
||||
- **大话题(曝光)**: #抖音热门 #今日份分享(1-2个)
|
||||
- **中话题(垂直)**: #健身打卡 #美食教程(2-3个)
|
||||
- **小话题(精准)**: #深圳探店 #程序员日常(2-3个)
|
||||
- 总计 5-8 个标签为最佳
|
||||
|
||||
## 适用场景
|
||||
|
||||
- 创作抖音/快手短视频脚本
|
||||
- 优化视频开头提升完播率
|
||||
- 为视频选择话题标签
|
||||
- 策划系列内容的选题方向
|
||||
- 将长内容改编为短视频脚本
|
||||
|
||||
## 不适用场景
|
||||
|
||||
- 写B站中长视频脚本(请用 bilibili-helper)
|
||||
- 需要真实拍摄指导(本技能专注文案脚本)
|
||||
- 直播话术(直播场景与短视频逻辑不同)
|
||||
- 涉及虚假宣传或违规内容
|
||||
|
||||
## 发布建议
|
||||
|
||||
- **黄金时段**: 早7-9点、午12-14点、晚18-22点
|
||||
- **频率**: 日更或隔日更,保持账号活跃
|
||||
- **封面**: 竖屏 9:16,文字大且少(不超过8字)
|
||||
- **合拍/合集**: 善用抖音合拍功能蹭热度
|
||||
@@ -1,97 +0,0 @@
|
||||
---
|
||||
name: excel-helper
|
||||
description: "Excel 表格助手 - 公式编写、数据透视表、图表可视化、宏自动化、数据清洗"
|
||||
metadata: { "openclaw": { "emoji": "📊" } }
|
||||
---
|
||||
|
||||
# Excel 数据助手
|
||||
|
||||
专业的数据处理和分析工具,帮你高效完成表格制作和数据计算。
|
||||
|
||||
## 核心功能
|
||||
|
||||
- **公式编写**: VLOOKUP、SUMIF、数据汇总、条件统计
|
||||
- **数据透视表**: 快速汇总、分析、交叉表
|
||||
- **图表可视化**: 柱状图、折线图、饼图、组合图
|
||||
- **数据清洗**: 去重、填充、格式统一
|
||||
- **宏自动化**: VBA 宏录制和编写
|
||||
|
||||
## 常用公式模板
|
||||
|
||||
### 汇总统计
|
||||
```excel
|
||||
=SUMIF(A:A,">=2024-01-01",B:B) -- 条件求和
|
||||
=COUNTIF(B:B,">100") -- 条件计数
|
||||
=AVERAGEIF(C:C,"北京",D:D) -- 条件平均
|
||||
```
|
||||
|
||||
### 查找引用
|
||||
```excel
|
||||
=VLOOKUP(E2,数据源!A:C,3,FALSE) -- 垂直查找
|
||||
=INDEX(B:B,MATCH(D2,A:A,0)) -- 反向查找
|
||||
=XLOOKUP(查找值,查找列,返回列) -- 现代查找
|
||||
```
|
||||
|
||||
### 日期处理
|
||||
```excel
|
||||
=TEXT(A2,"YYYY-MM") -- 格式化日期
|
||||
=EDATE(B2,3) -- 日期加减
|
||||
=WEEKDAY(C2,2) -- 星期几
|
||||
```
|
||||
|
||||
## 图表选择指南
|
||||
|
||||
| 数据类型 | 推荐图表 |
|
||||
|----------|----------|
|
||||
| 趋势变化 | 折线图 |
|
||||
| 比较大小 | 柱状图 |
|
||||
| 占比构成 | 饼图 |
|
||||
| 关联关系 | 散点图 |
|
||||
| 多维数据 | 雷达图 |
|
||||
|
||||
## 数据透视表技巧
|
||||
|
||||
1. 拖拽字段到行/列/值/筛选区域
|
||||
2. 值汇总方式:求和、计数、平均值
|
||||
3. 值显示方式:百分比、累计
|
||||
4. 切片器:交互式筛选
|
||||
|
||||
## 使用示例
|
||||
|
||||
```
|
||||
帮我写一个统计月度销售额的公式,需要按地区分类汇总
|
||||
```
|
||||
|
||||
```
|
||||
有一份客户数据表,有很多重复记录,帮我写清洗步骤
|
||||
```
|
||||
|
||||
```
|
||||
把这些数据做成可视化图表,展示季度对比趋势
|
||||
```
|
||||
|
||||
## VBA 常用宏
|
||||
|
||||
```vba
|
||||
Sub 批量格式化()
|
||||
For Each cell In Selection
|
||||
If cell.Value > 100 Then
|
||||
cell.Interior.Color = RGB(255, 0, 0)
|
||||
End If
|
||||
Next
|
||||
End Sub
|
||||
```
|
||||
|
||||
## 适用场景
|
||||
|
||||
- 数据汇总报表
|
||||
- 财务对账
|
||||
- 销售数据分析
|
||||
- 库存管理
|
||||
- 问卷调查结果分析
|
||||
|
||||
## 不适用场景
|
||||
|
||||
- Word 长文撰写(用 word-writer)
|
||||
- PPT 演示制作(用 ppt-designer)
|
||||
- 复杂编程逻辑(用 deepseek-helper)
|
||||
@@ -1,50 +0,0 @@
|
||||
---
|
||||
name: image-compress
|
||||
description: "图片压缩/转换 - 批量压缩、改尺寸、转格式(jpg/png/webp),本地处理不失隐私"
|
||||
metadata: { "openclaw": { "emoji": "🖼️" } }
|
||||
---
|
||||
|
||||
# 图片压缩 / 转换
|
||||
|
||||
帮用户压缩图片体积、调整尺寸、转换格式,全部本地处理,照片不外传。
|
||||
|
||||
## 能力概述
|
||||
|
||||
- **压缩**:降低体积(控制质量/分辨率),适合发邮件、传微信
|
||||
- **改尺寸**:按宽高或百分比缩放
|
||||
- **转格式**:jpg / png / webp 互转(webp 体积最小)
|
||||
- **批量**:处理整个文件夹
|
||||
|
||||
## 操作方式
|
||||
|
||||
用 Bash 工具,Python 的 Pillow 库(跨平台、纯本地):
|
||||
|
||||
```bash
|
||||
python -c "import PIL" 2>/dev/null || pip install -q Pillow
|
||||
|
||||
# 单张压缩(质量 75,宽度限制 1600px 等比缩放)
|
||||
python - <<'PY'
|
||||
from PIL import Image
|
||||
im = Image.open("input.jpg")
|
||||
if im.width > 1600:
|
||||
im = im.resize((1600, int(im.height*1600/im.width)))
|
||||
im.convert("RGB").save("output.jpg", "JPEG", quality=75, optimize=True)
|
||||
print("已压缩 -> output.jpg")
|
||||
PY
|
||||
|
||||
# 批量:当前目录所有 jpg/png -> webp(体积更小)
|
||||
python - <<'PY'
|
||||
from PIL import Image
|
||||
import glob, os
|
||||
for f in glob.glob("*.jpg") + glob.glob("*.png"):
|
||||
out = os.path.splitext(f)[0] + ".webp"
|
||||
Image.open(f).save(out, "WEBP", quality=80)
|
||||
print(f"{f} -> {out}")
|
||||
PY
|
||||
```
|
||||
|
||||
## 使用建议
|
||||
|
||||
- 处理前 `ls -lh` 看原图体积,处理后再 `ls -lh` 对比,告诉用户压缩了多少
|
||||
- 默认不覆盖原图(输出新文件名),除非用户明确要求覆盖
|
||||
- 含透明通道的 PNG 转 JPG 会丢透明,转 webp 可保留——按需选格式
|
||||
@@ -1,59 +0,0 @@
|
||||
---
|
||||
name: pdf-toolkit
|
||||
description: "PDF 工具箱 - 合并/拆分/提取文字/转图片,纯命令行处理本地 PDF"
|
||||
metadata: { "openclaw": { "emoji": "📄" } }
|
||||
---
|
||||
|
||||
# PDF 工具箱
|
||||
|
||||
帮用户对本地 PDF 文件做常见处理:合并、拆分、提取文字、转图片。优先用系统已有工具,
|
||||
没有就用 Python 的 pypdf(轻量纯 Python,必要时 `pip install pypdf` 自动安装)。
|
||||
|
||||
## 能力概述
|
||||
|
||||
- **合并**:把多个 PDF 拼成一个
|
||||
- **拆分**:按页拆成多份,或抽取指定页
|
||||
- **提取文字**:把 PDF 内容导出成纯文本,便于总结/检索
|
||||
- **页数/信息**:查看 PDF 总页数、元信息
|
||||
|
||||
## 操作方式
|
||||
|
||||
用 Bash 工具执行。下面以 pypdf 为例(跨平台、无需 Office/Acrobat):
|
||||
|
||||
```bash
|
||||
# 确保依赖(仅首次,已装会秒过)
|
||||
python -c "import pypdf" 2>/dev/null || pip install -q pypdf
|
||||
|
||||
# 合并 a.pdf b.pdf -> merged.pdf
|
||||
python - <<'PY'
|
||||
from pypdf import PdfWriter
|
||||
w = PdfWriter()
|
||||
for f in ["a.pdf", "b.pdf"]:
|
||||
w.append(f)
|
||||
w.write("merged.pdf"); w.close()
|
||||
print("已合并 -> merged.pdf")
|
||||
PY
|
||||
|
||||
# 提取全部文字
|
||||
python - <<'PY'
|
||||
from pypdf import PdfReader
|
||||
r = PdfReader("input.pdf")
|
||||
print("\n".join((p.extract_text() or "") for p in r.pages))
|
||||
PY
|
||||
|
||||
# 抽取第 1-3 页 -> sub.pdf
|
||||
python - <<'PY'
|
||||
from pypdf import PdfReader, PdfWriter
|
||||
r = PdfReader("input.pdf"); w = PdfWriter()
|
||||
for i in range(0, 3):
|
||||
w.add_page(r.pages[i])
|
||||
w.write("sub.pdf"); w.close()
|
||||
print("已抽取 1-3 页 -> sub.pdf")
|
||||
PY
|
||||
```
|
||||
|
||||
## 使用建议
|
||||
|
||||
- 处理前先 `ls` 确认文件存在、用 `python -c "from pypdf import PdfReader;print(len(PdfReader('x.pdf').pages))"` 看页数
|
||||
- 扫描件(图片型 PDF)提取不到文字属正常,需 OCR(提示用户)
|
||||
- 输出文件默认放在与源文件同目录,操作完告诉用户生成的文件名
|
||||
@@ -1,122 +0,0 @@
|
||||
---
|
||||
name: ppt-designer
|
||||
description: "PPT 演示助手 - 幻灯片设计、内容结构、视觉优化、动画建议、演讲稿"
|
||||
metadata: { "openclaw": { "emoji": "📽️" } }
|
||||
---
|
||||
|
||||
# PPT 演示助手
|
||||
|
||||
专业的演示文稿设计工具,帮你打造高效的商务演讲。
|
||||
|
||||
## 核心功能
|
||||
|
||||
- **内容结构**: 逻辑清晰的幻灯片大纲
|
||||
- **版式设计**: 封面、目录、内容页、结束页
|
||||
- **视觉优化**: 配色方案、图表设计、图标选择
|
||||
- **动画建议**: 幻灯片切换、元素动画
|
||||
- **演讲稿**: 每页演讲要点和话术
|
||||
|
||||
## 幻灯片结构模板
|
||||
|
||||
### 标准商务汇报 (10页)
|
||||
```
|
||||
1. 封面:标题 + 演讲者 + 日期
|
||||
2. 目录:内容概览
|
||||
3. 背景:为什么要做这个项目
|
||||
4. 目标:我们要达成什么
|
||||
5. 方法:怎么做
|
||||
6. 进展:目前做到了什么程度
|
||||
7. 数据:关键成果展示
|
||||
8. 分析:数据说明了什么
|
||||
9. 计划:下一步做什么
|
||||
10. 结束:谢谢 + Q&A
|
||||
```
|
||||
|
||||
### 产品发布 (15页)
|
||||
```
|
||||
1. 封面:产品名称 + 品牌
|
||||
2. 目录
|
||||
3. 市场需求:痛点分析
|
||||
4. 产品介绍:核心功能
|
||||
5. 技术架构:创新点
|
||||
6. 产品演示:截图/视频
|
||||
7. 竞品对比:优势凸显
|
||||
8. 用户案例:成功故事
|
||||
9. 市场前景:规模预测
|
||||
10. 商业模式:盈利方式
|
||||
11. 推广策略:渠道规划
|
||||
12. 团队介绍:核心成员
|
||||
13. 里程碑:发展历程
|
||||
14. 融资计划:资金用途
|
||||
15. 结束页:联系方式
|
||||
```
|
||||
|
||||
## 内容填充原则
|
||||
|
||||
| 页面 | 文字量 | 重点 |
|
||||
|------|--------|------|
|
||||
| 封面 | 标题 + 副标题 | 简洁有力 |
|
||||
| 目录 | 3-5 个章节 | 结构清晰 |
|
||||
| 内容页 | 6x6 法则 | 一页一观点 |
|
||||
| 图表页 | 标题 + 图表 + 结论 | 数据说话 |
|
||||
| 结束页 | 谢谢 + 联系方式 | 行动号召 |
|
||||
|
||||
## 视觉设计原则
|
||||
|
||||
1. **10-20-30 法则**: 10 页以内,20 分钟,30 号字体
|
||||
2. **KISS 原则**: Keep It Simple, Stupid
|
||||
3. **图片为主**: 一图胜千言
|
||||
4. **留白**: 页面四周留 10% 边距
|
||||
5. **对齐**: 元素左对齐或居中
|
||||
6. **色彩**: 主色 + 辅色 + 点缀色,不超过 3 种
|
||||
|
||||
## 配色方案推荐
|
||||
|
||||
| 场景 | 主色 | 辅色 |
|
||||
|------|------|------|
|
||||
| 商务汇报 | 蓝色 #2C5282 | 灰色 #718096 |
|
||||
| 科技产品 | 深蓝 #1A365D | 青色 #319795 |
|
||||
| 金融投资 | 金色 #D69E2E | 深灰 #2D3748 |
|
||||
| 医疗健康 | 绿色 #276749 | 浅绿 #68D391 |
|
||||
| 教育培训 | 橙色 #DD6B20 | 暖灰 #F7FAFC |
|
||||
|
||||
## 使用示例
|
||||
|
||||
```
|
||||
帮我设计一个产品发布会 PPT,包含 15 页
|
||||
```
|
||||
|
||||
```
|
||||
这是一份技术方案,帮我转成演示文稿结构
|
||||
```
|
||||
|
||||
```
|
||||
这张幻灯片内容太拥挤了,帮我精简
|
||||
```
|
||||
|
||||
```
|
||||
提供一个互联网产品路演的 PPT 结构
|
||||
```
|
||||
|
||||
## 常用快捷键
|
||||
|
||||
| 功能 | 快捷键 |
|
||||
|------|--------|
|
||||
| 新建幻灯片 | Ctrl+M |
|
||||
| 复制幻灯片 | Ctrl+D |
|
||||
| 放映 | F5 |
|
||||
| 画笔标注 | Ctrl+P |
|
||||
| 激光笔 | Ctrl+L |
|
||||
|
||||
## 适用场景
|
||||
|
||||
- 商务汇报演示
|
||||
- 产品发布会
|
||||
- 创业路演
|
||||
- 培训课件
|
||||
- 会议演讲
|
||||
|
||||
## 不适用场景
|
||||
|
||||
- Word 长文撰写(用 word-writer)
|
||||
- Excel 数据分析(用 excel-helper)
|
||||
@@ -1,52 +0,0 @@
|
||||
---
|
||||
name: qrcode-maker
|
||||
description: "二维码生成 - 把网址/文本/WiFi 信息生成二维码图片,本地离线生成"
|
||||
metadata: { "openclaw": { "emoji": "🔳" } }
|
||||
---
|
||||
|
||||
# 二维码生成
|
||||
|
||||
帮用户把网址、文本、联系方式、WiFi 信息生成二维码图片,本地离线生成,不上传任何数据。
|
||||
|
||||
## 能力概述
|
||||
|
||||
- **网址/文本二维码**:任意内容转二维码 PNG
|
||||
- **WiFi 二维码**:扫码即连,免手输密码
|
||||
- **终端预览**:直接在对话里用字符画展示二维码
|
||||
|
||||
## 操作方式
|
||||
|
||||
用 Bash 工具,Python 的 qrcode 库(轻量、纯本地):
|
||||
|
||||
```bash
|
||||
python -c "import qrcode" 2>/dev/null || pip install -q "qrcode[pil]"
|
||||
|
||||
# 网址/文本 -> PNG
|
||||
python - <<'PY'
|
||||
import qrcode
|
||||
qrcode.make("https://u-claw.org").save("qrcode.png")
|
||||
print("已生成 -> qrcode.png")
|
||||
PY
|
||||
|
||||
# WiFi 二维码(扫码自动连网)
|
||||
python - <<'PY'
|
||||
import qrcode
|
||||
ssid, pwd, enc = "MyWiFi", "password123", "WPA" # enc: WPA / WEP / nopass
|
||||
data = f"WIFI:T:{enc};S:{ssid};P:{pwd};;"
|
||||
qrcode.make(data).save("wifi-qr.png")
|
||||
print("已生成 WiFi 二维码 -> wifi-qr.png")
|
||||
PY
|
||||
|
||||
# 终端字符画预览(不存文件,直接看)
|
||||
python - <<'PY'
|
||||
import qrcode
|
||||
q = qrcode.QRCode(); q.add_data("https://u-claw.org"); q.make()
|
||||
q.print_ascii(invert=True)
|
||||
PY
|
||||
```
|
||||
|
||||
## 使用建议
|
||||
|
||||
- 先确认用户要编码的内容、是否保存为文件
|
||||
- 生成后用 Read 工具把 PNG 展示给用户看
|
||||
- 内容过长(如整段文字)二维码会很密集,提示用户改用短链
|
||||
@@ -1,51 +0,0 @@
|
||||
---
|
||||
name: web-to-markdown
|
||||
description: "网页转 Markdown - 抓取任意网页正文并转成干净的 Markdown,便于阅读/收藏/二次创作"
|
||||
metadata: { "openclaw": { "emoji": "🔗" } }
|
||||
---
|
||||
|
||||
# 网页转 Markdown
|
||||
|
||||
把用户给的网页链接抓下来,提取正文,转成干净的 Markdown,方便保存、总结或二次创作。
|
||||
|
||||
## 能力概述
|
||||
|
||||
- **正文提取**:去掉导航/广告/页脚,只留主体内容
|
||||
- **转 Markdown**:标题、列表、链接、代码块保留结构
|
||||
- **保存**:可写入本地 .md 文件
|
||||
|
||||
## 操作方式
|
||||
|
||||
用 Bash 工具。首选 jina.ai 的免费 reader(无需依赖,最省事):
|
||||
|
||||
```bash
|
||||
# 最简:jina reader 直接返回干净 Markdown(在链接前加 https://r.jina.ai/)
|
||||
curl -s "https://r.jina.ai/https://example.com/article" -o article.md
|
||||
echo "已保存 -> article.md"; head -40 article.md
|
||||
```
|
||||
|
||||
离线或 jina 不可用时,用 Python 本地转换:
|
||||
|
||||
```bash
|
||||
python -c "import markdownify,requests" 2>/dev/null || pip install -q markdownify requests beautifulsoup4
|
||||
python - <<'PY'
|
||||
import requests, re
|
||||
from bs4 import BeautifulSoup
|
||||
from markdownify import markdownify as md
|
||||
url = "https://example.com/article"
|
||||
html = requests.get(url, timeout=15, headers={"User-Agent":"Mozilla/5.0"}).text
|
||||
soup = BeautifulSoup(html, "html.parser")
|
||||
for t in soup(["script","style","nav","footer","aside"]): t.decompose()
|
||||
body = soup.find("article") or soup.find("main") or soup.body
|
||||
out = md(str(body), heading_style="ATX")
|
||||
out = re.sub(r"\n{3,}", "\n\n", out).strip()
|
||||
open("article.md","w",encoding="utf-8").write(out)
|
||||
print("已保存 -> article.md,", len(out), "字")
|
||||
PY
|
||||
```
|
||||
|
||||
## 使用建议
|
||||
|
||||
- 先问用户要不要保存到文件、文件名
|
||||
- 微信公众号/知乎等需要登录的页面可能抓取受限,如失败如实告知
|
||||
- 抓取后可顺手做"总结要点",结合 china-search / deepseek-helper 技能
|
||||
@@ -1,106 +0,0 @@
|
||||
---
|
||||
name: wechat-article
|
||||
description: "微信公众号文章写作 - 文章结构、排版规范、阅读转化优化、封面建议"
|
||||
metadata: { "openclaw": { "emoji": "💚" } }
|
||||
---
|
||||
|
||||
# 微信公众号文章写作助手
|
||||
|
||||
帮助运营者写出高阅读、高转发的微信公众号文章,覆盖从选题到排版的全流程。
|
||||
|
||||
## 功能概述
|
||||
|
||||
- **文章结构**: 标题 → 引言 → 正文 → 总结 → 引导关注
|
||||
- **排版规范**: 符合微信阅读习惯的段落、字号、配色建议
|
||||
- **标题优化**: 生成符合微信生态的标题(主标题 + 副标题)
|
||||
- **封面建议**: 头图尺寸(900x383)、配色、文字排版建议
|
||||
- **互动引导**: 点赞、在看、转发的 CTA 话术
|
||||
|
||||
## 文章结构模板
|
||||
|
||||
```
|
||||
【标题】控制在 30 字以内,前 15 字抓眼球
|
||||
【摘要】出现在分享卡片,控制在 54 字
|
||||
|
||||
【引言】1-2 段,建立共鸣或抛出问题
|
||||
(配图:与主题相关的首图)
|
||||
|
||||
【正文】
|
||||
## 小标题一
|
||||
内容段落...(每段 3-5 行)
|
||||
|
||||
## 小标题二
|
||||
内容段落...
|
||||
|
||||
## 小标题三
|
||||
内容段落...
|
||||
|
||||
【总结】回扣主题,升华观点
|
||||
|
||||
【尾部】
|
||||
觉得有收获?点个「在看」让更多人看到 👇
|
||||
关注我,每周分享 XX 干货
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 写完整文章
|
||||
```
|
||||
帮我写一篇公众号文章,主题是"为什么35岁是程序员的分水岭",3000字左右
|
||||
```
|
||||
|
||||
### 优化标题
|
||||
```
|
||||
文章主题是AI工具推荐,帮我写10个公众号标题
|
||||
```
|
||||
|
||||
### 改写排版
|
||||
```
|
||||
这篇文章内容不错但排版太密了,帮我优化成适合微信阅读的格式
|
||||
```
|
||||
|
||||
### 写引导语
|
||||
```
|
||||
帮我写一段文章开头,主题是教育焦虑,要能引起家长共鸣
|
||||
```
|
||||
|
||||
## 微信排版规范
|
||||
|
||||
- **正文字号**: 15-16px(推荐15px)
|
||||
- **字间距**: 1-2px
|
||||
- **行间距**: 1.75-2倍
|
||||
- **段间距**: 空一行
|
||||
- **两端缩进**: 16px(手机端留白)
|
||||
- **配色**: 正文 #3f3f3f,强调 #007AFF 或品牌色
|
||||
- **图片**: 宽度 100%,JPG 格式,单张不超过 5MB
|
||||
|
||||
## 标题技巧
|
||||
|
||||
1. **数字开头**: "3个方法/5分钟学会"
|
||||
2. **疑问式**: "为什么XX?答案出乎意料"
|
||||
3. **对比式**: "XX和XX的差距,在于这一点"
|
||||
4. **紧迫感**: "再不XX就晚了"
|
||||
5. **权威背书**: "XX专家/XX万人验证"
|
||||
|
||||
## 适用场景
|
||||
|
||||
- 撰写公众号原创文章(干货、观点、故事等)
|
||||
- 优化已有文章的标题和结构
|
||||
- 为文章设计排版方案
|
||||
- 写分享卡片的摘要文案
|
||||
- 设计文章尾部的关注引导
|
||||
|
||||
## 不适用场景
|
||||
|
||||
- 写小红书笔记(风格不同,请用 xiaohongshu-writer)
|
||||
- 需要直接在微信编辑器排版(本技能输出 Markdown 格式)
|
||||
- 涉及时政新闻等需要资质的内容
|
||||
- 需要真实数据支撑的行业报告(框架可用,数据需自备)
|
||||
|
||||
## 提升阅读量的关键
|
||||
|
||||
- 标题决定打开率,前 15 字是关键
|
||||
- 首屏内容决定是否继续阅读
|
||||
- 文章长度建议 1500-3000 字(太长容易跳出)
|
||||
- 每 300 字配一张图,打破阅读疲劳
|
||||
- 结尾的「在看」引导直接影响二次传播
|
||||
@@ -1,118 +0,0 @@
|
||||
---
|
||||
name: weibo-poster
|
||||
description: "微博内容创作 - 140字优化、话题热搜、@提及策略、配图描述、发布时机"
|
||||
metadata: { "openclaw": { "emoji": "🔴" } }
|
||||
---
|
||||
|
||||
# 微博内容创作助手
|
||||
|
||||
帮你写出高转发、高互动的微博内容,掌握微博生态的流量密码。
|
||||
|
||||
## 功能概述
|
||||
|
||||
- **字数优化**: 140 字精炼表达,或长微博结构优化
|
||||
- **话题策略**: 热搜话题 + 垂直话题组合使用
|
||||
- **@提及**: 合理 @ 相关大V和官方账号增加曝光
|
||||
- **配图建议**: 图片数量(1/3/6/9宫格)和内容建议
|
||||
- **发布时机**: 根据目标人群推荐最佳发布时间
|
||||
|
||||
## 微博内容模板
|
||||
|
||||
### 短微博(140字以内)
|
||||
```
|
||||
【观点/金句】一句话表达核心观点
|
||||
【补充说明】1-2句展开
|
||||
【话题标签】#话题一# #话题二#
|
||||
【配图】建议搭配的图片类型
|
||||
```
|
||||
|
||||
### 长微博
|
||||
```
|
||||
【标题】简短有力
|
||||
【正文】分段落论述,每段 2-3 句
|
||||
【总结】一句话总结
|
||||
【话题 + CTA】#话题# + 转发/评论引导
|
||||
```
|
||||
|
||||
### 九宫格图文
|
||||
```
|
||||
【文案】简短描述 + 情绪表达
|
||||
【9张图片】内容规划和排列顺序建议
|
||||
【话题】相关话题标签
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 写微博
|
||||
```
|
||||
帮我写一条关于"AI取代人类工作"的微博,要有观点有态度
|
||||
```
|
||||
|
||||
### 蹭热搜
|
||||
```
|
||||
今天热搜是#XX事件#,帮我写一条相关微博
|
||||
```
|
||||
|
||||
### 产品推广
|
||||
```
|
||||
帮我写一条推广我新产品的微博,产品是XX,目标用户是XX
|
||||
```
|
||||
|
||||
### 日常分享
|
||||
```
|
||||
帮我写一条记录深圳生活的微博,配九宫格照片描述
|
||||
```
|
||||
|
||||
## 140字写作技巧
|
||||
|
||||
- **先写后删**: 先完整表达,再精简到 140 字
|
||||
- **一条一观点**: 不要在一条微博里讲太多
|
||||
- **金句收尾**: 最后一句要值得被转发
|
||||
- **留白引发讨论**: 不要说太满,给评论区留空间
|
||||
- **善用符号**: |、/、→ 等符号节省字数
|
||||
|
||||
## 话题标签策略
|
||||
|
||||
| 类型 | 说明 | 示例 |
|
||||
|-----|------|------|
|
||||
| 热搜话题 | 当前微博热搜 | #热搜关键词# |
|
||||
| 超话 | 垂直社区 | #程序员超话# |
|
||||
| 自建话题 | 个人品牌 | #东升聊AI# |
|
||||
| 节日话题 | 节日营销 | #春节# |
|
||||
|
||||
## @提及策略
|
||||
|
||||
- @ 内容相关的行业大V(可能被转发)
|
||||
- @ 官方媒体账号(适合新闻类内容)
|
||||
- @ 品牌官方号(适合测评/反馈内容)
|
||||
- 不要无意义 @ 过多人(容易被判定垃圾信息)
|
||||
|
||||
## 配图建议
|
||||
|
||||
- **1张图**: 重点突出,适合海报/截图
|
||||
- **3张图**: 对比/过程展示
|
||||
- **6张图**: 故事叙述/多角度展示
|
||||
- **9张图**: 九宫格,视觉冲击力最强
|
||||
- **图片尺寸**: 建议 1080x1080 或 1080x1440
|
||||
|
||||
## 适用场景
|
||||
|
||||
- 日常微博内容创作
|
||||
- 蹭热搜话题获取曝光
|
||||
- 产品/活动的微博推广
|
||||
- 打造微博个人品牌
|
||||
- 写转发语增加传播力
|
||||
|
||||
## 不适用场景
|
||||
|
||||
- 写长文章(请用 wechat-article 或 zhihu-writer)
|
||||
- 写短视频脚本(请用 douyin-script)
|
||||
- 需要实时获取微博热搜数据
|
||||
- 微博投放广告的策略(涉及付费推广)
|
||||
|
||||
## 发布时间建议
|
||||
|
||||
- **工作日**: 午休 12:00-13:00、下班后 18:00-20:00
|
||||
- **周末**: 上午 10:00-11:00、晚上 20:00-22:00
|
||||
- **热点事件**: 第一时间发布,抢占话题先机
|
||||
- **避开时段**: 凌晨 0:00-7:00(除非目标是夜猫子群体)
|
||||
@@ -1,76 +0,0 @@
|
||||
---
|
||||
name: word-writer
|
||||
description: "Word 文档助手 - 文章撰写、格式排版、模板选择、目录生成、批量处理"
|
||||
metadata: { "openclaw": { "emoji": "📝" } }
|
||||
---
|
||||
|
||||
# Word 文档助手
|
||||
|
||||
专业的 Word 文档创作和排版工具,帮你快速生成规范的商务文档。
|
||||
|
||||
## 核心功能
|
||||
|
||||
- **文档撰写**: 报告、方案、合同、简历等各类文档
|
||||
- **格式排版**: 标题层级、正文样式、段落间距、页眉页脚
|
||||
- **模板选择**: 根据场景推荐合适的文档模板
|
||||
- **目录生成**: 自动生成规范目录结构
|
||||
- **批量处理**: 批量替换、批量格式调整
|
||||
|
||||
## 文档结构模板
|
||||
|
||||
### 商务报告
|
||||
```
|
||||
【封面】标题 + 副标题 + 日期 + 公司名
|
||||
【目录】自动生成
|
||||
【前言】背景、目的、范围
|
||||
【正文】分章节,每章含图表
|
||||
【结论】核心发现 + 建议
|
||||
【附录】数据来源、参考资料
|
||||
```
|
||||
|
||||
### 工作方案
|
||||
```
|
||||
1. 背景分析(SWOT)
|
||||
2. 目标设定(SMART)
|
||||
3. 执行计划(时间表 + 责任人)
|
||||
4. 资源需求(预算 + 人员)
|
||||
5. 风险预案
|
||||
```
|
||||
|
||||
## 常用快捷键
|
||||
|
||||
| 功能 | 快捷键 |
|
||||
|------|--------|
|
||||
| 加粗 | Ctrl+B |
|
||||
| 斜体 | Ctrl+I |
|
||||
| 下划线 | Ctrl+U |
|
||||
| 标题1 | Ctrl+Alt+1 |
|
||||
| 标题2 | Ctrl+Alt+2 |
|
||||
| 插入目录 | Ctrl+Shift+O |
|
||||
|
||||
## 使用示例
|
||||
|
||||
```
|
||||
帮我写一份项目立项报告,主题是"电商平台用户增长方案"
|
||||
```
|
||||
|
||||
```
|
||||
这篇论文的格式太乱了,帮我调整成学术规范格式
|
||||
```
|
||||
|
||||
```
|
||||
生成一个产品介绍文档的模板,包含产品特点、优势、应用场景
|
||||
```
|
||||
|
||||
## 适用场景
|
||||
|
||||
- 商务文档:报告、方案、计划书
|
||||
- 学术文档:论文、课题申报书
|
||||
- 行政文档:通知、请示、会议纪要
|
||||
- 个人文档:简历、求职信
|
||||
|
||||
## 不适用场景
|
||||
|
||||
- 需要真实数据分析(用 excel-helper)
|
||||
- 复杂表格制作(用 excel-helper)
|
||||
- PPT 制作(用 ppt-designer)
|
||||
@@ -1,91 +0,0 @@
|
||||
---
|
||||
name: xiaohongshu-writer
|
||||
description: "小红书笔记写作助手 - 标题优化、emoji策略、话题标签、封面文案、笔记结构"
|
||||
metadata: { "openclaw": { "emoji": "📕" } }
|
||||
---
|
||||
|
||||
# 小红书笔记写作助手
|
||||
|
||||
专业的小红书内容创作工具,帮你写出高互动、高收藏的爆款笔记。
|
||||
|
||||
## 功能概述
|
||||
|
||||
- **标题生成**: 符合小红书调性的吸睛标题(含数字、emoji、悬念)
|
||||
- **正文结构**: hook 开头 → 干货内容 → CTA 引导收藏/关注
|
||||
- **Emoji 策略**: 段落间自然穿插 emoji,提升阅读体验
|
||||
- **话题标签**: 根据内容推荐热门 # 话题,提升曝光
|
||||
- **封面文案**: 适合做封面图的短文案建议
|
||||
|
||||
## 笔记结构模板
|
||||
|
||||
```
|
||||
【标题】数字 + 痛点/好奇心 + emoji
|
||||
例: 打工人必看!5个让老板刮目相看的汇报技巧 💼
|
||||
|
||||
【开头 Hook】1-2句直击痛点,让人想继续看
|
||||
例: 每次汇报都紧张到手抖?其实你只是没掌握方法!
|
||||
|
||||
【正文】分点罗列,每点 2-3 行
|
||||
1️⃣ 第一点...
|
||||
2️⃣ 第二点...
|
||||
(穿插个人经验/截图说明)
|
||||
|
||||
【结尾 CTA】引导互动
|
||||
例: 觉得有用就 ❤️ 收藏起来慢慢看!有问题评论区见~
|
||||
|
||||
【话题标签】
|
||||
#职场干货 #汇报技巧 #打工人必看
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 写笔记
|
||||
```
|
||||
帮我写一篇小红书笔记,主题是"独居女生的10个安全tips"
|
||||
```
|
||||
|
||||
### 优化标题
|
||||
```
|
||||
我写了篇关于护肤的笔记,帮我起5个爆款标题
|
||||
```
|
||||
|
||||
### 话题推荐
|
||||
```
|
||||
我要发一篇深圳美食探店笔记,推荐相关话题标签
|
||||
```
|
||||
|
||||
### 改写风格
|
||||
```
|
||||
这段文字太正式了,帮我改成小红书风格(附原文)
|
||||
```
|
||||
|
||||
## 标题公式
|
||||
|
||||
1. **数字法**: "5个/10种/100元以内" + 关键词
|
||||
2. **反差法**: "月薪3千 vs 月薪3万的区别"
|
||||
3. **身份法**: "作为XX人/XX年经验告诉你"
|
||||
4. **测评法**: "亲测有效!" + 结果
|
||||
5. **合集法**: "XX合集|一篇搞定"
|
||||
|
||||
## 适用场景
|
||||
|
||||
- 创作各类小红书图文笔记
|
||||
- 优化已有笔记的标题和结构
|
||||
- 生成适合做封面的文案
|
||||
- 为笔记选择合适的话题标签
|
||||
- 学习小红书爆款写作技巧
|
||||
|
||||
## 不适用场景
|
||||
|
||||
- 写微信公众号长文(风格不同,请用 wechat-article)
|
||||
- 写知乎专业回答(调性不同,请用 zhihu-writer)
|
||||
- 需要真实数据/测评结果(本技能生成文案框架,数据需自备)
|
||||
- 违反小红书社区规范的内容(医疗、金融等敏感领域需谨慎)
|
||||
|
||||
## 写作要点
|
||||
|
||||
- 每段不超过 3 行,保持移动端阅读舒适度
|
||||
- emoji 不要堆砌,每段 1-2 个即可
|
||||
- 话题标签 3-8 个为佳,混合大小话题
|
||||
- 封面文案控制在 10 字以内,字大醒目
|
||||
- 发布时间建议: 早7-9点、午12-14点、晚20-22点
|
||||
@@ -1,111 +0,0 @@
|
||||
---
|
||||
name: zhihu-writer
|
||||
description: "知乎回答/文章写作 - 回答结构、专业语气、引用规范、话题关联、盐值优化"
|
||||
metadata: { "openclaw": { "emoji": "📝" } }
|
||||
---
|
||||
|
||||
# 知乎回答/文章写作助手
|
||||
|
||||
帮助创作者写出高赞、高收藏的知乎内容,包括问题回答和专栏文章。
|
||||
|
||||
## 功能概述
|
||||
|
||||
- **回答结构**: 先说结论 → 展开论证 → 总结升华
|
||||
- **专业语气**: 知乎特有的"专业但不装"的表达风格
|
||||
- **引用规范**: 数据来源标注、论文引用格式
|
||||
- **话题关联**: 推荐关联话题标签提升曝光
|
||||
- **盐值优化**: 内容质量建议,提升知乎盐值
|
||||
|
||||
## 回答结构模板
|
||||
|
||||
```
|
||||
【开头】直接回答问题 + 亮明立场(1-2句)
|
||||
"作为一个做了5年XX的人,我的回答是:..."
|
||||
|
||||
【分割线】
|
||||
---
|
||||
|
||||
【正文】分点论述,有理有据
|
||||
一、第一个论点
|
||||
论据 + 数据/案例支撑
|
||||
(可配图表增强说服力)
|
||||
|
||||
二、第二个论点
|
||||
论据 + 个人经验
|
||||
|
||||
三、第三个论点
|
||||
论据 + 行业分析
|
||||
|
||||
【总结】回扣问题,给出实操建议
|
||||
|
||||
【引流】(可选)
|
||||
更多XX内容可以关注我的专栏:XXX
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 写回答
|
||||
```
|
||||
帮我回答知乎问题"普通人如何开始学习AI?",要专业但通俗
|
||||
```
|
||||
|
||||
### 写专栏文章
|
||||
```
|
||||
帮我写一篇知乎专栏文章,主题是"为什么我不建议用ChatGPT学编程"
|
||||
```
|
||||
|
||||
### 优化回答
|
||||
```
|
||||
我的这个回答数据不好,帮我分析并改写(附原文)
|
||||
```
|
||||
|
||||
### 蹭热点
|
||||
```
|
||||
最近DeepSeek很火,帮我写一个相关的知乎回答思路
|
||||
```
|
||||
|
||||
## 知乎写作风格指南
|
||||
|
||||
### 语气要点
|
||||
- **自信但不傲慢**: "以我的经验来看" 而非 "我告诉你们"
|
||||
- **专业但易懂**: 技术术语需要解释,不要秀词汇量
|
||||
- **有态度但包容**: 可以有观点,但尊重不同立场
|
||||
- **真诚分享**: 多用个人经验和案例,少用空洞理论
|
||||
|
||||
### 格式要点
|
||||
- 段落清晰,善用加粗标注重点
|
||||
- 长回答用 h2/h3 分节
|
||||
- 数据和引用标明来源
|
||||
- 适当使用列表和表格整理信息
|
||||
- 配图要有信息量,不要纯装饰
|
||||
|
||||
## 盐值优化建议
|
||||
|
||||
- **内容质量**: 回答字数建议 500-3000 字,有干货
|
||||
- **原创度**: 避免复制粘贴,用自己的语言表达
|
||||
- **互动**: 回复评论,参与讨论
|
||||
- **领域专注**: 持续在 2-3 个话题下创作
|
||||
- **举报违规**: 适当举报,提升社区贡献分
|
||||
|
||||
## 适用场景
|
||||
|
||||
- 回答知乎热门问题获取曝光
|
||||
- 撰写知乎专栏原创文章
|
||||
- 优化已有回答提升排名
|
||||
- 建立知乎个人品牌
|
||||
- 通过知乎内容引流
|
||||
|
||||
## 不适用场景
|
||||
|
||||
- 写小红书风格的轻松内容(请用 xiaohongshu-writer)
|
||||
- 写微博短内容(请用 weibo-poster)
|
||||
- 知乎广告/软文(违反社区规范)
|
||||
- 需要实时查询知乎热榜数据
|
||||
|
||||
## 高赞回答的共同特征
|
||||
|
||||
1. **开头抓人**: 第一句话就让人想读下去
|
||||
2. **有独特视角**: 不要重复其他高赞答案的观点
|
||||
3. **有实质内容**: 数据、案例、个人经验缺一不可
|
||||
4. **排版舒适**: 移动端阅读友好
|
||||
5. **结尾有力**: 回扣问题,留下思考空间
|
||||
Reference in New Issue
Block a user