Skip to content

Commit e0fb2e6

Browse files
committed
fix(desktop): stop Windows update loop and GPU hangs
1 parent 83e660e commit e0fb2e6

16 files changed

Lines changed: 174 additions & 17 deletions

File tree

‎.github/workflows/octop-desktop.yml‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,15 @@ jobs:
153153
run: |
154154
set -euo pipefail
155155
VER=$(sed -nE 's/^version[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p' pyproject.toml | head -1)
156+
requested_tag="${{ github.event.inputs.release_tag }}"
157+
if [[ "${GITHUB_REF}" == refs/tags/v* && "${GITHUB_REF_NAME#v}" != "$VER" ]]; then
158+
echo "Refusing desktop release: tag ${GITHUB_REF_NAME} does not match pyproject.toml $VER" >&2
159+
exit 1
160+
fi
161+
if [[ -n "$requested_tag" && "${requested_tag#v}" != "$VER" ]]; then
162+
echo "Refusing desktop release: requested tag $requested_tag does not match pyproject.toml $VER" >&2
163+
exit 1
164+
fi
156165
echo "OCTOP_VERSION=${VER}" >> "$GITHUB_ENV"
157166
echo "version=${VER}"
158167

‎.github/workflows/release.yml‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,16 @@ jobs:
3333
with:
3434
fetch-depth: 0
3535

36+
- name: Verify release tag matches package version
37+
run: |
38+
set -euo pipefail
39+
package_version=$(sed -nE 's/^version[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p' pyproject.toml | head -1)
40+
tag_version="${GITHUB_REF_NAME#v}"
41+
if [[ -z "$package_version" || "$package_version" != "$tag_version" ]]; then
42+
echo "Refusing release: tag $GITHUB_REF_NAME does not match pyproject.toml $package_version" >&2
43+
exit 1
44+
fi
45+
3646
- uses: astral-sh/setup-uv@v4
3747
with:
3848
enable-cache: true

‎CHANGELOG.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
## [Unreleased]
88

9+
## [0.0.6] - 2026-09-22
10+
911
### 安全
1012

1113
- 桌面安装包与配置模板不再允许嵌入云厂商 API Key(含 DeepSeek)。首次运行不会预填真实密钥:优先本机 Ollama,云调用在密钥为空时直接拒绝并提示用户自行填写(即使环境里有 `DEEPSEEK_API_KEY` / `OPENAI_API_KEY` / `LLM_API_KEY` 也不写入 `providers`)。打包排除 `.env`、`octop.db`、`.freeos`,打 zip 前扫描 staging。生产/air-gap sidecar 不再回退 `LLM_API_KEY`。已发布的 **0.0.1–0.0.4** 安装包须下架(已从 Release 删除),并轮换可能泄露的 DeepSeek 密钥。
@@ -18,6 +20,7 @@
1820

1921
### 修复
2022

23+
- 撤回错误标记为 0.0.5、内部实际仍为 0.0.4 的 Windows 构建。桌面更新现在拒绝版本元数据与 portable 包内部版本不一致的下载,也拒绝重复安装当前或更旧版本,防止同一包循环下载、启动时反复解压约 1.15 GB / 6.8 万文件并拖垮系统。Windows WebView2 默认使用软件渲染,降低显卡驱动黑屏风险;发布工作流在标签与 `pyproject.toml` 版本不一致时直接失败。
2124
- 组织嵌入仍走 openXYOS 自己的登录(`localStorage token` / sidecar JWT),不把 FreeOS 桌面访客(`auth_token` / `POST /api/auth/local-session`)写进组织房间。重启/恢复 sidecar 始终带 `FREEOS_ORG_LOCAL_TEST=1`,保留 `demo@demo.com` / `user@demo.com`。本机会话跳过组织映射行,不删除、不占用 openXYOS 用户。代理剥离宿主 Cookie 与 `X-FreeOS-*`。 / Org embed keeps openXYOS login; FreeOS local-session must not replace org users. Sidecar restart still seeds test accounts. Proxy drops host cookies and identity headers.
2225
- Windows 升级刷新 `~/.freeos/portable` 时,若目录节点被占用(无法改名为 `portable.previous`,报 “being used by another process”),先尝试结束残留的 portable / `launch.py` 进程,再把新运行时**原地覆盖**进现有文件夹,避免留下空的锁定 `portable` 桩,也不要求重启。 / If renaming `portable` → `portable.previous` fails because the directory is in use, stop leftover host processes and overlay the new runtime in place.
2326
- 桌面 `POST /api/auth/local-session` 对已有 `~/.freeos`(多用户 / 组织映射行)或 WebView 非 `127.0.0.1` Host 返回 403,前端重试后掉进注册登录。本机会话在 loopback / `*.localhost` / Origin 为本机时签发 JWT 并选用已有工作室账号;SPA 在 `/` 跳到 `/projects` 丢掉 `?desktop=1` 之前记住桌面壳。403 修复后的路径是:可选模型配置(云 Key / 本机,可跳过)→ 第一个智能体 `/chat/main`,不经过登录墙,也不停在工作台列表。`/setup` 在 guest 已创建后不再打回登录页。

