diff --git a/.task/archive/feature-cross-session-reading.md b/.task/archive/feature-cross-session-reading.md new file mode 100644 index 00000000..3155e225 --- /dev/null +++ b/.task/archive/feature-cross-session-reading.md @@ -0,0 +1,34 @@ +# feature/cross-session-reading 任务卡 + +任务主记录:PR #67 https://git-panel.u1680832.nyat.app:30319/weichen/Agent-System-Dev/pulls/67(merge 816b40e8f7);状态:已完成(回填);owner:weichen + +## 基线与授权 + +- 权威仓:ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/Agent-System-Dev.git;目标分支:main;R0(initial_base_sha)= ffda6d33eabb23c9be5bac221529367d771c5f7b +- 工作区:/home/weichen/projects/Agent-System-Dev-wt-feature-cross-session-reading(worktree);分支:feature/cross-session-reading +- 授权与交付终点:实施授权(用户已批准计划并选择"提案+全量实现")。终点:documents 提案、core/backend/frontend 实现 PR 全部合入,Dev 仓 gitlink 钉入并 `make test` 组合验证通过,任务卡闭合。未授权:改写历史、分支保护变更、删除归档引用、发布版本(双钥匙)。 + +## 目标与非目标 + +目标:同项目会话间按需读取对话记录——分层权限(项目开关 `crossSession.read` 默认关 + 会话出站 `allowCrossRead` 默认 true / 入站 `crossPrivate` 默认 false)+ 两个内置工具(`list_conversations`/`read_conversation`,落地设计 4.8 规划的"会话历史检索")+ fail-closed 门禁(runmgr toolCtx 内逐条校验)+ UI(项目设置面板首个项目级设置 UI、会话两开关)。受影响:modules/documents(提案)、modules/core(Conversation 契约+TS 镜像+golden)、modules/backend(settings schema/迁移 v11/store/PATCH/工具+门禁)、modules/frontend(api/contracts/项目设置/会话开关)。 +非目标:项目关时的 per-session opt-in;跨项目读取;自动注入其他会话摘要(远期可选档);squad 中间工作区暴露;artifacts/知识可见性变更(本就项目共享)。 + +## 同步记录 + +| 节点 | sync_base_sha | tested_head_sha | checked_at | +|---|---|---|---| +| 启动 | ffda6d33eabb23c9be5bac221529367d771c5f7b | ffda6d33eabb23c9be5bac221529367d771c5f7b | 2026-09-30T14:55:34Z | +| 合入前(task-sync 并入上游 ca848883;最终 make test 于合并后头全绿) | ca8488834529f154b30dda5a96d9ea8896f1782c | 见下行 task-sync 检查点 | 2026-09-30T15:35:00Z | + +## 结果与验证 + +- 结果:跨会话读取全链路交付。子仓 PR 全部合入:Documents #21(提案 bf0988b)、Core #6(Conversation 契约字段 fd44179)、Backend #18(settings crossSession.read + 迁移 v11 + PATCH + list/read 工具 + runmgr fail-closed 门禁 cb7f79d)、Frontend #16(项目设置面板 + 会话开关 9c87953)、UI #15(contracts 镜像同步 c756479,make contracts 机制联动)。Dev 仓仅 gitlink 钉入。 +- 验证:① backend `go test ./...` 全绿,含 runmgr/crossread_test.go 门禁矩阵(项目开关×出站×私有×跨项目×跨租户×自读×归档×limit 钳制)与 store 迁移/标记持久化测试;② core `go test ./...` golden 一致;③ frontend `npm run build` + `npm test` 15/15;④ Dev `make test` 组合验证在钉入树(五子仓新 gitlink)两轮全绿(core/backend/frontend/ui/agentcmd 五套件);⑤ `make task-sync` 合入前节点通过。 +- 影响与剩余项:默认关闭,升级无行为变化。剩余(提案非目标):项目关时的 per-session opt-in、自动注入摘要档(远期)、squad 中间工作区暴露(非目标)。 +| 同步前验收 | ca8488834529f154b30dda5a96d9ea8896f1782c | e6c5cd99df36b8bfd549cacfa7bcb4a9b0f2aa2e | 2026-09-30T15:26:30Z | + +## 闭合记录(card-autoclose 回填) + +- 依据:merge 816b40e8f74d27218c2ed321dc88007fd298bed3(PR #67)已包含于 origin/main,分支成果已落盘;本段为合入后回填。 +- 合并主题:Merge pull request 'Deliver layered cross-session reading and pin the five module PRs' (#67) from feature/cross-session-reading into main +- 归档:随闭合移入 .task/archive/;任务执行期的过程证据见原任务记录与 PR。 diff --git a/.task/archive/feature-svc-auto-update.md b/.task/archive/feature-svc-auto-update.md new file mode 100644 index 00000000..7f14f633 --- /dev/null +++ b/.task/archive/feature-svc-auto-update.md @@ -0,0 +1,64 @@ +# feature/svc-auto-update 任务卡 + +任务主记录:PR #65 https://git-panel.u1680832.nyat.app:30319/weichen/Agent-System-Dev/pulls/65(merge 4ae98489fa);状态:已完成(回填);owner:weichen + +## 基线与授权 + +- 权威仓:ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/Agent-System-Dev.git;目标分支:main;R0(initial_base_sha)= ffda6d33eabb23c9be5bac221529367d771c5f7b +- 工作区:/home/weichen/projects/Agent-System-Dev-wt-feature-svc-auto-update(worktree);分支:feature/svc-auto-update +- 授权与交付终点:用户确认方案(按需 update 命令、可点名、local 源允许自动 pull)后授权实施; + 终点 = 实现合入 main 并回收工作区。仅改 Dev 仓 scripts/svc.sh / Makefile / docs/svc.md。 + +## 目标与非目标 + +目标:新增 `svc.sh update [name...] [--check] [--all] [--no-fetch]`—— +fetch 远端 → 逐服务比较登记 source 与最新(Dev 仓 origin/main / Releases 最新版本)→ +--check 只报告;否则更新过时且运行中的服务(点名/--all 含已停止)。local 源自动 pull +三重防护(分支==main + 树干净 + --ff-only);dev@ 源部署远端 main 最新(隔离 worktree); +release@ 源部署 Releases 最新(sha256 校验);postgres/static --dir 不适用。复用 deploy +全链路与持久化配置;update 事件入 history。Makefile 加 svc-update;帮助与文档同步。 + +非目标:不做常驻轮询/守护(--watch 不在本轮);不自动 pull Releases 仓以外的主检出分支; +不处理 local 源工作树的未提交变更内容(脏树=跳过)。 + +## 同步记录 + +| 节点 | sync_base_sha | tested_head_sha | checked_at | +|---|---|---|---| +| 启动 | ffda6d33eabb23c9be5bac221529367d771c5f7b | ffda6d33eabb23c9be5bac221529367d771c5f7b | 2026-09-30T15:02:15Z | + +## 关键实现与修复记录 + +- 修复 main 潜伏 bug(v2 重构引入):materialize_dev_commit 改为 $() 捕获 worktree 路径后, + git submodule update 与 make 的 stdout 混入捕获值 → 产物路径损坏(cp: File name too + long)。dev@sha 源的真实部署自 v2 起实际不可用(此前仅 dry-run 覆盖)。修复:构建期 + stdout 全部改走 stderr,stdout 只留 worktree 路径。已实测修复后 dev@73b2802 部署成功。 +- 修复本轮新代码:cmd_update 执行循环中 `local name=... rest=... from="${rest%%|*}..."` + 同语句后项引用前项,set -u 下 unbound variable。拆分两条 local 语句。 + +## 结果与验证 + +- 结果:update 命令落地(检测表 + 分源更新 + 三重 pull 防护 + 汇总退出码);Makefile + svc-update;--help(命令表/短帮助/lifecycle 主题)与 docs/svc.md(6.3 节/上手/配方/ + 事件表/排查表)同步;RESERVED_ENVS 加 update。 +- 验证(任务工作区 runtime 内,5 个测试服务全链路,已全部清理): + - dev@ 源完整更新链路:deploy --from dev@73b2802(真实隔离构建)→ update --check 报告 + 可更新 → update devold → 隔离构建 ffda6d3 组合 → source=dev@ffda6d3、HTTP 200、 + 标签 updtest 与端口 8794 保留(复用持久化 exec=)→ 构建 worktree 回收。 + - local 源防护:source 置为旧祖先提交(测试 fixture)→ 状态"可更新"→ 执行时"当前分支 + 非 main——跳过自动 pull",汇总 跳过=1、exit 0、登记未动。"领先远端 main(不动作)" + 路径由 local@ 天然覆盖(feature 分支领先 origin/main)。 + - 已停止 + 可更新:不加 --all 仅警告;--all 后更新并启动(release@0.0.9 → 0.1.0, + sha256 校验通过)。 + - postgres / static --dir:报告"不适用"。release@0.1.0:报告"✓ 最新"。 + - --check 只读性:前后 svc.env md5 一致。点名未登记服务 exit 2。--no-fetch 跳过网络 + 并在报告注明。history 记录 update begin/ok 事件。 +- 影响与剩余项:main 分支上"落后→自动 pull→重建"路径受三重防护保护、逻辑简单,留待 + 真实使用首次触发验证;--watch 常驻轮询未做(用户选择按需命令);dirty-tree 跳过路径 + 代码就位(同防护块),工作区(feature 分支)无法真实触发,与 pull 路径一并留待验证。 + +## 闭合记录(card-autoclose 回填) + +- 依据:merge 4ae98489fa567cc228f38f1a8c8bc4ad41e09de4(PR #65)已包含于 origin/main,分支成果已落盘;本段为合入后回填。 +- 合并主题:Merge pull request 'Add svc.sh update command: auto-detect repo advances and refresh services' (#65) from feature/svc-auto-update into main +- 归档:随闭合移入 .task/archive/;任务执行期的过程证据见原任务记录与 PR。 diff --git a/.task/feature-duplication-lesson-records.md b/.task/feature-duplication-lesson-records.md index f6580660..c27ec85e 100644 --- a/.task/feature-duplication-lesson-records.md +++ b/.task/feature-duplication-lesson-records.md @@ -42,3 +42,4 @@ | 同步前验收 | d54bfdd8e99d5d3d9a7f5b1db65ab90c841dd7e0 | 69871136e591b2b8416660651679554824efc823 | 2026-09-30T14:11:30Z | | 同步前验收 | 22d14db80c31e4bcfa24411f847018f527b16a01 | 6cd8cf1d0d8c7b82cabe4ecbdf82b5cfd83e39fe | 2026-09-30T14:19:33Z | | 同步前验收 | 7db242792a1561b2aad7877278d42f249369c2b9 | 314de91df3755cae006f5ced777eb71d0baa92b1 | 2026-09-30T14:29:40Z | +| 同步前验收 | ffda6d33eabb23c9be5bac221529367d771c5f7b | ac8db54ecb8ddc1e077bfec772a1665d3c2d753a | 2026-09-30T15:22:40Z | diff --git a/Makefile b/Makefile index 25b33130..1af32331 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ PLUGINS := plugins COMBO := core=$(shell git -C $(CORE) rev-parse --short HEAD 2>/dev/null)+backend=$(shell git -C $(BACKEND) rev-parse --short HEAD 2>/dev/null)+frontend=$(shell git -C $(FRONTEND) rev-parse --short HEAD 2>/dev/null)+ui=$(shell git -C $(UI) rev-parse --short HEAD 2>/dev/null)+agentcmd=$(shell git -C deps/agent-command rev-parse --short HEAD 2>/dev/null)+documents=$(shell git -C modules/documents rev-parse --short HEAD 2>/dev/null) VERSION := $(shell git describe --tags --always --dirty 2>/dev/null)+$(COMBO) -.PHONY: help sync status build frontend svc svc-list svc-commits svc-history test-backend test-frontend test-ui test-core test-agentcmd dev pin clean setup precheck task-start task-sync task-close task-finish version +.PHONY: help sync status build frontend svc svc-list svc-commits svc-history svc-update test-backend test-frontend test-ui test-core test-agentcmd dev pin clean setup precheck task-start task-sync task-close task-finish version version: @echo "agent-system $(VERSION)" @@ -49,6 +49,7 @@ help: @echo "make svc-list 服务总览:make svc-list [ARGS='--tag core'];数据库运维:scripts/svc.sh db create|dsn|backup|backups|restore|clone|copy|ls|set" @echo "make svc-commits 列出可部署提交:make svc-commits SRC=dev|release [N=20]" @echo "make svc-history 事件历史:make svc-history [ENV] [N=20](完整文档:docs/svc.md)" + @echo "make svc-update 自动更新过时服务:make svc-update [NAMES='dev pg1'] [ARGS='--check --all']" @echo "make dev 开发模式说明(前后端分离热更新)" @echo "make pin 推进 submodule 指针到各仓远端 main(提交前须 make test 通过)" @echo "make status submodule 状态" @@ -110,6 +111,9 @@ svc-commits: svc-history: scripts/svc.sh history $(ENV) $(N) +svc-update: + scripts/svc.sh update $(NAMES) $(ARGS) + demo: build $(BIN) serve --addr 127.0.0.1:8787 --data ./data --plugins $(PLUGINS) --mock diff --git a/docs/svc.md b/docs/svc.md index ed058965..5d6a9e96 100644 --- a/docs/svc.md +++ b/docs/svc.md @@ -73,6 +73,10 @@ scripts/svc.sh history # 事件追溯 scripts/svc.sh db backup pg1 # pg_dump -Fc,自动自检 scripts/svc.sh db restore pg1 --file --force # 覆盖恢复需 --force scripts/svc.sh db clone pg1 pg1test --port 8896 # 克隆一套到新端口 + +# 仓库更新后,一条命令把服务刷到最新 +scripts/svc.sh update --check # 先看哪些服务落后 +scripts/svc.sh update # 自动更新过时且运行中的服务 ``` ## 3. 概念与目录布局 @@ -80,8 +84,8 @@ scripts/svc.sh db clone pg1 pg1test --port 8896 # 克隆一套到新端口 ### 3.1 服务名 `^[a-z][a-z0-9-]{0,15}$`(小写字母开头,可含数字与连字符,≤16 位)。保留字不可用作 -服务名:`status list add set db rm create dsn commits history deploy stop start restart -logs help version`。 +服务名:`status list add set db rm create dsn commits history update deploy stop start +restart logs help version`。 三个名字有默认端口:`dev` = `127.0.0.1:8788`,`prod` = `127.0.0.1:8787`;其余名字按 名称哈希在 **8800–9099** 确定性派生端口,与已登记端口或正在监听的端口冲突时自动向后 @@ -293,7 +297,42 @@ scripts/svc.sh history [name] [N] # 默认全部服务、最近 20 条;首 合并各服务 `history.jsonl`(按时间排序),字段:时间 / 服务 / kind / 事件 / 结果 / 来源 / pid / 详情。事件类型见 §10.2。 -### 6.3 help +### 6.3 update(自动检索并更新) + +```bash +scripts/svc.sh update [name...] [--check] [--all] [--no-fetch] +``` + +仓库更新后把服务刷到最新:fetch 远端 → 逐服务比较登记来源与最新 → 报告并(可选)更新。 + +| 用法 | 行为 | +|---|---| +| `update --check` | 只输出检测表,不改任何状态 | +| `update` | 自动更新所有"过时且**运行中**"的服务 | +| `update dev pg1` | 点名更新(已停止的过时服务也会更新并启动) | +| `update --all` | 范围扩大到已停止服务 | +| `update --no-fetch` | 跳过网络 fetch,用本地已有引用比较(报告注明) | + +检测表输出:服务 / 当前来源 / 最新 / 状态。状态含义: + +- `✓ 最新`——登记来源与最新一致,不动作; +- `可更新`——本地来源是远端最新的祖先,将被更新; +- `领先远端 main(不动作)` / `与远端 main 分叉(不自动更新)`——不盲目降级或强更; +- `不适用`——postgres(无代码版本)与 static `--dir`(外部目录)不参与。 + +各来源的更新动作(复用 deploy 全链路:停旧 → 构建/校验 → 启动 → 健康检查;持久化配置 +与标签保留;写 `update` 事件入 history): + +| 来源 | 更新方式 | +|---|---| +| `local@` | 当前分支为 main 且工作树干净时:自动 `git pull --ff-only origin main` + `make sync` + 重建;**树脏或非 main 分支则跳过并警告**(绝不碰未提交现场) | +| `dev@` | `deploy --from dev@<远端 main 最新>`(隔离 worktree 构建,不碰主检出) | +| `release@<版本>` | `deploy --from release@`(sha256 校验) | + +任一服务失败不中断其余服务;最终汇总 `成功/失败/跳过`。退出码:0 全部成功或最新; +1 有失败;2 用法错误。 + +### 6.4 help ```bash scripts/svc.sh # 一屏速览(短帮助) @@ -487,6 +526,7 @@ scripts/svc.sh prod deploy --env .env.prod # 等价 | `restore` | ok / fail | 恢复(库、文件、表数) | | `clone` | ok / fail | 克隆(物理/在线) | | `copy` | ok / fail | 搬库 | +| `update` | begin / ok / fail | 自动更新(from → to) | | `rm` | ok | 注销(是否 purge) | ## 11. 安全与边界 @@ -526,6 +566,7 @@ scripts/svc.sh prod deploy --env .env.prod # 等价 | `worktree 回收失败` | 按提示手动 `git worktree remove --force runtime/.worktrees/` | | `子模块物化失败` | 子模块对象缺失:先 fetch;或网络问题重试 | | `目标库已有 N 张表——拒绝覆盖` | 确认后加 `--force`(会 drop 重建) | +| `update 报"当前分支非 main"或"工作树不干净" | local 源服务的自动 pull 被防护跳过:先自行收尾/pull,再重跑 update;或改用 `deploy --from dev@` | | `list 里服务显示未运行但进程在` | 手工起的进程不满足 pidcheck 标记:脚本不会动它;纳入管理请走 svc 部署 | ## 14. 配方索引 @@ -567,7 +608,12 @@ 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 # 只刷新指定服务 + +# ⑪ 总览与追溯 scripts/svc.sh list --tag test scripts/svc.sh history pg1 50 ``` diff --git a/modules/backend b/modules/backend index 9a545b1e..cb7f79d6 160000 --- a/modules/backend +++ b/modules/backend @@ -1 +1 @@ -Subproject commit 9a545b1e691bf26cf30fa08888d6cf31ae0ef109 +Subproject commit cb7f79d652eee7a5cb29ac3b3738672d8ff889c0 diff --git a/modules/core b/modules/core index e5c9c540..fd441794 160000 --- a/modules/core +++ b/modules/core @@ -1 +1 @@ -Subproject commit e5c9c540cd1f0735d26423b967c45f494fee9171 +Subproject commit fd441794b1e7bd9ac69b73b8722c93f599892c7a diff --git a/modules/documents b/modules/documents index 2c238af2..bf0988b2 160000 --- a/modules/documents +++ b/modules/documents @@ -1 +1 @@ -Subproject commit 2c238af24161cbda48db6b643b78384320a0d15e +Subproject commit bf0988b2b2eb61ad6a09241f91e0b0129da1300a diff --git a/modules/frontend b/modules/frontend index b2fbcf72..9c879538 160000 --- a/modules/frontend +++ b/modules/frontend @@ -1 +1 @@ -Subproject commit b2fbcf72bc872109cc1363709e393070389b637d +Subproject commit 9c879538772f64412fe588add76d7b921b5da2a4 diff --git a/modules/ui b/modules/ui index 7371f854..c7564791 160000 --- a/modules/ui +++ b/modules/ui @@ -1 +1 @@ -Subproject commit 7371f854cc9b48a48444c2cd8251c83655e0455f +Subproject commit c7564791f8f0eae8d06d13effc72cefe2528bd3e diff --git a/scripts/svc.sh b/scripts/svc.sh index f06acf9c..0c0cb566 100755 --- a/scripts/svc.sh +++ b/scripts/svc.sh @@ -19,6 +19,7 @@ # scripts/svc.sh db set [...] 等价于 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 help [主题] 分主题详细帮助 # # ════════════════════ 部署来源(agent/static 的 deploy)════════════════════ @@ -118,6 +119,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] 检测远端最新并更新过时服务 帮助:--help 完整帮助 | help [主题] 分主题(model deploy lifecycle db dbops config examples safety) 文档:docs/svc.md @@ -252,6 +254,17 @@ EOF 注销: scripts/svc.sh rm 停止并移除登记(目录/数据/历史保留) scripts/svc.sh rm --purge 连 runtime// 整个目录一起删除 + +自动更新(仓库更新后一条命令刷新服务): + 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 构建,不碰主检出) + - release@源:部署 Releases 仓最新版本(sha256 校验) + - postgres / static --dir 不参与更新;每次更新写 history(update 事件) EOF return 0 ;; db) @@ -444,6 +457,7 @@ CLI_LBASE=""; CLI_LKEY=""; CLI_LNAME=""; CLI_TAVILY="" CLI_FROM=""; CLI_PICK=""; CLI_DRYRUN=0 CLI_KIND=""; CLI_DIR=""; CLI_PURGE=0; CLI_REVEAL=0 CLI_APPLY=0; CLI_FORCE=0; CLI_ONLINE=0; CLI_FILE=""; CLI_OUT="" +CLI_CHECK=0; CLI_ALL=0; CLI_NOFETCH=0 CLI_DB_USER=""; CLI_DB_PASSWORD=""; CLI_DB_NAME=""; CLI_DB_AUTH="" CLI_TAGS=() @@ -503,7 +517,7 @@ addr_compose() { } # ---- 名称与端口 ---- -RESERVED_ENVS=" status list add set db rm create dsn commits history deploy stop start restart logs help version " +RESERVED_ENVS=" status list add set db rm create dsn commits history update deploy stop start restart logs help version " # valid_env NAME:服务名合法性(小写字母开头、a-z0-9-、≤16 位;保留字不可用)。 valid_env() { @@ -932,12 +946,13 @@ materialize_dev_commit() { git -C "$ROOT" worktree add --detach "$wt" "$full" >/dev/null || return 1 fi c_info "[$ENV_NAME] 物化子模块组合(git submodule update --init --recursive;缺对象时需网络)" - git -C "$wt" submodule update --init --recursive || { + # stdout 只留给最后的 worktree 路径(调用方 $() 捕获)——子模块/构建输出全部走 stderr。 + git -C "$wt" submodule update --init --recursive >&2 || { c_err "[$ENV_NAME] 子模块物化失败(对象缺失时先 git -C $ROOT fetch origin)" return 1 } c_info "[$ENV_NAME] 构建(make $target:npm ci + 构建,需网络)" - make -C "$wt" "$target" || return 1 + make -C "$wt" "$target" >&2 || return 1 echo "$wt" } @@ -2029,6 +2044,202 @@ cmd_history() { done } +# ---- 自动检索与更新 ---- + +# cmd_update [NAME...] [--check] [--all] [--no-fetch]: +# fetch 远端 → 逐服务比较登记 source 与最新(Dev 仓 origin/main / Releases 最新版本)→ +# --check 只报告;否则更新"过时且运行中"的服务(点名或 --all 时含已停止)。 +# local 源的自动 pull 受三重保护:当前分支==main + 工作树干净 + --ff-only。 +cmd_update() { + local names=() a + while [ $# -gt 0 ]; do + case "$1" in + --check) CLI_CHECK=1; shift ;; + --all) CLI_ALL=1; shift ;; + --no-fetch) CLI_NOFETCH=1; shift ;; + --) shift; break ;; + -h|--help) usage; exit 0 ;; + -*) c_err "update 未知参数:$1(可用:--check --all --no-fetch)"; return 2 ;; + *) names+=("$1"); shift ;; + esac + done + local targets=() d e + if [ "${#names[@]}" -gt 0 ]; then + for a in ${names[@]+"${names[@]}"}; do + valid_env "$a" || { c_err "非法服务名:$a"; return 2; } + [ -f "$RUNTIME/$a/svc.env" ] || { c_err "[$a] 未登记(scripts/svc.sh list 查看)"; return 2; } + targets+=("$a") + done + else + for d in "$RUNTIME"/*/; do + [ -f "${d}svc.env" ] || continue + e="$(basename "$d")" + [ "$e" = ".worktrees" ] && continue + valid_env "$e" || continue + targets+=("$e") + done + 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 + 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 + done + if [ "$need_rel" = 1 ]; then + releases_root + if [ "$CLI_NOFETCH" != 1 ]; then + git -C "$REL_ROOT" fetch --quiet 2>/dev/null || c_warn "Releases 仓 fetch 失败——基于本地缓存" + fi + local rel_head + rel_head="$(git -C "$REL_ROOT" rev-parse --verify --quiet HEAD || true)" + if [ -n "$rel_head" ]; then + latest_rel="$(git -C "$REL_ROOT" ls-tree --name-only "$rel_head:versions" 2>/dev/null | sort -V | tail -1 || true)" + fi + fi + + # 逐服务分析:状态判定 + 收集可更新动作。 + c_info "更新检测($fetch_note;root: $ROOT)" + printf ' %-12s %-20s %-12s %s\n' 服务 当前来源 最新 状态 + local acts=() state to latest_disp="" updated=0 failed=0 skipped=0 + for e in ${targets[@]+"${targets[@]}"}; do + local kind src cur st_running + kind="$(svc_kind "$e")" + src="$(sed -n 's/^source=//p' "$RUNTIME/$e/svc.env" | head -1)" + to="" + latest_disp="" + case "$kind" in + postgres) + state="不适用(数据库)" ;; + *) + case "$src" in + dir@*) + state="不适用(外部目录)" ;; + "") + state="来源未知(重新 deploy 后可参与更新)" ;; + local@*|dev@*) + cur="${src##*@}" + if [ -z "$latest_sha" ]; then + state="远端 main 未知"; latest_disp="-" + 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)" + 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 "-")" + else + state="与远端 main 分叉(不自动更新)"; latest_disp="-" + fi + ;; + release@*) + cur="${src#release@}" + latest_disp="${latest_rel:--}" + if [ -z "$latest_rel" ]; then + state="Releases 仓不可用" + elif [ "$cur" = "$latest_rel" ]; then + state="✓ 最新" + else + local ordered + ordered="$(printf '%s\n%s\n' "$cur" "$latest_rel" | sort -V | tail -1)" + if [ "$ordered" = "$latest_rel" ]; then + state="可更新"; to="$latest_rel" + else + state="领先 Releases(不动作)" + fi + fi + ;; + *) + state="来源未知(重新 deploy 后可参与更新)" ;; + esac + ;; + esac + printf ' %-12s %-20s %-12s %s\n' "$e" "${src:--}" "$latest_disp" "$state" + if [ "$state" = "可更新" ]; then + st_running="$(svc_pid_state "$e")" + if [ "${st_running%% *}" = alive ] || [ "$CLI_ALL" = 1 ] || [ "${#names[@]}" -gt 0 ]; then + acts+=("$e|$src|$to") + else + c_warn "[$e] 已停止且可更新——加 --all 或点名以更新(更新会启动服务)" + fi + fi + done + + if [ "$CLI_CHECK" = 1 ]; then + c_info "--check:仅报告,未做任何变更" + return 0 + fi + [ ${#acts[@]} -gt 0 ] || { c_ok "无需更新(全部最新或不适用)"; return 0; } + + # 执行更新:逐服务走 deploy 全链路(复用持久化配置与标签)。 + for a in ${acts[@]+"${acts[@]}"}; do + local name="${a%%|*}" rest="${a#*|}" + local from="${rest%%|*}" tov="${rest#*|}" + local deploy_from="" + env_config "$name" + 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" ;; + esac + c_info "[$name] 更新:$from → $tov($deploy_from)" + hist_event update begin "$(read_pid)" "from=$from to=$tov" + CLI_FROM="$deploy_from" + if agent_do_deploy; then + hist_event update ok "$(read_pid)" "from=$from to=$tov" + c_ok "[$name] 已更新:$from → $tov" + updated=$((updated + 1)) + else + hist_event update fail "$(read_pid)" "from=$from to=$tov(部署失败)" + c_err "[$name] 更新失败($from → $tov),其余服务继续" + failed=$((failed + 1)) + fi + CLI_FROM="" + done + c_info "更新汇总:成功 $updated / 失败 $failed / 跳过 $skipped / 其余最新或不适用" + [ "$failed" -eq 0 ] && return 0 || return 1 +} + # ---- main ---- main() { @@ -2069,6 +2280,7 @@ main() { rm) shift; cmd_rm "$@"; exit 0 ;; commits) shift; cmd_commits "$@"; exit 0 ;; history) shift; cmd_history "$@"; exit 0 ;; + update) shift; cmd_update "$@" || exit $?; exit 0 ;; esac ENV_NAME="$1"; shift # 动作词可省略(省略 = deploy);flags 可直接跟随服务名。