xb-init 2.2.2__tar.gz → 2.2.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {xb_init-2.2.2 → xb_init-2.2.4}/PKG-INFO +8 -1
- {xb_init-2.2.2 → xb_init-2.2.4}/README.md +7 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/pyproject.toml +1 -1
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/root/AGENTS.md.j2 +3 -1
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/root/README.md.j2 +15 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/root/build.py.j2 +105 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/root/dev.py.j2 +106 -0
- xb_init-2.2.4/xb/templates/root/docs/images/feishu-folder.png +0 -0
- xb_init-2.2.4/xb/templates/root/docs/images/feishu-permission.png +0 -0
- xb_init-2.2.4/xb/templates/root/docs/images/release-upload.png +0 -0
- xb_init-2.2.4/xb/templates/root/docs/images/update-dialog.png +0 -0
- xb_init-2.2.4/xb/templates/root/docs//350/207/252/345/212/250/346/233/264/346/226/260/350/257/264/346/230/216.md +322 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/utils/template_engine.py +5 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/.gitignore +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/docs/screenshot.png +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/tests/test_cli.py +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/tests/test_update_templates.py +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/tests/test_upgrade.py +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/uv.lock +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/__init__.py +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/cli.py +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/commands/__init__.py +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/commands/build.py +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/commands/dev.py +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/commands/doctor.py +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/commands/init.py +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/commands/upgrade.py +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/commands/version.py +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/backend/api/__init__.py.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/backend/api/config.py.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/backend/api/ports.py.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/backend/api/terminal/__init__.py.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/backend/api/terminal/routes.py.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/backend/api/update/__init__.py.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/backend/app_version.py.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/backend/backend_build.py.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/backend/main.py.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/backend/managers/__init__.py.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/backend/managers/logger_manager.py.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/backend/managers/path_manager.py.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/backend/managers/secret_obfuscator.py.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/backend/managers/sudoers_manager.py.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/backend/requirements.txt.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/backend/services/__init__.py.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/backend/services/feishu_client.py.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/backend/services/updater.py.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/configs/config_changes.json.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/configs/global_config.yaml.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/configs/secrets.yaml.example.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/configs/secrets.yaml.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/electron/launcher.js.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/electron/main.js.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/electron/package.json.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/electron/port_diagnostics.js.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/electron/preload.js.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/electron/prepare.js.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/electron/resources/postinst.py.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/electron/resources/postrm.py.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/electron/resources/uninstaller.nsh.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/frontend/index.html.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/frontend/package.json.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/frontend/src/App.vue.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/frontend/src/components/ConfigSetup.vue.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/frontend/src/components/DashboardHome.vue.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/frontend/src/components/FileManager.vue.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/frontend/src/components/GitVersionBadge.vue.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/frontend/src/components/RefreshButton.vue.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/frontend/src/components/TerminalPanel.vue.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/frontend/src/components/TerminalView.vue.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/frontend/src/components/ThemeToggle.vue.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/frontend/src/components/version/CommitHistoryPanel.vue.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/frontend/src/components/version/UpdatePanel.vue.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/frontend/src/composables/useTerminalSessions.js.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/frontend/src/composables/useUpdate.js.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/frontend/src/main.js.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/frontend/src/style.css.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/frontend/vite.config.js.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/root/.gitignore.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/root/pyproject.toml.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/scripts/example.py.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/scripts/feishu_upload.py.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/version/hooks/pre-commit.py.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/version/scripts/gui_version_manager.py.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/version/scripts/install_hooks.py.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/version/scripts/version_manager.py.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/version/scripts/web_version_manager.py.j2 +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/utils/__init__.py +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/utils/click_helpers.py +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/utils/validators.py +0 -0
- {xb_init-2.2.2 → xb_init-2.2.4}/xb/utils/version_check.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: xb-init
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.4
|
|
4
4
|
Summary: Project management tool for UV + FastAPI + Vue3 + Electron desktop applications
|
|
5
5
|
Project-URL: Homepage, https://github.com/xuebli/xb
|
|
6
6
|
Project-URL: Documentation, https://github.com/xuebli/xb#readme
|
|
@@ -126,6 +126,9 @@ Electron 二进制镜像通过 `ELECTRON_MIRROR` 环境变量注入。
|
|
|
126
126
|
> 到最快的(未安装 nrm 时自动全局安装);项目不放 `.npmrc`,npm 源完全由
|
|
127
127
|
> nrm 全局管理
|
|
128
128
|
> - electron 约 200MB 二进制走华为云镜像下载
|
|
129
|
+
> - uv 源同样自动优化:首次依赖同步(dev.py)与后端构建(build.py -b/-a)
|
|
130
|
+
> 前用 [chsrc](https://github.com/RubyMetric/chsrc) 测速切换最快镜像
|
|
131
|
+
> (`chsrc set uv first`,未安装 chsrc 时自动安装)
|
|
129
132
|
> - 若安装失败或超时,会打印提示但不中断项目创建,可稍后手动重试
|
|
130
133
|
> - 执行 `git init` 并提交首个 commit(包含所有文件和 lock 文件)
|
|
131
134
|
> - 若检测到 PyPI 有新版 xb,会询问是否先升级再创建项目
|
|
@@ -156,6 +159,10 @@ xb init demo --icon ./my-icon.png
|
|
|
156
159
|
|
|
157
160
|
## 应用内自动更新(`--update`)
|
|
158
161
|
|
|
162
|
+
> 详细原理与 AI 开发者注意事项见模板内
|
|
163
|
+
> **[xb/templates/root/docs/自动更新说明.md](xb/templates/root/docs/自动更新说明.md)**
|
|
164
|
+
> (生成的项目会自带 `docs/自动更新说明.md` 与配图)。
|
|
165
|
+
|
|
159
166
|
`xb init demo --update` 会让生成的项目内置与 [leo_stm32_board_test](../leo_stm32_board_test) 同源的自动更新能力,
|
|
160
167
|
以飞书云盘的一个文件夹作为发布"货架":
|
|
161
168
|
|
|
@@ -97,6 +97,9 @@ Electron 二进制镜像通过 `ELECTRON_MIRROR` 环境变量注入。
|
|
|
97
97
|
> 到最快的(未安装 nrm 时自动全局安装);项目不放 `.npmrc`,npm 源完全由
|
|
98
98
|
> nrm 全局管理
|
|
99
99
|
> - electron 约 200MB 二进制走华为云镜像下载
|
|
100
|
+
> - uv 源同样自动优化:首次依赖同步(dev.py)与后端构建(build.py -b/-a)
|
|
101
|
+
> 前用 [chsrc](https://github.com/RubyMetric/chsrc) 测速切换最快镜像
|
|
102
|
+
> (`chsrc set uv first`,未安装 chsrc 时自动安装)
|
|
100
103
|
> - 若安装失败或超时,会打印提示但不中断项目创建,可稍后手动重试
|
|
101
104
|
> - 执行 `git init` 并提交首个 commit(包含所有文件和 lock 文件)
|
|
102
105
|
> - 若检测到 PyPI 有新版 xb,会询问是否先升级再创建项目
|
|
@@ -127,6 +130,10 @@ xb init demo --icon ./my-icon.png
|
|
|
127
130
|
|
|
128
131
|
## 应用内自动更新(`--update`)
|
|
129
132
|
|
|
133
|
+
> 详细原理与 AI 开发者注意事项见模板内
|
|
134
|
+
> **[xb/templates/root/docs/自动更新说明.md](xb/templates/root/docs/自动更新说明.md)**
|
|
135
|
+
> (生成的项目会自带 `docs/自动更新说明.md` 与配图)。
|
|
136
|
+
|
|
130
137
|
`xb init demo --update` 会让生成的项目内置与 [leo_stm32_board_test](../leo_stm32_board_test) 同源的自动更新能力,
|
|
131
138
|
以飞书云盘的一个文件夹作为发布"货架":
|
|
132
139
|
|
|
@@ -491,7 +491,9 @@ Windows 下服务不会继承当前 PowerShell 的 stdin,也不会弹出后台
|
|
|
491
491
|
|
|
492
492
|
### 条目格式速查
|
|
493
493
|
|
|
494
|
-
顶层 key = **变更生效前用户所处的最后版本号**(即本次变更 commit **之前** `electron/package.json` 里的版本;不确定时问用户)。应用条件是 `用户当前版本 <= 块版本 < 要安装的包版本`(见 `backend/services/updater.py` 的 `filter_config_changes`),**不要**写成"引入变更的新版本号"——否则从上一旧版本直升引入版本的更新会被条件二挡掉,迁移失效。例:块 `"1.0.0"` 表示 1.0.0 及更早的用户配置还是旧结构,1.0.0 升 1.1.0 会应用,1.1.0 升 1.2.0
|
|
494
|
+
顶层 key = **变更生效前用户所处的最后版本号**(即本次变更 commit **之前** `electron/package.json` 里的版本;不确定时问用户)。应用条件是 `用户当前版本 <= 块版本 < 要安装的包版本`(见 `backend/services/updater.py` 的 `filter_config_changes`),**不要**写成"引入变更的新版本号"——否则从上一旧版本直升引入版本的更新会被条件二挡掉,迁移失效。例:块 `"1.0.0"` 表示 1.0.0 及更早的用户配置还是旧结构,1.0.0 升 1.1.0 会应用,1.1.0 升 1.2.0 不再应用。
|
|
495
|
+
|
|
496
|
+
**文件维护约定**:顶层版本块按版本号**从高到低排列**(新条目插在文件最上方,便于快速定位最新迁移);追加条目时用版本号元组倒序重排整个文件,**不要**直接 append 到末尾。(运行时 `filter_config_changes` 会自行按块版本升序应用,文件顺序只影响可读性,但仍必须遵守。)
|
|
495
497
|
|
|
496
498
|
```json
|
|
497
499
|
{
|
|
@@ -278,6 +278,9 @@ git commit -m "feat: add new feature"
|
|
|
278
278
|
应用内置「检查更新 → 下载校验 → 备份配置 → 自动安装」的完整闭环,
|
|
279
279
|
以飞书云盘的一个文件夹作为发布"货架"。
|
|
280
280
|
|
|
281
|
+
> 完整原理、发版/升级全流程截图与配置迁移详解见
|
|
282
|
+
> **[docs/自动更新说明.md](docs/自动更新说明.md)**。
|
|
283
|
+
|
|
281
284
|
### 发版(开发电脑)
|
|
282
285
|
|
|
283
286
|
1. 在 `configs/secrets.yaml` 填好 `feishu.app_id` / `app_secret` /
|
|
@@ -395,6 +398,18 @@ Set-Location frontend
|
|
|
395
398
|
npm install
|
|
396
399
|
```
|
|
397
400
|
|
|
401
|
+
**Python 依赖同步慢或失败**
|
|
402
|
+
|
|
403
|
+
uv 镜像源由 [chsrc](https://github.com/RubyMetric/chsrc) 自动管理:首次依赖同步
|
|
404
|
+
(`dev.py`)与后端构建(`build.py -b/-a`)前会自动 `chsrc set uv first`
|
|
405
|
+
测速并切换到最快源;没有 chsrc 时会自动安装
|
|
406
|
+
(`curl https://chsrc.run/posix | sudo bash`)。手动操作:
|
|
407
|
+
|
|
408
|
+
```bash
|
|
409
|
+
chsrc set uv first # 测速并切换最快 uv 源
|
|
410
|
+
chsrc set uv reset # 恢复默认源
|
|
411
|
+
```
|
|
412
|
+
|
|
398
413
|
**npm install 慢或超时**
|
|
399
414
|
|
|
400
415
|
项目的 npm 源由 [nrm](https://github.com/Pana/nrm) 全局管理。`./build.py` 在
|
|
@@ -68,6 +68,110 @@ def require(path: Path, label: str) -> None:
|
|
|
68
68
|
raise SystemExit(f"{label}构建失败,未找到产物: {path}")
|
|
69
69
|
|
|
70
70
|
|
|
71
|
+
def _sudo_password() -> str:
|
|
72
|
+
"""从 configs/secrets.yaml 读 sudo 密码;支持 obf1: 混淆形态(与后端同算法)。"""
|
|
73
|
+
import base64
|
|
74
|
+
|
|
75
|
+
import yaml
|
|
76
|
+
|
|
77
|
+
secrets_file = ROOT / "configs" / "secrets.yaml"
|
|
78
|
+
if not secrets_file.exists():
|
|
79
|
+
return ""
|
|
80
|
+
try:
|
|
81
|
+
data = yaml.safe_load(secrets_file.read_text(encoding="utf-8")) or {}
|
|
82
|
+
value = str(data.get("sudo_password") or "")
|
|
83
|
+
except Exception:
|
|
84
|
+
return ""
|
|
85
|
+
if value.startswith("obf1:"):
|
|
86
|
+
key = b"[[ package_name ]]$SecretObfuscation#2026"
|
|
87
|
+
try:
|
|
88
|
+
raw = base64.urlsafe_b64decode(value[len("obf1:"):].encode("ascii"))
|
|
89
|
+
return bytes(b ^ key[i % len(key)] for i, b in enumerate(raw)).decode("utf-8")
|
|
90
|
+
except Exception:
|
|
91
|
+
return ""
|
|
92
|
+
return value
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _save_sudo_password(password: str) -> None:
|
|
96
|
+
"""把 sudo 密码合并写进 configs/secrets.yaml(保留其他键;已 gitignore)。"""
|
|
97
|
+
import yaml
|
|
98
|
+
|
|
99
|
+
secrets_file = ROOT / "configs" / "secrets.yaml"
|
|
100
|
+
data = {}
|
|
101
|
+
if secrets_file.exists():
|
|
102
|
+
try:
|
|
103
|
+
data = yaml.safe_load(secrets_file.read_text(encoding="utf-8")) or {}
|
|
104
|
+
except Exception:
|
|
105
|
+
data = {}
|
|
106
|
+
data["sudo_password"] = password
|
|
107
|
+
secrets_file.parent.mkdir(parents=True, exist_ok=True)
|
|
108
|
+
secrets_file.write_text(
|
|
109
|
+
yaml.safe_dump(data, allow_unicode=True, sort_keys=False),
|
|
110
|
+
encoding="utf-8",
|
|
111
|
+
)
|
|
112
|
+
print("✓ sudo 密码已保存到 configs/secrets.yaml(已 gitignore),后续无需再输")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def chsrc_set_uv() -> None:
|
|
116
|
+
"""uv 换最优镜像源:chsrc set uv first(自动测速选最快)。
|
|
117
|
+
|
|
118
|
+
- 未安装 chsrc 时自动安装(curl https://chsrc.run/posix | sudo bash):
|
|
119
|
+
sudo 密码优先取 configs/secrets.yaml;没有则提示输入一次并自动保存,
|
|
120
|
+
之后所有 sudo 环节免交互;非交互终端会提示先去补 secrets.yaml
|
|
121
|
+
- chsrc 写用户级 uv 配置(~/.config/uv/uv.toml),对本机所有项目生效
|
|
122
|
+
- 失败只告警,不阻塞构建(沿用当前源)
|
|
123
|
+
"""
|
|
124
|
+
chsrc = "chsrc.exe" if sys.platform == "win32" else "chsrc"
|
|
125
|
+
if not shutil.which(chsrc):
|
|
126
|
+
if sys.platform == "win32":
|
|
127
|
+
print("⚠️ 未检测到 chsrc(https://chsrc.run),跳过 uv 换源")
|
|
128
|
+
return
|
|
129
|
+
print("→ 未检测到 chsrc,自动安装(curl https://chsrc.run/posix | sudo bash)...")
|
|
130
|
+
try:
|
|
131
|
+
password = _sudo_password()
|
|
132
|
+
interactive = sys.stdin is not None and sys.stdin.isatty()
|
|
133
|
+
if not password and interactive:
|
|
134
|
+
# 提示补上:输入一次自动保存进 secrets.yaml,后续 sudo 环节全部免交互
|
|
135
|
+
from getpass import getpass
|
|
136
|
+
|
|
137
|
+
password = getpass(
|
|
138
|
+
"secrets.yaml 未配置 sudo_password,请输入本机 sudo 密码(输入后自动保存): "
|
|
139
|
+
).strip()
|
|
140
|
+
if password:
|
|
141
|
+
_save_sudo_password(password)
|
|
142
|
+
if password:
|
|
143
|
+
subprocess.run(
|
|
144
|
+
["sudo", "-S", "-p", "", "-v"],
|
|
145
|
+
input=password + "\n",
|
|
146
|
+
text=True,
|
|
147
|
+
check=True,
|
|
148
|
+
timeout=30,
|
|
149
|
+
)
|
|
150
|
+
elif interactive:
|
|
151
|
+
pass # 用户未保存:交给 sudo 在终端交互询问(仅本次有效)
|
|
152
|
+
else:
|
|
153
|
+
print("⚠️ 无法安装 chsrc:secrets.yaml 未配置 sudo_password,请补上后重试")
|
|
154
|
+
return
|
|
155
|
+
subprocess.run(
|
|
156
|
+
["bash", "-c", "curl https://chsrc.run/posix | sudo bash"],
|
|
157
|
+
check=True,
|
|
158
|
+
timeout=180,
|
|
159
|
+
)
|
|
160
|
+
except Exception as e:
|
|
161
|
+
print(f"⚠️ chsrc 安装失败,跳过 uv 换源(检查 secrets.yaml 的 sudo_password): {e}")
|
|
162
|
+
return
|
|
163
|
+
try:
|
|
164
|
+
print("→ 正在切换 uv 最优镜像源 (chsrc set uv first)...")
|
|
165
|
+
result = subprocess.run(
|
|
166
|
+
[chsrc, "set", "uv", "first"], capture_output=True, text=True, timeout=120
|
|
167
|
+
)
|
|
168
|
+
output = ((result.stdout or "") + (result.stderr or "")).strip()
|
|
169
|
+
if output:
|
|
170
|
+
print(output)
|
|
171
|
+
except Exception as e:
|
|
172
|
+
print(f"⚠️ chsrc 设置 uv 源失败,沿用当前源: {e}")
|
|
173
|
+
|
|
174
|
+
|
|
71
175
|
def nrm_speedtest() -> None:
|
|
72
176
|
"""用 nrm 一键测速所有内置 npm 源并自动切换到最快的源。
|
|
73
177
|
|
|
@@ -404,6 +508,7 @@ if "-f" in TARGETS or "-a" in TARGETS:
|
|
|
404
508
|
if "-b" in TARGETS or "-a" in TARGETS:
|
|
405
509
|
# 后端:使用当前平台虚拟环境调用 PyInstaller。
|
|
406
510
|
clean(ROOT / "dist")
|
|
511
|
+
chsrc_set_uv()
|
|
407
512
|
run(["uv", "sync", "--extra", "dev"])
|
|
408
513
|
run([str(PYTHON), "backend/backend_build.py"])
|
|
409
514
|
require(ROOT / "dist" / ("server.exe" if sys.platform == "win32" else "server"), "后端")
|
|
@@ -22,6 +22,110 @@ for _stream in (sys.stdout, sys.stderr):
|
|
|
22
22
|
pass
|
|
23
23
|
|
|
24
24
|
|
|
25
|
+
def _sudo_password() -> str:
|
|
26
|
+
"""从 configs/secrets.yaml 读 sudo 密码;支持 obf1: 混淆形态(与后端同算法)。"""
|
|
27
|
+
import base64
|
|
28
|
+
|
|
29
|
+
import yaml
|
|
30
|
+
|
|
31
|
+
secrets_file = ROOT / "configs" / "secrets.yaml"
|
|
32
|
+
if not secrets_file.exists():
|
|
33
|
+
return ""
|
|
34
|
+
try:
|
|
35
|
+
data = yaml.safe_load(secrets_file.read_text(encoding="utf-8")) or {}
|
|
36
|
+
value = str(data.get("sudo_password") or "")
|
|
37
|
+
except Exception:
|
|
38
|
+
return ""
|
|
39
|
+
if value.startswith("obf1:"):
|
|
40
|
+
key = b"[[ package_name ]]$SecretObfuscation#2026"
|
|
41
|
+
try:
|
|
42
|
+
raw = base64.urlsafe_b64decode(value[len("obf1:"):].encode("ascii"))
|
|
43
|
+
return bytes(b ^ key[i % len(key)] for i, b in enumerate(raw)).decode("utf-8")
|
|
44
|
+
except Exception:
|
|
45
|
+
return ""
|
|
46
|
+
return value
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _save_sudo_password(password: str) -> None:
|
|
50
|
+
"""把 sudo 密码合并写进 configs/secrets.yaml(保留其他键;已 gitignore)。"""
|
|
51
|
+
import yaml
|
|
52
|
+
|
|
53
|
+
secrets_file = ROOT / "configs" / "secrets.yaml"
|
|
54
|
+
data = {}
|
|
55
|
+
if secrets_file.exists():
|
|
56
|
+
try:
|
|
57
|
+
data = yaml.safe_load(secrets_file.read_text(encoding="utf-8")) or {}
|
|
58
|
+
except Exception:
|
|
59
|
+
data = {}
|
|
60
|
+
data["sudo_password"] = password
|
|
61
|
+
secrets_file.parent.mkdir(parents=True, exist_ok=True)
|
|
62
|
+
secrets_file.write_text(
|
|
63
|
+
yaml.safe_dump(data, allow_unicode=True, sort_keys=False),
|
|
64
|
+
encoding="utf-8",
|
|
65
|
+
)
|
|
66
|
+
print("✓ sudo 密码已保存到 configs/secrets.yaml(已 gitignore),后续无需再输")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def chsrc_set_uv() -> None:
|
|
70
|
+
"""uv 换最优镜像源:chsrc set uv first(自动测速选最快)。
|
|
71
|
+
|
|
72
|
+
- 未安装 chsrc 时自动安装(curl https://chsrc.run/posix | sudo bash):
|
|
73
|
+
sudo 密码优先取 configs/secrets.yaml;没有则提示输入一次并自动保存,
|
|
74
|
+
之后所有 sudo 环节免交互;非交互终端会提示先去补 secrets.yaml
|
|
75
|
+
- chsrc 写用户级 uv 配置(~/.config/uv/uv.toml),对本机所有项目生效
|
|
76
|
+
- 失败只告警,不阻塞启动(沿用当前源)
|
|
77
|
+
"""
|
|
78
|
+
chsrc = "chsrc.exe" if sys.platform == "win32" else "chsrc"
|
|
79
|
+
if not shutil.which(chsrc):
|
|
80
|
+
if sys.platform == "win32":
|
|
81
|
+
print("⚠️ 未检测到 chsrc(https://chsrc.run),跳过 uv 换源")
|
|
82
|
+
return
|
|
83
|
+
print("→ 未检测到 chsrc,自动安装(curl https://chsrc.run/posix | sudo bash)...")
|
|
84
|
+
try:
|
|
85
|
+
password = _sudo_password()
|
|
86
|
+
interactive = sys.stdin is not None and sys.stdin.isatty()
|
|
87
|
+
if not password and interactive:
|
|
88
|
+
# 提示补上:输入一次自动保存进 secrets.yaml,后续 sudo 环节全部免交互
|
|
89
|
+
from getpass import getpass
|
|
90
|
+
|
|
91
|
+
password = getpass(
|
|
92
|
+
"secrets.yaml 未配置 sudo_password,请输入本机 sudo 密码(输入后自动保存): "
|
|
93
|
+
).strip()
|
|
94
|
+
if password:
|
|
95
|
+
_save_sudo_password(password)
|
|
96
|
+
if password:
|
|
97
|
+
subprocess.run(
|
|
98
|
+
["sudo", "-S", "-p", "", "-v"],
|
|
99
|
+
input=password + "\n",
|
|
100
|
+
text=True,
|
|
101
|
+
check=True,
|
|
102
|
+
timeout=30,
|
|
103
|
+
)
|
|
104
|
+
elif interactive:
|
|
105
|
+
pass # 用户未保存:交给 sudo 在终端交互询问(仅本次有效)
|
|
106
|
+
else:
|
|
107
|
+
print("⚠️ 无法安装 chsrc:secrets.yaml 未配置 sudo_password,请补上后重试")
|
|
108
|
+
return
|
|
109
|
+
subprocess.run(
|
|
110
|
+
["bash", "-c", "curl https://chsrc.run/posix | sudo bash"],
|
|
111
|
+
check=True,
|
|
112
|
+
timeout=180,
|
|
113
|
+
)
|
|
114
|
+
except Exception as e:
|
|
115
|
+
print(f"⚠️ chsrc 安装失败,跳过 uv 换源(检查 secrets.yaml 的 sudo_password): {e}")
|
|
116
|
+
return
|
|
117
|
+
try:
|
|
118
|
+
print("→ 正在切换 uv 最优镜像源 (chsrc set uv first)...")
|
|
119
|
+
result = subprocess.run(
|
|
120
|
+
[chsrc, "set", "uv", "first"], capture_output=True, text=True, timeout=120
|
|
121
|
+
)
|
|
122
|
+
output = ((result.stdout or "") + (result.stderr or "")).strip()
|
|
123
|
+
if output:
|
|
124
|
+
print(output)
|
|
125
|
+
except Exception as e:
|
|
126
|
+
print(f"⚠️ chsrc 设置 uv 源失败,沿用当前源: {e}")
|
|
127
|
+
|
|
128
|
+
|
|
25
129
|
def stop_service(name: str) -> None:
|
|
26
130
|
"""停止指定服务并删除 PID 文件。Windows 递归结束子进程。"""
|
|
27
131
|
pid_file = PID_DIR / f"{name}.pid"
|
|
@@ -212,6 +316,8 @@ if action in {"start", "restart"}:
|
|
|
212
316
|
print_port_owner(backend_port)
|
|
213
317
|
raise SystemExit(f"后端端口 {backend_port} 已被占用")
|
|
214
318
|
if not python.exists():
|
|
319
|
+
# 首次同步依赖前先切到最快的 uv 镜像源,避免默认源拖慢/拖挂
|
|
320
|
+
chsrc_set_uv()
|
|
215
321
|
print("→ 正在同步 Python 依赖,首次启动可能需要几分钟...")
|
|
216
322
|
try:
|
|
217
323
|
subprocess.run(
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
# 软件自动更新说明(xb `--update` 功能详解)
|
|
2
|
+
|
|
3
|
+
> 本文详细解释 `xb init demo --update` 生成的**应用内自动更新**功能
|
|
4
|
+
> (对应 xb 仓库提交 `e3d2c72`)。
|
|
5
|
+
> 看完这篇你就明白:新版本是怎么"上架"的、用户的软件是怎么"发现并装上"新版本的、
|
|
6
|
+
> 用户配置为什么不会丢、飞书那边要准备什么、以及**作为 AI 开发者你在这个机制下
|
|
7
|
+
> 必须遵守的三条纪律**。
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 一句话总览
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
开发电脑把新版本"寄存"到飞书网盘 → 用户的软件自己去飞书"查快递"
|
|
15
|
+
→ 发现新版本就"签收下载" → 自动"换新装"(安装) → 重新打开就是新版
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
整个过程用户的**配置、密钥、数据都不会丢**(第六节细说为什么)。
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 一、这个功能是什么、生成了什么
|
|
23
|
+
|
|
24
|
+
`xb init demo --update` 会在标准脚手架之上额外生成一套完整更新链路,
|
|
25
|
+
并默认附带 `--sudoers`(Ubuntu 免密安装 deb 用;Windows 会跳过密码输入,
|
|
26
|
+
运行时 sudo 相关能力自动 no-op)。
|
|
27
|
+
|
|
28
|
+
### 生成的文件与职责
|
|
29
|
+
|
|
30
|
+
| 位置 | 职责 |
|
|
31
|
+
|------|------|
|
|
32
|
+
| `backend/api/update/__init__.py` | 更新接口:检查 / 信息 / 下载 / 进度 / 安装 |
|
|
33
|
+
| `backend/services/updater.py` | 核心逻辑:版本比较、信息拉取、并发下载、SHA256 校验、配置备份与迁移、双平台安装编排 |
|
|
34
|
+
| `backend/services/feishu_client.py` | 飞书开放平台封装:token 缓存、文件夹列举、4 线程分块下载 |
|
|
35
|
+
| `backend/managers/secret_obfuscator.py` | 敏感值混淆(`obf1:` 前缀,防围观级别) |
|
|
36
|
+
| `backend/app_version.py` | 后端运行时版本号(打包态读不到 package.json,靠它) |
|
|
37
|
+
| `scripts/feishu_upload.py` | 发版端:安装包/版本信息上传飞书、旧文件清理 |
|
|
38
|
+
| `configs/config_changes.json` | **配置迁移清单**(初始为 `{}`,见第五节) |
|
|
39
|
+
| `frontend/src/components/version/UpdatePanel.vue` | 更新弹窗(版本信息 / 更新说明 / 迁移明细 / 进度条) |
|
|
40
|
+
| `frontend/src/components/version/CommitHistoryPanel.vue` | 版本弹窗里的提交历史 |
|
|
41
|
+
| `frontend/src/composables/useUpdate.js` | 前端更新逻辑:启动/每小时定时检查、下载进度轮询 |
|
|
42
|
+
| `GitVersionBadge.vue`(改造) | 顶栏版本号徽章 = 更新入口:点开是"版本弹窗"(提交历史 + 检查更新按钮) |
|
|
43
|
+
|
|
44
|
+
另外 `build.py` 会多出发布编排(上传询问、进度渲染、`--upload-only`),
|
|
45
|
+
`version_manager` 会在版本 bump 时多同步两个文件(共 4 个)。
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 二、角色分工
|
|
50
|
+
|
|
51
|
+
| 角色 | 干什么 | 用到的工具 |
|
|
52
|
+
|------|--------|-----------|
|
|
53
|
+
| **发版者**(开发电脑) | 打包 → 传飞书 → 写"货架标签" | `./build.py -a` 一条命令 |
|
|
54
|
+
| **用软件的人**(产线电脑) | 什么都不用干,软件自己检查、下载、装 | 顶栏版本号徽章 |
|
|
55
|
+
|
|
56
|
+
**比喻**:飞书云盘的一个文件夹就是"货架",安装包(`.deb`/`.exe`)是"货",
|
|
57
|
+
版本信息文件(`update_ubuntu.json` / `update_windows.json`)是"货架标签"。
|
|
58
|
+
**客户端永远只看标签决定有没有新货。**
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## 三、飞书侧准备(发版者一次性配置)
|
|
63
|
+
|
|
64
|
+
1. 在 [飞书开放平台](https://open.feishu.cn) 创建**企业自建应用**,得到
|
|
65
|
+
`app_id` 和 `app_secret`;
|
|
66
|
+
2. 开通 **`drive:drive`** 权限(最容易漏的一步!):
|
|
67
|
+
- 没有它:上传成功但列举/删除/下载全失败,客户端永远"没有新版本";
|
|
68
|
+
- **开通后必须"创建版本并发布"**,管理员审核通过才生效——只开通不发布 = 没开通;
|
|
69
|
+
|
|
70
|
+

|
|
71
|
+
|
|
72
|
+
3. 在飞书云盘建一个文件夹当"货架",复制浏览器**完整链接**:
|
|
73
|
+
`https://xxx.feishu.cn/drive/folder/一串字符`;
|
|
74
|
+
**并把第 1 步创建的自建应用添加为该文件夹的协作者**(应用对文件夹没有
|
|
75
|
+
协作权限时,token 再对也访问不了里面的文件):
|
|
76
|
+
|
|
77
|
+

|
|
78
|
+
4. 三样东西填进项目的 `configs/secrets.yaml`(已 gitignore,永不进仓库):
|
|
79
|
+
|
|
80
|
+
```yaml
|
|
81
|
+
feishu:
|
|
82
|
+
app_id: "cli_xxx"
|
|
83
|
+
app_secret: "xxx"
|
|
84
|
+
update_folder_url: "https://xxx.feishu.cn/drive/folder/一串字符"
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## 四、发版流程(开发电脑)
|
|
90
|
+
|
|
91
|
+
一条命令:`./build.py -a`(构建 + 询问上传 + 写标签),
|
|
92
|
+
或 `./build.py --upload-only` 只传已有安装包。
|
|
93
|
+
全流程实录(分片上传进度 → 上传结果 → 输入更新说明 → 版本信息上传 → 旧标签清理):
|
|
94
|
+
|
|
95
|
+

|
|
96
|
+
|
|
97
|
+
### 关键机制 1:凭证自动同步与混淆(发版者可以"开箱填明文")
|
|
98
|
+
|
|
99
|
+
1. 构建开始时,`build.py` 把你本机 `configs/secrets.yaml` 的内容与
|
|
100
|
+
`configs/secrets.yaml.example` 做**深度合并**(真实值覆盖写入,
|
|
101
|
+
**example 独有的占位键原样保留**);
|
|
102
|
+
2. Electron 打包前,把 example 里的敏感键(键名含 secret/password/token)
|
|
103
|
+
混淆成 `obf1:xxxx` 密文——打进安装包的就是密文,用户**开箱即用**;
|
|
104
|
+
3. `sudo_password` 是唯一例外:**无论何时强制留空**。它是你本机的管理员密码,
|
|
105
|
+
混淆也不允许随包分发,客户端用配置界面设置自己的密码;
|
|
106
|
+
4. 构建结束(无论成败)example 自动恢复为留空模板——**真实凭证永远只在你本机**。
|
|
107
|
+
|
|
108
|
+
> 混淆是"防围观"级别(XOR+Base64,可被软件自动还原),防好奇不防逆向。
|
|
109
|
+
|
|
110
|
+
### 关键机制 2:上传与"货架标签"
|
|
111
|
+
|
|
112
|
+
- 安装包大于 20MB 自动走**分片上传**(4MB/片,失败重试),终端按 5 秒一行
|
|
113
|
+
显示带时间戳的进度,上传结束立即补打 100%(不拖到提示语之后);
|
|
114
|
+
- **清理策略**:`update_*.json` 同名旧文件自动删除(防止客户端误读残缺旧标签);
|
|
115
|
+
**安装包只记录不删除**——货架上保留全部历史版本,方便回滚;
|
|
116
|
+
- 更新说明直接回车 = 采用最新一条 git 提交信息;
|
|
117
|
+
- 标签内容:版本号、发布日期、更新说明(按行数组)、安装包的
|
|
118
|
+
`file_token`(应用内下载唯一入口)/ URL(仅展示)/ SHA256 / 大小,
|
|
119
|
+
以及第五节的 `config_changes` 全文。
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## 五、配置迁移清单 `configs/config_changes.json`(AI 开发者重点!)
|
|
124
|
+
|
|
125
|
+
**这是整个功能里唯一需要你持续手工维护的文件,也是新手最容易写错的地方。**
|
|
126
|
+
|
|
127
|
+
### 5.1 它解决什么问题
|
|
128
|
+
|
|
129
|
+
用户机器上的配置文件是"存量"——新版本代码改了配置结构(新增键/删除键/改默认值),
|
|
130
|
+
用户升级后配置文件**不会自动重写**。迁移清单就是告诉安装器:
|
|
131
|
+
"从版本 X 升上来的用户,配置要怎么改"。
|
|
132
|
+
|
|
133
|
+
### 5.2 顶层 key 的语义(⚠️ 最容易写错)
|
|
134
|
+
|
|
135
|
+
**顶层 key = 变更生效前用户所处的最后版本号**(即引入变更的那次 commit **之前**
|
|
136
|
+
`electron/package.json` 里的版本),**不是**"引入变更的新版本号"。
|
|
137
|
+
|
|
138
|
+
运行时应用条件(`backend/services/updater.py` 的 `filter_config_changes`):
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
用户当前版本 <= 块版本 < 要安装的包版本
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
为什么不能写新版本号:假设变更随 1.1.0 发布、块却写成 `"1.1.0"`,
|
|
145
|
+
用户从 1.0.0 **直升 1.1.0** 时 `目标(1.1.0) > 块(1.1.0)` 不成立 → 被条件二挡掉 →
|
|
146
|
+
**迁移失效**。写成 `"1.0.0"` 则:1.0.0 升 1.1.0 应用 ✓,1.1.0 升 1.2.0 不再应用 ✓。
|
|
147
|
+
|
|
148
|
+
### 5.3 条目格式
|
|
149
|
+
|
|
150
|
+
```json
|
|
151
|
+
{
|
|
152
|
+
"1.0.0": {
|
|
153
|
+
"summary": "secrets.yaml 新增 test.test 占位键(随 1.1.0 发布)",
|
|
154
|
+
"files": [
|
|
155
|
+
{
|
|
156
|
+
"filename": "secrets.yaml",
|
|
157
|
+
"action": "merge",
|
|
158
|
+
"description": "新增 secrets 键 test.test(值取自随包模板,留空)",
|
|
159
|
+
"add_items": [{ "key": "test.test" }]
|
|
160
|
+
}
|
|
161
|
+
]
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### 5.4 三种 action
|
|
167
|
+
|
|
168
|
+
| action | 用途 | 说明 |
|
|
169
|
+
|--------|------|------|
|
|
170
|
+
| `merge` | YAML 增量合并(最常用) | 配合 `add_items`(补缺失键)/ `overwrite_items`(覆盖值或删除键)/ `delete_items`(删键)/ `keep_items`(保护名单,绝不触碰) |
|
|
171
|
+
| `create` | 新建配置文件 | 已存在则跳过,绝不覆盖用户文件 |
|
|
172
|
+
| `overwrite` | 整文件覆盖 | csv 等"程序自带数据"专用;启动时还会由 `sync_declared_overwrites` 自动对齐,覆盖前留 `.bak` |
|
|
173
|
+
|
|
174
|
+
要点:
|
|
175
|
+
|
|
176
|
+
- **没被清单提到的键一律原样保留**(以用户现有配置为基底做增量修改);
|
|
177
|
+
- `add_items` / `overwrite_items` **不带 value** 时,值从安装包内
|
|
178
|
+
`<文件名>.example` 模板取——这是 secrets 类条目的**规范写法**(真实值不进仓库);
|
|
179
|
+
业务 yaml(无 example)必须显式带 value;
|
|
180
|
+
- 落盘前键名含 secret/password/token 的明文值自动混淆;
|
|
181
|
+
- 用户已有该键时**跳过不覆盖**(幂等,不碰用户已填的值)。
|
|
182
|
+
|
|
183
|
+
### 5.5 文件维护约定
|
|
184
|
+
|
|
185
|
+
- 顶层版本块按版本号**从高到低**排列:新条目插在文件**最上方**;
|
|
186
|
+
- 追加条目时用版本号元组倒序**重排整个文件**,不要直接 append 到末尾;
|
|
187
|
+
(运行时会自行按块版本升序应用,文件顺序只影响可读性,但必须遵守。)
|
|
188
|
+
|
|
189
|
+
### 5.6 双保险机制(为什么"安装时跳过"不等于失败)
|
|
190
|
+
|
|
191
|
+
| 层 | 时机 | 读的 example | 负责 |
|
|
192
|
+
|----|------|-------------|------|
|
|
193
|
+
| 安装时迁移 | 新包安装**前** | **旧**包内的 example | 改值/删除/覆盖(必须在换文件前做) |
|
|
194
|
+
| 启动补齐 | 新包**首次启动** | **新**包内的 example | 新增键类(旧包 example 天然没有新键) |
|
|
195
|
+
|
|
196
|
+
所以:新增占位键类条目在安装时**静默跳过属正常现象**(日志刻意不打),
|
|
197
|
+
升级后首次启动会以 `[补齐] secrets.yaml: test.test` 的形式自动补上。
|
|
198
|
+
想让它安装时就生效,显式带 `"value": ""` 即可。
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## 六、客户端全流程与数据安全
|
|
203
|
+
|
|
204
|
+
### 流程
|
|
205
|
+
|
|
206
|
+
1. **检查**:启动时 / 每小时 / 点版本号徽章 → 版本弹窗 → 「🔄 检查更新」;
|
|
207
|
+
远程信息缓存 5 分钟(`?force=true` 跳过);
|
|
208
|
+
2. **下载**:4 线程分块并发(8MB/块)+ `.part` 临时文件防半截包 + SHA256 校验,
|
|
209
|
+
进度条与预计剩余时间实时显示。检查到新版本时会弹出更新弹窗
|
|
210
|
+
(版本信息 / 更新说明 / 配置迁移明细):
|
|
211
|
+
|
|
212
|
+

|
|
213
|
+
3. **安装**(点「立即安装更新」):
|
|
214
|
+
- 备份整个配置目录到 `datas/config_backups/版本号_时间戳/`;
|
|
215
|
+
- 应用第五节的迁移;
|
|
216
|
+
- Ubuntu:趁应用存活用免密 `sudo dpkg -i`(免密规则只放行
|
|
217
|
+
`datas/updates/*.deb`,且启动发证、退回收证);Windows:等应用退出后
|
|
218
|
+
`setup.exe /S` 静默安装(运行中的 exe 被锁,只能退出再装);
|
|
219
|
+
- 安装日志:`datas/logs/update_install.log`;
|
|
220
|
+
4. **首次启动**:`init_user_configs` 用新包 example 深合并补齐用户新增键
|
|
221
|
+
(日志 `[补齐] secrets.yaml: test.test`),已有值一律不覆盖。
|
|
222
|
+
|
|
223
|
+
### 数据安全三重保护
|
|
224
|
+
|
|
225
|
+
1. 升级前整体备份(保底);
|
|
226
|
+
2. 迁移只动清单上写的键,其余设置/密钥一个字符都不碰;覆盖前留 `.bak`;
|
|
227
|
+
3. 迁移失败软件照常启动(配置按默认值运行),不会"升级完打不开"。
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## 七、API 速查(`/api/update/*`)
|
|
232
|
+
|
|
233
|
+
| 方法 | 路径 | 说明 |
|
|
234
|
+
|------|------|------|
|
|
235
|
+
| GET | `/api/update/check` | 检查更新(`?force=true` 跳过 5 分钟缓存) |
|
|
236
|
+
| GET | `/api/update/info` | 完整版本信息(含 source: remote/local/none) |
|
|
237
|
+
| POST | `/api/update/download` | 后台线程开始下载 |
|
|
238
|
+
| GET | `/api/update/download/progress` | 下载进度(status/progress/downloaded/total) |
|
|
239
|
+
| POST | `/api/update/install` | 备份 → 迁移 → 拉起安装器(body: `{"apply_config": true}`) |
|
|
240
|
+
|
|
241
|
+
开发模式(源码运行)下所有接口直接返回 `packaged: false`,不支持应用内更新——
|
|
242
|
+
刻意设计:对源码装 deb 没有意义。
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## 八、AI 开发者三条纪律(与 AGENTS.md §H 一致,违反会直接造成线上事故)
|
|
247
|
+
|
|
248
|
+
### 纪律 1:改了 `configs/` 就要问迁移
|
|
249
|
+
|
|
250
|
+
commit 前检查暂存区(`git status --short -- configs/`),只要配置文件有修改,
|
|
251
|
+
**必须询问用户**是否需要在 `config_changes.json` 补迁移条目;用户授权过
|
|
252
|
+
"默认直接维护"则直接补并在回复中列出条目。
|
|
253
|
+
|
|
254
|
+
### 纪律 2:块版本号写"变更前",别写"新版本"
|
|
255
|
+
|
|
256
|
+
见 5.2。写错的后果是**部分升级路径迁移静默失效**,测试时很难发现。
|
|
257
|
+
|
|
258
|
+
### 纪律 3:真实密钥绝不进仓库
|
|
259
|
+
|
|
260
|
+
迁移条目、`config_changes.json`、`config_changes` 的 summary 里都不能出现
|
|
261
|
+
真实密钥——secrets 类条目一律不带 value(从 example 模板取);
|
|
262
|
+
`build.py` 会把 example 同步/混淆后随包分发并在结束后恢复留空模板;
|
|
263
|
+
`sudo_password` 在 example 中永远为空。
|
|
264
|
+
|
|
265
|
+
### 快速自检清单
|
|
266
|
+
|
|
267
|
+
- [ ] 块版本号 = 引入变更 commit 之前的 package.json 版本?
|
|
268
|
+
- [ ] 新块插在文件最上方(版本号从高到低)?
|
|
269
|
+
- [ ] secrets 类条目不带 value?
|
|
270
|
+
- [ ] 占位键的键已经同步加进 `configs/secrets.yaml.example`?
|
|
271
|
+
- [ ] 改动的配置文件只动了清单上声明的键?
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## 九、FAQ
|
|
276
|
+
|
|
277
|
+
**Q:客户端永远显示"没有新版本"?**
|
|
278
|
+
九成是飞书 `drive:drive` 权限没开通或没发布审核。此时发版端清理不了旧文件、
|
|
279
|
+
客户端用本地兜底信息。确认权限**已发布生效**。
|
|
280
|
+
|
|
281
|
+
**Q:下载慢?**
|
|
282
|
+
飞书对下载限速(单连接约 0.1MB/s),已做 4 线程分块提速约 3 倍;
|
|
283
|
+
等不及可从更新弹窗"前往下载"走飞书网页高速通道,手动安装。
|
|
284
|
+
|
|
285
|
+
**Q:升级后某功能不工作,发现新配置键没出现?**
|
|
286
|
+
看启动日志有没有 `[补齐]` 行;没有则检查新包内
|
|
287
|
+
`/opt/<应用>/configs/secrets.yaml.example`(或 Windows 对应目录)是否含该键——
|
|
288
|
+
大概率是打包含旧 build.py 或 example 里没加键。
|
|
289
|
+
|
|
290
|
+
**Q:安装时日志说"跳过",迁移是不是失败了?**
|
|
291
|
+
不是。新增键类条目在安装时静默跳过是预期行为(旧包模板没有新键),
|
|
292
|
+
升级后首次启动会自动补齐(见 5.6 双保险)。
|
|
293
|
+
|
|
294
|
+
**Q:升级失败会怎样?**
|
|
295
|
+
配置有备份目录、程序文件由安装器管理,旧文件都在;查
|
|
296
|
+
`datas/logs/update_install.log`,或手动 `sudo dpkg -i` 重试。
|
|
297
|
+
|
|
298
|
+
**Q:旧版本能回滚吗?**
|
|
299
|
+
能。飞书货架保留全部历史安装包;配置有 `config_backups` 与 `.bak`。
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
## 十、维护者文件速查(xb 模板仓库)
|
|
304
|
+
|
|
305
|
+
| 模板文件 | 职责 |
|
|
306
|
+
|----------|------|
|
|
307
|
+
| `xb/templates/root/build.py.j2` | 构建编排 + example 同步/混淆/恢复 + 上传进度渲染 |
|
|
308
|
+
| `xb/templates/scripts/feishu_upload.py.j2` | 分片上传、token 记录、旧文件清理 |
|
|
309
|
+
| `xb/templates/configs/config_changes.json.j2` | 迁移清单骨架(`{}`) |
|
|
310
|
+
| `xb/templates/backend/api/update/__init__.py.j2` | 更新接口 |
|
|
311
|
+
| `xb/templates/backend/services/updater.py.j2` | 核心更新逻辑 |
|
|
312
|
+
| `xb/templates/backend/services/feishu_client.py.j2` | 飞书接口封装 |
|
|
313
|
+
| `xb/templates/backend/managers/secret_obfuscator.py.j2` | `obf1:` 混淆 |
|
|
314
|
+
| `xb/templates/backend/managers/path_manager.py.j2` | 首启补齐(`init_user_configs`)/ overwrite 对齐(`sync_declared_overwrites`) |
|
|
315
|
+
| `xb/templates/frontend/src/components/version/*.j2` | 更新弹窗 + 提交历史 |
|
|
316
|
+
| `xb/templates/version/scripts/*.j2` | 版本 bump 四文件同步 + hook |
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
*最后更新:2026-09-20 · 对应 xb 提交 `e3d2c72`(--update 功能)及其后续修复:
|
|
321
|
+
块版本语义澄清、example 深合并保留占位键、sudo_password 强制留空、
|
|
322
|
+
静默跳过日志、弹窗遮挡修复、nrm/chsrc 源优化。*
|
|
@@ -377,6 +377,11 @@ class TemplateEngine:
|
|
|
377
377
|
# AGENTS.md:AI 编码助手项目级约定(opencode / Claude Code / Cursor 等会自动读取)
|
|
378
378
|
self._render_template("root/AGENTS.md.j2", target_dir / "AGENTS.md", context)
|
|
379
379
|
|
|
380
|
+
# docs/:自动更新说明 + 配图,仅 --update 项目随模板分发(原样拷贝,不渲染变量)
|
|
381
|
+
if context["enable_update"]:
|
|
382
|
+
docs_src = Path(__file__).resolve().parent.parent / "templates" / "root" / "docs"
|
|
383
|
+
if docs_src.exists():
|
|
384
|
+
shutil.copytree(docs_src, target_dir / "docs", dirs_exist_ok=True)
|
|
380
385
|
|
|
381
386
|
(target_dir / "datas" / "logs").mkdir(parents=True, exist_ok=True)
|
|
382
387
|
(target_dir / "datas" / "pids").mkdir(parents=True, exist_ok=True)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/frontend/src/components/version/UpdatePanel.vue.j2
RENAMED
|
File without changes
|
{xb_init-2.2.2 → xb_init-2.2.4}/xb/templates/frontend/src/composables/useTerminalSessions.js.j2
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|