‎README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
<p align="center">
1010
<a href="https://www.python.org/downloads/"><img alt="Python 3.12+" src="https://img.shields.io/badge/python-3.12%2B-blue?logo=python&logoColor=white" /></a>
11-
<a href="https://github.com/XYAIStudio/FreeOS/releases"><img alt="Version" src="https://img.shields.io/badge/version-0.0.4-orange" /></a>
11+
<a href="https://github.com/XYAIStudio/FreeOS/releases"><img alt="Version" src="https://img.shields.io/badge/version-0.0.6-orange" /></a>
1212
<a href="LICENSE"><img alt="License: MIT + Apache-2.0" src="https://img.shields.io/badge/license-MIT%20%2B%20Apache--2.0-green" /></a>
1313
<a href="https://github.com/TencentCloud/Octop"><img alt="Upstream: Octop" src="https://img.shields.io/badge/Upstream-Octop-1677ff.svg?style=flat" /></a>
1414
<a href="https://github.com/XYAIStudio/openXYOS"><img alt="Organization module: openXYOS" src="https://img.shields.io/badge/Organization-openXYOS-0f766e.svg?style=flat" /></a>
@@ -136,7 +136,7 @@ Operator detail: [docs/asset-loop.md](docs/asset-loop.md).
136136
`FreeOS-desktop-windows-arm64-<version>.exe`(ARM 电脑)。
137137
2. 双击安装包。安装程序会放到「程序文件」并创建开始菜单和桌面快捷方式。
138138
3. 打开 **FreeOS**。第一次启动会解压内置运行环境(可能要一两分钟),然后直接进入可用会话,无需先登录。
139-
4. 保存、导出或发布到账号时再在本机完成注册或登录(更广的服务或授权不挡起步)。侧栏 **Organization** 当前默认走宿主内组织能力;桌面 0.0.4 托管 Node + iframe(延续 0.0.3 过渡桥)、以及可选的完整 openXYOS Node 栈(`127.0.0.1:3780`,导出/同步/高级部署)都是 **过渡桥**,终态是把 openXYOS 迁入宿主。组织能力像工作室里另一间可独立布置的房间,与日常对话、助手协作同在一个 FreeOS,两种进入方式不捏成一种。
139+
4. 保存、导出或发布到账号时再在本机完成注册或登录(更广的服务或授权不挡起步)。侧栏 **Organization** 当前默认走宿主内组织能力;桌面 0.0.6 托管 Node + iframe(延续 0.0.3 过渡桥)、以及可选的完整 openXYOS Node 栈(`127.0.0.1:3780`,导出/同步/高级部署)都是 **过渡桥**,终态是把 openXYOS 迁入宿主。组织能力像工作室里另一间可独立布置的房间,与日常对话、助手协作同在一个 FreeOS,两种进入方式不捏成一种。
140140

