Skip to content
Merged
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
11 changes: 10 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ jobs:
package:
name: Package and end-to-end check
needs: [ui, backend]
runs-on: macos-26
# Hosted ARM Macs cannot start Colima's nested VZ VM. Colima's own macOS
# integration lane uses Intel; keep both universal slices in this build.
runs-on: macos-15-intel
timeout-minutes: 45
steps:
- uses: actions/checkout@v7
Expand All @@ -87,10 +89,17 @@ jobs:
npm ci --prefix ui
node scripts/package.mjs
- name: Start a local Docker engine
id: docker-engine
timeout-minutes: 10
run: |
brew install colima docker
colima start --cpu 2 --memory 2 --disk 10
docker pull alpine:3.21
- name: Diagnose Docker engine startup
if: failure() && steps.docker-engine.outcome == 'failure'
timeout-minutes: 1
run: |
find "$HOME/.colima" -type f \( -name 'ha.stderr.log' -o -name 'serial*.log' -o -name 'qemu.stderr.log' \) -print -exec tail -n 150 {} \;
- name: Run the backend through Rust Core and Docker
run: node scripts/e2e.mjs
- name: Stop the test engine
Expand Down
11 changes: 10 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ concurrency:
jobs:
release:
name: Release ${{ github.ref_name }}
runs-on: macos-26
# Match CI and Colima's macOS integration lane: hosted ARM Macs cannot
# start the nested VZ VM required by the real Docker check.
runs-on: macos-15-intel
timeout-minutes: 60
permissions:
contents: write
Expand All @@ -45,10 +47,17 @@ jobs:
- name: Build the package
run: node scripts/package.mjs
- name: Start a local Docker engine
id: docker-engine
timeout-minutes: 10
run: |
brew install colima docker
colima start --cpu 2 --memory 2 --disk 10
docker pull alpine:3.21
- name: Diagnose Docker engine startup
if: failure() && steps.docker-engine.outcome == 'failure'
timeout-minutes: 1
run: |
find "$HOME/.colima" -type f \( -name 'ha.stderr.log' -o -name 'serial*.log' -o -name 'qemu.stderr.log' \) -print -exec tail -n 150 {} \;
- name: Run the backend through Rust Core and Docker
run: node scripts/e2e.mjs
- name: Stop the test engine
Expand Down
26 changes: 23 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,10 @@ Lithe 会以「插件包不能包含别名、符号链接或特殊文件」拒
- 详情采用分组卡片与虚拟表格,显示实际 inspect 标签、端口、挂载和网络信息。
- 日志支持搜索、自动换行与横向滚动;HTTP(S) 链接标蓝并通过宿主打开系统浏览器。
- 容器文件页支持目录导航、拖入普通文件上传、原生选择文件上传,以及选定文件下载。
- 容器详情提供「打开终端」,经 API-24 打开 Lithe 原生终端,选择 `/bin/sh`、`/bin/bash` 或 `/bin/ash` 进入运行中的容器。

Compose 分组动作仅作用于已存在的容器,并不执行 `docker compose up/down`,
不解析 Compose 文件,也不编排服务依赖。尚不支持构建镜像、交互式终端、
不解析 Compose 文件,也不编排服务依赖。尚不支持构建镜像、
私有仓库登录、SSH/TLS/TCP 远端或 Dev Containers。

## 连接与数据
Expand Down Expand Up @@ -87,6 +88,19 @@ Elastic 自有仓库、Open WebUI、MinIO 和 pgvector 使用确切公开仓库
这些状态不跨工作树复用,也不写入安装包。浏览器打开复用 `browser.open` 宿主权限,
清单使用 `backend:2`;旧宿主会拒绝不支持的能力,不绕过 Web 页面导航限制。

交互终端需要支持 API-24 的本机 macOS Lithe 工作区,以及终端 PATH 中可用的 Docker CLI;
所选 Shell 必须已安装在容器内。插件声明 `terminal.create` 权限,通过后端能力发现调用
`lithe/host/terminal/create`,以独立 executable/argv 执行
`docker --host unix://<当前 socket> exec --interactive --tty <容器 ID> <Shell>`。
不拼接宿主 Shell 命令,也不向已有用户终端注入文字。容器暂停或停止时不能打开。
创建和重开由 Lithe 显示原生执行确认;取消后不自动重试。
相同工作区、引擎、容器与 Shell 再次点击会显示已有终端,关闭后再次点击通过 `reopen`
启动新进程并恢复最多 16 KiB 显示历史,不重放输入,也不恢复 Shell 变量或旧进程。
关闭按钮通过 `dispose` 释放该 Shell 的终端和历史;删除容器/Compose 分组时也先释放对应句柄。
终端与句柄只保存在宿主和后端内存中,后端结束、禁用插件或关闭工作区由宿主清理;
网页关闭只取消在途操作,已交付的终端继续由用户使用。未增加磁盘缓存或安装目录写入。
浏览器预览不具备 Lithe 原生终端服务,点击时明确提示不可用。

