feat: add experimental LoongArch desktop preview

GoodBuddy can now be cross-packaged as an isolated Debian loong64 preview with verified Electron, node-pty, and Koffi binaries. The preview remains separate from the standard release matrix and excludes Agent, Remote Runtime, OpenCode, and native Canvas until those dependencies support LoongArch.

The Chinese website identifies GoodBuddy as the first open-source desktop AI assistant with LoongArch support while clearly labeling the independently built package as a preview.

Release note: 新增龙芯 LoongArch 独立编译预览版,并提供完整构建文档;当前预览版不包含 Agent、Remote Runtime、OpenCode 和原生 Canvas。
This commit is contained in:
mesalogo
2026-08-25 18:41:35 +08:00
parent e5aebaa67d
commit 500f7d2735
13 changed files with 1294 additions and 15 deletions
+14 -1
View File
@@ -185,6 +185,13 @@ Release note: 修复左上角项目设置与消息通道项目设置不一致的
- `.github/workflows/packages.yml` is the canonical cross-platform packaging
workflow. It validates and builds `out` once, then packages on six native
runners: Windows, macOS, and Linux, each for x64 and arm64.
- The experimental Linux LoongArch (`loong64`) preview is built separately
through `build/loongarch-cross` by following
`docs/development/loongarch-preview-build.md`. It is not part of
`.github/workflows/packages.yml`, the six-runner GitHub Actions matrix,
standard release manifests, GitHub Release asset expectations, or the
production update index. Do not add it to those paths unless the user
explicitly requests a future production-support migration.
- Run the unified packager with
`npm run release:package -- --platform <platform> --arch <arch>`. It only
packages for the native host and writes to
@@ -271,7 +278,9 @@ Every version-tag release must follow this sequence. A branch-only push does
not require release notes.
1. Confirm that the user wants a release tag and identify the exact release
commit and the new `package.json` version.
commit and the new `package.json` version. Separately ask whether this
release should also receive an experimental LoongArch preview build; never
infer that choice from an earlier release.
2. Find the latest stable version tag reachable before the release commit and
inspect the complete commit and file diff from that tag to the release
commit. For the first tagged release, inspect the relevant repository
@@ -294,6 +303,10 @@ not require release notes.
`npm run release:notes:verify`, the required source validators, the
production build, and any native candidate launch probe available on the
current host. The six native CI jobs remain the cross-platform authority.
If the user requested a LoongArch preview in step 1, build it separately
from this exact candidate by following the LoongArch build document, record
its size and SHA-256, and keep it outside the standard GitHub/OSS release
asset set unless the user separately authorizes publishing it.
7. Fetch both remotes immediately before tagging. Inspect any remote branch
movement instead of overwriting or silently merging it. Confirm the working
tree is clean, the candidate tag is unused locally and remotely, and the
+1
View File
@@ -0,0 +1 @@
results
+54
View File
@@ -0,0 +1,54 @@
FROM ubuntu:26.04
ARG DEBIAN_FRONTEND=noninteractive
ARG RUST_TOOLCHAIN=1.93.1
RUN apt-get update \
&& apt-get install --yes --no-install-recommends \
binutils-loongarch64-linux-gnu \
build-essential \
ca-certificates \
cmake \
dpkg-dev \
fakeroot \
file \
g++-loongarch64-linux-gnu \
gcc-loongarch64-linux-gnu \
libc6-dev-loong64-cross \
make \
ninja-build \
node-gyp \
nodejs \
npm \
pkg-config \
python3 \
qemu-user \
rustup \
tar \
xz-utils \
&& rm -rf /var/lib/apt/lists/*
RUN rustup set profile minimal \
&& rustup toolchain install "${RUST_TOOLCHAIN}" \
--target loongarch64-unknown-linux-gnu
ENV CC=loongarch64-linux-gnu-gcc \
CXX=loongarch64-linux-gnu-g++ \
AR=loongarch64-linux-gnu-ar \
RANLIB=loongarch64-linux-gnu-ranlib \
STRIP=loongarch64-linux-gnu-strip \
CARGO_BUILD_TARGET=loongarch64-unknown-linux-gnu \
CARGO_TARGET_LOONGARCH64_UNKNOWN_LINUX_GNU_LINKER=loongarch64-linux-gnu-gcc \
RUSTUP_TOOLCHAIN=${RUST_TOOLCHAIN}
COPY toolchain.cmake /opt/goodbuddy/loongarch64-toolchain.cmake
COPY probe.sh /usr/local/bin/goodbuddy-loongarch-cross-probe
COPY preview-deb.sh /usr/local/bin/goodbuddy-loongarch-preview-deb
RUN chmod 0755 \
/usr/local/bin/goodbuddy-loongarch-cross-probe \
/usr/local/bin/goodbuddy-loongarch-preview-deb
WORKDIR /work
ENTRYPOINT ["/usr/local/bin/goodbuddy-loongarch-cross-probe"]
+78
View File
@@ -0,0 +1,78 @@
# LoongArch cross-compilation probe
This directory defines an isolated Ubuntu 26.04 amd64 environment for probing
GoodBuddy's Linux `loong64` prerequisites. It does not build Electron or
OpenCode.
The image includes:
- GCC and G++ targeting `loongarch64-linux-gnu`
- A LoongArch glibc development sysroot
- QEMU user-mode execution
- CMake and Ninja
- Rust's `loongarch64-unknown-linux-gnu` target
- Node.js, npm, and node-gyp
The probe validates:
- C, C++, CMake, and Rust LoongArch ELF output
- C, C++, and Rust execution through QEMU
- A minimal NAPI-RS Rust addon cross-build
- A Debian package with `Architecture: loong64`
- Koffi 3.1.4's published LoongArch glibc binding and the absence of a musl
binding
- The current npm availability of an `@napi-rs/canvas` LoongArch package
- A source cross-build of node-pty 1.1.0
The node-pty probe uses its upstream node-gyp 11.4.2 dependency. Ubuntu's
distribution node-gyp unconditionally links Linux addons with `-lnode`, which
is appropriate for Ubuntu's shared Node.js package but not for Electron native
addons.
Build and run it from this directory on an amd64 Docker host:
```sh
docker build \
--tag goodbuddy-loongarch-cross:ubuntu-26.04 \
.
mkdir -p results
docker run --rm \
--volume "$PWD/results:/results" \
goodbuddy-loongarch-cross:ubuntu-26.04
```
Each run writes to a new UTC timestamped directory below `results`. A passing
run contains `summary.txt`, `SHA256SUMS`, the generated ELF files, and the
probe DEB.
QEMU only checks user-mode execution against the cross sysroot. Native Electron
loading, PTY behavior, desktop integration, GPU, audio, and packaging must
still be validated on a real Debian `loong64` host.
Koffi 3.1.4 currently publishes only `linux_loong64/koffi.node`, not a
`musl_loong64` binding. That is sufficient for a Debian desktop package but
does not satisfy GoodBuddy Agent's current dual-glibc/musl bundle contract.
The NAPI-RS probe confirms that a Rust Node addon can be cross-compiled. It
does not build Canvas itself, whose Skia dependency requires a separate port
and native validation.
## Experimental desktop preview
`prepare-input.cjs` creates the architecture-neutral GoodBuddy input archive.
`preview-deb.sh` combines it with the verified community Electron 42.3.0
LoongArch shell and produces an isolated `goodbuddy-loongarch-preview` DEB.
The preview:
- uses a separate application name and user-data directory;
- includes the direct model path and Continue runtime;
- cross-builds node-pty against Electron 42.3.0;
- includes Koffi's LoongArch glibc binding;
- does not include GoodBuddy Agent, Remote Runtime, OpenCode, or native Canvas;
- is not part of the production release matrix.
See
[`docs/development/loongarch-preview-build.md`](../../docs/development/loongarch-preview-build.md)
for the complete build and validation procedure.
+108
View File
@@ -0,0 +1,108 @@
const {
existsSync,
mkdirSync,
renameSync,
rmSync,
statSync
} = require('node:fs')
const { dirname, join, resolve } = require('node:path')
const tar = require('tar')
const root = join(__dirname, '..', '..')
const defaultOutput = join(
root,
'dist',
'loongarch-cross',
'goodbuddy-loongarch-cross-input.tgz'
)
const inputPaths = [
'LICENSE',
'agent-runtime-lock.json',
'package.json',
'package-lock.json',
'remote-runtime-lock.json',
'out',
'resources/agent-release-keys.json',
'resources/release-notes.json',
'resources/skills',
'build/icon.png',
'build/icon-tray.png',
'node_modules/@continuedev/cli/package.json',
'node_modules/@continuedev/cli/dist/cn.js',
'node_modules/@continuedev/cli/dist/index.js',
'node_modules/@continuedev/cli/dist/xhr-sync-worker.js',
'node_modules/npm'
]
function parseArguments(argv) {
const options = { output: defaultOutput }
for (let index = 0; index < argv.length; index += 1) {
const argument = argv[index]
if (argument !== '--output') {
throw new Error(`未知参数:${argument}`)
}
const value = argv[index + 1]
if (!value) {
throw new Error('--output 缺少值')
}
options.output = resolve(value)
index += 1
}
return options
}
function verifyInputs(projectRoot = root) {
for (const relativePath of inputPaths) {
if (!existsSync(join(projectRoot, relativePath))) {
throw new Error(`龙芯交叉构建输入缺失:${relativePath}`)
}
}
}
async function createInputArchive(outputPath, projectRoot = root) {
verifyInputs(projectRoot)
const resolvedOutput = resolve(outputPath)
const temporaryOutput = `${resolvedOutput}.partial-${process.pid}`
mkdirSync(dirname(resolvedOutput), { recursive: true })
rmSync(temporaryOutput, { force: true })
try {
await tar.c(
{
cwd: projectRoot,
file: temporaryOutput,
gzip: true,
portable: true,
noMtime: true
},
inputPaths
)
if (!statSync(temporaryOutput).isFile()) {
throw new Error('龙芯交叉构建输入归档未生成')
}
rmSync(resolvedOutput, { force: true })
renameSync(temporaryOutput, resolvedOutput)
} finally {
rmSync(temporaryOutput, { force: true })
}
return resolvedOutput
}
async function main(argv = process.argv.slice(2)) {
const options = parseArguments(argv)
const output = await createInputArchive(options.output)
console.log(`龙芯交叉构建输入已生成:${output}`)
}
module.exports = {
createInputArchive,
inputPaths,
parseArguments,
verifyInputs
}
if (require.main === module) {
main().catch((error) => {
console.error(error)
process.exitCode = 1
})
}
+322
View File
@@ -0,0 +1,322 @@
#!/usr/bin/env bash
set -euo pipefail
input_archive="${1:?missing GoodBuddy input archive}"
electron_archive="${2:?missing Electron loong64 archive}"
output_root="${3:-/preview-output}"
electron_version="42.3.0"
electron_sha256="92b0ca0c9c18ed90166918a4ac1970266c4fa967aee9277031b3b250b905526e"
koffi_version="3.1.4"
node_pty_version="1.1.0"
node_gyp_version="11.4.2"
preview_iteration="1"
application_directory="GoodBuddy-LoongArch-Preview"
executable_name="goodbuddy-loongarch-preview"
assert_file() {
if ! test -f "$1"; then
echo "missing required file: $1" >&2
exit 1
fi
}
assert_loongarch_elf() {
local file_path="$1"
assert_file "${file_path}"
file "${file_path}"
readelf -h "${file_path}" | grep -Eq \
'Machine:[[:space:]]+(LoongArch|Loongson Loongarch)'
}
assert_file "${input_archive}"
assert_file "${electron_archive}"
mkdir -p "${output_root}"
actual_electron_sha256="$(
sha256sum "${electron_archive}" | cut -d ' ' -f 1
)"
if test "${actual_electron_sha256}" != "${electron_sha256}"; then
echo "Electron loong64 archive digest mismatch" >&2
exit 1
fi
run_id="$(date -u +%Y%m%dT%H%M%SZ)"
work_root="${output_root}/.work-${run_id}"
input_root="${work_root}/input"
package_root="${work_root}/package"
application_root="${package_root}/opt/${application_directory}"
resources_root="${application_root}/resources"
app_root="${resources_root}/app"
artifact_root="${output_root}/${run_id}"
if test -e "${work_root}" || test -e "${artifact_root}"; then
echo "refusing to reuse preview build directory" >&2
exit 1
fi
cleanup() {
rm -rf "${work_root}"
}
trap cleanup EXIT
mkdir -p "${input_root}" "${app_root}" "${artifact_root}"
tar -xzf "${input_archive}" -C "${input_root}"
project_version="$(
node -p \
"require('${input_root}/package.json').version"
)"
preview_version="${project_version}-loong64-preview.${preview_iteration}"
deb_version="${project_version}~loong64preview${preview_iteration}"
artifact_name="GoodBuddy-${project_version}-linux-loong64-preview.deb"
echo "== extract verified Electron shell =="
python3 -m zipfile -e "${electron_archive}" "${application_root}"
assert_loongarch_elf "${application_root}/electron"
mv "${application_root}/electron" \
"${application_root}/${executable_name}"
chmod 0755 "${application_root}/${executable_name}"
rm -f "${resources_root}/default_app.asar"
if test -f "${application_root}/chrome-sandbox"; then
chmod 4755 "${application_root}/chrome-sandbox"
fi
echo "== install production JavaScript dependencies =="
cp "${input_root}/package.json" "${app_root}/package.json"
cp "${input_root}/package-lock.json" "${app_root}/package-lock.json"
(
cd "${app_root}"
npm ci \
--ignore-scripts \
--omit=dev \
--cpu=loong64 \
--os=linux
)
rm -rf \
"${app_root}/node_modules/@esbuild" \
"${app_root}/node_modules/@img" \
"${app_root}/node_modules/@rollup"
find "${app_root}/node_modules/@koromix" \
-mindepth 1 \
-maxdepth 1 \
-type d \
-exec rm -rf -- {} + 2>/dev/null || true
find "${app_root}/node_modules/@napi-rs" \
-mindepth 1 \
-maxdepth 1 \
-type d \
-name 'canvas-*' \
-exec rm -rf -- {} + 2>/dev/null || true
node - "${input_root}" "${app_root}/package.json" \
"${preview_version}" <<'NODE'
const { readFileSync, writeFileSync } = require('node:fs')
const [inputRoot, outputPath, version] = process.argv.slice(2)
const metadata = JSON.parse(
readFileSync(`${inputRoot}/package.json`, 'utf8')
)
metadata.name = 'goodbuddy-loongarch-preview'
metadata.productName = 'GoodBuddy LoongArch Preview'
metadata.desktopName = 'GoodBuddy LoongArch Preview'
metadata.version = version
metadata.main = './out/main/index.js'
delete metadata.build
delete metadata.devDependencies
delete metadata.optionalDependencies
writeFileSync(outputPath, `${JSON.stringify(metadata, null, 2)}\n`)
NODE
rm -f "${app_root}/package-lock.json"
echo "== stage target Koffi binding =="
koffi_stage="${work_root}/koffi"
mkdir -p "${koffi_stage}"
koffi_pack_json="$(
npm pack \
"@koromix/koffi-linux-loong64@${koffi_version}" \
--ignore-scripts \
--json \
--pack-destination "${koffi_stage}"
)"
koffi_archive="$(
node -e \
'const value=JSON.parse(process.argv[1]); process.stdout.write(value[0].filename)' \
"${koffi_pack_json}"
)"
koffi_integrity="$(
node -p \
"require('${input_root}/package-lock.json').packages['node_modules/@koromix/koffi-linux-loong64'].integrity"
)"
node - "${koffi_stage}/${koffi_archive}" \
"${koffi_integrity}" <<'NODE'
const { createHash } = require('node:crypto')
const { readFileSync } = require('node:fs')
const [filePath, integrity] = process.argv.slice(2)
const [algorithm, expected] = integrity.split('-', 2)
const actual = createHash(algorithm)
.update(readFileSync(filePath))
.digest('base64')
if (actual !== expected) {
throw new Error('Koffi archive integrity mismatch')
}
NODE
koffi_target="${app_root}/node_modules/@koromix/koffi-linux-loong64"
mkdir -p "${koffi_target}"
tar \
-xzf "${koffi_stage}/${koffi_archive}" \
-C "${koffi_target}" \
--strip-components 1
koffi_binding="${koffi_target}/linux_loong64/koffi.node"
assert_loongarch_elf "${koffi_binding}"
echo "== cross-build node-pty for Electron ${electron_version} =="
node_gyp_root="${work_root}/node-gyp"
npm install \
--ignore-scripts \
--prefix "${node_gyp_root}" \
"node-gyp@${node_gyp_version}"
node_pty_root="${app_root}/node_modules/node-pty"
test "$(
node -p "require('${node_pty_root}/package.json').version"
)" = "${node_pty_version}"
rm -rf "${node_pty_root}/build" "${node_pty_root}/prebuilds"
(
cd "${node_pty_root}"
CC=loongarch64-linux-gnu-gcc \
CXX=loongarch64-linux-gnu-g++ \
npm_config_arch=loong64 \
npm_config_target_arch=loong64 \
npm_config_runtime=electron \
npm_config_target="${electron_version}" \
npm_config_disturl=https://electronjs.org/headers \
"${node_gyp_root}/node_modules/.bin/node-gyp" rebuild \
--arch=loong64
)
node_pty_binding="${node_pty_root}/build/Release/pty.node"
assert_loongarch_elf "${node_pty_binding}"
echo "== stage GoodBuddy application and architecture-neutral runtimes =="
cp -a "${input_root}/out" "${app_root}/out"
mkdir -p \
"${resources_root}/licenses" \
"${resources_root}/runtimes" \
"${resources_root}/skills"
cp -a "${input_root}/resources/skills/." "${resources_root}/skills/"
cp "${input_root}/resources/release-notes.json" \
"${resources_root}/release-notes.json"
cp "${input_root}/resources/agent-release-keys.json" \
"${resources_root}/agent-release-keys.json"
cp "${input_root}/agent-runtime-lock.json" \
"${resources_root}/agent-runtime-lock.json"
cp "${input_root}/remote-runtime-lock.json" \
"${resources_root}/remote-runtime-lock.json"
cp "${input_root}/build/icon.png" "${resources_root}/icon.png"
cp "${input_root}/build/icon-tray.png" \
"${resources_root}/tray-icon.png"
cp "${input_root}/LICENSE" \
"${resources_root}/licenses/GoodBuddy-0BSD.txt"
mkdir -p "${resources_root}/runtimes/continue/dist"
cp "${input_root}/node_modules/@continuedev/cli/package.json" \
"${resources_root}/runtimes/continue/package.json"
cp "${input_root}/node_modules/@continuedev/cli/dist/cn.js" \
"${resources_root}/runtimes/continue/dist/cn.js"
cp "${input_root}/node_modules/@continuedev/cli/dist/index.js" \
"${resources_root}/runtimes/continue/dist/index.js"
cp "${input_root}/node_modules/@continuedev/cli/dist/xhr-sync-worker.js" \
"${resources_root}/runtimes/continue/dist/xhr-sync-worker.js"
cp -a "${input_root}/node_modules/npm" \
"${resources_root}/runtimes/npm"
echo "== reject foreign native addons =="
while IFS= read -r -d '' native_addon; do
assert_loongarch_elf "${native_addon}"
done < <(
find "${app_root}/node_modules" \
-type f \
-name '*.node' \
-print0
)
if test -e "${resources_root}/agents" ||
test -e "${resources_root}/remote-runtimes" ||
test -e "${resources_root}/runtimes/opencode"
then
echo "preview package unexpectedly contains Agent or OpenCode payload" >&2
exit 1
fi
echo "== build isolated preview DEB =="
mkdir -p \
"${package_root}/DEBIAN" \
"${package_root}/usr/bin" \
"${package_root}/usr/share/applications" \
"${package_root}/usr/share/icons/hicolor/512x512/apps"
ln -s \
"/opt/${application_directory}/${executable_name}" \
"${package_root}/usr/bin/${executable_name}"
cp "${input_root}/build/icon.png" \
"${package_root}/usr/share/icons/hicolor/512x512/apps/${executable_name}.png"
cat > "${package_root}/usr/share/applications/${executable_name}.desktop" <<EOF
[Desktop Entry]
Name=GoodBuddy LoongArch Preview
Comment=GoodBuddy LoongArch 主体预览版
Exec=/opt/${application_directory}/${executable_name} %U
Terminal=false
Type=Application
Icon=${executable_name}
Categories=Utility;
StartupWMClass=GoodBuddy LoongArch Preview
EOF
cat > "${package_root}/DEBIAN/control" <<EOF
Package: goodbuddy-loongarch-preview
Version: ${deb_version}
Architecture: loong64
Maintainer: MesaLogo
Section: utils
Priority: optional
Depends: libasound2t64, libatspi2.0-0t64, libgbm1, libgtk-3-0t64, libnotify4, libnss3, libpulse0, libsecret-1-0, libuuid1, libxss1, libxtst6, xdg-utils
Description: GoodBuddy LoongArch desktop preview
Experimental LoongArch desktop build without bundled Agent, Remote Runtime,
OpenCode, or native Canvas image decoding.
EOF
artifact="${artifact_root}/${artifact_name}"
dpkg-deb --root-owner-group --build "${package_root}" "${artifact}"
echo "== verify preview package =="
test "$(dpkg-deb --field "${artifact}" Architecture)" = "loong64"
verify_root="${work_root}/verify"
mkdir "${verify_root}"
dpkg-deb --extract "${artifact}" "${verify_root}"
assert_loongarch_elf \
"${verify_root}/opt/${application_directory}/${executable_name}"
assert_loongarch_elf \
"${verify_root}/opt/${application_directory}/resources/app/${node_pty_binding#${app_root}/}"
assert_loongarch_elf \
"${verify_root}/opt/${application_directory}/resources/app/${koffi_binding#${app_root}/}"
sha256sum "${artifact}" > "${artifact_root}/SHA256SUMS"
cat > "${artifact_root}/preview-manifest.json" <<EOF
{
"formatVersion": 1,
"product": "GoodBuddy LoongArch Preview",
"goodBuddyVersion": "${project_version}",
"previewVersion": "${preview_version}",
"electronVersion": "${electron_version}",
"architecture": "loong64",
"artifact": "${artifact_name}",
"bundledAgent": false,
"bundledRemoteRuntime": false,
"bundledOpenCode": false,
"nativeCanvas": false,
"continueRuntime": true,
"nodePty": true,
"koffiGlibc": true,
"nativeLaunchVerified": false
}
EOF
cat "${artifact_root}/preview-manifest.json"
echo "artifact=${artifact}"
+314
View File
@@ -0,0 +1,314 @@
#!/usr/bin/env bash
set -euo pipefail
results_root="${1:-/results}"
run_id="$(date -u +%Y%m%dT%H%M%SZ)"
results="${results_root}/${run_id}"
mkdir -p "${results}"
log="${results}/probe.log"
exec > >(tee "${log}") 2>&1
assert_loongarch_elf() {
local file_path="$1"
file "${file_path}"
readelf -h "${file_path}" | grep -Eq \
'Machine:[[:space:]]+(LoongArch|Loongson Loongarch)'
}
echo "== tool versions =="
loongarch64-linux-gnu-gcc --version | head -n 1
loongarch64-linux-gnu-g++ --version | head -n 1
loongarch64-linux-gnu-ld --version | head -n 1
qemu-loongarch64 --version | head -n 1
node --version
npm --version
node-gyp --version
rustc --version
cargo --version
cmake --version | head -n 1
echo "== C probe =="
cat > "${results}/hello.c" <<'EOF'
#include <stdio.h>
int main(void) {
puts("goodbuddy-loongarch-c-ok");
return 0;
}
EOF
loongarch64-linux-gnu-gcc \
-O2 \
-Wall \
-Wextra \
-Werror \
"${results}/hello.c" \
-o "${results}/hello-c"
assert_loongarch_elf "${results}/hello-c"
test "$(
qemu-loongarch64 \
-L /usr/loongarch64-linux-gnu \
"${results}/hello-c"
)" = "goodbuddy-loongarch-c-ok"
echo "== C++ probe =="
cat > "${results}/hello.cpp" <<'EOF'
#include <iostream>
#include <string_view>
int main() {
constexpr std::string_view message{"goodbuddy-loongarch-cpp-ok"};
std::cout << message << '\n';
return 0;
}
EOF
loongarch64-linux-gnu-g++ \
-std=c++20 \
-O2 \
-Wall \
-Wextra \
-Werror \
"${results}/hello.cpp" \
-o "${results}/hello-cpp"
assert_loongarch_elf "${results}/hello-cpp"
test "$(
qemu-loongarch64 \
-L /usr/loongarch64-linux-gnu \
"${results}/hello-cpp"
)" = "goodbuddy-loongarch-cpp-ok"
echo "== CMake probe =="
mkdir "${results}/cmake-source"
cat > "${results}/cmake-source/CMakeLists.txt" <<'EOF'
cmake_minimum_required(VERSION 3.20)
project(goodbuddy_loongarch_probe LANGUAGES C)
add_executable(goodbuddy-cmake-probe main.c)
target_compile_options(
goodbuddy-cmake-probe
PRIVATE -Wall -Wextra -Werror
)
EOF
cp "${results}/hello.c" "${results}/cmake-source/main.c"
cmake \
-S "${results}/cmake-source" \
-B "${results}/cmake-build" \
-G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_TOOLCHAIN_FILE=/opt/goodbuddy/loongarch64-toolchain.cmake
cmake --build "${results}/cmake-build"
assert_loongarch_elf \
"${results}/cmake-build/goodbuddy-cmake-probe"
echo "== Rust probe =="
mkdir -p "${results}/rust-probe/src"
cat > "${results}/rust-probe/Cargo.toml" <<'EOF'
[package]
name = "goodbuddy-loongarch-rust-probe"
version = "0.1.0"
edition = "2024"
[dependencies]
EOF
cat > "${results}/rust-probe/src/main.rs" <<'EOF'
fn main() {
println!("goodbuddy-loongarch-rust-ok");
}
EOF
cargo build \
--release \
--manifest-path "${results}/rust-probe/Cargo.toml"
rust_binary="$(
find "${results}/rust-probe/target" \
-type f \
-name goodbuddy-loongarch-rust-probe \
-print \
-quit
)"
test -n "${rust_binary}"
assert_loongarch_elf "${rust_binary}"
test "$(
qemu-loongarch64 \
-L /usr/loongarch64-linux-gnu \
"${rust_binary}"
)" = "goodbuddy-loongarch-rust-ok"
echo "== NAPI-RS Rust addon probe =="
mkdir -p "${results}/napi-rs-probe/src"
cat > "${results}/napi-rs-probe/Cargo.toml" <<'EOF'
[package]
name = "goodbuddy-loongarch-napi-probe"
version = "0.1.0"
edition = "2024"
[lib]
crate-type = ["cdylib"]
[dependencies]
napi = { version = "3", default-features = false, features = ["napi8"] }
napi-derive = "3"
[build-dependencies]
napi-build = "2"
EOF
cat > "${results}/napi-rs-probe/build.rs" <<'EOF'
fn main() {
napi_build::setup();
}
EOF
cat > "${results}/napi-rs-probe/src/lib.rs" <<'EOF'
use napi_derive::napi;
#[napi]
pub fn architecture_probe() -> &'static str {
"loong64"
}
EOF
cargo build \
--release \
--manifest-path "${results}/napi-rs-probe/Cargo.toml"
napi_binding="$(
find "${results}/napi-rs-probe/target" \
-type f \
-name 'libgoodbuddy_loongarch_napi_probe.so' \
-print \
-quit
)"
test -n "${napi_binding}"
assert_loongarch_elf "${napi_binding}"
echo "== Debian package probe =="
package_root="${results}/deb-root"
mkdir -p \
"${package_root}/DEBIAN" \
"${package_root}/usr/lib/goodbuddy-loongarch-probe"
cp "${results}/hello-c" \
"${package_root}/usr/lib/goodbuddy-loongarch-probe/hello"
cat > "${package_root}/DEBIAN/control" <<'EOF'
Package: goodbuddy-loongarch-cross-probe
Version: 1.0.0
Architecture: loong64
Maintainer: MesaLogo
Description: GoodBuddy LoongArch cross-compilation probe
EOF
dpkg-deb --build \
"${package_root}" \
"${results}/goodbuddy-loongarch-cross-probe_1.0.0_loong64.deb"
test "$(
dpkg-deb \
--field \
"${results}/goodbuddy-loongarch-cross-probe_1.0.0_loong64.deb" \
Architecture
)" = "loong64"
echo "== Koffi prebuilt probe =="
mkdir "${results}/koffi"
npm pack \
"@koromix/koffi-linux-loong64@3.1.4" \
--ignore-scripts \
--pack-destination "${results}/koffi"
koffi_archive="$(
find "${results}/koffi" \
-maxdepth 1 \
-type f \
-name '*.tgz' \
-print \
-quit
)"
test -n "${koffi_archive}"
mkdir "${results}/koffi/package"
tar \
-xzf "${koffi_archive}" \
-C "${results}/koffi/package" \
--strip-components 1
koffi_binding="${results}/koffi/package/linux_loong64/koffi.node"
assert_loongarch_elf "${koffi_binding}"
if test -e "${results}/koffi/package/musl_loong64/koffi.node"; then
echo "Unexpected Koffi musl binding is present"
exit 1
fi
echo "Koffi loong64 currently provides a glibc binding only"
echo "== Canvas package availability probe =="
canvas_package_status=unavailable
if npm view \
"@napi-rs/canvas-linux-loong64-gnu" \
version \
> "${results}/canvas-package-version.txt" \
2> "${results}/canvas-package-error.txt"
then
canvas_package_status=available
fi
echo "@napi-rs/canvas loong64 package: ${canvas_package_status}"
echo "== node-pty cross-build probe =="
mkdir "${results}/node-pty"
npm pack \
"node-pty@1.1.0" \
--ignore-scripts \
--pack-destination "${results}/node-pty"
node_pty_archive="$(
find "${results}/node-pty" \
-maxdepth 1 \
-type f \
-name '*.tgz' \
-print \
-quit
)"
test -n "${node_pty_archive}"
mkdir "${results}/node-pty/package"
tar \
-xzf "${node_pty_archive}" \
-C "${results}/node-pty/package" \
--strip-components 1
(
cd "${results}/node-pty/package"
npm install --ignore-scripts --omit=dev
npm install \
--ignore-scripts \
--no-save \
"node-gyp@11.4.2"
CC=loongarch64-linux-gnu-gcc \
CXX=loongarch64-linux-gnu-g++ \
npm_config_arch=loong64 \
npm_config_target_arch=loong64 \
npm_config_runtime=electron \
npm_config_target=43.2.0 \
npm_config_disturl=https://electronjs.org/headers \
./node_modules/.bin/node-gyp rebuild \
--arch=loong64
)
node_pty_binding="${results}/node-pty/package/build/Release/pty.node"
assert_loongarch_elf "${node_pty_binding}"
echo "== summary =="
sha256sum \
"${results}/hello-c" \
"${results}/hello-cpp" \
"${rust_binary}" \
"${napi_binding}" \
"${results}/goodbuddy-loongarch-cross-probe_1.0.0_loong64.deb" \
"${koffi_binding}" \
"${node_pty_binding}" \
> "${results}/SHA256SUMS"
cat > "${results}/summary.txt" <<EOF
result=pass
target=loong64
c=pass
cpp=pass
cmake=pass
rust=pass
napi_rs=pass
qemu=pass
deb=pass
koffi=pass
koffi_musl=unavailable
canvas_package=${canvas_package_status}
node_pty=pass
EOF
cat "${results}/summary.txt"
echo "results=${results}"
+14
View File
@@ -0,0 +1,14 @@
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR loongarch64)
set(CMAKE_C_COMPILER loongarch64-linux-gnu-gcc)
set(CMAKE_CXX_COMPILER loongarch64-linux-gnu-g++)
set(CMAKE_AR loongarch64-linux-gnu-ar)
set(CMAKE_RANLIB loongarch64-linux-gnu-ranlib)
set(CMAKE_STRIP loongarch64-linux-gnu-strip)
set(CMAKE_FIND_ROOT_PATH /usr/loongarch64-linux-gnu)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
+251
View File
@@ -0,0 +1,251 @@
# GoodBuddy 龙芯(LoongArch)预览版构建
本文档是 GoodBuddy 龙芯预览版的权威构建与功能状态说明。当前目标是
Debian `loong64` 桌面,不代表 GoodBuddy 已将龙芯纳入正式发布支持矩阵。
## 当前状态
当前流程可以在 AMD64 Linux 主机上交叉构建一个独立的
`goodbuddy-loongarch-preview` DEB,并校验包内 Electron、node-pty 和 Koffi
均为 LoongArch ELF。
预览版使用社区维护的 Electron 42.3.0 LoongArch 构建。GoodBuddy 正式版本
当前使用 Electron 43.2.0,因此该预览包只用于移植和真机验证,不能作为正式
发布基线。Electron 官方目前也不提供 Linux LoongArch 预编译包。
已完成的实际构建结果:
| 项目 | 结果 |
| --- | --- |
| GoodBuddy 源版本 | `0.11.0` |
| 预览版本 | `0.11.0-loong64-preview.1` |
| Debian 架构 | `loong64` |
| Electron | 社区版 `42.3.0` |
| DEB 包名 | `goodbuddy-loongarch-preview` |
| 构建产物 | `GoodBuddy-0.11.0-linux-loong64-preview.deb` |
| 已验证构建时间(UTC) | `20260825T102937Z` |
| 已验证产物大小 | `187053208` bytes |
| 已验证产物 SHA-256 | `461ade49f10a20ef7b28163077a2ecd6afd2c5b1b0befc673c6d174fd58497b3` |
| DEB 解包、架构和动态库解析 | 已通过 |
| QEMU Electron 初始化 | 共享库解析通过;Chromium 多线程初始化在 QEMU 下异常退出 |
| 龙芯真机启动 | 尚未验证 |
版本号和产物名会跟随当前 `package.json` 版本变化,上表只记录首次成功构建。
## 功能列表
“已打包”只表示所需代码或组件进入 DEB,不等同于已在龙芯桌面完成运行验证。
| 功能 | 预览包状态 | 当前验证 |
| --- | --- | --- |
| GoodBuddy Electron 桌面主体 | 已打包 | Electron 可执行文件为 LoongArch ELF;尚未完成真机 GUI 启动 |
| React 界面和主进程业务代码 | 已打包 | 生产 bundle 构建、测试、类型检查和 lint 已通过 |
| 直接模型调用路径 | 已打包 | JavaScript 代码与生产依赖已包含;待龙芯真机端到端验证 |
| Continue Runtime | 已打包 | CLI JavaScript 入口及 npm Runtime 已包含;待真机执行验证 |
| 本地终端能力 | 已打包 | node-pty 1.1.0 已针对 Electron 42.3.0 交叉编译并验证为 LoongArch ELF;待真机 PTY 验证 |
| Koffi glibc 原生绑定 | 已打包 | Koffi 3.1.4 LoongArch glibc binding 已校验为 LoongArch ELF |
| 内置 Skills | 已打包 | 静态资源已进入 DEB |
| GoodBuddy Agent | **不包含** | 当前 Agent 的正式构建和双 glibc/musl 产物契约不支持 LoongArch |
| Remote Runtime | **不包含** | 当前没有 LoongArch Remote Runtime 产物 |
| OpenCode Runtime | **不包含** | OpenCode 所需 Bun Runtime 当前不支持 LoongArch |
| 原生 Canvas 图像解码 | **不包含** | npm 当前没有 `@napi-rs/canvas-linux-loong64-gnu` 包 |
| Koffi musl binding | **不包含** | Koffi 3.1.4 只发布了 `linux_loong64` glibc binding |
| 正式自动更新和发布索引 | **不支持** | 预览包不加入正式 CI、安装包矩阵或更新索引 |
在真机验证完成前,不应把“已打包”功能描述为正式可用。尤其是窗口显示、GPU、
音频、系统托盘、桌面集成、PTY 交互和模型调用都必须在真实 LoongArch 桌面上
重新验证。
## 与正式 GoodBuddy 隔离
预览版有意使用单独身份,避免覆盖正式安装:
- Debian 包名:`goodbuddy-loongarch-preview`
- 产品名:`GoodBuddy LoongArch Preview`
- 可执行文件:`goodbuddy-loongarch-preview`
- 安装目录:`/opt/GoodBuddy-LoongArch-Preview`
- Electron 应用身份与正式 GoodBuddy 不同,因此使用单独的用户数据目录
该流程不会修改 `.github/workflows/packages.yml`,也不会把 `loong64` 加入
正式 release manifest 或下载索引。
## 构建前提
### 本地源码主机
- 已完成 `npm ci`
- Node.js 和 npm 版本满足仓库要求
- 可以成功执行 `npm run build:bundle`
### AMD64 交叉构建主机
- Linux AMD64
- Docker
- 建议至少 30 GiB 内存和 50 GiB 可用磁盘
- 可访问 Ubuntu、npm、Electron headers 和社区 Electron Release
### Electron LoongArch shell
从
[`darkyzhou/electron-loong64`](https://github.com/darkyzhou/electron-loong64)
的 Releases 下载:
```text
electron-v42.3.0-linux-loong64.zip
```
构建脚本固定校验以下 SHA-256,摘要不匹配时必须停止:
```text
92b0ca0c9c18ed90166918a4ac1970266c4fa967aee9277031b3b250b905526e
```
这是社区产物,不是 Electron 官方发布的 Linux LoongArch 二进制。
## 第一步:生成架构无关输入
在 GoodBuddy 仓库根目录执行:
```sh
npm ci
npm run build:bundle
node build/loongarch-cross/prepare-input.cjs
```
默认输出:
```text
dist/loongarch-cross/goodbuddy-loongarch-cross-input.tgz
```
输入归档包含生产 `out`、锁文件、Skills、Continue Runtime、npm Runtime、
图标和发布元数据。它不包含 Agent、Remote Runtime 或 OpenCode 二进制。
可通过 `--output` 指定其他输出位置:
```sh
node build/loongarch-cross/prepare-input.cjs \
--output /path/to/goodbuddy-loongarch-cross-input.tgz
```
## 第二步:构建交叉工具链镜像
将 `build/loongarch-cross`、输入归档和 Electron zip 放到 AMD64 Docker
主机,然后执行:
```sh
docker build \
--tag goodbuddy-loongarch-cross:ubuntu-26.04 \
build/loongarch-cross
```
工具链镜像包括:
- `loongarch64-linux-gnu-gcc` 和 G++
- LoongArch glibc 开发 sysroot
- QEMU user mode
- CMake 和 Ninja
- Rust `loongarch64-unknown-linux-gnu` target
- Node.js、npm 和上游 node-gyp
可先运行基础探针:
```sh
mkdir -p build/loongarch-cross/results
docker run --rm \
--volume "$PWD/build/loongarch-cross/results:/results" \
goodbuddy-loongarch-cross:ubuntu-26.04
```
探针必须通过 C、C++、CMake、Rust、NAPI-RS、QEMU、DEB、Koffi 和
node-pty 检查。Canvas 和 Koffi musl 显示为 unavailable 是当前已知限制,
不是 Debian 桌面主体 DEB 的构建失败。
## 第三步:生成预览 DEB
假设交叉构建主机上的文件布局如下:
```text
input/goodbuddy-loongarch-cross-input.tgz
electron/electron-v42.3.0-linux-loong64.zip
preview-output/
```
运行:
```sh
docker run --rm \
--entrypoint /usr/local/bin/goodbuddy-loongarch-preview-deb \
--volume "$PWD/input:/input:ro" \
--volume "$PWD/electron:/electron:ro" \
--volume "$PWD/preview-output:/preview-output" \
goodbuddy-loongarch-cross:ubuntu-26.04 \
/input/goodbuddy-loongarch-cross-input.tgz \
/electron/electron-v42.3.0-linux-loong64.zip \
/preview-output
```
每次构建创建一个 UTC 时间戳目录,包含:
```text
GoodBuddy-<version>-linux-loong64-preview.deb
preview-manifest.json
SHA256SUMS
```
脚本会执行以下强制检查:
1. 校验 Electron zip 的固定 SHA-256。
2. 删除 npm 错误解析出的宿主架构可选原生包。
3. 按 lockfile integrity 校验并安装 Koffi LoongArch glibc binding。
4. 使用上游 node-gyp 11.4.2 和 Electron 42.3.0 headers 交叉编译 node-pty。
5. 拒绝 `node_modules` 中任何不是 LoongArch ELF 的 `.node` 文件。
6. 拒绝意外进入包内的 Agent、Remote Runtime 或 OpenCode 目录。
7. 生成 `Architecture: loong64` 的 DEB,解包后再次校验关键 ELF。
Ubuntu 发行版自带的 node-gyp 会在 Linux addon 链接阶段无条件添加
`-lnode`,不适合此 Electron 交叉编译;不要用它替换脚本固定的上游
node-gyp。
## 验证边界
### 已由交叉构建验证
- C、C++、CMake 和 Rust 生成 LoongArch ELF
- C、C++ 和 Rust 程序通过 QEMU user mode 执行
- 最小 NAPI-RS addon 交叉编译
- node-pty 针对 Electron headers 交叉编译
- Koffi LoongArch glibc binding 架构
- Electron、node-pty、Koffi 和 DEB `loong64` 元数据
- DEB 完整解包
- Debian Forky rootfs 中 Electron 全部共享库解析;该检查发现并补充了
`libpulse0` DEB 依赖
### 仍需真实龙芯桌面验证
1. 安装 DEB 并从桌面入口启动。
2. 验证窗口、渲染、GPU 回退、输入法、系统托盘和通知。
3. 配置模型并执行一次真实的直接模型对话。
4. 验证 Continue Runtime 启动、取消和退出清理。
5. 打开本地终端,验证 node-pty 输入、输出、窗口缩放和进程回收。
6. 验证 Koffi 使用路径。
7. 验证音频、文件选择器、剪贴板和外部链接。
8. 确认 Agent、Remote Runtime、OpenCode 和 Canvas 相关入口不会被误认为
预览版已支持。
QEMU 可以补充动态链接器和缺库检查,但不能替代上述 GUI、PTY、GPU、音频和
桌面集成真机测试。当前 Electron 在 QEMU user mode 下可以完成全部共享库
解析;继续进入 Chromium 多线程初始化后,QEMU 出现 target `SIGSEGV`,因此
不能用该结果判断真机 Electron 是否可以启动。
## 已知依赖问题
- npm 的 `--cpu=loong64 --os=linux` 仍可能安装宿主 x64 的 Koffi 可选包。
打包脚本会删除所有该作用域的已解析原生包,并按 lockfile integrity 手工
安装 LoongArch 包。
- `@napi-rs/canvas` 没有发布 LoongArch 原生包。基础 NAPI-RS 探针通过只说明
Rust addon 工具链可用,不代表 Skia/Canvas 已完成移植。
- Koffi 没有 LoongArch musl binding,因此当前不能满足 GoodBuddy Agent 的
双 glibc/musl 产物契约。
- 预览 Electron 比正式 GoodBuddy 低一个主版本。正式支持必须先获得与当前
Electron 基线一致、可重复构建并完成安全维护的 LoongArch shell。
+6 -5
View File
@@ -4,11 +4,12 @@
正式站点地址:<https://mesalogo.github.io/goodbuddy/>
首页将 GoodBuddy 定位为“免注册、支持信创软硬件的一站式 AI 助手”,
优先展示三大桌面系统与双架构下载入口、统一 Agent Runtime,以及知识库、
魔法笔记、智能心跳、桌面上下文和远程消息通道等桌面助手能力。下载区位于
主要功能说明之前,并明确列出统信 UOS、银河麒麟、海光、兆芯、鲲鹏和飞腾
对应的 Linux x64 / arm64 包。页面不重复设置底部下载推广区。
首页将 GoodBuddy 定位为“首个支持龙芯的开源桌面 AI 助理产品”和
“免注册、支持信创软硬件的一站式 AI 助手”,优先展示三大桌面系统、正式
x64 / arm64 下载入口、独立编译的龙芯 loong64 预览版、统一 Agent Runtime,
以及知识库、魔法笔记、智能心跳、桌面上下文和远程消息通道等桌面助手能力。
下载区位于主要功能说明之前,并明确列出统信 UOS、银河麒麟、海光、兆芯、
鲲鹏、飞腾和龙芯。页面不重复设置底部下载推广区。
英文页面位于 `en.html`,不展示信创适配文案,三个平台的下载按钮始终前往
GitHub 最新正式 Release。
首屏产品界面默认正面展示,在精确指针设备上使用克制的 3D 倾斜、
+22 -9
View File
@@ -5,14 +5,14 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta
name="description"
content="GoodBuddy 是免注册、支持信创软硬件的一站式 AI 助手,以统一 Agent Runtime 连接直连模型、OpenCode、Continue 与 DeepSeek Harness。"
content="GoodBuddy 是首个支持龙芯的开源桌面 AI 助理产品,也是免注册、支持信创软硬件的一站式 AI 助手;龙芯 LoongArch 当前提供独立编译预览版。"
/>
<link rel="canonical" href="https://mesalogo.github.io/goodbuddy/" />
<link rel="alternate" hreflang="zh-CN" href="https://mesalogo.github.io/goodbuddy/" />
<link rel="alternate" hreflang="en" href="https://mesalogo.github.io/goodbuddy/en.html" />
<link rel="alternate" hreflang="x-default" href="https://mesalogo.github.io/goodbuddy/en.html" />
<meta name="theme-color" content="#f6f8fb" />
<title>GoodBuddy|免注册、支持信创软硬件的一站式 AI 助手</title>
<title>GoodBuddy|首个支持龙芯的开源桌面 AI 助理产品</title>
<link
rel="icon"
href="./assets/goodbuddy-light.png"
@@ -116,7 +116,7 @@
<div class="hero-copy">
<div class="eyebrow">
<span class="status-dot" aria-hidden="true"></span>
Windows · macOS · Linux
Windows · macOS · Linux · 龙芯预览版
</div>
<h1 id="hero-title">
免注册,<br />
@@ -124,7 +124,9 @@
<span>一站式 AI 助手。</span>
</h1>
<p class="hero-lead">
作为桌面助手与 AI 编程工具台,GoodBuddy 管理对话、知识、笔记与任务,
GoodBuddy 是首个支持龙芯的开源桌面 AI 助理产品,龙芯 LoongArch
当前以独立编译预览版提供。作为桌面助手与 AI 编程工具台,GoodBuddy
管理对话、知识、笔记与任务,
也通过独创的统一 Agent Runtime 接入直连模型、OpenCode、Continue 和 DeepSeek Harness。
无需反复配置命令行,选择工具和项目即可开始。
</p>
@@ -137,6 +139,10 @@
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="m5 10 3 3 7-7" /></svg>
3 大桌面系统,x64 / arm64
</li>
<li>
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="m5 10 3 3 7-7" /></svg>
龙芯 LoongArch 独立编译预览版
</li>
<li>
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="m5 10 3 3 7-7" /></svg>
4 类 Agent Runtime 统一接入
@@ -234,7 +240,7 @@
<div class="section-inner proof-grid">
<div><strong>4 类 Runtime</strong><span>多种 AI 编程路径</span></div>
<div><strong>3 大系统</strong><span>Windows / macOS / Linux</span></div>
<div><strong>2 种架构</strong><span>x64 / arm64</span></div>
<div><strong>3 种架构</strong><span>x64 / arm64 / loong64 预览</span></div>
<div><strong>1 个入口</strong><span>选择、配置、运行、审计</span></div>
</div>
</section>
@@ -347,14 +353,21 @@
<aside class="domestic-support" aria-labelledby="domestic-support-title">
<div>
<p class="kicker">信创软硬件支持</p>
<h3 id="domestic-support-title">统信 UOS、银河麒麟,覆盖国产 x64 与 ARM64</h3>
<p>使用对应的 Linux x64 或 arm64 安装包。</p>
<h3 id="domestic-support-title">首个支持龙芯的开源桌面 AI 助理产品</h3>
<p>
正式下载覆盖国产 x64 与 ARM64;龙芯 LoongArch 使用独立编译的
loong64 预览版。
</p>
</div>
<div class="domestic-support__items">
<div><span>国产系统</span><strong>统信 UOS · 银河麒麟</strong></div>
<div>
<span>国产 CPU</span>
<strong>海光 · 兆芯(x64)<br />鲲鹏 · 飞腾(ARM64)</strong>
<strong>
海光 · 兆芯(x64)<br />
鲲鹏 · 飞腾(ARM64)<br />
龙芯(loong64 预览版)
</strong>
</div>
</div>
</aside>
@@ -512,7 +525,7 @@
</span>
<span>GoodBuddy</span>
</a>
<p>免注册、支持信创软硬件的一站式 AI 助手</p>
<p>首个支持龙芯的开源桌面 AI 助理产品</p>
<div class="footer-links">
<a href="#download">下载</a>
<a href="#features">Agent Runtime</a>
+2
View File
@@ -250,6 +250,8 @@ for (const breakpoint of ["1199px", "959px", "719px"]) {
const requiredCopy = [
"免注册",
"支持信创软硬件的一站式 AI 助手",
"首个支持龙芯的开源桌面 AI 助理产品",
"龙芯 LoongArch 独立编译预览版",
"桌面助手",
"AI 编程工具台",
"Windows、macOS、Linux",
+108
View File
@@ -0,0 +1,108 @@
import {
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync
} from 'node:fs'
import { createRequire } from 'node:module'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
interface LoongArchInputBuilderModule {
inputPaths: string[]
parseArguments(arguments_: string[]): { output: string }
verifyInputs(projectRoot?: string): void
}
const require = createRequire(import.meta.url)
const inputBuilder = require(
'../build/loongarch-cross/prepare-input.cjs'
) as LoongArchInputBuilderModule
const previewScript = readFileSync(
join(
process.cwd(),
'build',
'loongarch-cross',
'preview-deb.sh'
),
'utf8'
)
let temporaryDirectories: string[] = []
afterEach(() => {
for (const directory of temporaryDirectories) {
rmSync(directory, { recursive: true, force: true })
}
temporaryDirectories = []
})
describe('LoongArch cross-build input', () => {
it('supports an explicit output path and rejects unknown arguments', () => {
const output = join(tmpdir(), 'goodbuddy-loongarch-input.tgz')
expect(
inputBuilder.parseArguments(['--output', output]).output
).toBe(output)
expect(() =>
inputBuilder.parseArguments(['--unknown'])
).toThrow('未知参数')
expect(() =>
inputBuilder.parseArguments(['--output'])
).toThrow('--output 缺少值')
})
it('requires every architecture-neutral packaging input', () => {
const root = mkdtempSync(
join(tmpdir(), 'goodbuddy-loongarch-input-test-')
)
temporaryDirectories.push(root)
for (const relativePath of inputBuilder.inputPaths) {
const absolutePath = join(root, relativePath)
mkdirSync(dirname(absolutePath), { recursive: true })
writeFileSync(absolutePath, 'input')
}
expect(() => inputBuilder.verifyInputs(root)).not.toThrow()
const missingPath = inputBuilder.inputPaths.at(-1)
expect(missingPath).toBeDefined()
rmSync(join(root, missingPath!), { force: true })
expect(() => inputBuilder.verifyInputs(root)).toThrow(
`龙芯交叉构建输入缺失:${missingPath}`
)
})
})
describe('LoongArch preview package contract', () => {
it('keeps the preview isolated and verifies target native code', () => {
expect(previewScript).toContain(
'Package: goodbuddy-loongarch-preview'
)
expect(previewScript).toContain('Architecture: loong64')
expect(previewScript).toContain(
'goodbuddy-loongarch-preview'
)
expect(previewScript).toContain(
'@koromix/koffi-linux-loong64'
)
expect(previewScript).toContain(
'assert_loongarch_elf "${node_pty_binding}"'
)
expect(previewScript).toContain(
'assert_loongarch_elf "${koffi_binding}"'
)
expect(previewScript).toContain('libpulse0')
expect(previewScript).toContain('"bundledAgent": false')
expect(previewScript).toContain(
'"bundledRemoteRuntime": false'
)
expect(previewScript).toContain(
'"bundledOpenCode": false'
)
expect(previewScript).toContain('"nativeCanvas": false')
expect(previewScript).not.toContain('--no-sandbox')
})
})