141141
数据目录默认是 `%USERPROFILE%\.freeos`(可用环境变量 `FREEOS_HOME` 改)。旧版 Octop 的 `~/.octop` 仍会被识别。卸载安装包会清空安装目录(默认为 `Program Files\FreeOS`)并删除快捷方式,但**不会**删除该用户数据目录;详见 [desktop/README.md](desktop/README.md#windows-uninstall)。
142142

‎README_CN.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
<p align="center">
1212
<a href="https://www.python.org/downloads/"><img alt="Python 3.12+" src="https://img.shields.io/badge/python-3.12%2B-blue?logo=python&logoColor=white" /></a>
1313
<a href="LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/license-MIT-green" /></a>
14-
<a href="https://github.com/XYAIStudio/FreeOS/releases"><img alt="Version" src="https://img.shields.io/badge/version-0.0.4-orange" /></a>
14+
<a href="https://github.com/XYAIStudio/FreeOS/releases"><img alt="Version" src="https://img.shields.io/badge/version-0.0.6-orange" /></a>
1515
<a href="https://github.com/TencentCloud/Octop"><img alt="上游:Octop" src="https://img.shields.io/badge/上游-Octop-1677ff.svg?style=flat" /></a>
1616
<a href="https://github.com/XYAIStudio/openXYOS"><img alt="组织模块:openXYOS" src="https://img.shields.io/badge/组织模块-openXYOS-0f766e.svg?style=flat" /></a>
1717
<a href="https://pypi.org/project/octop/"><img src="https://img.shields.io/pypi/v/octop" alt="PyPI" /></a>

‎desktop/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ opt into `SHIP_OPENXYOS_RUNTIME=1`):
147147
make -f desktop/portable/Makefile green
148148
```
149149

150-
The managed Node sidecar is included in the 0.0.4 desktop release so the
150+
The managed Node sidecar is included in the 0.0.6 desktop release so the
151151
integrated Organization workspace works in an offline installation. Set
152152
`SKIP_ORG_SIDECAR=1` only for an explicitly slim, host-only development build.
153153

‎desktop/src/build/config.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ info:
1111
description: "FreeOS desktop — Python host with in-host organization"
1212
copyright: "(c) 2026, XYAI Studio"
1313
comments: "Wails v3 + green portable. Node sidecar optional via FREEOS_ORG_SIDECAR=1."
14-
version: "0.0.4"
14+
version: "0.0.6"
1515

1616
dev_mode:
1717
root_path: .

‎desktop/src/build/windows/info.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
{
22
"fixed": {
3-
"file_version": "0.0.4"
3+
"file_version": "0.0.6"
44
},
55
"info": {
66
"0000": {
7-
"ProductVersion": "0.0.4",
7+
"ProductVersion": "0.0.6",
88
"CompanyName": "XYAI Studio",
99
"FileDescription": "FreeOS desktop",
1010
"LegalCopyright": "Copyright © 2026 XYAI Studio",

‎desktop/src/download.go‎

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -569,17 +569,60 @@ func pendingPortableZip() string {
569569
if err != nil {
570570
return ""
571571
}
572-
defer reader.Close()
573-
if stampFromZip(reader.File) == "" {
572+
stamp := stampFromZip(reader.File)
573+
archiveVersion, versionErr := versionFromZip(reader.File)
574+
_ = reader.Close()
575+
if stamp == "" || versionErr != nil || archiveVersion == "" {
576+
rejectPendingPortable(path, "archive is missing a valid FreeOS stamp or version")
577+
return ""
578+
}
579+
metaPath := filepath.Join(productHome(), "updates", "pending.json")
580+
if data, readErr := os.ReadFile(metaPath); readErr == nil {
581+
var meta struct {
582+
Version string `json:"version"`
583+
}
584+
if json.Unmarshal(data, &meta) != nil || strings.TrimSpace(meta.Version) == "" {
585+
rejectPendingPortable(path, "pending metadata is invalid")
586+
return ""
587+
}
588+
if compareVersions(meta.Version, archiveVersion) != 0 {
589+
rejectPendingPortable(path, fmt.Sprintf(
590+
"release version %s does not match archive version %s",
591+
meta.Version,
592+
archiveVersion,
593+
))
594+
return ""
595+
}
596+
}
597+
currentVersion := portableVersion(portableDir())
598+
if currentVersion != "" && compareVersions(archiveVersion, currentVersion) <= 0 {
599+
rejectPendingPortable(path, fmt.Sprintf(
600+
"archive version %s is not newer than installed version %s",
601+
archiveVersion,
602+
currentVersion,
603+
))
574604
return ""
575605
}
576606
return path
577607
}
578608

609+
func rejectPendingPortable(path, reason string) {
610+
dir := filepath.Dir(path)
611+
log.Printf("portable: rejecting pending update: %s", reason)
612+
_ = os.Remove(path)
613+
_ = os.Remove(filepath.Join(dir, "pending.json"))
614+
_ = os.WriteFile(
615+
filepath.Join(dir, "pending-rejected.txt"),
616+
[]byte(time.Now().Format(time.RFC3339)+" "+reason+"\n"),
617+
0o644,
618+
)
619+
}
620+
579621
func clearPendingPortable() {
580622
dir := filepath.Join(productHome(), "updates")
581623
_ = os.Remove(filepath.Join(dir, "pending-portable.zip"))
582624
_ = os.Remove(filepath.Join(dir, "pending.json"))
625+
_ = os.Remove(filepath.Join(dir, "pending-rejected.txt"))
583626
}
584627

585628
func extractPortable(root string) error {

‎desktop/src/download_test.go‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,47 @@ func TestEnsurePortableAppliesPendingFreeOSZip(t *testing.T) {
251251
}
252252
}
253253

254+
func TestEnsurePortableRejectsMislabeledPendingZip(t *testing.T) {
255+
home := t.TempDir()
256+
t.Setenv("OCTOP_HOME", home)
257+
t.Setenv("OCTOP_DESKTOP_INSTALL_STAMP_FILE", "")
258+
root := portableDir()
259+
260+
currentZip := filepath.Join(t.TempDir(), "current.zip")
261+
writeTestGreenZipWithStamp(t, currentZip, "0.0.4", "current-build")
262+
if err := unzipGreen(currentZip, root); err != nil {
263+
t.Fatal(err)
264+
}
265+
t.Setenv("OCTOP_DESKTOP_PORTABLE_ZIP", currentZip)
266+
sentinel := filepath.Join(root, "keep.txt")
267+
if err := os.WriteFile(sentinel, []byte("keep"), 0o644); err != nil {
268+
t.Fatal(err)
269+
}
270+
271+
pendingDir := filepath.Join(home, "updates")
272+
if err := os.MkdirAll(pendingDir, 0o755); err != nil {
273+
t.Fatal(err)
274+
}
275+
pending := filepath.Join(pendingDir, "pending-portable.zip")
276+
writeTestGreenZipWithStamp(t, pending, "0.0.4", "mislabeled-build")
277+
if err := os.WriteFile(filepath.Join(pendingDir, "pending.json"), []byte(`{"version":"0.0.5"}`), 0o644); err != nil {
278+
t.Fatal(err)
279+
}
280+
281+
if err := ensurePortable(LocaleZH, func(string) {}); err != nil {
282+
t.Fatal(err)
283+
}
284+
if _, err := os.Stat(sentinel); err != nil {
285+
t.Fatalf("rejected update must preserve the current runtime: %v", err)
286+
}
287+
if _, err := os.Stat(pending); !os.IsNotExist(err) {
288+
t.Fatalf("rejected update zip must be removed: %v", err)
289+
}
290+
if _, err := os.Stat(filepath.Join(pendingDir, "pending-rejected.txt")); err != nil {
291+
t.Fatalf("rejection reason must be recorded: %v", err)
292+
}
293+
}
294+
254295
func TestEnsurePortableReplacesOctopLineageWithFreeOS(t *testing.T) {
255296
home := t.TempDir()
256297
t.Setenv("OCTOP_HOME", home)

0 commit comments

Comments
 (0)