diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9f3d5a3..c14bedc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -100,6 +100,25 @@ jobs: --no-audit \ --no-fund || true + # 5b) Pre-install China channel plugins (钉钉/飞书中国版/企业微信) + # 借鉴自 BytePioneer-AI/openclaw-china 生态:这些是非官方主仓库插件, + # 官方镜像默认不带 → U-Claw 离线预置 = 最大差异化价值。国内客户开箱即用, + # 不用自己 npm install(很多客户机没法翻墙装非镜像包)。 + # 钉钉走 WebSocket 出站免公网 IP,是国内最易接入的渠道。 + # 全部 || true:装不上不阻断发版(个别包偶发 404 时便携版仍可用微信/QQ)。 + for cn_pkg in \ + "@openclaw-china/dingtalk" \ + "@openclaw-china/feishu-china" \ + "@openclaw-china/wecom" \ + "@openclaw-china/qqbot"; do + echo "Installing China channel plugin: $cn_pkg" + npm install "${cn_pkg}@latest" \ + --registry="$mirror_npm" \ + --ignore-scripts \ + --no-audit \ + --no-fund || echo "WARN: $cn_pkg install failed, skipping (便携版仍可用其它渠道)" + done + # 6) Copy China-optimized skills if present skills_cn="$stage_dir/skills-cn" skills_target="$core_dir/node_modules/openclaw/skills" @@ -307,6 +326,7 @@ jobs: - 打开 Config 选模型、填入自己的 API Key 即可使用,支持 DeepSeek / 通义 / Claude / GPT 等国内外大模型 - 想用「虾盘云」中转站(一个 Key 调用国内外全部模型、免翻墙):先到 https://cloud.u-claw.org 注册并创建 Key + - **聊天渠道开箱即用**:便携版已离线预装 微信 / QQ / 钉钉 / 飞书 / 企业微信 插件,无需自己 npm 安装 - OpenClaw runtime: `${{ steps.openclaw_version.outputs.version }}` ### 下载 diff --git a/portable/Config.html b/portable/Config.html index 8b0e846..4de04d9 100644 --- a/portable/Config.html +++ b/portable/Config.html @@ -237,6 +237,13 @@ input:focus { border-color: #ff6b35; } + + +
@@ -332,6 +339,21 @@ input:focus { border-color: #ff6b35; } +
+

📲 钉钉 国内·最易

+

WebSocket 接入,免公网 IP

+
+
+ + +
+
+ + +

→ 从钉钉开放平台创建机器人应用

+
+
+

🏢 企业微信

接入企业微信机器人

@@ -438,10 +460,15 @@ function setupStep2() { 'uclaw-cloud': '虾盘云', minimax: 'MiniMax', kimi: 'Kimi (月之暗面)', deepseek: 'DeepSeek', zai: '智谱 GLM', qwen: '通义千问', doubao: '豆包', openai: 'OpenAI', anthropic: 'Anthropic Claude', groq: 'Groq', - siliconflow: '硅基流动', custom: '自定义' + siliconflow: '硅基流动', custom: '自定义', + ollama: 'Ollama(本地)', lmstudio: 'LM Studio(本地)' }; const name = providerNames[selectedProvider] || selectedProvider; - document.getElementById('step2desc').textContent = `请填写 ${name} 的 API Key`; + 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}"]`); @@ -514,8 +541,13 @@ function buildOpenClawConfig(provider, base, model, apiKey) { // --- Save config --- async function saveConfig() { - const apiKey = document.getElementById('apiKey').value.trim(); - if (!apiKey) { showToast('请填写 API Key', true); return; } + 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; @@ -582,6 +614,52 @@ async function loadConfig() { } 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 = ` + +

${p.label} 本地

+

${modelId}

`; + 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)); @@ -641,6 +719,10 @@ async function launchOpenClaw() { 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 { diff --git a/portable/config-server/server.js b/portable/config-server/server.js index ac130aa..f196846 100644 --- a/portable/config-server/server.js +++ b/portable/config-server/server.js @@ -443,6 +443,40 @@ const server = http.createServer((req, res) => { return; } + // API: Discover local models (Ollama / LM Studio) + // 借鉴 RealShocky/openclaw-windows:自动探测本机已装的本地模型, + // 用户无需手填 baseUrl/模型名,直接点选即可(便携版纯离线推理卖点)。 + // 静默失败:探测不到就返回空数组,不影响 Config 页面。 + if (req.url === '/api/local-models' && req.method === 'GET') { + (async () => { + const probes = [ + { provider: 'ollama', label: 'Ollama', base: 'http://127.0.0.1:11434/v1', api: 'http://127.0.0.1:11434/api/tags' }, + { provider: 'lmstudio', label: 'LM Studio', base: 'http://127.0.0.1:1234/v1', api: 'http://127.0.0.1:1234/v1/models' }, + ]; + const found = []; + await Promise.all(probes.map(async (p) => { + try { + const ctrl = new AbortController(); + const t = setTimeout(() => ctrl.abort(), 1200); + const r = await fetch(p.api, { signal: ctrl.signal }); + clearTimeout(t); + if (!r.ok) return; + const data = await r.json(); + // Ollama: { models:[{name}] } | LM Studio (OpenAI-style): { data:[{id}] } + const models = Array.isArray(data.models) + ? data.models.map(m => m.name).filter(Boolean) + : Array.isArray(data.data) + ? data.data.map(m => m.id).filter(Boolean) + : []; + if (models.length) found.push({ provider: p.provider, label: p.label, base: p.base, models }); + } catch { /* 探测失败:该 provider 未运行,跳过 */ } + })); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ providers: found })); + })(); + return; + } + // API: Save config if (req.url === '/api/config' && req.method === 'POST') { let body = ''; diff --git a/portable/skills-cn/image-compress/SKILL.md b/portable/skills-cn/image-compress/SKILL.md new file mode 100644 index 0000000..349e78b --- /dev/null +++ b/portable/skills-cn/image-compress/SKILL.md @@ -0,0 +1,50 @@ +--- +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 可保留——按需选格式 diff --git a/portable/skills-cn/pdf-toolkit/SKILL.md b/portable/skills-cn/pdf-toolkit/SKILL.md new file mode 100644 index 0000000..62de0b8 --- /dev/null +++ b/portable/skills-cn/pdf-toolkit/SKILL.md @@ -0,0 +1,59 @@ +--- +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(提示用户) +- 输出文件默认放在与源文件同目录,操作完告诉用户生成的文件名 diff --git a/portable/skills-cn/qrcode-maker/SKILL.md b/portable/skills-cn/qrcode-maker/SKILL.md new file mode 100644 index 0000000..8162428 --- /dev/null +++ b/portable/skills-cn/qrcode-maker/SKILL.md @@ -0,0 +1,52 @@ +--- +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 展示给用户看 +- 内容过长(如整段文字)二维码会很密集,提示用户改用短链 diff --git a/portable/skills-cn/web-to-markdown/SKILL.md b/portable/skills-cn/web-to-markdown/SKILL.md new file mode 100644 index 0000000..e73c761 --- /dev/null +++ b/portable/skills-cn/web-to-markdown/SKILL.md @@ -0,0 +1,51 @@ +--- +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 技能