Merge remote-tracking branch 'origin/main' into feature/binary-doc-index

This commit is contained in:
2026-10-01 05:25:41 +00:00
9 changed files with 290 additions and 126 deletions
+3
View File
@@ -11,3 +11,6 @@ runtime/
# ZCode CLI 会话生成的工具忽略文件(内容镜像 .gitignore,工具自动维护,不入库)
.zcodeignore
# Mimosa 安全扫描插件的逐会话状态(hook-status/reports/ledger,机器自动重建)
.mimosa/
@@ -0,0 +1,34 @@
# feature/exec-interpreters 任务卡
任务主记录:Backend https://git-home.u1680832.nyat.app:30319/weichen/Agent-System-Backend/pulls/33 ;Dev https://git-home.u1680832.nyat.app:30319/weichen/Agent-System-Dev/pulls/113 ;状态:已完成(回填);owner:weichen
## 基线与授权
- 权威仓:ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/Agent-System-Dev.git;目标分支:main;R0(initial_base_sha)= 4c4071e5d8e256dd8f82ba9c74efa1effd1e9df3
- 工作区:/home/weichen/projects/Agent-System-Dev-wt-feature-exec-interpreters(worktree);分支:feature/exec-interpreters
- 授权与交付终点:落地 run_shell / run_python 并打开 Backend 与 Dev PR。合入 main 由所有者执行。不恢复 write_docx。
## 目标与非目标
- 目标:注册 run_shell / run_python。脚本落盘到会话沙箱 .agent/ 并保留;解释器限 bash / python3 绝对路径;超时与请求取消先到者生效,超时杀掉进程组;非零退出与超时以文本返回。
- 附带:register_artifact 按扩展名写入 Office/PDF/图片 MIME,避免二进制被当成文本索引。
- 非目标:写侧护栏与 indexTextOf(feature/binary-doc-index)、预览嗅探、输入排队。
## 同步记录
| 节点 | sync_base_sha | tested_head_sha | checked_at |
|---|---|---|---|
| 启动 | 4c4071e5d8e256dd8f82ba9c74efa1effd1e9df3 | 4c4071e5d8e256dd8f82ba9c74efa1effd1e9df3 | 2026-10-01T03:58:48Z |
| 同步前验收 | 4c4071e5d8e256dd8f82ba9c74efa1effd1e9df3 | d62f6806d2 | 2026-10-01T04:04:49Z |
## 结果与验证
- 结果:Backend feature/exec-interpreters @ 2880b58 注册两个执行器,并在 app.go 挂上。
- 验证:Backend `go test ./...` 19 包 ok(含 echo 留痕、空脚本、runtime.exec 闸、进程组超时、python ZIP 魔数、register_artifact 回填)。Dev `make test` 通过:core、backend、frontend vitest 40、ui vitest 68、agent-command。
- 影响与剩余项:模型可执行工作区脚本并登记二进制交付物。合入顺序为 Backend PR 然后 Dev PR。部署后的真人模型验收留到所有者合入之后。
## 闭合记录(card-autoclose 回填)
- 依据:merge 345d9db1e6486ec818c9b4f3c5139a903d4eee8b(PR #113)已包含于 origin/main,分支成果已落盘;本段为合入后回填。
- 合并主题:Merge pull request 'Point backend at run_shell and run_python executors' (#113) from feature/exec-interpreters into main
- 归档:随闭合移入 .task/archive/;任务执行期的过程证据见原任务记录与 PR。
@@ -52,5 +52,12 @@
| 同步前验收 | 9f2a7fb4b806bb63f43c3ee759bdbb1ab1f97720 | 21a92441dce7787b8c3eaeaf731aad9a58215836 | 2026-09-30T16:49:57Z |
| 同步前验收 | bb7f541d42dc6bd2609e59fed6760f22ed5c213e | dcb5bc0f727b5ceaf95b73b5ea514e63d9a89ae0 | 2026-09-30T17:17:46Z |
| 同步前验收 | 978dc80817e7240f0a45a390ed2a6334041e84dd | 4eec51a954c1271729a3659544eb891f0181d8c2 | 2026-09-30T17:19:17Z |
| 同步前验收 | d64742c71c1ebb8c1c88aef4c55194510f1cdf34 | 0b883469a2ad9f3b6b7cdb23c0ce0fb94d2d36b3 | 2026-09-30T17:51:59Z |
| 同步前验收 | f65d89e59b6974c7a5db63be0677d192bfaa7e9c | 7ababf2a5420ab6047212ca86403314ee619fb99 | 2026-09-30T18:10:55Z |
| 同步前验收 | 0be23f9a4d002a977de2998f687bc8555c3edb63 | 87cc6b3b555b65a2fc724bc310ff8fff10497fa3 | 2026-09-30T18:39:38Z |
| 同步前验收 | 161fe684fc9ace176cae31a56a9337b2c0a70d59 | 29d24237f376fbf289b62c5e0df2c18679b90115 | 2026-09-30T19:19:50Z |
| 同步前验收 | 3108a27be8195bffbf3f647a996a952dba73a142 | 6b78db5977508c819e29cddf66768c08fd93a97a | 2026-09-30T19:31:01Z |
| 同步前验收 | 4c4071e5d8e256dd8f82ba9c74efa1effd1e9df3 | d62f6806d2ac19d8f069340c0dc8158e5f38983c | 2026-10-01T04:04:49Z |
| 同步前验收 | 4c4071e5d8e256dd8f82ba9c74efa1effd1e9df3 | eb33d7b76ad9f0c9f62918b9c4ea24dab1534441 | 2026-10-01T04:59:18Z |
| 同步前验收 | 76b2fb35a99e0847fff832b4e14e5766ea474c4e | 62d39037afafc98a6bbad3689cf479a299bc4ad8 | 2026-10-01T05:20:28Z |
| 同步前验收 | 3072611209c445a0d87d54545cfd23d2b971e177 | 957de90142bc98bde3f479092e3892754000ef15 | 2026-10-01T05:23:44Z |
+27
View File
@@ -0,0 +1,27 @@
# feature/preview-sniff-fallback 任务卡
任务主记录:Dev PR #96 https://git-home.u1680832.nyat.app:30319/weichen/Agent-System-Dev/pulls/96(子仓:UI PR #21、Frontend PR #22);状态:进行中;owner:weichen
## 基线与授权
- 权威仓:ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/Agent-System-Dev.git;目标分支:main;R0(initial_base_sha)= d64742c71c1ebb8c1c88aef4c55194510f1cdf34
- 工作区:/home/weichen/projects/Agent-System-Dev-wt-feature-preview-sniff-fallback(worktree);分支:feature/preview-sniff-fallback
- 授权与交付终点:用户已批准「研发智能体文档处理与自举方案评审会议纪要.docx 解析失败」整改方案中的渲染侧兜底轨道。允许:modules/ui、modules/frontend 代码与测试、父仓 .gitignore 与 gitlink 推进、推送分支并建 PR。终点=PR 建立;合入权在所有者(不自行代签合入)。
## 目标与非目标
- 根因:智能体经 write_artifact/fs_write_file 写出的 ".docx" 实为纯文本字节(生成侧无 OOXML 序列化),前端按扩展名派给 mammoth/JSZip 抛 "Can't find end of central directory"。本轨道只做渲染侧兜底,让存量坏文件可读、错误可解释。
- 改动:① previewKindOf 将 `.doc`(老版二进制 Word,非 OOXML)归 binary,不再派给 mammoth;② OfficePreview 失败态文案收敛为用户可读提示(原始错误进 console);③ docxLoader 喂 mammoth 前嗅探 ZIP 魔数(PK\x03\x04),非 ZIP 按文本/markdown 回退渲染并提示;④ 父仓 .gitignore 收编 `.mimosa/`(Mimosa 插件逐会话状态目录反复弄脏树、阻塞 precheck)。
- 非目标:不实现真正的 docx 生成;不动后端。xlsx/pptx 的 ZIP 嗅探已在本分支补上(2026-10-01);老版 .xls/.ppt 仍走原解析器。
## 同步记录
| 节点 | sync_base_sha | tested_head_sha | checked_at |
|---|---|---|---|
| 启动 | d64742c71c1ebb8c1c88aef4c55194510f1cdf34 | d64742c71c1ebb8c1c88aef4c55194510f1cdf34 | 2026-09-30T17:46:06Z |
## 结果与验证(完成后填写)
- 结果:UI e082889(含 origin/main 与 `.doc`→binary、Office 错误文案收敛);Frontend bfa634d(docx/xlsx/pptx 非 ZIP 按文本回退,.xls/.ppt 除外);父仓 .gitignore 含 `.mimosa/`。分支已并入 origin/main(a11fed4)。
- 验证:合并后 UI vitest 68、Frontend vitest 40,两边 tsc 通过。Dev `make test` 通过(core、backend、frontend 40、ui 68、agent-command,2026-10-01)。
- 影响与剩余项:伪 Office 文件预览可读。生成侧执行器在 feature/exec-interpreters,不在本 PR。合入顺序:UI #21 → Frontend #22 → Dev #96。
+62
View File
@@ -0,0 +1,62 @@
# feature/svc-remote-source 任务卡
任务主记录:Dev PR #102 https://git-home.u1680832.nyat.app:30319/weichen/Agent-System-Dev/pulls/102 ;状态:进行中;owner:weichen
## 基线与授权
- 权威仓:ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/Agent-System-Dev.git;目标分支:main;R0(initial_base_sha)= f65d89e59b6974c7a5db63be0677d192bfaa7e9c
- 工作区:/home/weichen/projects/Agent-System-Dev-wt-feature-svc-remote-source(worktree);分支:feature/svc-remote-source
- 启动方式说明:主检出存在 owner 本人未收尾现场(`modules/frontend` 子模块修改态,前端重设计进行中),`task.sh start` 按门禁硬拒。owner 在任务对话中明确指示本任务并要求"更新/部署不依赖当前仓库状态",即 collaboration.md 的"经 owner 确认"路径;本工作区由 Agent 以与 task-start 完全相同的命令手动建立(同 R0、同命名、子模块零漂移),**全程未写主检出、未触碰该未提交现场**。
- 授权与交付终点:修改 `scripts/svc.sh` 与 `docs/svc.md`(实现 + 文档),验证后推送并建 PR;不合并(合入由 owner 决定);不动主检出 `modules/frontend` 现场。
## 目标与非目标
**背景**:`scripts/svc.sh update dev` 在主检出工作树不干净时跳过更新("主检出工作树不干净——跳过")。owner 明确要求:更新、部署等操作不应依赖当前仓库状态;不指定时默认使用最新仓库;构建放在独立于当前仓库/目录的地方。
**目标**:
1. 新增"独立部署源仓":脚本自有的干净克隆(默认 `~/.cache/agent-system-svc/src`,`SVC_SRC_DIR` 覆盖;可随时删除重建),dev 源的解析、列举(commits)、worktree 物化全部基于它。
2. 默认来源改为远端最新:裸 `deploy` 与 `--from dev`(不带 @sha)= 源仓 `origin/main` 最新提交;`--from local` 保留为显式"构建当前工作区"。
3. `update` 去 pull 化:删除"local 源 → 主检出 pull(要求分支=main + 树干净)"路径;`local@`/`dev@` 源一律重新部署为 `dev@<远端最新>`(隔离 worktree 构建),与工作区脏净/所在分支无关。领先/分叉/提交不在远端三种情形仍不动作(不盲目降级)。
4. `update` 的 fetch 也走源仓(不再 fetch 主检出);`commits dev` 列表改为源仓 `origin/main`(自动确保+fetch),`[HEAD]` 标记改为 `[最新]`。
5. 文档同步:脚本头注释、`help` 各主题、`docs/svc.md`(§2/§5/§6.3/§9.3/§11/§13/§14)。
**非目标**:不改 `--from local` 语义与实现;不改 release 源、数据库、生命周期、dbops 行为;不动 Makefile。
## 同步记录
| 节点 | sync_base_sha | tested_head_sha | checked_at |
|---|---|---|---|
| 启动 | f65d89e59b6974c7a5db63be0677d192bfaa7e9c | f65d89e59b6974c7a5db63be0677d192bfaa7e9c | 2026-09-30(手动 task-start 复刻) |
| 同步前验收 | 4c4071e5d8e256dd8f82ba9c74efa1effd1e9df3 | (合并提交) | 2026-10-01T04:10:00Z |
## 结果与验证
- 结果:已实现(提交 a42fff6)。svc.sh 新增**独立部署源仓**(`SVC_SRC_DIR`,默认
`~/.cache/agent-system-svc/src`,可随时删除重建):dev 源的提交解析、`commits` 列举、
worktree 物化全部基于它;裸 `deploy` 与 `--from dev` 缺省取源仓 `origin/main` 最新;
`update` 删除"主检出 pull(要求分支=main+树干净)"路径,`local@`/`dev@` 源一律重新
部署为 `dev@<远端最新>`(领先/分叉/提交不在远端三种情形仍不动作);`docs/svc.md`
与脚本 help 各主题同步更新。
- 验证:
- `bash -n scripts/svc.sh` 通过;`help`/`help lifecycle` 渲染正常;
- `scripts/svc.sh commits dev`:首次自动克隆源仓并 fetch,列表来自源仓 origin/main,
顶提交标 `[最新]`;
- 裸 `deploy --dry-run`(无 `--from`):计划 `dev@0be23f9`(源仓远端最新);
- **端到端(复刻 owner 场景)**:在任务 worktree 制造脏现场(未跟踪文件 + 跟踪文件
修改)并手工登记 `source=local@d64742c` 的服务,`scripts/svc.sh update svc-selftest`
直接更新成功:`local@d64742c → 0be23f9`,从源仓物化隔离 worktree 构建(npm ci +
vite + go build),服务启动健康(pid 1228696,http://127.0.0.1:8812,来源
`dev@0be23f9`,版本组合完整),全程未触碰工作区;随后 stop + rm --purge 清理,
worktree 无残留;
- `make task-sync`:sync_base=0be23f9a4d,tested_head=87cc6b3b55(并入 PR #101 后组合);
- `make test` 全绿(core ok;backend 全部 ok 含 server 4.8s;frontend 5 文件 33 用例;
ui 7 文件 49 用例;agentcmd ok),退出码 0。
- 影响与剩余项:
- 行为变化:部署缺省来源从"当前工作区"改为"远端 main 最新"——构建当前工作区须显式
`--from local`;`update` 不再依赖主检出状态,owner 遇到的"工作树不干净——跳过"
场景消除。
- 观察项(非本任务引入):部署时对产物 `serve -h` 探测不到 `--auth` 会自动省略该
flag(0be23f9 产物即如此),与旧版行为一致,健康检查通过。
- 剩余项:PR 合入由 owner 决定;合入并在主检出 pull 后,旧版"树脏跳过"即消失。
+49 -37
View File
@@ -12,7 +12,8 @@ PostgreSQL 数据库(postgres)三类服务统一为"服务"模型——按
- **登记持久化**:所有状态落盘(`svc.env` 当前态 + `history.jsonl` 事件流),`list` /
`history` 全部从文件重建,脚本无内存状态、跨会话可追溯。
- **不碰现场**:不改主检出 git 状态;只停"自己 pidfile + cmdline 双校验"通过的进程;
端口冲突报错不抢;除 `--from local` 外的构建全部在隔离 worktree。
端口冲突报错不抢;除 `--from local` 外的构建全部在隔离 worktree(物化自独立部署
源仓,与当前工作区状态无关——脏树、任意分支均可部署/更新)。
分主题速查:`scripts/svc.sh help [model|deploy|lifecycle|db|dbops|config|examples|safety]`;
本文档与 `--help` 内容同源,以实现为准。
@@ -54,7 +55,8 @@ PostgreSQL 工具链定位顺序:`SVC_PG_BIN` 环境变量 → `pg_config --bi
## 2. 90 秒上手
```bash
# 一套演示后端(构建当前工作区 → 启动 → 健康检查)
# 一套演示后端(缺省部署远端 main 最新:独立部署源仓物化构建 → 启动 → 健康检查;
# 与当前工作区状态无关。要构建当前工作区:--from local)
scripts/svc.sh dev deploy --mock
# 一个数据库 + 让后端连上它
@@ -74,7 +76,7 @@ scripts/svc.sh db backup pg1 # pg_dump -Fc,自动自检
scripts/svc.sh db restore pg1 --file <dump> --force # 覆盖恢复需 --force
scripts/svc.sh db clone pg1 pg1test --port 8896 # 克隆一套到新端口
# 仓库更新后,一条命令把服务刷到最新
# 把服务刷到远端最新(不依赖当前工作区状态:主检出脏树、任意分支均可)
scripts/svc.sh update --check # 先看哪些服务落后
scripts/svc.sh update # 自动更新过时且运行中的服务
```
@@ -215,9 +217,30 @@ scripts/svc.sh rm <name> --purge # 连 runtime/<name>/ 整个目录一起删
## 5. 命令参考:部署来源
`deploy --from` 支持三种来源(缺省 `local`),agent 与 static 共用:
`deploy --from` 支持三种来源(**缺省 = `dev` 远端最新**),agent 与 static 共用:
### 5.1 local —— 当前工作区
### 5.1 dev(缺省)—— 远端最新 / 指定提交
```
scripts/svc.sh <name> deploy # 缺省 = dev 远端最新(origin/main)
--from dev # 同缺省
--from dev@<sha> # 指定提交(短/长 sha 均可)
```
部署源来自**独立部署源仓**:脚本自有的干净克隆,默认 `~/.cache/agent-system-svc/src`
(`SVC_SRC_DIR` 覆盖;首次使用自动从本仓 origin 克隆,可随时删除、下次自动重建)。
dev 源的提交解析、`commits` 列举与构建 worktree 物化全部基于它——**更新与部署因此
不依赖当前工作区状态**:主检出脏树、任意分支、甚至未 fetch 都不影响。
流程:源仓自动 fetch → `git worktree add --detach runtime/.worktrees/<sha> <sha>` →
递归 `git submodule update --init --recursive`(子模块精确落位该提交的 gitlink 组合)→
`make build`/`make frontend` → 产物复制 → worktree 回收。全程不触碰当前工作区。
- 可用提交列表:`scripts/svc.sh commits dev [N]`(列源仓 origin/main,自动确保+fetch);
- 需要 npm/go 网络(构建在 worktree 内进行);
- worktree 回收失败会提示手动命令,不阻塞部署结果。
### 5.2 local —— 当前工作区(显式)
```
--from local
@@ -225,23 +248,8 @@ scripts/svc.sh rm <name> --purge # 连 runtime/<name>/ 整个目录一起删
在主检出跑 `make build`(agent)或 `make frontend`(static),产物复制到服务目录。
**这是唯一会写主检出的路径**,写的都是 gitignore 目录(`bin/`、`node_modules/`、
`modules/frontend/dist/`),不产生任何 git 状态变化。适合"把我现在的工作区跑起来"。
### 5.2 dev@\<sha\> —— Dev 仓提交
```
--from dev # 当前 HEAD
--from dev@<sha> # 指定提交(短/长 sha 均可)
```
流程:`git worktree add --detach runtime/.worktrees/<sha> <sha>` → 递归
`git submodule update --init --recursive`(子模块精确落位该提交的 gitlink 组合)→
`make build`/`make frontend` → 产物复制 → worktree 回收。全程不触碰当前工作区。
- 可用提交列表:`scripts/svc.sh commits dev [N]`;
- 只列本地已有对象;远端新提交先 `git -C <仓库> fetch`;
- 需要 npm/go 网络(构建在 worktree 内进行);
- worktree 回收失败会提示手动命令,不阻塞部署结果。
`modules/frontend/dist/`),不产生任何 git 状态变化;也是**唯一依赖当前工作区状态**
的来源。适合"把我现在的工作区跑起来"。
### 5.3 release@\<版本\> —— Releases 仓制品
@@ -264,8 +272,8 @@ scripts/svc.sh <name> deploy --pick # 交互式编号选择
scripts/svc.sh <name> deploy --dry-run # 只打印计划
```
`commits` 表格:dev 源标注 `[HEAD]` 与 `[已部署:<服务名>]`(从各服务 `svc.env source=`
汇总);release 源额外给出版本号与对应 Dev 提交。
`commits` 表格:dev 源标注 `[最新]`(源仓 origin/main 顶)与 `[已部署:<服务名>]`
(从各服务 `svc.env source=` 汇总);release 源额外给出版本号与对应 Dev 提交。
## 6. 命令参考:全局命令
@@ -303,7 +311,8 @@ scripts/svc.sh history [name] [N] # 默认全部服务、最近 20 条;首
scripts/svc.sh update [name...] [--check] [--all] [--no-fetch]
```
仓库更新后把服务刷到最新:fetch 远端 → 逐服务比较登记来源与最新 → 报告并(可选)更新。
把服务刷到远端最新:fetch 独立部署源仓 → 逐服务比较登记来源与最新 → 报告并(可选)
更新。**全程不依赖当前工作区状态**(主检出脏树、任意分支均可更新)。
| 用法 | 行为 |
|---|---|
@@ -317,7 +326,8 @@ scripts/svc.sh update [name...] [--check] [--all] [--no-fetch]
- `✓ 最新`——登记来源与最新一致,不动作;
- `可更新`——本地来源是远端最新的祖先,将被更新;
- `领先远端 main(不动作)` / `与远端 main 分叉(不自动更新)`——不盲目降级或强更;
- `领先远端 main(不动作)` / `与远端 main 分叉(不自动更新)` /
`提交不在远端(不自动更新)`——不盲目降级或强更;
- `不适用`——postgres(无代码版本)与 static `--dir`(外部目录)不参与。
各来源的更新动作(复用 deploy 全链路:停旧 → 构建/校验 → 启动 → 健康检查;持久化配置
@@ -325,8 +335,7 @@ scripts/svc.sh update [name...] [--check] [--all] [--no-fetch]
| 来源 | 更新方式 |
|---|---|
| `local@<sha>` | 当前分支为 main 且工作树干净时:自动 `git pull --ff-only origin main` + `make sync` + 重建;**树脏或非 main 分支则跳过并警告**(绝不碰未提交现场) |
| `dev@<sha>` | `deploy --from dev@<远端 main 最新>`(隔离 worktree 构建,不碰主检出) |
| `local@<sha>` / `dev@<sha>` | 一律 `deploy --from dev@<远端 main 最新>`(独立部署源仓 + 隔离 worktree 构建;不 pull 主检出、不看工作树状态) |
| `release@<版本>` | `deploy --from release@<Releases 最新版本>`(sha256 校验) |
任一服务失败不中断其余服务;最终汇总 `成功/失败/跳过`。退出码:0 全部成功或最新;
@@ -479,6 +488,7 @@ scripts/svc.sh prod deploy --env .env.prod # 等价
|---|---|
| `SVC_<NAME>_ADDR/DATA/DSN/AUTH/MOCK` | 按服务覆盖(NAME 大写、`-`→`_`,如 `staging` → `SVC_STAGING_ADDR`) |
| `SVC_RELEASES_DIR` | Releases 仓路径(默认 `../Agent-System-Releases`) |
| `SVC_SRC_DIR` | 独立部署源仓路径(默认 `~/.cache/agent-system-svc/src`) |
| `SVC_PG_BIN` | PostgreSQL 工具链 bindir |
| `SVC_MODEL_BASE/KEY/NAME` | 主模型(播种 `settings.models.chat`) |
| `SVC_LIGHT_BASE/KEY/NAME` | 轻量模型 |
@@ -531,9 +541,10 @@ scripts/svc.sh prod deploy --env .env.prod # 等价
## 11. 安全与边界
- **写入边界**:只写 `runtime/<name>/` 与 `runtime/.worktrees/`;不改主检出 git 状态;
`--from local` 是唯一写主检出构建产物的路径(均 gitignore 目录);`rm --purge` 只删
已登记服务的 `runtime/<name>/`。
- **写入边界**:只写 `runtime/<name>/`、`runtime/.worktrees/` 与独立部署源仓
(`SVC_SRC_DIR`,默认 `~/.cache/agent-system-svc/src`,可随时删除重建);不改主检出
git 状态;`--from local` 是唯一写主检出构建产物的路径(均 gitignore 目录),也是
唯一依赖当前工作区状态的来源;`rm --purge` 只删已登记服务的 `runtime/<name>/`。
- **进程边界**:只停"pidfile + cmdline 标记"双校验通过的进程;postgres 走 `pg_ctl -m
fast`,失败退 SIGTERM,SIGKILL 不用于 postmaster;agent/static TERM 10s 后才 KILL。
- **端口边界**:被他人占用报错不抢;显式端口双重校验(登记 + 监听);派生端口避让。
@@ -562,18 +573,19 @@ scripts/svc.sh prod deploy --env .env.prod # 等价
| `pg_ctl start 失败` | 看 pg.log 常见:端口被占、pgdata 权限(须 700)、postmaster 残留 |
| `pidfile 指向的进程已不存在` | 脚本自动清理 pidfile;直接重新 start 即可 |
| `sha256 校验失败` | 制品与 manifest 不符:不要部署;在 Releases 仓重新 build |
| `Dev 仓无此提交` | 本地无该对象:`git -C <仓库> fetch` 后重试 |
| `worktree 回收失败` | 按提示手动 `git worktree remove --force runtime/.worktrees/<sha>` |
| `子模块物化失败` | 子模块对象缺失:先 fetch;或网络问题重试 |
| `部署源仓无此提交` | 源仓已自动 fetch 仍无此对象:确认 sha 正确(`scripts/svc.sh commits dev`) |
| `worktree 回收失败` | 按提示手动 `git -C <部署源仓> worktree remove --force runtime/.worktrees/<sha>` |
| `子模块物化失败` | 网络或子模块对象问题:重试(部署源仓会自动 fetch);或检查 git 远端可达性 |
| `目标库已有 N 张表——拒绝覆盖` | 确认后加 `--force`(会 drop 重建) |
| `update 报"当前分支非 main"或"工作树不干净" | local 源服务的自动 pull 被防护跳过:先自行收尾/pull,再重跑 update;或改用 `deploy --from dev@<sha>` |
| `提交不在远端(不自动更新)` | 该服务部署自未推送的本地提交(如 `--from local`):重新 `deploy`(缺省即远端最新)即可纳入更新轨道 |
| `list 里服务显示未运行但进程在` | 手工起的进程不满足 pidcheck 标记:脚本不会动它;纳入管理请走 svc 部署 |
## 14. 配方索引
```bash
# ① 演示后端
# ① 演示后端(缺省部署远端 main 最新;--from local 构建当前工作区)
scripts/svc.sh dev deploy --mock
scripts/svc.sh dev deploy --from local --mock
# ② 建库 + 后端连库
scripts/svc.sh db create pg1 --port 8890
@@ -608,7 +620,7 @@ scripts/svc.sh db copy pgdev pgprod --db app --force
# ⑨ 换端口
scripts/svc.sh set pg1 --port 8895 --apply
# ⑩ 仓库更新后刷新服务
# ⑩ 把服务刷到远端最新(主检出脏树/任意分支均可)
scripts/svc.sh update --check # 看哪些落后
scripts/svc.sh update # 更新过时且运行中的(数据与配置保留)
scripts/svc.sh update dev # 只刷新指定服务
+106 -87
View File
@@ -19,13 +19,15 @@
# scripts/svc.sh db set <name> [...] 等价于 set
# scripts/svc.sh commits [dev|release] [N] 可部署提交列表(默认 dev,N=20)
# scripts/svc.sh history [name] [N] 事件历史(默认全部,最近 20 条)
# scripts/svc.sh update [name] [--check] [--all] 检测远端最新并更新过时服务
# scripts/svc.sh update [name] [--check] [--all] 检测远端最新并更新过时服务(与当前工作区状态无关)
# scripts/svc.sh help [主题] 分主题详细帮助
#
# ════════════════════ 部署来源(agent/static 的 deploy)════════════════════
# (缺省)dev@远端最新 不指定 --from 时默认部署源仓 origin/main 最新提交——
# 更新/部署不依赖当前工作区状态(脏树、任意分支均可)
# --from dev[@<sha>] 同缺省;@<sha> 显式指定提交。物化与构建在独立部署源仓的
# 隔离 worktree(runtime/.worktrees/<sha>)进行,不触碰当前工作区
# --from local 构建当前工作区(唯一写主检出构建产物的路径,均 gitignore)
# --from dev[@<sha>] 隔离 worktree(runtime/.worktrees/<sha>)物化该提交的
# 子模块组合并构建,不触碰当前工作区
# --from release[@<版本|sha>] Releases 仓制品(sha256 对 versions/<v>/manifest.json 校验)
# --pick [dev|release] 交互式编号选提交;--dry-run 只打印部署计划
#
@@ -73,9 +75,10 @@
# make svc ENV=dev ARGS="--env .env.dev --port 9000"
#
# ════════════════════ 安全约束 ════════════════════
# - 只写 runtime/<name>/ 与 runtime/.worktrees/;不改主检出 git 状态、不动他人现场;
# - 只写 runtime/<name>/、runtime/.worktrees/ 与独立部署源仓(SVC_SRC_DIR,默认
# ~/.cache/agent-system-svc/src,可随时删除重建);不改主检出 git 状态、不动他人现场;
# --from local 是唯一写主检出构建产物(bin/、node_modules/、dist/,均 gitignore)
# 的路径,dev@sha/release/static 构建全部在隔离 worktree;
# 的路径,dev@sha/release/static 构建全部在隔离 worktree(自部署源仓物化);
# - 只停自己启动的进程:pidfile + /proc/<pid>/cmdline 标记双校验;postgres 走
# pg_ctl -m fast 优雅停机,SIGKILL 不用于 postmaster;
# - 端口被非本脚本进程占用时报错并给出占用者,不抢不杀;
@@ -92,6 +95,10 @@ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
RUNTIME="$ROOT/runtime"
BIN="$ROOT/bin/agent-system"
SVC_ENVFILE=""
# 独立部署源仓:脚本自有的干净克隆(默认 ~/.cache/agent-system-svc/src,SVC_SRC_DIR 覆盖)。
# dev 源的解析/列举/worktree 物化全部基于它——更新与部署因此不依赖当前工作区状态
# (脏树、任意分支均可);目录可随时删除,下次使用自动重建。
SRC_DIR="${SVC_SRC_DIR:-${XDG_CACHE_HOME:-$HOME/.cache}/agent-system-svc/src}"
# 提示信息走 stderr:stdout 只留给数据(提交表/服务表/DSN),便于 $( ) 捕获组合。
c_info() { printf '\033[1;34m[svc]\033[0m %s\n' "$*" >&2; }
@@ -110,7 +117,7 @@ svc.sh —— 本机服务管理:后端(agent) / 前端(static) / 数据库(po
常用:
scripts/svc.sh list [--tag T] 服务总览
scripts/svc.sh <name> deploy --from dev@<sha> 按提交部署并启动(agent/static)
scripts/svc.sh <name> deploy [--from dev@<sha>] 部署并启动(缺省=远端 main 最新)
scripts/svc.sh <name> start|stop|restart|logs [N] 生命周期与日志
scripts/svc.sh <name> status 服务详情(版本/来源/DSN 等)
scripts/svc.sh db create pg1 --port 8890 建数据库
@@ -119,7 +126,7 @@ svc.sh —— 本机服务管理:后端(agent) / 前端(static) / 数据库(po
scripts/svc.sh rm pg1 [--purge] 注销(--purge 连数据删除)
scripts/svc.sh commits [dev|release] [N] 可部署提交
scripts/svc.sh history [name] [N] 事件历史
scripts/svc.sh update [name] [--check] 检测远端最新并更新过时服务
scripts/svc.sh update [name] [--check] 刷新过时服务到远端最新(不依赖工作区状态)
帮助:--help 完整帮助 | help [主题] 分主题(model deploy lifecycle db dbops config examples safety)
文档:docs/svc.md
@@ -204,13 +211,18 @@ EOF
语法:scripts/svc.sh <name> deploy [--kind agent|static] [--from 源] [参数...]
部署来源(--from,默认 local):
部署来源(--from,缺省=dev 远端最新):
(缺省)/ dev 部署独立部署源仓的 origin/main 最新提交——与当前工作区
状态无关(脏树、任意分支均可)。
dev@<sha> 显式指定提交,物化与构建同上。
两者统一流程:在独立部署源仓(SVC_SRC_DIR,默认 ~/.cache/agent-system-svc/src,
首次使用自动从 origin 克隆)上 git worktree add --detach 出
runtime/.worktrees/<sha>,递归物化子模块组合(精确落位 gitlink),
make build/frontend,产物复制后回收 worktree。全程不触碰当前工作区。
local 构建当前工作区(make build / make frontend),产物复制到
服务目录。注意:这是唯一会写主检出构建产物(bin/、
node_modules/、dist/——均 gitignore)的路径。
dev[@<sha>] 在隔离 worktree(runtime/.worktrees/<sha>)检出该提交,
递归物化子模块组合(精确落位 gitlink),make build/frontend,
产物复制后回收 worktree。子模块对象缺失时需先 git fetch。
node_modules/、dist/——均 gitignore)的路径,也是唯一
依赖当前工作区状态的来源。
release[@<版本|sha>] 取 Releases 仓 build/<v>/agent-system(+plugins),
sha256 对 versions/<v>/manifest.json 校验,不符即拒绝。
--pick [dev|release] 交互式编号选择(需 TTY;列表与提示走 stderr)
@@ -255,14 +267,14 @@ EOF
scripts/svc.sh rm <name> 停止并移除登记(目录/数据/历史保留)
scripts/svc.sh rm <name> --purge 连 runtime/<name>/ 整个目录一起删除
自动更新(仓库更新后一条命令刷新服务):
自动更新(一条命令把服务刷到远端最新;与当前工作区状态无关):
scripts/svc.sh update --check 只报告:每服务 当前来源 vs 最新(fetch 后比较)
scripts/svc.sh update 更新所有"过时且运行中"的服务
scripts/svc.sh update dev prod 点名更新(已停止的过时服务也会更新并启动)
scripts/svc.sh update --all 含已停止服务;--no-fetch 跳过网络
- local 源:自动 git pull --ff-only origin main + make sync 后重建
(仅当前分支==main 且工作树干净;否则跳过并警告,不碰未提交现场)
- dev@sha 源:直接部署远端 main 最新提交(隔离 worktree 构建,不碰主检出)
- local@/dev@ 源:一律重新部署为 dev@<远端 main 最新>(独立部署源仓 + 隔离
worktree 构建;主检出脏净、所在分支均不影响,也不 pull 主检出)
- 领先远端 main / 与远端分叉 / 提交不在远端:报告但不动作(不盲目降级)
- release@源:部署 Releases 仓最新版本(sha256 校验)
- postgres / static --dir 不参与更新;每次更新写 history(update 事件)
EOF
@@ -345,6 +357,7 @@ dotenv 格式:KEY=VALUE,支持 # 注释、成对引号、export 前缀;只
环境变量(X 为服务名大写,- → _):
SVC_X_ADDR / SVC_X_DATA / SVC_X_DSN / SVC_X_AUTH / SVC_X_MOCK
SVC_RELEASES_DIR Releases 仓路径(默认 ../Agent-System-Releases)
SVC_SRC_DIR 独立部署源仓路径(默认 ~/.cache/agent-system-svc/src)
SVC_PG_BIN postgres 工具链 bindir(默认 pg_config --bindir 探测)
SVC_MODEL_BASE/KEY/NAME 主模型(agent 启动健康后播种)
SVC_LIGHT_BASE/KEY/NAME 轻量模型
@@ -362,8 +375,9 @@ EOF
cat >&2 <<'EOF'
【examples】常见配方
① 起一套演示后端(当前工作区构建):
① 起一套演示后端(缺省部署远端 main 最新;--from local 才构建当前工作区):
scripts/svc.sh dev deploy --mock
scripts/svc.sh dev deploy --from local --mock
② 建库 + 后端连库:
scripts/svc.sh db create pg1 --port 8890
scripts/svc.sh pg1 start
@@ -401,9 +415,10 @@ EOF
【safety】安全约束与边界
写入边界:
- 只写 runtime/<name>/ 与 runtime/.worktrees/(构建 worktree,用后即收);
- 只写 runtime/<name>/、runtime/.worktrees/(构建 worktree,用后即收)与独立
部署源仓(SVC_SRC_DIR,默认 ~/.cache/agent-system-svc/src,可随时删除重建);
- 不改主检出 git 状态;--from local 是唯一写主检出构建产物(bin/、node_modules/、
dist/——均 gitignore)的路径;
dist/——均 gitignore)的路径,也是唯一依赖当前工作区状态的来源;
- rm --purge 只删 runtime/<name>/,且要求服务已登记。
进程边界:
@@ -781,6 +796,31 @@ releases_root() {
fi
}
# svc_src_ready:确保独立部署源仓就绪(不存在则从 $ROOT 的 origin 首次克隆)并 fetch
# 远端;--no-fetch 跳过网络。同一次命令运行内幂等(首个调用定下 fetch 策略)。
# 失败 return 1(无 origin 可克隆/克隆失败),调用方自行决定报错或降级。
SRC_READY_DONE=0
svc_src_ready() {
[ "$SRC_READY_DONE" = 1 ] && return 0
if [ ! -d "$SRC_DIR/.git" ]; then
local url
url="$(git -C "$ROOT" remote get-url origin 2>/dev/null || true)"
[ -n "$url" ] || { c_err "无法确定 origin 地址($ROOT 无 origin 远端);可用 SVC_SRC_DIR 指向已有仓"; return 1; }
c_info "初始化独立部署源仓:$SRC_DIR(← origin;与当前工作区无关,可随时删除重建)"
mkdir -p "$(dirname "$SRC_DIR")"
if ! git clone --quiet "$url" "$SRC_DIR" >&2; then
c_err "部署源仓克隆失败:$url → $SRC_DIR"
return 1
fi
fi
if [ "${1:-}" != "--no-fetch" ]; then
c_info "git fetch(独立部署源仓:$SRC_DIR)"
git -C "$SRC_DIR" fetch origin --quiet 2>/dev/null || c_warn "部署源仓 fetch 失败——基于本地缓存($SRC_DIR)"
fi
SRC_READY_DONE=1
return 0
}
deployed_envs() {
local key="$1" d e s out=""
for d in "$RUNTIME"/*/; do
@@ -797,20 +837,21 @@ print_commit_list() { # SRC N PICK:提交表到 stdout;PICK=1 时填充 PICK
local src="$1" count="$2" pick="$3"
local sep=$'\037'
if [ "$src" = dev ]; then
c_info "Dev 仓提交($ROOT;倒序取 $count 条)——部署:scripts/svc.sh <name> deploy --from dev@<sha>"
svc_src_ready || { c_err "部署源仓不可用,无法列出可部署提交"; return 1; }
c_info "Dev 仓提交(独立部署源仓 $SRC_DIR 的 origin/main;倒序取 $count 条)——部署:scripts/svc.sh <name> deploy --from dev@<sha>"
printf ' %3s %-9s %-10s %s\n' '#' 'sha' '日期' '主题'
local head_short; head_short="$(git -C "$ROOT" rev-parse --short HEAD)"
local latest_short; latest_short="$(git -C "$SRC_DIR" rev-parse --short origin/main 2>/dev/null || true)"
local n=0 h date subject marks envs
while IFS="$sep" read -r h date subject; do
[ -z "$h" ] && continue
n=$((n + 1))
marks=""
[ "$h" = "$head_short" ] && marks="${marks}[HEAD]"
[ "$h" = "$latest_short" ] && marks="${marks}[最新]"
envs="$(deployed_envs "dev@$h")"
[ -n "$envs" ] && marks="${marks}[已部署:$envs]"
printf ' %3d %-9s %-10s %s %s\n' "$n" "$h" "$date" "$subject" "$marks"
[ "$pick" = 1 ] && PICK_OPTS+=("$h")
done < <(git -C "$ROOT" log -n "$count" --format="%h${sep}%ad${sep}%s" --date=short)
done < <(git -C "$SRC_DIR" log -n "$count" --format="%h${sep}%ad${sep}%s" --date=short origin/main)
else
c_info "Releases 仓提交($REL_ROOT;倒序取 $count 条)——部署:scripts/svc.sh <name> deploy --from release@<版本>"
printf ' %3s %-9s %-10s %-8s %-9s %s\n' '#' 'sha' '日期' '版本' 'dev提交' '主题'
@@ -882,7 +923,7 @@ resolve_release() { # REF(空=最新) → REL_COMMIT/REL_VERSION/SRC_DEVCOMMIT
}
resolve_source() { # → SRC_KINDSRC(local|dev|release) + SRC_LABEL(+SRC_FULL/SRC_SHA/REL_*)
local spec="${CLI_FROM:-local}"
local spec="${CLI_FROM:-dev}"
if [ -n "$CLI_PICK" ]; then
[ "$CLI_PICK" = release ] && releases_root
spec="$(pick_commit "$CLI_PICK")"
@@ -893,16 +934,18 @@ resolve_source() { # → SRC_KINDSRC(local|dev|release) + SRC_LABEL(+SRC_FULL/
SRC_LABEL="local@$(git -C "$ROOT" rev-parse --short HEAD)"
;;
dev|dev@*)
local ref="HEAD"
# 缺省 ref=origin/main(独立部署源仓的远端最新);显式 @<sha> 亦可。
local ref="origin/main"
case "$spec" in dev@*) ref="${spec#dev@}" ;; esac
svc_src_ready || { c_err "部署源仓不可用——dev 源依赖它;或改用 --from local 构建当前工作区"; exit 2; }
local full
full="$(git -C "$ROOT" rev-parse --verify --quiet "${ref}^{commit}")" || {
c_err "Dev 仓无此提交:$ref(若是远端新提交,先 git -C $ROOT fetch;可用项:scripts/svc.sh commits dev)"
full="$(git -C "$SRC_DIR" rev-parse --verify --quiet "${ref}^{commit}")" || {
c_err "部署源仓无此提交:$ref(已自动 fetch;可用项:scripts/svc.sh commits dev)"
exit 2
}
SRC_KINDSRC=dev
SRC_FULL="$full"
SRC_SHA="$(git -C "$ROOT" rev-parse --short "$full")"
SRC_SHA="$(git -C "$SRC_DIR" rev-parse --short "$full")"
SRC_LABEL="dev@$SRC_SHA"
;;
release|release@*)
@@ -942,13 +985,13 @@ materialize_dev_commit() {
if [ -d "$wt" ]; then
c_info "[$ENV_NAME] 复用已有构建 worktree:$wt"
else
c_info "[$ENV_NAME] 物化 Dev 提交 $sha → 隔离 worktree(不触碰当前工作区)"
git -C "$ROOT" worktree add --detach "$wt" "$full" >/dev/null || return 1
c_info "[$ENV_NAME] 物化 Dev 提交 $sha → 隔离 worktree(源:独立部署源仓 $SRC_DIR,不触碰当前工作区)"
git -C "$SRC_DIR" worktree add --detach "$wt" "$full" >/dev/null || return 1
fi
c_info "[$ENV_NAME] 物化子模块组合(git submodule update --init --recursive;缺对象时需网络)"
# stdout 只留给最后的 worktree 路径(调用方 $() 捕获)——子模块/构建输出全部走 stderr。
git -C "$wt" submodule update --init --recursive >&2 || {
c_err "[$ENV_NAME] 子模块物化失败(对象缺失时先 git -C $ROOT fetch origin)"
c_err "[$ENV_NAME] 子模块物化失败(网络或对象缺失;重试会自动 fetch 部署源仓)"
return 1
}
c_info "[$ENV_NAME] 构建(make $target:npm ci + 构建,需网络)"
@@ -960,8 +1003,8 @@ materialize_dev_commit() {
recycle_worktree() {
local wt="$RUNTIME/.worktrees/$1"
c_info "[$ENV_NAME] 回收构建 worktree"
git -C "$ROOT" worktree remove --force "$wt" >/dev/null 2>&1 \
|| c_warn "[$ENV_NAME] worktree 回收失败,可手动:git -C $ROOT worktree remove --force $wt"
git -C "$SRC_DIR" worktree remove --force "$wt" >/dev/null 2>&1 \
|| c_warn "[$ENV_NAME] worktree 回收失败,可手动:git -C $SRC_DIR worktree remove --force $wt"
}
materialize_release() {
@@ -1027,7 +1070,7 @@ print_agent_plan() {
printf ' 来源:%s(make build 当前工作区;唯一写主检出构建产物的路径)\n' "$SRC_LABEL"
printf ' 产物:%s ← %s + %s/plugins\n' "$ENV_BIN" "$BIN" "$ROOT" ;;
dev)
printf ' 来源:%s(完整提交 %s)\n' "$SRC_LABEL" "$SRC_FULL"
printf ' 来源:%s(完整提交 %s;独立部署源仓 %s)\n' "$SRC_LABEL" "$SRC_FULL" "$SRC_DIR"
printf ' 物化:worktree %s(submodule update --init --recursive + make build)\n' "$RUNTIME/.worktrees/$SRC_SHA" ;;
release)
printf ' 来源:%s(Releases 提交 %s)\n' "$SRC_LABEL" "$(git -C "$REL_ROOT" rev-parse --short "$REL_COMMIT")"
@@ -1229,8 +1272,8 @@ print_static_plan() {
else
case "$SRC_KINDSRC" in
local) printf ' 来源:%s(make frontend → htdocs/)\n' "$SRC_LABEL" ;;
dev) printf ' 来源:%s(完整提交 %s)\n 物化:worktree %s(make frontend → htdocs/)\n' \
"$SRC_LABEL" "$SRC_FULL" "$RUNTIME/.worktrees/$SRC_SHA" ;;
dev) printf ' 来源:%s(完整提交 %s;独立部署源仓 %s)\n 物化:worktree %s(make frontend → htdocs/)\n' \
"$SRC_LABEL" "$SRC_FULL" "$SRC_DIR" "$RUNTIME/.worktrees/$SRC_SHA" ;;
release) printf ' 来源:release —— 不支持(制品内嵌前端);请用 dev|local 或 --dir\n' ;;
esac
fi
@@ -2047,9 +2090,10 @@ cmd_history() {
# ---- 自动检索与更新 ----
# cmd_update [NAME...] [--check] [--all] [--no-fetch]:
# fetch 远端 → 逐服务比较登记 source 与最新(Dev 仓 origin/main / Releases 最新版本)→
# fetch 独立部署源仓 → 逐服务比较登记 source 与远端 main 最新(Releases 最新版本)→
# --check 只报告;否则更新"过时且运行中"的服务(点名或 --all 时含已停止)。
# local 源的自动 pull 受三重保护:当前分支==main + 工作树干净 + --ff-only。
# local@/dev@ 源一律重新部署为 dev@<远端最新>(隔离 worktree 构建)——与主检出
# 工作树状态完全无关(脏树、任意分支均可);领先/分叉/提交不在远端不动作。
cmd_update() {
local names=() a
while [ $# -gt 0 ]; do
@@ -2081,28 +2125,21 @@ cmd_update() {
fi
[ "${#targets[@]}" -gt 0 ] || { c_info "暂无已登记服务(db create / deploy 会登记)"; return 0; }
# fetch 远端(一次性;失败不致命,退化为本地缓存引用比较)。
local fetch_note=""
if [ "$CLI_NOFETCH" = 1 ]; then
fetch_note="基于本地缓存引用"
else
c_info "git fetch origin(检索远端更新)"
if git -C "$ROOT" fetch origin --quiet 2>/dev/null; then
:
else
fetch_note="fetch 失败,基于本地缓存引用"
c_warn "git fetch 失败——$fetch_note"
fi
fi
local latest_sha
latest_sha="$(git -C "$ROOT" rev-parse --verify --quiet origin/main || true)"
# Releases 最新版本(仅当存在 release 源服务时才访问 Releases 仓)。
local latest_rel="" need_rel=0 e_src
# 按目标来源决定访问哪些仓:local@/dev@ 走独立部署源仓;release@ 走 Releases 仓。
local latest_sha="" latest_rel="" need_rel=0 need_dev=0 e_src
for e in ${targets[@]+"${targets[@]}"}; do
e_src="$(sed -n 's/^source=//p' "$RUNTIME/$e/svc.env" | head -1)"
case "$e_src" in release@*) need_rel=1 ;; esac
case "$e_src" in release@*) need_rel=1 ;; local@*|dev@*) need_dev=1 ;; esac
done
local fetch_note="" src_args=""
[ "$CLI_NOFETCH" = 1 ] && { fetch_note="--no-fetch,基于本地缓存引用"; src_args="--no-fetch"; }
if [ "$need_dev" = 1 ]; then
if svc_src_ready $src_args; then
latest_sha="$(git -C "$SRC_DIR" rev-parse --verify --quiet origin/main || true)"
else
c_warn "部署源仓不可用——local/dev 源服务本轮无法检测与更新"
fi
fi
if [ "$need_rel" = 1 ]; then
releases_root
if [ "$CLI_NOFETCH" != 1 ]; then
@@ -2138,14 +2175,17 @@ cmd_update() {
cur="${src##*@}"
if [ -z "$latest_sha" ]; then
state="远端 main 未知"; latest_disp="-"
elif ! git -C "$SRC_DIR" cat-file -e "${cur}^{commit}" 2>/dev/null; then
state="提交不在远端(不自动更新;重新 deploy 可纳入更新轨道)"
latest_disp="$(git -C "$SRC_DIR" rev-parse --short "$latest_sha" 2>/dev/null || echo "-")"
elif [ "${latest_sha:0:${#cur}}" = "$cur" ]; then
state="✓ 最新"; latest_disp="$(git -C "$ROOT" rev-parse --short "$latest_sha" 2>/dev/null || echo "-")"
elif git -C "$ROOT" merge-base --is-ancestor "$cur" "$latest_sha" 2>/dev/null; then
to="$(git -C "$ROOT" rev-parse --short "$latest_sha" 2>/dev/null || true)"
state="✓ 最新"; latest_disp="$(git -C "$SRC_DIR" rev-parse --short "$latest_sha" 2>/dev/null || echo "-")"
elif git -C "$SRC_DIR" merge-base --is-ancestor "$cur" "$latest_sha" 2>/dev/null; then
to="$(git -C "$SRC_DIR" rev-parse --short "$latest_sha" 2>/dev/null || true)"
latest_disp="$to"
state="可更新"
elif git -C "$ROOT" merge-base --is-ancestor "$latest_sha" "$cur" 2>/dev/null; then
state="领先远端 main(不动作)"; latest_disp="$(git -C "$ROOT" rev-parse --short "$latest_sha" 2>/dev/null || echo "-")"
elif git -C "$SRC_DIR" merge-base --is-ancestor "$latest_sha" "$cur" 2>/dev/null; then
state="领先远端 main(不动作)"; latest_disp="$(git -C "$SRC_DIR" rev-parse --short "$latest_sha" 2>/dev/null || echo "-")"
else
state="与远端 main 分叉(不自动更新)"; latest_disp="-"
fi
@@ -2195,32 +2235,11 @@ cmd_update() {
local from="${rest%%|*}" tov="${rest#*|}"
local deploy_from=""
env_config "$name"
# local@/dev@ 源统一重新部署为 dev@<远端最新>:独立部署源仓 + 隔离 worktree 构建,
# 不 pull 主检出、不看工作树状态(脏树、任意分支均可更新)。
case "$from" in
local@*)
local branch dirty
branch="$(git -C "$ROOT" rev-parse --abbrev-ref HEAD)"
if [ "$branch" != "main" ]; then
c_warn "[$name] 当前分支 $branch 非 main——跳过自动 pull(手动更新后重跑 update)"
skipped=$((skipped + 1)); continue
fi
dirty="$(git -C "$ROOT" status --porcelain | grep -v '^.. \.zcode/' || true)"
if [ -n "$dirty" ]; then
c_warn "[$name] 主检出工作树不干净——跳过(不碰未提交现场)"
skipped=$((skipped + 1)); continue
fi
c_info "[$name] git pull --ff-only origin main + make sync"
if ! git -C "$ROOT" pull --ff-only origin main >/dev/null 2>&1; then
c_err "[$name] 自动 pull 失败(可能本地 main 与远端分叉)——跳过,请手动处理"
hist_event update fail "$(read_pid)" "pull 失败(from=$from)"
failed=$((failed + 1)); continue
fi
if ! make -C "$ROOT" sync >/dev/null 2>&1; then
c_warn "[$name] make sync 失败——继续构建(若子模块缺失构建会失败)"
fi
deploy_from="local"
;;
dev@*) deploy_from="dev@$tov" ;;
release@*) deploy_from="release@$tov" ;;
local@*|dev@*) deploy_from="dev@$tov" ;;
release@*) deploy_from="release@$tov" ;;
esac
c_info "[$name] 更新:$from → $tov($deploy_from)"
hist_event update begin "$(read_pid)" "from=$from to=$tov"