Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion .github/workflows/installers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,37 @@ jobs:
compression-level: 0
retention-days: 3

linux-tarballs:
needs: verify
strategy:
fail-fast: false
matrix:
include:
- runner: ubuntu-latest
arch: x64
- runner: ubuntu-24.04-arm
arch: arm64
runs-on: ${{ matrix.runner }}
timeout-minutes: 60
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: ${{ env.NODE_VERSION }}
cache: npm
- run: npm ci
- name: Build and verify Linux tar.gz
run: npm run desktop:linux:tarball -- --${{ matrix.arch }}
- uses: actions/upload-artifact@v7
with:
name: botcf-agent-linux-${{ matrix.arch }}
path: out/installers/botcf-agent-*-linux-${{ matrix.arch }}.tar.gz
if-no-files-found: error
compression-level: 0
retention-days: 3

publish:
needs: [verify, macos-arm64, windows-x64]
needs: [verify, macos-arm64, windows-x64, linux-tarballs]
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
Expand Down
26 changes: 22 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

<p align="center">
<a href="LICENSE"><img alt="License: PolyForm Noncommercial 1.0.0" src="https://img.shields.io/badge/license-PolyForm%20Noncommercial-blue"></a>
<img alt="Platforms: macOS and Windows" src="https://img.shields.io/badge/platform-macOS%20%7C%20Windows-lightgrey">
<img alt="Platforms: macOS, Windows, and Linux" src="https://img.shields.io/badge/platform-macOS%20%7C%20Windows%20%7C%20Linux-lightgrey">
<img alt="Powered by DeepSeek Harness" src="https://img.shields.io/badge/agent-DeepSeek%20Harness-6b5cff">
</p>

Expand All @@ -32,7 +32,7 @@ BOTCF的 Agent 主引擎已经替换为 DeepSeek Harness(DSH)。开发服务

## 第一次使用

1. 从本项目的 Releases 页面下载 macOS Apple 芯片版或 Windows x64 安装包。
1. 从本项目的 Releases 页面下载 macOS Apple 芯片版、Windows x64 安装包,或与机器架构匹配的 Linux tar.gz
2. 启动 BOTCF Agent,并在登录页输入 BOTCF 账号。
3. 登录成功后,应用会自动补齐专属兜底 Key,并同步当前 Key 的模型与思考能力;默认使用 `🚀codex-pro`,也可以随时切换到其他 Key,前台模型列表会随之替换。

Expand Down Expand Up @@ -95,7 +95,7 @@ DSH 模型选择器采用三级来源解析思考等级:先读取 New API `/v1

## 桌面安装包

公开发布提供两个原生安装包:macOS Apple 芯片版(DMG)Windows x64 版(NSIS EXE)。两者都由 GitHub Actions 在对应系统的原生 Runner 上构建,避免原生模块交叉编译导致不兼容。应用内更新还会使用 macOS ZIP、blockmap 与 `latest*.yml` 元数据
公开发布提供 macOS Apple 芯片版(DMG)Windows x64 版(NSIS EXE)和 Linux x64/arm64 目录压缩包。Linux 版本由 Linux 原生环境生成 `tar.gz`,解压后使用包内 `install.sh` 安装,不依赖 `deb` 或 AppImage;所有平台都应在目标平台构建,避免原生模块交叉编译导致不兼容

Windows 的 Sharp、Koffi、SWC、ripgrep、Node 内建加载器和 node-pty 都包含平台原生文件,并随包携带经过 SHA-256 固定校验的官方 MinGit,版本中心不依赖系统 `PATH` 或用户另行安装 Git。发布校验不仅检查 npm 依赖清单和 Windows x64 PE 文件,还会通过打包后的 Electron 实际启动 ripgrep,防止 `app.asar` 虚拟路径被误传给子进程;完整操作和故障案例见[桌面打包与应用内更新手册](docs/desktop-release-playbook.md)。

Expand All @@ -105,8 +105,26 @@ npm run desktop:installer -- --mac dmg zip --arm64

# Windows x64
npm run desktop:installer -- --win nsis --x64

# Linux x64(解压后执行 install.sh)
npm run desktop:linux:tarball -- --x64

# Linux arm64
npm run desktop:linux:tarball -- --arm64
```

Linux 压缩包默认支持用户级安装:

```bash
tar -xzf out/installers/botcf-agent-X.Y.Z-linux-x64.tar.gz
cd botcf-agent-X.Y.Z-linux-x64
./install.sh
```

