Compare commits
10 Commits
main
...
08f22edf44
| Author | SHA1 | Date | |
|---|---|---|---|
| 08f22edf44 | |||
| dd8e0f55c3 | |||
| 0be8c3e3fe | |||
| a273d80cf3 | |||
| 4a42071172 | |||
| d2cd9d4528 | |||
| 45026b9d35 | |||
| 3221856d50 | |||
| 46a1218fc8 | |||
| b076815171 |
35
.gitea/workflows/tests.yml
Normal file
35
.gitea/workflows/tests.yml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
name: Tests
|
||||||
|
|
||||||
|
# Gitea reads both .gitea/workflows/ and .github/workflows/, but which one a given
|
||||||
|
# instance scans depends on its config — so the workflow lives here, in Gitea's
|
||||||
|
# native location, and .github/workflows/tests.yml stays as a copy for if we ever
|
||||||
|
# move back to GitHub. tests/ci-parity.test.mjs keeps the two identical.
|
||||||
|
#
|
||||||
|
# Runs in a node container rather than using setup-node: on a self-hosted Gitea
|
||||||
|
# the action has to be fetched from GitHub, which is one more thing that can be
|
||||||
|
# missing on a fresh runner.
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ['**']
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
# Self-hosted Gitea runners are registered as internal_docker, not ubuntu-latest.
|
||||||
|
runs-on: internal_docker
|
||||||
|
container:
|
||||||
|
image: node:22.22.3
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Node version
|
||||||
|
run: node --version
|
||||||
|
|
||||||
|
- name: Generated files are up to date
|
||||||
|
run: node portable/lib/i18n/build-messages.mjs --check
|
||||||
|
|
||||||
|
- name: Tests
|
||||||
|
# Bare `node --test` discovers **/*.test.mjs. `node --test tests/` fails
|
||||||
|
# with "Cannot find module" — Node reads the path as a module, not a dir.
|
||||||
|
run: node --test
|
||||||
49
.githooks/pre-push
Executable file
49
.githooks/pre-push
Executable file
@@ -0,0 +1,49 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Runs the test suite before anything leaves this machine.
|
||||||
|
#
|
||||||
|
# This exists because CI cannot be relied on here. The suite sat unrun for the
|
||||||
|
# whole life of the upstream project — nothing invoked `node --test`, so every
|
||||||
|
# assertion in tests/ was decoration. We wired up a workflow, and on our own
|
||||||
|
# Gitea it produced zero runs, most likely because no runner is registered.
|
||||||
|
#
|
||||||
|
# A hook is not a substitute for CI: it only protects the machine it is installed
|
||||||
|
# on, and --no-verify skips it. But it is the one layer that works with no server
|
||||||
|
# support at all, and it turns "the tests exist" back into "the tests ran".
|
||||||
|
#
|
||||||
|
# Install: git config core.hooksPath .githooks
|
||||||
|
# Skip once (say the push is docs-only and node is unavailable):
|
||||||
|
# git push --no-verify
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if ! command -v node >/dev/null 2>&1; then
|
||||||
|
echo "pre-push: node not found, skipping tests." >&2
|
||||||
|
echo " Run them wherever you do have node before merging." >&2
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "pre-push: checking generated files are up to date…"
|
||||||
|
if ! node portable/lib/i18n/build-messages.mjs --check; then
|
||||||
|
echo "" >&2
|
||||||
|
echo "pre-push: generated files are stale." >&2
|
||||||
|
echo " Run: node portable/lib/i18n/build-messages.mjs" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "pre-push: running tests…"
|
||||||
|
# Bare `node --test` discovers **/*.test.mjs. `node --test tests/` does not work:
|
||||||
|
# Node treats the path as a module to load and dies with "Cannot find module".
|
||||||
|
# That form was in CLAUDE.md for the life of the project, which is a good part of
|
||||||
|
# why nobody ever got the suite to run.
|
||||||
|
if ! node --test > /tmp/uclaw-prepush.log 2>&1; then
|
||||||
|
echo "" >&2
|
||||||
|
tail -40 /tmp/uclaw-prepush.log >&2
|
||||||
|
echo "" >&2
|
||||||
|
echo "pre-push: tests failed, push aborted." >&2
|
||||||
|
echo " Full output: /tmp/uclaw-prepush.log" >&2
|
||||||
|
echo " To push anyway: git push --no-verify" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
passed=$(grep -oE '^# pass [0-9]+' /tmp/uclaw-prepush.log | grep -oE '[0-9]+' || echo '?')
|
||||||
|
echo "pre-push: $passed tests passed."
|
||||||
46
.github/ISSUE_TEMPLATE/bug_report.md
vendored
46
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,42 +1,42 @@
|
|||||||
---
|
---
|
||||||
name: 🐛 Bug / 安装问题
|
name: 🐛 Something is broken
|
||||||
about: 运行出错、安装失败、功能异常
|
about: It errors, will not install, or behaves wrongly
|
||||||
title: "[Bug] "
|
title: "[Bug] "
|
||||||
labels: bug
|
labels: bug
|
||||||
assignees: ''
|
assignees: ''
|
||||||
---
|
---
|
||||||
|
|
||||||
## 问题描述
|
## What happened
|
||||||
|
|
||||||
<!-- 简单描述遇到了什么问题 -->
|
<!-- One or two sentences. What did you expect, and what happened instead? -->
|
||||||
|
|
||||||
## 环境信息
|
## The error, as text
|
||||||
|
|
||||||
| 项目 | 填写 |
|
<!--
|
||||||
|------|------|
|
Paste the actual output. Not a screenshot of it — nobody can search or copy
|
||||||
| 操作系统 | macOS / Windows 10 / Windows 11 |
|
from a screenshot, and the useful part is usually the line that got cropped.
|
||||||
| Node.js 版本 | `node -v` 的输出 |
|
|
||||||
| 使用方式 | 便携 U 盘版 / 一键安装版 / Electron 桌面版 / Linux 可启动版 |
|
|
||||||
| 出错步骤 | setup.bat / setup.sh / Windows-Start.bat / Mac-Start.command / 其他 |
|
|
||||||
|
|
||||||
## 完整报错信息
|
If U-Claw failed to start, there is a report at:
|
||||||
|
data/.openclaw/diagnostics.txt
|
||||||
<!-- 请完整贴出终端/命令行的报错,不要只截图 -->
|
API keys are already stripped from it. Read it before attaching it.
|
||||||
|
-->
|
||||||
|
|
||||||
```
|
```
|
||||||
粘贴报错信息到这里
|
paste here
|
||||||
```
|
```
|
||||||
|
|
||||||
## 复现步骤
|
## Where
|
||||||
|
|
||||||
1.
|
- **OS and version**: <!-- macOS 14.5 / Windows 11 23H2 / Ubuntu 24.04 -->
|
||||||
2.
|
- **Which build**: <!-- portable USB / one-line install / bootable USB -->
|
||||||
3.
|
- **Version**: <!-- contents of OPENCLAW_VERSION, or the commit hash -->
|
||||||
|
|
||||||
## 已尝试的方法
|
## Steps to reproduce
|
||||||
|
|
||||||
<!-- 描述你试过哪些方法,避免重复建议 -->
|
1.
|
||||||
|
2.
|
||||||
|
3.
|
||||||
|
|
||||||
## 截图(可选)
|
## What you already tried
|
||||||
|
|
||||||
<!-- 如果有截图可以附上 -->
|
<!-- Saves everyone repeating it. -->
|
||||||
|
|||||||
18
.github/ISSUE_TEMPLATE/config.yml
vendored
18
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,11 +1,11 @@
|
|||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: 📖 完整教程
|
- name: 📖 Getting started
|
||||||
url: https://u-claw.org/tutorial.html
|
url: https://gitea.fanghe.it.com/zhenghy/u-claw#get-started
|
||||||
about: 从零开始的安装教程,含模型配置、平台接入
|
about: Three steps from plugging in the drive to talking to it.
|
||||||
- name: 🔍 查看 FAQ
|
- name: ❓ Common questions
|
||||||
url: https://github.com/dongsheng123132/u-claw#faq
|
url: https://gitea.fanghe.it.com/zhenghy/u-claw#faq
|
||||||
about: 常见问题解答,先看这里
|
about: Where settings live, offline use, which file system to use.
|
||||||
- name: 💬 联系作者(微信)
|
- name: 🔒 Security
|
||||||
url: https://u-claw.org/guide.html#remote-support
|
url: https://gitea.fanghe.it.com/zhenghy/u-claw/src/branch/main/SECURITY.md
|
||||||
about: 远程协助安装,微信 hecare888
|
about: Found a vulnerability? Read this before opening a public issue.
|
||||||
|
|||||||
26
.github/ISSUE_TEMPLATE/feature_request.md
vendored
26
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,23 +1,29 @@
|
|||||||
---
|
---
|
||||||
name: 💡 功能建议
|
name: 💡 Suggest something
|
||||||
about: 建议新功能或改进现有功能
|
about: A new capability, or a change to an existing one
|
||||||
title: "[Feature] "
|
title: "[Feature] "
|
||||||
labels: enhancement
|
labels: enhancement
|
||||||
assignees: ''
|
assignees: ''
|
||||||
---
|
---
|
||||||
|
|
||||||
## 建议描述
|
## What are you trying to do
|
||||||
|
|
||||||
<!-- 你希望增加什么功能?解决什么问题? -->
|
<!--
|
||||||
|
The situation first, the feature second.
|
||||||
|
|
||||||
## 使用场景
|
"I'd like X" is hard to act on. "I'm doing Y, I need X, because Z" can be
|
||||||
|
discussed — and quite often there is already a way to do Y that we can point
|
||||||
|
you at, or a better X than the one either of us thought of.
|
||||||
|
-->
|
||||||
|
|
||||||
<!-- 在什么情况下会用到这个功能? -->
|
## What is in the way now
|
||||||
|
|
||||||
## 期望的效果
|
<!-- How do you handle it today, and what does that cost you? -->
|
||||||
|
|
||||||
<!-- 描述理想的使用方式或界面 -->
|
## What you have in mind
|
||||||
|
|
||||||
## 补充信息
|
<!-- Optional. If you have a shape in mind, describe it. -->
|
||||||
|
|
||||||
<!-- 其他相关背景、截图、参考项目等 -->
|
## Anything else
|
||||||
|
|
||||||
|
<!-- Related tools that do this well, screenshots, links. -->
|
||||||
|
|||||||
72
.github/PULL_REQUEST_TEMPLATE.md
vendored
72
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,51 +1,53 @@
|
|||||||
<!--
|
<!--
|
||||||
感谢贡献 U-Claw!请按以下模板填写,方便维护者审阅。
|
Thanks for sending this.
|
||||||
空白或仅有标题的 PR 通常会被关闭,请勿提交无说明的改动。
|
|
||||||
|
A PR with a title and no description usually gets closed — not out of
|
||||||
|
strictness, but because nobody can tell whether it is safe to merge.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
## 改动类型 / Type
|
## What this changes
|
||||||
|
|
||||||
<!-- 勾选适用的([x]) -->
|
<!-- One or two sentences. What is different afterwards? -->
|
||||||
|
|
||||||
- [ ] 🐛 Bug 修复
|
## Why
|
||||||
- [ ] ✨ 新功能
|
|
||||||
- [ ] 📝 文档/README
|
|
||||||
- [ ] 🔧 脚本/构建(portable, install, bootable, u-claw-app)
|
|
||||||
- [ ] 🎨 技能 (skills-cn)
|
|
||||||
- [ ] ♻️ 重构(不改变行为)
|
|
||||||
- [ ] 🧪 测试
|
|
||||||
|
|
||||||
## 改动了什么 / What
|
<!-- The problem it solves. Link an issue if there is one. -->
|
||||||
|
|
||||||
<!-- 一两句话说清楚这个 PR 做了什么。不要复制 git diff。 -->
|
## Type
|
||||||
|
|
||||||
## 为什么 / Why
|
- [ ] 🐛 Fix
|
||||||
|
- [ ] ✨ New capability
|
||||||
|
- [ ] 📝 Docs
|
||||||
|
- [ ] 🎨 Skill content (`skills/`)
|
||||||
|
- [ ] ♻️ Refactor — no behaviour change
|
||||||
|
- [ ] 🔧 Build, CI or tooling
|
||||||
|
|
||||||
<!-- 哪个 issue / 哪个使用场景驱动了这次改动?关联 issue: #xxx -->
|
## Which builds this touches
|
||||||
|
|
||||||
## 测试方式 / How to verify
|
- [ ] `portable/` — the USB build
|
||||||
|
- [ ] `install/` — one-line install
|
||||||
|
- [ ] `bootable/` — Linux USB
|
||||||
|
- [ ] Shared (`lib/`, `skills/`, tests)
|
||||||
|
|
||||||
|
## How you tested it
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
告诉维护者怎么验证。比如:
|
Not decoration. Say what you actually ran.
|
||||||
- 在 macOS Apple Silicon 上跑 `bash install/install.sh`,安装成功
|
|
||||||
- 双击 `portable/Mac-Start.command`,能正常打开 dashboard
|
node --test # NOT `node --test tests/` — that runs nothing
|
||||||
- Windows 11 PowerShell 跑 `irm https://u-claw.org/install.ps1 | iex`,安装成功
|
|
||||||
|
And for anything touching a launcher or an installer, say which platform you
|
||||||
|
ran it on. A change to a .bat verified only on a Mac is worth saying so about —
|
||||||
|
that has been the main source of breakage here.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
## 影响范围 / Scope
|
- [ ] `node --test` passes
|
||||||
|
- [ ] Ran it on: <!-- macOS / Windows / Linux -->
|
||||||
|
|
||||||
<!-- 勾选受影响的模块 -->
|
## Checklist
|
||||||
|
|
||||||
- [ ] portable/ (便携 USB)
|
- [ ] No `node_modules/`, `app/`, `data/`, `*.dmg`, `*.exe` in the diff
|
||||||
- [ ] u-claw-app/ (Electron 桌面)
|
- [ ] No API keys, tokens or passwords in any file
|
||||||
- [ ] bootable/ (Linux 可启动 U 盘)
|
- [ ] User-facing text is in `lib/messages/*.json`, not hardcoded in a script
|
||||||
- [ ] install/ (一键安装脚本)
|
- [ ] `.bat` files stay pure ASCII with CRLF; `.command` files stay LF-only
|
||||||
- [ ] skills-cn/ (中国本地化技能)
|
- [ ] Unrelated files are not reformatted
|
||||||
- [ ] 仅文档
|
|
||||||
|
|
||||||
## 自检 / Checklist
|
|
||||||
|
|
||||||
- [ ] 我已经在本地实际跑过相关脚本/功能
|
|
||||||
- [ ] 没有提交 `node_modules/`、`app/`、`data/` 等运行时产物
|
|
||||||
- [ ] 没有把任何 API Key、Token 写进文件
|
|
||||||
- [ ] commit message 能说清楚改了什么
|
|
||||||
|
|||||||
76
.github/workflows/release.yml
vendored
76
.github/workflows/release.yml
vendored
@@ -19,7 +19,7 @@ permissions:
|
|||||||
jobs:
|
jobs:
|
||||||
portable-windows-full:
|
portable-windows-full:
|
||||||
name: Portable Windows full bundle (Node + OpenClaw pre-installed)
|
name: Portable Windows full bundle (Node + OpenClaw pre-installed)
|
||||||
runs-on: ubuntu-latest
|
runs-on: internal_docker
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
@@ -31,6 +31,7 @@ jobs:
|
|||||||
mkdir -p "$stage_dir"
|
mkdir -p "$stage_dir"
|
||||||
cp -R portable/. "$stage_dir/"
|
cp -R portable/. "$stage_dir/"
|
||||||
cp OPENCLAW_VERSION "$stage_dir/OPENCLAW_VERSION"
|
cp OPENCLAW_VERSION "$stage_dir/OPENCLAW_VERSION"
|
||||||
|
cp NODE_VERSION "$stage_dir/NODE_VERSION"
|
||||||
rm -rf "$stage_dir/data/logs" 2>/dev/null || true
|
rm -rf "$stage_dir/data/logs" 2>/dev/null || true
|
||||||
|
|
||||||
- name: Pre-install Node.js (Windows) and OpenClaw deps
|
- name: Pre-install Node.js (Windows) and OpenClaw deps
|
||||||
@@ -39,9 +40,9 @@ jobs:
|
|||||||
# Absolute paths so `cd "$core_dir"` later doesn't break `du`/`find`
|
# Absolute paths so `cd "$core_dir"` later doesn't break `du`/`find`
|
||||||
stage_dir="$(pwd)/dist/u-claw-portable-windows-${tag_name}"
|
stage_dir="$(pwd)/dist/u-claw-portable-windows-${tag_name}"
|
||||||
openclaw_version=$(tr -d '[:space:]' < OPENCLAW_VERSION)
|
openclaw_version=$(tr -d '[:space:]' < OPENCLAW_VERSION)
|
||||||
node_version="v22.22.1"
|
node_version=$(tr -d "[:space:]" < NODE_VERSION)
|
||||||
mirror_node="https://npmmirror.com/mirrors/node"
|
mirror_node="https://nodejs.org/dist"
|
||||||
mirror_npm="https://registry.npmmirror.com"
|
mirror_npm="https://registry.npmjs.org"
|
||||||
|
|
||||||
app_dir="$stage_dir/app"
|
app_dir="$stage_dir/app"
|
||||||
runtime_dir="$app_dir/runtime"
|
runtime_dir="$app_dir/runtime"
|
||||||
@@ -49,8 +50,8 @@ jobs:
|
|||||||
mkdir -p "$runtime_dir" "$core_dir"
|
mkdir -p "$runtime_dir" "$core_dir"
|
||||||
|
|
||||||
# 1) Download Node for Windows x64 only.
|
# 1) Download Node for Windows x64 only.
|
||||||
# Mac users: Mac-Start.command runs setup.sh on first launch which
|
# Mac users: START HERE - Mac.command runs setup.sh on first launch which
|
||||||
# downloads node-mac-arm64 from npmmirror.com (~30MB, ~10s in China).
|
# downloads node-mac-arm64 from nodejs.org (~30MB).
|
||||||
# Stanley pattern: ship one platform per zip, keep size reasonable.
|
# Stanley pattern: ship one platform per zip, keep size reasonable.
|
||||||
win_node="$runtime_dir/node-win-x64"
|
win_node="$runtime_dir/node-win-x64"
|
||||||
mkdir -p "$win_node"
|
mkdir -p "$win_node"
|
||||||
@@ -118,18 +119,19 @@ jobs:
|
|||||||
--registry="$mirror_npm" \
|
--registry="$mirror_npm" \
|
||||||
--ignore-scripts \
|
--ignore-scripts \
|
||||||
--no-audit \
|
--no-audit \
|
||||||
--no-fund || echo "WARN: $cn_pkg install failed, skipping (便携版仍可用其它渠道)"
|
--no-fund || echo "WARN: $cn_pkg install failed, skipping — the drive still works with the other channels"
|
||||||
done
|
done
|
||||||
|
|
||||||
# 6) Copy China-optimized skills if present
|
# 6) Install skills from the manifest.
|
||||||
skills_cn="$stage_dir/skills-cn"
|
# skills/ is the single source of truth (see skills/README.md). Ship it
|
||||||
|
# plus the installer onto the USB so the drive can re-install or switch
|
||||||
|
# locale offline, then populate OpenClaw's skills dir from it.
|
||||||
skills_target="$core_dir/node_modules/openclaw/skills"
|
skills_target="$core_dir/node_modules/openclaw/skills"
|
||||||
if [ -d "$skills_cn" ] && [ -d "$skills_target" ]; then
|
mkdir -p "$stage_dir/lib" "$skills_target"
|
||||||
for skill_dir in "$skills_cn"/*/; do
|
cp -R skills "$stage_dir/skills"
|
||||||
skill_name=$(basename "$skill_dir")
|
cp lib/install-skills.mjs "$stage_dir/lib/install-skills.mjs"
|
||||||
[ ! -d "$skills_target/$skill_name" ] && cp -R "$skill_dir" "$skills_target/$skill_name"
|
node "$stage_dir/lib/install-skills.mjs" --target "$skills_target" --locale en
|
||||||
done
|
echo "Skills on the drive: $(ls -1 "$skills_target" | wc -l)"
|
||||||
fi
|
|
||||||
|
|
||||||
# 6b) Install WeChat (Weixin) plugin into app/extensions/openclaw-weixin
|
# 6b) Install WeChat (Weixin) plugin into app/extensions/openclaw-weixin
|
||||||
# 官方 npm 包 @tencent-weixin/openclaw-weixin 自带 dist/index.js(发布时已编译,无需 build)。
|
# 官方 npm 包 @tencent-weixin/openclaw-weixin 自带 dist/index.js(发布时已编译,无需 build)。
|
||||||
@@ -191,7 +193,11 @@ jobs:
|
|||||||
tag_name="${GITHUB_REF_NAME:-${{ github.event.inputs.tag }}}"
|
tag_name="${GITHUB_REF_NAME:-${{ github.event.inputs.tag }}}"
|
||||||
cd dist
|
cd dist
|
||||||
zip -qry "u-claw-portable-windows-${tag_name}.zip" "u-claw-portable-windows-${tag_name}"
|
zip -qry "u-claw-portable-windows-${tag_name}.zip" "u-claw-portable-windows-${tag_name}"
|
||||||
|
# Publish a checksum next to every asset so installers can verify what
|
||||||
|
# they downloaded instead of trusting "the file is bigger than 1MB".
|
||||||
|
sha256sum "u-claw-portable-windows-${tag_name}.zip" > "u-claw-portable-windows-${tag_name}.zip.sha256"
|
||||||
ls -lh "u-claw-portable-windows-${tag_name}.zip"
|
ls -lh "u-claw-portable-windows-${tag_name}.zip"
|
||||||
|
cat "u-claw-portable-windows-${tag_name}.zip.sha256"
|
||||||
|
|
||||||
- name: Size guard (lesson from 5916ff5 — GitHub 2GB asset cap)
|
- name: Size guard (lesson from 5916ff5 — GitHub 2GB asset cap)
|
||||||
run: |
|
run: |
|
||||||
@@ -211,12 +217,14 @@ jobs:
|
|||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: portable-windows-full
|
name: portable-windows-full
|
||||||
path: dist/u-claw-portable-windows-*.zip
|
path: |
|
||||||
|
dist/u-claw-portable-windows-*.zip
|
||||||
|
dist/u-claw-portable-windows-*.zip.sha256
|
||||||
|
|
||||||
publish:
|
publish:
|
||||||
name: Publish GitHub Release
|
name: Publish GitHub Release
|
||||||
needs: [portable-windows-full]
|
needs: [portable-windows-full]
|
||||||
runs-on: ubuntu-latest
|
runs-on: internal_docker
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
@@ -248,18 +256,36 @@ jobs:
|
|||||||
body: |
|
body: |
|
||||||
## U-Claw ${{ steps.tag.outputs.tag }}
|
## U-Claw ${{ steps.tag.outputs.tag }}
|
||||||
|
|
||||||
- 打开 Config 选模型、填入自己的 API Key 即可使用,支持 DeepSeek / 通义 / Claude / GPT 等国内外大模型
|
An AI assistant on a USB drive. Plug it into any Mac or Windows
|
||||||
- 想用「虾盘云」中转站(一个 Key 调用国内外全部模型、免翻墙):先到 https://cloud.u-claw.org 注册并创建 Key
|
machine — no installer, no admin rights, nothing left behind.
|
||||||
- **聊天渠道开箱即用**:便携版已离线预装 微信 / QQ / 钉钉 / 飞书 / 企业微信 插件,无需自己 npm 安装
|
|
||||||
|
- Paste one API key and you are running. U-Claw works out the
|
||||||
|
provider from the key itself and checks it before accepting it.
|
||||||
|
- Your settings, keys and memory live on the drive, so they travel
|
||||||
|
with you between machines.
|
||||||
- OpenClaw runtime: `${{ steps.openclaw_version.outputs.version }}`
|
- OpenClaw runtime: `${{ steps.openclaw_version.outputs.version }}`
|
||||||
|
|
||||||
### 下载
|
### Downloads
|
||||||
|
|
||||||
- **Windows 便携完整版(U 盘版)**:`u-claw-portable-windows-*.zip`(约 150-200 MB,已预装 Node.js + OpenClaw,**解压即用,零网络依赖**)。解压到 U 盘后双击 `Windows-Start.bat` 启动。
|
- **Windows** — `u-claw-portable-windows-*.zip`, roughly 150–200 MB
|
||||||
- **macOS 便携版**:源码在 `portable/` 目录,`bash setup.sh` 自动下载 Node + OpenClaw(国内镜像约 1 分钟),双击 `Mac-Start.command` 启动。
|
with Node.js and OpenClaw already inside. Unzip onto the drive and
|
||||||
|
double-click `START HERE - Windows.bat`. No network needed to start.
|
||||||
|
- **macOS** — clone the repo, run `bash portable/advanced/setup.sh`
|
||||||
|
(about a minute to fetch Node and OpenClaw), then double-click
|
||||||
|
`START HERE - Mac.command`.
|
||||||
|
|
||||||
|
Each asset has a `.sha256` next to it. Check it before you run it.
|
||||||
|
|
||||||
|
### Format the drive as exFAT
|
||||||
|
|
||||||
|
Not NTFS. **macOS can only read NTFS, not write to it**, so an
|
||||||
|
NTFS drive silently breaks the one thing this product is for —
|
||||||
|
your settings will not follow you to a Mac.
|
||||||
|
|
||||||
|
exFAT is slower with many small files, which is why U-Claw moves its
|
||||||
|
rebuildable caches onto the host's local disk at startup. Your actual
|
||||||
|
data stays on the drive.
|
||||||
|
|
||||||
> **U 盘格式建议**:请用 **NTFS** 格式化 U 盘(NOT exFAT/FAT32)。Node.js 在 exFAT 上 IO 极慢且不支持符号链接,可能导致启动失败。
|
|
||||||
> Windows 下右键 U 盘 → 格式化 → 文件系统选择 NTFS。
|
|
||||||
files: |
|
files: |
|
||||||
artifacts/portable-windows-full/*
|
artifacts/portable-windows-full/*
|
||||||
fail_on_unmatched_files: false
|
fail_on_unmatched_files: false
|
||||||
|
|||||||
31
.github/workflows/tests.yml
vendored
Normal file
31
.github/workflows/tests.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
name: Tests
|
||||||
|
|
||||||
|
# Copy of .gitea/workflows/tests.yml, kept for if we ever move back to GitHub.
|
||||||
|
# Gitea reads both locations but which one a given instance scans depends on its
|
||||||
|
# config, so the workflow lives in .gitea/ and this is the mirror.
|
||||||
|
# tests/ci-parity.test.mjs keeps the two running the same thing.
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ['**']
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
# Self-hosted Gitea runners are registered as internal_docker, not ubuntu-latest.
|
||||||
|
runs-on: internal_docker
|
||||||
|
container:
|
||||||
|
image: node:22.22.3
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Node version
|
||||||
|
run: node --version
|
||||||
|
|
||||||
|
- name: Generated files are up to date
|
||||||
|
run: node portable/lib/i18n/build-messages.mjs --check
|
||||||
|
|
||||||
|
- name: Tests
|
||||||
|
# Bare `node --test` discovers **/*.test.mjs. `node --test tests/` fails
|
||||||
|
# with "Cannot find module" — Node reads the path as a module, not a dir.
|
||||||
|
run: node --test
|
||||||
42
.github/workflows/track-upstream.yml
vendored
42
.github/workflows/track-upstream.yml
vendored
@@ -22,11 +22,11 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
force_version:
|
force_version:
|
||||||
description: '强制使用的上游 OpenClaw 版本(留空=用 npm latest)'
|
description: 'Pin a specific upstream OpenClaw version (blank = whatever npm says is latest)'
|
||||||
required: false
|
required: false
|
||||||
default: ''
|
default: ''
|
||||||
dry_run:
|
dry_run:
|
||||||
description: '只检测不改动(true/false)'
|
description: 'Check only, change nothing (true/false)'
|
||||||
required: false
|
required: false
|
||||||
default: 'false'
|
default: 'false'
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ permissions:
|
|||||||
jobs:
|
jobs:
|
||||||
track:
|
track:
|
||||||
name: Check upstream & auto-bump
|
name: Check upstream & auto-bump
|
||||||
runs-on: ubuntu-latest
|
runs-on: internal_docker
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
@@ -58,19 +58,13 @@ jobs:
|
|||||||
force="${{ github.event.inputs.force_version }}"
|
force="${{ github.event.inputs.force_version }}"
|
||||||
if [ -n "$force" ]; then
|
if [ -n "$force" ]; then
|
||||||
latest="${force#v}"
|
latest="${force#v}"
|
||||||
echo "强制指定上游版本: $latest"
|
echo "Forced upstream version: $latest"
|
||||||
else
|
else
|
||||||
latest="$(npm view openclaw version --registry=https://registry.npmmirror.com 2>/dev/null | tr -d '[:space:]')"
|
latest="$(npm view openclaw version --registry=https://registry.npmjs.org 2>/dev/null | tr -d '[:space:]')"
|
||||||
# npmmirror 偶尔缓存陈旧,官方源兜底取较大值
|
|
||||||
official="$(npm view openclaw version 2>/dev/null | tr -d '[:space:]')"
|
|
||||||
if [ -n "$official" ] && [ "$official" != "$latest" ]; then
|
|
||||||
# 取版本号较大的那个(npmmirror 缓存滞后时用官方源)
|
|
||||||
latest="$(printf '%s\n%s\n' "$latest" "$official" | sort -V | tail -1)"
|
|
||||||
fi
|
|
||||||
echo "npm latest openclaw: $latest"
|
echo "npm latest openclaw: $latest"
|
||||||
fi
|
fi
|
||||||
if [ -z "$latest" ]; then
|
if [ -z "$latest" ]; then
|
||||||
echo "::error::无法获取上游 openclaw 版本(npm view 失败)"
|
echo "::error::Could not read the upstream openclaw version — npm view failed"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "latest=$latest" >> "$GITHUB_OUTPUT"
|
echo "latest=$latest" >> "$GITHUB_OUTPUT"
|
||||||
@@ -82,7 +76,7 @@ jobs:
|
|||||||
shell_ver="$(node -p "require('./u-claw-app/package.json').version")"
|
shell_ver="$(node -p "require('./u-claw-app/package.json').version")"
|
||||||
echo "pinned=$pinned" >> "$GITHUB_OUTPUT"
|
echo "pinned=$pinned" >> "$GITHUB_OUTPUT"
|
||||||
echo "shell_ver=$shell_ver" >> "$GITHUB_OUTPUT"
|
echo "shell_ver=$shell_ver" >> "$GITHUB_OUTPUT"
|
||||||
echo "当前 pin: $pinned | 壳版本: $shell_ver"
|
echo "Currently pinned: $pinned | shell version: $shell_ver"
|
||||||
|
|
||||||
- name: Decide whether to bump
|
- name: Decide whether to bump
|
||||||
id: decide
|
id: decide
|
||||||
@@ -90,18 +84,18 @@ jobs:
|
|||||||
latest="${{ steps.upstream.outputs.latest }}"
|
latest="${{ steps.upstream.outputs.latest }}"
|
||||||
pinned="${{ steps.current.outputs.pinned }}"
|
pinned="${{ steps.current.outputs.pinned }}"
|
||||||
if [ "$latest" = "$pinned" ]; then
|
if [ "$latest" = "$pinned" ]; then
|
||||||
echo "✅ 已是最新(pin=$pinned),无需发版。"
|
echo "Already up to date (pinned=$pinned) — nothing to release."
|
||||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
# 只在 latest 严格 > pinned 时发版(避免上游撤版导致回退)
|
# 只在 latest 严格 > pinned 时发版(避免上游撤版导致回退)
|
||||||
newer="$(printf '%s\n%s\n' "$pinned" "$latest" | sort -V | tail -1)"
|
newer="$(printf '%s\n%s\n' "$pinned" "$latest" | sort -V | tail -1)"
|
||||||
if [ "$newer" != "$latest" ]; then
|
if [ "$newer" != "$latest" ]; then
|
||||||
echo "⚠️ npm latest ($latest) 不比当前 pin ($pinned) 新,跳过。"
|
echo "npm latest ($latest) is not newer than the pin ($pinned) — skipping."
|
||||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
echo "🆕 发现上游新版: $pinned → $latest"
|
echo "Upstream has moved: $pinned -> $latest"
|
||||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Compute next shell version (patch bump)
|
- name: Compute next shell version (patch bump)
|
||||||
@@ -112,7 +106,7 @@ jobs:
|
|||||||
IFS='.' read -r MA MI PA <<< "$cur"
|
IFS='.' read -r MA MI PA <<< "$cur"
|
||||||
PA=$((PA + 1))
|
PA=$((PA + 1))
|
||||||
next="${MA}.${MI}.${PA}"
|
next="${MA}.${MI}.${PA}"
|
||||||
echo "壳版本 bump: $cur → $next"
|
echo "Bumping shell version: $cur -> $next"
|
||||||
echo "next=$next" >> "$GITHUB_OUTPUT"
|
echo "next=$next" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Apply changes (pin + version files)
|
- name: Apply changes (pin + version files)
|
||||||
@@ -129,7 +123,7 @@ jobs:
|
|||||||
# 这里写一份让 main 分支也保持一致。
|
# 这里写一份让 main 分支也保持一致。
|
||||||
node -e "const f='u-claw-app/package.json';const p=require('./'+f);p.version='$next';require('fs').writeFileSync(f, JSON.stringify(p,null,2)+'\n')"
|
node -e "const f='u-claw-app/package.json';const p=require('./'+f);p.version='$next';require('fs').writeFileSync(f, JSON.stringify(p,null,2)+'\n')"
|
||||||
|
|
||||||
echo "已写入: OPENCLAW_VERSION=$latest, 壳版本=$next"
|
echo "Wrote OPENCLAW_VERSION=$latest, shell version=$next"
|
||||||
|
|
||||||
- name: Commit, tag & push
|
- name: Commit, tag & push
|
||||||
if: steps.decide.outputs.changed == 'true' && github.event.inputs.dry_run != 'true'
|
if: steps.decide.outputs.changed == 'true' && github.event.inputs.dry_run != 'true'
|
||||||
@@ -141,10 +135,10 @@ jobs:
|
|||||||
git config user.email "bot@u-claw.org"
|
git config user.email "bot@u-claw.org"
|
||||||
|
|
||||||
git add OPENCLAW_VERSION portable/OPENCLAW_VERSION u-claw-app/package.json
|
git add OPENCLAW_VERSION portable/OPENCLAW_VERSION u-claw-app/package.json
|
||||||
git commit -m "chore(upstream): 跟随 OpenClaw $latest,发布 v$next
|
git commit -m "chore(upstream): follow OpenClaw $latest, release v$next
|
||||||
|
|
||||||
自动跟踪上游:OpenClaw npm latest 升到 $latest,
|
Automated: upstream OpenClaw moved to $latest, so the shell version
|
||||||
自动 bump 壳版本到 $next 并打 tag 触发发版。"
|
is bumped to $next and tagged, which triggers a release."
|
||||||
|
|
||||||
git push origin HEAD:main
|
git push origin HEAD:main
|
||||||
|
|
||||||
@@ -152,10 +146,10 @@ jobs:
|
|||||||
git tag "v$next"
|
git tag "v$next"
|
||||||
git push origin "v$next"
|
git push origin "v$next"
|
||||||
|
|
||||||
echo "✅ 已推送 v$next,release.yml 将自动构建并发布。"
|
echo "Pushed v$next — release.yml will build and publish it."
|
||||||
|
|
||||||
- name: Dry-run summary
|
- name: Dry-run summary
|
||||||
if: steps.decide.outputs.changed == 'true' && github.event.inputs.dry_run == 'true'
|
if: steps.decide.outputs.changed == 'true' && github.event.inputs.dry_run == 'true'
|
||||||
run: |
|
run: |
|
||||||
echo "🔍 DRY RUN — 不改动。"
|
echo "DRY RUN — nothing changed."
|
||||||
echo "若执行将:OPENCLAW_VERSION ${{ steps.current.outputs.pinned }} → ${{ steps.upstream.outputs.latest }},发布 v${{ steps.nextver.outputs.next }}"
|
echo "Would set OPENCLAW_VERSION ${{ steps.current.outputs.pinned }} -> ${{ steps.upstream.outputs.latest }} and release v${{ steps.nextver.outputs.next }}"
|
||||||
|
|||||||
212
CLAUDE.md
212
CLAUDE.md
@@ -7,11 +7,11 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
**This repo IS the USB drive content**, minus large dependencies. The relationship:
|
**This repo IS the USB drive content**, minus large dependencies. The relationship:
|
||||||
|
|
||||||
```
|
```
|
||||||
代码库(git)= U 盘骨架(脚本 + HTML + 小文件)
|
repository (git) = the drive's skeleton — scripts, HTML, small files
|
||||||
↓ bash setup.sh
|
↓ bash setup.sh
|
||||||
完整文件夹 = U 盘内容(骨架 + Node.js + OpenClaw)
|
working folder = the drive's contents — skeleton + Node.js + OpenClaw
|
||||||
↓ 拷贝到 U 盘
|
↓ copy onto a USB drive
|
||||||
U 盘 = 插上就能用
|
USB drive = plug in and go
|
||||||
```
|
```
|
||||||
|
|
||||||
The repo is NOT a "build tool" or "generator" — it IS the USB structure. `setup.sh` only fills in large deps that can't go in git. After `setup.sh`, the `portable/` folder is directly copyable to a USB drive.
|
The repo is NOT a "build tool" or "generator" — it IS the USB structure. `setup.sh` only fills in large deps that can't go in git. After `setup.sh`, the `portable/` folder is directly copyable to a USB drive.
|
||||||
@@ -29,14 +29,14 @@ Distribution forms:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Portable version — build dev copy
|
# Portable version — build dev copy
|
||||||
cd portable && bash setup.sh # Downloads Node.js v22 + OpenClaw + WeChat/QQ plugin to app/
|
cd portable/advanced && bash setup.sh # Downloads Node.js v22 + OpenClaw + plugins into ../app/
|
||||||
bash Mac-Start.command # Launch (Mac ARM64). Windows: Windows-Start.bat
|
cd .. && bash "START HERE - Mac.command" # Launch (Mac). Windows: double-click "START HERE - Windows.bat"
|
||||||
|
|
||||||
# Copy to USB drive
|
# Copy to USB drive
|
||||||
cp -R portable/ /Volumes/YOUR_USB/U-Claw/
|
cp -R portable/ /Volumes/YOUR_USB/U-Claw/
|
||||||
|
|
||||||
# Electron desktop app
|
# Electron desktop app
|
||||||
cd u-claw-app && bash setup.sh # One-click: Node.js + Electron + deps (China mirrors)
|
cd u-claw-app && bash setup.sh # One-click: Node.js + Electron + deps
|
||||||
npm run dev # Dev mode
|
npm run dev # Dev mode
|
||||||
npm run build:mac-arm64 # Build Mac ARM64 DMG
|
npm run build:mac-arm64 # Build Mac ARM64 DMG
|
||||||
npm run build:win # Build Windows NSIS + portable
|
npm run build:win # Build Windows NSIS + portable
|
||||||
@@ -44,7 +44,7 @@ npm run build:win # Build Windows NSIS + portable
|
|||||||
# Bootable Linux USB (run on Windows PowerShell as Admin)
|
# Bootable Linux USB (run on Windows PowerShell as Admin)
|
||||||
cd bootable
|
cd bootable
|
||||||
.\1-prepare-usb.ps1 # Write Ventoy to USB (formats drive!)
|
.\1-prepare-usb.ps1 # Write Ventoy to USB (formats drive!)
|
||||||
.\2-download-iso.ps1 # Download Ubuntu ISO (~5.8GB, China mirrors)
|
.\2-download-iso.ps1 # Download Ubuntu ISO (~5.8GB)
|
||||||
.\3-create-persistence.ps1 # Create 20GB ext4 persistence image
|
.\3-create-persistence.ps1 # Create 20GB ext4 persistence image
|
||||||
.\4-copy-to-usb.ps1 # Copy ISO + persistence + scripts to USB
|
.\4-copy-to-usb.ps1 # Copy ISO + persistence + scripts to USB
|
||||||
```
|
```
|
||||||
@@ -52,15 +52,36 @@ cd bootable
|
|||||||
### Tests
|
### Tests
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
node --test tests/ # Run all tests (node:test, no test framework dep)
|
node --test # Run every test (node:test, no framework dependency)
|
||||||
node --test tests/windows-launchers.test.mjs # Run one test file
|
node --test tests/windows-launchers.test.mjs # Run one file
|
||||||
|
|
||||||
|
# NOT `node --test tests/` — Node reads that path as a module to load and dies
|
||||||
|
# with "Cannot find module". That form was documented here for years, which is
|
||||||
|
# a good part of why the suite went unrun.
|
||||||
```
|
```
|
||||||
|
|
||||||
Tests assert on the **text/behavior of the launcher scripts** (`.bat`/`.command`) — e.g. that
|
Tests assert on the **text/behavior of the launchers and shared `.mjs` helpers** — e.g. that the
|
||||||
`Windows-Start.bat` escapes `^(...^)` parens inside IF blocks and disables OpenClaw bonjour
|
Start launchers stay thin shells over `lib/start.mjs`, that `.bat` files are pure ASCII with CRLF,
|
||||||
discovery. They read repo files as strings; they do **not** spawn OpenClaw. There is no root
|
that no China-routed download source comes back, that the two installers resolve skills the same
|
||||||
`package.json` — tests are not run by the release CI (`.github/workflows/release.yml` only
|
way, and that every message key exists in every catalogue. They read repo files as strings; they
|
||||||
builds and publishes). Run them locally before pushing launcher changes.
|
do **not** spawn OpenClaw. There is no root
|
||||||
|
`package.json`.
|
||||||
|
|
||||||
|
**Enable the pre-push hook after cloning** — it is per-clone and nothing installs it for you:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git config core.hooksPath .githooks
|
||||||
|
```
|
||||||
|
|
||||||
|
It runs the suite and the generated-file check before anything leaves your machine.
|
||||||
|
`git push --no-verify` skips it. This matters because CI cannot be relied on here:
|
||||||
|
the suite went unrun for the whole life of the upstream project, and on our own
|
||||||
|
Gitea the workflow produced zero runs — most likely no runner is registered. The
|
||||||
|
hook is the one layer that works with no server support at all.
|
||||||
|
|
||||||
|
**As of 2026-08-17 tests also run in CI where a runner exists** — `.github/workflows/tests.yml` runs
|
||||||
|
`node --test` on every push and PR. Before that they were local-only, so the guards they
|
||||||
|
encode could regress unnoticed.
|
||||||
|
|
||||||
**CI workflows** (`.github/workflows/`): `release.yml` builds Win/Mac portable + desktop and
|
**CI workflows** (`.github/workflows/`): `release.yml` builds Win/Mac portable + desktop and
|
||||||
publishes a GitHub Release on tag push. `track-upstream.yml` runs daily (cron) — checks
|
publishes a GitHub Release on tag push. `track-upstream.yml` runs daily (cron) — checks
|
||||||
@@ -83,43 +104,63 @@ portable/ THE USB content (= repo + setup.sh downloads)
|
|||||||
{Mac,Windows}-Install — copy USB → computer (~/.uclaw/ or %USERPROFILE%)
|
{Mac,Windows}-Install — copy USB → computer (~/.uclaw/ or %USERPROFILE%)
|
||||||
{Mac,Windows}-Diagnose — health check / collect logs for bug reports
|
{Mac,Windows}-Diagnose — health check / collect logs for bug reports
|
||||||
{Mac-OpenClaw-CLI,OpenClaw-CLI.bat} — drop into raw `openclaw` CLI
|
{Mac-OpenClaw-CLI,OpenClaw-CLI.bat} — drop into raw `openclaw` CLI
|
||||||
*.html (Welcome, Config, U-Claw, SkillHub) — local UI pages
|
*.html (Welcome, U-Claw, SkillHub) — local UI pages
|
||||||
|
Config.html — redirect shim only; the settings UI itself is
|
||||||
|
config-server/public/index.html (single implementation since 2026-08-17).
|
||||||
|
The old standalone copy called the API on a root-relative path, which
|
||||||
|
cannot work from file:// — Save failed silently for anyone who opened it.
|
||||||
config-server/server.js — local HTTP server (port 18788-18798) backing
|
config-server/server.js — local HTTP server (port 18788-18798) backing
|
||||||
Config.html: writes openclaw.json, WeChat QR login, update-status API
|
Config.html: writes openclaw.json, WeChat QR login, update-status API
|
||||||
lib/ — Node helpers (see "lib/ helpers" below)
|
lib/ — Node helpers (see "lib/ helpers" below)
|
||||||
default-config.json — seed config copied to data/.openclaw/ on first run
|
default-config.json — seed config copied to data/.openclaw/ on first run
|
||||||
app/core/ (OpenClaw) + app/runtime/ (Node.js) — downloaded by setup.sh
|
app/core/ (OpenClaw) + app/runtime/ (Node.js) — downloaded by setup.sh
|
||||||
data/.openclaw/openclaw.json — user config (on USB, portable)
|
data/.openclaw/openclaw.json — user config (on USB, portable)
|
||||||
skills-cn/ — 17 个中国本地化技能(小红书/微博/B站/抖音/知乎/
|
skills/ Single source of truth for skill content + distribution (2026-08-17).
|
||||||
微信公众号/Word/Excel/PPT/天气/搜索/翻译/DeepSeek/
|
manifest.json — the only thing installers read
|
||||||
图片压缩/PDF工具/二维码/网页转Markdown)
|
en/<id>/SKILL.md — content
|
||||||
|
Replaces the old three-way split (portable/skills-cn/ on disk,
|
||||||
|
heredocs in install.sh, here-strings in install.ps1). See skills/README.md.
|
||||||
|
Installed by lib/install-skills.mjs; release.yml copies skills/ + the
|
||||||
|
installer onto the drive so it can re-install offline.
|
||||||
|
|
||||||
u-claw-app/ [DEPRECATED 2026-06-19, archived — not built/published] Electron desktop app
|
u-claw-app/ [DEPRECATED 2026-06-19, archived — not built/published] Electron desktop app
|
||||||
(main.js ~400 lines). Kept for archive only; see u-claw-app/DEPRECATED.md.
|
(main.js ~400 lines). Kept for archive only; see u-claw-app/DEPRECATED.md.
|
||||||
Bundles Node.js in resources/runtime/node-{platform}-{arch}
|
Bundles Node.js in resources/runtime/node-{platform}-{arch}
|
||||||
|
|
||||||
bootable/ Linux 可启动 U 盘模块(完全独立,不依赖其他模块)
|
bootable/ Linux bootable USB. Fully self-contained — references nothing
|
||||||
4 步 PowerShell 脚本 (Windows 上制作)
|
from the other modules, on purpose.
|
||||||
Ventoy 1.0.99 + Ubuntu 24.04 LTS + casper-rw 持久化
|
Four PowerShell scripts, run on Windows, in order.
|
||||||
linux-setup/ — setup-openclaw.sh 安装到 /opt/u-claw/
|
Ventoy 1.0.99 + Ubuntu 24.04 LTS + casper-rw persistence
|
||||||
独立仓库镜像: github.com/dongsheng123132/u-claw-linux
|
linux-setup/setup-openclaw.sh installs into /opt/u-claw/
|
||||||
|
Mirror: gitea.fanghe.it.com/zhenghy/u-claw-linux
|
||||||
|
|
||||||
install/ 一键在线安装模块(curl | bash / irm | iex)
|
install/ One-line network install (curl | bash / irm | iex).
|
||||||
install.sh (Mac/Linux) + install.ps1 (Windows)
|
install.sh (Mac/Linux) + install.ps1 (Windows)
|
||||||
7 步流程: 系统检测 → Node.js → OpenClaw → QQ插件 → 技能 → 模型配置 → 启动脚本
|
Detect → Node.js → OpenClaw → QQ plugin → skills (from
|
||||||
安装到 ~/.uclaw/,与 Mac-Install.command 结果相同
|
skills/manifest.json) → model → start script → verify.
|
||||||
|
Installs to ~/.uclaw/; same result as advanced/Mac-Install.command.
|
||||||
|
These run with no checkout, so they cannot read origin.json
|
||||||
|
and carry URLs as literals — tests/origin.test.mjs keeps the
|
||||||
|
two in step.
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
> **Note**: 官网 (u-claw.org) 已拆分到独立私有仓库 [u-claw.org](https://github.com/dongsheng123132/u-claw.org),本仓库不再包含 website/ 和 vercel.json。
|
> **This is a fork.** Upstream is `github.com/dongsheng123132/u-claw`; we host at
|
||||||
> **虾航**: AI人导航站 (nav.u-claw.org) 在独立私有仓库 [xiahang](https://github.com/dongsheng123132/xiahang)。
|
> `gitea.fanghe.it.com/zhenghy/u-claw` and do **not** send changes back — the overseas
|
||||||
|
> build removes the China-market defaults that are the point of the upstream project.
|
||||||
|
> See `origin.json` for every URL this build fetches at runtime, and the migration
|
||||||
|
> notes in the workspace root.
|
||||||
|
>
|
||||||
|
> **The website is upstream's and we do not have it.** They split it out on 2026-04-14
|
||||||
|
> (commit `65f830e`) into a private repo of their own. `guide.html` lives there, so the
|
||||||
|
> website work in the overseas plan is blocked until we either get access or build our own.
|
||||||
|
|
||||||
Both portable and desktop versions auto-find a free port in range 18789–18799 and start the OpenClaw gateway. On first run, they detect whether a model is configured — if not, they open Config.html; otherwise, they open the dashboard.
|
Both portable and desktop versions auto-find a free port in range 18789–18799 and start the OpenClaw gateway. On first run, they detect whether a model is configured — if not, they open Config.html; otherwise, they open the dashboard.
|
||||||
|
|
||||||
## Key Technical Details
|
## Key Technical Details
|
||||||
|
|
||||||
- **Node.js discovery**: Portable looks at `app/runtime/node-mac-arm64/bin/node`; Electron looks at `resources/runtime/node-{platform}-{arch}` then falls back to system `node`
|
- **Node.js discovery**: Portable looks at `app/runtime/node-mac-arm64/bin/node`; Electron looks at `resources/runtime/node-{platform}-{arch}` then falls back to system `node`
|
||||||
- **China mirrors**: All downloads use `npmmirror.com` — Node.js binaries from `npmmirror.com/mirrors/node`, npm packages from `registry.npmmirror.com`
|
- **Download sources (overseas build, 2026-08-17)**: Node.js binaries from `nodejs.org/dist`, npm packages from `registry.npmjs.org`, GitHub assets direct. No China-routed mirrors or GitHub proxies remain in scripts, launchers or CI — `tests/no-china-sources.test.mjs` fails the build if any come back.
|
||||||
- **`OPENCLAW_VERSION` file**: single source of truth for the bundled OpenClaw runtime version (e.g. `2026.6.8`). CI reads it to pin the npm install; it's copied into `portable/` so USB users / `check-update.mjs` can compare installed vs latest. Bump this file to upgrade.
|
- **`OPENCLAW_VERSION` file**: single source of truth for the bundled OpenClaw runtime version (e.g. `2026.6.8`). CI reads it to pin the npm install; it's copied into `portable/` so USB users / `check-update.mjs` can compare installed vs latest. Bump this file to upgrade.
|
||||||
- **Environment variables**: `OPENCLAW_HOME`, `OPENCLAW_STATE_DIR`, `OPENCLAW_CONFIG_PATH` control where OpenClaw reads config
|
- **Environment variables**: `OPENCLAW_HOME`, `OPENCLAW_STATE_DIR`, `OPENCLAW_CONFIG_PATH` control where OpenClaw reads config
|
||||||
- **macOS quarantine**: Mac scripts run `xattr -rd com.apple.quarantine` to remove Gatekeeper blocks
|
- **macOS quarantine**: Mac scripts run `xattr -rd com.apple.quarantine` to remove Gatekeeper blocks
|
||||||
@@ -137,33 +178,76 @@ Pure-Node, zero-dependency `.mjs` modules (use `fetch` + `node:zlib` only). All
|
|||||||
| File | Purpose |
|
| File | Purpose |
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| `check-update.mjs` / `publish-latest.mjs` | Portable self-update: check installed vs latest `OPENCLAW_VERSION`; publish helper. |
|
| `check-update.mjs` / `publish-latest.mjs` | Portable self-update: check installed vs latest `OPENCLAW_VERSION`; publish helper. |
|
||||||
| `portable-cache.mjs` | **启动加速核心**:把"重 IO、可重建"的缓存从 U 盘搬到本机硬盘。算出本机缓存根(win `%LOCALAPPDATA%\U-Claw\<slot>` / mac `~/Library/Caches/U-Claw` / linux `$XDG_CACHE_HOME`,UUID 隔离让换盘符仍复用),输出 `NODE_COMPILE_CACHE` 路径,并把 `data/.openclaw/browser` 做成 junction(win)/symlink(mac) 指向本机盘——浏览器 user-data 的海量随机小写不再砸 U 盘。CLI 打印 `KEY=VALUE` 供启动脚本 source。静默失败:取不到就缓存留 U 盘照常启动。 |
|
| `portable-cache.mjs` | **The startup-speed core.** Moves the heavy, rebuildable caches off the USB drive onto local disk: works out a cache root (win `%LOCALAPPDATA%\U-Claw\<slot>`, mac `~/Library/Caches/U-Claw`, linux `$XDG_CACHE_HOME`), keyed by a UUID so the same cache is reused when the drive gets a different letter. Emits `NODE_COMPILE_CACHE`, and redirects `data/.openclaw/browser` to local disk via a junction (Windows) or symlink (macOS) — the browser profile's endless small random writes are what makes a USB drive feel broken. Prints `KEY=VALUE` for the launcher to read. Fails silently: if it cannot, the cache stays on the drive and startup continues. |
|
||||||
| `prewarm.mjs` | gateway 首轮预热:端口就绪后后台静默 GET `/ready`→`/status`→`/models`(带 `x-openclaw-token`),把 config/model 子系统在 runtime 内存里热起来,用户首次点发送不再等。零依赖、短超时、后台 detach。 |
|
| `prewarm.mjs` | Wakes the gateway once it is listening: background `GET /ready` → `/status` → `/models` with the auth token, so the config and model subsystems are warm before the user's first message. Zero dependencies, short timeout, detached. |
|
||||||
| `loading.html` | 启动首屏(splash):双击启动后立刻打开,给即时反馈消除"黑窗假死"。本页每秒 fetch `/ready`,gateway 真就绪后自动 `location.replace` 跳 Dashboard——天然规避"gateway 没起就开 Dashboard 拒连"(issue #46/#48)。端口经 `?port=` 传入。 |
|
| `loading.html` | The splash. Opens the instant the launcher starts, so a slow first run looks like progress rather than a frozen black window. Polls `/ready` once a second and moves on when the gateway answers — which sidesteps the old "dashboard opened before the gateway existed, connection refused" failure (issues #46, #48). Port arrives as `?port=`. |
|
||||||
| `wait-gateway.bat` | Windows 兜底:现由 `loading.html` 首屏轮询并自动跳转;本脚本退居兜底——万一首屏 `file://` fetch 被浏览器拦,仍轮询端口、就绪后开 Dashboard。 |
|
| `start.mjs` | **All startup logic (since 2026-08-17).** Runtime detection, data folders, cache acceleration, default config, dependency install, NO_PROXY, WeChat plugin, config-server launch and port discovery, gateway port scan, opening the splash and Settings, prewarm, the fallback watcher, and cleanup on exit. The `.bat` and `.command` files are ~30-line shells over it. Two reasons: the platforms cannot drift apart again, and every user-facing string becomes translatable — neither is possible while the logic lives in a `.bat` that must stay pure ASCII. |
|
||||||
| `maintain.sh` | Maintenance/diagnostics script. |
|
| `i18n.mjs` + `messages/{en,zh-CN}.json` | Strings for the launcher and the local pages. Resolution order: explicit override > `uclaw.locale` on the drive > system locale > `en`. **The language follows the drive, not the machine** — someone who set it up in Chinese and plugs it into a colleague's English Windows still gets Chinese. Keys are semantic, and a missing one falls back to readable English rather than leaking the key. |
|
||||||
|
| `self-heal.mjs` | Checks and repairs run when startup fails, before the user is told anything. Missing folders, a damaged settings file (moved aside, never deleted), a leftover port record, an interrupted copy, a dangling cache link. What it cannot fix produces a redacted diagnostics report. Every repair must be safe on a healthy drive — they all run whenever startup fails, including for reasons none of them explain. |
|
||||||
|
| `provider-detect.mjs` | Maps an API key prefix to its provider, so nobody has to know what a Base URL is. `sk-ant-` must be checked before the plain `sk-` catch-all. |
|
||||||
|
| ~~`wait-gateway.bat`~~ | Removed 2026-08-17. The fallback watcher moved into `start.mjs` as `watchGatewayReady()`, so macOS has it too — it was Windows-only before. |
|
||||||
|
|
||||||
### 启动加速(吸收自 v2 u-clawx 4.0 的工程经验,2026-06-17)
|
### Why startup is fast off a slow drive (2026-06-17)
|
||||||
|
|
||||||
便携版从 U 盘启动慢,瓶颈在 **U 盘随机小写 IO** + **首屏无反馈** + **首轮冷启动**。移植 4.0 的 4 个可在纯脚本层复刻的手段:
|
Running from a USB drive is slow for three separate reasons, and each needed its
|
||||||
|
own fix: **small random writes on the drive**, **no feedback on the first
|
||||||
|
screen**, and **a genuinely cold first run**.
|
||||||
|
|
||||||
1. **缓存搬本机**(`portable-cache.mjs`):浏览器 user-data(OpenClaw 硬编码在 `CONFIG_DIR/browser/`,无单独环境变量,故用 junction/symlink 重定向)+ V8 编译缓存(`NODE_COMPILE_CACHE`)落本机 SSD。业务数据(`openclaw.json`、`memory`、账号)仍留 U 盘,便携性不变。UUID 隔离让 D:→E: 换盘符仍命中同一份本机缓存。
|
1. **Move the caches to local disk** (`portable-cache.mjs`). The browser profile
|
||||||
2. **Node 编译缓存**:`openclaw.mjs` 本就调 `module.enableCompileCache()`,但默认落系统 temp(可能被清)。启动脚本显式把 `NODE_COMPILE_CACHE` 指向本机固定目录,二次启动稳定命中。
|
is the worst offender — OpenClaw hardcodes it at `CONFIG_DIR/browser/` with no
|
||||||
3. **启动首屏**(`loading.html`):双击即弹,自轮询自跳转。
|
environment variable to move it, so we redirect it with a junction (Windows)
|
||||||
4. **首轮预热**(`prewarm.mjs`):后台唤醒 config/model。
|
or symlink (macOS). The V8 compile cache goes the same way via
|
||||||
5. **动态探测**:Windows 把写死的 `timeout /t 2`(等 config-server)改成轮询 18788,省掉白等。
|
`NODE_COMPILE_CACHE`. **Business data stays on the drive** — `openclaw.json`,
|
||||||
|
`memory`, accounts — so portability is unaffected. A UUID key means the same
|
||||||
|
cache is reused when the drive comes up as `E:` instead of `D:`.
|
||||||
|
2. **Pin the compile cache.** `openclaw.mjs` already calls
|
||||||
|
`module.enableCompileCache()`, but it defaults to system temp, which gets
|
||||||
|
cleaned. Pointing `NODE_COMPILE_CACHE` at a stable local directory makes the
|
||||||
|
second launch reliably fast rather than occasionally fast.
|
||||||
|
3. **Show something immediately** (`loading.html`). It opens on double-click and
|
||||||
|
polls for readiness itself.
|
||||||
|
4. **Warm the gateway** (`prewarm.mjs`) in the background once the port answers.
|
||||||
|
5. **Poll instead of guessing.** Windows used to `timeout /t 2` waiting for the
|
||||||
|
config server; it now polls the port and proceeds the moment it is up.
|
||||||
|
|
||||||
> 注:OpenClaw 自身的临时/lock/chrome-mcp 文件已走 `os.tmpdir()`(系统 temp,**不在 U 盘**),无需处理;真正落 U 盘的只有 `OPENCLAW_HOME=data/` 下的内容。
|
> OpenClaw's own temp, lock and chrome-mcp files already go to `os.tmpdir()` —
|
||||||
|
> system temp, **not the drive** — so they need no handling. The only thing that
|
||||||
|
> genuinely lands on the drive is what lives under `OPENCLAW_HOME=data/`.
|
||||||
|
|
||||||
> **纯开源,无追踪**: 这个开源版**不含**设备指纹 (`fingerprint.mjs`)、自动开户 (`bootstrap-xiapan.mjs`/`xiapan-client.mjs`)、崩溃上报 (`report-bug.mjs`) 等商业版逻辑——这些已在 2026-06-17 移除。U-Claw 不绑定设备、不打指纹、不向 `api.u-claw.org` 上传任何数据。
|
> **No tracking.** This build contains none of the commercial-version logic —
|
||||||
|
> no device fingerprinting (`fingerprint.mjs`), no automatic account creation
|
||||||
|
> (`bootstrap-xiapan.mjs` / `xiapan-client.mjs`), no crash reporting
|
||||||
|
> (`report-bug.mjs`). Upstream removed them on 2026-06-17. Nothing is bound to a
|
||||||
|
> device and nothing is uploaded to `api.u-claw.org`.
|
||||||
|
|
||||||
### 模型配置 ("选模型填 Key")
|
### Choosing a model (rewritten 2026-08-17)
|
||||||
|
|
||||||
Config.html 列出国内外大模型供用户挑选。首选卡片是 **虾盘云** (Xiapan Cloud 中转站,`api.u-claw.org/v1`)
|
Settings is `config-server/public/index.html`. `Config.html` in `advanced/` is a
|
||||||
——一个 Key 调用 DeepSeek / Claude / GPT / 通义 等全部模型,但和其它 provider 一样**需要用户自己去
|
redirect shim to it — the old standalone copy called the API on a root-relative
|
||||||
`https://u-claw.org/cloud.html` 注册拿 Key**,不再自动开户。其余 provider (DeepSeek/通义/Kimi/智谱/
|
path while only ever being opened from `file://`, so its Save button had been
|
||||||
豆包/MiniMax/OpenAI/Claude/Groq/硅基流动/自定义) 填各家官方 Key 即可。配置只写本地
|
failing silently.
|
||||||
`data/.openclaw/openclaw.json`。
|
|
||||||
|
**The first screen asks for one thing: an API key.** It used to lay out twelve
|
||||||
|
provider cards and then demand a Base URL and a model name — three technical
|
||||||
|
fields on the first screen a non-technical user ever saw.
|
||||||
|
|
||||||
|
The key itself says which provider it belongs to (`lib/provider-detect.mjs`
|
||||||
|
maps the prefix), so:
|
||||||
|
|
||||||
|
1. Paste the key
|
||||||
|
2. After an 800ms pause, `POST /api/test-key` sends a 1-token request and reports
|
||||||
|
what actually happened — "connected in 340ms", or a sentence saying what to do
|
||||||
|
3. A model dropdown appears once it works, so the choice is not locked to
|
||||||
|
whatever the first key implied
|
||||||
|
|
||||||
|
The check runs server-side deliberately: the browser cannot reach most model
|
||||||
|
endpoints cross-origin, and doing it in the config server means the key never
|
||||||
|
leaves the machine by any path the page controls.
|
||||||
|
|
||||||
|
Endpoint and model name still exist, under a collapsed *Advanced* section that
|
||||||
|
only opens by itself when the key is not one we recognise.
|
||||||
|
|
||||||
|
Everything is written to `data/.openclaw/openclaw.json` on the drive.
|
||||||
|
|
||||||
## What NOT to Commit
|
## What NOT to Commit
|
||||||
|
|
||||||
@@ -172,29 +256,33 @@ Never commit runtime dependencies or build artifacts. These are all in .gitignor
|
|||||||
- `u-claw-app/node_modules/`, `u-claw-app/release/`, `u-claw-app/resources/runtime/`
|
- `u-claw-app/node_modules/`, `u-claw-app/release/`, `u-claw-app/resources/runtime/`
|
||||||
- `*.dmg`, `*.exe`, `*.blockmap`
|
- `*.dmg`, `*.exe`, `*.blockmap`
|
||||||
|
|
||||||
Release artifacts go to GitHub Releases, not the repo.
|
Release artifacts go to the release page, not the repo.
|
||||||
|
|
||||||
## Branding Rules
|
## Branding Rules
|
||||||
|
|
||||||
- Use only official `openclaw` (not `openclaw-cn` or any community fork)
|
- Use only official `openclaw` (not `openclaw-cn` or any community fork)
|
||||||
- All npm installs reference `openclaw@latest` (official package)
|
- All npm installs reference `openclaw@latest` (official package)
|
||||||
- External links point to `u-claw.org` (our site) or `github.com/openclaw/openclaw` (upstream)
|
- External links go to `origin.json`'s `urls`, or to `github.com/openclaw/openclaw` for the runtime.
|
||||||
|
We do **not** own `u-claw.org` — that is the upstream project's domain, so never
|
||||||
|
offer an address on it as a support route.
|
||||||
- No references to competitor products (Qclaw, AutoClaw) in any tracked files
|
- No references to competitor products (Qclaw, AutoClaw) in any tracked files
|
||||||
- Skill marketplace links point to `skillhub.tencent.com` or `github.com/openclaw/clawhub`
|
- Skill marketplace links point to `skillhub.tencent.com` or `github.com/openclaw/clawhub`
|
||||||
|
|
||||||
## Platform Support Status
|
## Platform Support Status
|
||||||
|
|
||||||
- Mac Apple Silicon (ARM64): ✅ Working
|
- Mac Apple Silicon (ARM64): ✅ Working
|
||||||
- Mac Intel (x64): ✅ Working(portable 需先运行 setup.sh 下载 node-mac-x64)
|
- Mac Intel (x64): ✅ Working — run `setup.sh` first to fetch `node-mac-x64`
|
||||||
- Windows x64: 🚧 In development
|
- Windows x64: 🚧 **Carries unverified changes.** A large amount of PowerShell and
|
||||||
- Linux x64 (Bootable USB): ✅ `bootable/` 目录 + 独立仓库 [u-claw-linux](https://github.com/dongsheng123132/u-claw-linux)
|
the `.bat` path resolution were written on a Mac with no PowerShell available and
|
||||||
|
have never been through a parser. See `U盘实测清单.md` in the workspace root.
|
||||||
|
- Linux x64 (bootable USB): ✅ see `bootable/`, mirrored at [u-claw-linux](https://gitea.fanghe.it.com/zhenghy/u-claw-linux)
|
||||||
|
|
||||||
## Bootable Linux Key Details
|
## Bootable Linux Key Details
|
||||||
|
|
||||||
- **制作环境**: Windows 10/11 + PowerShell (Admin),4 步脚本
|
- **Built on**: Windows 10/11, PowerShell as Administrator, four scripts in order
|
||||||
- **U 盘要求**: 32GB+ USB 3.0
|
- **Drive**: 32 GB or larger, USB 3.0
|
||||||
- **技术栈**: Ventoy 1.0.99 引导 → Ubuntu 24.04 ISO → casper-rw 持久化 → OpenClaw 安装到 /opt/u-claw/
|
- **Stack**: Ventoy 1.0.99 → Ubuntu 24.04 ISO → casper-rw persistence → OpenClaw in /opt/u-claw/
|
||||||
- **国内镜像**: ISO 下载走清华/阿里/中科大,Node.js 和 npm 走 npmmirror.com
|
- **Sources**: ISO from releases.ubuntu.com, Node.js from nodejs.org, packages from registry.npmjs.org
|
||||||
- **Linux 环境变量**: `OPENCLAW_HOME=/opt/u-claw/data/.openclaw`
|
- **Environment**: `OPENCLAW_HOME=/opt/u-claw/data/.openclaw`
|
||||||
- **bootable/ 完全独立**: 不引用 portable/、u-claw-app/ 的任何文件,修改互不影响
|
- **Self-contained**: references nothing from `portable/` or `u-claw-app/`, so a change here cannot break them or vice versa
|
||||||
- **同步**: bootable/ 内容与 u-claw-linux 仓库保持一致,改一边要记得同步另一边
|
- **Mirrored**: kept identical to the u-claw-linux repo — a change in one needs the same change in the other
|
||||||
|
|||||||
237
CONTRIBUTING.md
237
CONTRIBUTING.md
@@ -1,164 +1,147 @@
|
|||||||
# 贡献指南 / Contributing to U-Claw
|
# Contributing
|
||||||
|
|
||||||
感谢有兴趣参与 U-Claw 开发!这份文档会告诉你怎么开始、怎么提 issue、怎么提 PR。
|
## The mental model, first
|
||||||
|
|
||||||
## TL;DR
|
> **This repository is the USB drive's contents** — scripts, HTML, small files.
|
||||||
|
> **After `setup.sh` it is a working drive** — the above plus Node.js and OpenClaw.
|
||||||
|
|
||||||
- **报 bug** → 用 [Bug 模板](.github/ISSUE_TEMPLATE/bug_report.md),**贴完整报错**,不要只发截图
|
It is not a build tool that produces a drive. It *is* the drive. That one idea
|
||||||
- **提需求** → 用 [Feature 模板](.github/ISSUE_TEMPLATE/feature_request.md),先说清楚使用场景
|
explains most of the layout.
|
||||||
- **写代码** → fork → 改 → 自己跑过 → 提 PR,PR 说明照模板填,不要空白 PR
|
|
||||||
- **改文档** → 直接 PR 即可
|
|
||||||
|
|
||||||
## 项目结构(记住这个心智模型)
|
Four distribution forms, deliberately independent — changing one does not touch
|
||||||
|
the others:
|
||||||
|
|
||||||
> **本仓库 = U 盘骨架**:脚本 + HTML + 小文件
|
| Directory | Form | Entry point |
|
||||||
> **`bash setup.sh` 之后 = U 盘内容**:骨架 + Node.js + OpenClaw
|
|---|---|---|
|
||||||
|
| `portable/` | USB drive — **the main product** | `advanced/setup.sh` → `START HERE - Mac.command` |
|
||||||
|
| `bootable/` | Linux USB that boots a machine with no OS | `1-prepare-usb.ps1` → `4-copy-to-usb.ps1` |
|
||||||
|
| `install/` | One-line network install | `install.sh` / `install.ps1` |
|
||||||
|
| `u-claw-app/` | Electron desktop — **deprecated**, archive only | not built |
|
||||||
|
|
||||||
四种发布形态,互相独立,改其中一个不影响其它:
|
## This is a fork
|
||||||
|
|
||||||
| 目录 | 形态 | 入口 |
|
Upstream is `github.com/dongsheng123132/u-claw`. We host at
|
||||||
|------|------|------|
|
`gitea.fanghe.it.com/zhenghy/u-claw` and **do not send changes back**: this build
|
||||||
| `portable/` | 便携 USB | `setup.sh` → `Mac-Start.command` / `Windows-Start.bat` |
|
removes the China-market defaults that are the point of the upstream project.
|
||||||
| `u-claw-app/` | Electron 桌面 | `npm run dev` / `npm run build:mac-arm64` |
|
|
||||||
| `bootable/` | Linux 可启动 U 盘 | `1-prepare-usb.ps1` → `4-copy-to-usb.ps1` |
|
|
||||||
| `install/` | 一键在线安装 | `install.sh` (Mac/Linux) / `install.ps1` (Windows) |
|
|
||||||
|
|
||||||
## 开发环境
|
If you are fixing something we inherited unchanged, consider telling them too.
|
||||||
|
|
||||||
|
## Getting set up
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Clone
|
git clone ssh://git@gitea.fanghe.it.com:222/zhenghy/u-claw.git
|
||||||
git clone https://github.com/dongsheng123132/u-claw.git
|
|
||||||
cd u-claw
|
cd u-claw
|
||||||
|
|
||||||
# 2. 选一个形态调试。最快的是 portable/
|
# Enable the pre-push hook. Nothing installs it for you, and it is the only
|
||||||
cd portable
|
# thing reliably running the tests — see below.
|
||||||
bash setup.sh # 下载 Node.js + OpenClaw 到 app/
|
git config core.hooksPath .githooks
|
||||||
bash Mac-Start.command # macOS 启动
|
|
||||||
# 或 Windows: 双击 Windows-Start.bat
|
# The fastest thing to iterate on is the portable build
|
||||||
|
cd portable/advanced && bash setup.sh # fetches Node.js + OpenClaw into ../app/
|
||||||
|
cd .. && bash "START HERE - Mac.command"
|
||||||
```
|
```
|
||||||
|
|
||||||
平台支持现状:
|
| Platform | State |
|
||||||
|
|---|---|
|
||||||
|
| macOS Apple Silicon | ✅ main development platform |
|
||||||
|
| macOS Intel | ✅ works — `setup.sh` fetches `node-mac-x64` first |
|
||||||
|
| Windows x64 | 🚧 **needs verification** — see below |
|
||||||
|
| Linux x64 | ✅ via `bootable/` |
|
||||||
|
|
||||||
- macOS Apple Silicon (ARM64):✅ 主开发平台
|
> **Windows currently carries unverified changes.** A large amount of PowerShell
|
||||||
- macOS Intel:✅ 工作(需先跑 setup.sh 下 node-mac-x64)
|
> and several `.bat` path-resolution changes were written on a Mac with no
|
||||||
- Windows x64:🚧 持续完善
|
> PowerShell available, so they have never been through a parser. If you have a
|
||||||
- Linux x64 (Bootable USB):✅ 用 `bootable/`
|
> Windows machine, running that check is the single most useful thing you can do
|
||||||
|
> right now. `U盘实测清单.md` in the workspace root has the exact commands.
|
||||||
|
|
||||||
## 提 Issue 的好习惯
|
## Tests
|
||||||
|
|
||||||
### 报 bug
|
|
||||||
|
|
||||||
**至少包含这四样**:
|
|
||||||
|
|
||||||
1. 操作系统 + 版本(如 macOS 14.5 / Windows 11 23H2)
|
|
||||||
2. 使用的形态(portable / install / bootable / u-claw-app)
|
|
||||||
3. **完整的错误日志**(贴文字,不要只截图)
|
|
||||||
4. 你试过哪些步骤
|
|
||||||
|
|
||||||
不写复现步骤的 issue,维护者通常没办法处理。
|
|
||||||
|
|
||||||
### 提需求
|
|
||||||
|
|
||||||
- 先说**使用场景**,再说功能。
|
|
||||||
- "我希望能 X" → 不够。"我在做 Y,需要 X,因为 Z" → 才能讨论。
|
|
||||||
|
|
||||||
## 提 PR 的好习惯
|
|
||||||
|
|
||||||
### 提交前先想清楚
|
|
||||||
|
|
||||||
1. **改动是不是真的有人需要?** 大改动建议先开 issue 讨论。
|
|
||||||
2. **改动会不会破坏其它形态?** 比如改 `portable/` 不要影响 `bootable/`。
|
|
||||||
3. **你跑过吗?** PR 模板里要求列出测试方式,不是装饰。
|
|
||||||
|
|
||||||
### 不要做的事
|
|
||||||
|
|
||||||
- ❌ 提交 `node_modules/`、`app/`、`data/`、`*.dmg`、`*.exe`(已在 `.gitignore`)
|
|
||||||
- ❌ 把 API Key、Token、密码写进任何文件
|
|
||||||
- ❌ 改 README 加自己的推广链接
|
|
||||||
- ❌ 提空白 PR(只有标题没说明)—— 会直接关闭
|
|
||||||
- ❌ 大规模格式化无关代码("顺手 prettier 全仓"这种)
|
|
||||||
|
|
||||||
### Commit message
|
|
||||||
|
|
||||||
短、说人话、能让维护者一眼看懂改了什么:
|
|
||||||
|
|
||||||
```
|
|
||||||
fix(portable): node-extract path missing intermediate dir
|
|
||||||
fix(install.sh): npm wrapper不能用 node 直接执行
|
|
||||||
docs: 补充 Windows 11 ARM64 支持说明
|
|
||||||
feat(skills-cn): 增加 douyin-script 技能
|
|
||||||
```
|
|
||||||
|
|
||||||
不接受:"update"、"fix"、"修改若干文件" 这种。
|
|
||||||
|
|
||||||
### 分支与 PR 流程
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. fork → clone 你的 fork
|
node --test # everything
|
||||||
git clone https://github.com/<你的用户名>/u-claw.git
|
node --test tests/windows-launchers.test.mjs # one file
|
||||||
|
|
||||||
# 2. 创建分支(不要在 main 上直接改)
|
|
||||||
git checkout -b fix/install-sh-npm-path
|
|
||||||
|
|
||||||
# 3. 改 → 自己跑过 → 提交
|
|
||||||
git add <具体文件> # 不要 git add .
|
|
||||||
git commit -m "fix(install): ..."
|
|
||||||
|
|
||||||
# 4. 推到你的 fork
|
|
||||||
git push origin fix/install-sh-npm-path
|
|
||||||
|
|
||||||
# 5. 在 GitHub 上发起 PR,填好模板
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 修改各形态时的注意事项
|
**Not `node --test tests/`.** Node reads that path as a module to load and dies
|
||||||
|
with "Cannot find module" — zero tests run, exit code 1. That form was the
|
||||||
|
documented command here for a long time, which is a good part of why the suite
|
||||||
|
went unrun for the life of the project.
|
||||||
|
|
||||||
### `portable/`
|
The suite asserts on the *text and behaviour* of scripts rather than spawning
|
||||||
|
OpenClaw: that `.bat` files stay pure ASCII (Chinese Windows `cmd.exe` reads
|
||||||
|
non-ASCII as GBK and mis-parses), that `.command` files stay LF-only, that no
|
||||||
|
China-routed download source comes back, that both installers resolve skills
|
||||||
|
identically, that every UI string exists in every language, and that nothing
|
||||||
|
points at a host `origin.json` does not declare.
|
||||||
|
|
||||||
- **不要在仓库里提交 `app/` 或 `data/`**,那是 `setup.sh` 下载/生成的
|
CI runs it where a runner exists (`.gitea/workflows/tests.yml`, mirrored to
|
||||||
- Mac 启动脚本要 `chmod +x`,并清理 quarantine 属性
|
`.github/`). On our Gitea there is currently no registered runner, so the
|
||||||
- Windows 启动脚本要正确处理 `cd /d "%DIR%core"`
|
pre-push hook is doing the work. Install it.
|
||||||
- 配置文件放 `data/.openclaw/openclaw.json`,便携属性靠这个
|
|
||||||
|
|
||||||
### `u-claw-app/` (Electron)
|
## Filing an issue
|
||||||
|
|
||||||
- `main.js` 大约 400 行,改前先理解整体流程
|
**A bug report needs four things**, or nobody can act on it:
|
||||||
- Node.js 要找 `resources/runtime/node-{platform}-{arch}`,找不到再 fall back 到系统 node
|
|
||||||
- 用户配置在 `app.getPath('userData')/.openclaw/`,不要硬编码路径
|
|
||||||
|
|
||||||
### `bootable/`
|
1. OS and version — `macOS 14.5`, `Windows 11 23H2`
|
||||||
|
2. Which form — portable / install / bootable
|
||||||
|
3. **The full error as text.** Not a screenshot of text.
|
||||||
|
4. What you already tried
|
||||||
|
|
||||||
- 4 步 PowerShell 脚本必须**按顺序**跑
|
**A feature request needs a situation, not a feature.** "I'd like X" is not
|
||||||
- ISO 下载走清华/阿里/中科大镜像,不要直接用 ubuntu.com
|
enough to discuss. "I'm doing Y, I need X, because Z" is.
|
||||||
- `bootable/` 与独立仓库 `dongsheng123132/u-claw-linux` 内容保持同步,改一边记得同步另一边
|
|
||||||
|
|
||||||
### `install/`
|
## Sending a change
|
||||||
|
|
||||||
- Mac/Linux 走 `install.sh`,Windows 走 `install.ps1`
|
Before you write it:
|
||||||
- 全部走 npmmirror.com 镜像,不能假设用户能访问 GitHub/npm 官方
|
|
||||||
- 安装目录固定为 `~/.uclaw/`(Mac/Linux)或 `%USERPROFILE%\.uclaw\`(Windows)
|
|
||||||
- 启动脚本要找空闲端口(18789-18799),不要写死
|
|
||||||
|
|
||||||
### `skills-cn/`
|
- **Does anyone need this?** For anything large, open an issue first.
|
||||||
|
- **Does it break another form?** A change in `portable/` should not affect
|
||||||
|
`bootable/`.
|
||||||
|
- **Did you run it?** Not just the tests — the actual thing.
|
||||||
|
|
||||||
- 技能格式是 `<skill-name>/SKILL.md`,frontmatter 里有 `name`、`description`、`metadata`
|
Do not:
|
||||||
- 技能内容用中文写,给中国用户看
|
|
||||||
- 提交新技能前先看现有技能(小红书/微博/B 站等)的写法
|
|
||||||
|
|
||||||
## 行为准则
|
- Commit `node_modules/`, `app/`, `data/`, `*.dmg`, `*.exe` — all in `.gitignore`
|
||||||
|
- Put an API key, token or password in any file
|
||||||
|
- Reformat unrelated code ("ran prettier over everything" makes review impossible)
|
||||||
|
- Open a PR with a title and no description
|
||||||
|
|
||||||
简单说:
|
Commit messages: say what changed and why, in a sentence someone can act on.
|
||||||
|
|
||||||
- 对人有礼貌,对事可以严格
|
```
|
||||||
- 不要在 issue 里互相攻击
|
fix(install.sh): npm wrapper cannot be run through node directly
|
||||||
- 维护者可能回复慢(这是开源副业),请耐心
|
feat(skills): add linkedin-post
|
||||||
- 不接受任何形式的歧视、骚扰、钓鱼
|
docs: note Windows 11 ARM64 support
|
||||||
|
```
|
||||||
|
|
||||||
## 联系
|
Not `update`, `fix`, or `changed some files`.
|
||||||
|
|
||||||
- Issue 区:日常问题、bug、需求
|
```bash
|
||||||
- 官网:[u-claw.org](https://u-claw.org)
|
git switch -c fix/something # never work on main
|
||||||
- 邮件(仅紧急安全问题):见 README
|
git add <specific files> # not git add .
|
||||||
|
git push -u origin fix/something
|
||||||
|
```
|
||||||
|
|
||||||
---
|
## Notes per form
|
||||||
|
|
||||||
再次感谢!🦞
|
**`portable/`**
|
||||||
|
- Never commit `app/` or `data/` — `setup.sh` produces them
|
||||||
|
- `.command` files need the executable bit and LF endings
|
||||||
|
- `.bat` files must be pure ASCII with CRLF, and must not contain an unescaped
|
||||||
|
`)` inside an `IF (...)` block — that closes the block early and the window
|
||||||
|
flash-exits. There is a test for it, and it has caught real regressions.
|
||||||
|
- User config lives in `data/.openclaw/openclaw.json`. That file being on the
|
||||||
|
drive is the entire portability story; do not move it.
|
||||||
|
- User-facing text belongs in `lib/messages/*.json`, never in a script
|
||||||
|
|
||||||
|
**`bootable/`**
|
||||||
|
- The four PowerShell scripts must run **in order**
|
||||||
|
- Fully self-contained: it references nothing from `portable/` on purpose
|
||||||
|
|
||||||
|
**`install/`**
|
||||||
|
- These run through `curl | bash` with **no checkout**, so they cannot read
|
||||||
|
`origin.json` and carry URLs as literals. `tests/origin.test.mjs` fails if
|
||||||
|
those literals drift from `origin.json` — that is what stops a host migration
|
||||||
|
from half-happening.
|
||||||
|
- Neither script may contain skill text. Both call `lib/install-skills.mjs`.
|
||||||
|
|
||||||
|
**`u-claw-app/`** — deprecated 2026-06-19, kept for archive. Do not add to it.
|
||||||
|
|||||||
98
HANDOFF.md
98
HANDOFF.md
@@ -1,77 +1,51 @@
|
|||||||
# Handoff Document — U-Claw Linux Remote Access
|
# Post-mortem: an unformatted persistence file bricks the boot
|
||||||
|
|
||||||
## Current Status
|
*2026-03-17. Kept because the failure gives no useful signal about its own
|
||||||
|
cause, and it will happen again to whoever builds the next drive.*
|
||||||
|
|
||||||
- U盘已就绪:Ventoy + Ubuntu 24.04 ISO + persistence + u-claw-linux scripts
|
## What happened
|
||||||
- 所有源码已推送到 GitHub main 分支
|
|
||||||
- website/guide.html 已添加快速命令参考卡片
|
|
||||||
- enable-ssh.sh 已添加到 bootable/linux-setup/
|
|
||||||
|
|
||||||
## Linux 启动后需要做的事
|
A freshly built bootable USB dropped straight into an initramfs prompt instead
|
||||||
|
of booting Ubuntu. Nothing on screen pointed at the real problem.
|
||||||
|
|
||||||
1. 从 U 盘启动 → Ventoy 菜单选 Ubuntu
|
`persistence.dat` on the drive was a 20 GB **empty** file with no ext4
|
||||||
2. 如果首次使用,格式化持久化:
|
filesystem in it. `3-create-persistence.ps1` had found no standard WSL, taken
|
||||||
```
|
its fallback path, and produced a sparse file it never formatted. Ventoy read
|
||||||
sudo bash /media/*/Ventoy/u-claw-linux/format-persistence.sh
|
`ventoy.json`, tried to mount that file, failed, and the boot collapsed into
|
||||||
```
|
initramfs.
|
||||||
然后重启
|
|
||||||
3. 连接 WiFi
|
|
||||||
4. 安装 OpenClaw:
|
|
||||||
```
|
|
||||||
sudo bash /media/*/Ventoy/u-claw-linux/setup-openclaw.sh
|
|
||||||
```
|
|
||||||
5. 启用 SSH(让 Mac mini 远程控制):
|
|
||||||
```
|
|
||||||
sudo bash /media/*/Ventoy/u-claw-linux/enable-ssh.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
## SSH 连接信息
|
The chain is silent at every step: the build script reports success, Ventoy
|
||||||
|
reports nothing, and initramfs says only that it has no root.
|
||||||
|
|
||||||
- **用户名**: ubuntu (Live USB 默认用户)
|
## The fix at the time
|
||||||
- **密码**: 运行 enable-ssh.sh 时设置
|
|
||||||
- **IP 地址**: 运行 enable-ssh.sh 后会显示,格式类似 192.168.x.x
|
|
||||||
- **前提**: 两台机器在同一个 WiFi/局域网下
|
|
||||||
|
|
||||||
## Mac mini Claude Code 接手方法
|
Formatted it using docker-desktop's WSL, which has `mkfs.ext4` even though it is
|
||||||
|
not a standard WSL distribution:
|
||||||
|
|
||||||
1. 用户在 Linux 上运行 `enable-ssh.sh`,获取 IP 和设置密码
|
```bash
|
||||||
2. 用户告诉 Mac mini 上的 Claude Code:IP 地址和密码
|
dd if=/dev/zero of=/tmp/persistence.dat bs=1M count=0 seek=20480
|
||||||
3. Mac mini Claude Code 通过 Bash 工具执行:
|
/sbin/mkfs.ext4 -F -L casper-rw /tmp/persistence.dat
|
||||||
```
|
```
|
||||||
ssh ubuntu@<IP> 'command'
|
|
||||||
```
|
|
||||||
或建立持久连接进行调试
|
|
||||||
|
|
||||||
## 文件位置(Linux 上)
|
Then copied it to `C:` and moved it onto the drive — docker-desktop's WSL cannot
|
||||||
|
reach other drive letters directly.
|
||||||
|
|
||||||
| 路径 | 说明 |
|
## How to tell a persistence file is real
|
||||||
|------|------|
|
|
||||||
| `/media/*/Ventoy/u-claw-linux/` | U 盘上的脚本 |
|
|
||||||
| `/opt/u-claw/` | 安装后的 OpenClaw |
|
|
||||||
| `/opt/u-claw/start-openclaw.sh` | 启动脚本 |
|
|
||||||
| `/opt/u-claw/data/.openclaw/openclaw.json` | 配置文件 |
|
|
||||||
|
|
||||||
## 注意事项
|
Read two bytes at offset 1080. They should be `53 EF` — the little-endian form
|
||||||
|
of the ext4 magic number `0xEF53`. An empty file has zeros there.
|
||||||
|
|
||||||
- Live USB 每次重启后,除了持久化分区内的数据,其他都会重置
|
This is the check worth keeping: it takes a second and distinguishes "built
|
||||||
- SSH server 需要每次重启后重新安装(除非持久化生效)
|
correctly" from "built, and will fail at boot on someone else's desk".
|
||||||
- 持久化生效后,apt 安装的包会保留
|
|
||||||
|
|
||||||
## 2026-03-17 修复:persistence.dat 未格式化导致启动失败
|
## What changed in the scripts
|
||||||
|
|
||||||
### 问题
|
- `3-create-persistence.ps1` now tries docker-desktop's WSL before giving up
|
||||||
- U 盘上的 `persistence.dat`(20GB)是空文件,没有 ext4 文件系统
|
- It verifies the ext4 magic number after formatting
|
||||||
- `3-create-persistence.ps1` 因无可用标准 WSL,走了 Method B,只创建了稀疏空文件
|
- It only falls back to an unformatted sparse file when there is no WSL at all,
|
||||||
- Ventoy 启动 Ubuntu → 读 ventoy.json → 尝试挂载空的 persistence.dat → mount 失败 → 卡在 initramfs
|
and says so loudly — that path requires the user to run `mkfs.ext4` by hand on
|
||||||
|
first boot, and then reboot
|
||||||
|
|
||||||
### 修复过程
|
The user-facing version of this is in
|
||||||
1. 用 docker-desktop WSL 的 `/sbin/mkfs.ext4` 格式化:
|
[`bootable/README.md`](bootable/README.md) and
|
||||||
- 在 WSL 内创建稀疏文件:`dd if=/dev/zero of=/tmp/persistence.dat bs=1M count=0 seek=20480`
|
[`bootable/TROUBLESHOOTING.md`](bootable/TROUBLESHOOTING.md).
|
||||||
- 格式化:`/sbin/mkfs.ext4 -F -L casper-rw /tmp/persistence.dat`
|
|
||||||
- 复制到 C: → 移动到 E:(因为 docker-desktop WSL 无法直接访问 E: 盘)
|
|
||||||
2. 验证:offset 1080 处读到 `53 EF`(little-endian `0xEF53`),确认 ext4 有效
|
|
||||||
|
|
||||||
### 代码改进
|
|
||||||
- `3-create-persistence.ps1` Method B 现在会尝试 docker-desktop WSL 格式化
|
|
||||||
- 添加了 ext4 magic number 验证步骤
|
|
||||||
- 只有在完全没有任何 WSL 时才降级为未格式化的空文件
|
|
||||||
|
|||||||
1
NODE_VERSION
Normal file
1
NODE_VERSION
Normal file
@@ -0,0 +1 @@
|
|||||||
|
v22.22.3
|
||||||
618
README.md
618
README.md
@@ -1,16 +1,16 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="assets/readme/hero.svg" alt="U-Claw:随身携带的 AI 工作空间" width="100%" />
|
<img src="assets/readme/hero.svg" alt="U-Claw — your AI workspace on a USB drive" width="100%" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h1 align="center">U-Claw(虾盘)</h1>
|
<h1 align="center">U-Claw</h1>
|
||||||
|
|
||||||
<p align="center"><strong>把 AI 工作空间装进 U 盘:插上任意电脑,配置、记忆和工具都跟着走。</strong></p>
|
<p align="center"><strong>An AI workspace on a USB drive. Plug it into any computer — your settings, memory and tools come with it.</strong></p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/dongsheng123132/u-claw/releases">下载便携版</a> ·
|
<a href="https://gitea.fanghe.it.com/zhenghy/u-claw/releases">Download</a> ·
|
||||||
<a href="https://u-claw.org/tutorial.html">完整教程</a> ·
|
<a href="https://u-claw.org/guide.html">Guide</a> ·
|
||||||
<a href="#快速开始制作便携版-u-盘">从源码制作</a> ·
|
<a href="#build-it-yourself">Build from source</a> ·
|
||||||
<a href="#english">English</a>
|
<a href="README.zh-CN.md">中文</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@@ -20,556 +20,140 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> 这不是 U 盘制作工具。这个仓库就是 U 盘的文件骨架:运行一次 `setup.sh` 补齐运行时,再把整个 `portable/` 拷到盘里即可。
|
> This repository is not a tool that builds a USB drive — it **is** the drive's contents. Run `setup.sh` once to fetch the runtime, then copy `portable/` onto the drive.
|
||||||
|
|
||||||
## 三步开始
|
## Why a USB drive
|
||||||
|
|
||||||
1. 从 [Releases](https://github.com/dongsheng123132/u-claw/releases) 下载 Windows 便携版,解压到 U 盘后双击 `Windows-Start.bat`。
|
Three reasons, and they are the whole pitch:
|
||||||
2. 或克隆仓库,在 `portable/` 运行 `bash setup.sh`,下载 Node.js 与 OpenClaw。
|
|
||||||
3. 首次启动在本地配置模型 Key;配置只保存在 U 盘的 `data/.openclaw/openclaw.json`。
|
|
||||||
|
|
||||||
| 你得到什么 | 为什么重要 |
|
1. **Your work computer will not let you install software.** Finance, government contracting and most regulated industries lock this down. A USB drive needs no installer and no admin rights.
|
||||||
| --- | --- |
|
2. **Your setup travels with you.** Model keys, chat history and project memory live in `data/` on the drive, not on whichever machine you borrowed.
|
||||||
| 随身 AI 工作空间 | 换电脑时带走配置、记忆与工具,不在主机残留用户数据。 |
|
3. **Nothing is left behind on the host.** Pull the drive out and walk away. (One caveat, stated plainly: U-Claw writes a rebuildable cache to the host's local disk to keep startup fast on slow drives. There is a one-click clean-up under `advanced/`.)
|
||||||
| 本地优先 | 不绑定设备、不打指纹、不上传本地配置。 |
|
|
||||||
| 多种使用方式 | 便携 U 盘、在线一键安装、可启动 Linux U 盘各自独立。 |
|
|
||||||
|
|
||||||
> [!TIP]
|
## Get started
|
||||||
> ### 👑 同作者新品:U-King · AI 装机管家
|
|
||||||
> 一键装好 **Claude Code / Codex / OpenClaw / Hermes** 四大 AI 神器,驱动自动配好——一个 Key 直用 Claude、GPT、DeepSeek、Kimi 等 24+ 全球模型,免翻墙。绿色版仅几 MB,双击即用,还带 AI 作图 / 视频 / 本地大模型 / 多终端工作台。
|
|
||||||
>
|
|
||||||
> **免费下载:[www.u-king.org](https://www.u-king.org)**(国内镜像 [cloud.u-claw.org/uking](https://cloud.u-claw.org/uking/))
|
|
||||||
>
|
|
||||||
> *U-Claw 让 AI 住进 U 盘;U-King 帮你把全套 AI 编程神器装进电脑。*
|
|
||||||
> *EN: U-King — one-click installer for Claude Code / Codex / OpenClaw, with one API key for 24+ global models → [u-king.org](https://www.u-king.org)*
|
|
||||||
|
|
||||||
---
|
1. Download the portable build from [Releases](https://gitea.fanghe.it.com/zhenghy/u-claw/releases), unzip it onto a USB drive, and double-click `START HERE - Windows.bat` (or `START HERE - Mac.command`).
|
||||||
|
2. Settings opens by itself. Paste an API key — U-Claw identifies the provider and checks the key before letting you continue.
|
||||||
|
3. Start talking.
|
||||||
|
|
||||||
<a id="中文"></a>
|
**No key yet?** [Google Gemini](https://aistudio.google.com/apikey) is the quickest: sign in with a Google account, no card, free tier. [OpenRouter](https://openrouter.ai/keys) is a good second choice if you want one key across many models.
|
||||||
|
|
||||||
## 中文
|
## Models
|
||||||
|
|
||||||
### 这是什么
|
|
||||||
|
|
||||||
U-Claw(虾盘)是一个**制作教程 + 全套源代码**,教你把 [OpenClaw](https://github.com/openclaw/openclaw)(开源 AI 助手框架)做成 U 盘——插上任意电脑,双击就能用 AI。为什么叫虾盘?U-Claw = USB + Claw(虾钳),U 盘 + AI = 虾盘。
|
|
||||||
|
|
||||||
代码库本身就是 U 盘的文件骨架,运行 `setup.sh` 补齐大依赖后,整个 `portable/` 目录直接拷贝到 U 盘即可。
|
|
||||||
|
|
||||||
> 📖 **[完整教程](https://u-claw.org/tutorial.html)** — 从零开始的手工安装指南、模型配置、聊天平台接入,小白也能看懂。
|
|
||||||
|
|
||||||
### 🧩 模型随便选:国内外大模型都能用
|
|
||||||
|
|
||||||
U-Claw 是纯开源工具,**不绑定设备、不打指纹、不上传任何数据**。首次启动会自动打开 Config 页面,选一个模型、填入自己的 API Key 即可一键启动:
|
|
||||||
|
|
||||||
- **虾盘云(首选 · 中转站)**:一个 Key 调用 DeepSeek / Claude / GPT / 通义 等国内外全部大模型,无需翻墙。注册并充值:[u-claw.org/cloud.html](https://u-claw.org/cloud.html)
|
|
||||||
- **各家官方 Key 也行**:DeepSeek、通义千问、Kimi、智谱 GLM、豆包、MiniMax(国内)/ OpenAI、Claude、Groq(国外)/ 硅基流动、任意 OpenAI 兼容地址(自定义)
|
|
||||||
- **配置只存在本地** `data/.openclaw/openclaw.json`,拔盘不留痕
|
|
||||||
|
|
||||||
### 一键安装(推荐)
|
|
||||||
|
|
||||||
不需要 U 盘,一行命令直接装到电脑:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Mac / Linux
|
|
||||||
curl -fsSL https://u-claw.org/install.sh | bash
|
|
||||||
|
|
||||||
# Windows (PowerShell 管理员)
|
|
||||||
irm https://u-claw.org/install.ps1 | iex
|
|
||||||
```
|
|
||||||
|
|
||||||
自动完成: Node.js 下载 → OpenClaw 安装 → 10 个中国技能 → 模型配置 → 启动脚本生成。全程国内镜像,无需翻墙。
|
|
||||||
|
|
||||||
详见 [`install/README.md`](install/README.md)。
|
|
||||||
|
|
||||||
### 快速开始:制作便携版 U 盘
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. 克隆代码
|
|
||||||
git clone https://github.com/dongsheng123132/u-claw.git
|
|
||||||
|
|
||||||
# 2. 补齐大依赖(Node.js + OpenClaw,国内镜像,约 1 分钟)
|
|
||||||
cd u-claw/portable && bash setup.sh
|
|
||||||
|
|
||||||
# 3. 拷贝到 U 盘
|
|
||||||
cp -R portable/ /Volumes/你的U盘/U-Claw/ # Mac
|
|
||||||
# 或 Windows 资源管理器直接拖过去
|
|
||||||
```
|
|
||||||
|
|
||||||
**完成!** 插上 U 盘,双击启动脚本就能用。
|
|
||||||
|
|
||||||
### U 盘功能一览
|
|
||||||
|
|
||||||
| 功能 | Mac | Windows |
|
|
||||||
|------|-----|---------|
|
|
||||||
| **免安装运行** | `Mac-Start.command` | `Windows-Start.bat` |
|
|
||||||
| **功能菜单** | `Mac-Menu.command` | `Windows-Menu.bat` |
|
|
||||||
| **安装到电脑** | `Mac-Install.command` | `Windows-Install.bat` |
|
|
||||||
| **首次配置** | `Config.html` | `Config.html` |
|
|
||||||
|
|
||||||
### U 盘文件结构
|
|
||||||
|
|
||||||
```
|
|
||||||
U-Claw/ ← 整个拷到 U 盘
|
|
||||||
├── Mac-Start.command Mac 免安装运行
|
|
||||||
├── Mac-Menu.command Mac 功能菜单
|
|
||||||
├── Mac-Install.command 安装到 Mac
|
|
||||||
├── Windows-Start.bat Windows 免安装运行
|
|
||||||
├── Windows-Menu.bat Windows 功能菜单
|
|
||||||
├── Windows-Install.bat 安装到 Windows
|
|
||||||
├── Config.html 首次配置页面
|
|
||||||
├── setup.sh 补齐依赖(开发者用)
|
|
||||||
├── app/ ← 大依赖(setup.sh 下载,不进 git)
|
|
||||||
│ ├── core/ OpenClaw + QQ 插件
|
|
||||||
│ └── runtime/
|
|
||||||
│ ├── node-mac-arm64/ Mac Apple Silicon
|
|
||||||
│ ├── node-mac-x64/ Mac Intel
|
|
||||||
│ └── node-win-x64/ Windows 64-bit
|
|
||||||
└── data/ ← 用户数据(不进 git)
|
|
||||||
├── .openclaw/ 配置文件
|
|
||||||
├── memory/ AI 记忆
|
|
||||||
└── backups/ 备份
|
|
||||||
```
|
|
||||||
|
|
||||||
### Linux 可启动版
|
|
||||||
|
|
||||||
连操作系统都没有?没关系。可启动版可以让任意电脑从 U 盘直接启动 Ubuntu + AI:
|
|
||||||
|
|
||||||
- 本仓库内:[`bootable/`](bootable/) 目录(与其他模块完全独立,互不影响)
|
|
||||||
- 独立仓库:[u-claw-linux](https://github.com/dongsheng123132/u-claw-linux)(内容一致,方便单独克隆)
|
|
||||||
|
|
||||||
基于 Ventoy + Ubuntu 24.04 LTS + 持久化存储,在 Windows 上运行 4 步 PowerShell 脚本即可制作。详见 [`bootable/README.md`](bootable/README.md)。
|
|
||||||
|
|
||||||
> **关于桌面安装版(Electron)**:`u-claw-app/` 的 Electron 桌面版(`.exe` 安装版 / 绿色版 / `.dmg`)**已于 2026-06-19 停止发布并归档**,原因见 [`u-claw-app/DEPRECATED.md`](u-claw-app/DEPRECATED.md)。U-Claw 现在只发布「便携 U 盘版」——这才是产品的本质:插上 U 盘、解压即用。
|
|
||||||
|
|
||||||
### 直接下载发行版
|
|
||||||
|
|
||||||
[GitHub Releases](https://github.com/dongsheng123132/u-claw/releases) 提供便携 U 盘版:
|
|
||||||
|
|
||||||
- `u-claw-portable-windows-vX.Y.Z.zip` — Windows 便携完整版(已预装 Node + OpenClaw,**解压到 U 盘双击 `Windows-Start.bat` 即用**)
|
|
||||||
- **Mac**:源码在 [`portable/`](portable/) 目录,`bash setup.sh` 自动下载 Node + OpenClaw(国内镜像约 1 分钟),双击 `Mac-Start.command` 启动
|
|
||||||
|
|
||||||
> ⚠️ **U 盘请用 NTFS 格式**(不要 exFAT/FAT32):Node.js 在 exFAT 上 IO 极慢且不支持符号链接,可能导致启动失败。
|
|
||||||
|
|
||||||
### 支持的 AI 模型
|
|
||||||
|
|
||||||
**国产模型(无需翻墙):**
|
|
||||||
|
|
||||||
| 模型 | 推荐场景 |
|
|
||||||
|------|----------|
|
|
||||||
| DeepSeek | 编程首选,极便宜 |
|
|
||||||
| Kimi K2.5 | 长文档,256K 上下文 |
|
|
||||||
| 通义千问 Qwen | 免费额度大 |
|
|
||||||
| 智谱 GLM | 学术场景 |
|
|
||||||
| MiniMax | 语音多模态 |
|
|
||||||
| 豆包 Doubao | 火山引擎 |
|
|
||||||
|
|
||||||
**国际模型:** Claude · GPT · Gemini(需翻墙或中转)
|
|
||||||
|
|
||||||
### 支持的聊天平台
|
|
||||||
|
|
||||||
| 平台 | 状态 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| QQ | ✅ 已预装 | 输入 AppID + Secret 即可 |
|
|
||||||
| 飞书 | ✅ 内置 | 企业首选 |
|
|
||||||
| Telegram | ✅ 内置 | 海外推荐 |
|
|
||||||
| WhatsApp | ✅ 内置 | Baileys 协议 |
|
|
||||||
| Discord | ✅ 内置 | — |
|
|
||||||
| 微信 | ✅ 社区插件 | iPad 协议 |
|
|
||||||
|
|
||||||
### 国内镜像
|
|
||||||
|
|
||||||
所有脚本默认走国内镜像,无需翻墙:
|
|
||||||
|
|
||||||
| 资源 | 镜像 |
|
|
||||||
|------|------|
|
|
||||||
| npm 包 | `registry.npmmirror.com` |
|
|
||||||
| Node.js | `npmmirror.com/mirrors/node` |
|
|
||||||
| Electron | `npmmirror.com/mirrors/electron` |
|
|
||||||
|
|
||||||
### 开发 & 贡献
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/dongsheng123132/u-claw.git
|
|
||||||
cd u-claw/portable && bash setup.sh
|
|
||||||
bash Mac-Start.command # Mac 测试
|
|
||||||
```
|
|
||||||
|
|
||||||
**平台支持:**
|
|
||||||
|
|
||||||
| 平台 | 状态 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| Mac Apple Silicon (M1-M4) | ✅ | 便携版 |
|
|
||||||
| Mac Intel (x64) | ✅ | 便携版 |
|
|
||||||
| Windows x64 | 🚧 开发中 | 便携版 |
|
|
||||||
| Linux x64(可启动 U 盘) | ✅ | [`bootable/`](bootable/) 目录 |
|
|
||||||
|
|
||||||
欢迎 PR!特别需要:Windows 脚本完善、教程翻译。
|
|
||||||
|
|
||||||
### 🦞 寻找技术伙伴
|
|
||||||
|
|
||||||
U-Claw 是一个快速成长的开源项目,目前已有不少商业合作机会。
|
|
||||||
|
|
||||||
我们正在寻找:
|
|
||||||
- **技术伙伴** — 全栈 / Node.js / Electron / 脚本自动化
|
|
||||||
- **资源合作** — 渠道、内容、社区运营
|
|
||||||
|
|
||||||
如果你对 AI 工具的落地和商业化感兴趣,欢迎联系:
|
|
||||||
|
|
||||||
- 微信: hecare888
|
|
||||||
- Telegram: [@dsds8848](https://t.me/dsds8848)
|
|
||||||
- Twitter/X: [@Bitplus888](https://x.com/Bitplus888)
|
|
||||||
- Email: [hefangsheng@gmail.com](mailto:hefangsheng@gmail.com)
|
|
||||||
- GitHub: [@dongsheng123132](https://github.com/dongsheng123132)
|
|
||||||
- 官网: [u-claw.org](https://u-claw.org)
|
|
||||||
|
|
||||||
### FAQ
|
|
||||||
|
|
||||||
**Q: 需要翻墙吗?**
|
|
||||||
不需要。安装和运行全程使用国内镜像,国产模型 API 直连。
|
|
||||||
|
|
||||||
**Q: U 盘需要多大?**
|
|
||||||
4GB+(完整约 2.3GB)。
|
|
||||||
|
|
||||||
**Q: 能分发吗?**
|
|
||||||
MIT 协议,随便复制分发。
|
|
||||||
|
|
||||||
**Q: Mac 提示"未验证的开发者"?**
|
|
||||||
右键脚本 → 打开。
|
|
||||||
|
|
||||||
**Q: setup.bat / setup.sh 执行失败,提示模块找不到?**
|
|
||||||
通常是 npm install 过程中网络中断导致 `node_modules` 不完整。解决步骤:
|
|
||||||
1. 删除不完整的依赖:`rmdir /s /q portable\app\core\node_modules`(Windows)或 `rm -rf portable/app/core/node_modules`(Mac)
|
|
||||||
2. 切换淘宝镜像重新安装:`cd portable/app/core && npm install --registry=https://registry.npmmirror.com`
|
|
||||||
|
|
||||||
**Q: 系统已有 Node.js v24,安装失败?**
|
|
||||||
Node.js v24 是最新开发版,部分依赖尚不兼容。需要 **v20 或 v22 LTS**。删除已下载的 runtime 目录后重新运行 setup,它会自动下载内置的 Node v22:
|
|
||||||
```bash
|
|
||||||
# Windows
|
|
||||||
rmdir /s /q portable\app\runtime\node-win-x64
|
|
||||||
setup.bat
|
|
||||||
|
|
||||||
# Mac
|
|
||||||
rm -rf portable/app/runtime/node-mac-arm64
|
|
||||||
bash setup.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
**Q: Mac 上提示 `.toSorted is not a function`?**
|
|
||||||
系统旧版 Node.js 被检测到并跳过了内置版本下载,但旧版 Node 不支持 `.toSorted()`(需要 v20+)。删除 runtime 目录让脚本重新下载内置 Node v22:
|
|
||||||
```bash
|
|
||||||
rm -rf portable/app/runtime/node-mac-arm64
|
|
||||||
bash setup.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
**Q: 如何同时配置多个 AI 模型并切换?**
|
|
||||||
支持同时配置多个 provider!打开 `Config.html` → 在 Providers 区域点击「添加」,逐个填入各模型的 API Key 和地址(如 DeepSeek、Kimi、通义等)→ 保存后,在聊天界面左上角下拉菜单随时切换。配置持久保存在 U 盘上。
|
|
||||||
|
|
||||||
**Q: U 盘安装后无法创建文件 / 写入失败?**
|
|
||||||
两种可能:① U 盘侧面有物理写保护开关,拨到解锁位置;② U 盘格式不兼容,建议格式化为 **exFAT**(Mac/Windows/Linux 三端均支持读写)。
|
|
||||||
|
|
||||||
**Q: 从 Ubuntu 向 U 盘复制时符号链接丢失?**
|
|
||||||
`node_modules/.bin/` 下有大量符号链接,FAT32/exFAT 在直接 `cp -R` 时会跳过。用 `rsync -aL` 可将符号链接展开为真实文件:
|
|
||||||
```bash
|
|
||||||
rsync -aL --progress portable/ /media/YOUR_USB/U-Claw/
|
|
||||||
```
|
|
||||||
|
|
||||||
**Q: QQbot 报错 `Unknown channel: qqbot`?**
|
|
||||||
Bundle 里的 `@sliverp/qqbot` 是未编译的 TypeScript 源码,需要先编译:
|
|
||||||
```bash
|
|
||||||
cd portable/app/core/node_modules/@sliverp/qqbot
|
|
||||||
npm install && npm run build
|
|
||||||
```
|
|
||||||
正式 Release 包已修复此问题,建议从 [Releases](https://github.com/dongsheng123132/u-claw/releases) 下载最新版。
|
|
||||||
|
|
||||||
### 联系 & 合作
|
|
||||||
|
|
||||||
<img src="assets/wechat-qr.jpg" width="220" alt="微信二维码 — 贺去病 ai 工作室" align="right" />
|
|
||||||
|
|
||||||
- 微信: hecare888(或扫右侧二维码)
|
|
||||||
- Telegram: [@dsds8848](https://t.me/dsds8848)
|
|
||||||
- Twitter/X: [@Bitplus888](https://x.com/Bitplus888)
|
|
||||||
- Email: [hefangsheng@gmail.com](mailto:hefangsheng@gmail.com)
|
|
||||||
- GitHub: [@dongsheng123132](https://github.com/dongsheng123132)
|
|
||||||
- 官网: [u-claw.org](https://u-claw.org)
|
|
||||||
|
|
||||||
**🤝 招募代理 / 带货合作**
|
|
||||||
|
|
||||||
虾盘 3.0 体验极佳,退货率极低,售后由我们负责——你只管卖货:
|
|
||||||
|
|
||||||
- **抖店 / 直播带货**:提供最高佣金比例,产品已在多个直播间验证转化
|
|
||||||
- **代理分销**:买断或按单分润均可谈,支持定制版本
|
|
||||||
- **技术合作**:有开发能力者欢迎深度合作
|
|
||||||
- **👑 U-King 装机管家分销(新)**:[U-King](https://www.u-king.org) 软件本身免费送客户,客户按用量给 AI 模型 API 充值,你拿**持续分润**——不压货、零售后(我们负责),赚的是长期流水。适合装机店、AI 培训、社群主理人
|
|
||||||
|
|
||||||
有意向请微信联系(备注「代理合作」优先处理)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<a id="english"></a>
|
|
||||||
|
|
||||||
## English
|
|
||||||
|
|
||||||
### What is this
|
|
||||||
|
|
||||||
U-Claw (aka "虾盘" / "Xia Pan" in Chinese, meaning "Claw Drive") is a **tutorial + complete source code** for building an [OpenClaw](https://github.com/openclaw/openclaw) (open-source AI assistant framework) USB drive — plug it into any computer, double-click, and start using AI.
|
|
||||||
|
|
||||||
The codebase itself is the USB file skeleton. Run `setup.sh` to download large dependencies, then copy the entire `portable/` directory to a USB drive.
|
|
||||||
|
|
||||||
> 📖 **[Full Tutorial](https://u-claw.org/tutorial.html)** — Step-by-step manual installation, model setup, chat platform integration.
|
|
||||||
|
|
||||||
### One-Line Install (Recommended)
|
|
||||||
|
|
||||||
No USB needed — install directly to your computer:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Mac / Linux
|
|
||||||
curl -fsSL https://u-claw.org/install.sh | bash
|
|
||||||
|
|
||||||
# Windows (PowerShell as Admin)
|
|
||||||
irm https://u-claw.org/install.ps1 | iex
|
|
||||||
```
|
|
||||||
|
|
||||||
Automatically downloads Node.js, installs OpenClaw, configures 10 Chinese-optimized skills, and sets up your AI model. All downloads use China mirrors.
|
|
||||||
|
|
||||||
See [`install/README.md`](install/README.md) for details.
|
|
||||||
|
|
||||||
### Quick Start: Build a Portable USB
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Clone
|
|
||||||
git clone https://github.com/dongsheng123132/u-claw.git
|
|
||||||
|
|
||||||
# 2. Download dependencies (Node.js + OpenClaw, ~1 min)
|
|
||||||
cd u-claw/portable && bash setup.sh
|
|
||||||
|
|
||||||
# 3. Copy to USB drive
|
|
||||||
cp -R portable/ /Volumes/YOUR_USB/U-Claw/ # Mac
|
|
||||||
# Or drag & drop on Windows
|
|
||||||
```
|
|
||||||
|
|
||||||
**Done!** Plug in the USB, double-click the start script, and you're running AI.
|
|
||||||
|
|
||||||
### USB Features
|
|
||||||
|
|
||||||
| Feature | Mac | Windows |
|
|
||||||
|---------|-----|---------|
|
|
||||||
| **Run (no install)** | `Mac-Start.command` | `Windows-Start.bat` |
|
|
||||||
| **Menu** | `Mac-Menu.command` | `Windows-Menu.bat` |
|
|
||||||
| **Install to PC** | `Mac-Install.command` | `Windows-Install.bat` |
|
|
||||||
| **First-time config** | `Config.html` | `Config.html` |
|
|
||||||
|
|
||||||
### File Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
U-Claw/ ← Copy entire folder to USB
|
|
||||||
├── Mac-Start.command Mac launcher
|
|
||||||
├── Mac-Menu.command Mac menu
|
|
||||||
├── Mac-Install.command Install to Mac
|
|
||||||
├── Windows-Start.bat Windows launcher
|
|
||||||
├── Windows-Menu.bat Windows menu
|
|
||||||
├── Windows-Install.bat Install to Windows
|
|
||||||
├── Config.html First-time config page
|
|
||||||
├── setup.sh Download dependencies (dev use)
|
|
||||||
├── app/ ← Large deps (downloaded by setup.sh, not in git)
|
|
||||||
│ ├── core/ OpenClaw + QQ plugin
|
|
||||||
│ └── runtime/
|
|
||||||
│ ├── node-mac-arm64/ Mac Apple Silicon
|
|
||||||
│ ├── node-mac-x64/ Mac Intel
|
|
||||||
│ └── node-win-x64/ Windows 64-bit
|
|
||||||
└── data/ ← User data (not in git)
|
|
||||||
├── .openclaw/ Config file
|
|
||||||
├── memory/ AI memory
|
|
||||||
└── backups/ Backups
|
|
||||||
```
|
|
||||||
|
|
||||||
### Linux Bootable USB
|
|
||||||
|
|
||||||
No operating system? No problem. Boot any computer from USB into Ubuntu + AI:
|
|
||||||
|
|
||||||
- In this repo: [`bootable/`](bootable/) directory (fully independent from other modules)
|
|
||||||
- Standalone repo: [u-claw-linux](https://github.com/dongsheng123132/u-claw-linux) (same content, easier to clone separately)
|
|
||||||
|
|
||||||
Based on Ventoy + Ubuntu 24.04 LTS + persistence. 4-step PowerShell scripts on Windows. See [`bootable/README.md`](bootable/README.md) for details.
|
|
||||||
|
|
||||||
> **About the Electron desktop app**: `u-claw-app/` (the `.exe` installer / portable / `.dmg` builds) was **deprecated and is no longer published as of 2026-06-19** — see [`u-claw-app/DEPRECATED.md`](u-claw-app/DEPRECATED.md). U-Claw now ships only the portable USB build, which is the product's essence: plug in the USB and run.
|
|
||||||
|
|
||||||
### Supported AI Models
|
|
||||||
|
|
||||||
**Chinese models (no VPN needed):**
|
|
||||||
|
|
||||||
| Model | Best for |
|
| Model | Best for |
|
||||||
|-------|----------|
|
|---|---|
|
||||||
| DeepSeek | Coding, extremely cheap |
|
| **Claude** | Best all-rounder |
|
||||||
| Kimi K2.5 | Long documents, 256K context |
|
| **GPT** | Most widely used |
|
||||||
| Qwen | Large free tier |
|
| **Gemini** | Free tier, easiest signup |
|
||||||
| GLM (Zhipu) | Academic use |
|
| **SEA-LION** | Malay, Tamil and Singlish — built by AI Singapore |
|
||||||
| MiniMax | Voice & multimodal |
|
| **Local model** | Offline. Nothing leaves the machine. |
|
||||||
| Doubao | Volcengine ecosystem |
|
|
||||||
|
|
||||||
**International models:** Claude · GPT · Gemini (VPN or relay required in China)
|
Others, including DeepSeek, Kimi, Qwen and GLM, are available under *Advanced* in Settings.
|
||||||
|
|
||||||
### Supported Chat Platforms
|
> **A note on SEA-LION**: the hosted API is free but currently rate-limited to 10 requests per minute per user, and there is no published paid tier. That is fine for chatting and tight for agent workloads, so it is offered as a specialist choice rather than a default.
|
||||||
|
|
||||||
|
## Chat platforms
|
||||||
|
|
||||||
| Platform | Status | Notes |
|
| Platform | Status | Notes |
|
||||||
|----------|--------|-------|
|
|---|---|---|
|
||||||
| QQ | ✅ Pre-installed | Enter AppID + Secret |
|
| Telegram | ✅ Built-in | — |
|
||||||
| Feishu (Lark) | ✅ Built-in | Enterprise favorite |
|
| WhatsApp | ⚠️ Built-in | Uses the Baileys protocol, which is unofficial. There is a real risk of your account being banned — see the warning in Settings before connecting a number you care about. |
|
||||||
| Telegram | ✅ Built-in | International |
|
|
||||||
| WhatsApp | ✅ Built-in | Baileys protocol |
|
|
||||||
| Discord | ✅ Built-in | — |
|
| Discord | ✅ Built-in | — |
|
||||||
| WeChat | ✅ Community plugin | iPad protocol |
|
| Slack | ✅ Built-in | — |
|
||||||
|
| WeChat · QQ · Feishu · DingTalk | Available | Mainly useful for users in China — see [README.zh-CN.md](README.zh-CN.md) |
|
||||||
|
|
||||||
### China Mirrors
|
## Build it yourself
|
||||||
|
|
||||||
All scripts use China mirrors by default — no VPN needed:
|
|
||||||
|
|
||||||
| Resource | Mirror |
|
|
||||||
|----------|--------|
|
|
||||||
| npm packages | `registry.npmmirror.com` |
|
|
||||||
| Node.js | `npmmirror.com/mirrors/node` |
|
|
||||||
| Electron | `npmmirror.com/mirrors/electron` |
|
|
||||||
|
|
||||||
### Development & Contributing
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/dongsheng123132/u-claw.git
|
git clone https://gitea.fanghe.it.com/zhenghy/u-claw.git
|
||||||
cd u-claw/portable && bash setup.sh
|
cd u-claw/portable/advanced && bash setup.sh # fetches Node.js + OpenClaw, about a minute
|
||||||
bash Mac-Start.command # Test on Mac
|
|
||||||
|
cp -R portable/ /Volumes/YOUR_USB/U-Claw/ # Mac
|
||||||
|
# On Windows, drag the folder onto the drive
|
||||||
```
|
```
|
||||||
|
|
||||||
**Platform Support:**
|
## What is on the drive
|
||||||
|
|
||||||
| Platform | Status | Notes |
|
| | Mac | Windows |
|
||||||
|----------|--------|-------|
|
|---|---|---|
|
||||||
| Mac Apple Silicon (M1-M4) | ✅ | Portable + Desktop |
|
| **Run it** | `START HERE - Mac.command` | `START HERE - Windows.bat` |
|
||||||
| Mac Intel (x64) | ✅ | Portable + Desktop |
|
| **All the other tools** | `Mac-Menu.command` | `Windows-Menu.bat` |
|
||||||
| Windows x64 | 🚧 In progress | Portable + Desktop |
|
| **Copy onto this computer** | `Mac-Install.command` | `Windows-Install.bat` |
|
||||||
| Linux x64 (Bootable USB) | ✅ | [`bootable/`](bootable/) directory |
|
| **Settings** | opens on first run | opens on first run |
|
||||||
|
|
||||||
PRs welcome! Especially: Windows scripts, documentation.
|
```
|
||||||
|
U-Claw/
|
||||||
|
├── START HERE - Windows.bat ← double-click this
|
||||||
|
├── START HERE - Mac.command ← or this
|
||||||
|
├── Read me first.html Three-step walkthrough
|
||||||
|
├── advanced/ Diagnostics, CLI, install-to-PC, build scripts
|
||||||
|
├── skills/ Skill content + manifest.json
|
||||||
|
├── lib/ Launcher, i18n, helpers
|
||||||
|
├── app/ Runtime (fetched by setup.sh, not in git)
|
||||||
|
│ ├── core/ OpenClaw
|
||||||
|
│ └── runtime/node-{mac-arm64,mac-x64,win-x64}/
|
||||||
|
└── data/ Your data (not in git)
|
||||||
|
├── .openclaw/ Config
|
||||||
|
├── memory/ What the AI remembers
|
||||||
|
└── backups/
|
||||||
|
```
|
||||||
|
|
||||||
### 🔧 Professional Services / 专业服务
|
## Linux bootable USB
|
||||||
|
|
||||||
Need help? We offer remote support and custom development:
|
No operating system on the machine at all? Boot it from the drive into Ubuntu with U-Claw already installed. See [`bootable/`](bootable/), built on Ventoy + Ubuntu 24.04 LTS with persistence.
|
||||||
|
|
||||||
| Service | Description | Price |
|
> The Electron desktop app (`u-claw-app/`) was **deprecated on 2026-06-19** and is no longer published — see [`u-claw-app/DEPRECATED.md`](u-claw-app/DEPRECATED.md). The portable USB build is the product.
|
||||||
|---------|-------------|-------|
|
|
||||||
| **Remote Installation** | We remotely install OpenClaw + skills + model config for you | Free |
|
|
||||||
| **Troubleshooting** | Startup failures, port conflicts, network issues | From ¥50 |
|
|
||||||
| **Model Tuning** | API key setup, model switching, prompt optimization | From ¥50 |
|
|
||||||
| **Custom Development** | Custom skills, enterprise private deployment, QQ/WeChat/Feishu bot integration | From ¥200 |
|
|
||||||
| **USB Green Edition** | Pre-built portable USB with your custom skills & models | From ¥100 |
|
|
||||||
|
|
||||||
**One-click remote support** — run one command, we connect and fix it:
|
## Is this safe?
|
||||||
|
|
||||||
|
- **Your API key is written to the drive only.** It is never sent anywhere except to the model provider you chose.
|
||||||
|
- **No telemetry.** No device fingerprinting, no usage reporting, no account required. This build has none of that code in it.
|
||||||
|
- **Your conversations stay in `data/` on the drive.**
|
||||||
|
- **What does leave the machine**: your messages go to whichever model provider you configured, exactly as they would in that provider's own app. If that is not acceptable for your data, use the local model option instead.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
See [CONTRIBUTING.md](CONTRIBUTING.md). Tests run on every push:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Mac / Linux
|
node --test tests/
|
||||||
curl -fsSL https://u-claw.org/remote.sh | bash
|
|
||||||
|
|
||||||
# Windows (Admin PowerShell)
|
|
||||||
irm https://u-claw.org/remote.ps1 | iex
|
|
||||||
```
|
```
|
||||||
|
|
||||||
WeChat: **hecare888** (备注「U-Claw 远程」优先处理)
|
They check the things that break quietly: launchers staying thin, `.bat` files staying pure ASCII, no China-routed download sources creeping back, both installers agreeing on the skill list, and every UI string existing in every language.
|
||||||
|
|
||||||
👉 [View full service details / 查看完整服务详情](https://u-claw.org/guide.html#remote-support)
|
## FAQ
|
||||||
|
|
||||||
### 🦞 Looking for Partners
|
**Do I need to install anything?**
|
||||||
|
No. That is the point.
|
||||||
|
|
||||||
U-Claw is a fast-growing open-source project with real commercial opportunities.
|
**Does it work on both Mac and Windows?**
|
||||||
|
Yes, from the same drive. Windows builds ship with the runtime included; on Mac the first launch fetches it (about a minute).
|
||||||
|
|
||||||
We're looking for:
|
**Where do my settings live?**
|
||||||
- **Technical partners** — Full-stack / Node.js / Electron / scripting
|
`data/.openclaw/openclaw.json` on the drive. Copy the drive, copy your setup.
|
||||||
- **Resource partners** — Distribution, content, community
|
|
||||||
|
|
||||||
If you're interested in AI tooling and commercialization, let's talk:
|
**Can I use it without an internet connection?**
|
||||||
|
Yes, with a local model. Everything else needs to reach your model provider.
|
||||||
|
|
||||||
- Telegram: [@dsds8848](https://t.me/dsds8848)
|
**Which file system should the drive use?**
|
||||||
- Twitter/X: [@Bitplus888](https://x.com/Bitplus888)
|
exFAT. macOS can only read NTFS, not write to it, which would break the whole "your settings travel with you" idea.
|
||||||
- Email: [hefangsheng@gmail.com](mailto:hefangsheng@gmail.com)
|
|
||||||
- GitHub: [@dongsheng123132](https://github.com/dongsheng123132)
|
|
||||||
- WeChat: hecare888
|
|
||||||
- Website: [u-claw.org](https://u-claw.org)
|
|
||||||
|
|
||||||
### FAQ
|
## About this fork
|
||||||
|
|
||||||
**Q: Do I need a VPN?**
|
This is a fork of [dongsheng123132/u-claw](https://github.com/dongsheng123132/u-claw),
|
||||||
No. All downloads use China mirrors. Chinese AI model APIs work directly.
|
reworked for Singapore and Southeast Asia: international defaults, a bilingual
|
||||||
|
interface, and a much lower barrier for non-technical users.
|
||||||
|
|
||||||
**Q: How big should the USB drive be?**
|
**We do not send changes back upstream** — what this build removes is exactly
|
||||||
4GB+ (~2.3GB full).
|
what the upstream project exists for.
|
||||||
|
|
||||||
**Q: Can I redistribute?**
|
## Contact
|
||||||
MIT license — copy and share freely.
|
|
||||||
|
|
||||||
**Q: Mac says "unverified developer"?**
|
- Issues — [gitea.fanghe.it.com/zhenghy/u-claw/issues](https://gitea.fanghe.it.com/zhenghy/u-claw/issues)
|
||||||
Right-click the script → Open.
|
- Site — [u-claw.org](https://u-claw.org)
|
||||||
|
|
||||||
**Q: setup.bat / setup.sh fails with "module not found"?**
|
|
||||||
Usually caused by a network interruption during `npm install`, leaving `node_modules` incomplete. Fix:
|
|
||||||
1. Delete incomplete dependencies: `rmdir /s /q portable\app\core\node_modules` (Windows) or `rm -rf portable/app/core/node_modules` (Mac)
|
|
||||||
2. Reinstall using China mirror: `cd portable/app/core && npm install --registry=https://registry.npmmirror.com`
|
|
||||||
|
|
||||||
**Q: Already have Node.js v24 and installation fails?**
|
|
||||||
Node.js v24 is a dev release — some dependencies aren't compatible yet. You need **v20 or v22 LTS**. Delete the runtime folder to force a fresh download of the bundled Node v22:
|
|
||||||
```bash
|
|
||||||
# Windows
|
|
||||||
rmdir /s /q portable\app\runtime\node-win-x64
|
|
||||||
setup.bat
|
|
||||||
|
|
||||||
# Mac
|
|
||||||
rm -rf portable/app/runtime/node-mac-arm64
|
|
||||||
bash setup.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
**Q: Mac shows `.toSorted is not a function`?**
|
|
||||||
Your system Node.js was detected and the bundled version was skipped, but the system version is too old (needs v20+). Delete the runtime folder to re-download the bundled Node v22:
|
|
||||||
```bash
|
|
||||||
rm -rf portable/app/runtime/node-mac-arm64
|
|
||||||
bash setup.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
**Q: How do I use multiple AI models / providers?**
|
|
||||||
Multiple providers are supported! Open `Config.html` → click "Add" in the Providers section → enter API Key and endpoint for each model (DeepSeek, Kimi, Qwen, etc.) → save. Switch between models via the dropdown in the chat interface. Config is saved persistently on the USB drive.
|
|
||||||
|
|
||||||
**Q: USB drive shows "cannot create file" / write errors?**
|
|
||||||
Two possibilities: ① The USB drive has a physical write-protect switch on the side — slide it to unlock; ② Format incompatibility — format the drive as **exFAT** (supported on Mac/Windows/Linux).
|
|
||||||
|
|
||||||
**Q: Symlinks missing when copying from Ubuntu to USB?**
|
|
||||||
`node_modules/.bin/` contains many symlinks that get skipped during direct `cp -R`. Use `rsync -aL` to expand symlinks into real files:
|
|
||||||
```bash
|
|
||||||
rsync -aL --progress portable/ /media/YOUR_USB/U-Claw/
|
|
||||||
```
|
|
||||||
|
|
||||||
**Q: QQbot error: `Unknown channel: qqbot`?**
|
|
||||||
The bundled `@sliverp/qqbot` is uncompiled TypeScript source. Compile it manually:
|
|
||||||
```bash
|
|
||||||
cd portable/app/core/node_modules/@sliverp/qqbot
|
|
||||||
npm install && npm run build
|
|
||||||
```
|
|
||||||
This is fixed in the latest [Release](https://github.com/dongsheng123132/u-claw/releases) — downloading the pre-built release is recommended.
|
|
||||||
|
|
||||||
### Contact & Partnership
|
|
||||||
|
|
||||||
<img src="assets/wechat-qr.jpg" width="220" alt="WeChat QR — He Qubing AI Studio" align="right" />
|
|
||||||
|
|
||||||
- WeChat: hecare888 (or scan QR on the right)
|
|
||||||
- Telegram: [@dsds8848](https://t.me/dsds8848)
|
|
||||||
- Twitter/X: [@Bitplus888](https://x.com/Bitplus888)
|
|
||||||
- Email: [hefangsheng@gmail.com](mailto:hefangsheng@gmail.com)
|
|
||||||
- GitHub: [@dongsheng123132](https://github.com/dongsheng123132)
|
|
||||||
- Website: [u-claw.org](https://u-claw.org)
|
|
||||||
|
|
||||||
**🤝 Reseller / Affiliate Program**
|
|
||||||
|
|
||||||
U-Claw 3.0 delivers excellent user experience with very low return rates. We handle all after-sales support — you focus on selling:
|
|
||||||
|
|
||||||
- **Live commerce / TikTok shop**: Top commission rates, proven conversion in live streams
|
|
||||||
- **Reseller / distribution**: Revenue share or wholesale, custom branded versions available
|
|
||||||
- **Technical partnership**: Deep collaboration welcome for developers
|
|
||||||
- **👑 U-King distribution (new)**: give away [U-King](https://www.u-king.org) (free AI setup manager) — earn **recurring revenue share** on your customers' API top-ups. No inventory, we handle support
|
|
||||||
|
|
||||||
Interested? WeChat hecare888 (mention "partnership" for priority response).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Made with 🦞 by [贺去病 ai 工作室](https://github.com/dongsheng123132)**
|
<p align="center"><strong>English</strong> · <a href="README.zh-CN.md">中文</a></p>
|
||||||
|
|
||||||
|
<p align="center">MIT License</p>
|
||||||
|
|||||||
165
README.zh-CN.md
Normal file
165
README.zh-CN.md
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
<p align="center">
|
||||||
|
<img src="assets/readme/hero.svg" alt="U-Claw —— 装在 U 盘里的 AI 工作空间" width="100%" />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h1 align="center">U-Claw</h1>
|
||||||
|
|
||||||
|
<p align="center"><strong>装在 U 盘里的 AI 工作空间。插到任何一台电脑上,设置、记忆和工具都跟着走。</strong></p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://gitea.fanghe.it.com/zhenghy/u-claw/releases">下载</a> ·
|
||||||
|
<a href="#三步开始">三步开始</a> ·
|
||||||
|
<a href="#自己构建">从源码构建</a> ·
|
||||||
|
<a href="README.md">English</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-1f7a8c?style=flat-square" alt="MIT License" /></a>
|
||||||
|
<img src="https://img.shields.io/badge/Portable-USB%20first-123343?style=flat-square" alt="USB first" />
|
||||||
|
<img src="https://img.shields.io/badge/Runtime-OpenClaw-48d1c1?style=flat-square" alt="OpenClaw runtime" />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> 这个仓库不是"做 U 盘的工具",**它本身就是 U 盘里的内容**。跑一次 `setup.sh` 把运行时补齐,然后把 `portable/` 整个拷到盘里就行。
|
||||||
|
|
||||||
|
## 为什么是 U 盘
|
||||||
|
|
||||||
|
三个理由,也就是这个产品的全部卖点:
|
||||||
|
|
||||||
|
1. **公司电脑经常不让装软件。** 金融、政府外包和多数受监管行业都锁得很死。U 盘不需要安装程序,也不需要管理员权限。
|
||||||
|
2. **你的配置跟着你走。** 模型密钥、聊天记录、项目记忆都在盘上的 `data/` 里,不在你临时借用的那台机器上。
|
||||||
|
3. **主机上不留东西。** 拔盘走人。(有一条要说清楚:为了让慢速 U 盘启动不至于难以忍受,U-Claw 会往主机本地磁盘写一份可重建的缓存。`advanced/` 里有一键清理。)
|
||||||
|
|
||||||
|
## 三步开始
|
||||||
|
|
||||||
|
1. 从 [Releases](https://gitea.fanghe.it.com/zhenghy/u-claw/releases) 下载便携版,解压到 U 盘,双击 `START HERE - Windows.bat`(或 `START HERE - Mac.command`)
|
||||||
|
2. 设置页会自动打开。**粘贴一个 API 密钥就行** —— U-Claw 会自己认出是哪家的,并在接受之前先测一下通不通
|
||||||
|
3. 开始对话
|
||||||
|
|
||||||
|
**还没有密钥?** [Google Gemini](https://aistudio.google.com/apikey) 最容易拿:用 Google 账号登录,不用绑卡,有免费额度。想一个密钥用多种模型的话,[OpenRouter](https://openrouter.ai/keys) 是不错的第二选择。
|
||||||
|
|
||||||
|
## 用哪个模型
|
||||||
|
|
||||||
|
| 模型 | 适合 |
|
||||||
|
|---|---|
|
||||||
|
| **Claude** | 综合最强 |
|
||||||
|
| **GPT** | 用的人最多 |
|
||||||
|
| **Gemini** | 有免费额度,注册最简单 |
|
||||||
|
| **SEA-LION** | 马来语、泰米尔语、Singlish —— AI Singapore 做的 |
|
||||||
|
| **本地模型** | 离线。数据不出这台机器。 |
|
||||||
|
|
||||||
|
其他模型(DeepSeek、Kimi、通义、GLM 等)在设置页的「高级」里填自己的接口地址即可。
|
||||||
|
|
||||||
|
> **关于 SEA-LION**:托管 API 免费,但目前限流 **每人每分钟 10 次请求**,且官方没公布付费档。聊天够用,agent 类工作量偏紧 —— 所以它是特色选项而不是默认。
|
||||||
|
|
||||||
|
## 聊天软件
|
||||||
|
|
||||||
|
| 平台 | 状态 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| Telegram | ✅ 内置 | — |
|
||||||
|
| WhatsApp | ⚠️ 内置 | 用的是 Baileys 非官方协议,**账号有被封的真实风险**。接入之前先看设置页里的提示,不要接你离不开的号码 |
|
||||||
|
| Discord | ✅ 内置 | — |
|
||||||
|
| Slack | ✅ 内置 | — |
|
||||||
|
| 微信 · QQ · 飞书 · 钉钉 | 可选 | 主要给在中国的用户,在设置页的折叠区里 |
|
||||||
|
|
||||||
|
## 自己构建
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone ssh://git@gitea.fanghe.it.com:222/zhenghy/u-claw.git
|
||||||
|
cd u-claw/portable/advanced && bash setup.sh # 下载 Node.js + OpenClaw,约一分钟
|
||||||
|
|
||||||
|
cp -R portable/ /Volumes/你的U盘/U-Claw/ # Mac
|
||||||
|
# Windows 上直接把文件夹拖到盘里
|
||||||
|
```
|
||||||
|
|
||||||
|
## 盘上有什么
|
||||||
|
|
||||||
|
U 盘里**只有三个能点的文件**,其余都在 `advanced/` 里。
|
||||||
|
|
||||||
|
| | Mac | Windows |
|
||||||
|
|---|---|---|
|
||||||
|
| **启动** | `START HERE - Mac.command` | `START HERE - Windows.bat` |
|
||||||
|
| **看说明** | `Read me first.html` | `Read me first.html` |
|
||||||
|
| **设置** | 首次启动自动打开 | 首次启动自动打开 |
|
||||||
|
| *诊断、命令行、装到本机* | `advanced/` | `advanced/` |
|
||||||
|
|
||||||
|
```
|
||||||
|
U-Claw/
|
||||||
|
├── START HERE - Windows.bat ← 双击这个
|
||||||
|
├── START HERE - Mac.command ← 或这个
|
||||||
|
├── Read me first.html 三步上手
|
||||||
|
├── advanced/ 诊断、命令行、装到电脑、构建脚本
|
||||||
|
├── skills/ 技能内容和 manifest.json
|
||||||
|
├── lib/ 启动器、i18n、辅助脚本
|
||||||
|
├── app/ 运行时(setup.sh 下载,不在 git 里)
|
||||||
|
└── data/ 你的数据(不在 git 里)
|
||||||
|
├── .openclaw/ 配置
|
||||||
|
├── memory/ AI 记住的东西
|
||||||
|
└── backups/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Linux 可启动 U 盘
|
||||||
|
|
||||||
|
目标电脑上连操作系统都没有?从盘启动进 Ubuntu,U-Claw 已经装好。见 [`bootable/`](bootable/),基于 Ventoy + Ubuntu 24.04 LTS + 持久化。
|
||||||
|
|
||||||
|
> Electron 桌面版(`u-claw-app/`)**已于 2026-06-19 停止发布**,代码仅作存档,见 [`u-claw-app/DEPRECATED.md`](u-claw-app/DEPRECATED.md)。便携 U 盘版才是产品本体。
|
||||||
|
|
||||||
|
## 这安全吗
|
||||||
|
|
||||||
|
- **你的 API 密钥只写在盘上。** 除了你自己选的那家模型服务商,不会发到任何地方。
|
||||||
|
- **没有任何遥测。** 不打设备指纹、不上报使用情况、不需要注册账号 —— 这些代码在这个版本里根本不存在。
|
||||||
|
- **对话记录留在盘上的 `data/` 里。**
|
||||||
|
- **确实会离开这台机器的**:你发的消息会到你配置的那家模型服务商,和你直接用他们的 App 是一样的。如果这对你的数据不可接受,请用本地模型。
|
||||||
|
|
||||||
|
## U 盘用什么格式
|
||||||
|
|
||||||
|
**exFAT。不要用 NTFS。**
|
||||||
|
|
||||||
|
macOS 对 NTFS **只读**,用 NTFS 的话「配置跟着盘走」这件事在 Mac 上直接失效 —— 首次启动连配置都写不进去。
|
||||||
|
|
||||||
|
exFAT 处理大量小文件确实慢,所以 U-Claw 启动时会把可重建的缓存挪到主机本地磁盘。你的实际数据仍然留在盘上。
|
||||||
|
|
||||||
|
## 参与开发
|
||||||
|
|
||||||
|
见 [CONTRIBUTING.md](CONTRIBUTING.md)。每次 push 前会跑测试:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node --test
|
||||||
|
```
|
||||||
|
|
||||||
|
> 不是 `node --test tests/` —— Node 会把那个路径当模块加载并报 `Cannot find module`,一个测试都不跑。
|
||||||
|
|
||||||
|
它检查的是那些会悄悄坏掉的东西:启动器有没有保持精简、`.bat` 有没有保持纯 ASCII、有没有中国下载源溜回来、两个安装器对技能的处理是否一致、每个界面字符串是不是在每种语言里都有。
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
**需要安装什么吗?**
|
||||||
|
不需要。这就是重点。
|
||||||
|
|
||||||
|
**Mac 和 Windows 都能用吗?**
|
||||||
|
同一个盘,两边都能用。Windows 版发布时已经带好运行时;Mac 第一次启动会自己下载,约一分钟。
|
||||||
|
|
||||||
|
**我的设置存在哪?**
|
||||||
|
盘上的 `data/.openclaw/openclaw.json`。复制这个盘,就复制了你的整套配置。
|
||||||
|
|
||||||
|
**能离线用吗?**
|
||||||
|
可以,用本地模型。其他都需要能连上你的模型服务商。
|
||||||
|
|
||||||
|
**界面语言能改吗?**
|
||||||
|
能,设置页里。而且**语言记在盘上而不是浏览器里** —— 你把盘插到同事的英文系统上,看到的仍然是中文。
|
||||||
|
|
||||||
|
## 关于这个分支
|
||||||
|
|
||||||
|
这是 [dongsheng123132/u-claw](https://github.com/dongsheng123132/u-claw) 的一个 fork,面向新加坡 / 东南亚市场做了改造:默认值国际化、界面双语、降低非技术用户的上手门槛。
|
||||||
|
|
||||||
|
**我们不向上游提交改动** —— 这个版本移除的中国市场默认值,恰恰是上游项目存在的意义。
|
||||||
|
|
||||||
|
## 联系
|
||||||
|
|
||||||
|
- Issues — [gitea.fanghe.it.com/zhenghy/u-claw/issues](https://gitea.fanghe.it.com/zhenghy/u-claw/issues)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<p align="center"><a href="README.md">English</a> · <strong>中文</strong></p>
|
||||||
|
|
||||||
|
<p align="center">MIT License</p>
|
||||||
100
SECURITY.md
100
SECURITY.md
@@ -1,61 +1,71 @@
|
|||||||
# 安全策略 / Security Policy
|
# Security policy
|
||||||
|
|
||||||
感谢你关注 U-Claw 的安全性。本文档说明如何向我们报告安全漏洞,以及我们当前支持的版本。
|
## Reporting a vulnerability
|
||||||
|
|
||||||
## 报告漏洞 / Reporting a Vulnerability
|
**Please do not put details in a public issue.**
|
||||||
|
|
||||||
**请不要在公开 issue 里发布安全漏洞细节。**
|
Open a report at
|
||||||
|
https://gitea.fanghe.it.com/zhenghy/u-claw/issues — mark it clearly as a
|
||||||
|
security report and keep the details minimal until we can arrange a private
|
||||||
|
channel.
|
||||||
|
|
||||||
如果你发现了 U-Claw 的安全问题,请通过以下任一渠道私下联系:
|
> **This is a fork.** Upstream is `github.com/dongsheng123132/u-claw`. If the
|
||||||
|
> issue is in code we inherited unchanged, it affects their users too — please
|
||||||
|
> tell them as well. If it is in something this fork changed (the launcher, the
|
||||||
|
> i18n layer, the skill installer, the key-check endpoint, the self-heal
|
||||||
|
> routines), it is ours.
|
||||||
|
>
|
||||||
|
> We do not yet have a dedicated security address of our own. `origin.json`
|
||||||
|
> records that gap; until it is filled, the issue tracker is the route.
|
||||||
|
|
||||||
- 邮件:`hefangsheng@u-claw.org`(建议附 PoC、影响范围、复现步骤)
|
We will acknowledge within **3 working days** and give a first assessment within
|
||||||
- GitHub 私密漏洞报告:在 [u-claw 仓库](https://github.com/dongsheng123132/u-claw) 的 Security 标签页 → "Report a vulnerability"
|
**14 days**. Please give us a chance to fix it before disclosing publicly.
|
||||||
|
|
||||||
我们会在 **3 个工作日内** 确认收到,并在 **14 天内** 给出初步评估。请允许我们在公开披露前修复问题。
|
### What helps
|
||||||
|
|
||||||
### 报告内容建议
|
- Which module — `portable/`, `install/`, `bootable/`
|
||||||
|
- Version or commit hash
|
||||||
|
- How it is triggered, and from where: local, LAN, or the internet
|
||||||
|
- Steps to reproduce, or a proof of concept
|
||||||
|
- What you think the fix looks like, if you have a view
|
||||||
|
|
||||||
- 受影响的模块(portable / install / bootable / u-claw-app)
|
## What is in scope
|
||||||
- 受影响的版本或 commit hash
|
|
||||||
- 触发条件、攻击面(本地 / 局域网 / 互联网)
|
|
||||||
- 复现步骤或 PoC 代码
|
|
||||||
- 你建议的缓解或修复方向
|
|
||||||
|
|
||||||
## 支持的版本 / Supported Versions
|
- Command injection, path traversal, arbitrary write or code execution in
|
||||||
|
U-Claw's own scripts
|
||||||
|
- Injection through the loading or writing of `data/.openclaw/openclaw.json`
|
||||||
|
- Launchers (`START HERE - *`, `lib/start.mjs`) being hijacked through a
|
||||||
|
malicious directory name or environment variable
|
||||||
|
- Man-in-the-middle exposure on the one-line install path
|
||||||
|
(`curl | bash`, `irm | iex`)
|
||||||
|
- Privilege escalation in what the bootable USB scripts produce
|
||||||
|
- The key-check endpoint (`/api/test-key`) — it handles API keys server-side
|
||||||
|
- The diagnostics report leaking a secret it should have redacted
|
||||||
|
|
||||||
| 模块 | 版本 | 状态 |
|
## What is not
|
||||||
|------|------|------|
|
|
||||||
| `portable/` | 当前 main | ✅ 接受报告 |
|
|
||||||
| `install/` (`install.sh` / `install.ps1`) | 当前 main | ✅ 接受报告 |
|
|
||||||
| `bootable/` (Linux U 盘) | 当前 main | ✅ 接受报告 |
|
|
||||||
| `u-claw-app/` Electron | 当前 main | ✅ 接受报告 |
|
|
||||||
| 历史 release tag | — | ⚠️ 仅做严重等级评估,不一定回滚补丁 |
|
|
||||||
|
|
||||||
我们暂未发布稳定版本号,所有修复直接在 main 分支推送。
|
- **Upstream dependencies** — Node.js, OpenClaw, Electron, Ventoy, Ubuntu.
|
||||||
|
Report those to their projects; we track and upgrade versions.
|
||||||
|
- **A user publishing their own API key** in a screenshot or a public repo.
|
||||||
|
- **Physical access to the drive.** If someone has the drive, they have the
|
||||||
|
data — that is the trade a portable product makes, and it is stated plainly
|
||||||
|
in the README rather than defended against.
|
||||||
|
- **What an attacker can do once they already have admin or root** on the
|
||||||
|
machine.
|
||||||
|
|
||||||
## 范围 / In-Scope vs Out-of-Scope
|
## Supported
|
||||||
|
|
||||||
### 在范围内(请报告)
|
| Module | Status |
|
||||||
|
|---|---|
|
||||||
|
| `portable/` | current `main` — reports accepted |
|
||||||
|
| `install/` | current `main` — reports accepted |
|
||||||
|
| `bootable/` | current `main` — reports accepted |
|
||||||
|
| `u-claw-app/` | **deprecated 2026-06-19**, no longer built or published |
|
||||||
|
| older tags | severity assessed, but no backported patches |
|
||||||
|
|
||||||
- U-Claw 自身脚本中的命令注入、路径穿越、任意写入、代码执行
|
There are no stable version numbers yet; fixes go straight to `main`.
|
||||||
- `data/.openclaw/openclaw.json` 配置加载/写入逻辑中的注入
|
|
||||||
- 启动脚本(`Mac-Start.command` / `Windows-Start.bat` / `start.sh`)
|
|
||||||
在恶意目录名/环境变量下被劫持的可能
|
|
||||||
- 一键安装 (`curl | bash` / `irm | iex`) 链路上的 MITM 风险
|
|
||||||
- Bootable USB 制作脚本生成的产物在 Linux Live 环境下的提权问题
|
|
||||||
|
|
||||||
### 不在范围内
|
## Credit
|
||||||
|
|
||||||
- 上游依赖(Node.js / OpenClaw / Electron / Ventoy / Ubuntu)的漏洞 —
|
Anyone who reports responsibly is credited in the fixing commit and the release
|
||||||
请直接报给上游项目;我们只跟踪并升级版本。
|
notes, unless you would rather not be.
|
||||||
- 用户主动把自己的 API Key 写到公开仓库 / 截图泄露,不属于本项目缺陷。
|
|
||||||
- 物理接触 USB 后的所有攻击(含偷换 USB、键盘记录等),属于硬件安全场景。
|
|
||||||
- 在用户已经获得 admin/root 权限的情况下能做的进一步动作。
|
|
||||||
|
|
||||||
## 致谢 / Acknowledgements
|
|
||||||
|
|
||||||
发现并负责任披露安全问题的研究者,将在修复发布的 commit message 和 release notes 中署名感谢(除非你要求匿名)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
> 本策略借鉴了 GitHub 推荐的开源安全披露格式,未来会随项目成熟度更新。
|
|
||||||
|
|||||||
@@ -17,11 +17,8 @@ $SHA256Expected = "3a4c9877b483ab46d7c3fbe165a0db275e1ae3cfe56a5657e5a47c2f99a99
|
|||||||
$CacheDir = Join-Path $PSScriptRoot ".download-cache"
|
$CacheDir = Join-Path $PSScriptRoot ".download-cache"
|
||||||
$ISOPath = Join-Path $CacheDir $ISOName
|
$ISOPath = Join-Path $CacheDir $ISOName
|
||||||
|
|
||||||
# Mirror list (China mirrors first)
|
# Mirror list (official source first)
|
||||||
$Mirrors = @(
|
$Mirrors = @(
|
||||||
"https://mirrors.tuna.tsinghua.edu.cn/ubuntu-releases/24.04.4/$ISOName",
|
|
||||||
"https://mirrors.aliyun.com/ubuntu-releases/24.04.4/$ISOName",
|
|
||||||
"https://mirrors.ustc.edu.cn/ubuntu-releases/24.04.4/$ISOName",
|
|
||||||
"https://releases.ubuntu.com/24.04.4/$ISOName"
|
"https://releases.ubuntu.com/24.04.4/$ISOName"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,18 @@
|
|||||||
|
> **Historical, upstream, and left in Chinese on purpose.**
|
||||||
|
>
|
||||||
|
> This is a record of improvements the upstream project made to the bootable
|
||||||
|
> module before we forked. Nothing references it, and it describes work already
|
||||||
|
> done rather than how anything currently behaves — translating it would imply
|
||||||
|
> it is current documentation, which it is not.
|
||||||
|
>
|
||||||
|
> For how the bootable USB actually works now, see
|
||||||
|
> [README.md](README.md) and [TROUBLESHOOTING.md](TROUBLESHOOTING.md).
|
||||||
|
> The one lesson from here that still matters — an unformatted `persistence.dat`
|
||||||
|
> silently bricking the boot — is carried forward in both, and in
|
||||||
|
> [`../HANDOFF.md`](../HANDOFF.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
# U-Claw Linux Bootable USB 改进总结
|
# U-Claw Linux Bootable USB 改进总结
|
||||||
|
|
||||||
## 改进概述
|
## 改进概述
|
||||||
@@ -206,7 +221,7 @@ bash /opt/u-claw/test-installation.sh
|
|||||||
## 贡献与反馈
|
## 贡献与反馈
|
||||||
|
|
||||||
欢迎通过以下渠道提供反馈:
|
欢迎通过以下渠道提供反馈:
|
||||||
- GitHub Issues: https://github.com/dongsheng123132/u-claw/issues
|
- GitHub Issues: https://gitea.fanghe.it.com/zhenghy/u-claw/issues
|
||||||
- 微信: hecare888
|
- 微信: hecare888
|
||||||
- 邮件: [项目维护者]
|
- 邮件: [项目维护者]
|
||||||
|
|
||||||
|
|||||||
@@ -1,235 +1,184 @@
|
|||||||
# U-Claw Bootable USB (Linux)
|
# Bootable Linux USB
|
||||||
|
|
||||||
> **把任意电脑变成 AI 工作站 — 插上 U 盘,开机即用**
|
> Turn any computer into an AI workstation — boot from the drive, no operating
|
||||||
>
|
> system required on the machine itself.
|
||||||
> **Turn any computer into an AI workstation — just boot from USB**
|
|
||||||
|
|
||||||
## 独立性说明
|
## How this differs from the portable build
|
||||||
|
|
||||||
本目录 (`bootable/`) 在 u-claw 主仓库中保持**目录级别的独立**:
|
The portable build (`portable/`) needs the computer to already run Windows or
|
||||||
|
macOS. This one does not need the machine to have an operating system at all:
|
||||||
|
it boots Ubuntu off the drive and runs U-Claw inside it.
|
||||||
|
|
||||||
- 不依赖仓库内 `portable/`、`u-claw-app/`、`website/` 中的任何文件
|
That makes it the answer for a machine that is locked down, wiped, or running
|
||||||
- 所有脚本内部硬编码了 URL 和路径,完全自包含
|
something you cannot install onto.
|
||||||
- 出问题只影响 `bootable/` 自身,不会波及其他模块
|
|
||||||
- 同时维护了一份**独立仓库**:[u-claw-linux](https://github.com/dongsheng123132/u-claw-linux),内容一致
|
|
||||||
|
|
||||||
## 这是什么
|
## Self-contained on purpose
|
||||||
|
|
||||||
制作一个**可启动的 Linux AI U 盘**:
|
This directory does not reference `portable/`, `u-claw-app/` or anything else in
|
||||||
|
the repository. Every script hardcodes its own URLs and paths. A change here
|
||||||
|
cannot break another module, and a change elsewhere cannot break this.
|
||||||
|
|
||||||
- 插上任意电脑,从 U 盘启动,直接进入 Ubuntu 桌面
|
There is also a standalone mirror at
|
||||||
- 一键安装 OpenClaw AI 助手,桌面图标双击即用
|
[u-claw-linux](https://gitea.fanghe.it.com/zhenghy/u-claw-linux) with the same
|
||||||
- 内置持久化存储,安装的软件和数据重启后保留
|
contents, for cloning it on its own.
|
||||||
- **不需要目标电脑有任何操作系统**
|
|
||||||
|
|
||||||
> 与便携版(`portable/`)的区别:便携版需要电脑已有 Windows/Mac 系统,可启动版连系统都不需要。
|
## What ends up on the drive
|
||||||
|
|
||||||
## 技术方案
|
|
||||||
|
|
||||||
```
|
```
|
||||||
┌────────────────────────────────────────────┐
|
Ventoy boot area (hidden partition)
|
||||||
│ U 盘结构 │
|
BIOS + UEFI dual-mode boot, Ventoy 1.0.99
|
||||||
│ │
|
|
||||||
│ Ventoy 引导区(隐藏分区) │
|
Ventoy data partition (visible)
|
||||||
│ - BIOS + UEFI 双模式启动 │
|
ubuntu-24.04.4-desktop-amd64.iso 5.8 GB
|
||||||
│ - 开源引导管理器 v1.0.99 │
|
persistence.dat 20 GB
|
||||||
│ │
|
ventoy/ventoy.json config
|
||||||
│ Ventoy 数据分区(可见) │
|
u-claw-linux/
|
||||||
│ ubuntu-24.04.4-desktop-amd64.iso 5.8GB │
|
├── setup-openclaw.sh
|
||||||
│ persistence.dat 20GB │
|
└── start-openclaw.sh
|
||||||
│ ventoy/ventoy.json 配置 │
|
|
||||||
│ u-claw-linux/ 脚本 │
|
|
||||||
│ ├── setup-openclaw.sh │
|
|
||||||
│ └── start-openclaw.sh │
|
|
||||||
└────────────────────────────────────────────┘
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**三个核心技术选型:**
|
| Choice | Why |
|
||||||
|
|---|---|
|
||||||
|
| **Ventoy 1.0.99** | Drop an ISO in and it boots — no burning, and several systems can share the drive |
|
||||||
|
| **Ubuntu 24.04 LTS** | Long-term support, the widest hardware driver coverage |
|
||||||
|
| **casper-rw persistence** | Lets a live USB keep data across reboots |
|
||||||
|
|
||||||
| 技术 | 为什么选它 |
|
## What you need
|
||||||
|------|-----------|
|
|
||||||
| **Ventoy 1.0.99** | ISO 文件直接丢进去就能启动,不用烧录,可放多个系统 |
|
|
||||||
| **Ubuntu 24.04 LTS** | 长期支持版,驱动兼容性最好,社区最大 |
|
|
||||||
| **casper-rw 持久化** | 让 Live USB 也能保存数据,重启不丢失 |
|
|
||||||
|
|
||||||
## 硬件要求
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| USB drive | **32 GB or larger.** USB 3.0 strongly recommended — on 2.0 everything is painful |
|
||||||
|
| Build machine | Windows 10/11, PowerShell 5.1+ |
|
||||||
|
| Target machine | x86_64 (Intel or AMD), any brand |
|
||||||
|
| Network | Needed once, when installing OpenClaw |
|
||||||
|
|
||||||
| 项目 | 要求 |
|
## Building it — four steps
|
||||||
|------|------|
|
|
||||||
| U 盘 | **32GB+**,强烈推荐 USB 3.0(蓝色接口) |
|
|
||||||
| 制作环境 | Windows 10/11,PowerShell 5.1+ |
|
|
||||||
| 目标电脑 | x86_64(Intel / AMD),任意品牌 |
|
|
||||||
| 网络 | 首次安装 OpenClaw 时需要联网 |
|
|
||||||
|
|
||||||
## 快速制作(4 步)
|
Open PowerShell **as Administrator** on Windows:
|
||||||
|
|
||||||
在 Windows 上以**管理员身份**打开 PowerShell:
|
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
cd path\to\u-claw\bootable
|
cd path\to\u-claw\bootable
|
||||||
|
|
||||||
# Step 1: 下载 Ventoy 并写入 U 盘(会格式化!)
|
.\1-prepare-usb.ps1 # Write Ventoy to the drive — THIS FORMATS IT
|
||||||
.\1-prepare-usb.ps1
|
.\2-download-iso.ps1 # Download Ubuntu 24.04 (~5.8 GB)
|
||||||
|
.\3-create-persistence.ps1 # Create the persistence image (20 GB default)
|
||||||
# Step 2: 下载 Ubuntu 24.04 ISO(~5.8GB,国内镜像)
|
.\4-copy-to-usb.ps1 # Copy everything onto the drive
|
||||||
.\2-download-iso.ps1
|
|
||||||
|
|
||||||
# Step 3: 创建持久化镜像(默认 20GB)
|
|
||||||
.\3-create-persistence.ps1
|
|
||||||
|
|
||||||
# Step 4: 拷贝所有文件到 U 盘
|
|
||||||
.\4-copy-to-usb.ps1
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 自动脚本失败时的手动兜底流程
|
### What each step does
|
||||||
|
|
||||||
> 由 @wzf9 在 issue #28 反馈整理,适合 Ventoy/ISO 自动下载失败、网络不稳定或需要离线制作的场景。
|
**1 · Write Ventoy** — lists your USB devices for confirmation, downloads Ventoy,
|
||||||
|
then hands you its GUI to pick the drive and click Install.
|
||||||
|
**This erases the drive. Back it up first.**
|
||||||
|
|
||||||
1. **Ventoy 下载或安装失败**
|
**2 · Download Ubuntu** — fetches the 24.04.4 desktop ISO from
|
||||||
- 手动下载 Ventoy Windows 版:https://github.com/ventoy/Ventoy/releases
|
`releases.ubuntu.com`, verifies its SHA256, and caches it so a retry does not
|
||||||
- 解压后运行 `Ventoy2Disk.exe`
|
re-download 5.8 GB.
|
||||||
- 选择目标 U 盘并点击 Install
|
|
||||||
- 注意:这一步会格式化 U 盘,先备份数据
|
|
||||||
|
|
||||||
2. **Ubuntu ISO 自动下载失败**
|
**3 · Create persistence** — the step that actually decides whether this works.
|
||||||
- 手动下载 Ubuntu 24.04.4 Desktop ISO:
|
|
||||||
https://releases.ubuntu.com/24.04/ubuntu-24.04.4-desktop-amd64.iso
|
- **With WSL installed** → creates a properly formatted ext4 image directly
|
||||||
- 手动下载 SHA256SUMS:
|
- **With only docker-desktop's WSL** → falls back to `/sbin/mkfs.ext4`
|
||||||
https://releases.ubuntu.com/24.04/SHA256SUMS
|
- **With no WSL at all** → creates an empty sparse file that **you must format
|
||||||
- 将 ISO 放到 `bootable\.download-cache\ubuntu-24.04.4-desktop-amd64.iso`
|
by hand** on first boot (see below)
|
||||||
- 可用 PowerShell 校验哈希:
|
|
||||||
|
The volume label must be `casper-rw` — that is the name Ubuntu looks for.
|
||||||
|
|
||||||
|
**4 · Copy to the drive** — finds the Ventoy drive by its label, checks free
|
||||||
|
space, copies the ISO, `persistence.dat`, `ventoy.json` and the setup scripts.
|
||||||
|
|
||||||
|
## When the scripts fail
|
||||||
|
|
||||||
|
Reported by @wzf9 in issue #28. Useful when a download fails, the network is
|
||||||
|
unreliable, or you are building offline.
|
||||||
|
|
||||||
|
**Ventoy would not download or install**
|
||||||
|
|
||||||
|
Get the Windows build from https://github.com/ventoy/Ventoy/releases, unzip,
|
||||||
|
run `Ventoy2Disk.exe`, pick the drive, click Install. It formats the drive.
|
||||||
|
|
||||||
|
**The ISO would not download**
|
||||||
|
|
||||||
|
Fetch it yourself:
|
||||||
|
- https://releases.ubuntu.com/24.04/ubuntu-24.04.4-desktop-amd64.iso
|
||||||
|
- https://releases.ubuntu.com/24.04/SHA256SUMS
|
||||||
|
|
||||||
|
Put the ISO at `bootable\.download-cache\ubuntu-24.04.4-desktop-amd64.iso`, then
|
||||||
|
check it:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
(Get-FileHash -Algorithm SHA256 ".\.download-cache\ubuntu-24.04.4-desktop-amd64.iso").Hash -eq "3a4c9877b483ab46d7c3fbe165a0db275e1ae3cfe56a5657e5a47c2f99a99d1e"
|
(Get-FileHash -Algorithm SHA256 ".\.download-cache\ubuntu-24.04.4-desktop-amd64.iso").Hash -eq "3a4c9877b483ab46d7c3fbe165a0db275e1ae3cfe56a5657e5a47c2f99a99d1e"
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **继续创建持久化文件**
|
**The persistence file is empty (no WSL)**
|
||||||
|
|
||||||
```powershell
|
Boot into Ubuntu once, then:
|
||||||
.\3-create-persistence.ps1
|
|
||||||
```
|
|
||||||
|
|
||||||
如果脚本提示没有 WSL 或只能创建空文件,首次进入 Ubuntu 后需要手动格式化:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo mkfs.ext4 -F -L casper-rw /media/*/Ventoy/persistence.dat
|
sudo mkfs.ext4 -F -L casper-rw /media/*/Ventoy/persistence.dat
|
||||||
```
|
```
|
||||||
|
|
||||||
格式化完成后重启,持久化才会生效。
|
**Reboot after formatting** — persistence does not take effect until you do.
|
||||||
|
|
||||||
4. **拷贝到 U 盘**
|
**The copy step failed**
|
||||||
- 优先运行:
|
|
||||||
|
|
||||||
```powershell
|
Put these on the Ventoy data partition by hand:
|
||||||
.\4-copy-to-usb.ps1
|
|
||||||
```
|
|
||||||
|
|
||||||
- 如果自动拷贝失败,也可以手动放到 Ventoy 数据分区根目录:
|
| From | To (drive root) |
|
||||||
|
|---|---|
|
||||||
| 本地路径 | U 盘目标 |
|
|
||||||
|---------|----------|
|
|
||||||
| `bootable\linux-setup\` | `u-claw-linux\` |
|
| `bootable\linux-setup\` | `u-claw-linux\` |
|
||||||
| `bootable\ventoy\` | `ventoy\` |
|
| `bootable\ventoy\` | `ventoy\` |
|
||||||
| `bootable\.download-cache\persistence.dat` | `persistence.dat` |
|
| `bootable\.download-cache\persistence.dat` | `persistence.dat` |
|
||||||
| `bootable\.download-cache\ubuntu-24.04.4-desktop-amd64.iso` | `ubuntu-24.04.4-desktop-amd64.iso` |
|
| `bootable\.download-cache\ubuntu-24.04.4-desktop-amd64.iso` | (same name) |
|
||||||
|
|
||||||
5. **目标电脑启动**
|
## Using it
|
||||||
- 从 U 盘启动,Ventoy 菜单选择 Ubuntu
|
|
||||||
- 如遇 Secure Boot 拦截,进 BIOS 关闭 Secure Boot
|
### First time
|
||||||
- 进入 Ubuntu 桌面后运行:
|
|
||||||
|
1. Plug the drive into the target machine
|
||||||
|
2. Power on and press the boot-menu key:
|
||||||
|
|
||||||
|
| Brand | Key |
|
||||||
|
|---|---|
|
||||||
|
| Dell | F12 |
|
||||||
|
| Lenovo | F12 |
|
||||||
|
| HP | F9 |
|
||||||
|
| ASUS | F2 or DEL |
|
||||||
|
| Acer | F12 |
|
||||||
|
| MSI | F11 |
|
||||||
|
| Others | F12 or F2 usually; check the splash screen |
|
||||||
|
|
||||||
|
3. Choose the USB device, then Ubuntu from the Ventoy menu
|
||||||
|
4. Wait for the desktop — one to three minutes off a USB drive is normal
|
||||||
|
5. Connect to Wi-Fi
|
||||||
|
6. Open a terminal (`Ctrl+Alt+T`) and run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo bash /media/*/Ventoy/u-claw-linux/setup-openclaw.sh
|
sudo bash /media/*/Ventoy/u-claw-linux/setup-openclaw.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
## 每一步做了什么
|
7. A **U-Claw AI Assistant** icon appears on the desktop
|
||||||
|
8. Double-click it, then paste an API key in the browser
|
||||||
|
|
||||||
### Step 1: 写入 Ventoy 引导 (`1-prepare-usb.ps1`)
|
### After that
|
||||||
|
|
||||||
- 列出所有 USB 设备,让你确认
|
Plug in, boot, double-click the icon. Everything you set up is still there.
|
||||||
- 从 GitHub 下载 Ventoy 1.0.99
|
|
||||||
- 启动 Ventoy2Disk.exe GUI
|
|
||||||
- 你在 GUI 中选择 U 盘 → 点 Install
|
|
||||||
- **注意:会格式化 U 盘,数据全丢!提前备份!**
|
|
||||||
|
|
||||||
### Step 2: 下载 Ubuntu ISO (`2-download-iso.ps1`)
|
## What `setup-openclaw.sh` does
|
||||||
|
|
||||||
- 从国内镜像下载 Ubuntu 24.04.4 桌面版(~5.8GB)
|
| Step | |
|
||||||
- 镜像优先级:清华 → 阿里 → 中科大 → 官方
|
|---|---|
|
||||||
- SHA256 校验确保文件完整
|
| 1 | Check it is running as root — it needs `sudo` |
|
||||||
- 有缓存,不会重复下载
|
| 2 | Install `curl` and `xdg-utils` |
|
||||||
|
| 3 | Create `/opt/u-claw/{runtime,core,data}` |
|
||||||
|
| 4 | Download Node.js from `nodejs.org` |
|
||||||
|
| 5 | Write `package.json` |
|
||||||
|
| 6 | Install OpenClaw and the QQ plugin from `registry.npmjs.org` |
|
||||||
|
| 7 | Write the default config — gateway and token |
|
||||||
|
| 8 | Install the start script into `/opt/u-claw/` |
|
||||||
|
| 9 | Create the desktop shortcut, optionally starting on login |
|
||||||
|
|
||||||
### Step 3: 创建持久化镜像 (`3-create-persistence.ps1`)
|
## Configuration
|
||||||
|
|
||||||
这是整个方案**最关键**的一步:
|
|
||||||
|
|
||||||
- 检测是否安装了 WSL(Windows 子系统 Linux)
|
|
||||||
- **有 WSL** → 用 `mkfs.ext4` 直接创建格式化好的 ext4 镜像
|
|
||||||
- **没 WSL** → 创建稀疏文件,首次进 Linux 后需手动格式化
|
|
||||||
- 卷标必须是 `casper-rw`(Ubuntu 持久化的约定)
|
|
||||||
- 默认 20GB,可选 1-28GB
|
|
||||||
|
|
||||||
### Step 4: 拷贝到 U 盘 (`4-copy-to-usb.ps1`)
|
|
||||||
|
|
||||||
- 自动识别 Ventoy U 盘(通过卷标)
|
|
||||||
- 检查剩余空间
|
|
||||||
- 拷贝 4 样东西:ISO、persistence.dat、ventoy.json、安装脚本
|
|
||||||
|
|
||||||
## 使用方法
|
|
||||||
|
|
||||||
### 首次使用
|
|
||||||
|
|
||||||
1. 将 U 盘插入目标电脑
|
|
||||||
2. 开机按启动键:
|
|
||||||
|
|
||||||
| 品牌 | 启动键 |
|
|
||||||
|------|--------|
|
|
||||||
| Dell 戴尔 | F12 |
|
|
||||||
| Lenovo 联想 | F12 |
|
|
||||||
| HP 惠普 | F9 |
|
|
||||||
| ASUS 华硕 | F2 或 DEL |
|
|
||||||
| Acer 宏碁 | F12 |
|
|
||||||
| MSI 微星 | F11 |
|
|
||||||
| Huawei 华为 | F12 |
|
|
||||||
| Xiaomi 小米 | F12 |
|
|
||||||
|
|
||||||
3. 启动菜单选择 USB 设备
|
|
||||||
4. Ventoy 菜单 → 选择 Ubuntu
|
|
||||||
5. 等待 Ubuntu 桌面加载
|
|
||||||
6. 连接 Wi-Fi
|
|
||||||
7. 打开终端(`Ctrl+Alt+T` 或右键桌面 → Open Terminal)
|
|
||||||
8. 运行安装命令:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo bash /media/*/Ventoy/u-claw-linux/setup-openclaw.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
9. 桌面出现 **"U-Claw AI Assistant"** 图标
|
|
||||||
10. 双击图标 → 浏览器打开 → 配置 AI 模型
|
|
||||||
|
|
||||||
### 日常使用
|
|
||||||
|
|
||||||
1. 插入 U 盘 → 开机选 USB → Ubuntu 桌面
|
|
||||||
2. 双击桌面图标
|
|
||||||
3. 所有数据自动保留
|
|
||||||
|
|
||||||
## 安装脚本详解 (`setup-openclaw.sh`)
|
|
||||||
|
|
||||||
9 个步骤,完全自包含:
|
|
||||||
|
|
||||||
| 步骤 | 操作 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| 1 | 检查 root 权限 | 必须 `sudo` 运行 |
|
|
||||||
| 2 | 安装系统依赖 | `curl`, `xdg-utils` |
|
|
||||||
| 3 | 创建目录 | `/opt/u-claw/{runtime,core,data}` |
|
|
||||||
| 4 | 下载 Node.js v22 | 国内镜像优先,官方回退 |
|
|
||||||
| 5 | 创建 package.json | — |
|
|
||||||
| 6 | 安装 OpenClaw + QQ 插件 | npm 国内镜像 |
|
|
||||||
| 7 | 写默认配置 | gateway + token |
|
|
||||||
| 8 | 安装启动脚本 | → `/opt/u-claw/` |
|
|
||||||
| 9 | 创建桌面快捷方式 | 可选开机自启 |
|
|
||||||
|
|
||||||
## 核心配置文件
|
|
||||||
|
|
||||||
### `ventoy/ventoy.json`
|
### `ventoy/ventoy.json`
|
||||||
|
|
||||||
@@ -245,126 +194,71 @@ sudo bash /media/*/Ventoy/u-claw-linux/setup-openclaw.sh
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
告诉 Ventoy:启动 Ubuntu ISO 时自动加载 `persistence.dat`。`autosel: 1` = 不弹确认框。
|
Tells Ventoy to attach `persistence.dat` when booting that ISO. `autosel: 1`
|
||||||
|
skips the confirmation prompt.
|
||||||
|
|
||||||
### Linux 端环境变量
|
### Environment on Linux
|
||||||
|
|
||||||
| 变量 | 值 |
|
| Variable | Value |
|
||||||
|------|-----|
|
|---|---|
|
||||||
| `OPENCLAW_HOME` | `/opt/u-claw/data/.openclaw` |
|
| `OPENCLAW_HOME` | `/opt/u-claw/data/.openclaw` |
|
||||||
| `OPENCLAW_STATE_DIR` | `/opt/u-claw/data/.openclaw` |
|
| `OPENCLAW_STATE_DIR` | `/opt/u-claw/data/.openclaw` |
|
||||||
| `OPENCLAW_CONFIG_PATH` | `/opt/u-claw/data/.openclaw/openclaw.json` |
|
| `OPENCLAW_CONFIG_PATH` | `/opt/u-claw/data/.openclaw/openclaw.json` |
|
||||||
|
|
||||||
## 文件结构
|
## Files
|
||||||
|
|
||||||
```
|
```
|
||||||
bootable/
|
bootable/
|
||||||
├── README.md 本文件
|
├── README.md this file
|
||||||
├── 1-prepare-usb.ps1 Step 1: Ventoy 写入
|
├── 1-prepare-usb.ps1 write Ventoy
|
||||||
├── 2-download-iso.ps1 Step 2: Ubuntu ISO 下载
|
├── 2-download-iso.ps1 download Ubuntu
|
||||||
├── 3-create-persistence.ps1 Step 3: 持久化镜像
|
├── 3-create-persistence.ps1 create the persistence image
|
||||||
├── 4-copy-to-usb.ps1 Step 4: 拷贝到 U 盘
|
├── 4-copy-to-usb.ps1 copy onto the drive
|
||||||
├── linux-setup/
|
├── linux-setup/
|
||||||
│ ├── format-persistence.sh 格式化持久化镜像
|
│ ├── format-persistence.sh format the persistence image
|
||||||
│ ├── setup-openclaw.sh 一键安装 OpenClaw
|
│ ├── setup-openclaw.sh install OpenClaw
|
||||||
│ ├── start-openclaw.sh 启动脚本
|
│ ├── start-openclaw.sh launcher
|
||||||
│ └── openclaw.desktop 桌面快捷方式
|
│ └── openclaw.desktop desktop shortcut
|
||||||
└── ventoy/
|
└── ventoy/
|
||||||
└── ventoy.json Ventoy 持久化配置
|
└── ventoy.json persistence config
|
||||||
```
|
```
|
||||||
|
|
||||||
## 实践经验与注意事项
|
## Things learned the hard way
|
||||||
|
|
||||||
### 制作阶段
|
**An unformatted `persistence.dat` drops Ubuntu into initramfs.** Ventoy cannot
|
||||||
|
mount a file with no ext4 filesystem in it, and the boot fails in a way that
|
||||||
|
gives no hint about the cause. To check whether a file is actually formatted,
|
||||||
|
read two bytes at offset 1080 — they should be `0x53 0xEF`, the little-endian
|
||||||
|
form of the ext4 magic number `0xEF53`.
|
||||||
|
|
||||||
1. **U 盘选择很重要**
|
**Formatting requires a reboot.** Running `mkfs.ext4` and continuing in the same
|
||||||
- 必须 32GB+(ISO 5.8GB + 持久化 20GB + 系统开销)
|
session looks like it worked and silently does not persist.
|
||||||
- 强烈建议 USB 3.0,否则启动和运行都会很慢
|
|
||||||
- 推荐品牌:闪迪、金士顿、三星(杂牌盘容易出问题)
|
|
||||||
- 避免使用 USB Hub,直接插主板接口
|
|
||||||
|
|
||||||
2. **Step 1 会清空 U 盘**
|
**Drive quality matters more than you would expect.** Cheap unbranded drives fail
|
||||||
- Ventoy 安装会格式化整个 U 盘,**务必提前备份**
|
in ways that look like software problems. Use USB 3.0, plug into the board
|
||||||
- 脚本会列出所有 USB 设备让你确认,看清楚再操作
|
directly rather than through a hub, and prefer 32 GB+ (5.8 GB ISO + 20 GB
|
||||||
|
persistence + overhead leaves little room otherwise).
|
||||||
|
|
||||||
3. **Step 3 持久化镜像(重要教训)**
|
**Sizing**: 20 GB persistence on a 32 GB drive, 40 GB+ on a 64 GB drive.
|
||||||
- 有标准 WSL(Ubuntu 等)→ 自动创建 ext4 镜像(最省事)
|
|
||||||
- 只有 docker-desktop WSL → 脚本会尝试用 `/sbin/mkfs.ext4` 格式化(2026-03-17 修复)
|
|
||||||
- 完全没有 WSL → 创建空文件,**必须**首次进 Linux 后手动格式化:
|
|
||||||
```bash
|
|
||||||
sudo mkfs.ext4 -F -L casper-rw /media/*/Ventoy/persistence.dat
|
|
||||||
```
|
|
||||||
格式化后**必须重启**才能生效
|
|
||||||
- **踩坑记录**:空的 persistence.dat(无 ext4 文件系统)会导致 Ventoy 挂载失败,
|
|
||||||
Ubuntu 启动直接掉进 initramfs。验证方法:读取文件 offset 1080 处的 2 字节,
|
|
||||||
应为 `0x53 0xEF`(ext4 magic number `0xEF53` 的 little-endian 表示)
|
|
||||||
- 大小建议:32GB U 盘选 20GB,64GB U 盘可选 40GB+
|
|
||||||
|
|
||||||
4. **ISO 下载失败**
|
## Troubleshooting
|
||||||
- 脚本默认走清华/阿里/中科大国内镜像,无需翻墙
|
|
||||||
- 如果全部失败,手动下载 Ubuntu ISO 放到 `.download-cache/` 目录即可
|
|
||||||
|
|
||||||
### 启动阶段
|
| Problem | Fix |
|
||||||
|
|---|---|
|
||||||
|
| Will not boot from USB | In BIOS: disable Secure Boot, enable USB boot. Try both UEFI and Legacy/CSM |
|
||||||
|
| No Ubuntu in the Ventoy menu | The ISO must be in the root of the Ventoy data partition |
|
||||||
|
| Boot stops at initramfs | `persistence.dat` is not formatted. `mkfs.ext4 -F -L casper-rw`, then reboot |
|
||||||
|
| Data lost on reboot | Same cause — check the file is ext4 and the label is exactly `casper-rw` |
|
||||||
|
| No USB boot entry at all | Try another port; some machines disable USB boot in BIOS by default |
|
||||||
|
| Desktop takes ages to load | Normal off a USB drive. USB 3.0 on a USB 3.0 port helps a lot |
|
||||||
|
| OpenClaw install fails | Check the network — it needs `nodejs.org` and `registry.npmjs.org` |
|
||||||
|
| Wi-Fi does not work | Ubuntu 24.04 covers most chipsets; otherwise tether over USB from a phone |
|
||||||
|
| Browser does not open | Go to `http://localhost:18789` yourself |
|
||||||
|
| Port already in use | OpenClaw uses 18789–18799. Close the terminal window and open it again |
|
||||||
|
| Wrong screen resolution | Settings → Displays |
|
||||||
|
|
||||||
5. **Secure Boot 问题**
|
**Performance**: running off a USB drive is slower than a disk — that is
|
||||||
- 部分电脑需要关闭 Secure Boot 才能从 U 盘启动
|
physics, not a bug. Inference happens at your model provider, so conversation
|
||||||
- 进 BIOS → Security → Secure Boot → Disabled
|
speed is unaffected; only startup and file operations are slower.
|
||||||
- 不同品牌进 BIOS 的方式不同(通常 DEL 或 F2)
|
|
||||||
|
|
||||||
6. **找不到 USB 启动项**
|
For anything not covered here, see [TROUBLESHOOTING.md](TROUBLESHOOTING.md).
|
||||||
- 换个 USB 口试试
|
|
||||||
- 有些电脑默认禁用了 USB 启动,需要在 BIOS 中开启
|
|
||||||
- Legacy/CSM 模式和 UEFI 模式都试试
|
|
||||||
|
|
||||||
7. **Ubuntu 桌面加载慢**
|
|
||||||
- 正常现象,Live USB 从 U 盘读取比硬盘慢
|
|
||||||
- USB 3.0 U 盘 + USB 3.0 接口会快很多
|
|
||||||
- 首次加载约 1-3 分钟
|
|
||||||
|
|
||||||
### 使用阶段
|
|
||||||
|
|
||||||
8. **Wi-Fi 连接**
|
|
||||||
- Ubuntu 24.04 支持大多数 Wi-Fi 芯片
|
|
||||||
- 不行的话用手机 USB 共享网络,或 USB 无线网卡
|
|
||||||
|
|
||||||
9. **OpenClaw 安装需要网络**
|
|
||||||
- 国内镜像优先,无需翻墙
|
|
||||||
- 安装过程约 1-2 分钟
|
|
||||||
|
|
||||||
10. **端口冲突**
|
|
||||||
- OpenClaw 使用端口 18789-18799
|
|
||||||
- 提示端口占用 → 关闭终端窗口再重新打开
|
|
||||||
|
|
||||||
11. **数据位置**
|
|
||||||
- 安装目录:`/opt/u-claw/`
|
|
||||||
- 配置文件:`/opt/u-claw/data/.openclaw/openclaw.json`
|
|
||||||
- 所有数据保存在持久化镜像中,重启不丢
|
|
||||||
|
|
||||||
12. **性能预期**
|
|
||||||
- U 盘运行比硬盘慢,这是物理限制
|
|
||||||
- AI 推理在云端,本地只跑网关,对话速度不受影响
|
|
||||||
|
|
||||||
## 常见故障排查
|
|
||||||
|
|
||||||
| 问题 | 解决方案 |
|
|
||||||
|------|---------|
|
|
||||||
| 无法从 U 盘启动 | BIOS 关闭 Secure Boot,开启 USB Boot |
|
|
||||||
| Ventoy 菜单无 Ubuntu | ISO 文件是否在 Ventoy 数据分区根目录 |
|
|
||||||
| 启动卡在 initramfs | persistence.dat 未格式化为 ext4,用 `mkfs.ext4 -F -L casper-rw` 格式化后重启 |
|
|
||||||
| 持久化不生效(重启丢数据) | persistence.dat 是否已格式化为 ext4,卷标是否为 `casper-rw` |
|
|
||||||
| OpenClaw 安装失败 | 检查网络,确认能访问 npmmirror.com |
|
|
||||||
| 浏览器打不开 | 手动打开浏览器访问 `http://localhost:18789` |
|
|
||||||
| 屏幕分辨率不对 | Settings → Displays → Resolution |
|
|
||||||
|
|
||||||
## 技术说明
|
|
||||||
|
|
||||||
- **Ventoy**: 开源引导管理器,ISO/WIM/VHD 直接启动,更新 ISO 不用重新格式化
|
|
||||||
- **Persistence**: Ventoy persistence 插件 + `casper-rw` 标签 ext4 镜像
|
|
||||||
- **Node.js**: v22.14.0 LTS,npmmirror.com(国内)或 nodejs.org
|
|
||||||
- **OpenClaw**: npm latest,安装到 `/opt/u-claw/`
|
|
||||||
- **完全独立**: 不引用仓库内 `portable/`、`u-claw-app/`、`website/` 的任何文件
|
|
||||||
|
|
||||||
## 详细故障排除
|
|
||||||
|
|
||||||
遇到问题请参考详细故障排除指南:
|
|
||||||
- [TROUBLESHOOTING.md](TROUBLESHOOTING.md) - 完整的问题排查步骤和解决方案
|
|
||||||
|
|||||||
@@ -1,224 +1,206 @@
|
|||||||
# U-Claw Bootable USB 故障排除指南
|
# Troubleshooting the bootable USB
|
||||||
|
|
||||||
## 常见问题与解决方案
|
## While building the drive
|
||||||
|
|
||||||
### 1. Ventoy安装失败
|
### Ventoy will not install
|
||||||
|
|
||||||
**问题**: Ventoy安装时提示"Access Denied"或无法写入U盘
|
*"Access Denied", or it cannot write to the drive.*
|
||||||
**解决方案**:
|
|
||||||
1. 以管理员身份运行PowerShell
|
|
||||||
2. 关闭所有可能占用U盘的程序(文件资源管理器、杀毒软件等)
|
|
||||||
3. 尝试使用Ventoy的"只读模式"安装
|
|
||||||
4. 如果U盘有写保护开关,请关闭它
|
|
||||||
|
|
||||||
### 2. Ubuntu ISO下载缓慢或失败
|
1. Run PowerShell **as Administrator**
|
||||||
|
2. Close anything holding the drive open — File Explorer, antivirus, backup tools
|
||||||
|
3. Try Ventoy's read-only install mode
|
||||||
|
4. Check for a physical write-protect switch on the drive
|
||||||
|
|
||||||
**问题**: 下载Ubuntu ISO时速度慢或中断
|
### The ISO download is slow or fails
|
||||||
**解决方案**:
|
|
||||||
1. 脚本会自动尝试多个国内镜像(清华、阿里、中科大)
|
|
||||||
2. 如果全部失败,可以手动下载:
|
|
||||||
- 访问 https://mirrors.tuna.tsinghua.edu.cn/ubuntu-releases/24.04/
|
|
||||||
- 下载 `ubuntu-24.04.4-desktop-amd64.iso`
|
|
||||||
- 放到 `bootable/.download-cache/` 目录
|
|
||||||
3. 重新运行 `2-download-iso.ps1`
|
|
||||||
|
|
||||||
### 3. 持久化存储创建失败
|
The script pulls from `releases.ubuntu.com` and verifies the SHA256. If it keeps
|
||||||
|
failing, download it yourself:
|
||||||
|
|
||||||
**问题**: `3-create-persistence.ps1` 提示磁盘空间不足
|
- https://releases.ubuntu.com/24.04/ubuntu-24.04.4-desktop-amd64.iso
|
||||||
**解决方案**:
|
|
||||||
1. 确保U盘至少有32GB空间
|
|
||||||
2. 默认持久化大小为20GB,可以修改脚本中的 `$PersistenceSizeGB` 变量
|
|
||||||
3. 最小建议值:8GB
|
|
||||||
|
|
||||||
### 4. Linux启动后无法进入桌面
|
Put it in `bootable/.download-cache/` and run `2-download-iso.ps1` again — it
|
||||||
|
finds the cached file and verifies it rather than downloading again.
|
||||||
|
|
||||||
**问题**: 从U盘启动后黑屏或卡住
|
### Not enough space for the persistence image
|
||||||
**解决方案**:
|
|
||||||
1. 启动时按 `F6` 或 `Esc` 进入引导选项
|
|
||||||
2. 添加内核参数:
|
|
||||||
- `nomodeset` - 禁用显卡驱动
|
|
||||||
- `quiet splash` - 禁用启动画面
|
|
||||||
3. 如果使用NVIDIA显卡,尝试 `nouveau.modeset=0`
|
|
||||||
|
|
||||||
### 5. OpenClaw安装失败
|
`3-create-persistence.ps1` needs room for a 20 GB file alongside a 5.8 GB ISO.
|
||||||
|
|
||||||
**问题**: `setup-openclaw.sh` 执行失败
|
- A 32 GB drive is the practical minimum
|
||||||
**解决方案**:
|
- Change `$PersistenceSizeGB` in the script to use less
|
||||||
1. **网络问题**:
|
- Below about 8 GB there is not enough room to install anything meaningful
|
||||||
```bash
|
|
||||||
# 测试网络连接
|
|
||||||
ping -c 3 npmmirror.com
|
|
||||||
|
|
||||||
# 如果网络有问题,使用代理
|
|
||||||
export http_proxy=http://your-proxy:port
|
|
||||||
export https_proxy=http://your-proxy:port
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **权限问题**:
|
## While booting
|
||||||
```bash
|
|
||||||
# 确保以root运行
|
|
||||||
sudo bash setup-openclaw.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **依赖问题**:
|
### Black screen, or it hangs before the desktop
|
||||||
```bash
|
|
||||||
# 手动安装依赖
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install curl xdg-utils
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6. OpenClaw无法启动
|
Press `F6` or `Esc` at the boot menu to add kernel parameters:
|
||||||
|
|
||||||
**问题**: `start-openclaw.sh` 启动失败
|
| Parameter | What it does |
|
||||||
**解决方案**:
|
|---|---|
|
||||||
1. **检查Node.js**:
|
| `nomodeset` | Skips the graphics driver — the usual fix |
|
||||||
```bash
|
| `nouveau.modeset=0` | For NVIDIA cards specifically |
|
||||||
/opt/u-claw/runtime/node-linux-x64/bin/node --version
|
| `quiet splash` | Removes the splash screen so you can see where it stops |
|
||||||
# 应该显示 v22.14.0
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **检查OpenClaw安装**:
|
### It drops into an initramfs prompt
|
||||||
```bash
|
|
||||||
ls -la /opt/u-claw/core/node_modules/openclaw/
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **端口冲突**:
|
`persistence.dat` has no ext4 filesystem in it. This is the single most common
|
||||||
```bash
|
build failure, and the error message says nothing about the real cause.
|
||||||
# 检查端口占用
|
|
||||||
ss -tlnp | grep :18789
|
|
||||||
|
|
||||||
# 如果端口被占用,手动指定端口
|
|
||||||
cd /opt/u-claw/core
|
|
||||||
node node_modules/openclaw/openclaw.mjs gateway run --port 18800
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7. 浏览器无法打开
|
|
||||||
|
|
||||||
**问题**: 启动后浏览器没有自动打开
|
|
||||||
**解决方案**:
|
|
||||||
1. **手动打开浏览器**:
|
|
||||||
- 访问 http://localhost:18789
|
|
||||||
- 或 http://127.0.0.1:18789
|
|
||||||
|
|
||||||
2. **检查防火墙**:
|
|
||||||
```bash
|
|
||||||
# Ubuntu Live通常没有防火墙,但可以检查
|
|
||||||
sudo ufw status
|
|
||||||
```
|
|
||||||
|
|
||||||
### 8. 持久化数据丢失
|
|
||||||
|
|
||||||
**问题**: 重启后安装的软件或数据丢失
|
|
||||||
**解决方案**:
|
|
||||||
1. 确保启动时选择了"Ubuntu (persistence)"选项
|
|
||||||
2. 检查持久化文件大小:
|
|
||||||
```bash
|
|
||||||
ls -lh /media/ubuntu/persistence.dat
|
|
||||||
# 应该显示约20GB
|
|
||||||
```
|
|
||||||
|
|
||||||
3. 如果持久化损坏,重新创建:
|
|
||||||
```bash
|
|
||||||
# 在Windows上重新运行 3-create-persistence.ps1
|
|
||||||
```
|
|
||||||
|
|
||||||
### 9. 性能问题
|
|
||||||
|
|
||||||
**问题**: 系统运行缓慢
|
|
||||||
**解决方案**:
|
|
||||||
1. **使用USB 3.0接口**(蓝色接口)
|
|
||||||
2. **关闭不必要的特效**:
|
|
||||||
```bash
|
|
||||||
# 安装gnome-tweaks调整性能
|
|
||||||
sudo apt-get install gnome-tweaks
|
|
||||||
```
|
|
||||||
3. **增加swap空间**(仅限持久化模式):
|
|
||||||
```bash
|
|
||||||
sudo fallocate -l 2G /swapfile
|
|
||||||
sudo chmod 600 /swapfile
|
|
||||||
sudo mkswap /swapfile
|
|
||||||
sudo swapon /swapfile
|
|
||||||
```
|
|
||||||
|
|
||||||
### 10. 硬件兼容性问题
|
|
||||||
|
|
||||||
**问题**: 某些硬件不工作(WiFi、蓝牙、声卡等)
|
|
||||||
**解决方案**:
|
|
||||||
1. **更新内核**(仅限持久化模式):
|
|
||||||
```bash
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install linux-generic-hwe-24.04
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **安装额外驱动**:
|
|
||||||
```bash
|
|
||||||
# 检查可用驱动
|
|
||||||
ubuntu-drivers devices
|
|
||||||
|
|
||||||
# 安装推荐驱动
|
|
||||||
sudo ubuntu-drivers autoinstall
|
|
||||||
```
|
|
||||||
|
|
||||||
## 调试技巧
|
|
||||||
|
|
||||||
### 查看日志
|
|
||||||
```bash
|
```bash
|
||||||
# OpenClaw日志
|
sudo mkfs.ext4 -F -L casper-rw /media/*/Ventoy/persistence.dat
|
||||||
|
```
|
||||||
|
|
||||||
|
Then **reboot** — formatting does not take effect in the same session.
|
||||||
|
|
||||||
|
To check whether a file is actually formatted, read two bytes at offset 1080;
|
||||||
|
they should be `0x53 0xEF`.
|
||||||
|
|
||||||
|
### No USB entry in the boot menu
|
||||||
|
|
||||||
|
- Try a different port
|
||||||
|
- In BIOS: disable Secure Boot, enable USB boot
|
||||||
|
- Try both UEFI and Legacy/CSM modes
|
||||||
|
|
||||||
|
## After booting
|
||||||
|
|
||||||
|
### The OpenClaw install fails
|
||||||
|
|
||||||
|
**Network** — it needs to reach `nodejs.org` and `registry.npmjs.org`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -I https://registry.npmjs.org
|
||||||
|
curl -I https://nodejs.org/dist
|
||||||
|
|
||||||
|
# behind a corporate proxy
|
||||||
|
export http_proxy=http://your-proxy:port
|
||||||
|
export https_proxy=http://your-proxy:port
|
||||||
|
```
|
||||||
|
|
||||||
|
**Permissions** — it has to run as root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo bash setup-openclaw.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
**Missing packages**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt-get update && sudo apt-get install curl xdg-utils
|
||||||
|
```
|
||||||
|
|
||||||
|
### U-Claw will not start
|
||||||
|
|
||||||
|
Check Node.js is there and is the version we pin:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/opt/u-claw/runtime/node-linux-x64/bin/node --version
|
||||||
|
```
|
||||||
|
|
||||||
|
Check OpenClaw landed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ls -la /opt/u-claw/core/node_modules/openclaw/
|
||||||
|
```
|
||||||
|
|
||||||
|
Check for a port conflict — U-Claw uses 18789 to 18799:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ss -tlnp | grep :18789
|
||||||
|
|
||||||
|
# start it somewhere else if something else owns the range
|
||||||
|
cd /opt/u-claw/core
|
||||||
|
node node_modules/openclaw/openclaw.mjs gateway run --port 18800
|
||||||
|
```
|
||||||
|
|
||||||
|
### The browser does not open by itself
|
||||||
|
|
||||||
|
Go to `http://localhost:18789` yourself. Ubuntu Live normally has no firewall,
|
||||||
|
but if you suspect one:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo ufw status
|
||||||
|
```
|
||||||
|
|
||||||
|
### Data disappears after a reboot
|
||||||
|
|
||||||
|
1. Make sure you picked the persistence entry in the Ventoy menu
|
||||||
|
2. Check the file is the size you expect:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ls -lh /media/*/Ventoy/persistence.dat
|
||||||
|
```
|
||||||
|
|
||||||
|
3. If it is corrupt, re-run `3-create-persistence.ps1` on the Windows machine
|
||||||
|
|
||||||
|
### Everything is slow
|
||||||
|
|
||||||
|
Some of this is unavoidable — a USB drive is slower than a disk. What helps:
|
||||||
|
|
||||||
|
- **USB 3.0 drive in a USB 3.0 port.** The largest single difference by far.
|
||||||
|
- Turn down desktop effects: `sudo apt-get install gnome-tweaks`
|
||||||
|
- Add swap, if you are running with persistence:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo fallocate -l 2G /swapfile
|
||||||
|
sudo chmod 600 /swapfile
|
||||||
|
sudo mkswap /swapfile
|
||||||
|
sudo swapon /swapfile
|
||||||
|
```
|
||||||
|
|
||||||
|
Inference happens at your model provider, so conversation speed is not affected
|
||||||
|
by any of this — only startup and file operations are.
|
||||||
|
|
||||||
|
### Wi-Fi, Bluetooth or sound does not work
|
||||||
|
|
||||||
|
With persistence enabled you can install drivers and keep them:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install linux-generic-hwe-24.04 # newer kernel
|
||||||
|
|
||||||
|
ubuntu-drivers devices # what is available
|
||||||
|
sudo ubuntu-drivers autoinstall # install the recommended ones
|
||||||
|
```
|
||||||
|
|
||||||
|
If Wi-Fi still will not come up, tether over USB from a phone — it needs no
|
||||||
|
drivers.
|
||||||
|
|
||||||
|
## Digging deeper
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# U-Claw's own log
|
||||||
tail -f /opt/u-claw/data/logs/openclaw.log
|
tail -f /opt/u-claw/data/logs/openclaw.log
|
||||||
|
|
||||||
# 系统日志
|
# kernel and system
|
||||||
dmesg | tail -20
|
dmesg | tail -20
|
||||||
journalctl -xe
|
journalctl -xe
|
||||||
```
|
|
||||||
|
|
||||||
### 测试网络
|
# is the network reachable
|
||||||
```bash
|
curl -I https://registry.npmjs.org
|
||||||
# 测试国内镜像
|
|
||||||
curl -I https://registry.npmmirror.com
|
|
||||||
curl -I https://npmmirror.com/mirrors/node
|
|
||||||
|
|
||||||
# 测试AI API
|
# where the space went
|
||||||
curl https://api.deepseek.com/health
|
df -h /media/*/Ventoy
|
||||||
```
|
|
||||||
|
|
||||||
### 检查磁盘使用
|
|
||||||
```bash
|
|
||||||
# 查看U盘使用情况
|
|
||||||
df -h /media/ubuntu
|
|
||||||
|
|
||||||
# 查看大文件
|
|
||||||
du -sh /opt/u-claw/*
|
du -sh /opt/u-claw/*
|
||||||
```
|
```
|
||||||
|
|
||||||
## 紧急恢复
|
## If it will not boot at all
|
||||||
|
|
||||||
如果系统完全无法启动:
|
1. **Get your data off first.** Plug the drive into any Windows or Mac machine —
|
||||||
|
the Ventoy data partition is readable, so back up anything under
|
||||||
|
`u-claw-linux/` before you touch anything else.
|
||||||
|
2. **Rebuild.** Format the drive and run the four PowerShell scripts again.
|
||||||
|
3. **Ask.** Open an issue at
|
||||||
|
https://gitea.fanghe.it.com/zhenghy/u-claw/issues with what you saw on screen
|
||||||
|
and which step it failed at.
|
||||||
|
|
||||||
1. **从其他电脑访问U盘**:
|
## Getting a better result
|
||||||
- 在Windows/Mac上插入U盘
|
|
||||||
- 备份 `u-claw-linux/` 目录下的重要数据
|
|
||||||
|
|
||||||
2. **重新制作U盘**:
|
**When building**: use a good USB 3.0 drive, give persistence at least 20 GB,
|
||||||
- 格式化U盘
|
and turn off real-time antivirus scanning for the build — it slows the ISO write
|
||||||
- 重新运行所有4个PowerShell脚本
|
enormously and occasionally corrupts it.
|
||||||
|
|
||||||
3. **寻求帮助**:
|
**On first boot**: run the system updates and install the recommended drivers
|
||||||
- GitHub Issues: https://github.com/dongsheng123132/u-claw/issues
|
once, while you have network. With persistence on, you only do it once.
|
||||||
- 微信: hecare888
|
|
||||||
|
|
||||||
## 性能优化建议
|
**Long term**: if you find yourself using it daily on the same machine, a real
|
||||||
|
dual-boot install will be faster than any USB drive can be.
|
||||||
1. **制作时**:
|
|
||||||
- 使用高质量的USB 3.0 U盘
|
|
||||||
- 分配足够的持久化空间(建议20GB+)
|
|
||||||
- 关闭杀毒软件实时扫描
|
|
||||||
|
|
||||||
2. **使用时**:
|
|
||||||
- 首次启动后运行系统更新
|
|
||||||
- 安装推荐驱动
|
|
||||||
- 定期清理缓存
|
|
||||||
|
|
||||||
3. **长期使用**:
|
|
||||||
- 考虑安装到硬盘(双系统)
|
|
||||||
- 定期备份重要数据
|
|
||||||
- 关注Ubuntu安全更新
|
|
||||||
|
|||||||
@@ -7,10 +7,10 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# ── All constants defined here (independent of any other project files) ──
|
# ── All constants defined here (independent of any other project files) ──
|
||||||
NODE_VERSION="v22.16.0"
|
NODE_VERSION="v22.22.3"
|
||||||
NODE_MIRROR="https://npmmirror.com/mirrors/node"
|
NODE_MIRROR="https://nodejs.org/dist"
|
||||||
NODE_OFFICIAL="https://nodejs.org/dist"
|
NODE_OFFICIAL="https://nodejs.org/dist"
|
||||||
NPM_MIRROR="https://registry.npmmirror.com"
|
NPM_MIRROR="https://registry.npmjs.org"
|
||||||
INSTALL_DIR="/opt/u-claw"
|
INSTALL_DIR="/opt/u-claw"
|
||||||
NODE_ARCHIVE="node-${NODE_VERSION}-linux-x64.tar.xz"
|
NODE_ARCHIVE="node-${NODE_VERSION}-linux-x64.tar.xz"
|
||||||
|
|
||||||
|
|||||||
@@ -46,128 +46,128 @@ warn() {
|
|||||||
((WARNINGS++))
|
((WARNINGS++))
|
||||||
}
|
}
|
||||||
|
|
||||||
echo "1. 检查基本目录结构..."
|
echo "1. Folder layout"
|
||||||
check "安装目录存在" "[ -d '$INSTALL_DIR' ]" "Directory $INSTALL_DIR"
|
check "install directory" "[ -d '$INSTALL_DIR' ]" "Directory $INSTALL_DIR"
|
||||||
check "runtime目录" "[ -d '$INSTALL_DIR/runtime' ]" "Runtime directory"
|
check "runtime/" "[ -d '$INSTALL_DIR/runtime' ]" "Runtime directory"
|
||||||
check "core目录" "[ -d '$INSTALL_DIR/core' ]" "Core directory"
|
check "core/" "[ -d '$INSTALL_DIR/core' ]" "Core directory"
|
||||||
check "data目录" "[ -d '$INSTALL_DIR/data' ]" "Data directory"
|
check "data/" "[ -d '$INSTALL_DIR/data' ]" "Data directory"
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "2. 检查Node.js安装..."
|
echo "2. Node.js"
|
||||||
NODE_BIN="$INSTALL_DIR/runtime/node-linux-x64/bin/node"
|
NODE_BIN="$INSTALL_DIR/runtime/node-linux-x64/bin/node"
|
||||||
if [ -x "$NODE_BIN" ]; then
|
if [ -x "$NODE_BIN" ]; then
|
||||||
NODE_VERSION=$("$NODE_BIN" --version 2>/dev/null || echo "")
|
NODE_VERSION=$("$NODE_BIN" --version 2>/dev/null || echo "")
|
||||||
echo -e "✓ Node.js版本: ${GREEN}$NODE_VERSION${NC}"
|
echo -e "[OK] Node.js ${GREEN}$NODE_VERSION${NC}"
|
||||||
|
|
||||||
if [ "$NODE_VERSION" != "v22.14.0" ]; then
|
if [ "$NODE_VERSION" != "v22.22.3" ]; then
|
||||||
warn "Node.js版本" "Expected v22.14.0, found $NODE_VERSION"
|
warn "Node.js version" "Expected v22.22.3, found $NODE_VERSION"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo -e "${RED}✗ Node.js未找到或不可执行${NC}"
|
echo -e "${RED}[!!] Node.js is missing or will not run${NC}"
|
||||||
((ERRORS++))
|
((ERRORS++))
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "3. 检查OpenClaw安装..."
|
echo "3. OpenClaw"
|
||||||
if [ -d "$INSTALL_DIR/core/node_modules/openclaw" ]; then
|
if [ -d "$INSTALL_DIR/core/node_modules/openclaw" ]; then
|
||||||
echo -e "✓ OpenClaw已安装"
|
echo -e "[OK] OpenClaw is installed"
|
||||||
|
|
||||||
# 检查主要文件
|
# 检查主要文件
|
||||||
OPENCLAW_ENTRY=$(find "$INSTALL_DIR/core/node_modules/openclaw" -name "openclaw.mjs" -maxdepth 2 2>/dev/null | head -1)
|
OPENCLAW_ENTRY=$(find "$INSTALL_DIR/core/node_modules/openclaw" -name "openclaw.mjs" -maxdepth 2 2>/dev/null | head -1)
|
||||||
if [ -n "$OPENCLAW_ENTRY" ]; then
|
if [ -n "$OPENCLAW_ENTRY" ]; then
|
||||||
echo -e "✓ OpenClaw入口文件: ${GREEN}$OPENCLAW_ENTRY${NC}"
|
echo -e "[OK] entry point: ${GREEN}$OPENCLAW_ENTRY${NC}"
|
||||||
else
|
else
|
||||||
echo -e "${RED}✗ 未找到openclaw.mjs${NC}"
|
echo -e "${RED}[!!] openclaw.mjs not found${NC}"
|
||||||
((ERRORS++))
|
((ERRORS++))
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo -e "${RED}✗ OpenClaw未安装${NC}"
|
echo -e "${RED}[!!] OpenClaw is not installed${NC}"
|
||||||
((ERRORS++))
|
((ERRORS++))
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "4. 检查配置文件..."
|
echo "4. Configuration"
|
||||||
CONFIG_FILE="$INSTALL_DIR/data/.openclaw/openclaw.json"
|
CONFIG_FILE="$INSTALL_DIR/data/.openclaw/openclaw.json"
|
||||||
if [ -f "$CONFIG_FILE" ]; then
|
if [ -f "$CONFIG_FILE" ]; then
|
||||||
echo -e "✓ 配置文件存在"
|
echo -e "[OK] config file present"
|
||||||
|
|
||||||
# 检查配置格式
|
# 检查配置格式
|
||||||
if python3 -m json.tool "$CONFIG_FILE" >/dev/null 2>&1; then
|
if python3 -m json.tool "$CONFIG_FILE" >/dev/null 2>&1; then
|
||||||
echo -e "✓ 配置文件格式正确"
|
echo -e "[OK] config parses"
|
||||||
else
|
else
|
||||||
warn "配置文件" "JSON格式可能有问题"
|
warn "config file" "may not be valid JSON"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo -e "${YELLOW}⚠ 配置文件不存在(首次运行正常)${NC}"
|
echo -e "${YELLOW}[ ?] no config file yet — normal before the first run${NC}"
|
||||||
((WARNINGS++))
|
((WARNINGS++))
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "5. 检查启动脚本..."
|
echo "5. Launcher"
|
||||||
START_SCRIPT="$INSTALL_DIR/start-openclaw.sh"
|
START_SCRIPT="$INSTALL_DIR/start-openclaw.sh"
|
||||||
if [ -x "$START_SCRIPT" ]; then
|
if [ -x "$START_SCRIPT" ]; then
|
||||||
echo -e "✓ 启动脚本存在且可执行"
|
echo -e "[OK] start script present and executable"
|
||||||
else
|
else
|
||||||
if [ -f "$START_SCRIPT" ]; then
|
if [ -f "$START_SCRIPT" ]; then
|
||||||
echo -e "${YELLOW}⚠ 启动脚本存在但不可执行${NC}"
|
echo -e "${YELLOW}[ ?] start script is present but not executable${NC}"
|
||||||
chmod +x "$START_SCRIPT" 2>/dev/null && echo " 已添加执行权限"
|
chmod +x "$START_SCRIPT" 2>/dev/null && echo " added the executable bit"
|
||||||
else
|
else
|
||||||
echo -e "${RED}✗ 启动脚本未找到${NC}"
|
echo -e "${RED}[!!] start script not found${NC}"
|
||||||
((ERRORS++))
|
((ERRORS++))
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "6. 检查端口可用性..."
|
echo "6. Ports"
|
||||||
PORT_FOUND=false
|
PORT_FOUND=false
|
||||||
for port in $(seq 18789 18799); do
|
for port in $(seq 18789 18799); do
|
||||||
if ! ss -tlnp 2>/dev/null | grep -q ":$port "; then
|
if ! ss -tlnp 2>/dev/null | grep -q ":$port "; then
|
||||||
echo -e "✓ 端口 $port 可用"
|
echo -e "[OK] port $port is free"
|
||||||
PORT_FOUND=true
|
PORT_FOUND=true
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
if [ "$PORT_FOUND" = false ]; then
|
if [ "$PORT_FOUND" = false ]; then
|
||||||
warn "端口" "所有端口18789-18799都被占用"
|
warn "ports" "18789-18799 are all in use — U-Claw may already be running"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "7. 检查桌面快捷方式..."
|
echo "7. Desktop shortcut"
|
||||||
if [ -f "$HOME/Desktop/openclaw.desktop" ] || [ -f "$HOME/.local/share/applications/openclaw.desktop" ]; then
|
if [ -f "$HOME/Desktop/openclaw.desktop" ] || [ -f "$HOME/.local/share/applications/openclaw.desktop" ]; then
|
||||||
echo -e "✓ 桌面快捷方式已创建"
|
echo -e "[OK] shortcut created"
|
||||||
else
|
else
|
||||||
warn "桌面快捷方式" "未找到桌面快捷方式"
|
warn "desktop shortcut" "not found"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "============================================"
|
echo "============================================"
|
||||||
echo " 测试结果汇总"
|
echo " Results"
|
||||||
echo "============================================"
|
echo "============================================"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
if [ $ERRORS -eq 0 ]; then
|
if [ $ERRORS -eq 0 ]; then
|
||||||
echo -e "${GREEN}✅ 所有关键检查通过!${NC}"
|
echo -e "${GREEN}[OK] Everything essential checks out.${NC}"
|
||||||
echo " 安装看起来是成功的。"
|
echo " The install looks good."
|
||||||
echo ""
|
echo ""
|
||||||
echo " 启动命令:"
|
echo " Start it with:"
|
||||||
echo " bash /opt/u-claw/start-openclaw.sh"
|
echo " bash /opt/u-claw/start-openclaw.sh"
|
||||||
echo ""
|
echo ""
|
||||||
echo " 或双击桌面上的 'U-Claw AI Assistant' 图标"
|
echo " Or double-click the U-Claw AI Assistant icon on the desktop."
|
||||||
else
|
else
|
||||||
echo -e "${RED}❌ 发现 $ERRORS 个错误${NC}"
|
echo -e "${RED}[!!] $ERRORS problem(s) found${NC}"
|
||||||
echo " 请检查上述失败的项目。"
|
echo " Look at the failed checks above."
|
||||||
echo ""
|
echo ""
|
||||||
echo " 常见解决方案:"
|
echo " Usually one of these:"
|
||||||
echo " 1. 重新运行安装脚本:"
|
echo " 1. Run the install script again:"
|
||||||
echo " sudo bash /opt/u-claw/setup-openclaw.sh"
|
echo " sudo bash /opt/u-claw/setup-openclaw.sh"
|
||||||
echo " 2. 检查网络连接"
|
echo " 2. Check the network"
|
||||||
echo " 3. 查看详细日志"
|
echo " 3. Read the log"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ $WARNINGS -gt 0 ]; then
|
if [ $WARNINGS -gt 0 ]; then
|
||||||
echo -e "${YELLOW}⚠ 有 $WARNINGS 个警告${NC}"
|
echo -e "${YELLOW}[ ?] $WARNINGS warning(s)${NC}"
|
||||||
echo " 这些可能不会影响基本功能,但建议修复。"
|
echo " These probably will not stop it working, but are worth fixing."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
@@ -176,25 +176,25 @@ echo "============================================"
|
|||||||
# 提供快速修复选项
|
# 提供快速修复选项
|
||||||
if [ $ERRORS -gt 0 ]; then
|
if [ $ERRORS -gt 0 ]; then
|
||||||
echo ""
|
echo ""
|
||||||
read -rp "是否尝试自动修复问题? (y/N): " FIX
|
read -rp "Try to fix these automatically? (y/N): " FIX
|
||||||
if [[ "$FIX" =~ ^[Yy]$ ]]; then
|
if [[ "$FIX" =~ ^[Yy]$ ]]; then
|
||||||
echo ""
|
echo ""
|
||||||
echo "尝试修复..."
|
echo "Fixing…"
|
||||||
|
|
||||||
# 修复启动脚本权限
|
# 修复启动脚本权限
|
||||||
if [ -f "$START_SCRIPT" ] && [ ! -x "$START_SCRIPT" ]; then
|
if [ -f "$START_SCRIPT" ] && [ ! -x "$START_SCRIPT" ]; then
|
||||||
echo " 修复启动脚本权限..."
|
echo " Restoring the start script permissions"
|
||||||
sudo chmod +x "$START_SCRIPT"
|
sudo chmod +x "$START_SCRIPT"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 重新安装OpenClaw
|
# 重新安装OpenClaw
|
||||||
if [ ! -d "$INSTALL_DIR/core/node_modules/openclaw" ]; then
|
if [ ! -d "$INSTALL_DIR/core/node_modules/openclaw" ]; then
|
||||||
echo " 重新安装OpenClaw..."
|
echo " Reinstalling OpenClaw"
|
||||||
cd "$INSTALL_DIR/core"
|
cd "$INSTALL_DIR/core"
|
||||||
sudo npm install --registry=https://registry.npmmirror.com openclaw@latest 2>/dev/null || true
|
sudo npm install --registry=https://registry.npmjs.org openclaw@latest 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "修复完成。请重新运行此测试脚本。"
|
echo "Done. Run this check again to confirm."
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
@@ -1,132 +1,102 @@
|
|||||||
# U-Claw 一键安装 / One-Line Install
|
# One-line install
|
||||||
|
|
||||||
> 一行命令安装 OpenClaw + 技能包 + 模型配置
|
Installs OpenClaw, the skills and a model configuration in a single command,
|
||||||
> One command to install OpenClaw + skills + model config
|
without a USB drive.
|
||||||
|
|
||||||
## 安装 / Install
|
## Install
|
||||||
|
|
||||||
### Mac / Linux
|
**Mac / Linux**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -fsSL https://u-claw.org/install.sh | bash
|
curl -fsSL https://u-claw.org/install.sh | bash
|
||||||
```
|
```
|
||||||
|
|
||||||
### Windows (PowerShell 管理员)
|
**Windows** (PowerShell)
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
irm https://u-claw.org/install.ps1 | iex
|
irm https://u-claw.org/install.ps1 | iex
|
||||||
```
|
```
|
||||||
|
|
||||||
### 本地运行
|
**From a checkout**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Mac/Linux
|
bash install/install.sh # Mac / Linux
|
||||||
bash install/install.sh
|
powershell -ExecutionPolicy Bypass -File install\install.ps1 # Windows
|
||||||
|
|
||||||
# Windows
|
|
||||||
powershell -ExecutionPolicy Bypass -File install\install.ps1
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 安装流程
|
> These pipe a script from the network straight into a shell. That pattern is
|
||||||
|
> widely warned about, and on a managed corporate machine an EDR agent will
|
||||||
|
> often block it outright. If you are on a work laptop, use the USB build
|
||||||
|
> instead — that is what it is for.
|
||||||
|
|
||||||
### Mac / Linux(7 步)
|
## What it does
|
||||||
|
|
||||||
| 步骤 | 内容 | 说明 |
|
| Step | Mac / Linux | Windows |
|
||||||
|------|------|------|
|
|---|---|---|
|
||||||
| 1 | Node.js v22 | 安装到 `~/.uclaw/runtime/`,系统有 v20+ 则复用 |
|
| 1 | Node.js — into `~/.uclaw/runtime/`, or reuses the system copy if it is v20+ | same, under `%USERPROFILE%\.uclaw\` |
|
||||||
| 2 | OpenClaw | npm 安装到 `~/.uclaw/core/` |
|
| 2 | OpenClaw via npm | downloads a pre-built bundle, **checksum verified** |
|
||||||
| 3 | QQ 插件 | 非致命,失败不影响主功能 |
|
| 3 | QQ plugin — optional, a failure here does not stop anything else | same |
|
||||||
| 4 | 10 个中国技能 | 小红书、B站、微博等内容创作技能 |
|
| 4 | Skills — read from `skills/manifest.json`, fetched from the repo | same |
|
||||||
| 5 | 模型配置 | 中文交互菜单,默认 DeepSeek |
|
| 5 | Model — pick one and paste a key | same |
|
||||||
| 6 | 启动脚本 | 生成 start.command (Mac) / start.sh (Linux) |
|
| 6 | Start script — `start.command` / `start.sh` | `start.bat` |
|
||||||
| 7 | 验证 | 检查各组件安装状态 |
|
| 7 | Verify what landed | same |
|
||||||
|
|
||||||
### Windows(6 步)
|
Neither script contains any skill text of its own. Both call
|
||||||
|
`lib/install-skills.mjs`, which is the same installer the USB build and the
|
||||||
|
release pipeline use — that is what keeps the two platforms from drifting apart,
|
||||||
|
which they had badly done before.
|
||||||
|
|
||||||
| 步骤 | 内容 | 说明 |
|
## Models
|
||||||
|------|------|------|
|
|
||||||
| 1 | Node.js v22 | 下载到 `%USERPROFILE%\.uclaw\runtime\`,系统有 v20+ 则复用 |
|
|
||||||
| 2 | OpenClaw + QQ 插件 | 下载预打包 bundle,走国内镜像 |
|
|
||||||
| 3 | 10 个中国技能 | 小红书、B站、微博等内容创作技能 |
|
|
||||||
| 4 | 模型配置 | 交互菜单,默认 DeepSeek |
|
|
||||||
| 5 | 启动脚本 | 生成 start.bat |
|
|
||||||
| 6 | 验证 | 检查各组件安装状态 |
|
|
||||||
|
|
||||||
## 支持的 AI 模型
|
The menu leads with what is easiest to get working:
|
||||||
|
|
||||||
### 国内(无需翻墙)
|
| | Model | Why |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | **Gemini** | Easiest key to obtain — Google account, no card, free tier |
|
||||||
|
| 2 | **Claude** | Best all-rounder |
|
||||||
|
| 3 | **GPT** | Most widely used |
|
||||||
|
| 4 | **OpenRouter** | One key across many models |
|
||||||
|
| 5 | **SEA-LION** | Malay, Tamil and Singlish. Free, but rate-limited to 10 requests a minute |
|
||||||
|
| 6 | **Local** | Runs offline through Ollama |
|
||||||
|
| 7 | Other | Paste your own endpoint and model name |
|
||||||
|
|
||||||
| 编号 | 模型 | 说明 |
|
Others — DeepSeek, Kimi, Qwen, GLM and so on — work fine through option 7.
|
||||||
|------|------|------|
|
|
||||||
| 1 | DeepSeek | ⭐ 推荐,性价比最高 |
|
|
||||||
| 2 | Kimi/月之暗面 | 长文档 |
|
|
||||||
| 3 | 通义千问 | 有免费额度 |
|
|
||||||
| 4 | 智谱GLM | 学术场景 |
|
|
||||||
| 5 | MiniMax | 多模态 |
|
|
||||||
| 6 | 豆包 | 火山引擎 |
|
|
||||||
| 7 | 硅基流动 | 聚合多模型 |
|
|
||||||
|
|
||||||
### 海外
|
## Where things land
|
||||||
|
|
||||||
| 编号 | 模型 |
|
|
||||||
|------|------|
|
|
||||||
| 8 | Claude |
|
|
||||||
| 9 | GPT |
|
|
||||||
|
|
||||||
### 本地
|
|
||||||
|
|
||||||
| 编号 | 模型 |
|
|
||||||
|------|------|
|
|
||||||
| 10 | Ollama |
|
|
||||||
|
|
||||||
## 安装目录结构
|
|
||||||
|
|
||||||
```
|
```
|
||||||
~/.uclaw/
|
~/.uclaw/
|
||||||
├── runtime/node-{platform}/ # Node.js v22
|
├── runtime/node-{platform}/ Node.js
|
||||||
├── core/ # OpenClaw + QQ 插件
|
├── core/ OpenClaw and plugins
|
||||||
│ ├── package.json
|
│ └── node_modules/openclaw/skills/ installed skills
|
||||||
│ └── node_modules/
|
|
||||||
│ └── openclaw/skills/ ← 技能在这里
|
|
||||||
├── data/
|
├── data/
|
||||||
│ ├── .openclaw/openclaw.json ← 模型配置
|
│ ├── .openclaw/openclaw.json your model and key
|
||||||
│ ├── memory/ ← AI 记忆
|
│ ├── memory/ what the AI remembers
|
||||||
│ └── backups/ ← 备份
|
│ └── backups/
|
||||||
├── start.command (Mac) ← 启动脚本
|
└── start.command | start.sh | start.bat
|
||||||
└── start.bat (Windows) ← 启动脚本
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 技术细节
|
## Notes
|
||||||
|
|
||||||
- **Node.js**: 只装到 `~/.uclaw/runtime/`,不动系统 Node;系统已有 v20+ 则复用
|
- **Nothing touches your system Node.** It installs into `~/.uclaw/runtime/`, and
|
||||||
- **镜像**: Mac/Linux 全走 `npmmirror.com`(Node 二进制 + npm 包);Windows bundle 下载依次尝试 ghfast.top → ghproxy.net → gh.idayer.com → 直连,国内无需翻墙
|
reuses a system copy only if it is already v20 or newer.
|
||||||
- **Windows 编码**: `chcp 65001` + UTF8 输出确保中文显示
|
- **Download sources** are `nodejs.org` and `registry.npmjs.org`; the Windows
|
||||||
- **管道模式**: `curl | bash` 时自动跳过交互,使用默认配置
|
bundle comes from our own releases and its SHA256 is checked before it is
|
||||||
- **卸载**: `rm -rf ~/.uclaw` (Mac/Linux) 或删除 `%USERPROFILE%\.uclaw` (Windows)
|
unpacked. Until recently the only check was "is the file bigger than 1MB",
|
||||||
|
which is not a check.
|
||||||
|
- **Piped mode**: under `curl | bash` there is no terminal to prompt on, so it
|
||||||
|
takes the defaults and you set the model afterwards.
|
||||||
|
- **Uninstall**: `rm -rf ~/.uclaw`, or delete `%USERPROFILE%\.uclaw` on Windows.
|
||||||
|
|
||||||
## 包含技能
|
## How this relates to the other builds
|
||||||
|
|
||||||
| 技能 | 用途 |
|
|
||||||
|------|------|
|
|
||||||
| xiaohongshu-writer | 小红书笔记写作 |
|
|
||||||
| bilibili-helper | B站视频优化 |
|
|
||||||
| weibo-poster | 微博内容创作 |
|
|
||||||
| wechat-article | 微信公众号文章 |
|
|
||||||
| zhihu-writer | 知乎回答/文章 |
|
|
||||||
| douyin-script | 抖音短视频脚本 |
|
|
||||||
| china-search | 国内搜索引擎 |
|
|
||||||
| china-translate | 中英互译 |
|
|
||||||
| china-weather | 天气查询 |
|
|
||||||
| deepseek-helper | DeepSeek API 助手 |
|
|
||||||
|
|
||||||
## 与其他分发形式的关系
|
|
||||||
|
|
||||||
```
|
```
|
||||||
install/ ← 一键在线安装(本模块)
|
install/ one-line install over the network — this module
|
||||||
portable/ ← U 盘便携版
|
portable/ the USB build, the main product
|
||||||
u-claw-app/ ← Electron 桌面版
|
bootable/ a Linux USB that boots a machine with no OS
|
||||||
bootable/ ← Linux 可启动 U 盘
|
u-claw-app/ Electron desktop app — deprecated, kept for archive
|
||||||
```
|
```
|
||||||
|
|
||||||
install/ 安装的结果与 portable/ 的 Mac-Install.command 相同,
|
The result is the same as running `advanced/Mac-Install.command` from the USB
|
||||||
区别是不需要 U 盘,直接从网络下载所有组件。
|
build. The difference is only that everything is fetched from the network rather
|
||||||
|
than copied off a drive.
|
||||||
|
|||||||
@@ -22,9 +22,16 @@ $RUNTIME_DIR = "$UCLAW_DIR\runtime"
|
|||||||
$CORE_DIR = "$UCLAW_DIR\core"
|
$CORE_DIR = "$UCLAW_DIR\core"
|
||||||
$DATA_DIR = "$UCLAW_DIR\data"
|
$DATA_DIR = "$UCLAW_DIR\data"
|
||||||
$CONFIG_PATH = "$DATA_DIR\.openclaw\openclaw.json"
|
$CONFIG_PATH = "$DATA_DIR\.openclaw\openclaw.json"
|
||||||
$NODE_VERSION = "v22.16.0"
|
$NODE_VERSION = "v22.22.3"
|
||||||
$MIRROR = "https://registry.npmmirror.com"
|
$MIRROR = "https://registry.npmjs.org"
|
||||||
$NODE_MIRROR = "https://npmmirror.com/mirrors/node"
|
$NODE_MIRROR = "https://nodejs.org/dist"
|
||||||
|
$OPENCLAW_VERSION = "2026.7.1-2"
|
||||||
|
|
||||||
|
# Skills: content lives in skills/, described by skills/manifest.json.
|
||||||
|
# UCLAW_REF pins which revision irm|iex pulls skill content from.
|
||||||
|
$UCLAW_LOCALE = if ($env:UCLAW_LOCALE) { $env:UCLAW_LOCALE } else { "en" }
|
||||||
|
$UCLAW_REF = if ($env:UCLAW_REF) { $env:UCLAW_REF } else { "main" }
|
||||||
|
$SKILL_INSTALLER_URL = "https://gitea.fanghe.it.com/zhenghy/u-claw/raw/branch/$UCLAW_REF/lib/install-skills.mjs"
|
||||||
|
|
||||||
# ---- Color helpers ----
|
# ---- Color helpers ----
|
||||||
function Write-Green($msg) { Write-Host $msg -ForegroundColor Green }
|
function Write-Green($msg) { Write-Host $msg -ForegroundColor Green }
|
||||||
@@ -170,11 +177,8 @@ if (Test-Path "$CORE_DIR\node_modules\openclaw") {
|
|||||||
Write-Green " [OK] OpenClaw already installed, skip"
|
Write-Green " [OK] OpenClaw already installed, skip"
|
||||||
} else {
|
} else {
|
||||||
Write-Cyan " Downloading pre-bundled package (no npm install needed)..."
|
Write-Cyan " Downloading pre-bundled package (no npm install needed)..."
|
||||||
$BUNDLE_GITHUB_URL = "https://github.com/dongsheng123132/u-claw/releases/download/v1.0.0-bundle/openclaw-bundle.zip"
|
$BUNDLE_GITHUB_URL = "https://gitea.fanghe.it.com/zhenghy/u-claw/releases/download/v1.0.0-bundle/openclaw-bundle.zip"
|
||||||
$BUNDLE_MIRRORS = @(
|
$BUNDLE_MIRRORS = @(
|
||||||
"https://ghfast.top/https://github.com/dongsheng123132/u-claw/releases/download/v1.0.0-bundle/openclaw-bundle.zip",
|
|
||||||
"https://ghproxy.net/https://github.com/dongsheng123132/u-claw/releases/download/v1.0.0-bundle/openclaw-bundle.zip",
|
|
||||||
"https://gh.idayer.com/https://github.com/dongsheng123132/u-claw/releases/download/v1.0.0-bundle/openclaw-bundle.zip",
|
|
||||||
$BUNDLE_GITHUB_URL
|
$BUNDLE_GITHUB_URL
|
||||||
)
|
)
|
||||||
$bundleZip = "$env:TEMP\openclaw-bundle.zip"
|
$bundleZip = "$env:TEMP\openclaw-bundle.zip"
|
||||||
@@ -205,8 +209,6 @@ if (Test-Path "$CORE_DIR\node_modules\openclaw") {
|
|||||||
if (-not $downloaded) {
|
if (-not $downloaded) {
|
||||||
Write-Host " Trying curl fallback..." -ForegroundColor DarkGray
|
Write-Host " Trying curl fallback..." -ForegroundColor DarkGray
|
||||||
$curlMirrors = @(
|
$curlMirrors = @(
|
||||||
"https://ghfast.top/https://github.com/dongsheng123132/u-claw/releases/download/v1.0.0-bundle/openclaw-bundle.zip",
|
|
||||||
"https://ghproxy.net/https://github.com/dongsheng123132/u-claw/releases/download/v1.0.0-bundle/openclaw-bundle.zip",
|
|
||||||
$BUNDLE_GITHUB_URL
|
$BUNDLE_GITHUB_URL
|
||||||
)
|
)
|
||||||
foreach ($curlUrl in $curlMirrors) {
|
foreach ($curlUrl in $curlMirrors) {
|
||||||
@@ -226,6 +228,32 @@ if (Test-Path "$CORE_DIR\node_modules\openclaw") {
|
|||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Integrity check. Until this landed the installer only checked that the
|
||||||
|
# download was larger than 1MB, then extracted and ran it -- the same
|
||||||
|
# "fetch and execute, unverified" shape we warn about elsewhere.
|
||||||
|
# The publisher writes <asset>.sha256 next to the bundle; see release.yml.
|
||||||
|
$expectedHash = $null
|
||||||
|
try {
|
||||||
|
$hashBody = (Invoke-WebRequest -Uri "$BUNDLE_GITHUB_URL.sha256" -UseBasicParsing -TimeoutSec 30).Content
|
||||||
|
$expectedHash = ($hashBody -split '\s+')[0].Trim().ToLower()
|
||||||
|
} catch {
|
||||||
|
$expectedHash = $null
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($expectedHash) {
|
||||||
|
$actualHash = (Get-FileHash -Path $bundleZip -Algorithm SHA256).Hash.ToLower()
|
||||||
|
if ($actualHash -ne $expectedHash) {
|
||||||
|
Remove-Item -Force $bundleZip -ErrorAction SilentlyContinue
|
||||||
|
Write-Red " [FAIL] The downloaded file does not match its published checksum."
|
||||||
|
Write-Yellow " It was damaged in transit, or it is not the file we published."
|
||||||
|
Write-Yellow " Nothing was installed. Try again on a different network."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
Write-Green " [OK] Checksum verified"
|
||||||
|
} else {
|
||||||
|
Write-Yellow " [!] No published checksum found - integrity was not verified."
|
||||||
|
}
|
||||||
|
|
||||||
Write-Host " Extracting..." -ForegroundColor Cyan
|
Write-Host " Extracting..." -ForegroundColor Cyan
|
||||||
$tempExtract = "$env:TEMP\openclaw-bundle-extract"
|
$tempExtract = "$env:TEMP\openclaw-bundle-extract"
|
||||||
if (Test-Path $tempExtract) { Remove-Item -Recurse -Force $tempExtract }
|
if (Test-Path $tempExtract) { Remove-Item -Recurse -Force $tempExtract }
|
||||||
@@ -254,213 +282,36 @@ if (Test-Path "$CORE_DIR\node_modules\@sliverp\qqbot") {
|
|||||||
Write-Host ""
|
Write-Host ""
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Step 4: China-optimized skills (10 skills)
|
# Step 4: Install skills from the manifest
|
||||||
# ============================================================
|
# ============================================================
|
||||||
Write-Host " [3/6] Install China skills (10) ..." -ForegroundColor White
|
# Skill content lives in skills/ and is described by skills/manifest.json.
|
||||||
|
# This script carries none of it, which is what keeps the shell and PowerShell
|
||||||
|
# installers identical (tests/skills-manifest.test.mjs enforces that) and keeps
|
||||||
|
# non-ASCII bytes out of the launchers.
|
||||||
|
Write-Host " [3/6] Installing skills ..." -ForegroundColor White
|
||||||
|
|
||||||
$SKILLS_TARGET = "$CORE_DIR\node_modules\openclaw\skills"
|
$SKILLS_TARGET = "$CORE_DIR\node_modules\openclaw\skills"
|
||||||
if (-not (Test-Path $SKILLS_TARGET)) { New-Item -ItemType Directory -Force -Path $SKILLS_TARGET | Out-Null }
|
if (-not (Test-Path $SKILLS_TARGET)) { New-Item -ItemType Directory -Force -Path $SKILLS_TARGET | Out-Null }
|
||||||
|
|
||||||
$skillCount = 0
|
$skillInstaller = Join-Path $PSScriptRoot "..\lib\install-skills.mjs"
|
||||||
|
$skillTemp = $null
|
||||||
$skills = @{
|
if (-not (Test-Path $skillInstaller)) {
|
||||||
"bilibili-helper" = @'
|
# irm | iex has no checkout: fetch the installer, then let it fetch content.
|
||||||
---
|
$skillTemp = Join-Path $env:TEMP "uclaw-install-skills.mjs"
|
||||||
name: bilibili-helper
|
try {
|
||||||
description: "Bilibili content helper - video title, tags, cover design, category selection"
|
Invoke-WebRequest -Uri $SKILL_INSTALLER_URL -OutFile $skillTemp -UseBasicParsing -TimeoutSec 60
|
||||||
metadata: { "openclaw": { "emoji": "TV" } }
|
$skillInstaller = $skillTemp
|
||||||
---
|
} catch {
|
||||||
|
Write-Host " [!] Could not download the skill installer - skipping skills." -ForegroundColor Yellow
|
||||||
# Bilibili Content Helper
|
Write-Host " You can add them later from the Skill Hub." -ForegroundColor DarkGray
|
||||||
|
$skillInstaller = $null
|
||||||
Optimize video titles, descriptions, tags and covers for better recommendations on Bilibili.
|
|
||||||
|
|
||||||
## Title formulas
|
|
||||||
|
|
||||||
1. Question: "Why XX? You'll understand after watching"
|
|
||||||
2. Tutorial: "XX Tutorial | Step by step from scratch"
|
|
||||||
3. Review: "Spent XX yuan on XX, is it worth it?"
|
|
||||||
4. Challenge: "Challenge XX days using only XX"
|
|
||||||
5. Ranking: "Top 10 XX of the year"
|
|
||||||
'@
|
|
||||||
|
|
||||||
"china-search" = @'
|
|
||||||
---
|
|
||||||
name: china-search
|
|
||||||
description: "China search engines - Baidu, Sogou, Bing China search"
|
|
||||||
metadata: { "openclaw": { "emoji": "search" } }
|
|
||||||
---
|
|
||||||
|
|
||||||
# China Search Engine Helper
|
|
||||||
|
|
||||||
Search via Baidu, Sogou, Bing China using curl.
|
|
||||||
|
|
||||||
## Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -s -L "https://www.baidu.com/s?wd=keyword" -H "User-Agent: Mozilla/5.0"
|
|
||||||
curl -s -L "https://weixin.sogou.com/weixin?query=keyword" -H "User-Agent: Mozilla/5.0"
|
|
||||||
curl -s -L "https://cn.bing.com/search?q=keyword" -H "User-Agent: Mozilla/5.0"
|
|
||||||
```
|
|
||||||
'@
|
|
||||||
|
|
||||||
"china-translate" = @'
|
|
||||||
---
|
|
||||||
name: china-translate
|
|
||||||
description: "CN-EN translation - tech docs, UI localization, cultural adaptation"
|
|
||||||
metadata: { "openclaw": { "emoji": "globe" } }
|
|
||||||
---
|
|
||||||
|
|
||||||
# CN-EN Translation Helper
|
|
||||||
|
|
||||||
Professional Chinese-English bidirectional translation for tech docs and product localization.
|
|
||||||
|
|
||||||
## Common terms
|
|
||||||
|
|
||||||
| English | Chinese |
|
|
||||||
|---------|---------|
|
|
||||||
| Deploy | Bu Shu |
|
|
||||||
| Repository | Cang Ku |
|
|
||||||
| Container | Rong Qi |
|
|
||||||
| Middleware | Zhong Jian Jian |
|
|
||||||
| Token | Token / Ling Pai |
|
|
||||||
| Prompt | Ti Shi Ci |
|
|
||||||
'@
|
|
||||||
|
|
||||||
"china-weather" = @'
|
|
||||||
---
|
|
||||||
name: china-weather
|
|
||||||
description: "China city weather query - supports Chinese city names, wttr.in API"
|
|
||||||
metadata: { "openclaw": { "emoji": "sun" } }
|
|
||||||
---
|
|
||||||
|
|
||||||
# China Weather Query
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -s "wttr.in/Shenzhen?lang=zh"
|
|
||||||
curl -s "wttr.in/Beijing?format=j1"
|
|
||||||
```
|
|
||||||
'@
|
|
||||||
|
|
||||||
"deepseek-helper" = @'
|
|
||||||
---
|
|
||||||
name: deepseek-helper
|
|
||||||
description: "DeepSeek API helper - model selection, API guide, pricing info"
|
|
||||||
metadata: { "openclaw": { "emoji": "robot" } }
|
|
||||||
---
|
|
||||||
|
|
||||||
# DeepSeek API Helper
|
|
||||||
|
|
||||||
## Models
|
|
||||||
|
|
||||||
| Model | Use case | Context |
|
|
||||||
|-------|----------|---------|
|
|
||||||
| deepseek-v4-flash | Daily chat, fast and cheap | 128K |
|
|
||||||
| deepseek-v4-pro | Complex reasoning, code | 128K |
|
|
||||||
|
|
||||||
> Note: the legacy names `deepseek-chat` / `deepseek-coder` / `deepseek-reasoner` are
|
|
||||||
> no longer accepted by api.deepseek.com and return HTTP 400.
|
|
||||||
|
|
||||||
- OpenAI-compatible API, base_url: https://api.deepseek.com
|
|
||||||
- Direct access in China, API Key: https://platform.deepseek.com
|
|
||||||
'@
|
|
||||||
|
|
||||||
"douyin-script" = @'
|
|
||||||
---
|
|
||||||
name: douyin-script
|
|
||||||
description: "Douyin/Kuaishou short video script - hook, structure, hashtag strategy"
|
|
||||||
metadata: { "openclaw": { "emoji": "video" } }
|
|
||||||
---
|
|
||||||
|
|
||||||
# Douyin/Kuaishou Script Helper
|
|
||||||
|
|
||||||
## First 3-second Hook formulas
|
|
||||||
|
|
||||||
1. Counter-intuitive: "What you've been doing is actually wrong"
|
|
||||||
2. Number shock: "Only 100 yuan but better than 1000 yuan"
|
|
||||||
3. Suspense: "Guess what this is for?"
|
|
||||||
4. Emotional: "Workers fell silent after watching..."
|
|
||||||
5. Result first: "The final result is amazing!"
|
|
||||||
|
|
||||||
## Hashtags: large + medium + small topics, 5-8 total
|
|
||||||
## Best time: 7-9am, 12-2pm, 6-10pm
|
|
||||||
'@
|
|
||||||
|
|
||||||
"wechat-article" = @'
|
|
||||||
---
|
|
||||||
name: wechat-article
|
|
||||||
description: "WeChat article writing - structure, formatting, conversion optimization"
|
|
||||||
metadata: { "openclaw": { "emoji": "green" } }
|
|
||||||
---
|
|
||||||
|
|
||||||
# WeChat Article Writing Helper
|
|
||||||
|
|
||||||
## Formatting rules
|
|
||||||
|
|
||||||
- Body font 15-16px, line height 1.75-2x
|
|
||||||
- Body color #3f3f3f, accent #007AFF
|
|
||||||
- 3-5 lines per paragraph, 1 image per 300 chars
|
|
||||||
- Title under 30 chars, first 15 chars must catch attention
|
|
||||||
'@
|
|
||||||
|
|
||||||
"weibo-poster" = @'
|
|
||||||
---
|
|
||||||
name: weibo-poster
|
|
||||||
description: "Weibo content creation - 140-char optimization, trending topics, images"
|
|
||||||
metadata: { "openclaw": { "emoji": "red" } }
|
|
||||||
---
|
|
||||||
|
|
||||||
# Weibo Content Helper
|
|
||||||
|
|
||||||
## 140-char tips: Write then trim, one point per post, end with golden quote
|
|
||||||
## Images: 1 for highlight / 3 for comparison / 6 for narrative / 9 for grid
|
|
||||||
## Timing: Weekday lunch + after work, Weekend morning + evening
|
|
||||||
'@
|
|
||||||
|
|
||||||
"xiaohongshu-writer" = @'
|
|
||||||
---
|
|
||||||
name: xiaohongshu-writer
|
|
||||||
description: "Xiaohongshu note writer - title optimization, emoji strategy, hashtags"
|
|
||||||
metadata: { "openclaw": { "emoji": "book" } }
|
|
||||||
---
|
|
||||||
|
|
||||||
# Xiaohongshu Note Writer
|
|
||||||
|
|
||||||
## Title formulas
|
|
||||||
|
|
||||||
1. Numbers: "5 ways / 10 types / under 100 yuan"
|
|
||||||
2. Contrast: "3K salary vs 30K salary"
|
|
||||||
3. Review: "Personally tested, really works!"
|
|
||||||
4. Collection: "XX Collection | All in one post"
|
|
||||||
|
|
||||||
## Tips: Max 3 lines per paragraph, 1-2 emoji per paragraph, 3-8 hashtags
|
|
||||||
'@
|
|
||||||
|
|
||||||
"zhihu-writer" = @'
|
|
||||||
---
|
|
||||||
name: zhihu-writer
|
|
||||||
description: "Zhihu answer/article writing - structure, professional tone, citations"
|
|
||||||
metadata: { "openclaw": { "emoji": "pen" } }
|
|
||||||
---
|
|
||||||
|
|
||||||
# Zhihu Writing Helper
|
|
||||||
|
|
||||||
## Answer structure: Conclusion first -> Point-by-point -> Summary
|
|
||||||
## Style: Confident not arrogant, professional but accessible
|
|
||||||
## Quality: 500-3000 chars, original, reply to comments, focus on 2-3 topics
|
|
||||||
'@
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($skillName in $skills.Keys) {
|
|
||||||
$skillDir = "$SKILLS_TARGET\$skillName"
|
|
||||||
if (-not (Test-Path $skillDir)) {
|
|
||||||
New-Item -ItemType Directory -Force -Path $skillDir | Out-Null
|
|
||||||
[IO.File]::WriteAllText("$skillDir\SKILL.md", $skills[$skillName], (New-Object System.Text.UTF8Encoding $false))
|
|
||||||
$skillCount++
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Green " [OK] China skills installed (+$skillCount)"
|
if ($skillInstaller) {
|
||||||
|
& $INSTALL_NODE $skillInstaller --target $SKILLS_TARGET --locale $UCLAW_LOCALE --ref $UCLAW_REF
|
||||||
|
if ($skillTemp) { Remove-Item -Force $skillTemp -ErrorAction SilentlyContinue }
|
||||||
|
}
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|||||||
842
install/install.sh
Executable file → Normal file
842
install/install.sh
Executable file → Normal file
File diff suppressed because it is too large
Load Diff
167
lib/install-skills.mjs
Normal file
167
lib/install-skills.mjs
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// Installs skills from skills/manifest.json into a target directory.
|
||||||
|
//
|
||||||
|
// This is the only place skill content is handled. install.sh and install.ps1
|
||||||
|
// call it and contain no skill text of their own, which is what keeps the two
|
||||||
|
// platforms provably identical (see tests/skills-manifest.test.mjs) and keeps
|
||||||
|
// non-ASCII bytes out of .bat launchers (see tests/windows-launchers.test.mjs).
|
||||||
|
//
|
||||||
|
// node lib/install-skills.mjs --target <dir> [--locale en] [--persona general]
|
||||||
|
// [--source <dir>] [--ref <git-ref>]
|
||||||
|
// [--list] [--dry-run] [--json]
|
||||||
|
|
||||||
|
import { mkdirSync, readFileSync, writeFileSync, existsSync, copyFileSync } from 'node:fs';
|
||||||
|
import { dirname, join, resolve } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
// Where to fetch from lives in origin.json so a move to our own host is one
|
||||||
|
// edit. Falling back to literals keeps this working when the file is absent —
|
||||||
|
// the remote installers download this script on its own, without the repo.
|
||||||
|
const ORIGIN = (() => {
|
||||||
|
for (const dir of [join(scriptDirOf(), '..'), scriptDirOf()]) {
|
||||||
|
try { return JSON.parse(readFileSync(join(dir, 'origin.json'), 'utf8')); } catch { /* try next */ }
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})();
|
||||||
|
|
||||||
|
// A template rather than a base URL: Gitea and GitHub lay raw paths out
|
||||||
|
// differently, and hardcoding either shape makes the other impossible.
|
||||||
|
const RAW_TEMPLATE = ORIGIN?.urls?.rawTemplate
|
||||||
|
?? 'https://gitea.fanghe.it.com/zhenghy/u-claw/raw/branch/{ref}/{path}';
|
||||||
|
const DEFAULT_REF = ORIGIN?.repo?.ref ?? 'main';
|
||||||
|
function scriptDirOf() { return dirname(fileURLToPath(import.meta.url)); }
|
||||||
|
const scriptDir = scriptDirOf();
|
||||||
|
|
||||||
|
function parseArgs(argv) {
|
||||||
|
const opts = { locale: 'en', personas: [], list: false, dryRun: false, json: false, ref: DEFAULT_REF };
|
||||||
|
for (let i = 0; i < argv.length; i++) {
|
||||||
|
const arg = argv[i];
|
||||||
|
const next = () => {
|
||||||
|
const value = argv[++i];
|
||||||
|
if (value === undefined) fail(`${arg} needs a value`);
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
switch (arg) {
|
||||||
|
case '--target': opts.target = next(); break;
|
||||||
|
case '--source': opts.source = next(); break;
|
||||||
|
case '--locale': opts.locale = next(); break;
|
||||||
|
case '--persona': opts.personas.push(next()); break;
|
||||||
|
case '--ref': opts.ref = next(); break;
|
||||||
|
case '--list': opts.list = true; break;
|
||||||
|
case '--dry-run': opts.dryRun = true; break;
|
||||||
|
case '--json': opts.json = true; break;
|
||||||
|
case '--help': case '-h': usage(); process.exit(0); break;
|
||||||
|
default: fail(`unknown argument: ${arg}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return opts;
|
||||||
|
}
|
||||||
|
|
||||||
|
function usage() {
|
||||||
|
process.stdout.write(
|
||||||
|
'Usage: install-skills.mjs --target <dir> [--locale en] [--persona <id>]...\n' +
|
||||||
|
' [--source <dir>] [--ref <git-ref>] [--list] [--dry-run] [--json]\n'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fail(message) {
|
||||||
|
process.stderr.write(`install-skills: ${message}\n`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Content lives in the repo checkout or on the USB. The one-line installers have
|
||||||
|
// neither, so they fall through to fetching from GitHub at a pinned ref.
|
||||||
|
function resolveLocalSource(explicit) {
|
||||||
|
const candidates = [explicit, join(scriptDir, '..', 'skills'), join(scriptDir, 'skills')];
|
||||||
|
for (const dir of candidates) {
|
||||||
|
if (dir && existsSync(join(dir, 'manifest.json'))) return resolve(dir);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchText(url) {
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) throw new Error(`${response.status} ${response.statusText} for ${url}`);
|
||||||
|
return response.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
function rawUrl(ref, path) {
|
||||||
|
return RAW_TEMPLATE.replace('{ref}', ref).replace('{path}', `skills/${path}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectSkills(manifest, { locale, personas }) {
|
||||||
|
return manifest.skills
|
||||||
|
.filter((skill) => skill.status === 'shipping')
|
||||||
|
.filter((skill) => (skill.locales ?? []).includes(locale))
|
||||||
|
// No --persona means install everything available for the locale.
|
||||||
|
.filter((skill) => personas.length === 0 || personas.some((p) => (skill.personas ?? []).includes(p)))
|
||||||
|
.sort((a, b) => a.id.localeCompare(b.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const opts = parseArgs(process.argv.slice(2));
|
||||||
|
if (!opts.target && !opts.list) fail('--target is required (or use --list)');
|
||||||
|
|
||||||
|
const localSource = resolveLocalSource(opts.source);
|
||||||
|
const remote = localSource === null;
|
||||||
|
|
||||||
|
let manifest;
|
||||||
|
try {
|
||||||
|
manifest = JSON.parse(
|
||||||
|
remote
|
||||||
|
? await fetchText(rawUrl(opts.ref, 'manifest.json'))
|
||||||
|
: readFileSync(join(localSource, 'manifest.json'), 'utf8')
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
fail(`could not read the skill manifest: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const selected = selectSkills(manifest, opts);
|
||||||
|
|
||||||
|
if (opts.list) {
|
||||||
|
process.stdout.write(
|
||||||
|
opts.json
|
||||||
|
? `${JSON.stringify(selected.map((s) => s.id))}\n`
|
||||||
|
: `${selected.map((s) => s.id).join('\n')}\n`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let installed = 0;
|
||||||
|
let skipped = 0;
|
||||||
|
const failures = [];
|
||||||
|
|
||||||
|
for (const skill of selected) {
|
||||||
|
const destDir = join(opts.target, skill.id);
|
||||||
|
const destFile = join(destDir, 'SKILL.md');
|
||||||
|
|
||||||
|
if (existsSync(destFile)) { skipped++; continue; }
|
||||||
|
if (opts.dryRun) { installed++; continue; }
|
||||||
|
|
||||||
|
try {
|
||||||
|
mkdirSync(destDir, { recursive: true });
|
||||||
|
if (remote) {
|
||||||
|
writeFileSync(destFile, await fetchText(rawUrl(opts.ref, `${opts.locale}/${skill.id}/SKILL.md`)), 'utf8');
|
||||||
|
} else {
|
||||||
|
const src = join(localSource, opts.locale, skill.id, 'SKILL.md');
|
||||||
|
if (!existsSync(src)) throw new Error(`missing ${src}`);
|
||||||
|
copyFileSync(src, destFile);
|
||||||
|
}
|
||||||
|
installed++;
|
||||||
|
} catch (error) {
|
||||||
|
// One bad skill should not abort the install — report at the end instead.
|
||||||
|
failures.push(`${skill.id}: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.json) {
|
||||||
|
process.stdout.write(`${JSON.stringify({ installed, skipped, failed: failures })}\n`);
|
||||||
|
} else {
|
||||||
|
process.stdout.write(`skills installed: ${installed}, already present: ${skipped}\n`);
|
||||||
|
for (const failure of failures) process.stderr.write(` failed: ${failure}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failures.length > 0 && installed === 0) process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => fail(error.message));
|
||||||
48
origin.json
Normal file
48
origin.json
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
"comment": [
|
||||||
|
"Where this build points at for downloads, updates and support.",
|
||||||
|
"",
|
||||||
|
"This is a fork of github.com/dongsheng123132/u-claw, now hosted on our own",
|
||||||
|
"Gitea. Every URL the build fetches at runtime is listed here so moving hosts",
|
||||||
|
"is one edit, not a hunt.",
|
||||||
|
"",
|
||||||
|
"The one-line installers (install/install.sh, install/install.ps1) run via",
|
||||||
|
"curl|bash with no checkout, so they cannot read this file — they carry the",
|
||||||
|
"same URLs as literals. tests/origin.test.mjs fails if those literals and this",
|
||||||
|
"file ever disagree, so a migration cannot half-happen.",
|
||||||
|
"",
|
||||||
|
"rawTemplate is a template, not a base URL, because Gitea and GitHub lay raw",
|
||||||
|
"paths out differently: Gitea is /{owner}/{repo}/raw/branch/{ref}/{path},",
|
||||||
|
"GitHub is raw.githubusercontent.com/{owner}/{repo}/{ref}/{path}."
|
||||||
|
],
|
||||||
|
"repo": {
|
||||||
|
"host": "gitea.fanghe.it.com",
|
||||||
|
"sshPort": 222,
|
||||||
|
"owner": "zhenghy",
|
||||||
|
"name": "u-claw",
|
||||||
|
"ref": "main"
|
||||||
|
},
|
||||||
|
"urls": {
|
||||||
|
"web": "https://gitea.fanghe.it.com/zhenghy/u-claw",
|
||||||
|
"ssh": "ssh://git@gitea.fanghe.it.com:222/zhenghy/u-claw.git",
|
||||||
|
"rawTemplate": "https://gitea.fanghe.it.com/zhenghy/u-claw/raw/branch/{ref}/{path}",
|
||||||
|
"releases": "https://gitea.fanghe.it.com/zhenghy/u-claw/releases",
|
||||||
|
"issues": "https://gitea.fanghe.it.com/zhenghy/u-claw/issues",
|
||||||
|
"website": "https://u-claw.org"
|
||||||
|
},
|
||||||
|
"support": {
|
||||||
|
"issues": "https://gitea.fanghe.it.com/zhenghy/u-claw/issues",
|
||||||
|
"email": null,
|
||||||
|
"note": "We do not own u-claw.org — it is the upstream project's domain, so every @u-claw.org address in this tree reaches them, not us. Until we have our own address, support routes to the issue tracker above, which we do control. Set email here when we have one; tests/origin.test.mjs will find every place that still needs updating."
|
||||||
|
},
|
||||||
|
"upstream": {
|
||||||
|
"repo": "https://github.com/dongsheng123132/u-claw",
|
||||||
|
"note": "Kept for reference only. We do not send changes back — the overseas fork removes the China-market defaults that are the point of the upstream project.",
|
||||||
|
"addresses": {
|
||||||
|
"security": "hefangsheng@u-claw.org",
|
||||||
|
"author": "hello@u-claw.org",
|
||||||
|
"ciBot": "bot@u-claw.org",
|
||||||
|
"note": "Upstream's, left in place where they are factually about upstream."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
portable/NODE_VERSION
Normal file
1
portable/NODE_VERSION
Normal file
@@ -0,0 +1 @@
|
|||||||
|
v22.22.3
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="zh-CN">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<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>
|
<style>
|
||||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
body {
|
body {
|
||||||
@@ -164,90 +164,90 @@ h1 .emoji { font-size: 1.2em; }
|
|||||||
color: #999;
|
color: #999;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
<script src="data/.openclaw/locale.js"></script>
|
||||||
|
<script src="lib/i18n/messages.js"></script>
|
||||||
|
<script src="lib/i18n/i18n.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1><span class="emoji">🦞</span> U-Claw 使用指南</h1>
|
<h1><span class="emoji">🦞</span> <span data-i18n="welcome.title">U-Claw — Getting Started</span></h1>
|
||||||
<div class="subtitle">AI助手便携U盘 · 3步开始使用</div>
|
<div class="subtitle" data-i18n="welcome.subtitle">Your AI assistant on a USB drive · three steps</div>
|
||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="section-title">快速开始</div>
|
<div class="section-title" data-i18n="welcome.quickstart">Quick start</div>
|
||||||
<div class="steps">
|
<div class="steps">
|
||||||
<div class="step">
|
<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">
|
<div class="step-desc">
|
||||||
根据您的系统,双击对应的启动文件:<br><br>
|
<span data-i18n="welcome.step1_desc">Double-click the file for your computer:</span><br><br>
|
||||||
<strong>Windows 用户:</strong>双击 <code>Windows-Start.bat</code><br>
|
<strong>Windows</strong> — <code>START HERE - Windows.bat</code><br>
|
||||||
<strong>Mac 用户:</strong>双击 <code>Mac-Start.command</code>
|
<strong>Mac</strong> — <code>START HERE - Mac.command</code>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="step">
|
<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">
|
<div class="step-desc">
|
||||||
首次启动会自动打开配置页面,按照提示操作:<br>
|
<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>
|
||||||
1. 选择一个AI模型(推荐 <strong>DeepSeek</strong> - 便宜好用)<br>
|
<a href="https://aistudio.google.com/apikey" target="_blank" rel="noopener" data-i18n="welcome.step2_link">Get a free Gemini key →</a>
|
||||||
2. 填写API Key(去模型官网注册获取)<br>
|
|
||||||
3. 点击"保存配置"
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="step">
|
<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">
|
<div class="step-desc">
|
||||||
配置完成后,浏览器会自动打开AI对话界面<br>
|
<span data-i18n="welcome.step3_desc">Once the key is saved, the chat window opens by itself. That is it.</span>
|
||||||
地址:<code>http://127.0.0.1:18789</code><br>
|
|
||||||
现在可以和AI助手对话了!
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="success-box">
|
<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>
|
</div>
|
||||||
|
|
||||||
<div class="section">
|
<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-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">
|
<div class="model-item">
|
||||||
<strong>🌟 Claude</strong>
|
<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>
|
||||||
</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>
|
||||||
|
|
||||||
<div class="section">
|
<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">
|
<div class="tip-box">
|
||||||
<strong>💡 运行诊断工具</strong>
|
<strong data-i18n="welcome.trouble_lead">💡 Run the check-up tool</strong>
|
||||||
<p>
|
<p>
|
||||||
如果启动失败,双击运行诊断工具:<br>
|
<span data-i18n="welcome.trouble_desc">If U-Claw will not start, double-click:</span><br>
|
||||||
Windows: <code>Windows-Diagnose.bat</code><br>
|
Windows — <code>Windows-Diagnose.bat</code><br>
|
||||||
Mac: <code>Mac-Diagnose.command</code>
|
Mac — <code>Mac-Diagnose.command</code>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="text-align: center; margin-top: 30px;">
|
<div style="text-align: center; margin-top: 30px;">
|
||||||
<a href="Config.html" class="btn">打开配置页面</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">查看文档</a>
|
<a href="https://u-claw.org" class="btn" target="_blank" rel="noopener" data-i18n="welcome.open_docs">Documentation</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="footer">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</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
|
|
||||||
52
portable/advanced/Check-PowerShell.ps1
Normal file
52
portable/advanced/Check-PowerShell.ps1
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# Parses every .ps1 in the repository and reports syntax errors.
|
||||||
|
#
|
||||||
|
# Why this exists: most of this project's PowerShell was written on a Mac, where
|
||||||
|
# no parser is available. "It looks right" is not a check -- a single stray
|
||||||
|
# character stops the Windows installer from running at all, and there is no way
|
||||||
|
# to find that out except on Windows.
|
||||||
|
#
|
||||||
|
# Run from the repository root:
|
||||||
|
# powershell -ExecutionPolicy Bypass -File portable\advanced\Check-PowerShell.ps1
|
||||||
|
#
|
||||||
|
# Exit code is the number of files with errors, so CI can use it too.
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$root = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
|
||||||
|
$failed = 0
|
||||||
|
$checked = 0
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " Parsing every .ps1 under $root" -ForegroundColor Cyan
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
Get-ChildItem -Path $root -Recurse -Filter *.ps1 -File |
|
||||||
|
Where-Object { $_.FullName -notmatch '\\node_modules\\|\\app\\|\\\.git\\' } |
|
||||||
|
Sort-Object FullName |
|
||||||
|
ForEach-Object {
|
||||||
|
$checked++
|
||||||
|
$errors = $null
|
||||||
|
$relative = $_.FullName.Substring($root.Length).TrimStart('\')
|
||||||
|
[void][System.Management.Automation.Language.Parser]::ParseFile($_.FullName, [ref]$null, [ref]$errors)
|
||||||
|
|
||||||
|
if ($errors -and $errors.Count -gt 0) {
|
||||||
|
$failed++
|
||||||
|
Write-Host (" FAIL " + $relative) -ForegroundColor Red
|
||||||
|
foreach ($e in $errors) {
|
||||||
|
$line = $e.Extent.StartLineNumber
|
||||||
|
Write-Host (" line " + $line + ": " + $e.Message) -ForegroundColor Red
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host (" ok " + $relative) -ForegroundColor Green
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
if ($failed -eq 0) {
|
||||||
|
Write-Host " $checked file(s) parsed, no syntax errors." -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host " $failed of $checked file(s) have syntax errors." -ForegroundColor Red
|
||||||
|
Write-Host " Send the red lines above to whoever wrote them." -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
exit $failed
|
||||||
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"
|
LOG_FILE="$UCLAW_DIR/diagnostic-log.txt"
|
||||||
|
|
||||||
GREEN='\033[0;32m'
|
GREEN='\033[0;32m'
|
||||||
@@ -16,10 +16,10 @@ clear
|
|||||||
echo ""
|
echo ""
|
||||||
echo " ========================================"
|
echo " ========================================"
|
||||||
echo " U-Claw Diagnostic Tool"
|
echo " U-Claw Diagnostic Tool"
|
||||||
echo " 诊断工具"
|
echo " Check-up"
|
||||||
echo " ========================================"
|
echo " ========================================"
|
||||||
echo ""
|
echo ""
|
||||||
echo " Checking system... 正在检查系统..."
|
echo " Checking this computer…"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Clear old log
|
# Clear old log
|
||||||
@@ -135,7 +135,7 @@ echo " Log saved to: diagnostic-log.txt"
|
|||||||
echo ""
|
echo ""
|
||||||
echo " Next steps:"
|
echo " Next steps:"
|
||||||
echo " 1. Check diagnostic-log.txt for details"
|
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 " to auto-install missing dependencies"
|
||||||
echo ""
|
echo ""
|
||||||
read -p " Press Enter to exit..."
|
read -p " Press Enter to exit..."
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
UCLAW_DIR="$(cd "$(dirname "$0")" && pwd)"
|
UCLAW_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
APP_DIR="$UCLAW_DIR/app"
|
APP_DIR="$UCLAW_DIR/app"
|
||||||
INSTALL_TARGET="$HOME/.uclaw"
|
INSTALL_TARGET="$HOME/.uclaw"
|
||||||
|
|
||||||
@@ -18,16 +18,16 @@ NC='\033[0m'
|
|||||||
BOLD='\033[1m'
|
BOLD='\033[1m'
|
||||||
DIM='\033[2m'
|
DIM='\033[2m'
|
||||||
|
|
||||||
NODE_VER="v22.14.0"
|
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.3)"
|
||||||
MIRROR="https://registry.npmmirror.com"
|
MIRROR="https://registry.npmjs.org"
|
||||||
NODE_MIRROR="https://npmmirror.com/mirrors/node"
|
NODE_MIRROR="https://nodejs.org/dist"
|
||||||
|
|
||||||
clear
|
clear
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${CYAN}${BOLD}"
|
echo -e "${CYAN}${BOLD}"
|
||||||
echo " ╔══════════════════════════════════════╗"
|
echo " ╔══════════════════════════════════════╗"
|
||||||
echo " ║ U-Claw 安装到 Mac ║"
|
echo " ║ Install U-Claw on this Mac ║"
|
||||||
echo " ║ 从 U 盘离线安装 ║"
|
echo " ║ Offline, from the drive ║"
|
||||||
echo " ╚══════════════════════════════════════╝"
|
echo " ╚══════════════════════════════════════╝"
|
||||||
echo -e "${NC}"
|
echo -e "${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
@@ -35,7 +35,7 @@ echo ""
|
|||||||
# ---- Check CPU ----
|
# ---- Check CPU ----
|
||||||
ARCH=$(uname -m)
|
ARCH=$(uname -m)
|
||||||
if [ "$ARCH" = "arm64" ]; then
|
if [ "$ARCH" = "arm64" ]; then
|
||||||
echo -e " ${GREEN}Apple Silicon (M 系列) ✓${NC}"
|
echo -e " ${GREEN}Apple Silicon${NC}"
|
||||||
NODE_PLATFORM="node-mac-arm64"
|
NODE_PLATFORM="node-mac-arm64"
|
||||||
else
|
else
|
||||||
echo -e " ${YELLOW}Intel Mac${NC}"
|
echo -e " ${YELLOW}Intel Mac${NC}"
|
||||||
@@ -45,18 +45,18 @@ echo ""
|
|||||||
|
|
||||||
# ---- Check existing installation ----
|
# ---- Check existing installation ----
|
||||||
if [ -d "$INSTALL_TARGET" ]; then
|
if [ -d "$INSTALL_TARGET" ]; then
|
||||||
echo -e " ${YELLOW}检测到已有安装: $INSTALL_TARGET${NC}"
|
echo -e " ${YELLOW}Already installed at $INSTALL_TARGET${NC}"
|
||||||
read -p " 覆盖安装?(y/n): " -n 1 OVERWRITE
|
read -p " Install over it? (y/n): " -n 1 OVERWRITE
|
||||||
echo ""
|
echo ""
|
||||||
if [ "$OVERWRITE" != "y" ] && [ "$OVERWRITE" != "Y" ]; then
|
if [ "$OVERWRITE" != "y" ] && [ "$OVERWRITE" != "Y" ]; then
|
||||||
echo -e " ${DIM}已取消${NC}"
|
echo -e " ${DIM}Cancelled${NC}"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ---- Step 1: Check environment ----
|
# ---- Step 1: Check environment ----
|
||||||
echo -e " ${BOLD}[1/4] 检查环境...${NC}"
|
echo -e " ${BOLD}[1/4] Checking${NC}"
|
||||||
|
|
||||||
NEED_DOWNLOAD_NODE=false
|
NEED_DOWNLOAD_NODE=false
|
||||||
NEED_DOWNLOAD_OPENCLAW=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"
|
USB_NPM="$APP_DIR/runtime/$NODE_PLATFORM/bin/npm"
|
||||||
|
|
||||||
if [ -f "$USB_NODE" ]; then
|
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"
|
USE_NODE="usb"
|
||||||
elif command -v node >/dev/null 2>&1; then
|
elif command -v node >/dev/null 2>&1; then
|
||||||
SYS_VER=$(node --version)
|
SYS_VER=$(node --version)
|
||||||
MAJOR=$(echo "$SYS_VER" | sed 's/v//' | cut -d. -f1)
|
MAJOR=$(echo "$SYS_VER" | sed 's/v//' | cut -d. -f1)
|
||||||
if [ "$MAJOR" -ge 20 ] 2>/dev/null; then
|
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"
|
USE_NODE="system"
|
||||||
else
|
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
|
NEED_DOWNLOAD_NODE=true
|
||||||
USE_NODE="download"
|
USE_NODE="download"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo -e " ${YELLOW}Node.js: 未安装${NC}"
|
echo -e " ${YELLOW}Node.js: not installed${NC}"
|
||||||
NEED_DOWNLOAD_NODE=true
|
NEED_DOWNLOAD_NODE=true
|
||||||
USE_NODE="download"
|
USE_NODE="download"
|
||||||
fi
|
fi
|
||||||
@@ -88,10 +88,10 @@ fi
|
|||||||
# Check OpenClaw
|
# Check OpenClaw
|
||||||
USB_OPENCLAW="$APP_DIR/core/node_modules/openclaw/openclaw.mjs"
|
USB_OPENCLAW="$APP_DIR/core/node_modules/openclaw/openclaw.mjs"
|
||||||
if [ -f "$USB_OPENCLAW" ]; then
|
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"
|
USE_OPENCLAW="usb"
|
||||||
else
|
else
|
||||||
echo -e " ${YELLOW}OpenClaw: U 盘内未找到,需要在线下载${NC}"
|
echo -e " ${YELLOW}OpenClaw: not on the drive, will download it${NC}"
|
||||||
NEED_DOWNLOAD_OPENCLAW=true
|
NEED_DOWNLOAD_OPENCLAW=true
|
||||||
USE_OPENCLAW="download"
|
USE_OPENCLAW="download"
|
||||||
fi
|
fi
|
||||||
@@ -99,7 +99,7 @@ fi
|
|||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# ---- Step 2: Create install directory ----
|
# ---- 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"
|
||||||
mkdir -p "$INSTALL_TARGET/data/.openclaw"
|
mkdir -p "$INSTALL_TARGET/data/.openclaw"
|
||||||
@@ -107,27 +107,27 @@ mkdir -p "$INSTALL_TARGET/data/memory"
|
|||||||
mkdir -p "$INSTALL_TARGET/data/backups"
|
mkdir -p "$INSTALL_TARGET/data/backups"
|
||||||
|
|
||||||
# ---- Step 3: Copy/Download Node.js ----
|
# ---- 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"
|
NODE_INSTALL_DIR="$INSTALL_TARGET/runtime/$NODE_PLATFORM"
|
||||||
|
|
||||||
case $USE_NODE in
|
case $USE_NODE in
|
||||||
usb)
|
usb)
|
||||||
echo -e " ${CYAN}从 U 盘复制 Node.js...${NC}"
|
echo -e " ${CYAN}Copying Node.js from the drive${NC}"
|
||||||
mkdir -p "$NODE_INSTALL_DIR"
|
mkdir -p "$NODE_INSTALL_DIR"
|
||||||
cp -R "$APP_DIR/runtime/$NODE_PLATFORM/"* "$NODE_INSTALL_DIR/"
|
cp -R "$APP_DIR/runtime/$NODE_PLATFORM/"* "$NODE_INSTALL_DIR/"
|
||||||
chmod +x "$NODE_INSTALL_DIR/bin/node"
|
chmod +x "$NODE_INSTALL_DIR/bin/node"
|
||||||
INSTALL_NODE="$NODE_INSTALL_DIR/bin/node"
|
INSTALL_NODE="$NODE_INSTALL_DIR/bin/node"
|
||||||
INSTALL_NPM="$NODE_INSTALL_DIR/bin/npm"
|
INSTALL_NPM="$NODE_INSTALL_DIR/bin/npm"
|
||||||
echo -e " ${GREEN}Node.js 安装完成 ✓${NC}"
|
echo -e " ${GREEN}Node.js installed${NC}"
|
||||||
;;
|
;;
|
||||||
system)
|
system)
|
||||||
INSTALL_NODE="$(which node)"
|
INSTALL_NODE="$(which node)"
|
||||||
INSTALL_NPM="$(which npm)"
|
INSTALL_NPM="$(which npm)"
|
||||||
echo -e " ${GREEN}使用系统 Node.js ✓${NC}"
|
echo -e " ${GREEN}Using the system Node.js${NC}"
|
||||||
;;
|
;;
|
||||||
download)
|
download)
|
||||||
echo -e " ${CYAN}从国内镜像下载 Node.js $NODE_VER...${NC}"
|
echo -e " ${CYAN}Downloading Node.js $NODE_VER...${NC}"
|
||||||
PLATFORM_NAME="darwin-$ARCH"
|
PLATFORM_NAME="darwin-$ARCH"
|
||||||
TARBALL="node-${NODE_VER}-${PLATFORM_NAME}.tar.gz"
|
TARBALL="node-${NODE_VER}-${PLATFORM_NAME}.tar.gz"
|
||||||
URL="${NODE_MIRROR}/${NODE_VER}/${TARBALL}"
|
URL="${NODE_MIRROR}/${NODE_VER}/${TARBALL}"
|
||||||
@@ -139,26 +139,26 @@ case $USE_NODE in
|
|||||||
chmod +x "$NODE_INSTALL_DIR/bin/node"
|
chmod +x "$NODE_INSTALL_DIR/bin/node"
|
||||||
INSTALL_NODE="$NODE_INSTALL_DIR/bin/node"
|
INSTALL_NODE="$NODE_INSTALL_DIR/bin/node"
|
||||||
INSTALL_NPM="$NODE_INSTALL_DIR/bin/npm"
|
INSTALL_NPM="$NODE_INSTALL_DIR/bin/npm"
|
||||||
echo -e " ${GREEN}Node.js 下载安装完成 ✓${NC}"
|
echo -e " ${GREEN}Node.js installed${NC}"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# ---- Step 4: Copy/Download OpenClaw ----
|
# ---- 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"
|
CORE_INSTALL_DIR="$INSTALL_TARGET/core"
|
||||||
|
|
||||||
case $USE_OPENCLAW in
|
case $USE_OPENCLAW in
|
||||||
usb)
|
usb)
|
||||||
echo -e " ${CYAN}从 U 盘复制 OpenClaw + 插件...${NC}"
|
echo -e " ${CYAN}Copying OpenClaw and its plugins from the drive${NC}"
|
||||||
mkdir -p "$CORE_INSTALL_DIR"
|
mkdir -p "$CORE_INSTALL_DIR"
|
||||||
cp -R "$APP_DIR/core/"* "$CORE_INSTALL_DIR/"
|
cp -R "$APP_DIR/core/"* "$CORE_INSTALL_DIR/"
|
||||||
echo -e " ${GREEN}OpenClaw 安装完成 ✓${NC}"
|
echo -e " ${GREEN}OpenClaw installed${NC}"
|
||||||
;;
|
;;
|
||||||
download)
|
download)
|
||||||
echo -e " ${CYAN}从国内镜像下载 OpenClaw...${NC}"
|
echo -e " ${CYAN}Downloading OpenClaw${NC}"
|
||||||
mkdir -p "$CORE_INSTALL_DIR"
|
mkdir -p "$CORE_INSTALL_DIR"
|
||||||
cat > "$CORE_INSTALL_DIR/package.json" << 'PKGEOF'
|
cat > "$CORE_INSTALL_DIR/package.json" << 'PKGEOF'
|
||||||
{
|
{
|
||||||
@@ -173,7 +173,7 @@ PKGEOF
|
|||||||
cd "$CORE_INSTALL_DIR"
|
cd "$CORE_INSTALL_DIR"
|
||||||
"$INSTALL_NODE" "$INSTALL_NPM" install --registry="$MIRROR" 2>&1 | tail -3
|
"$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
|
"$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
|
esac
|
||||||
|
|
||||||
@@ -198,7 +198,7 @@ done
|
|||||||
# ---- Create launch script ----
|
# ---- Create launch script ----
|
||||||
cat > "$INSTALL_TARGET/start.command" << 'STARTEOF'
|
cat > "$INSTALL_TARGET/start.command" << 'STARTEOF'
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
DIR="$(cd "$(dirname "$0")" && pwd)"
|
DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
ARCH=$(uname -m)
|
ARCH=$(uname -m)
|
||||||
NODE_PLATFORM="node-mac-$( [ "$ARCH" = "arm64" ] && echo "arm64" || echo "x64" )"
|
NODE_PLATFORM="node-mac-$( [ "$ARCH" = "arm64" ] && echo "arm64" || echo "x64" )"
|
||||||
|
|
||||||
@@ -240,18 +240,18 @@ echo ""
|
|||||||
INSTALL_SIZE=$(du -sh "$INSTALL_TARGET" | cut -f1)
|
INSTALL_SIZE=$(du -sh "$INSTALL_TARGET" | cut -f1)
|
||||||
|
|
||||||
echo -e " ${GREEN}${BOLD}╔══════════════════════════════════════╗"
|
echo -e " ${GREEN}${BOLD}╔══════════════════════════════════════╗"
|
||||||
echo -e " ║ ✅ 安装成功! ║"
|
echo -e " ║ Installed. ║"
|
||||||
echo -e " ╚══════════════════════════════════════╝${NC}"
|
echo -e " ╚══════════════════════════════════════╝${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${BOLD}安装位置:${NC} $INSTALL_TARGET"
|
echo -e " ${BOLD}Location:${NC} $INSTALL_TARGET"
|
||||||
echo -e " ${BOLD}大小:${NC} $INSTALL_SIZE"
|
echo -e " ${BOLD}Size:${NC} $INSTALL_SIZE"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${BOLD}启动方式:${NC}"
|
echo -e " ${BOLD}To start it:${NC}"
|
||||||
echo -e " 双击 ${CYAN}$INSTALL_TARGET/start.command${NC}"
|
echo -e " Double-click ${CYAN}$INSTALL_TARGET/start.command${NC}"
|
||||||
echo -e " 或终端运行: ${CYAN}bash ~/.uclaw/start.command${NC}"
|
echo -e " or run: ${CYAN}bash ~/.uclaw/start.command${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${BOLD}首次使用:${NC}"
|
echo -e " ${BOLD}First time:${NC}"
|
||||||
echo -e " 启动后浏览器自动打开配置页面"
|
echo -e " Settings opens in your browser by itself"
|
||||||
echo -e " 选择 AI 模型 → 填写 API Key → 开始用"
|
echo -e " Paste an API key and you are ready"
|
||||||
echo ""
|
echo ""
|
||||||
read -p " 按回车关闭..."
|
read -p " Press Enter to close…"
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
# 双击运行:代理env + 直连可达 + 真发一条对话
|
# 双击运行:代理env + 直连可达 + 真发一条对话
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
UCLAW_DIR="$(cd "$(dirname "$0")" && pwd)"
|
UCLAW_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
APP_DIR="$UCLAW_DIR/app"
|
APP_DIR="$UCLAW_DIR/app"
|
||||||
CONFIG_FILE="$UCLAW_DIR/data/.openclaw/openclaw.json"
|
CONFIG_FILE="$UCLAW_DIR/data/.openclaw/openclaw.json"
|
||||||
|
|
||||||
@@ -17,8 +17,8 @@ esac
|
|||||||
# 退而求其次:用系统 node
|
# 退而求其次:用系统 node
|
||||||
if [ ! -x "$NODE_BIN" ]; then NODE_BIN="$(command -v node)"; fi
|
if [ ! -x "$NODE_BIN" ]; then NODE_BIN="$(command -v node)"; fi
|
||||||
if [ -z "$NODE_BIN" ] || [ ! -x "$NODE_BIN" ]; then
|
if [ -z "$NODE_BIN" ] || [ ! -x "$NODE_BIN" ]; then
|
||||||
echo " [错误] 找不到 Node 运行环境。请先正常启动一次 U-Claw。"
|
echo " Node.js is missing. Start U-Claw once first, then run this."
|
||||||
read -p " 按回车关闭..."
|
read -p " Press Enter to close."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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"
|
"$NODE_BIN" "$UCLAW_DIR/lib/intranet-check.mjs" "$CONFIG_FILE"
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo " 把整个窗口截图发给技术支持即可。"
|
echo " Screenshot this whole window if you need to send it to someone."
|
||||||
echo ""
|
echo ""
|
||||||
read -p " 按回车关闭..."
|
read -p " Press Enter to close."
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
# 双击运行:命令行配 Ollama / newapi 并当场实测(不碰网页设置)
|
# 双击运行:命令行配 Ollama / newapi 并当场实测(不碰网页设置)
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
UCLAW_DIR="$(cd "$(dirname "$0")" && pwd)"
|
UCLAW_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
APP_DIR="$UCLAW_DIR/app"
|
APP_DIR="$UCLAW_DIR/app"
|
||||||
CONFIG_FILE="$UCLAW_DIR/data/.openclaw/openclaw.json"
|
CONFIG_FILE="$UCLAW_DIR/data/.openclaw/openclaw.json"
|
||||||
|
|
||||||
@@ -16,8 +16,8 @@ case "$ARCH" in
|
|||||||
esac
|
esac
|
||||||
if [ ! -x "$NODE_BIN" ]; then NODE_BIN="$(command -v node)"; fi
|
if [ ! -x "$NODE_BIN" ]; then NODE_BIN="$(command -v node)"; fi
|
||||||
if [ -z "$NODE_BIN" ] || [ ! -x "$NODE_BIN" ]; then
|
if [ -z "$NODE_BIN" ] || [ ! -x "$NODE_BIN" ]; then
|
||||||
echo " [错误] 找不到 Node 运行环境。请先正常启动一次 U-Claw。"
|
echo " Node.js is missing. Start U-Claw once first, then run this."
|
||||||
read -p " 按回车关闭..."
|
read -p " Press Enter to close."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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"
|
"$NODE_BIN" "$UCLAW_DIR/lib/setup-local-model.mjs" "$CONFIG_FILE"
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
read -p " 按回车关闭..."
|
read -p " Press Enter to close."
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
# U-Claw Menu - Portable AI Agent
|
# U-Claw Menu - Portable AI Agent
|
||||||
# macOS version
|
# macOS version
|
||||||
|
|
||||||
UCLAW_DIR="$(cd "$(dirname "$0")" && pwd)"
|
UCLAW_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
APP_DIR="$UCLAW_DIR/app"
|
APP_DIR="$UCLAW_DIR/app"
|
||||||
|
|
||||||
# Migration shim: rename old core-mac to core for existing USB users
|
# Migration shim: rename old core-mac to core for existing USB users
|
||||||
@@ -60,66 +60,66 @@ run_oc() {
|
|||||||
show_menu() {
|
show_menu() {
|
||||||
clear
|
clear
|
||||||
local NODE_VER=$("$NODE_BIN" --version 2>/dev/null || echo "N/A")
|
local NODE_VER=$("$NODE_BIN" --version 2>/dev/null || echo "N/A")
|
||||||
local CFG_STATUS="${RED}未配置${NC}"
|
local CFG_STATUS="${RED}not set up${NC}"
|
||||||
[ -f "$CONFIG_PATH" ] && CFG_STATUS="${GREEN}已配置${NC}"
|
[ -f "$CONFIG_PATH" ] && CFG_STATUS="${GREEN}ready${NC}"
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${CYAN}${BOLD}╔══════════════════════════════════════╗"
|
echo -e " ${CYAN}${BOLD}╔══════════════════════════════════════╗"
|
||||||
echo -e " ║ U-Claw 虾盘 v1.1 ║"
|
echo -e " ║ U-Claw v1.1 ║"
|
||||||
echo -e " ║ Portable AI Agent ║"
|
echo -e " ║ Portable AI Agent ║"
|
||||||
echo -e " ╚══════════════════════════════════════╝${NC}"
|
echo -e " ╚══════════════════════════════════════╝${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " Node: ${GREEN}${NODE_VER}${NC} 配置: ${CFG_STATUS}"
|
echo -e " Node: ${GREEN}${NODE_VER}${NC} Model: ${CFG_STATUS}"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${WHITE}${BOLD}── 配置 ──────────────────────────────${NC}"
|
echo -e " ${WHITE}${BOLD}-- Setup ---------------------------------${NC}"
|
||||||
echo -e " ${GREEN}[1]${NC} 配置向导(选模型、填 API Key)"
|
echo -e " ${GREEN}[1]${NC} Set up a model and paste your key"
|
||||||
echo -e " ${GREEN}[2]${NC} 打开网页控制台"
|
echo -e " ${GREEN}[2]${NC} Open Settings in the browser"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${WHITE}${BOLD}── 聊天平台 ──────────────────────────${NC}"
|
echo -e " ${WHITE}${BOLD}-- Chat apps -----------------------------${NC}"
|
||||||
echo -e " ${GREEN}[3]${NC} 接入 QQ 机器人(推荐,1分钟搞定)"
|
echo -e " ${GREEN}[3]${NC} Connect WhatsApp / Telegram / Slack / Discord"
|
||||||
echo -e " ${GREEN}[4]${NC} 接入其他平台(飞书/Telegram/微信)"
|
echo -e " ${GREEN}[4]${NC} Connect a chat app used in China"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${WHITE}${BOLD}── 维护 ──────────────────────────────${NC}"
|
echo -e " ${WHITE}${BOLD}-- Maintenance ---------------------------${NC}"
|
||||||
echo -e " ${GREEN}[5]${NC} 诊断修复"
|
echo -e " ${GREEN}[5]${NC} Check and fix problems"
|
||||||
echo -e " ${GREEN}[6]${NC} 备份配置"
|
echo -e " ${GREEN}[6]${NC} Back up your settings"
|
||||||
echo -e " ${GREEN}[7]${NC} 恢复备份"
|
echo -e " ${GREEN}[7]${NC} Restore a backup"
|
||||||
echo -e " ${GREEN}[8]${NC} 系统信息"
|
echo -e " ${GREEN}[8]${NC} System information"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${WHITE}${BOLD}── 高级维护 ──────────────────────────${NC}"
|
echo -e " ${WHITE}${BOLD}-- Advanced ------------------------------${NC}"
|
||||||
echo -e " ${GREEN}[9]${NC} 杀死残留进程"
|
echo -e " ${GREEN}[9]${NC} Stop leftover processes"
|
||||||
echo -e " ${GREEN}[10]${NC} 查看日志"
|
echo -e " ${GREEN}[10]${NC} View logs"
|
||||||
echo -e " ${GREEN}[11]${NC} 恢复出厂设置"
|
echo -e " ${GREEN}[11]${NC} Reset to factory settings"
|
||||||
echo -e " ${GREEN}[12]${NC} 卸载"
|
echo -e " ${GREEN}[12]${NC} Uninstall"
|
||||||
echo -e " ${GREEN}[13]${NC} 检查更新"
|
echo -e " ${GREEN}[13]${NC} Check for updates"
|
||||||
echo -e " ${GREEN}[14]${NC} 清理空间"
|
echo -e " ${GREEN}[14]${NC} Free up space"
|
||||||
echo -e " ${GREEN}[15]${NC} 插件管理"
|
echo -e " ${GREEN}[15]${NC} Plugins"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${DIM}[0] 退出${NC}"
|
echo -e " ${DIM}[0] Quit${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
}
|
}
|
||||||
|
|
||||||
# [1] Config wizard
|
# [1] Config wizard
|
||||||
do_config() {
|
do_config() {
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${CYAN}${BOLD}━━━ 配置向导 ━━━${NC}"
|
echo -e " ${CYAN}${BOLD}--- Set up a model ---${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${WHITE}国产模型选择提示:${NC}"
|
echo -e " ${WHITE}Picking a provider in the wizard:${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " DeepSeek → 选 Custom Provider"
|
echo -e " DeepSeek → choose Custom Provider"
|
||||||
echo -e " URL: https://api.deepseek.com/v1"
|
echo -e " URL: https://api.deepseek.com/v1"
|
||||||
echo -e " 模型: deepseek-v4-flash"
|
echo -e " model: deepseek-v4-flash"
|
||||||
echo -e " Kimi → 选 Moonshot AI"
|
echo -e " Kimi → choose Moonshot AI"
|
||||||
echo -e " 通义千问 → 选 Qwen"
|
echo -e " Qwen -> choose Qwen"
|
||||||
echo -e " 豆包 → 选 Volcano Engine"
|
echo -e " Doubao -> choose Volcano Engine"
|
||||||
echo ""
|
echo ""
|
||||||
read -p " 按回车启动配置向导..."
|
read -p " Press Enter to start the wizard…"
|
||||||
run_oc onboard
|
run_oc onboard
|
||||||
}
|
}
|
||||||
|
|
||||||
# [2] Web dashboard
|
# [2] Web dashboard
|
||||||
do_dashboard() {
|
do_dashboard() {
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${CYAN}启动网页控制台...${NC}"
|
echo -e " ${CYAN}Opening Settings…${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
cd "$CORE_DIR"
|
cd "$CORE_DIR"
|
||||||
|
|
||||||
@@ -128,7 +128,7 @@ do_dashboard() {
|
|||||||
while lsof -i :$PORT >/dev/null 2>&1; do
|
while lsof -i :$PORT >/dev/null 2>&1; do
|
||||||
PORT=$((PORT + 1))
|
PORT=$((PORT + 1))
|
||||||
if [ $PORT -gt 18799 ]; then
|
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
|
return
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
@@ -142,79 +142,79 @@ do_dashboard() {
|
|||||||
sleep 0.5
|
sleep 0.5
|
||||||
if curl --noproxy '*' -s -o /dev/null "http://127.0.0.1:$PORT/" 2>/dev/null; then
|
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"
|
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
|
open "$URL" 2>/dev/null
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
echo " 关闭此窗口会停止服务"
|
echo " Closing this window stops U-Claw"
|
||||||
wait $PID
|
wait $PID
|
||||||
}
|
}
|
||||||
|
|
||||||
# [3] QQ Bot (pre-installed)
|
# [3] QQ Bot (pre-installed)
|
||||||
do_qq() {
|
do_qq() {
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${CYAN}${BOLD}━━━ 接入 QQ 机器人 ━━━${NC}"
|
echo -e " ${CYAN}${BOLD}--- Connect a QQ bot ---${NC}"
|
||||||
echo ""
|
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 ""
|
||||||
echo " 获取方式: 访问 q.qq.com → 创建机器人"
|
echo " Get them at q.qq.com by creating a bot"
|
||||||
echo ""
|
echo ""
|
||||||
read -p " AppID: " QQ_ID
|
read -p " AppID: " QQ_ID
|
||||||
read -p " AppSecret: " QQ_SECRET
|
read -p " AppSecret: " QQ_SECRET
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
if [ -z "$QQ_ID" ] || [ -z "$QQ_SECRET" ]; then
|
if [ -z "$QQ_ID" ] || [ -z "$QQ_SECRET" ]; then
|
||||||
echo -e " ${YELLOW}已取消${NC}"
|
echo -e " ${YELLOW}Cancelled${NC}"
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
run_oc channels add --channel qqbot --token "${QQ_ID}:${QQ_SECRET}" 2>&1 || true
|
run_oc channels add --channel qqbot --token "${QQ_ID}:${QQ_SECRET}" 2>&1 || true
|
||||||
echo ""
|
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
|
if [ -n "$QQ_ALLOW" ]; then
|
||||||
run_oc config set channels.qqbot.allowFrom "\"${QQ_ALLOW}\"" 2>&1 || true
|
run_oc config set channels.qqbot.allowFrom "\"${QQ_ALLOW}\"" 2>&1 || true
|
||||||
echo -e " ${GREEN}白名单已设置${NC}"
|
echo -e " ${GREEN}Allow-list set${NC}"
|
||||||
fi
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${GREEN}QQ 机器人配置完成!重启网关后生效。${NC}"
|
echo -e " ${GREEN}QQ bot configured. Restart U-Claw for it to take effect.${NC}"
|
||||||
}
|
}
|
||||||
|
|
||||||
# [4] Other platforms
|
# [4] Other platforms
|
||||||
do_platforms() {
|
do_platforms() {
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${CYAN}${BOLD}━━━ 其他聊天平台 ━━━${NC}"
|
echo -e " ${CYAN}${BOLD}--- Other chat apps ---${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${GREEN}[a]${NC} 飞书 Feishu — 企业首选"
|
echo -e " ${GREEN}[a]${NC} Feishu / Lark"
|
||||||
echo -e " ${GREEN}[b]${NC} Telegram — 海外推荐"
|
echo -e " ${GREEN}[b]${NC} Telegram"
|
||||||
echo -e " ${GREEN}[c]${NC} 微信(社区插件) — iPad协议"
|
echo -e " ${GREEN}[c]${NC} WeChat (community plugin)"
|
||||||
echo -e " ${GREEN}[d]${NC} Discord"
|
echo -e " ${GREEN}[d]${NC} Discord"
|
||||||
echo ""
|
echo ""
|
||||||
read -p " 选择 (a-d): " -n 1 CH
|
read -p " Choose (a-d): " -n 1 CH
|
||||||
echo ""
|
echo ""
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
case $CH in
|
case $CH in
|
||||||
a) echo " 飞书: 访问 open.feishu.cn/app 创建应用" ;;
|
a) echo " Feishu: create an app at open.feishu.cn/app" ;;
|
||||||
b) echo " Telegram: 找 @BotFather 创建机器人" ;;
|
b) echo " Telegram: create a bot with @BotFather" ;;
|
||||||
c)
|
c)
|
||||||
echo -e " ${YELLOW}安装微信插件...${NC}"
|
echo -e " ${YELLOW}Installing the WeChat plugin…${NC}"
|
||||||
run_oc plugins install @icesword760/openclaw-wechat 2>&1 || true
|
run_oc plugins install @icesword760/openclaw-wechat 2>&1 || true
|
||||||
;;
|
;;
|
||||||
d) echo " Discord: 访问 discord.com/developers/applications" ;;
|
d) echo " Discord: create an app at discord.com/developers/applications" ;;
|
||||||
*) echo " 无效选择" ;;
|
*) echo " Not a valid choice" ;;
|
||||||
esac
|
esac
|
||||||
echo ""
|
echo ""
|
||||||
echo " 配置完成后运行配置向导 [1] 绑定平台"
|
echo " Then run [1] to finish connecting it"
|
||||||
}
|
}
|
||||||
|
|
||||||
# [5] Doctor
|
# [5] Doctor
|
||||||
do_doctor() {
|
do_doctor() {
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${CYAN}━━━ 诊断修复 ━━━${NC}"
|
echo -e " ${CYAN}━━━ Check and fix problems ━━━${NC}"
|
||||||
echo ""
|
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
|
# [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}"
|
[ -d "$DATA_DIR/memory" ] && cp -R "$DATA_DIR/memory" "$BK/" 2>/dev/null && echo -e " ${GREEN} + memory/${NC}"
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${GREEN}备份完成: $BK${NC}"
|
echo -e " ${GREEN}Backed up to $BK${NC}"
|
||||||
echo " 大小: $(du -sh "$BK" | cut -f1)"
|
echo " Size: $(du -sh "$BK" | cut -f1)"
|
||||||
}
|
}
|
||||||
|
|
||||||
# [7] Restore
|
# [7] Restore
|
||||||
@@ -237,48 +237,48 @@ do_restore() {
|
|||||||
echo ""
|
echo ""
|
||||||
local BK_DIR="$DATA_DIR/backups"
|
local BK_DIR="$DATA_DIR/backups"
|
||||||
if [ ! -d "$BK_DIR" ] || [ -z "$(ls -A "$BK_DIR" 2>/dev/null)" ]; then
|
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
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo " 可用备份:"
|
echo " Available backups:"
|
||||||
local i=1
|
local i=1
|
||||||
for b in "$BK_DIR"/*/; do
|
for b in "$BK_DIR"/*/; do
|
||||||
echo -e " ${GREEN}[$i]${NC} $(basename "$b") ($(du -sh "$b" | cut -f1))"
|
echo -e " ${GREEN}[$i]${NC} $(basename "$b") ($(du -sh "$b" | cut -f1))"
|
||||||
i=$((i+1))
|
i=$((i+1))
|
||||||
done
|
done
|
||||||
echo ""
|
echo ""
|
||||||
read -p " 选择编号: " NUM
|
read -p " Which one? " NUM
|
||||||
|
|
||||||
local j=1
|
local j=1
|
||||||
for b in "$BK_DIR"/*/; do
|
for b in "$BK_DIR"/*/; do
|
||||||
if [ "$j" = "$NUM" ]; then
|
if [ "$j" = "$NUM" ]; then
|
||||||
[ -f "$b/openclaw.json" ] && cp "$b/openclaw.json" "$CONFIG_PATH" && 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} + 记忆已恢复${NC}"
|
[ -d "$b/memory" ] && cp -R "$b/memory" "$DATA_DIR/" && echo -e " ${GREEN} + memory restored${NC}"
|
||||||
echo -e " ${GREEN}恢复完成${NC}"
|
echo -e " ${GREEN}Restored${NC}"
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
j=$((j+1))
|
j=$((j+1))
|
||||||
done
|
done
|
||||||
echo -e " ${RED}无效选择${NC}"
|
echo -e " ${RED}Not a valid choice${NC}"
|
||||||
}
|
}
|
||||||
|
|
||||||
# [8] System info
|
# [8] System info
|
||||||
do_sysinfo() {
|
do_sysinfo() {
|
||||||
echo ""
|
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 " 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 " Node: $("$NODE_BIN" --version 2>/dev/null)"
|
||||||
echo " 路径: $UCLAW_DIR"
|
echo " Path: $UCLAW_DIR"
|
||||||
echo " 大小: $(du -sh "$UCLAW_DIR" 2>/dev/null | cut -f1)"
|
echo " Size: $(du -sh "$UCLAW_DIR" 2>/dev/null | cut -f1)"
|
||||||
echo " 磁盘: $(df -h "$UCLAW_DIR" | tail -1 | awk '{print $4 " 可用"}')"
|
echo " Disk: $(df -h "$UCLAW_DIR" | tail -1 | awk '{print $4 " free"}')"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Main loop
|
# Main loop
|
||||||
while true; do
|
while true; do
|
||||||
show_menu
|
show_menu
|
||||||
read -p " 请选择 [0-15]: " CHOICE
|
read -p " Choose [0-15]: " CHOICE
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
case $CHOICE in
|
case $CHOICE in
|
||||||
@@ -297,10 +297,10 @@ while true; do
|
|||||||
13) do_update ;;
|
13) do_update ;;
|
||||||
14) do_cleanup ;;
|
14) do_cleanup ;;
|
||||||
15) do_plugins ;;
|
15) do_plugins ;;
|
||||||
0) echo -e " ${CYAN}再见!${NC}"; exit 0 ;;
|
0) echo -e " ${CYAN}Bye.${NC}"; exit 0 ;;
|
||||||
*) echo -e " ${RED}无效选择${NC}" ;;
|
*) echo -e " ${RED}Not a valid choice${NC}" ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
read -p " 按回车返回..."
|
read -p " Press Enter to go back…"
|
||||||
done
|
done
|
||||||
@@ -2,10 +2,10 @@
|
|||||||
# ============================================================
|
# ============================================================
|
||||||
# U-Claw - Interactive CLI (macOS)
|
# U-Claw - Interactive CLI (macOS)
|
||||||
# 进阶用户:双击打开一个配置好环境的终端,可直接敲 openclaw 命令。
|
# 进阶用户:双击打开一个配置好环境的终端,可直接敲 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"
|
APP_DIR="$UCLAW_DIR/app"
|
||||||
CORE_DIR="$APP_DIR/core"
|
CORE_DIR="$APP_DIR/core"
|
||||||
DATA_DIR="$UCLAW_DIR/data"
|
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'
|
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)
|
ARCH=$(uname -m)
|
||||||
if [ "$ARCH" = "arm64" ]; then
|
if [ "$ARCH" = "arm64" ]; then
|
||||||
NODE_DIR="$APP_DIR/runtime/node-mac-arm64"
|
NODE_DIR="$APP_DIR/runtime/node-mac-arm64"
|
||||||
@@ -27,12 +27,12 @@ fi
|
|||||||
|
|
||||||
NODE_BIN="$NODE_DIR/bin/node"
|
NODE_BIN="$NODE_DIR/bin/node"
|
||||||
if [ ! -f "$NODE_BIN" ]; then
|
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..."
|
read -p " Press Enter to exit..."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
if [ ! -x "$CORE_DIR/node_modules/.bin/openclaw" ]; then
|
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..."
|
read -p " Press Enter to exit..."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
# 只读:故意不传 --fix/--repair/--force。卡住可按 Ctrl+C 安全中断。
|
# 只读:故意不传 --fix/--repair/--force。卡住可按 Ctrl+C 安全中断。
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
UCLAW_DIR="$(cd "$(dirname "$0")" && pwd)"
|
UCLAW_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
APP_DIR="$UCLAW_DIR/app"
|
APP_DIR="$UCLAW_DIR/app"
|
||||||
CORE_DIR="$APP_DIR/core"
|
CORE_DIR="$APP_DIR/core"
|
||||||
DATA_DIR="$UCLAW_DIR/data"
|
DATA_DIR="$UCLAW_DIR/data"
|
||||||
@@ -20,13 +20,13 @@ case "$ARCH" in
|
|||||||
esac
|
esac
|
||||||
if [ ! -x "$NODE_BIN" ]; then NODE_BIN="$(command -v node)"; fi
|
if [ ! -x "$NODE_BIN" ]; then NODE_BIN="$(command -v node)"; fi
|
||||||
if [ -z "$NODE_BIN" ] || [ ! -x "$NODE_BIN" ]; then
|
if [ -z "$NODE_BIN" ] || [ ! -x "$NODE_BIN" ]; then
|
||||||
echo " [错误] 找不到 Node 运行环境。"
|
echo " Node.js is missing from this drive."
|
||||||
read -p " 按回车关闭..."
|
read -p " Press Enter to close."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
if [ ! -f "$OPENCLAW_MJS" ]; then
|
if [ ! -f "$OPENCLAW_MJS" ]; then
|
||||||
echo " [错误] 找不到 OpenClaw 运行时 (app/core/node_modules/openclaw)。"
|
echo " OpenClaw is missing from this drive."
|
||||||
read -p " 按回车关闭..."
|
read -p " Press Enter to close."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -39,14 +39,14 @@ xattr -rd com.apple.quarantine "$UCLAW_DIR" 2>/dev/null || true
|
|||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo " ========================================"
|
echo " ========================================"
|
||||||
echo " OpenClaw Doctor (官方体检, 英文, 较慢)"
|
echo " OpenClaw Doctor — the upstream check. Thorough, and slow."
|
||||||
echo " ========================================"
|
echo " ========================================"
|
||||||
echo " 请确保 U-Claw 已经在运行。卡住可 Ctrl+C 中断(只读,安全)。"
|
echo " U-Claw needs to be running. Ctrl+C is safe at any point — this only reads."
|
||||||
echo " 想要快速的中文体检请改用 Mac-IntranetFix.command。"
|
echo " For a network-focused check, use Mac-IntranetFix.command instead."
|
||||||
echo ""
|
echo ""
|
||||||
read -p " 按回车开始..."
|
read -p " Press Enter to start."
|
||||||
|
|
||||||
"$NODE_BIN" "$OPENCLAW_MJS" doctor --non-interactive
|
"$NODE_BIN" "$OPENCLAW_MJS" doctor --non-interactive
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
read -p " 按回车关闭..."
|
read -p " Press Enter to close."
|
||||||
@@ -3,9 +3,9 @@ chcp 65001 >nul 2>&1
|
|||||||
title U-Claw Interactive CLI
|
title U-Claw Interactive CLI
|
||||||
|
|
||||||
REM 进阶用户:打开一个已配置好环境变量的命令行,可直接敲 openclaw 命令。
|
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 "APP_DIR=%UCLAW_DIR%app"
|
||||||
set "CORE_DIR=%APP_DIR%\core"
|
set "CORE_DIR=%APP_DIR%\core"
|
||||||
set "DATA_DIR=%UCLAW_DIR%data"
|
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%" (
|
if not exist "%NODE_BIN%" (
|
||||||
echo [ERROR] Node.js runtime not found
|
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
|
pause
|
||||||
exit /b 1
|
exit /b 1
|
||||||
)
|
)
|
||||||
|
|
||||||
if not exist "%CORE_DIR%\node_modules\.bin\openclaw.cmd" (
|
if not exist "%CORE_DIR%\node_modules\.bin\openclaw.cmd" (
|
||||||
echo [ERROR] OpenClaw not installed yet
|
echo [ERROR] OpenClaw not installed yet
|
||||||
echo Please run Windows-Start.bat once first.
|
echo Please run START HERE - Windows.bat once first.
|
||||||
pause
|
pause
|
||||||
exit /b 1
|
exit /b 1
|
||||||
)
|
)
|
||||||
@@ -4,11 +4,11 @@ title U-Claw - OpenClaw Doctor (advanced)
|
|||||||
setlocal
|
setlocal
|
||||||
|
|
||||||
REM Official OpenClaw health check (English, advanced users).
|
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 first), otherwise doctor stalls while probing a gateway that is not up yet.
|
||||||
REM Read-only: we deliberately do NOT pass --fix/--repair/--force.
|
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 "APP_DIR=%UCLAW_DIR%app"
|
||||||
set "CORE_DIR=%APP_DIR%\core"
|
set "CORE_DIR=%APP_DIR%\core"
|
||||||
set "DATA_DIR=%UCLAW_DIR%data"
|
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>
|
<!DOCTYPE html>
|
||||||
<html lang="zh-CN">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<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>
|
<style>
|
||||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
body { font-family: -apple-system, "Microsoft YaHei", sans-serif; background: #0a0a0a; color: #e0e0e0; min-height: 100vh; }
|
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 { text-align: center; margin-top: 40px; padding-top: 20px; border-top: 1px solid #222; color: #666; }
|
||||||
.footer a { color: #ff6b35; text-decoration: none; }
|
.footer a { color: #ff6b35; text-decoration: none; }
|
||||||
</style>
|
</style>
|
||||||
|
<script src="../data/.openclaw/locale.js"></script>
|
||||||
|
<script src="../lib/i18n/messages.js"></script>
|
||||||
|
<script src="../lib/i18n/i18n.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h1>🦞 <span>U-Claw</span> 虾盘 <span class="version">v1.1</span></h1>
|
<h1>🦞 <span>U-Claw</span> <span class="version">v1.1</span></h1>
|
||||||
<p>Portable AI Agent — 插上就能用的 AI 助手</p>
|
<p data-i18n="nav.tagline">Portable AI agent — plug in and go</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Status Bar -->
|
<!-- Status Bar -->
|
||||||
<div class="status-bar">
|
<div class="status-bar">
|
||||||
<div class="status-dot off" id="statusDot"></div>
|
<div class="status-dot off" id="statusDot"></div>
|
||||||
<span id="statusText">OpenClaw 未运行</span>
|
<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">打开控制台 →</a>
|
<a href="#" id="statusLink" class="status-link" style="display:none" target="_blank" data-i18n="nav.open_console">Open it →</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- OS Tabs -->
|
<!-- OS Tabs -->
|
||||||
@@ -75,29 +78,29 @@ body { font-family: -apple-system, "Microsoft YaHei", sans-serif; background: #0
|
|||||||
<!-- Mac Panel -->
|
<!-- Mac Panel -->
|
||||||
<div class="panel active" id="panel-mac">
|
<div class="panel active" id="panel-mac">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h3>🚀 快速启动</h3>
|
<h3>🚀 <span data-i18n="nav.quickstart">Quick start</span></h3>
|
||||||
<div class="step">
|
<div class="step">
|
||||||
<div class="step-num">1</div>
|
<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>
|
||||||
<div class="step">
|
<div class="step">
|
||||||
<div class="step-num">2</div>
|
<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>
|
</div>
|
||||||
<p style="margin-top:12px; color:#999;">⚠️ 首次运行如果提示"无法验证开发者":右键点击 → 打开</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;">📝 也可以直接双击 <code>Config.html</code> 打开中文配置页面</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>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h3>⚙️ 全部功能(配置/诊断/备份)</h3>
|
<h3>⚙️ <span data-i18n="nav.everything">Everything else</span></h3>
|
||||||
<p>双击 <code>Mac-Menu.command</code> 打开全功能菜单:</p>
|
<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">
|
||||||
<div class="grid-item"><h4>中文配置页面</h4><p>Config.html — 选模型、填 Key、接 QQ</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>QQ Bot 接入</h4><p>已预装,只需 AppID + Secret</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>诊断修复</h4><p>openclaw doctor 一键检测</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>备份恢复</h4><p>配置和记忆一键备份</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>技能浏览</h4><p>52 个预装技能</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>系统信息</h4><p>版本、端口、运行状态</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -105,52 +108,52 @@ body { font-family: -apple-system, "Microsoft YaHei", sans-serif; background: #0
|
|||||||
<!-- Windows Panel -->
|
<!-- Windows Panel -->
|
||||||
<div class="panel" id="panel-win">
|
<div class="panel" id="panel-win">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h3>🚀 快速启动</h3>
|
<h3>🚀 <span data-i18n="nav.quickstart">Quick start</span></h3>
|
||||||
<div class="step">
|
<div class="step">
|
||||||
<div class="step-num">1</div>
|
<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>
|
||||||
<div class="step">
|
<div class="step">
|
||||||
<div class="step-num">2</div>
|
<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>
|
</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>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h3>⚙️ 全部功能(配置/诊断/备份)</h3>
|
<h3>⚙️ <span data-i18n="nav.everything">Everything else</span></h3>
|
||||||
<p>双击 <code>Windows-Menu.bat</code> 打开全功能菜单:</p>
|
<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">
|
||||||
<div class="grid-item"><h4>配置向导</h4><p>选模型、填 API Key、选平台</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>QQ Bot 接入</h4><p>腾讯官方,3条命令搞定</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>诊断修复</h4><p>openclaw doctor 一键检测</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>备份恢复</h4><p>配置和记忆一键备份</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>技能浏览</h4><p>52 个预装技能</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>镜像配置</h4><p>npm 淘宝镜像</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Common sections -->
|
<!-- 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="card">
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
<div class="grid-item"><h4>DeepSeek</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>Kimi K2.5</h4><p>256K 上下文,长文档</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>通义千问 Qwen</h4><p>免费额度大</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>智谱 GLM</h4><p>学术、中文 NLP</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>MiniMax</h4><p>语音、多模态</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>豆包 Doubao</h4><p>字节跳动,火山引擎</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>
|
</div>
|
||||||
|
|
||||||
<div class="section-title">📖 更多资料</div>
|
<div class="section-title">📖 <span data-i18n="nav.more_title">More</span></div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="grid">
|
<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('../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('技能市场.html')" style="cursor:pointer"><h4>🎯 技能市场</h4><p>39 个精选技能</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>💬 联系我们</h4><p>微信: hecare888</p></div>
|
<div class="grid-item"><h4>💬 <span data-i18n="nav.link_contact">Contact</span></h4><p><a href="https://gitea.fanghe.it.com/zhenghy/u-claw/issues" target="_blank" rel="noopener" style="color:#ff6b35" data-i18n="nav.link_contact_d">Open an issue</a></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"><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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -195,13 +198,13 @@ async function checkStatus() {
|
|||||||
}
|
}
|
||||||
if (found) {
|
if (found) {
|
||||||
document.getElementById('statusDot').className = 'status-dot on';
|
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');
|
var link = document.getElementById('statusLink');
|
||||||
link.style.display = 'inline';
|
link.style.display = 'inline';
|
||||||
link.href = 'http://127.0.0.1:' + found + '/#token=uclaw';
|
link.href = 'http://127.0.0.1:' + found + '/#token=uclaw';
|
||||||
} else {
|
} else {
|
||||||
document.getElementById('statusDot').className = 'status-dot off';
|
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';
|
document.getElementById('statusLink').style.display = 'none';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
@echo off
|
@echo off
|
||||||
setlocal EnableDelayedExpansion
|
setlocal EnableDelayedExpansion
|
||||||
chcp 65001 >nul 2>&1
|
chcp 65001 >nul 2>&1
|
||||||
title U-Claw - Diagnostic Tool
|
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"
|
set "LOG_FILE=%UCLAW_DIR%diagnostic-log.txt"
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
@@ -27,18 +27,18 @@ for /f "tokens=2 delims==" %%v in ('wmic os get OSArchitecture /format:list 2^>n
|
|||||||
echo. >> "%LOG_FILE%"
|
echo. >> "%LOG_FILE%"
|
||||||
|
|
||||||
REM 1. Check Node.js
|
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 "NODE_BIN=%UCLAW_DIR%app\runtime\node-win-x64\node.exe"
|
||||||
set "ERROR_COUNT=0"
|
set "ERROR_COUNT=0"
|
||||||
if exist "%NODE_BIN%" (
|
if exist "%NODE_BIN%" (
|
||||||
echo [OK] Node.js found >> "%LOG_FILE%"
|
echo [OK] Node.js found >> "%LOG_FILE%"
|
||||||
for /f "tokens=*" %%v in ('"%NODE_BIN%" --version 2^>^&1') do (
|
for /f "tokens=*" %%v in ('"%NODE_BIN%" --version 2^>^&1') do (
|
||||||
echo Version: %%v >> "%LOG_FILE%"
|
echo Version: %%v >> "%LOG_FILE%"
|
||||||
echo ✓ Node.js 运行环境: %%v
|
echo [OK] Node.js: %%v
|
||||||
)
|
)
|
||||||
) else (
|
) else (
|
||||||
echo [ERROR] Node.js not found >> "%LOG_FILE%"
|
echo [ERROR] Node.js not found >> "%LOG_FILE%"
|
||||||
echo ✗ Node.js 运行环境: 缺失
|
echo [!!] Node.js: missing
|
||||||
echo Path: %NODE_BIN% >> "%LOG_FILE%"
|
echo Path: %NODE_BIN% >> "%LOG_FILE%"
|
||||||
set /a ERROR_COUNT+=1
|
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
|
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
|
REM 2. Check core directory
|
||||||
echo [2/7] 检查依赖目录...
|
echo [2/7] Checking folders
|
||||||
set "CORE_DIR=%UCLAW_DIR%app\core"
|
set "CORE_DIR=%UCLAW_DIR%app\core"
|
||||||
if exist "%CORE_DIR%" (
|
if exist "%CORE_DIR%" (
|
||||||
echo [OK] core directory exists >> "%LOG_FILE%"
|
echo [OK] core directory exists >> "%LOG_FILE%"
|
||||||
echo ✓ 依赖目录: 正常
|
echo [OK] Folders: ok
|
||||||
) else (
|
) else (
|
||||||
echo [ERROR] core directory not found >> "%LOG_FILE%"
|
echo [ERROR] core directory not found >> "%LOG_FILE%"
|
||||||
echo ✗ 依赖目录: 缺失
|
echo [!!] Folders: missing
|
||||||
set /a ERROR_COUNT+=1
|
set /a ERROR_COUNT+=1
|
||||||
)
|
)
|
||||||
|
|
||||||
REM 3. Check node_modules
|
REM 3. Check node_modules
|
||||||
echo [3/7] 检查 npm 依赖包...
|
echo [3/7] Checking dependencies
|
||||||
if exist "%CORE_DIR%\node_modules" (
|
if exist "%CORE_DIR%\node_modules" (
|
||||||
echo [OK] node_modules exists >> "%LOG_FILE%"
|
echo [OK] node_modules exists >> "%LOG_FILE%"
|
||||||
echo ✓ npm 依赖包: 已安装
|
echo [OK] Dependencies: installed
|
||||||
) else (
|
) else (
|
||||||
echo [ERROR] node_modules not found >> "%LOG_FILE%"
|
echo [ERROR] node_modules not found >> "%LOG_FILE%"
|
||||||
echo ✗ npm 依赖包: 未安装
|
echo [!!] Dependencies: not installed
|
||||||
set /a ERROR_COUNT+=1
|
set /a ERROR_COUNT+=1
|
||||||
)
|
)
|
||||||
|
|
||||||
REM 4. Check OpenClaw
|
REM 4. Check OpenClaw
|
||||||
echo [4/7] 检查 OpenClaw 核心文件...
|
echo [4/7] Checking OpenClaw
|
||||||
set "OPENCLAW_MJS=%CORE_DIR%\node_modules\openclaw\openclaw.mjs"
|
set "OPENCLAW_MJS=%CORE_DIR%\node_modules\openclaw\openclaw.mjs"
|
||||||
if exist "%OPENCLAW_MJS%" (
|
if exist "%OPENCLAW_MJS%" (
|
||||||
echo [OK] openclaw.mjs found >> "%LOG_FILE%"
|
echo [OK] openclaw.mjs found >> "%LOG_FILE%"
|
||||||
echo ✓ OpenClaw 核心: 正常
|
echo [OK] OpenClaw: ok
|
||||||
) else (
|
) else (
|
||||||
echo [ERROR] openclaw.mjs not found >> "%LOG_FILE%"
|
echo [ERROR] openclaw.mjs not found >> "%LOG_FILE%"
|
||||||
echo ✗ OpenClaw 核心: 缺失
|
echo [!!] OpenClaw: missing
|
||||||
echo Path: %OPENCLAW_MJS% >> "%LOG_FILE%"
|
echo Path: %OPENCLAW_MJS% >> "%LOG_FILE%"
|
||||||
set /a ERROR_COUNT+=1
|
set /a ERROR_COUNT+=1
|
||||||
)
|
)
|
||||||
|
|
||||||
REM 5. Check config
|
REM 5. Check config
|
||||||
echo [5/7] 检查配置文件...
|
echo [5/7] Checking settings
|
||||||
set "STATE_DIR=%UCLAW_DIR%data\.openclaw"
|
set "STATE_DIR=%UCLAW_DIR%data\.openclaw"
|
||||||
if exist "%STATE_DIR%\openclaw.json" (
|
if exist "%STATE_DIR%\openclaw.json" (
|
||||||
echo [OK] Config file exists >> "%LOG_FILE%"
|
echo [OK] Config file exists >> "%LOG_FILE%"
|
||||||
echo ✓ 配置文件: 正常
|
echo [OK] Settings file: ok
|
||||||
REM Check if model is configured
|
REM Check if model is configured
|
||||||
findstr /c:"model" "%STATE_DIR%\openclaw.json" >nul 2>&1
|
findstr /c:"model" "%STATE_DIR%\openclaw.json" >nul 2>&1
|
||||||
if !errorlevel!==0 (
|
if !errorlevel!==0 (
|
||||||
echo ✓ AI 模型: 已配置
|
echo [OK] Model: configured
|
||||||
) else (
|
) else (
|
||||||
echo ⚠ AI 模型: 未配置(首次使用请先配置)
|
echo [ ?] Model: not set up yet
|
||||||
)
|
)
|
||||||
) else (
|
) else (
|
||||||
echo [WARNING] Config not found >> "%LOG_FILE%"
|
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
|
REM 6. Check port availability
|
||||||
echo [6/7] 检查端口占用...
|
echo [6/7] Checking ports
|
||||||
set "PORT_ISSUE=0"
|
set "PORT_ISSUE=0"
|
||||||
for /l %%p in (18789,1,18799) do (
|
for /l %%p in (18789,1,18799) do (
|
||||||
netstat -an | findstr ":%%p " | findstr "LISTENING" >nul 2>&1
|
netstat -an | findstr ":%%p " | findstr "LISTENING" >nul 2>&1
|
||||||
if !errorlevel!==0 (
|
if !errorlevel!==0 (
|
||||||
echo [WARNING] Port %%p is in use >> "%LOG_FILE%"
|
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 (
|
for /f "tokens=5" %%a in ('netstat -ano ^| findstr ":%%p " ^| findstr "LISTENING"') do (
|
||||||
echo PID: %%a >> "%LOG_FILE%"
|
echo PID: %%a >> "%LOG_FILE%"
|
||||||
)
|
)
|
||||||
@@ -116,11 +116,11 @@ for /l %%p in (18789,1,18799) do (
|
|||||||
)
|
)
|
||||||
if "!PORT_ISSUE!"=="0" (
|
if "!PORT_ISSUE!"=="0" (
|
||||||
echo [OK] Ports 18789-18799 available >> "%LOG_FILE%"
|
echo [OK] Ports 18789-18799 available >> "%LOG_FILE%"
|
||||||
echo ✓ 端口 18789-18799: 全部可用
|
echo [OK] Ports 18789-18799: all free
|
||||||
)
|
)
|
||||||
|
|
||||||
REM 7. Test OpenClaw startup
|
REM 7. Test OpenClaw startup
|
||||||
echo [7/7] 测试 OpenClaw 启动...
|
echo [7/7] Testing OpenClaw
|
||||||
echo. >> "%LOG_FILE%"
|
echo. >> "%LOG_FILE%"
|
||||||
echo Testing OpenClaw startup: >> "%LOG_FILE%"
|
echo Testing OpenClaw startup: >> "%LOG_FILE%"
|
||||||
echo ---------------------------------------- >> "%LOG_FILE%"
|
echo ---------------------------------------- >> "%LOG_FILE%"
|
||||||
@@ -133,15 +133,15 @@ if exist "%NODE_BIN%" if exist "%OPENCLAW_MJS%" (
|
|||||||
cd /d "%CORE_DIR%"
|
cd /d "%CORE_DIR%"
|
||||||
for /f "tokens=*" %%v in ('"%NODE_BIN%" "%OPENCLAW_MJS%" --version 2^>^&1') do (
|
for /f "tokens=*" %%v in ('"%NODE_BIN%" "%OPENCLAW_MJS%" --version 2^>^&1') do (
|
||||||
echo %%v >> "%LOG_FILE%"
|
echo %%v >> "%LOG_FILE%"
|
||||||
echo ✓ OpenClaw 启动测试: 通过 (%%v^)
|
echo [OK] OpenClaw starts: yes (%%v^)
|
||||||
)
|
)
|
||||||
if !errorlevel! neq 0 (
|
if !errorlevel! neq 0 (
|
||||||
echo ✗ OpenClaw 启动测试: 失败
|
echo [!!] OpenClaw starts: no
|
||||||
echo [ERROR] OpenClaw failed to start >> "%LOG_FILE%"
|
echo [ERROR] OpenClaw failed to start >> "%LOG_FILE%"
|
||||||
set /a ERROR_COUNT+=1
|
set /a ERROR_COUNT+=1
|
||||||
)
|
)
|
||||||
) else (
|
) else (
|
||||||
echo ⚠ OpenClaw 启动测试: 跳过(文件缺失)
|
echo [ ?] OpenClaw starts: skipped, files missing
|
||||||
echo [SKIP] Cannot test - required files missing >> "%LOG_FILE%"
|
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 Error count: !ERROR_COUNT! >> "%LOG_FILE%"
|
||||||
|
|
||||||
echo ========================================
|
echo ========================================
|
||||||
echo 诊断完成
|
echo Check complete
|
||||||
echo ========================================
|
echo ========================================
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
if !ERROR_COUNT!==0 (
|
if !ERROR_COUNT!==0 (
|
||||||
echo ✅ 检查结果: 全部正常!
|
echo [OK] Everything checks out.
|
||||||
echo 所有必需的组件都已就绪,可以正常使用。
|
echo Every component U-Claw needs is present.
|
||||||
echo.
|
echo.
|
||||||
echo 下一步:
|
echo Next:
|
||||||
echo - 双击 Windows-Start.bat 启动服务
|
echo - Double-click START HERE - Windows.bat to run it
|
||||||
echo - 或双击 Config.html 配置 AI 模型
|
echo - Settings opens by itself the first time
|
||||||
) else (
|
) else (
|
||||||
echo ❌ 检查结果: 发现 !ERROR_COUNT! 个问题
|
echo [!!] Found !ERROR_COUNT! problem^(s^).
|
||||||
echo.
|
echo.
|
||||||
echo 解决方案:
|
echo What to do:
|
||||||
echo 1. 查看 diagnostic-log.txt 了解详细错误
|
echo 1. Open diagnostic-log.txt for the details
|
||||||
echo 2. 尝试重新运行 Windows-Start.bat
|
echo 2. Run START HERE - Windows.bat again
|
||||||
echo (会自动安装缺失的依赖)
|
echo it reinstalls anything missing by itself
|
||||||
echo 3. 如问题仍然存在,访问:
|
echo 3. Still stuck? See:
|
||||||
echo github.com/dongsheng123132/u-claw
|
echo gitea.fanghe.it.com/zhenghy/u-claw
|
||||||
)
|
)
|
||||||
echo.
|
echo.
|
||||||
echo 诊断日志已保存: diagnostic-log.txt
|
echo Saved a log to diagnostic-log.txt
|
||||||
echo.
|
echo.
|
||||||
pause
|
pause
|
||||||
@@ -1,62 +1,62 @@
|
|||||||
@echo off
|
@echo off
|
||||||
setlocal EnableDelayedExpansion
|
setlocal EnableDelayedExpansion
|
||||||
chcp 65001 >nul 2>&1
|
chcp 65001 >nul 2>&1
|
||||||
title U-Claw - Install to Windows
|
title U-Claw - Install to Windows
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo ========================================
|
echo ========================================
|
||||||
echo U-Claw 安装到 Windows
|
echo Install U-Claw on this PC
|
||||||
echo 从 U 盘离线安装
|
echo Offline, from the drive
|
||||||
echo ========================================
|
echo ========================================
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
set "UCLAW_DIR=%~dp0"
|
for %%I in ("%~dp0..") do set "UCLAW_DIR=%%~fI\"
|
||||||
set "APP_DIR=%UCLAW_DIR%app"
|
set "APP_DIR=%UCLAW_DIR%app"
|
||||||
set "INSTALL_TARGET=%USERPROFILE%\.uclaw"
|
set "INSTALL_TARGET=%USERPROFILE%\.uclaw"
|
||||||
set "MIRROR=https://registry.npmmirror.com"
|
set "MIRROR=https://registry.npmjs.org"
|
||||||
set "NODE_MIRROR=https://npmmirror.com/mirrors/node"
|
set "NODE_MIRROR=https://nodejs.org/dist"
|
||||||
set "NODE_VER=v22.22.1"
|
set "NODE_VER=v22.22.3"
|
||||||
|
|
||||||
REM ---- Step 1: Check environment ----
|
REM ---- Step 1: Check environment ----
|
||||||
echo [1/4] 检查环境...
|
echo [1/4] Checking
|
||||||
|
|
||||||
set "USE_NODE=none"
|
set "USE_NODE=none"
|
||||||
set "USB_NODE=%APP_DIR%\runtime\node-win-x64\node.exe"
|
set "USB_NODE=%APP_DIR%\runtime\node-win-x64\node.exe"
|
||||||
|
|
||||||
if exist "%USB_NODE%" (
|
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"
|
set "USE_NODE=usb"
|
||||||
) else (
|
) else (
|
||||||
where node >nul 2>&1
|
where node >nul 2>&1
|
||||||
if !errorlevel!==0 (
|
if !errorlevel!==0 (
|
||||||
for /f "tokens=*" %%v in ('node --version') do (
|
for /f "tokens=*" %%v in ('node --version') do (
|
||||||
echo Node.js: 检查系统版本 %%v
|
echo Node.js: checking the system copy, %%v
|
||||||
set "SYS_VER=%%v"
|
set "SYS_VER=%%v"
|
||||||
)
|
)
|
||||||
REM Use only supported LTS majors. Newer Current releases can break native deps.
|
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"
|
for /f "tokens=1 delims=." %%m in ("!SYS_VER:v=!") do set "MAJOR=%%m"
|
||||||
if !MAJOR! equ 20 (
|
if !MAJOR! equ 20 (
|
||||||
echo Node.js: 使用系统的 !SYS_VER!
|
echo Node.js: using the system copy, !SYS_VER!
|
||||||
set "USE_NODE=system"
|
set "USE_NODE=system"
|
||||||
) else if !MAJOR! equ 22 (
|
) else if !MAJOR! equ 22 (
|
||||||
echo Node.js: 使用系统的 !SYS_VER!
|
echo Node.js: using the system copy, !SYS_VER!
|
||||||
set "USE_NODE=system"
|
set "USE_NODE=system"
|
||||||
) else (
|
) 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"
|
set "USE_NODE=download"
|
||||||
)
|
)
|
||||||
) else (
|
) else (
|
||||||
echo Node.js: 未安装
|
echo Node.js: not installed
|
||||||
set "USE_NODE=download"
|
set "USE_NODE=download"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
set "USB_OPENCLAW=%APP_DIR%\core\node_modules\openclaw\openclaw.mjs"
|
set "USB_OPENCLAW=%APP_DIR%\core\node_modules\openclaw\openclaw.mjs"
|
||||||
if exist "%USB_OPENCLAW%" (
|
if exist "%USB_OPENCLAW%" (
|
||||||
echo OpenClaw: 使用 U 盘内的
|
echo OpenClaw: using the copy on the drive
|
||||||
set "USE_OPENCLAW=usb"
|
set "USE_OPENCLAW=usb"
|
||||||
) else (
|
) else (
|
||||||
echo OpenClaw: U 盘内未找到,需要在线下载
|
echo OpenClaw: not on the drive, will download it
|
||||||
set "USE_OPENCLAW=download"
|
set "USE_OPENCLAW=download"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -64,10 +64,10 @@ echo.
|
|||||||
|
|
||||||
REM ---- Step 2: Check existing ----
|
REM ---- Step 2: Check existing ----
|
||||||
if exist "%INSTALL_TARGET%" (
|
if exist "%INSTALL_TARGET%" (
|
||||||
echo 检测到已有安装: %INSTALL_TARGET%
|
echo Already installed: %INSTALL_TARGET%
|
||||||
set /p OVERWRITE=" 覆盖安装?(y/n): "
|
set /p OVERWRITE=" Install over it?(y/n): "
|
||||||
if /i not "!OVERWRITE!"=="y" (
|
if /i not "!OVERWRITE!"=="y" (
|
||||||
echo 已取消
|
echo Cancelled
|
||||||
pause
|
pause
|
||||||
exit /b 0
|
exit /b 0
|
||||||
)
|
)
|
||||||
@@ -75,7 +75,7 @@ if exist "%INSTALL_TARGET%" (
|
|||||||
)
|
)
|
||||||
|
|
||||||
REM ---- Step 3: Create directories ----
|
REM ---- Step 3: Create directories ----
|
||||||
echo [2/4] 创建安装目录...
|
echo [2/4] Creating folders
|
||||||
mkdir "%INSTALL_TARGET%" 2>nul
|
mkdir "%INSTALL_TARGET%" 2>nul
|
||||||
mkdir "%INSTALL_TARGET%\data\.openclaw" 2>nul
|
mkdir "%INSTALL_TARGET%\data\.openclaw" 2>nul
|
||||||
mkdir "%INSTALL_TARGET%\data\memory" 2>nul
|
mkdir "%INSTALL_TARGET%\data\memory" 2>nul
|
||||||
@@ -84,20 +84,20 @@ mkdir "%INSTALL_TARGET%\data\logs" 2>nul
|
|||||||
echo.
|
echo.
|
||||||
|
|
||||||
REM ---- Step 4: Copy/Download Node.js ----
|
REM ---- Step 4: Copy/Download Node.js ----
|
||||||
echo [3/4] 安装 Node.js...
|
echo [3/4] Installing Node.js
|
||||||
|
|
||||||
if "!USE_NODE!"=="usb" (
|
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
|
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_NODE=%INSTALL_TARGET%\runtime\node-win-x64\node.exe"
|
||||||
set "INSTALL_NPM=%INSTALL_TARGET%\runtime\node-win-x64\npm.cmd"
|
set "INSTALL_NPM=%INSTALL_TARGET%\runtime\node-win-x64\npm.cmd"
|
||||||
echo Node.js 安装完成!
|
echo Node.js Installed!
|
||||||
) else if "!USE_NODE!"=="system" (
|
) else if "!USE_NODE!"=="system" (
|
||||||
set "INSTALL_NODE=node"
|
set "INSTALL_NODE=node"
|
||||||
set "INSTALL_NPM=npm"
|
set "INSTALL_NPM=npm"
|
||||||
echo 使用系统 Node.js
|
echo Using the system Node.js
|
||||||
) else (
|
) else (
|
||||||
echo 从国内镜像下载 Node.js %NODE_VER%...
|
echo Downloading Node.js %NODE_VER%
|
||||||
set "NODE_ZIP=node-%NODE_VER%-win-x64.zip"
|
set "NODE_ZIP=node-%NODE_VER%-win-x64.zip"
|
||||||
set "NODE_URL=%NODE_MIRROR%/%NODE_VER%/!NODE_ZIP!"
|
set "NODE_URL=%NODE_MIRROR%/%NODE_VER%/!NODE_ZIP!"
|
||||||
echo URL: !NODE_URL!
|
echo URL: !NODE_URL!
|
||||||
@@ -106,13 +106,13 @@ if "!USE_NODE!"=="usb" (
|
|||||||
REM Download using curl (available on Windows 10+)
|
REM Download using curl (available on Windows 10+)
|
||||||
curl -# -L "!NODE_URL!" -o "%TEMP%\!NODE_ZIP!"
|
curl -# -L "!NODE_URL!" -o "%TEMP%\!NODE_ZIP!"
|
||||||
if !errorlevel! neq 0 (
|
if !errorlevel! neq 0 (
|
||||||
echo [ERROR] 下载失败!请检查网络连接
|
echo [ERROR] Download failed. Check your network connection.
|
||||||
echo 也可以手动下载: !NODE_URL!
|
echo You can also download it yourself: !NODE_URL!
|
||||||
pause
|
pause
|
||||||
exit /b 1
|
exit /b 1
|
||||||
)
|
)
|
||||||
|
|
||||||
echo 解压中...
|
echo Unpacking
|
||||||
mkdir "%INSTALL_TARGET%\runtime\node-win-x64" 2>nul
|
mkdir "%INSTALL_TARGET%\runtime\node-win-x64" 2>nul
|
||||||
powershell -command "Expand-Archive -Path '%TEMP%\!NODE_ZIP!' -DestinationPath '%TEMP%\node-extract' -Force"
|
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
|
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_NODE=%INSTALL_TARGET%\runtime\node-win-x64\node.exe"
|
||||||
set "INSTALL_NPM=%INSTALL_TARGET%\runtime\node-win-x64\npm.cmd"
|
set "INSTALL_NPM=%INSTALL_TARGET%\runtime\node-win-x64\npm.cmd"
|
||||||
echo Node.js 下载安装完成!
|
echo Node.js installed
|
||||||
)
|
)
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
REM ---- Step 5: Copy/Download OpenClaw ----
|
REM ---- Step 5: Copy/Download OpenClaw ----
|
||||||
echo [4/4] 安装 OpenClaw...
|
echo [4/4] Installing OpenClaw
|
||||||
|
|
||||||
if "!USE_OPENCLAW!"=="usb" (
|
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
|
xcopy /s /e /q /y "%APP_DIR%\core" "%INSTALL_TARGET%\core\" >nul
|
||||||
echo OpenClaw 安装完成!
|
echo OpenClaw Installed!
|
||||||
) else (
|
) else (
|
||||||
echo 从国内镜像下载 OpenClaw...
|
echo Downloading OpenClaw
|
||||||
mkdir "%INSTALL_TARGET%\core" 2>nul
|
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"
|
(echo {"name":"u-claw-core","version":"1.0.0","private":true,"dependencies":{"openclaw":"latest"}})>"%INSTALL_TARGET%\core\package.json"
|
||||||
cd /d "%INSTALL_TARGET%\core"
|
cd /d "%INSTALL_TARGET%\core"
|
||||||
call "!INSTALL_NPM!" install --registry=%MIRROR%
|
call "!INSTALL_NPM!" install --registry=%MIRROR%
|
||||||
call "!INSTALL_NPM!" install @sliverp/qqbot@latest --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"
|
set "QQ_DIR=%INSTALL_TARGET%\core\node_modules\@sliverp\qqbot"
|
||||||
if exist "!QQ_DIR!" (
|
if exist "!QQ_DIR!" (
|
||||||
if not exist "!QQ_DIR!\dist\index.js" (
|
if not exist "!QQ_DIR!\dist\index.js" (
|
||||||
echo 编译 QQbot 插件运行文件...
|
echo Building the QQ plugin
|
||||||
pushd "!QQ_DIR!"
|
pushd "!QQ_DIR!"
|
||||||
call "!INSTALL_NPM!" install --include=dev --registry=%MIRROR% >nul 2>&1
|
call "!INSTALL_NPM!" install --include=dev --registry=%MIRROR% >nul 2>&1
|
||||||
call "!INSTALL_NPM!" run build >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!\node_modules\openclaw" rmdir /s /q "!QQ_DIR!\node_modules\openclaw" 2>nul
|
||||||
if exist "!QQ_DIR!\dist\index.js" (
|
if exist "!QQ_DIR!\dist\index.js" (
|
||||||
echo QQbot 插件运行文件已就绪!
|
echo QQ plugin ready
|
||||||
) else (
|
) 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 ========================================
|
||||||
echo 安装成功!
|
echo Installed!
|
||||||
echo ========================================
|
echo ========================================
|
||||||
echo.
|
echo.
|
||||||
echo 安装位置: %INSTALL_TARGET%
|
echo Location: %INSTALL_TARGET%
|
||||||
echo.
|
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 启动方式:
|
echo To start it:
|
||||||
echo 双击 %INSTALL_TARGET%\start.bat
|
echo Double-click %INSTALL_TARGET%\start.bat
|
||||||
echo.
|
echo.
|
||||||
echo 首次使用:
|
echo First time:
|
||||||
echo 启动后浏览器自动打开配置页面
|
echo Settings opens in your browser by itself
|
||||||
echo 选择 AI 模型 - 填写 API Key - 开始用
|
echo Paste an API key and you are ready
|
||||||
echo.
|
echo.
|
||||||
pause
|
pause
|
||||||
@@ -3,13 +3,13 @@ chcp 65001 >nul 2>&1
|
|||||||
title U-Claw - Intranet Check
|
title U-Claw - Intranet Check
|
||||||
setlocal
|
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 "NODE_BIN=%UCLAW_DIR%app\runtime\node-win-x64\node.exe"
|
||||||
set "CONFIG_PATH=%UCLAW_DIR%data\.openclaw\openclaw.json"
|
set "CONFIG_PATH=%UCLAW_DIR%data\.openclaw\openclaw.json"
|
||||||
|
|
||||||
if not exist "%NODE_BIN%" (
|
if not exist "%NODE_BIN%" (
|
||||||
echo [ERROR] Node runtime not found.
|
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.
|
echo.
|
||||||
pause
|
pause
|
||||||
exit /b 1
|
exit /b 1
|
||||||
@@ -3,13 +3,13 @@ chcp 65001 >nul 2>&1
|
|||||||
title U-Claw - Local / Intranet Model Setup
|
title U-Claw - Local / Intranet Model Setup
|
||||||
setlocal
|
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 "NODE_BIN=%UCLAW_DIR%app\runtime\node-win-x64\node.exe"
|
||||||
set "CONFIG_PATH=%UCLAW_DIR%data\.openclaw\openclaw.json"
|
set "CONFIG_PATH=%UCLAW_DIR%data\.openclaw\openclaw.json"
|
||||||
|
|
||||||
if not exist "%NODE_BIN%" (
|
if not exist "%NODE_BIN%" (
|
||||||
echo [ERROR] Node runtime not found.
|
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.
|
echo.
|
||||||
pause
|
pause
|
||||||
exit /b 1
|
exit /b 1
|
||||||
@@ -3,7 +3,7 @@ setlocal EnableDelayedExpansion
|
|||||||
chcp 65001 >nul 2>&1
|
chcp 65001 >nul 2>&1
|
||||||
title U-Claw Menu
|
title U-Claw Menu
|
||||||
|
|
||||||
set "UCLAW_DIR=%~dp0"
|
for %%I in ("%~dp0..") do set "UCLAW_DIR=%%~fI\"
|
||||||
set "APP_DIR=%UCLAW_DIR%app"
|
set "APP_DIR=%UCLAW_DIR%app"
|
||||||
|
|
||||||
REM Migration shim: rename old core-win to core for existing USB users
|
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 Current version: %CUR_VER%
|
||||||
|
|
||||||
echo Fetching latest version...
|
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!"=="" (
|
if "!LATEST_VER!"=="" (
|
||||||
echo Could not fetch latest version ^(network issue?^)
|
echo Could not fetch latest version ^(network issue?^)
|
||||||
@@ -507,7 +507,7 @@ if /i not "!doupdate!"=="y" (
|
|||||||
echo.
|
echo.
|
||||||
echo Updating...
|
echo Updating...
|
||||||
cd /d "%CORE_DIR%"
|
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
|
for /f "tokens=*" %%v in ('"%NODE_BIN%" -e "console.log(require('./node_modules/openclaw/package.json').version)"') do set NEW_VER=%%v
|
||||||
echo.
|
echo.
|
||||||
echo Updated! %CUR_VER% - %NEW_VER%
|
echo Updated! %CUR_VER% - %NEW_VER%
|
||||||
@@ -3,13 +3,13 @@ setlocal EnableDelayedExpansion
|
|||||||
chcp 65001 >nul 2>&1
|
chcp 65001 >nul 2>&1
|
||||||
title U-Claw Portable Setup
|
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 "APP_DIR=%SCRIPT_DIR%app"
|
||||||
set "CORE_DIR=%APP_DIR%\core"
|
set "CORE_DIR=%APP_DIR%\core"
|
||||||
set "RUNTIME_DIR=%APP_DIR%\runtime"
|
set "RUNTIME_DIR=%APP_DIR%\runtime"
|
||||||
set "MIRROR=https://registry.npmmirror.com"
|
set "MIRROR=https://registry.npmjs.org"
|
||||||
set "NODE_MIRROR=https://npmmirror.com/mirrors/node"
|
set "NODE_MIRROR=https://nodejs.org/dist"
|
||||||
set "NODE_VERSION=v22.22.1"
|
set "NODE_VERSION=v22.22.3"
|
||||||
set "ALL_PLATFORMS=false"
|
set "ALL_PLATFORMS=false"
|
||||||
if "%~1"=="--all-platforms" set "ALL_PLATFORMS=true"
|
if "%~1"=="--all-platforms" set "ALL_PLATFORMS=true"
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ echo U-Claw Portable Setup
|
|||||||
echo ========================================
|
echo ========================================
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
echo 系统: Windows x64
|
echo System: Windows x64
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
REM ---- 1. Download Node.js (Current Platform - Windows) ----
|
REM ---- 1. Download Node.js (Current Platform - Windows) ----
|
||||||
@@ -165,25 +165,16 @@ if exist "%QQ_DIR%\dist\index.js" (
|
|||||||
)
|
)
|
||||||
:qq_build_done
|
:qq_build_done
|
||||||
|
|
||||||
REM ---- 4. Install China-optimized skills ----
|
REM ---- 4. Install skills from the manifest ----
|
||||||
set "SKILLS_CN=%SCRIPT_DIR%skills-cn"
|
REM Content lives in ..\skills (see skills\README.md); this script carries none.
|
||||||
set "SKILLS_TARGET=%CORE_DIR%\node_modules\openclaw\skills"
|
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 exist "%SKILL_INSTALLER%" if exist "%SKILLS_TARGET%" (
|
||||||
if not exist "%SKILLS_TARGET%" goto skip_skills_install
|
echo [SKILLS] Installing skills...
|
||||||
|
"%NODE_TARGET%\node.exe" "%SKILL_INSTALLER%" --target "%SKILLS_TARGET%" --locale en
|
||||||
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
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
echo [OK] China skills installed (+%SKILL_COUNT% skills)
|
|
||||||
|
|
||||||
:skip_skills_install
|
|
||||||
|
|
||||||
REM ---- Done ----
|
REM ---- Done ----
|
||||||
echo.
|
echo.
|
||||||
@@ -192,8 +183,8 @@ echo Setup Complete!
|
|||||||
echo ========================================
|
echo ========================================
|
||||||
echo.
|
echo.
|
||||||
echo To start:
|
echo To start:
|
||||||
echo Mac: bash Mac-Start.command
|
echo Mac: bash START HERE - Mac.command
|
||||||
echo Windows: Double-click Windows-Start.bat
|
echo Windows: Double-click START HERE - Windows.bat
|
||||||
echo.
|
echo.
|
||||||
echo Directory structure:
|
echo Directory structure:
|
||||||
echo app\core\ - OpenClaw + dependencies
|
echo app\core\ - OpenClaw + dependencies
|
||||||
@@ -4,14 +4,14 @@ param(
|
|||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
$scriptDir = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)
|
||||||
$appDir = Join-Path $scriptDir "app"
|
$appDir = Join-Path $scriptDir "app"
|
||||||
$coreDir = Join-Path $appDir "core"
|
$coreDir = Join-Path $appDir "core"
|
||||||
$runtimeDir = Join-Path $appDir "runtime"
|
$runtimeDir = Join-Path $appDir "runtime"
|
||||||
|
|
||||||
$mirror = "https://registry.npmmirror.com"
|
$mirror = "https://registry.npmjs.org"
|
||||||
$nodeMirror = "https://npmmirror.com/mirrors/node"
|
$nodeMirror = "https://nodejs.org/dist"
|
||||||
$nodeVersion = "v22.22.1"
|
$nodeVersion = "v22.22.3"
|
||||||
|
|
||||||
function Write-Step {
|
function Write-Step {
|
||||||
param(
|
param(
|
||||||
@@ -268,20 +268,14 @@ else {
|
|||||||
|
|
||||||
Ensure-QQPluginBuild -CoreDir $coreDir -NpmCmd $npmCmd
|
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"
|
$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)) {
|
if ((Test-Path $skillInstaller) -and (Test-Path -Path $skillsTarget -PathType Container)) {
|
||||||
Write-Step "->" "Installing localized skills from skills-cn..." "Cyan"
|
Write-Step "->" "Installing skills..." "Cyan"
|
||||||
$skillCount = 0
|
& (Join-Path $windowsNodeTarget "node.exe") $skillInstaller --target $skillsTarget --locale en
|
||||||
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"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
@@ -290,8 +284,8 @@ Write-Host " Setup Complete" -ForegroundColor Green
|
|||||||
Write-Host "========================================" -ForegroundColor Green
|
Write-Host "========================================" -ForegroundColor Green
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host " Launch:"
|
Write-Host " Launch:"
|
||||||
Write-Host " Mac: bash Mac-Start.command" -ForegroundColor Cyan
|
Write-Host " Mac: bash START HERE - Mac.command" -ForegroundColor Cyan
|
||||||
Write-Host " Windows: double-click Windows-Start.bat" -ForegroundColor Cyan
|
Write-Host " Windows: double-click START HERE - Windows.bat" -ForegroundColor Cyan
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host " Structure:"
|
Write-Host " Structure:"
|
||||||
Write-Host " app/core/ -> OpenClaw and dependencies"
|
Write-Host " app/core/ -> OpenClaw and dependencies"
|
||||||
@@ -2,18 +2,18 @@
|
|||||||
# ============================================================
|
# ============================================================
|
||||||
# U-Claw Portable — 开发环境搭建脚本
|
# U-Claw Portable — 开发环境搭建脚本
|
||||||
# 用法: bash setup.sh
|
# 用法: bash setup.sh
|
||||||
# 作用: 下载 Node.js 运行时 + 安装 OpenClaw 到 app/ 目录
|
# 作用: 下载 Node.js 运行时 + Installing OpenClaw 到 app/ 目录
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
APP_DIR="$SCRIPT_DIR/app"
|
APP_DIR="$SCRIPT_DIR/app"
|
||||||
CORE_DIR="$APP_DIR/core"
|
CORE_DIR="$APP_DIR/core"
|
||||||
RUNTIME_DIR="$APP_DIR/runtime"
|
RUNTIME_DIR="$APP_DIR/runtime"
|
||||||
MIRROR="https://registry.npmmirror.com"
|
MIRROR="https://registry.npmjs.org"
|
||||||
NODE_MIRROR="https://npmmirror.com/mirrors/node"
|
NODE_MIRROR="https://nodejs.org/dist"
|
||||||
NODE_VERSION="v22.22.1"
|
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.3)"
|
||||||
ALL_PLATFORMS=false
|
ALL_PLATFORMS=false
|
||||||
[ "$1" = "--all-platforms" ] && ALL_PLATFORMS=true
|
[ "$1" = "--all-platforms" ] && ALL_PLATFORMS=true
|
||||||
|
|
||||||
@@ -41,20 +41,20 @@ if [ "$OS" = "Darwin" ]; then
|
|||||||
NODE_DIR_NAME="node-mac-x64"
|
NODE_DIR_NAME="node-mac-x64"
|
||||||
fi
|
fi
|
||||||
else
|
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
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo -e " 系统: ${GREEN}$OS $ARCH${NC}"
|
echo -e " System: ${GREEN}$OS $ARCH${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# ---- 1. Download Node.js (Current Platform) ----
|
# ---- 1. Download Node.js (Current Platform) ----
|
||||||
NODE_TARGET="$RUNTIME_DIR/$NODE_DIR_NAME"
|
NODE_TARGET="$RUNTIME_DIR/$NODE_DIR_NAME"
|
||||||
|
|
||||||
if [ -f "$NODE_TARGET/bin/node" ]; then
|
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
|
else
|
||||||
echo -e " ${CYAN}↓${NC} 下载 Node.js $NODE_VERSION ($PLATFORM)..."
|
echo -e " ${CYAN}↓${NC} Downloading Node.js $NODE_VERSION ($PLATFORM)"
|
||||||
mkdir -p "$NODE_TARGET"
|
mkdir -p "$NODE_TARGET"
|
||||||
|
|
||||||
NODE_URL="$NODE_MIRROR/$NODE_VERSION/node-$NODE_VERSION-$PLATFORM.tar.gz"
|
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
|
curl -fSL "$NODE_URL" | tar xz -C "$NODE_TARGET" --strip-components=1
|
||||||
|
|
||||||
if [ -f "$NODE_TARGET/bin/node" ]; then
|
if [ -f "$NODE_TARGET/bin/node" ]; then
|
||||||
echo -e " ${GREEN}✓${NC} Node.js ($PLATFORM) 下载完成"
|
echo -e " ${GREEN}✓${NC} Node.js ($PLATFORM) downloaded"
|
||||||
else
|
else
|
||||||
echo -e " ${RED}✗ Node.js 下载失败${NC}"
|
echo -e " ${RED}✗ Node.js Download failed${NC}"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
@@ -74,9 +74,9 @@ fi
|
|||||||
if [ "$ALL_PLATFORMS" = "true" ]; then
|
if [ "$ALL_PLATFORMS" = "true" ]; then
|
||||||
WIN_NODE_TARGET="$RUNTIME_DIR/node-win-x64"
|
WIN_NODE_TARGET="$RUNTIME_DIR/node-win-x64"
|
||||||
if [ -f "$WIN_NODE_TARGET/node.exe" ]; then
|
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
|
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"
|
mkdir -p "$WIN_NODE_TARGET"
|
||||||
|
|
||||||
WIN_NODE_URL="$NODE_MIRROR/$NODE_VERSION/node-$NODE_VERSION-win-x64.zip"
|
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"
|
rm -f "$TMP_ZIP"
|
||||||
|
|
||||||
if [ -f "$WIN_NODE_TARGET/node.exe" ]; then
|
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
|
else
|
||||||
echo -e " ${CYAN}⚠${NC} Windows runtime下载失败 (不影响当前平台使用)"
|
echo -e " ${CYAN}⚠${NC} Windows runtime download failed — this platform still works"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ---- 2. Install OpenClaw ----
|
# ---- 2. Install OpenClaw ----
|
||||||
if [ -d "$CORE_DIR/node_modules/openclaw" ]; then
|
if [ -d "$CORE_DIR/node_modules/openclaw" ]; then
|
||||||
echo -e " ${GREEN}✓${NC} OpenClaw 已安装,跳过"
|
echo -e " ${GREEN}✓${NC} OpenClaw already installed, skipping"
|
||||||
else
|
else
|
||||||
echo -e " ${CYAN}↓${NC} 安装 OpenClaw..."
|
echo -e " ${CYAN}↓${NC} Installing OpenClaw..."
|
||||||
mkdir -p "$CORE_DIR"
|
mkdir -p "$CORE_DIR"
|
||||||
|
|
||||||
# Init package.json if not exists (pinned OpenClaw version from OPENCLAW_VERSION)
|
# Init package.json if not exists (pinned OpenClaw version from OPENCLAW_VERSION)
|
||||||
@@ -114,7 +114,7 @@ else
|
|||||||
if [ ! -f "$OPENCLAW_VERSION_FILE" ]; then
|
if [ ! -f "$OPENCLAW_VERSION_FILE" ]; then
|
||||||
OPENCLAW_VERSION_FILE="$(dirname "$0")/../OPENCLAW_VERSION"
|
OPENCLAW_VERSION_FILE="$(dirname "$0")/../OPENCLAW_VERSION"
|
||||||
fi
|
fi
|
||||||
OPENCLAW_VERSION="2026.4.29"
|
OPENCLAW_VERSION="2026.7.1-2"
|
||||||
if [ -f "$OPENCLAW_VERSION_FILE" ]; then
|
if [ -f "$OPENCLAW_VERSION_FILE" ]; then
|
||||||
OPENCLAW_VERSION="$(tr -d '[:space:]' < "$OPENCLAW_VERSION_FILE")"
|
OPENCLAW_VERSION="$(tr -d '[:space:]' < "$OPENCLAW_VERSION_FILE")"
|
||||||
# Copy version file into portable/ so USB users can read it without repo root
|
# Copy version file into portable/ so USB users can read it without repo root
|
||||||
@@ -136,55 +136,61 @@ PKGJSON
|
|||||||
# Install with China mirror(缓存留盘内,拔盘不留痕)
|
# Install with China mirror(缓存留盘内,拔盘不留痕)
|
||||||
# --ignore-scripts 必须加:openclaw 的 preinstall 脚本会调系统 `node`,但便携版
|
# --ignore-scripts 必须加:openclaw 的 preinstall 脚本会调系统 `node`,但便携版
|
||||||
# 用的是 app/runtime 下的 node(不在 PATH),未装 Node 的 Mac 用户会因
|
# 用的是 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"
|
NODE_BIN="$NODE_TARGET/bin/node"
|
||||||
NPM_BIN="$NODE_TARGET/bin/npm"
|
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
|
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
|
fi
|
||||||
|
|
||||||
# ---- 3. Install QQ Plugin ----
|
# ---- 3. Install QQ Plugin ----
|
||||||
if [ -d "$CORE_DIR/node_modules/@sliverp/qqbot" ]; then
|
if [ -d "$CORE_DIR/node_modules/@sliverp/qqbot" ]; then
|
||||||
echo -e " ${GREEN}✓${NC} QQ 插件已安装,跳过"
|
echo -e " ${GREEN}✓${NC} QQ plugin already installed, skipping"
|
||||||
else
|
else
|
||||||
echo -e " ${CYAN}↓${NC} 安装 QQ 插件..."
|
echo -e " ${CYAN}↓${NC} Installing the QQ plugin..."
|
||||||
NODE_BIN="$NODE_TARGET/bin/node"
|
NODE_BIN="$NODE_TARGET/bin/node"
|
||||||
NPM_BIN="$NODE_TARGET/bin/npm"
|
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
|
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
|
fi
|
||||||
|
|
||||||
# ---- 4. Install China-optimized skills ----
|
# ---- 3b. Stage what the drive needs to stand alone ----
|
||||||
SKILLS_CN="$SCRIPT_DIR/skills-cn"
|
# release.yml puts these on the drive; a locally built drive has to match, or the
|
||||||
SKILLS_TARGET="$CORE_DIR/node_modules/openclaw/skills"
|
# 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
|
# ---- 4. Install skills from the manifest ----
|
||||||
echo -e " ${CYAN}↓${NC} 安装中国优化技能 (skills-cn)..."
|
# Content lives in ../skills (see skills/README.md); this script carries none.
|
||||||
SKILL_COUNT=0
|
SKILLS_TARGET="$CORE_DIR/node_modules/openclaw/skills"
|
||||||
for skill_dir in "$SKILLS_CN"/*/; do
|
SKILL_INSTALLER="$SCRIPT_DIR/lib/install-skills.mjs"
|
||||||
skill_name=$(basename "$skill_dir")
|
[ -f "$SKILL_INSTALLER" ] || SKILL_INSTALLER="$SCRIPT_DIR/../lib/install-skills.mjs"
|
||||||
if [ ! -d "$SKILLS_TARGET/$skill_name" ]; then
|
|
||||||
cp -R "$skill_dir" "$SKILLS_TARGET/$skill_name"
|
if [ -f "$SKILL_INSTALLER" ] && [ -d "$SKILLS_TARGET" ]; then
|
||||||
SKILL_COUNT=$((SKILL_COUNT + 1))
|
echo -e " ${CYAN}\u2193${NC} Installing skills..."
|
||||||
fi
|
"$NODE_TARGET/bin/node" "$SKILL_INSTALLER" --target "$SKILLS_TARGET" --locale "${UCLAW_LOCALE:-en}"
|
||||||
done
|
|
||||||
echo -e " ${GREEN}✓${NC} 中国技能安装完成 (+$SKILL_COUNT 个)"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ---- Done ----
|
# ---- Done ----
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${GREEN}════════════════════════════════════════${NC}"
|
echo -e "${GREEN}════════════════════════════════════════${NC}"
|
||||||
echo -e "${GREEN} ✅ 搭建完成!${NC}"
|
echo -e "${GREEN} Done.${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " 启动方式:"
|
echo -e " To start it:"
|
||||||
echo -e " Mac: ${CYAN}bash Mac-Start.command${NC}"
|
echo -e " Mac: ${CYAN}double-click \"START HERE - Mac.command\"${NC}"
|
||||||
echo -e " Windows: 双击 ${CYAN}Windows-Start.bat${NC}"
|
echo -e " Windows: double-click ${CYAN}START HERE - Windows.bat${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " 目录结构:"
|
echo -e " Layout:"
|
||||||
echo -e " app/core/ ← OpenClaw + 依赖"
|
echo -e " app/core/ OpenClaw and its dependencies"
|
||||||
echo -e " app/runtime/ ← Node.js $NODE_VERSION"
|
echo -e " app/runtime/ ← Node.js $NODE_VERSION"
|
||||||
echo -e " data/ ← 运行后自动生成"
|
echo -e " data/ created on first run"
|
||||||
echo ""
|
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}"
|
echo -e "${GREEN}════════════════════════════════════════${NC}"
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -2,13 +2,144 @@
|
|||||||
const http = require('http');
|
const http = require('http');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const { execFileSync } = require('child_process');
|
||||||
const { deflateSync } = require('zlib');
|
const { deflateSync } = require('zlib');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
|
|
||||||
const PORT_RANGE_START = 18788;
|
const PORT_RANGE_START = 18788;
|
||||||
const PORT_RANGE_END = 18798;
|
const PORT_RANGE_END = 18798;
|
||||||
const CONFIG_PATH = path.join(__dirname, '../data/.openclaw/openclaw.json');
|
const DATA_DIR = path.join(__dirname, '../data');
|
||||||
const RUNTIME_PATH = path.join(__dirname, '../data/.openclaw/runtime.json');
|
const CONFIG_PATH = path.join(DATA_DIR, '.openclaw/openclaw.json');
|
||||||
|
const RUNTIME_PATH = path.join(DATA_DIR, '.openclaw/runtime.json');
|
||||||
|
const UCLAW_META_PATH = path.join(DATA_DIR, '.openclaw/uclaw-meta.json');
|
||||||
|
|
||||||
|
function getStateDir() {
|
||||||
|
return process.env.OPENCLAW_STATE_DIR || path.join(DATA_DIR, '.openclaw');
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveNodeBinary() {
|
||||||
|
const runtimeRoot = path.join(__dirname, '../app/runtime');
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
return path.join(runtimeRoot, 'node-win-x64/node.exe');
|
||||||
|
}
|
||||||
|
if (process.platform === 'darwin') {
|
||||||
|
const dir = process.arch === 'arm64' ? 'node-mac-arm64' : 'node-mac-x64';
|
||||||
|
return path.join(runtimeRoot, dir, 'bin/node');
|
||||||
|
}
|
||||||
|
const dir = process.arch === 'arm64' ? 'node-linux-arm64' : 'node-linux-x64';
|
||||||
|
return path.join(runtimeRoot, dir, 'bin/node');
|
||||||
|
}
|
||||||
|
|
||||||
|
const OPENCLAW_ENTRY = path.join(__dirname, '../app/core/node_modules/openclaw/openclaw.mjs');
|
||||||
|
|
||||||
|
function openclawEnv() {
|
||||||
|
return {
|
||||||
|
...process.env,
|
||||||
|
OPENCLAW_HOME: DATA_DIR,
|
||||||
|
OPENCLAW_STATE_DIR: getStateDir(),
|
||||||
|
OPENCLAW_CONFIG_PATH: CONFIG_PATH,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function readConfigFile() {
|
||||||
|
if (!fs.existsSync(CONFIG_PATH)) return {};
|
||||||
|
return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractUclawMeta(config) {
|
||||||
|
if (!config.uclaw) return config;
|
||||||
|
const uclaw = config.uclaw;
|
||||||
|
delete config.uclaw;
|
||||||
|
try {
|
||||||
|
const existing = fs.existsSync(UCLAW_META_PATH)
|
||||||
|
? JSON.parse(fs.readFileSync(UCLAW_META_PATH, 'utf8'))
|
||||||
|
: {};
|
||||||
|
fs.writeFileSync(UCLAW_META_PATH, `${JSON.stringify({ ...existing, ...uclaw }, null, 2)}\n`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[uclaw-meta] write failed:', err.message);
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeConfigPreserve(existing, incoming) {
|
||||||
|
const next = { ...incoming };
|
||||||
|
if (existing.channels) {
|
||||||
|
next.channels = { ...existing.channels, ...(next.channels || {}) };
|
||||||
|
}
|
||||||
|
if (existing.commands?.ownerAllowFrom?.length) {
|
||||||
|
next.commands = {
|
||||||
|
...(existing.commands || {}),
|
||||||
|
...(next.commands || {}),
|
||||||
|
ownerAllowFrom: next.commands?.ownerAllowFrom?.length
|
||||||
|
? next.commands.ownerAllowFrom
|
||||||
|
: existing.commands.ownerAllowFrom,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (existing.plugins && !next.plugins) next.plugins = existing.plugins;
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeConfigFile(config) {
|
||||||
|
const existing = readConfigFile();
|
||||||
|
let next = mergeConfigPreserve(existing, config);
|
||||||
|
next = extractUclawMeta(next);
|
||||||
|
delete next.agent;
|
||||||
|
const dir = path.dirname(CONFIG_PATH);
|
||||||
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||||
|
fs.writeFileSync(CONFIG_PATH, `${JSON.stringify(next, null, 2)}\n`);
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
function runOpenclawCli(args) {
|
||||||
|
const node = resolveNodeBinary();
|
||||||
|
if (!fs.existsSync(node) || !fs.existsSync(OPENCLAW_ENTRY)) {
|
||||||
|
throw new Error('OpenClaw runtime not found on this drive');
|
||||||
|
}
|
||||||
|
return execFileSync(node, [OPENCLAW_ENTRY, ...args], {
|
||||||
|
env: openclawEnv(),
|
||||||
|
encoding: 'utf8',
|
||||||
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
}).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function telegramPairingPath() {
|
||||||
|
return path.join(getStateDir(), 'credentials', 'telegram-pairing.json');
|
||||||
|
}
|
||||||
|
|
||||||
|
function readTelegramPairingRequests() {
|
||||||
|
const filePath = telegramPairingPath();
|
||||||
|
if (!fs.existsSync(filePath)) return [];
|
||||||
|
let parsed;
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const requests = Array.isArray(parsed.requests) ? parsed.requests : [];
|
||||||
|
const now = Date.now();
|
||||||
|
return requests.filter((req) => {
|
||||||
|
const created = Date.parse(req.createdAt || '');
|
||||||
|
return Number.isFinite(created) && now - created < 3600 * 1000;
|
||||||
|
}).map((req) => ({
|
||||||
|
code: req.code,
|
||||||
|
id: req.id,
|
||||||
|
createdAt: req.createdAt,
|
||||||
|
username: req.meta?.username || req.meta?.name || '',
|
||||||
|
firstName: req.meta?.firstName || '',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function approveTelegramPairing(code) {
|
||||||
|
const snapshot = readConfigFile();
|
||||||
|
const channels = snapshot.channels ? { ...snapshot.channels } : null;
|
||||||
|
const out = runOpenclawCli(['pairing', 'approve', 'telegram', code, '--notify']);
|
||||||
|
const after = readConfigFile();
|
||||||
|
if (channels) {
|
||||||
|
after.channels = { ...channels, ...(after.channels || {}) };
|
||||||
|
writeConfigFile(after);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
// ── WeChat Login State ──────────────────────────────────────────────────────
|
// ── WeChat Login State ──────────────────────────────────────────────────────
|
||||||
const DEFAULT_WECHAT_BASE_URL = 'https://ilinkai.weixin.qq.com';
|
const DEFAULT_WECHAT_BASE_URL = 'https://ilinkai.weixin.qq.com';
|
||||||
@@ -404,6 +535,162 @@ const server = http.createServer((req, res) => {
|
|||||||
return;
|
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, modelsToTry, discoverGoogleOpenAiModels } = 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);
|
||||||
|
const candidates = modelsToTry(detected, model);
|
||||||
|
let lastStatus = null;
|
||||||
|
const tryChat = async (tryModel) => {
|
||||||
|
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: tryModel, max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }),
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
for (const tryModel of candidates) {
|
||||||
|
const response = await tryChat(tryModel);
|
||||||
|
if (response.ok) {
|
||||||
|
return reply({ ok: true, provider: detected, baseUrl, model: tryModel, latencyMs: Date.now() - startedAt });
|
||||||
|
}
|
||||||
|
lastStatus = response.status;
|
||||||
|
// Only try the next Gemini model when this account cannot see the first one.
|
||||||
|
if (!(detected?.id === 'google' && response.status === 404)) {
|
||||||
|
return reply({ ok: false, provider: detected, status: response.status, reason: classifyFailure({ status: response.status }) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (detected?.id === 'google' && lastStatus === 404) {
|
||||||
|
const discovered = await discoverGoogleOpenAiModels(baseUrl, apiKey);
|
||||||
|
const extra = discovered.filter((id) => !candidates.includes(id));
|
||||||
|
const rank = (id) => (id.includes('flash') ? 0 : 10) + (id.includes('2.0') ? 0 : 5) + (id.includes('lite') ? 1 : 0);
|
||||||
|
extra.sort((a, b) => rank(a) - rank(b));
|
||||||
|
for (const tryModel of extra) {
|
||||||
|
const response = await tryChat(tryModel);
|
||||||
|
if (response.ok) {
|
||||||
|
return reply({ ok: true, provider: detected, baseUrl, model: tryModel, latencyMs: Date.now() - startedAt });
|
||||||
|
}
|
||||||
|
lastStatus = response.status;
|
||||||
|
if (response.status !== 404) {
|
||||||
|
return reply({ ok: false, provider: detected, status: response.status, reason: classifyFailure({ status: response.status }) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return reply({ ok: false, provider: detected, status: lastStatus, reason: classifyFailure({ status: lastStatus }) });
|
||||||
|
} 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
|
// API: Get config
|
||||||
if (req.url === '/api/config' && req.method === 'GET') {
|
if (req.url === '/api/config' && req.method === 'GET') {
|
||||||
try {
|
try {
|
||||||
@@ -497,18 +784,49 @@ const server = http.createServer((req, res) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// API: Telegram pairing — list pending DM approvals (no CLI for the user)
|
||||||
|
if (req.url === '/api/telegram/pairing' && req.method === 'GET') {
|
||||||
|
try {
|
||||||
|
const requests = readTelegramPairingRequests();
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ requests }));
|
||||||
|
} catch (err) {
|
||||||
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ error: err.message }));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API: Telegram pairing — one-click approve from the Control Panel
|
||||||
|
if (req.url === '/api/telegram/pairing/approve' && req.method === 'POST') {
|
||||||
|
let body = '';
|
||||||
|
req.on('data', chunk => { body += chunk; });
|
||||||
|
req.on('end', () => {
|
||||||
|
try {
|
||||||
|
const { code } = JSON.parse(body || '{}');
|
||||||
|
if (!code || typeof code !== 'string') {
|
||||||
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ error: 'Missing pairing code' }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const message = approveTelegramPairing(code.trim().toUpperCase());
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ ok: true, message }));
|
||||||
|
} catch (err) {
|
||||||
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ error: err.message }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// API: Save config
|
// API: Save config
|
||||||
if (req.url === '/api/config' && req.method === 'POST') {
|
if (req.url === '/api/config' && req.method === 'POST') {
|
||||||
let body = '';
|
let body = '';
|
||||||
req.on('data', chunk => body += chunk);
|
req.on('data', chunk => body += chunk);
|
||||||
req.on('end', () => {
|
req.on('end', () => {
|
||||||
try {
|
try {
|
||||||
const config = JSON.parse(body);
|
writeConfigFile(JSON.parse(body));
|
||||||
// 清除旧版废弃键,防止 OpenClaw 报 "agent.* was moved" 错误
|
|
||||||
delete config.agent;
|
|
||||||
const dir = path.dirname(CONFIG_PATH);
|
|
||||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
||||||
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
res.end(JSON.stringify({ ok: true }));
|
res.end(JSON.stringify({ ok: true }));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -519,6 +837,25 @@ const server = http.createServer((req, res) => {
|
|||||||
return;
|
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
|
// Serve static files
|
||||||
const filePath = req.url === '/'
|
const filePath = req.url === '/'
|
||||||
? path.join(__dirname, 'public/index.html')
|
? path.join(__dirname, 'public/index.html')
|
||||||
|
|||||||
@@ -4,19 +4,19 @@
|
|||||||
// 1. 读取 OPENCLAW_VERSION 文件得到当前版本号
|
// 1. 读取 OPENCLAW_VERSION 文件得到当前版本号
|
||||||
// 2. 5s timeout 拉 OSS 上的 latest.json
|
// 2. 5s timeout 拉 OSS 上的 latest.json
|
||||||
// 3. 比对版本号,有新版就写 update-available.json 到 STATE_DIR
|
// 3. 比对版本号,有新版就写 update-available.json 到 STATE_DIR
|
||||||
// 4. Welcome.html / Config.html 启动时读这个文件,有就显示提示条
|
// 4. Read me first.html / Config.html 启动时读这个文件,有就显示提示条
|
||||||
//
|
//
|
||||||
// 设计原则:
|
// 设计原则:
|
||||||
// - 静默失败:网络坏、OSS 挂、json 格式错、本地版本号缺失,都不能影响 OpenClaw 启动
|
// - 静默失败:网络坏、OSS 挂、json 格式错、本地版本号缺失,都不能影响 OpenClaw 启动
|
||||||
// - 只读不下载:检查到新版只写 update-available.json,下载交给用户点链接到浏览器去做
|
// - 只读不下载:检查到新版只写 update-available.json,下载交给用户点链接到浏览器去做
|
||||||
// - 异步:调用方应该 detach 跑(Windows-Start.bat 用 start /B),不阻塞主流程
|
// - 异步:调用方应该 detach 跑(START HERE - Windows.bat 用 start /B),不阻塞主流程
|
||||||
//
|
//
|
||||||
// latest.json 格式(你在 OSS 上手动维护或用 publish-latest.mjs 生成):
|
// latest.json 格式(你在 OSS 上手动维护或用 publish-latest.mjs 生成):
|
||||||
// {
|
// {
|
||||||
// "version": "2026.4.30",
|
// "version": "2026.4.30",
|
||||||
// "releaseDate": "2026-05-03",
|
// "releaseDate": "2026-05-03",
|
||||||
// "downloadUrl": "https://u-claw-oss.56chat.cn/u-claw-open/u-claw-portable-v2026.4.30.zip",
|
// "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"
|
// "notes": "修复 xxx,新增 yyy"
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
|||||||
93
portable/lib/i18n.mjs
Normal file
93
portable/lib/i18n.mjs
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function localeFromMeta(configPath) {
|
||||||
|
if (!configPath) return null;
|
||||||
|
try {
|
||||||
|
const metaPath = join(dirname(configPath), 'uclaw-meta.json');
|
||||||
|
if (!existsSync(metaPath)) return null;
|
||||||
|
return normalise(JSON.parse(readFileSync(metaPath, 'utf8'))?.locale);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveLocale({ override, configPath } = {}) {
|
||||||
|
return (
|
||||||
|
normalise(override) ||
|
||||||
|
localeFromConfig(configPath) ||
|
||||||
|
localeFromMeta(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);
|
||||||
598
portable/lib/i18n/messages.js
Normal file
598
portable/lib/i18n/messages.js
Normal file
@@ -0,0 +1,598 @@
|
|||||||
|
/* 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.",
|
||||||
|
"tg.steps_title": "After saving, connect your Telegram account:",
|
||||||
|
"tg.step1": "Save the bot token above (click “Open dashboard” or save channels).",
|
||||||
|
"tg.step2": "On your phone, open Telegram, find your bot, and send /start.",
|
||||||
|
"tg.step3": "Come back here — a button will appear. Click it once. No command line.",
|
||||||
|
"tg.waiting": "Waiting for you to send /start to your bot on Telegram…",
|
||||||
|
"tg.pending": "Someone wants to connect: {who}. Click below to allow.",
|
||||||
|
"tg.approve": "Allow this Telegram account",
|
||||||
|
"tg.approving": "Approving…",
|
||||||
|
"tg.approved": "✅ Telegram connected — go chat in Telegram.",
|
||||||
|
"tg.approved_toast": "Telegram account approved.",
|
||||||
|
"tg.approve_failed": "Could not approve: {reason}",
|
||||||
|
"tg.poll_failed": "Could not check pairing status. Is U-Claw still running?",
|
||||||
|
"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. Attach it to an issue at https://gitea.fanghe.it.com/zhenghy/u-claw/issues 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.",
|
||||||
|
"nav.link_contact_d": "Open an issue",
|
||||||
|
"skill.uclaw-help.name": "About U-Claw",
|
||||||
|
"skill.uclaw-help.desc": "How it works, where things are, what to do when something breaks",
|
||||||
|
"cat.uclaw": "U-Claw"
|
||||||
|
},
|
||||||
|
"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 盘还插着。",
|
||||||
|
"tg.steps_title": "保存 Bot Token 后,连接你的 Telegram:",
|
||||||
|
"tg.step1": "先保存上面的 Bot Token(点「打开对话界面」或保存聊天软件设置)。",
|
||||||
|
"tg.step2": "在手机上打开 Telegram,找到你的机器人,发送 /start。",
|
||||||
|
"tg.step3": "回到这个页面 —— 会出现一个按钮,点一下就行,不用敲命令。",
|
||||||
|
"tg.waiting": "等你去 Telegram 给机器人发 /start…",
|
||||||
|
"tg.pending": "有人要连接:{who}。点下面按钮允许。",
|
||||||
|
"tg.approve": "允许这个 Telegram 账号",
|
||||||
|
"tg.approving": "正在批准…",
|
||||||
|
"tg.approved": "✅ Telegram 已连通 —— 去 Telegram 里聊吧。",
|
||||||
|
"tg.approved_toast": "Telegram 账号已批准。",
|
||||||
|
"tg.approve_failed": "批准失败:{reason}",
|
||||||
|
"tg.poll_failed": "无法检查配对状态。U-Claw 还在运行吗?",
|
||||||
|
"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": "里面的密钥已被去掉。需要帮忙的话,把它附到 https://gitea.fanghe.it.com/zhenghy/u-claw/issues 的 issue 里。",
|
||||||
|
"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 进去就多一个。",
|
||||||
|
"nav.link_contact_d": "提 issue",
|
||||||
|
"skill.uclaw-help.name": "关于 U-Claw",
|
||||||
|
"skill.uclaw-help.desc": "它怎么用、东西在哪、出问题了怎么办",
|
||||||
|
"cat.uclaw": "U-Claw"
|
||||||
|
}
|
||||||
|
};
|
||||||
297
portable/lib/i18n/skills-data.js
Normal file
297
portable/lib/i18n/skills-data.js
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
/* 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": "uclaw-help",
|
||||||
|
"categories": [
|
||||||
|
"uclaw"
|
||||||
|
],
|
||||||
|
"personas": [
|
||||||
|
"developer",
|
||||||
|
"admin",
|
||||||
|
"sales",
|
||||||
|
"marketing",
|
||||||
|
"finance",
|
||||||
|
"boss",
|
||||||
|
"general"
|
||||||
|
],
|
||||||
|
"emoji": "🦞"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": "🗒️"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
167
portable/lib/install-skills.mjs
Normal file
167
portable/lib/install-skills.mjs
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// Installs skills from skills/manifest.json into a target directory.
|
||||||
|
//
|
||||||
|
// This is the only place skill content is handled. install.sh and install.ps1
|
||||||
|
// call it and contain no skill text of their own, which is what keeps the two
|
||||||
|
// platforms provably identical (see tests/skills-manifest.test.mjs) and keeps
|
||||||
|
// non-ASCII bytes out of .bat launchers (see tests/windows-launchers.test.mjs).
|
||||||
|
//
|
||||||
|
// node lib/install-skills.mjs --target <dir> [--locale en] [--persona general]
|
||||||
|
// [--source <dir>] [--ref <git-ref>]
|
||||||
|
// [--list] [--dry-run] [--json]
|
||||||
|
|
||||||
|
import { mkdirSync, readFileSync, writeFileSync, existsSync, copyFileSync } from 'node:fs';
|
||||||
|
import { dirname, join, resolve } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
// Where to fetch from lives in origin.json so a move to our own host is one
|
||||||
|
// edit. Falling back to literals keeps this working when the file is absent —
|
||||||
|
// the remote installers download this script on its own, without the repo.
|
||||||
|
const ORIGIN = (() => {
|
||||||
|
for (const dir of [join(scriptDirOf(), '..'), scriptDirOf()]) {
|
||||||
|
try { return JSON.parse(readFileSync(join(dir, 'origin.json'), 'utf8')); } catch { /* try next */ }
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})();
|
||||||
|
|
||||||
|
// A template rather than a base URL: Gitea and GitHub lay raw paths out
|
||||||
|
// differently, and hardcoding either shape makes the other impossible.
|
||||||
|
const RAW_TEMPLATE = ORIGIN?.urls?.rawTemplate
|
||||||
|
?? 'https://gitea.fanghe.it.com/zhenghy/u-claw/raw/branch/{ref}/{path}';
|
||||||
|
const DEFAULT_REF = ORIGIN?.repo?.ref ?? 'main';
|
||||||
|
function scriptDirOf() { return dirname(fileURLToPath(import.meta.url)); }
|
||||||
|
const scriptDir = scriptDirOf();
|
||||||
|
|
||||||
|
function parseArgs(argv) {
|
||||||
|
const opts = { locale: 'en', personas: [], list: false, dryRun: false, json: false, ref: DEFAULT_REF };
|
||||||
|
for (let i = 0; i < argv.length; i++) {
|
||||||
|
const arg = argv[i];
|
||||||
|
const next = () => {
|
||||||
|
const value = argv[++i];
|
||||||
|
if (value === undefined) fail(`${arg} needs a value`);
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
switch (arg) {
|
||||||
|
case '--target': opts.target = next(); break;
|
||||||
|
case '--source': opts.source = next(); break;
|
||||||
|
case '--locale': opts.locale = next(); break;
|
||||||
|
case '--persona': opts.personas.push(next()); break;
|
||||||
|
case '--ref': opts.ref = next(); break;
|
||||||
|
case '--list': opts.list = true; break;
|
||||||
|
case '--dry-run': opts.dryRun = true; break;
|
||||||
|
case '--json': opts.json = true; break;
|
||||||
|
case '--help': case '-h': usage(); process.exit(0); break;
|
||||||
|
default: fail(`unknown argument: ${arg}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return opts;
|
||||||
|
}
|
||||||
|
|
||||||
|
function usage() {
|
||||||
|
process.stdout.write(
|
||||||
|
'Usage: install-skills.mjs --target <dir> [--locale en] [--persona <id>]...\n' +
|
||||||
|
' [--source <dir>] [--ref <git-ref>] [--list] [--dry-run] [--json]\n'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fail(message) {
|
||||||
|
process.stderr.write(`install-skills: ${message}\n`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Content lives in the repo checkout or on the USB. The one-line installers have
|
||||||
|
// neither, so they fall through to fetching from GitHub at a pinned ref.
|
||||||
|
function resolveLocalSource(explicit) {
|
||||||
|
const candidates = [explicit, join(scriptDir, '..', 'skills'), join(scriptDir, 'skills')];
|
||||||
|
for (const dir of candidates) {
|
||||||
|
if (dir && existsSync(join(dir, 'manifest.json'))) return resolve(dir);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchText(url) {
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) throw new Error(`${response.status} ${response.statusText} for ${url}`);
|
||||||
|
return response.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
function rawUrl(ref, path) {
|
||||||
|
return RAW_TEMPLATE.replace('{ref}', ref).replace('{path}', `skills/${path}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectSkills(manifest, { locale, personas }) {
|
||||||
|
return manifest.skills
|
||||||
|
.filter((skill) => skill.status === 'shipping')
|
||||||
|
.filter((skill) => (skill.locales ?? []).includes(locale))
|
||||||
|
// No --persona means install everything available for the locale.
|
||||||
|
.filter((skill) => personas.length === 0 || personas.some((p) => (skill.personas ?? []).includes(p)))
|
||||||
|
.sort((a, b) => a.id.localeCompare(b.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const opts = parseArgs(process.argv.slice(2));
|
||||||
|
if (!opts.target && !opts.list) fail('--target is required (or use --list)');
|
||||||
|
|
||||||
|
const localSource = resolveLocalSource(opts.source);
|
||||||
|
const remote = localSource === null;
|
||||||
|
|
||||||
|
let manifest;
|
||||||
|
try {
|
||||||
|
manifest = JSON.parse(
|
||||||
|
remote
|
||||||
|
? await fetchText(rawUrl(opts.ref, 'manifest.json'))
|
||||||
|
: readFileSync(join(localSource, 'manifest.json'), 'utf8')
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
fail(`could not read the skill manifest: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const selected = selectSkills(manifest, opts);
|
||||||
|
|
||||||
|
if (opts.list) {
|
||||||
|
process.stdout.write(
|
||||||
|
opts.json
|
||||||
|
? `${JSON.stringify(selected.map((s) => s.id))}\n`
|
||||||
|
: `${selected.map((s) => s.id).join('\n')}\n`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let installed = 0;
|
||||||
|
let skipped = 0;
|
||||||
|
const failures = [];
|
||||||
|
|
||||||
|
for (const skill of selected) {
|
||||||
|
const destDir = join(opts.target, skill.id);
|
||||||
|
const destFile = join(destDir, 'SKILL.md');
|
||||||
|
|
||||||
|
if (existsSync(destFile)) { skipped++; continue; }
|
||||||
|
if (opts.dryRun) { installed++; continue; }
|
||||||
|
|
||||||
|
try {
|
||||||
|
mkdirSync(destDir, { recursive: true });
|
||||||
|
if (remote) {
|
||||||
|
writeFileSync(destFile, await fetchText(rawUrl(opts.ref, `${opts.locale}/${skill.id}/SKILL.md`)), 'utf8');
|
||||||
|
} else {
|
||||||
|
const src = join(localSource, opts.locale, skill.id, 'SKILL.md');
|
||||||
|
if (!existsSync(src)) throw new Error(`missing ${src}`);
|
||||||
|
copyFileSync(src, destFile);
|
||||||
|
}
|
||||||
|
installed++;
|
||||||
|
} catch (error) {
|
||||||
|
// One bad skill should not abort the install — report at the end instead.
|
||||||
|
failures.push(`${skill.id}: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.json) {
|
||||||
|
process.stdout.write(`${JSON.stringify({ installed, skipped, failed: failures })}\n`);
|
||||||
|
} else {
|
||||||
|
process.stdout.write(`skills installed: ${installed}, already present: ${skipped}\n`);
|
||||||
|
for (const failure of failures) process.stderr.write(` failed: ${failure}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failures.length > 0 && installed === 0) process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => fail(error.message));
|
||||||
@@ -83,7 +83,7 @@ async function chatProbe(t) {
|
|||||||
const res = await requestText(url, {
|
const res = await requestText(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json', ...(t.apiKey ? { Authorization: `Bearer ${t.apiKey}` } : {}) },
|
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,
|
timeoutMs: CHAT_TIMEOUT_MS,
|
||||||
});
|
});
|
||||||
const ms = Date.now() - started;
|
const ms = Date.now() - started;
|
||||||
@@ -126,76 +126,77 @@ function requestText(rawUrl, { method = 'GET', headers = {}, body, timeoutMs })
|
|||||||
async function main() {
|
async function main() {
|
||||||
const configPath = process.argv[2] || process.env.OPENCLAW_CONFIG_PATH;
|
const configPath = process.argv[2] || process.env.OPENCLAW_CONFIG_PATH;
|
||||||
line('========================================');
|
line('========================================');
|
||||||
line(' U-Claw 内网体检 / Intranet Check');
|
line(' U-Claw — corporate network check');
|
||||||
line(` Node ${process.version}`);
|
line(` Node ${process.version}`);
|
||||||
line('========================================');
|
line('========================================');
|
||||||
line('');
|
line('');
|
||||||
|
|
||||||
// 1) 代理环境
|
// 1) 代理环境
|
||||||
line('【1】代理环境检查');
|
line('[1] System proxy');
|
||||||
const proxyKeys = ['HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'http_proxy', 'https_proxy', 'all_proxy'];
|
const proxyKeys = ['HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'http_proxy', 'https_proxy', 'all_proxy'];
|
||||||
const setProxies = proxyKeys.filter((k) => process.env[k]);
|
const setProxies = proxyKeys.filter((k) => process.env[k]);
|
||||||
if (setProxies.length) {
|
if (setProxies.length) {
|
||||||
for (const k of setProxies) line(` ${k} = ${process.env[k]}`);
|
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 {
|
} else {
|
||||||
line(' (未检测到系统代理)');
|
line(' No system proxy set.');
|
||||||
}
|
}
|
||||||
line('');
|
line('');
|
||||||
|
|
||||||
if (!configPath) { line('未提供配置路径,结束。'); return; }
|
if (!configPath) { line('No config path given — nothing to check.'); return; }
|
||||||
let config;
|
let config;
|
||||||
try { config = JSON.parse(readFileSync(configPath, 'utf8')); }
|
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);
|
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 建议
|
// 2) NO_PROXY 建议
|
||||||
const hosts = Array.from(new Set(providers.map((p) => hostOf(p.baseUrl)).filter(Boolean)));
|
const hosts = Array.from(new Set(providers.map((p) => hostOf(p.baseUrl)).filter(Boolean)));
|
||||||
const noProxy = applyNoProxy(hosts);
|
const noProxy = applyNoProxy(hosts);
|
||||||
line('【2】建议的 NO_PROXY(让这些地址直连、绕开代理)');
|
line('[2] Suggested NO_PROXY — hosts that should bypass the proxy');
|
||||||
line(` ${noProxy.join(',')}`);
|
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('');
|
line('');
|
||||||
|
|
||||||
// 3) 直连可达
|
// 3) 直连可达
|
||||||
line('【3】直连测试(绕过代理,看能否摸到模型服务)');
|
line('[3] Direct connection — can this machine reach the model at all, proxy bypassed');
|
||||||
for (const p of providers) {
|
for (const p of providers) {
|
||||||
const r = await reachProbe(p.baseUrl, p.apiKey);
|
const r = await reachProbe(p.baseUrl, p.apiKey);
|
||||||
if (r.ok) line(` [${p.name}] ${p.baseUrl} → ✓ 可达 HTTP ${r.status} (${r.ms}ms)`);
|
if (r.ok) line(` [${p.name}] ${p.baseUrl} -> reachable, HTTP ${r.status} (${r.ms}ms)`);
|
||||||
else line(` [${p.name}] ${p.baseUrl} → ✗ 失败 ${r.error} (${r.ms}ms)`);
|
else line(` [${p.name}] ${p.baseUrl} -> FAILED ${r.error} (${r.ms}ms)`);
|
||||||
}
|
}
|
||||||
line('');
|
line('');
|
||||||
|
|
||||||
// 4) 端到端实测
|
// 4) 端到端实测
|
||||||
line('【4】实测:真发一条对话给模型');
|
line('[4] Real request — actually asking the model something');
|
||||||
const t = pickTarget(config, providers);
|
const t = pickTarget(config, providers);
|
||||||
if (!t) { line(' 找不到可测的模型 id,跳过。'); }
|
if (!t) { line(' No model ID to test with — skipped.'); }
|
||||||
else {
|
else {
|
||||||
line(` 模型:${t.provName} / ${t.modelId}`);
|
line(` Model: ${t.provName} / ${t.modelId}`);
|
||||||
line(' 发送中,请稍候...');
|
line(' Sending…');
|
||||||
const c = await chatProbe(t);
|
const c = await chatProbe(t);
|
||||||
if (c.ok) {
|
if (c.ok) {
|
||||||
line('');
|
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) {
|
} else if (c.status) {
|
||||||
line('');
|
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 {
|
} else {
|
||||||
line('');
|
line('');
|
||||||
line(` ✗ 直连失败:${c.error} (${c.ms}ms)`);
|
line(` Could not connect: ${c.error} (${c.ms}ms)`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
line('');
|
line('');
|
||||||
|
|
||||||
// 结论
|
// 结论
|
||||||
line('========================================');
|
line('========================================');
|
||||||
line(' 怎么看结果:');
|
line(' What the results mean:');
|
||||||
line(' · 第4步「跑通了」 → 一切正常,以后双击 Windows-Start.bat 即可。');
|
line(' Step 4 worked Everything is fine. Start U-Claw normally.');
|
||||||
line(' · 第3步可达但程序里用不了 → 是系统代理在劫持,新版启动脚本已自动绕开(NO_PROXY)。');
|
line(' Step 3 reachable but A proxy is intercepting the request. U-Claw already')
|
||||||
line(' · 第3步「直连失败/超时」 → 地址错 / 内网不通 / 防火墙 / 模型服务没起,找机房管理员。');
|
line(' the app cannot use it bypasses it at startup via NO_PROXY.');
|
||||||
line(' · 出现 401/403 → 网络是通的,只是 API Key 不对。');
|
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('========================================');
|
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>
|
<!DOCTYPE html>
|
||||||
<html lang="zh-CN">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<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 思路)。
|
启动首屏(移植自 v2 u-clawx 4.0 的 splash 思路)。
|
||||||
双击启动脚本后立刻打开本页,给用户即时反馈,消除"黑窗假死"体感。
|
双击启动脚本后立刻打开本页,给用户即时反馈,消除"黑窗假死"体感。
|
||||||
@@ -100,40 +100,41 @@ body.ready .ready-wrap { display: block; }
|
|||||||
.btn-primary:hover { background: #ff6a30; }
|
.btn-primary:hover { background: #ff6a30; }
|
||||||
.foot { margin-top: 24px; font-size: .82em; opacity: .65; line-height: 1.6; }
|
.foot { margin-top: 24px; font-size: .82em; opacity: .65; line-height: 1.6; }
|
||||||
</style>
|
</style>
|
||||||
|
<script src="../data/.openclaw/locale.js"></script>
|
||||||
|
<script src="i18n/messages.js"></script>
|
||||||
|
<script src="i18n/i18n.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="box">
|
<div class="box">
|
||||||
|
|
||||||
<!-- 启动中 -->
|
<!-- Starting -->
|
||||||
<div class="loading-wrap">
|
<div class="loading-wrap">
|
||||||
<div class="logo">🦞</div>
|
<div class="logo">🦞</div>
|
||||||
<h1>U-Claw 正在启动</h1>
|
<h1 data-i18n="loading.starting">Starting U-Claw…</h1>
|
||||||
<div class="tip">首次从 U 盘启动需要展开内置组件,<br>稍等片刻,就绪后会提示你下一步。</div>
|
<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="spinner"></div>
|
||||||
<div class="bar"><i></i></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="elapsed" id="elapsed"></div>
|
||||||
<div class="note">
|
<div class="note">
|
||||||
慢 U 盘首次启动通常 30–90 秒。<br>
|
<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>
|
||||||
<a class="manual" id="manual" href="#">配置中心</a>。
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 就绪后:让用户自己选 -->
|
<!-- Ready: let the user choose -->
|
||||||
<div class="ready-wrap">
|
<div class="ready-wrap">
|
||||||
<div class="logo">🦞</div>
|
<div class="logo">🦞</div>
|
||||||
<h1>U-Claw 已就绪</h1>
|
<h1 data-i18n="loading.ready">U-Claw is ready</h1>
|
||||||
<div class="tip">选择下一步 — 直接开始聊天,或先去配置模型和渠道。</div>
|
<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">
|
<div class="actions">
|
||||||
<a class="btn btn-primary" id="goDash">🚀 进入 Dashboard
|
<a class="btn btn-primary" id="goDash"><span data-i18n="loading.open_dashboard">🚀 Start chatting</span>
|
||||||
<span class="sub">开始和 AI 聊天</span>
|
|
||||||
</a>
|
</a>
|
||||||
<a class="btn" id="goConfig">⚙️ 配置 / 改模型
|
<a class="btn" id="goConfig"><span data-i18n="loading.open_settings">⚙️ Settings</span>
|
||||||
<span class="sub">填写或更换模型 Key、充值、连接微信等渠道</span>
|
<span class="sub" data-i18n="loading.settings_hint">Add or change your API key, and connect chat apps.</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -156,7 +157,7 @@ body.ready .ready-wrap { display: block; }
|
|||||||
|
|
||||||
function fmt(ms) {
|
function fmt(ms) {
|
||||||
var s = Math.floor(ms / 1000);
|
var s = Math.floor(ms / 1000);
|
||||||
return '已等待 ' + s + ' 秒';
|
return window.UClawI18n ? window.UClawI18n.t('loading.elapsed', { seconds: s }) : s + 's';
|
||||||
}
|
}
|
||||||
|
|
||||||
function tickElapsed() {
|
function tickElapsed() {
|
||||||
@@ -187,8 +188,10 @@ body.ready .ready-wrap { display: block; }
|
|||||||
// 兜底:5 分钟还没起来,提示去控制台
|
// 兜底:5 分钟还没起来,提示去控制台
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
if (!ready) {
|
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 =
|
document.getElementById('status').innerHTML =
|
||||||
'启动较慢,可先打开 <a class="manual" href="' + configCenter + '">配置中心</a> 检查';
|
slow + ' <a class="manual" href="' + configCenter + '">' + label + '</a>';
|
||||||
}
|
}
|
||||||
}, 300000);
|
}, 300000);
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ detect_install_mode() {
|
|||||||
|
|
||||||
do_kill_gateway() {
|
do_kill_gateway() {
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${CYAN}${BOLD}━━━ 杀死残留进程 ━━━${NC}"
|
echo -e " ${CYAN}${BOLD}--- Stop leftover processes ---${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
local FOUND=0
|
local FOUND=0
|
||||||
@@ -42,7 +42,7 @@ do_kill_gateway() {
|
|||||||
if [ -n "$PIDS" ]; then
|
if [ -n "$PIDS" ]; then
|
||||||
for PID in $PIDS; do
|
for PID in $PIDS; do
|
||||||
local CMD=$(ps -p "$PID" -o comm= 2>/dev/null || echo "unknown")
|
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
|
FOUND=1
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
@@ -52,20 +52,20 @@ do_kill_gateway() {
|
|||||||
local OC_PIDS=$(pgrep -f "openclaw.mjs gateway" 2>/dev/null)
|
local OC_PIDS=$(pgrep -f "openclaw.mjs gateway" 2>/dev/null)
|
||||||
if [ -n "$OC_PIDS" ]; then
|
if [ -n "$OC_PIDS" ]; then
|
||||||
for PID in $OC_PIDS; do
|
for PID in $OC_PIDS; do
|
||||||
echo -e " ${YELLOW}OpenClaw 网关进程: PID $PID${NC}"
|
echo -e " ${YELLOW}OpenClaw gateway: PID $PID${NC}"
|
||||||
FOUND=1
|
FOUND=1
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$FOUND" = "0" ]; then
|
if [ "$FOUND" = "0" ]; then
|
||||||
echo -e " ${GREEN}没有发现残留进程${NC}"
|
echo -e " ${GREEN}Nothing left running${NC}"
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
read -p " 确认杀死以上进程? (y/N): " CONFIRM
|
read -p " Stop these? (y/N): " CONFIRM
|
||||||
if [ "$CONFIRM" != "y" ] && [ "$CONFIRM" != "Y" ]; then
|
if [ "$CONFIRM" != "y" ] && [ "$CONFIRM" != "Y" ]; then
|
||||||
echo -e " ${YELLOW}已取消${NC}"
|
echo -e " ${YELLOW}Cancelled${NC}"
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -82,24 +82,24 @@ do_kill_gateway() {
|
|||||||
pgrep -f "openclaw.mjs gateway" 2>/dev/null | xargs kill 2>/dev/null
|
pgrep -f "openclaw.mjs gateway" 2>/dev/null | xargs kill 2>/dev/null
|
||||||
|
|
||||||
sleep 1
|
sleep 1
|
||||||
echo -e " ${GREEN}进程已清理${NC}"
|
echo -e " ${GREEN}Processes stopped${NC}"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── [10] View/export/clean logs ──────────────────────────────
|
# ── [10] View/export/clean logs ──────────────────────────────
|
||||||
|
|
||||||
do_logs() {
|
do_logs() {
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${CYAN}${BOLD}━━━ 日志管理 ━━━${NC}"
|
echo -e " ${CYAN}${BOLD}--- Logs ---${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
mkdir -p "$LOG_DIR"
|
mkdir -p "$LOG_DIR"
|
||||||
local LOG_FILE="$LOG_DIR/gateway.log"
|
local LOG_FILE="$LOG_DIR/gateway.log"
|
||||||
|
|
||||||
echo -e " ${GREEN}[a]${NC} 查看最近日志(最后 50 行)"
|
echo -e " ${GREEN}[a]${NC} Show the last 50 lines"
|
||||||
echo -e " ${GREEN}[b]${NC} 导出日志到桌面"
|
echo -e " ${GREEN}[b]${NC} Copy the log to the desktop"
|
||||||
echo -e " ${GREEN}[c]${NC} 清理 7 天前的旧日志"
|
echo -e " ${GREEN}[c]${NC} Delete logs older than 7 days"
|
||||||
echo ""
|
echo ""
|
||||||
read -p " 选择 (a-c): " -n 1 LOG_CHOICE
|
read -p " Choose (a-c): " -n 1 LOG_CHOICE
|
||||||
echo ""
|
echo ""
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
@@ -110,13 +110,13 @@ do_logs() {
|
|||||||
echo ""
|
echo ""
|
||||||
tail -50 "$LOG_FILE"
|
tail -50 "$LOG_FILE"
|
||||||
else
|
else
|
||||||
echo -e " ${YELLOW}日志文件不存在: $LOG_FILE${NC}"
|
echo -e " ${YELLOW}No log file yet: $LOG_FILE${NC}"
|
||||||
echo " 启动网关后会自动生成日志。"
|
echo " One is created the first time U-Claw runs."
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
b)
|
b)
|
||||||
if [ ! -f "$LOG_FILE" ]; then
|
if [ ! -f "$LOG_FILE" ]; then
|
||||||
echo -e " ${YELLOW}没有日志可导出${NC}"
|
echo -e " ${YELLOW}No log to copy${NC}"
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
local TS=$(date +%Y%m%d_%H%M%S)
|
local TS=$(date +%Y%m%d_%H%M%S)
|
||||||
@@ -124,26 +124,26 @@ do_logs() {
|
|||||||
[ ! -d "$EXPORT_DIR" ] && EXPORT_DIR="$HOME"
|
[ ! -d "$EXPORT_DIR" ] && EXPORT_DIR="$HOME"
|
||||||
local EXPORT_FILE="$EXPORT_DIR/uclaw-logs-$TS.txt"
|
local EXPORT_FILE="$EXPORT_DIR/uclaw-logs-$TS.txt"
|
||||||
cp "$LOG_FILE" "$EXPORT_FILE"
|
cp "$LOG_FILE" "$EXPORT_FILE"
|
||||||
echo -e " ${GREEN}日志已导出: $EXPORT_FILE${NC}"
|
echo -e " ${GREEN}Copied to: $EXPORT_FILE${NC}"
|
||||||
echo " 大小: $(du -sh "$EXPORT_FILE" | cut -f1)"
|
echo " Size: $(du -sh "$EXPORT_FILE" | cut -f1)"
|
||||||
;;
|
;;
|
||||||
c)
|
c)
|
||||||
local COUNT=$(find "$LOG_DIR" -name "*.log" -mtime +7 2>/dev/null | wc -l | tr -d ' ')
|
local COUNT=$(find "$LOG_DIR" -name "*.log" -mtime +7 2>/dev/null | wc -l | tr -d ' ')
|
||||||
if [ "$COUNT" = "0" ]; then
|
if [ "$COUNT" = "0" ]; then
|
||||||
echo -e " ${GREEN}没有需要清理的旧日志${NC}"
|
echo -e " ${GREEN}No old logs to delete${NC}"
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
echo -e " 找到 ${YELLOW}$COUNT${NC} 个超过 7 天的日志文件"
|
echo -e " Found ${YELLOW}$COUNT${NC} log files older than 7 days"
|
||||||
read -p " 确认清理? (y/N): " CONFIRM
|
read -p " Delete them? (y/N): " CONFIRM
|
||||||
if [ "$CONFIRM" = "y" ] || [ "$CONFIRM" = "Y" ]; then
|
if [ "$CONFIRM" = "y" ] || [ "$CONFIRM" = "Y" ]; then
|
||||||
find "$LOG_DIR" -name "*.log" -mtime +7 -delete 2>/dev/null
|
find "$LOG_DIR" -name "*.log" -mtime +7 -delete 2>/dev/null
|
||||||
echo -e " ${GREEN}旧日志已清理${NC}"
|
echo -e " ${GREEN}Old logs deleted${NC}"
|
||||||
else
|
else
|
||||||
echo -e " ${YELLOW}已取消${NC}"
|
echo -e " ${YELLOW}Cancelled${NC}"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo -e " ${RED}无效选择${NC}"
|
echo -e " ${RED}Not a valid choice${NC}"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
@@ -152,46 +152,46 @@ do_logs() {
|
|||||||
|
|
||||||
do_factory_reset() {
|
do_factory_reset() {
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${RED}${BOLD}━━━ 恢复出厂设置 ━━━${NC}"
|
echo -e " ${RED}${BOLD}--- Reset to factory settings ---${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${RED}警告: 此操作将删除所有配置和记忆数据!${NC}"
|
echo -e " ${RED}This deletes every setting and everything the AI remembers.${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
echo " 将执行以下操作:"
|
echo " It will:"
|
||||||
echo " 1. 自动备份当前配置和记忆"
|
echo " 1. Back up your settings and memory first"
|
||||||
echo " 2. 删除配置文件 (openclaw.json)"
|
echo " 2. Delete openclaw.json"
|
||||||
echo " 3. 删除记忆数据 (memory/)"
|
echo " 3. Delete memory/"
|
||||||
echo " 4. 恢复默认配置"
|
echo " 4. Put the defaults back"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${YELLOW}请输入 RESET 确认(区分大小写):${NC}"
|
echo -e " ${YELLOW}Type RESET to confirm (case-sensitive):${NC}"
|
||||||
read -p " > " CONFIRM
|
read -p " > " CONFIRM
|
||||||
|
|
||||||
if [ "$CONFIRM" != "RESET" ]; then
|
if [ "$CONFIRM" != "RESET" ]; then
|
||||||
echo -e " ${YELLOW}已取消${NC}"
|
echo -e " ${YELLOW}Cancelled${NC}"
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Step 1: Auto backup
|
# Step 1: Auto backup
|
||||||
echo ""
|
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 TS=$(date +%Y%m%d_%H%M%S)
|
||||||
local BK="$BACKUP_DIR/pre-reset_$TS"
|
local BK="$BACKUP_DIR/pre-reset_$TS"
|
||||||
mkdir -p "$BK"
|
mkdir -p "$BK"
|
||||||
[ -f "$CONFIG_PATH" ] && cp "$CONFIG_PATH" "$BK/" 2>/dev/null
|
[ -f "$CONFIG_PATH" ] && cp "$CONFIG_PATH" "$BK/" 2>/dev/null
|
||||||
[ -d "$DATA_DIR/memory" ] && cp -R "$DATA_DIR/memory" "$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
|
# 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 "$CONFIG_PATH" 2>/dev/null
|
||||||
rm -f "$DATA_DIR/config.json" 2>/dev/null
|
rm -f "$DATA_DIR/config.json" 2>/dev/null
|
||||||
|
|
||||||
# Step 3: Delete memory
|
# 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
|
rm -rf "$DATA_DIR/memory" 2>/dev/null
|
||||||
mkdir -p "$DATA_DIR/memory"
|
mkdir -p "$DATA_DIR/memory"
|
||||||
|
|
||||||
# Step 4: Restore default config
|
# Step 4: Restore default config
|
||||||
echo -e " ${CYAN}[4/4] 恢复默认配置...${NC}"
|
echo -e " ${CYAN}[4/4] Restoring defaults…${NC}"
|
||||||
mkdir -p "$STATE_DIR"
|
mkdir -p "$STATE_DIR"
|
||||||
if [ -f "$DEFAULT_CONFIG" ]; then
|
if [ -f "$DEFAULT_CONFIG" ]; then
|
||||||
cp "$DEFAULT_CONFIG" "$CONFIG_PATH"
|
cp "$DEFAULT_CONFIG" "$CONFIG_PATH"
|
||||||
@@ -207,75 +207,75 @@ CFGEOF
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${GREEN}出厂设置已恢复!${NC}"
|
echo -e " ${GREEN}Reset complete.${NC}"
|
||||||
echo -e " 备份位置: $BK"
|
echo -e " Your old settings are at $BK"
|
||||||
echo -e " 请重新运行配置向导 [1] 设置模型。"
|
echo -e " Run the setup wizard again to add a model."
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── [12] Uninstall ───────────────────────────────────────────
|
# ── [12] Uninstall ───────────────────────────────────────────
|
||||||
|
|
||||||
do_uninstall() {
|
do_uninstall() {
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${RED}${BOLD}━━━ 卸载 U-Claw ━━━${NC}"
|
echo -e " ${RED}${BOLD}--- Uninstall U-Claw ---${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
local MODE=$(detect_install_mode)
|
local MODE=$(detect_install_mode)
|
||||||
|
|
||||||
case $MODE in
|
case $MODE in
|
||||||
installed)
|
installed)
|
||||||
echo " 检测到: 已安装版本 (~/.uclaw/)"
|
echo " Found: installed copy at ~/.uclaw/"
|
||||||
echo ""
|
echo ""
|
||||||
echo " 将删除以下目录:"
|
echo " These folders will be deleted:"
|
||||||
echo -e " ${YELLOW}$HOME/.uclaw/${NC}"
|
echo -e " ${YELLOW}$HOME/.uclaw/${NC}"
|
||||||
if [ -d "$HOME/.uclaw" ]; then
|
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
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${RED}请输入 UNINSTALL 确认:${NC}"
|
echo -e " ${RED}Type UNINSTALL to confirm:${NC}"
|
||||||
read -p " > " CONFIRM
|
read -p " > " CONFIRM
|
||||||
if [ "$CONFIRM" != "UNINSTALL" ]; then
|
if [ "$CONFIRM" != "UNINSTALL" ]; then
|
||||||
echo -e " ${YELLOW}已取消${NC}"
|
echo -e " ${YELLOW}Cancelled${NC}"
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
rm -rf "$HOME/.uclaw"
|
rm -rf "$HOME/.uclaw"
|
||||||
echo -e " ${GREEN}卸载完成!${NC}"
|
echo -e " ${GREEN}Uninstalled.${NC}"
|
||||||
;;
|
;;
|
||||||
usb)
|
usb)
|
||||||
echo " 检测到: U 盘模式"
|
echo " Found: running from a USB drive"
|
||||||
echo ""
|
echo ""
|
||||||
echo " 便携版不需要卸载 —— 只需:"
|
echo " There is nothing to uninstall. Just:"
|
||||||
echo " 1. 关闭所有 U-Claw 窗口"
|
echo " 1. Close every U-Claw window"
|
||||||
echo " 2. 安全弹出 U 盘"
|
echo " 2. Eject the drive"
|
||||||
echo " 3. 如需清理电脑上的数据,删除 ~/.uclaw/ (如果存在)"
|
echo " 3. If you want nothing left on this computer, delete ~/.uclaw/"
|
||||||
echo ""
|
echo ""
|
||||||
if [ -d "$HOME/.uclaw" ]; then
|
if [ -d "$HOME/.uclaw" ]; then
|
||||||
echo -e " ${YELLOW}发现本机数据: ~/.uclaw/${NC}"
|
echo -e " ${YELLOW}Found data on this computer: ~/.uclaw/${NC}"
|
||||||
echo " 大小: $(du -sh "$HOME/.uclaw" 2>/dev/null | cut -f1)"
|
echo " Size: $(du -sh "$HOME/.uclaw" 2>/dev/null | cut -f1)"
|
||||||
read -p " 是否删除本机数据? (y/N): " DEL
|
read -p " Delete it? (y/N): " DEL
|
||||||
if [ "$DEL" = "y" ] || [ "$DEL" = "Y" ]; then
|
if [ "$DEL" = "y" ] || [ "$DEL" = "Y" ]; then
|
||||||
rm -rf "$HOME/.uclaw"
|
rm -rf "$HOME/.uclaw"
|
||||||
echo -e " ${GREEN}本机数据已清理${NC}"
|
echo -e " ${GREEN}Deleted${NC}"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
portable)
|
portable)
|
||||||
echo " 检测到: 便携版本"
|
echo " Found: portable copy"
|
||||||
echo ""
|
echo ""
|
||||||
echo " 便携版不需要卸载 —— 直接删除文件夹即可:"
|
echo " There is nothing to uninstall — just delete the folder:"
|
||||||
echo -e " ${YELLOW}$UCLAW_DIR${NC}"
|
echo -e " ${YELLOW}$UCLAW_DIR${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
if [ -d "$HOME/.uclaw" ]; then
|
if [ -d "$HOME/.uclaw" ]; then
|
||||||
echo -e " ${YELLOW}发现本机数据: ~/.uclaw/${NC}"
|
echo -e " ${YELLOW}Found data on this computer: ~/.uclaw/${NC}"
|
||||||
echo " 大小: $(du -sh "$HOME/.uclaw" 2>/dev/null | cut -f1)"
|
echo " Size: $(du -sh "$HOME/.uclaw" 2>/dev/null | cut -f1)"
|
||||||
read -p " 是否删除本机数据? (y/N): " DEL
|
read -p " Delete it? (y/N): " DEL
|
||||||
if [ "$DEL" = "y" ] || [ "$DEL" = "Y" ]; then
|
if [ "$DEL" = "y" ] || [ "$DEL" = "Y" ]; then
|
||||||
rm -rf "$HOME/.uclaw"
|
rm -rf "$HOME/.uclaw"
|
||||||
echo -e " ${GREEN}本机数据已清理${NC}"
|
echo -e " ${GREEN}Deleted${NC}"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
echo " Electron 桌面版卸载:"
|
echo " Electron desktop app:"
|
||||||
echo " macOS: 将 U-Claw.app 从「应用程序」拖到废纸篓"
|
echo " macOS: drag U-Claw.app from Applications to the Trash"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
@@ -284,15 +284,15 @@ do_uninstall() {
|
|||||||
|
|
||||||
do_update() {
|
do_update() {
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${CYAN}${BOLD}━━━ 检查更新 ━━━${NC}"
|
echo -e " ${CYAN}${BOLD}--- Check for updates ---${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
if [ ! -f "$NODE_BIN" ]; then
|
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
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo -e " ${DIM}正在查询最新版本...${NC}"
|
echo -e " ${DIM}Looking up the latest version…${NC}"
|
||||||
|
|
||||||
# Get current version
|
# Get current version
|
||||||
local CUR_VER=""
|
local CUR_VER=""
|
||||||
@@ -301,58 +301,58 @@ do_update() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -z "$CUR_VER" ]; then
|
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
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Get latest version from npmmirror
|
# Get latest version from the npm registry
|
||||||
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)
|
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
|
if [ -z "$LATEST_VER" ] || [ "$LATEST_VER" = "error" ]; then
|
||||||
echo -e " ${YELLOW}无法获取最新版本(网络问题?)${NC}"
|
echo -e " ${YELLOW}Could not reach the registry. Check your network.${NC}"
|
||||||
echo " 当前版本: $CUR_VER"
|
echo " Installed: $CUR_VER"
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo " 当前版本: $CUR_VER"
|
echo " Installed: $CUR_VER"
|
||||||
echo " 最新版本: $LATEST_VER"
|
echo " Latest: $LATEST_VER"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
if [ "$CUR_VER" = "$LATEST_VER" ]; then
|
if [ "$CUR_VER" = "$LATEST_VER" ]; then
|
||||||
echo -e " ${GREEN}已是最新版本!${NC}"
|
echo -e " ${GREEN}Already up to date.${NC}"
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo -e " ${YELLOW}有新版本可用!${NC}"
|
echo -e " ${YELLOW}A newer version is available.${NC}"
|
||||||
read -p " 是否立即升级? (y/N): " DO_UPDATE
|
read -p " Update now? (y/N): " DO_UPDATE
|
||||||
if [ "$DO_UPDATE" != "y" ] && [ "$DO_UPDATE" != "Y" ]; then
|
if [ "$DO_UPDATE" != "y" ] && [ "$DO_UPDATE" != "Y" ]; then
|
||||||
echo -e " ${YELLOW}已取消${NC}"
|
echo -e " ${YELLOW}Cancelled${NC}"
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${CYAN}正在升级...${NC}"
|
echo -e " ${CYAN}Updating…${NC}"
|
||||||
cd "$CORE_DIR"
|
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)
|
local NEW_VER=$("$NODE_BIN" -e "console.log(require('./node_modules/openclaw/package.json').version)" 2>/dev/null)
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${GREEN}升级完成!${NC} $CUR_VER → $NEW_VER"
|
echo -e " ${GREEN}Updated.${NC} $CUR_VER → $NEW_VER"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── [14] Disk cleanup (P1) ───────────────────────────────────
|
# ── [14] Disk cleanup (P1) ───────────────────────────────────
|
||||||
|
|
||||||
do_cleanup() {
|
do_cleanup() {
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${CYAN}${BOLD}━━━ 清理空间 ━━━${NC}"
|
echo -e " ${CYAN}${BOLD}--- Free up space ---${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Show directory sizes
|
# 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 "$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 "$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 "$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)"
|
[ -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 ""
|
echo ""
|
||||||
|
|
||||||
local CLEANED=0
|
local CLEANED=0
|
||||||
@@ -362,17 +362,17 @@ do_cleanup() {
|
|||||||
local BK_COUNT=$(ls -d "$BACKUP_DIR"/*/ 2>/dev/null | wc -l | tr -d ' ')
|
local BK_COUNT=$(ls -d "$BACKUP_DIR"/*/ 2>/dev/null | wc -l | tr -d ' ')
|
||||||
if [ "$BK_COUNT" -gt 3 ]; then
|
if [ "$BK_COUNT" -gt 3 ]; then
|
||||||
local OLD_COUNT=$((BK_COUNT - 3))
|
local OLD_COUNT=$((BK_COUNT - 3))
|
||||||
echo -e " ${YELLOW}发现 $BK_COUNT 个备份,保留最新 3 个,删除 $OLD_COUNT 个旧备份${NC}"
|
echo -e " ${YELLOW}Found $BK_COUNT backups. Keeping the newest 3, deleting $OLD_COUNT.${NC}"
|
||||||
read -p " 确认? (y/N): " DEL_BK
|
read -p " Go ahead? (y/N): " DEL_BK
|
||||||
if [ "$DEL_BK" = "y" ] || [ "$DEL_BK" = "Y" ]; then
|
if [ "$DEL_BK" = "y" ] || [ "$DEL_BK" = "Y" ]; then
|
||||||
ls -dt "$BACKUP_DIR"/*/ 2>/dev/null | tail -n "$OLD_COUNT" | while read -r DIR; do
|
ls -dt "$BACKUP_DIR"/*/ 2>/dev/null | tail -n "$OLD_COUNT" | while read -r DIR; do
|
||||||
rm -rf "$DIR"
|
rm -rf "$DIR"
|
||||||
done
|
done
|
||||||
echo -e " ${GREEN}旧备份已清理${NC}"
|
echo -e " ${GREEN}Old backups deleted${NC}"
|
||||||
CLEANED=1
|
CLEANED=1
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo " 备份: ${BK_COUNT} 个(无需清理)"
|
echo " Backups: ${BK_COUNT} — nothing to clear"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -380,33 +380,33 @@ do_cleanup() {
|
|||||||
if [ -d "$LOG_DIR" ]; then
|
if [ -d "$LOG_DIR" ]; then
|
||||||
local OLD_LOGS=$(find "$LOG_DIR" -name "*.log" -mtime +7 2>/dev/null | wc -l | tr -d ' ')
|
local OLD_LOGS=$(find "$LOG_DIR" -name "*.log" -mtime +7 2>/dev/null | wc -l | tr -d ' ')
|
||||||
if [ "$OLD_LOGS" -gt 0 ]; then
|
if [ "$OLD_LOGS" -gt 0 ]; then
|
||||||
echo -e " ${YELLOW}发现 $OLD_LOGS 个超过 7 天的日志${NC}"
|
echo -e " ${YELLOW}Found $OLD_LOGS log files older than 7 days${NC}"
|
||||||
read -p " 确认清理? (y/N): " DEL_LOG
|
read -p " Delete them? (y/N): " DEL_LOG
|
||||||
if [ "$DEL_LOG" = "y" ] || [ "$DEL_LOG" = "Y" ]; then
|
if [ "$DEL_LOG" = "y" ] || [ "$DEL_LOG" = "Y" ]; then
|
||||||
find "$LOG_DIR" -name "*.log" -mtime +7 -delete 2>/dev/null
|
find "$LOG_DIR" -name "*.log" -mtime +7 -delete 2>/dev/null
|
||||||
echo -e " ${GREEN}旧日志已清理${NC}"
|
echo -e " ${GREEN}Old logs deleted${NC}"
|
||||||
CLEANED=1
|
CLEANED=1
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo " 日志: 无需清理"
|
echo " Logs: nothing to clear"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Clean npm cache
|
# Clean npm cache
|
||||||
echo ""
|
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
|
if [ "$DEL_CACHE" = "y" ] || [ "$DEL_CACHE" = "Y" ]; then
|
||||||
"$NPM_BIN" cache clean --force 2>/dev/null
|
"$NPM_BIN" cache clean --force 2>/dev/null
|
||||||
echo -e " ${GREEN}npm 缓存已清理${NC}"
|
echo -e " ${GREEN}npm cache cleared${NC}"
|
||||||
CLEANED=1
|
CLEANED=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$CLEANED" = "0" ]; then
|
if [ "$CLEANED" = "0" ]; then
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${GREEN}没有需要清理的内容${NC}"
|
echo -e " ${GREEN}Nothing to clear${NC}"
|
||||||
else
|
else
|
||||||
echo ""
|
echo ""
|
||||||
echo " 清理后总计: $(du -sh "$UCLAW_DIR" 2>/dev/null | cut -f1)"
|
echo " Now using: $(du -sh "$UCLAW_DIR" 2>/dev/null | cut -f1)"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,56 +414,56 @@ do_cleanup() {
|
|||||||
|
|
||||||
do_plugins() {
|
do_plugins() {
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${CYAN}${BOLD}━━━ 插件管理 ━━━${NC}"
|
echo -e " ${CYAN}${BOLD}--- Plugins ---${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${GREEN}[a]${NC} 列出已安装插件"
|
echo -e " ${GREEN}[a]${NC} List installed plugins"
|
||||||
echo -e " ${GREEN}[b]${NC} 安装插件"
|
echo -e " ${GREEN}[b]${NC} Install a plugin"
|
||||||
echo -e " ${GREEN}[c]${NC} 卸载插件"
|
echo -e " ${GREEN}[c]${NC} Remove a plugin"
|
||||||
echo ""
|
echo ""
|
||||||
read -p " 选择 (a-c): " -n 1 PLG_CHOICE
|
read -p " Choose (a-c): " -n 1 PLG_CHOICE
|
||||||
echo ""
|
echo ""
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
case $PLG_CHOICE in
|
case $PLG_CHOICE in
|
||||||
a)
|
a)
|
||||||
echo -e " ${WHITE}已安装插件:${NC}"
|
echo -e " ${WHITE}Installed plugins:${NC}"
|
||||||
echo ""
|
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)
|
b)
|
||||||
echo " 常用插件:"
|
echo " Commonly used:"
|
||||||
echo " @icesword760/openclaw-wechat — 微信"
|
echo " @icesword760/openclaw-wechat WeChat"
|
||||||
echo " @nicepkg/openclaw-plugin-qq — QQ(社区版)"
|
echo " @nicepkg/openclaw-plugin-qq QQ (community build)"
|
||||||
echo ""
|
echo ""
|
||||||
read -p " 输入插件名称(留空取消): " PLG_NAME
|
read -p " Plugin name (blank to cancel): " PLG_NAME
|
||||||
if [ -z "$PLG_NAME" ]; then
|
if [ -z "$PLG_NAME" ]; then
|
||||||
echo -e " ${YELLOW}已取消${NC}"
|
echo -e " ${YELLOW}Cancelled${NC}"
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${CYAN}正在安装 $PLG_NAME ...${NC}"
|
echo -e " ${CYAN}Installing $PLG_NAME…${NC}"
|
||||||
run_oc plugins install "$PLG_NAME" 2>&1
|
run_oc plugins install "$PLG_NAME" 2>&1
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${GREEN}安装完成${NC}"
|
echo -e " ${GREEN}Installed${NC}"
|
||||||
;;
|
;;
|
||||||
c)
|
c)
|
||||||
echo -e " ${WHITE}已安装插件:${NC}"
|
echo -e " ${WHITE}Installed plugins:${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
run_oc plugins list 2>&1 || true
|
run_oc plugins list 2>&1 || true
|
||||||
echo ""
|
echo ""
|
||||||
read -p " 输入要卸载的插件名称(留空取消): " PLG_NAME
|
read -p " Plugin to remove (blank to cancel): " PLG_NAME
|
||||||
if [ -z "$PLG_NAME" ]; then
|
if [ -z "$PLG_NAME" ]; then
|
||||||
echo -e " ${YELLOW}已取消${NC}"
|
echo -e " ${YELLOW}Cancelled${NC}"
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${CYAN}正在卸载 $PLG_NAME ...${NC}"
|
echo -e " ${CYAN}Removing $PLG_NAME…${NC}"
|
||||||
run_oc plugins remove "$PLG_NAME" 2>&1
|
run_oc plugins remove "$PLG_NAME" 2>&1
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${GREEN}卸载完成${NC}"
|
echo -e " ${GREEN}Removed${NC}"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo -e " ${RED}无效选择${NC}"
|
echo -e " ${RED}Not a valid choice${NC}"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|||||||
295
portable/lib/messages/en.json
Normal file
295
portable/lib/messages/en.json
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
{
|
||||||
|
"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.",
|
||||||
|
"tg.steps_title": "After saving, connect your Telegram account:",
|
||||||
|
"tg.step1": "Save the bot token above (click “Open dashboard” or save channels).",
|
||||||
|
"tg.step2": "On your phone, open Telegram, find your bot, and send /start.",
|
||||||
|
"tg.step3": "Come back here — a button will appear. Click it once. No command line.",
|
||||||
|
"tg.waiting": "Waiting for you to send /start to your bot on Telegram…",
|
||||||
|
"tg.pending": "Someone wants to connect: {who}. Click below to allow.",
|
||||||
|
"tg.approve": "Allow this Telegram account",
|
||||||
|
"tg.approving": "Approving…",
|
||||||
|
"tg.approved": "✅ Telegram connected — go chat in Telegram.",
|
||||||
|
"tg.approved_toast": "Telegram account approved.",
|
||||||
|
"tg.approve_failed": "Could not approve: {reason}",
|
||||||
|
"tg.poll_failed": "Could not check pairing status. Is U-Claw still running?",
|
||||||
|
"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. Attach it to an issue at https://gitea.fanghe.it.com/zhenghy/u-claw/issues 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.",
|
||||||
|
"nav.link_contact_d": "Open an issue",
|
||||||
|
"skill.uclaw-help.name": "About U-Claw",
|
||||||
|
"skill.uclaw-help.desc": "How it works, where things are, what to do when something breaks",
|
||||||
|
"cat.uclaw": "U-Claw"
|
||||||
|
}
|
||||||
295
portable/lib/messages/zh-CN.json
Normal file
295
portable/lib/messages/zh-CN.json
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
{
|
||||||
|
"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 盘还插着。",
|
||||||
|
"tg.steps_title": "保存 Bot Token 后,连接你的 Telegram:",
|
||||||
|
"tg.step1": "先保存上面的 Bot Token(点「打开对话界面」或保存聊天软件设置)。",
|
||||||
|
"tg.step2": "在手机上打开 Telegram,找到你的机器人,发送 /start。",
|
||||||
|
"tg.step3": "回到这个页面 —— 会出现一个按钮,点一下就行,不用敲命令。",
|
||||||
|
"tg.waiting": "等你去 Telegram 给机器人发 /start…",
|
||||||
|
"tg.pending": "有人要连接:{who}。点下面按钮允许。",
|
||||||
|
"tg.approve": "允许这个 Telegram 账号",
|
||||||
|
"tg.approving": "正在批准…",
|
||||||
|
"tg.approved": "✅ Telegram 已连通 —— 去 Telegram 里聊吧。",
|
||||||
|
"tg.approved_toast": "Telegram 账号已批准。",
|
||||||
|
"tg.approve_failed": "批准失败:{reason}",
|
||||||
|
"tg.poll_failed": "无法检查配对状态。U-Claw 还在运行吗?",
|
||||||
|
"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": "里面的密钥已被去掉。需要帮忙的话,把它附到 https://gitea.fanghe.it.com/zhenghy/u-claw/issues 的 issue 里。",
|
||||||
|
"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 进去就多一个。",
|
||||||
|
"nav.link_contact_d": "提 issue",
|
||||||
|
"skill.uclaw-help.name": "关于 U-Claw",
|
||||||
|
"skill.uclaw-help.desc": "它怎么用、东西在哪、出问题了怎么办",
|
||||||
|
"cat.uclaw": "U-Claw"
|
||||||
|
}
|
||||||
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>
|
||||||
118
portable/lib/provider-detect.mjs
Normal file
118
portable/lib/provider-detect.mjs
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
// 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',
|
||||||
|
// Google AI Studio now issues Auth keys (AQ.Ab…) alongside legacy Standard keys (AIzaSy…).
|
||||||
|
prefixes: ['AQ.', 'AIza'],
|
||||||
|
baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai',
|
||||||
|
// New AI Studio accounts often cannot use 2.5-flash yet (404). Try 2.0 first.
|
||||||
|
model: 'gemini-2.0-flash',
|
||||||
|
models: ['gemini-2.0-flash', 'gemini-flash-latest', 'gemini-2.5-flash', 'gemini-2.5-flash-lite', '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 === 400 || 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 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Google rotates model availability per account. When the first choice 404s, try the rest.
|
||||||
|
export function modelsToTry(provider, requestedModel) {
|
||||||
|
const primary = String(requestedModel || provider?.model || '').trim();
|
||||||
|
if (provider?.id === 'google') {
|
||||||
|
return [...new Set([primary, provider.model, ...(provider.models || [])].filter(Boolean))];
|
||||||
|
}
|
||||||
|
return primary ? [primary] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// AQ auth keys only work on the OpenAI-compatible surface. Ask Google which models
|
||||||
|
// this key can actually see instead of guessing from a static list.
|
||||||
|
export async function discoverGoogleOpenAiModels(baseUrl, apiKey) {
|
||||||
|
const root = String(baseUrl || '').replace(/\/+$/, '');
|
||||||
|
if (!root || !apiKey) return [];
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${root}/models`, {
|
||||||
|
headers: { Authorization: `Bearer ${apiKey}` },
|
||||||
|
});
|
||||||
|
if (!res.ok) return [];
|
||||||
|
const body = await res.json();
|
||||||
|
const rows = body.data || body.models || [];
|
||||||
|
return rows
|
||||||
|
.map((m) => String(m.id || m.name || '').replace(/^models\//, ''))
|
||||||
|
.filter(Boolean);
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,7 +21,7 @@ const REPO_ROOT = resolve(__dirname, '../..');
|
|||||||
|
|
||||||
// 默认 OSS 路径前缀(你可以改成自己的 bucket / domain)
|
// 默认 OSS 路径前缀(你可以改成自己的 bucket / domain)
|
||||||
const DEFAULT_OSS_BASE = 'https://u-claw-oss.56chat.cn/u-claw-open';
|
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) {
|
function parseArgs(argv) {
|
||||||
const args = { notes: '', ossBase: DEFAULT_OSS_BASE };
|
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,
|
// For offline and corporate-network situations: ask a few questions, write
|
||||||
// 再当场实测能不能连上、能不能回话。全程不依赖会挂的 dashboard 网页。
|
// 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
|
||||||
// 1) Ollama(本机,http://127.0.0.1:11434)
|
// when someone reaches for this tool.
|
||||||
// 2) newapi / 任意 OpenAI 兼容中转(内网 IP + token)
|
|
||||||
//
|
//
|
||||||
// 写入只 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 { readFileSync, writeFileSync, existsSync, copyFileSync, mkdirSync } from 'node:fs';
|
||||||
import { dirname } from 'node:path';
|
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 http from 'node:http';
|
||||||
import https from 'node:https';
|
import https from 'node:https';
|
||||||
|
|
||||||
// 输入抽象:真控制台(TTY)走交互式 readline;被管道喂入(测试/脚本)则一次读完按行出队。
|
|
||||||
function makePrompter() {
|
function makePrompter() {
|
||||||
if (input.isTTY) {
|
if (input.isTTY) {
|
||||||
const rl = createInterface({ input, output });
|
const rl = createInterface({ input, output });
|
||||||
@@ -69,7 +75,7 @@ async function chatTest(baseUrl, apiKey, modelId) {
|
|||||||
const res = await requestText(url, {
|
const res = await requestText(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json', ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}) },
|
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,
|
timeoutMs: CHAT_TIMEOUT_MS,
|
||||||
});
|
});
|
||||||
const ms = Date.now() - started;
|
const ms = Date.now() - started;
|
||||||
@@ -111,10 +117,10 @@ function requestText(rawUrl, { method = 'GET', headers = {}, body, timeoutMs })
|
|||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const configPath = process.argv[2] || process.env.OPENCLAW_CONFIG_PATH;
|
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('========================================');
|
||||||
line(' U-Claw 内网/本地模型 一键配置');
|
line(' U-Claw — set up a local or self-hosted model');
|
||||||
line('========================================');
|
line('========================================');
|
||||||
line('');
|
line('');
|
||||||
|
|
||||||
@@ -122,32 +128,32 @@ async function main() {
|
|||||||
const ask = rl.ask;
|
const ask = rl.ask;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
line('选择模型类型:');
|
line('Which kind of model?');
|
||||||
line(' 1) Ollama(本机部署,http://127.0.0.1:11434)');
|
line(' 1) Ollama running on this machine (http://127.0.0.1:11434)');
|
||||||
line(' 2) newapi / 其它 OpenAI 兼容中转(内网 IP + token)');
|
line(' 2) A self-hosted or company endpoint (any OpenAI-compatible URL + token)');
|
||||||
const kind = await ask('输入 1 或 2', '1');
|
const kind = await ask('Enter 1 or 2', '1');
|
||||||
|
|
||||||
let providerKey, baseUrl, apiKey, modelId;
|
let providerKey, baseUrl, apiKey, modelId;
|
||||||
if (kind === '2') {
|
if (kind === '2') {
|
||||||
providerKey = 'newapi';
|
providerKey = 'newapi';
|
||||||
line('');
|
line('');
|
||||||
line('提示:baseUrl 通常形如 http://192.168.1.50:3000/v1(注意大多要带 /v1)');
|
line('The endpoint usually looks like http://192.168.1.50:3000/v1 — most need the /v1 on the end.');
|
||||||
baseUrl = await ask('newapi 地址 baseUrl', 'http://192.168.1.50:3000/v1');
|
baseUrl = await ask('Endpoint URL', 'http://192.168.1.50:3000/v1');
|
||||||
apiKey = await ask('token / API Key', '');
|
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 {
|
} else {
|
||||||
providerKey = 'ollama';
|
providerKey = 'ollama';
|
||||||
line('');
|
line('');
|
||||||
baseUrl = await ask('Ollama 地址(一般本机默认即可)', 'http://127.0.0.1:11434/v1');
|
baseUrl = await ask('Ollama URL — the default is right unless you changed it', 'http://127.0.0.1:11434/v1');
|
||||||
// Ollama 的 OpenAI 兼容端点在 /v1;自动补上
|
// Ollama's OpenAI-compatible endpoint lives at /v1; add it if missing
|
||||||
if (!/\/v1\/?$/.test(baseUrl)) baseUrl = baseUrl.replace(/\/+$/, '') + '/v1';
|
if (!/\/v1\/?$/.test(baseUrl)) baseUrl = baseUrl.replace(/\/+$/, '') + '/v1';
|
||||||
apiKey = 'ollama'; // 本地占位 key,任意值即可
|
apiKey = 'ollama'; // Ollama ignores auth; any placeholder works
|
||||||
modelId = await ask('模型名(先用 ollama list 查,如 qwen2.5 / llama3.1)', 'qwen2.5');
|
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 = {};
|
let config = {};
|
||||||
if (existsSync(configPath)) {
|
if (existsSync(configPath)) {
|
||||||
try { config = JSON.parse(readFileSync(configPath, 'utf8')); }
|
try { config = JSON.parse(readFileSync(configPath, 'utf8')); }
|
||||||
@@ -174,39 +180,39 @@ async function main() {
|
|||||||
|
|
||||||
writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
|
writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
|
||||||
line('');
|
line('');
|
||||||
line(`✓ 已写入配置:${configPath}`);
|
line(`✓ Saved to ${configPath}`);
|
||||||
line(` provider=${providerKey} baseUrl=${baseUrl} model=${modelId}`);
|
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('正在实测:发一条对话给模型...');
|
line('Testing it — sending one message to the model…');
|
||||||
const r = await chatTest(baseUrl, apiKey, modelId);
|
const r = await chatTest(baseUrl, apiKey, modelId);
|
||||||
line('');
|
line('');
|
||||||
if (r.ok) {
|
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('');
|
||||||
line('配置完成。现在双击 Windows-Start.bat 即可正常使用(对话可走 CLI 或 Dashboard)。');
|
line('You are set. Start U-Claw normally and it will use this model.');
|
||||||
} else if (r.status) {
|
} 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('');
|
line('');
|
||||||
if (r.status === 401 || r.status === 403) line('→ 网络通,但 token / key 不对(Ollama 可忽略鉴权,newapi 请核对 token)。');
|
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('→ 网络通,但路径或模型 ID 不对(检查 baseUrl 是否要带 /v1、模型名是否正确)。');
|
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('→ 网络通,服务端报错,把上面内容发管理员。');
|
else line(' The network is fine; the server itself errored. Send the text above to whoever runs it.');
|
||||||
} else {
|
} else {
|
||||||
const host = (() => { try { return new URL(withScheme(baseUrl)).hostname; } catch { return baseUrl; } })();
|
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('');
|
line('');
|
||||||
if (providerKey === 'ollama') {
|
if (providerKey === 'ollama') {
|
||||||
line('→ 本机 Ollama 没连上,多半是 Ollama 没启动或模型没拉。请在本机执行:');
|
line(' Ollama is not answering. It is usually not running, or the model was never pulled. On this machine, run:');
|
||||||
line(' ollama serve (启动服务,若已是后台服务可跳过)');
|
line(' ollama serve starts it, skip if it already runs in the background');
|
||||||
line(` ollama pull ${modelId} (把模型拉到本地,离线需提前准备好)`);
|
line(` ollama pull ${modelId} downloads the model — do this while you still have internet`);
|
||||||
line(' 然后重新运行本工具。');
|
line(' Then run this tool again.');
|
||||||
} else {
|
} else {
|
||||||
line('→ 是地址错 / 内网不通 / 防火墙 / 模型服务没起。');
|
line(' Either the address is wrong, the network cannot reach it, a firewall is blocking it, or the service is down.');
|
||||||
line(' 在这台机器上自测:');
|
line(' Test it from this machine with:');
|
||||||
line(` ping ${host}`);
|
line(` ping ${host}`);
|
||||||
line(' 让机房管理员确认 IP、端口、防火墙放行。');
|
line(' If that also fails, ask whoever runs the server to confirm the address, port and firewall rules.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
576
portable/lib/start.mjs
Normal file
576
portable/lib/start.mjs
Normal file
@@ -0,0 +1,576 @@
|
|||||||
|
#!/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://.
|
||||||
|
function readUclawMeta() {
|
||||||
|
try {
|
||||||
|
const metaPath = join(paths.state, 'uclaw-meta.json');
|
||||||
|
if (!existsSync(metaPath)) return null;
|
||||||
|
return JSON.parse(readFileSync(metaPath, 'utf8'));
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function localeFromLocaleJs() {
|
||||||
|
try {
|
||||||
|
const content = readFileSync(join(paths.state, 'locale.js'), 'utf8');
|
||||||
|
const match = content.match(/UCLAW_LOCALE\s*=\s*(["'])([^"']+)\1/);
|
||||||
|
return match ? match[2] : null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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') || localeFromLocaleJs());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
const fromConfig = JSON.parse(readFileSync(paths.config, 'utf8'))?.uclaw?.[key];
|
||||||
|
if (fromConfig !== undefined) return fromConfig;
|
||||||
|
} catch {
|
||||||
|
/* try sidecar */
|
||||||
|
}
|
||||||
|
const fromMeta = readUclawMeta()?.[key];
|
||||||
|
return fromMeta !== undefined ? fromMeta : 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. **结尾有力**: 回扣问题,留下思考空间
|
|
||||||
87
portable/skills/README.md
Normal file
87
portable/skills/README.md
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
# Skills
|
||||||
|
|
||||||
|
Single source of truth for skill content and distribution.
|
||||||
|
|
||||||
|
## Why this directory exists
|
||||||
|
|
||||||
|
Skill content used to be inlined as heredocs inside `install/install.sh` and as
|
||||||
|
here-strings inside `install/install.ps1`, with a third copy on disk under
|
||||||
|
`portable/skills-cn/`. The three drifted:
|
||||||
|
|
||||||
|
| Source | Skills | Typical length |
|
||||||
|
|---|---|---|
|
||||||
|
| `portable/skills-cn/` | 17 | full |
|
||||||
|
| `install/install.sh` | 10 | ~40% of full |
|
||||||
|
| `install/install.ps1` | 10 | ~17% of full |
|
||||||
|
|
||||||
|
Seven skills (`excel-helper`, `word-writer`, `ppt-designer`, `pdf-toolkit`,
|
||||||
|
`image-compress`, `qrcode-maker`, `web-to-markdown`) existed **only** on the USB
|
||||||
|
build — anyone who used the one-line installer never got them.
|
||||||
|
|
||||||
|
`tests/windows-launchers.test.mjs` also requires customer-facing `.bat` launchers
|
||||||
|
to be pure ASCII, because Chinese Windows `cmd.exe` reads non-ASCII bytes as GBK
|
||||||
|
and mis-parses the script. That constraint is what pushed the `.ps1` copies into
|
||||||
|
being truncated in the first place.
|
||||||
|
|
||||||
|
Separating content from distribution removes all of this: the installers carry no
|
||||||
|
skill text at all.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
skills/
|
||||||
|
manifest.json # the only thing installers read
|
||||||
|
en/
|
||||||
|
<skill-id>/SKILL.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## manifest.json
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"personas": [ { "id": "developer", "tier": "expert" }, ... ],
|
||||||
|
"skills": [
|
||||||
|
{
|
||||||
|
"id": "excel-helper",
|
||||||
|
"status": "shipping", // "shipping" | "planned"
|
||||||
|
"locales": ["en"], // which locales have a SKILL.md on disk
|
||||||
|
"categories": ["office"],
|
||||||
|
"personas": ["admin", "finance"],
|
||||||
|
"emoji": "📊"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`status: "planned"` entries have no content yet. They are listed so the roadmap
|
||||||
|
lives next to the code — installers skip them. `replaces` records which retired
|
||||||
|
skill an entry supersedes.
|
||||||
|
|
||||||
|
## Installing
|
||||||
|
|
||||||
|
`lib/install-skills.mjs` is the only installer. It reads the manifest, filters by
|
||||||
|
locale and persona, and writes to the target directory.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node lib/install-skills.mjs --target <dir> [--locale en] [--persona general] [--dry-run] [--list]
|
||||||
|
```
|
||||||
|
|
||||||
|
It resolves content in this order:
|
||||||
|
|
||||||
|
1. `skills/` next to the script (repo checkout and USB builds)
|
||||||
|
2. `--source <dir>` if given
|
||||||
|
3. Download from GitHub at the pinned ref (one-line remote installers, which have
|
||||||
|
no repo on disk)
|
||||||
|
|
||||||
|
## Adding a skill
|
||||||
|
|
||||||
|
1. Create `skills/en/<id>/SKILL.md` with the standard front matter
|
||||||
|
2. Add an entry to `manifest.json` with `status: "shipping"`
|
||||||
|
3. Run `node --test tests/` — `tests/skills-manifest.test.mjs` checks that every
|
||||||
|
shipping skill has content, that front matter matches the manifest, and that
|
||||||
|
the shell and PowerShell installers agree on the resulting skill list
|
||||||
|
|
||||||
|
Do not add skill text to any `.sh`, `.ps1` or `.bat` file. The parity test fails
|
||||||
|
if the two installers disagree, and the ASCII test fails if non-ASCII ends up in a
|
||||||
|
`.bat`.
|
||||||
77
portable/skills/en/claude-helper/SKILL.md
Normal file
77
portable/skills/en/claude-helper/SKILL.md
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
---
|
||||||
|
name: claude-helper
|
||||||
|
description: "Getting better results from the model - prompting, context, and knowing when the tool is wrong for the job"
|
||||||
|
metadata: { "openclaw": { "emoji": "🤖" } }
|
||||||
|
---
|
||||||
|
|
||||||
|
# Getting Better Results
|
||||||
|
|
||||||
|
How to ask, what to include, and when to stop asking.
|
||||||
|
|
||||||
|
## The three things that change output most
|
||||||
|
|
||||||
|
**1. Say what the output is for.** "Summarise this" and "summarise this for
|
||||||
|
someone deciding whether to attend" produce different, both-correct summaries.
|
||||||
|
The purpose does more work than any phrasing trick.
|
||||||
|
|
||||||
|
**2. Give the real material.** A paraphrase of a document produces an answer
|
||||||
|
about the paraphrase. Paste the document, attach the file, share the error in
|
||||||
|
full — including the parts that look irrelevant.
|
||||||
|
|
||||||
|
**3. Say what a good answer looks like.** Length, format, who reads it. "Three
|
||||||
|
bullets a non-technical manager can act on" beats "be concise".
|
||||||
|
|
||||||
|
## What does not help
|
||||||
|
|
||||||
|
- Politeness formulas, threats, or claiming urgency
|
||||||
|
- "You are a world-class expert in…" — state the task, not a persona
|
||||||
|
- Asking the same thing again in the hope of a different answer. Change what you
|
||||||
|
gave it instead.
|
||||||
|
|
||||||
|
## When to start a new conversation
|
||||||
|
|
||||||
|
Long conversations drift. Start fresh when:
|
||||||
|
- You have changed subject entirely
|
||||||
|
- Earlier wrong turns keep resurfacing
|
||||||
|
- The context has filled with material that no longer matters
|
||||||
|
|
||||||
|
Carry forward a short summary rather than the whole history.
|
||||||
|
|
||||||
|
## When the answer might be wrong
|
||||||
|
|
||||||
|
Language models produce fluent text regardless of whether they know the answer.
|
||||||
|
Confidence is not a signal. Check independently when the answer involves:
|
||||||
|
|
||||||
|
- **Specific numbers, dates, prices, versions** — especially recent ones
|
||||||
|
- **Citations, links, case law, standards** — these get fabricated convincingly
|
||||||
|
- **Anything you will act on** without being able to reverse it
|
||||||
|
|
||||||
|
Ask for the reasoning or the source, and treat a refusal to give one as a warning.
|
||||||
|
|
||||||
|
## When to use something else
|
||||||
|
|
||||||
|
- **Arithmetic on real data** — use a spreadsheet or a script, not the model
|
||||||
|
- **Anything needing today's facts** — the model needs to be given them
|
||||||
|
- **Legal, medical or financial decisions** — a draft to take to a professional,
|
||||||
|
not the answer
|
||||||
|
|
||||||
|
## Example prompts
|
||||||
|
|
||||||
|
```
|
||||||
|
Rewrite my prompt so it gets a more useful answer
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
What information are you missing to answer this properly?
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
Which parts of that answer should I verify before I use it?
|
||||||
|
```
|
||||||
|
|
||||||
|
## Working notes
|
||||||
|
|
||||||
|
- When the user's request is ambiguous in a way that changes the answer, ask one
|
||||||
|
question rather than producing two versions.
|
||||||
|
- Say plainly when something is outside what you can check, rather than hedging
|
||||||
|
through it.
|
||||||
77
portable/skills/en/email-campaign/SKILL.md
Normal file
77
portable/skills/en/email-campaign/SKILL.md
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
---
|
||||||
|
name: email-campaign
|
||||||
|
description: "Marketing and outreach email - subject lines, structure, and not getting marked as spam"
|
||||||
|
metadata: { "openclaw": { "emoji": "✉️" } }
|
||||||
|
---
|
||||||
|
|
||||||
|
# Email Campaigns
|
||||||
|
|
||||||
|
Newsletters, product announcements and outreach.
|
||||||
|
|
||||||
|
## Subject lines
|
||||||
|
|
||||||
|
The only part most recipients read. A good one is specific and short enough to
|
||||||
|
survive a phone's inbox — roughly 40 characters.
|
||||||
|
|
||||||
|
| Weak | Better |
|
||||||
|
|---|---|
|
||||||
|
| "Our January Newsletter" | "We cut setup from 20 minutes to 3" |
|
||||||
|
| "Exciting product update!" | "Export to Excel is live" |
|
||||||
|
| "Quick question" | "Question about your Q3 rollout" |
|
||||||
|
|
||||||
|
Avoid: ALL CAPS, three exclamation marks, "RE:" on a mail that is not a reply,
|
||||||
|
and fake urgency. These are also the things spam filters weigh.
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
One line Why this email exists
|
||||||
|
The point What changed, what you are offering, what you want
|
||||||
|
One action A single link or ask
|
||||||
|
Sign-off A real name
|
||||||
|
```
|
||||||
|
|
||||||
|
**One ask per email.** Two links means neither gets clicked.
|
||||||
|
|
||||||
|
## Preview text
|
||||||
|
|
||||||
|
The line after the subject in most clients. It is a second subject line, and
|
||||||
|
leaving it to default means the recipient reads "View this email in your
|
||||||
|
browser". Write it.
|
||||||
|
|
||||||
|
## Landing in the inbox
|
||||||
|
|
||||||
|
- Send from a real person's address, not `noreply@`
|
||||||
|
- Working unsubscribe link, honoured immediately — in Singapore this is a legal
|
||||||
|
requirement under the PDPA, not a courtesy
|
||||||
|
- Do not attach; link instead
|
||||||
|
- Plain-text-ish beats a heavy template for anything that is meant to read as
|
||||||
|
personal
|
||||||
|
|
||||||
|
## Cold outreach
|
||||||
|
|
||||||
|
Different rules. Reference something specific about them in the first line — if
|
||||||
|
you cannot, you are not ready to send it. Keep it under 120 words. Ask for a
|
||||||
|
reply, not a meeting.
|
||||||
|
|
||||||
|
## Example prompts
|
||||||
|
|
||||||
|
```
|
||||||
|
Announcement email for this release — one ask, keep it short
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
Five subject lines for this, no hype
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
Rewrite this cold email — it reads like a template
|
||||||
|
```
|
||||||
|
|
||||||
|
## Working notes
|
||||||
|
|
||||||
|
- Ask who the list is and what they last heard from you. An email to people who
|
||||||
|
signed up last week is not the same as one to a year-old list.
|
||||||
|
- Never invent a personalisation detail in outreach. Getting it wrong is worse
|
||||||
|
than sending something generic.
|
||||||
|
- Do not send anything on the user's behalf. Draft it; they send it.
|
||||||
101
portable/skills/en/excel-helper/SKILL.md
Normal file
101
portable/skills/en/excel-helper/SKILL.md
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
---
|
||||||
|
name: excel-helper
|
||||||
|
description: "Spreadsheet helper - formulas, pivot tables, charts, data cleaning, macros"
|
||||||
|
metadata: { "openclaw": { "emoji": "📊" } }
|
||||||
|
---
|
||||||
|
|
||||||
|
# Spreadsheet Helper
|
||||||
|
|
||||||
|
Data processing and analysis for Excel, Google Sheets and Numbers.
|
||||||
|
|
||||||
|
## What it does
|
||||||
|
|
||||||
|
- **Formulas**: lookups, conditional sums, aggregation, statistics
|
||||||
|
- **Pivot tables**: fast summaries, cross-tabs, grouping
|
||||||
|
- **Charts**: column, line, pie, combo — and which one to pick
|
||||||
|
- **Data cleaning**: dedupe, fill gaps, normalise formats
|
||||||
|
- **Macros**: recording and writing VBA
|
||||||
|
|
||||||
|
## Formula patterns
|
||||||
|
|
||||||
|
### Aggregation
|
||||||
|
```excel
|
||||||
|
=SUMIFS(B:B, A:A, ">="&DATE(2026,1,1)) -- conditional sum
|
||||||
|
=COUNTIF(B:B, ">100") -- conditional count
|
||||||
|
=AVERAGEIF(C:C, "Singapore", D:D) -- conditional average
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lookups
|
||||||
|
```excel
|
||||||
|
=VLOOKUP(E2, Source!A:C, 3, FALSE) -- vertical lookup
|
||||||
|
=INDEX(B:B, MATCH(D2, A:A, 0)) -- reverse lookup
|
||||||
|
=XLOOKUP(key, lookup_range, return_range) -- modern lookup, prefer this
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dates
|
||||||
|
```excel
|
||||||
|
=TEXT(A2, "DD/MM/YYYY") -- SG date format
|
||||||
|
=EDATE(B2, 3) -- add/subtract months
|
||||||
|
=WEEKDAY(C2, 2) -- day of week, Mon = 1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Picking a chart
|
||||||
|
|
||||||
|
| What you're showing | Chart |
|
||||||
|
|---|---|
|
||||||
|
| Change over time | Line |
|
||||||
|
| Comparing sizes | Column |
|
||||||
|
| Parts of a whole | Pie (only if ≤ 5 slices) |
|
||||||
|
| Relationship between two variables | Scatter |
|
||||||
|
| Several metrics at once | Radar |
|
||||||
|
|
||||||
|
## Pivot table basics
|
||||||
|
|
||||||
|
1. Drag fields into Rows / Columns / Values / Filters
|
||||||
|
2. Value summary: sum, count, average
|
||||||
|
3. Value display: % of total, running total
|
||||||
|
4. Slicers for interactive filtering
|
||||||
|
|
||||||
|
## Example prompts
|
||||||
|
|
||||||
|
```
|
||||||
|
Write me a formula that totals monthly sales broken down by region
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
This customer list has lots of duplicates — give me cleaning steps
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
Turn this data into a chart showing the quarter-on-quarter trend
|
||||||
|
```
|
||||||
|
|
||||||
|
## A macro to start from
|
||||||
|
|
||||||
|
```vba
|
||||||
|
Sub HighlightLargeValues()
|
||||||
|
For Each cell In Selection
|
||||||
|
If cell.Value > 100 Then
|
||||||
|
cell.Interior.Color = RGB(255, 0, 0)
|
||||||
|
End If
|
||||||
|
Next
|
||||||
|
End Sub
|
||||||
|
```
|
||||||
|
|
||||||
|
## Good for
|
||||||
|
|
||||||
|
- Summary reports
|
||||||
|
- Reconciliation
|
||||||
|
- Sales analysis
|
||||||
|
- Inventory tracking
|
||||||
|
- Survey results
|
||||||
|
|
||||||
|
## Not for
|
||||||
|
|
||||||
|
- Long-form documents (use word-writer)
|
||||||
|
- Slide decks (use ppt-designer)
|
||||||
|
|
||||||
|
## Accuracy
|
||||||
|
|
||||||
|
When numbers matter: show the working, state where each figure came from, and say
|
||||||
|
so plainly when a result is uncertain rather than guessing.
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user