docs: 文档与发布脚本同步到 v1.0.0 子进程架构

README/架构文档仍在描述 C ABI 动态库加载,与 v1.0.0 实际实现不符。
新用户按文档走会去做 -buildmode=c-shared,产物新内核根本不加载。

README.md / README_EN.md:
- 设计要点补子进程架构段(三面通信、崩溃自愈、真热重载)
- 代码结构 plugin/ 描述:.so 动态加载器 → 子进程加载器
- 项目状态补 v1.0.0 条目(6 类缺陷 + 实测数字),v0.9.0 标注 ABI 已退场
- 新增「下载」章节:三变体对照 + 各平台包格式 + macOS 限制

assets/docs/{zh,en}/ARCHITECTURE.md:
- 四种加载方式表:外部 .so/C ABI → 外部子进程/握手+stdio JSON-RPC
- 加载流程改写为 exec.Command → 继承 fd → 握手 → init → start
- 内置 vs 外部对照表 7 行更新
- 新增「子进程插件的三个通信面」小节,含每个面的选择理由

assets/docs/{zh,en}/OVERVIEW.md:插件系统段落改写

deploy/ 发布脚本三处回归(v0.7.2 的 2c5f9ff 把 package/ 移到
deploy/packaging/ 使目录深度 1→2,但没改相对路径,此后两个版本
的发布都没有二进制资产):
- build.sh:.syso 按目标平台 hide/restore(trap 兜底),恢复
  windows 目标的 CXX,arm64 刻意不带 CXX
- installer.nsi:5 处 ..\build → ..\..\build,PRODUCT_VERSION 可注入
  (原先硬编码 0.8.0)
- homeagent.spec:server 变体补装 waiter(control-server 声明了 CLI 却没装)

deploy/scripts/upload_assets.py:release 资产上传(两步签名 URL → OBS
PUT)。放 deploy/scripts/ 而非 scripts/,因为后者在 .gitignore 里。
支持 GITCODE_REPO/ASSET_DIR 环境变量以复用于 SDK 仓。
This commit is contained in:
JianFeeeee
2026-09-03 19:26:17 +08:00
parent f91b27aedb
commit 9b92a04230
10 changed files with 278 additions and 37 deletions

107
deploy/scripts/upload_assets.py Executable file
View File

@ -0,0 +1,107 @@
#!/usr/bin/env python3
"""上传 release 资产到 gitcode两步取签名 URL → PUT 到 OBS
用法: upload_assets.py <tag> <token> [file...]
不传 file 时上传 dist/release/ 下全部发布产物。
环境变量:
GITCODE_REPO 目标仓库,默认 JianFeeeee/HomeAgentSDK 仓传 JianFeeeee/homeagent-sdk
ASSET_DIR 资产目录,默认 <repo>/dist/release
为何两步gitcode 的 release 附件不走 API 直传,而是先向
`releases/<tag>/upload_url` 要一个 OBS 预签名 URL带 x-obs-* 回调头),
再把文件 PUT 到那个 URL。回调头必须原样透传否则 OBS 收下了文件但
gitcode 侧不会登记为 release 附件。
"""
import json
import os
import sys
import urllib.error
import urllib.parse
import urllib.request
REPO = os.environ.get("GITCODE_REPO", "JianFeeeee/HomeAgent")
API = "https://gitcode.com/api/v5/repos"
# 发布产物后缀。注意 Windows 安装器是 HomeAgent_v*_win64.exe
# 与 bin/ 里的裸 .exe 靠 _win64.exe 后缀区分。
ARTIFACT_SUFFIXES = (
".tar.gz",
".zip",
".deb",
".rpm",
".pkg",
"_win64.exe",
)
def is_artifact(name: str) -> bool:
return name == "SHA256SUMS" or name.endswith(ARTIFACT_SUFFIXES)
def get_upload_url(tag: str, token: str, filename: str) -> tuple[str, dict]:
q = urllib.parse.urlencode({"file_name": filename})
url = f"{API}/{REPO}/releases/{tag}/upload_url?{q}"
req = urllib.request.Request(url, headers={"private-token": token})
with urllib.request.urlopen(req, timeout=30) as r:
data = json.loads(r.read())
return data["url"], data.get("headers", {})
def put_file(url: str, headers: dict, path: str) -> tuple[int, str]:
size = os.path.getsize(path)
with open(path, "rb") as f:
body = f.read()
req = urllib.request.Request(url, data=body, method="PUT")
for k, v in headers.items():
req.add_header(k, v)
req.add_header("Content-Length", str(size))
try:
# 大文件Full 变体安装包近 100MB给足超时。
with urllib.request.urlopen(req, timeout=900) as r:
return r.status, r.read().decode("utf-8", "replace")[:300]
except urllib.error.HTTPError as e:
return e.code, e.read().decode("utf-8", "replace")[:300]
except Exception as e: # noqa: BLE001
return 0, f"{type(e).__name__}: {e}"
def main() -> int:
if len(sys.argv) < 3:
print(__doc__)
return 2
tag, token = sys.argv[1], sys.argv[2]
outdir = os.environ.get("ASSET_DIR") or os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
"dist",
"release",
)
files = sys.argv[3:] or sorted(
f for f in os.listdir(outdir) if is_artifact(f)
)
print(f"repo={REPO} tag={tag} dir={outdir}", flush=True)
failed = []
for name in files:
path = os.path.join(outdir, name)
if not os.path.isfile(path):
print(f"skip (missing): {name}", flush=True)
continue
mib = os.path.getsize(path) / 1048576
print(f"==> {name} ({mib:.1f} MiB)", flush=True)
try:
url, headers = get_upload_url(tag, token, name)
except Exception as e: # noqa: BLE001
print(f" upload_url FAILED: {e}", flush=True)
failed.append(name)
continue
status, body = put_file(url, headers, path)
ok = 200 <= status < 300
print(f" PUT -> {status} {'OK' if ok else body}", flush=True)
if not ok:
failed.append(name)
print(f"\n{'ALL OK' if not failed else f'{len(failed)} FAILED: ' + ', '.join(failed)}")
return 1 if failed else 0
if __name__ == "__main__":
sys.exit(main())