## 开发与验证

需要 Node.js 24+、Rust 1.87+ 和 macOS。官方 Rust SDK 锁定在
Expand Down Expand Up @@ -126,11 +140,17 @@ docker pull alpine:3.21
node scripts/e2e.mjs
```

CI 与发布流程的打包和真实 Docker 集成车道使用 `macos-15-intel`,与 Colima 上游的 macOS
集成车道一致,避免托管 ARM Mac 无法启动嵌套 VZ 虚拟机的问题;仍构建和校验
arm64 / x86_64 两个 universal slice。引擎启动限时 10 分钟,失败时输出 Lima 启动日志。

该场景读取现有资源,创建专用临时容器,验证日志、统计、进程、暂停、恢复、
重启、停止、启动、删除、中文文件名分块上传/下载、原生选中文件上传对应的后端路径、
取消传输清理、浏览器请求的 Core 权限/上下文路由、连接持久化及包文件不变性。
取消传输清理、浏览器与 API-24 终端请求的 Core 权限/上下文路由、停止容器拒绝终端、连接持久化及包文件不变性。
浏览器实际启动、原生选择/保存对话框和 Finder 拖入仍应在目标 Lithe 宿主验收,
Core 测试只验证请求交给宿主,不伪造或宣称浏览器已启动。测试完成后删除临时容器。
Core 测试只验证请求交给宿主,不伪造或宣称浏览器或原生终端已启动;无原生窗口的测试宿主对终端请求明确返回不可用。
安装包内的终端仍需在最新 Lithe 验收:打开/重复显示、输入、Ctrl-C、缩放、关闭后重开历史、取消确认、释放和禁用后无残留进程。
测试完成后删除临时容器。
不要在包含真实数据的引擎上执行全局清理测试;本场景不调用全局 prune。

所有插件自有界面文案来自 `shared/i18n/`;Docker 返回的诊断与资源名保持原文。
Expand Down
1 change: 1 addition & 0 deletions backend/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ mod files;
mod http;
mod logos;
mod service;
mod terminal;
mod time;

use lithe_plugin_sdk::Backend;
Expand Down
18 changes: 17 additions & 1 deletion backend/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ pub struct Service {
terminals: Mutex<HashMap<(PathBuf, String), bool>>,
logos: crate::logos::Logos,
files: crate::files::Files,
native_terminals: crate::terminal::Terminals,
}

impl Service {
Expand All @@ -46,6 +47,7 @@ impl Service {
Arc::new(Self {
logos: crate::logos::Logos::new(state.clone()),
files: crate::files::Files::new(state.clone()),
native_terminals: crate::terminal::Terminals::default(),
state,
environment,
choice: Mutex::new(choice),
Expand All @@ -61,6 +63,8 @@ impl Service {
});
let functions: &[(&str, Handler)] = &[
("browser.open", Self::open_browser),
("containers.terminal", Self::open_terminal),
("containers.terminalDispose", Self::dispose_terminal),
("files.list", Self::files_list),
("files.uploadBegin", Self::upload_begin),
("files.uploadLocal", Self::upload_local),
Expand Down Expand Up @@ -108,6 +112,15 @@ impl Service {
self.logos.get(text(&params, "image")?, context)
}

fn open_terminal(&self, params: Value, context: &Context) -> Result<Value, FunctionError> {
self.native_terminals.open(&self.docker()?, &params, context)
}

fn dispose_terminal(&self, params: Value, context: &Context) -> Result<Value, FunctionError> {
self.native_terminals
.dispose(&self.endpoint()?.socket, &params, context)
}

fn open_browser(&self, params: Value, context: &Context) -> Result<Value, FunctionError> {
let url = text(&params, "url")?;
let parsed = reqwest::Url::parse(url).map_err(|_| invalid("Invalid web URL."))?;
Expand Down Expand Up @@ -262,7 +275,9 @@ impl Service {
flag(&params, "force"),
flag(&params, "volumes")
);
self.docker()?.call(context, "DELETE", &target, None)?;
let docker = self.docker()?;
self.native_terminals.dispose_container(&docker.socket, id, context)?;
docker.call(context, "DELETE", &target, None)?;
lock(&self.terminals).retain(|(_, container), _| container != id);
Ok(Value::Null)
}
Expand Down Expand Up @@ -393,6 +408,7 @@ impl Service {
let percentage = (index * 100 / containers.len().max(1)) as u8;
context.progress(&format!("{} {name}", capital(action)), Some(percentage));
if action == "remove" {
self.native_terminals.dispose_container(&docker.socket, id, context)?;
docker.call(context, "DELETE", &format!("/containers/{}?force=1", encode(id)), None)?;
lock(&self.terminals).retain(|(_, container), _| container != id);
} else {
Expand Down
Loading
Loading