需要安装到 `/opt` 并创建系统级桌面入口时使用 `sudo ./install.sh --system`。Linux 版本仍需要兼容的 glibc、Electron 图形库、Bash 和 Git;应用会在启动前动态选择可用的 Secret Service 或 KWallet 后端。相关服务都不可用时,自动登录和 SSH 凭据只能保持在当前进程中。Linux 当前不启用应用内更新,升级时重新下载并执行安装脚本即可。

卸载用户级安装运行 `./uninstall.sh`;卸载系统级安装运行 `sudo ./uninstall.sh --system`。卸载默认保留登录凭据、用户配置和 DSH 数据。

发布流水线优先使用 Apple Developer ID、公证凭据和 Windows Authenticode 证书;仓库未配置商业证书时,macOS 改用固定 requirement 的 ad-hoc 测试签名,Windows 生成未签名测试安装包,并在发布说明中明确风险。发布页同时提供 SHA-256 校验值用于核对文件完整性。签名证书和密码只允许通过 CI Secret 注入,禁止提交到仓库。

### 软件更新
Expand Down Expand Up @@ -185,7 +203,7 @@ npm run lint
- 服务容灾:按配置的 BOTCF 地址依次尝试,网络故障时自动切换备用地址,凭据错误不会盲目重试。
- 请求优化:只读取当前选中 Token 的密钥;响应设有超时和体积限制,避免无关请求、慢请求或异常大响应占用进程。
- 并发登录:登录请求按顺序执行并带版本标识,新请求不会复用旧请求的结果。
- 自动登录:优先用 Electron `safeStorage` 加密密码;系统安全存储不可用时仅在当前进程内保留,不向磁盘写入明文。
- 自动登录:优先用 Electron `safeStorage` 加密密码;Linux 启动时按桌面环境和 D-Bus 服务动态选择 `gnome-libsecret` 或 `kwallet5/6`,系统安全存储不可用时仅在当前进程内保留,不向磁盘写入明文。
- 会话校验:登录后定期检查账号状态;短时网络波动保留当前会话,连续失败达到阈值后才清理模型路由并返回登录页。
- Token 管理:内置控制台支持创建、编辑、启停、删除和切换当前 Token;前台只展示当前 Token 可用的模型。
- 模型能力:优先使用远端元数据,结合兼容规则和用户手动覆盖生成推理等级;上下文窗口切换后会重新同步现有 BOTCF 路由。
Expand Down
1 change: 1 addition & 0 deletions desktop/dsh-runtime.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export function startDshRuntime({
"127.0.0.1",
"--port",
"0",
"--no-open",
];
const child = spawn(nodeExecutable, arguments_, {
cwd: workspace,
Expand Down
84 changes: 84 additions & 0 deletions desktop/linux-password-store.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { execFileSync } from "node:child_process";

const SERVICE_BACKENDS = Object.freeze([
Object.freeze({ backend: "kwallet6", service: "org.kde.kwalletd6" }),
Object.freeze({ backend: "kwallet5", service: "org.kde.kwalletd5" }),
Object.freeze({ backend: "kwallet", service: "org.kde.kwalletd" }),
Object.freeze({ backend: "gnome-libsecret", service: "org.freedesktop.secrets" }),
]);

function desktopValues(env) {
return [
env.XDG_CURRENT_DESKTOP,
env.XDG_SESSION_DESKTOP,
env.DESKTOP_SESSION,
]
.filter((value) => typeof value === "string")
.flatMap((value) => value.split(/[:;,]/u))
.map((value) => value.trim().toLowerCase())
.filter(Boolean);
}

function defaultServiceProbe(service, env = process.env) {
if (!env.DBUS_SESSION_BUS_ADDRESS) return false;
try {
execFileSync("busctl", ["--user", "status", service], {
stdio: "ignore",
timeout: 1_200,
});
return true;
} catch {
return false;
}
}

function kdeBackends(env, values) {
const version = String(env.KDE_SESSION_VERSION || "").trim();
if (version === "6" || values.some((value) => value === "kde6" || value === "plasma6")) {
return ["kwallet6", "kwallet5", "kwallet"];
}
if (version === "5" || values.some((value) => value === "kde5" || value === "plasma5")) {
return ["kwallet5", "kwallet6", "kwallet"];
}
return ["kwallet6", "kwallet5", "kwallet"];
}

function serviceForBackend(backend) {
return SERVICE_BACKENDS.find((entry) => entry.backend === backend)?.service;
}

/**
* Select a usable Chromium password-store backend before Electron emits ready.
* An empty result intentionally leaves Electron's own platform heuristic intact.
*/
export function detectLinuxPasswordStore({
env = process.env,
probeService = defaultServiceProbe,
} = {}) {
const probe = probeService === defaultServiceProbe
? (service) => defaultServiceProbe(service, env)
: probeService;
const values = desktopValues(env);
const isKde = values.some((value) => value === "kde" || value.startsWith("plasma")) ||
/^(1|true|yes)$/iu.test(String(env.KDE_FULL_SESSION || "")) ||
Boolean(env.KDE_SESSION_VERSION);

if (isKde) {
for (const backend of kdeBackends(env, values)) {
const service = serviceForBackend(backend);
if (service && probe(service)) return backend;
}
}

const secretService = serviceForBackend("gnome-libsecret");
if (secretService && probe(secretService)) return "gnome-libsecret";

if (!isKde) {
for (const backend of ["kwallet6", "kwallet5", "kwallet"]) {
const service = serviceForBackend(backend);
if (service && probe(service)) return backend;
}
}

return "";
}
8 changes: 8 additions & 0 deletions desktop/main.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
} from "electron";
import { startDshRuntime } from "./dsh-runtime.mjs";
import { DEFAULT_NEWAPI_ROUTES } from "./domains/auth.mjs";
import { detectLinuxPasswordStore } from "./linux-password-store.mjs";
import { bundledPluginMode } from "./local-plugins.mjs";
import {
clearPendingPlugin,
Expand All @@ -35,6 +36,13 @@ import {

const productName = app.isPackaged ? "BOTCF" : "BOTCF(开发版)";

if (process.platform === "linux") {
const passwordStore = detectLinuxPasswordStore();
if (passwordStore) {
app.commandLine.appendSwitch("password-store", passwordStore);
}
}

app.setName(productName);
const isolatedUserData = resolveLaobosUserDataPath(
app.getPath("appData"),
Expand Down
10 changes: 9 additions & 1 deletion docs/desktop-release-playbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ BOTCF 的源码、安装包与自动更新元数据统一发布在公开仓库 `
| --- | --- | --- | --- |
| macOS ARM64 | DMG | ZIP + blockmap | `latest-mac.yml` |
| Windows x64 | NSIS Setup EXE | EXE + blockmap | `latest.yml` |
| Linux x64/arm64 | tar.gz + `install.sh` | 重新安装 tar.gz | 无,校验 SHA-256 |

Release 公开前必须保证元数据引用的文件已经完整上传。

Expand All @@ -24,6 +25,10 @@ npm run desktop:installer:test-update -- --mac dmg zip --arm64

# Windows x64
npm run desktop:installer -- --win nsis --x64

# Linux,必须分别在目标架构的原生 Linux 环境执行
npm run desktop:linux:tarball -- --x64
npm run desktop:linux:tarball -- --arm64
```

正式发布优先使用 GitHub Actions 的 `macos-15` 与 `windows-latest` 原生 Runner。跨平台构建只能用于受控测试,而且必须显式准备目标平台的 optional dependencies。
Expand All @@ -41,6 +46,9 @@ botcf-agent-X.Y.Z-windows-x64-setup.exe
botcf-agent-X.Y.Z-windows-x64-setup.exe.blockmap
latest.yml

botcf-agent-X.Y.Z-linux-x64.tar.gz
botcf-agent-X.Y.Z-linux-arm64.tar.gz

SHA256SUMS
```

Expand All @@ -59,7 +67,7 @@ shasum -a 256 out/installers/*

Windows 专项验证当前检查 10 个 PE x64 文件,覆盖内置 MinGit、Koffi、Sharp/libvips、Next SWC、ripgrep、Node 内建加载器,以及 node-pty 的 ConPTY 模块、进程列表模块和兼容 DLL。只统计 npm 包数量不能证明原生运行时完整。

原生文件“存在”也不等于“可启动”。`verify-packaged-executables.mjs` 使用打包后的 Electron 作为 Node 运行时,从 `app.asar` 加载应用依赖,取得其实际返回的二进制路径并执行 `rg --version`。macOS 与 Windows 的正式工作流都必须运行该校验。
原生文件“存在”也不等于“可启动”。`verify-packaged-executables.mjs` 使用打包后的 Electron 作为 Node 运行时,从 `app.asar` 加载应用依赖,取得其实际返回的二进制路径并执行 `rg --version`;Linux 还校验 Landlock 启动器的物理路径。macOS、WindowsLinux 的正式工作流都必须运行该校验。

## 4. macOS 更新记录

Expand Down
10 changes: 8 additions & 2 deletions docs/project-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ Windows 生产数据默认位于:
%APPDATA%\BOTCF
```

Linux 生产数据默认位于:

```text
~/.config/botcf-dsh-agent
```

DSH Home 位于应用数据目录下的 `dsh`。软件更新缓存由 electron-updater 管理:

```text
Expand All @@ -84,9 +90,9 @@ Windows: %LOCALAPPDATA%\laobos-agent-studio-updater\pending
- DSH 只监听本机回环地址和随机端口。
- Electron 默认拒绝网页权限请求。
- 文件接口执行工作区边界、真实路径、符号链接和敏感文件检查。
- 密码、SSH 私钥和自动登录凭据优先由 `safeStorage` 加密;当系统安全存储暂不可用时,自动登录凭据只保留在当前进程内存中,退出应用即清除,不写入磁盘。
- 密码、SSH 私钥和自动登录凭据优先由 `safeStorage` 加密;Linux 启动时按桌面环境和 D-Bus 服务动态选择 Secret Service 或 KWallet,系统安全存储暂不可用时,自动登录凭据只保留在当前进程内存中,退出应用即清除,不写入磁盘。
- 更新器使用固定 GitHub Release 仓库,渲染器不能传入任意 URL 或安装路径。
- 正式 macOS 包需要 Developer ID 与公证;正式 Windows 包需要 Authenticode。
- 正式 macOS 包需要 Developer ID 与公证;正式 Windows 包需要 Authenticode;Linux tar.gz 通过 SHA-256 校验清单验证完整性
- ad-hoc/未签名构建只用于受控测试,不代表发布者身份。

## 开发与验证
Expand Down
16 changes: 16 additions & 0 deletions electron-builder.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ productName: BOTCF
asar: true
asarUnpack:
- "packages/**"
- "node_modules/@deepseek-ai/node-addon-landlock-run/**"
- "node_modules/@img/sharp-linux-*/**"
- "node_modules/@img/sharp-libvips-linux-*/**"
- "node_modules/@vscode/ripgrep/**"
- "node_modules/node-pty/**"
compression: normal
npmRebuild: false
electronUpdaterCompatibility: ">=2.16"
Expand Down Expand Up @@ -51,6 +56,17 @@ mac:
- arm64
artifactName: "botcf-agent-${version}-macos-${arch}.${ext}"

linux:
icon: public/botcf-logo.png
category: Development
executableName: botcf-agent
target:
- target: dir
arch:
- x64
- arm64
artifactName: "botcf-agent-${version}-linux-${arch}.${ext}"

win:
icon: build/botcf-icon.ico
executableName: botcf-agent
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"desktop:package": "npm run build:desktop-plugins && node scripts/package-desktop.mjs",
"desktop:make": "npm run build:desktop-plugins && node scripts/make-desktop.mjs",
"desktop:installer": "npm run build:desktop-plugins && electron-builder --publish never",
"desktop:linux:tarball": "npm run build:desktop-plugins && node scripts/package-linux-tarball.mjs",
"desktop:installer:test-update": "npm run build:desktop-plugins && electron-builder --config electron-builder.test-update.yml --publish never",
"site:dev": "WRANGLER_LOG_PATH=.wrangler/wrangler.log vinext dev",
"site:start": "WRANGLER_LOG_PATH=.wrangler/wrangler.log vinext start",
Expand Down
2 changes: 2 additions & 0 deletions packages/laobos-system-tools/lib/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@ window.__ModuleLoader__.load({
@media(max-width:720px){.lbs-grid,.lbs-split{grid-template-columns:1fr}.lbs-panel{min-height:auto}}
/* ── 设置面板平坦化:去遮罩、去圆角,平坦覆盖中央设置区(左右止于项目栏) ── */
.VOzbGW_overlay{z-index:2147483000;justify-content:flex-start;align-items:stretch;left:var(--lbs-left-column,0px);right:var(--lbs-right-column,0px)}
/* Portalled settings menus live under body, so keep them above the flattened panel. */
body > [role="menu"]{z-index:2147483600!important}
.VOzbGW_mask{display:none!important}
.VOzbGW_panel{flex:1;width:auto;height:100vh;max-width:none;max-height:none;border-radius:0;box-shadow:none;background:var(--dsw-alias-bg-base)}
.VOzbGW_nav{border-right:1px solid var(--dsw-alias-border-l1);box-sizing:border-box;width:218px;padding:26px 14px 0;gap:22px}
Expand Down
Loading