Skip to content

fix(cli): 把本地存储根写成设置服务读的那个 env 名 —— OS_STORAGE_ROOTOS_STORAGE_LOCAL_ROOT(方案 B) - #5601

Merged
baozhoutao merged 2 commits into
mainfrom
claude/issue-4968-storage-root-env-channel
Aug 5, 2026
Merged

fix(cli): 把本地存储根写成设置服务读的那个 env 名 —— OS_STORAGE_ROOTOS_STORAGE_LOCAL_ROOT(方案 B)#5601
baozhoutao merged 2 commits into
mainfrom
claude/issue-4968-storage-root-env-channel

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Fixes #4968

按 16:41Z 维护者裁决的方案 B 落地:CLI 改写设置服务自己的 env key OS_STORAGE_LOCAL_ROOT,旧名 OS_STORAGE_ROOTreadEnvWithDeprecation 形态保留一个 release(改名弃用,非长期双读),读到旧名时打弃用 warn。

不动 packages/services/service-storage/**(hasAny / applySettings 消费缝归 #5536)、不动 packages/services/service-settings/** 语义、不动 releases 目录、不动 packages/runtime/**packages/rest/**

前提复核:成立(先证后改)

修改前的最新 origin/main 上重跑复现,16:15Z 核对评论的证据链逐条成立:

⚠ Boot diagnostics — 1 warning logged during startup:
  2026-08-05T17:10:25.953Z WARN StorageServicePlugin: storage adapter swapped
  (LocalStorageAdapter → LocalStorageAdapter). Existing files were NOT migrated
  and may be unreachable through the new adapter.

同一次 --fresh 启动(构造侧 🧪 Fresh OS_HOME: /tmp/objectstack-dev-L6v8I6),GET /api/settings/storage:

"local_root": {
  "value": "./.objectstack/data/uploads",
  "source": "default",
  "locked": false,
  "cascadeChain": [ { "scope": "default", "value": "./.objectstack/data/uploads", "effective": true } ]
}

并且补了一条核对评论没有的直接证据 —— 真实上传一个文件,看字节落在哪:

=== bytes landed where? ===
/home/user/objectstack-4968/examples/app-showcase/.objectstack/data/uploads/user/8a55af07-….txt
--- tempdir ---
(空)

--fresh 的隔离承诺确实被破坏:字节落在项目 cwd,不在 tempdir,且进程退出后留在工作树里。

根因与修法

设置服务从它自己拥有的命名空间派生 env 名 —— envKeyOf('storage','local_root') = OS_STORAGE_LOCAL_ROOT(settings-service.types.ts:215),而 CLI 自造了 OS_STORAGE_ROOT。两条通道从未相遇:os serve 按运维给的根构造 adapter,StorageServicePluginkernel:ready 从 settings 重新解析,只看到 manifest 的 schema 默认值,就把 adapter 换掉。

所以 OS_STORAGE_ROOT 只对恰好等于该默认值的那一个值生效 —— 这正是普通 pnpm dev 从没暴露它的原因。

修在生产者侧,不在消费者侧加容忍读:

  • dev.ts 发布 OS_STORAGE_LOCAL_ROOT;
  • serve.ts 新增单一通道 resolveStorageLocalRootEnv(),与 os migrate 的 storage 引导(data-migration-plugins.ts)共用,使 CLI 落字节的位置与 server 完全一致。

⚠ 一处裁决未点名、但方案 B 验收项依赖的细节:旧名必须回写

裁决文本只说"以 readEnvWithDeprecation('OS_STORAGE_LOCAL_ROOT','OS_STORAGE_ROOT') 形态保留"。但光"读到"不够:设置服务只查 OS_STORAGE_LOCAL_ROOT,所以旧名供值时必须同时把值 stamp 到新名上,否则旧名部署会 100% 原样保留本单要修的缺陷(adapter 建在 /srv/uploads,settings 仍停在 schema 默认值,kernel:ready 照swap)。

export function resolveStorageLocalRootEnv(): string | undefined {
  const value = readEnvWithDeprecation('OS_STORAGE_LOCAL_ROOT', 'OS_STORAGE_ROOT');
  if (value === undefined) return undefined;
  if (typeof process !== 'undefined' && process.env
    && process.env.OS_STORAGE_LOCAL_ROOT === undefined) {
    process.env.OS_STORAGE_LOCAL_ROOT = value;
  }
  return value;
}

这一步是验收项"旧名走弃用通道 → source:'env'、不 swap"能成立的前提,不是额外发挥;方向仍是方案 B(契约优先、单一拼写),没有引入第二条语义。设置服务的 this.env 持有 process.env活引用且按需读(settings-service.ts:127 / :507),所以 stamp 与插件构造顺序无关。

验收:三种启动形态真机核(维护者点名项)

形态 settings local_root source swap warn 弃用 warn
plain dev(不设 env) ./.objectstack/data/uploads default
--fresh /tmp/objectstack-dev-jrW4lp/uploads env
生产新名 OS_STORAGE_LOCAL_ROOT=/srv/uploads /srv/uploads env
生产旧名 OS_STORAGE_ROOT=/srv/uploads /srv/uploads env 有,1 条

--fresh 修后:

value  = /tmp/objectstack-dev-jrW4lp/uploads
source = env
locked = true  Set via env: OS_STORAGE_LOCAL_ROOT

同样的上传探针,字节回到 tempdir、项目 cwd 干净:

=== project cwd (was WRONG before) ===
(空)
=== tempdir (correct) ===
/tmp/objectstack-dev-jrW4lp/uploads/user/a76ff778-….txt

旧名形态的弃用 warn(原文):

[ObjectStack] Env var `OS_STORAGE_ROOT` is deprecated; rename it to `OS_STORAGE_LOCAL_ROOT`.
The legacy name still works for now but will be removed in a future major release.

showcase 启动诊断从 1 条 warning 变为 0 条 —— 那条 swap 警告本身是准确的(#4096 已把谓词改对),本 PR 一个字没动它;它不再响是因为 swap 不再发生。

反向验证(方向先判后跑)

判断 1 —— 删掉 stamp(只保留裁决文本字面写的弃用读): 预判 2 红 3 绿。跑批与预判一致:

✓ reads the canonical name the settings service derives, quietly
× still reads the legacy name AND stamps it onto the canonical one
  → expected undefined to be '/srv/legacy-uploads'
× feeds the capability arg from the legacy name end to end
  → expected undefined to be '/srv/legacy-uploads'
✓ lets the canonical name win when both are set, without warning
✓ sets nothing when neither name is set, so the default still applies
Tests  2 failed | 9 passed (11)

这正是 stamp 是承重件、不是装饰的证据:只做弃用读会留 3/5 绿。

判断 2 —— 把 dev.ts 改回写 OS_STORAGE_ROOT: 预判单测保持绿,因为 serve 子进程仍会经弃用通道桥接,缺陷不会复现,变化只是每次 --fresh 多响一条运维根本没设过的弃用 warn。核实结论:全仓没有任何测试触及 dev.tslocalEnv / freshStorageRoot(grep 仅命中本 PR 的测试文件),该处只有真机 --fresh 形态能钉住,单测钉不住 —— 如实报告,不假装有覆盖。

测试

新增 5 例(serve-storage-capability.test.ts),覆盖裁决点名的四种组合 + 端到端:新名生效且静默、旧名生效 + 弃用 warn + 回写、旧名端到端喂到 capability arg、两名同设新名赢且不 warn、都不设时不 stamp 且回落默认。

顺手改了一个名不副实的既有用例名:honours OS_STORAGE_ROOT… 其实从不读 env(它传参),名字挂着一个它不碰的变量,正是 env 通道长期没被审视的缝 —— 改为 honours an explicit root…

pnpm --workspace-concurrency=2 --filter @objectstack/cli typecheck   → 通过(无输出)
pnpm --workspace-concurrency=2 --filter @objectstack/cli test        → Test Files 82 passed (82) · Tests 812 passed (812)
node scripts/check-nul-bytes.mjs                                     → OK(5524 files,no raw control bytes)

已 merge 当前 origin/main(81087877e)后重跑,同样全绿。

消费半径巡检

resolveStorageLocalRootEnv 的调用方:serve.ts 能力槽位、data-migration-plugins.ts(os migrate files-to-references)—— 两处都已切到同一通道。全仓 grep OS_STORAGE_ROOT,剩余命中只有两处刻意不动的:

文档

  • environment-variables.mdx:OS_STORAGE_LOCAL_ROOT 为正名并说明它就是 Setup → Settings → File Storage → Root directory 的同一个值;旧名单列一行标注弃用,并写清"改名前任何非默认值都被静默丢弃"这一交接事实。
  • backup-restore.mdx:改用新名,并加一条 warn Callout —— 提醒运维核对备份目录里是否真有文件,旧版本上按配置路径备份会拷到空目录;给出用 Setup 页面对账的办法。

界外发现

查重(含 closed)后立单 #5594(finding,未进 pm:queue,unassigned):dev --fresh 注释声称 tempdir "owns ALL persistent state",但 app 自声明的 cwd 相对 datasource 路径(showcase 的 showcase_external.db)仍写进项目树并在退出后留存。本 PR 修好了其中的 storage 那一半,余下的是声明口径问题,属观察类,交 PM 分诊。


Generated by Claude Code

claude added 2 commits August 5, 2026 19:53
CLI 与设置服务对同一个值用了两个拼写。CLI 自造了 `OS_STORAGE_ROOT`;
设置服务从它自己拥有的命名空间派生 env 名 ——
`envKeyOf('storage','local_root')` = `OS_STORAGE_LOCAL_ROOT` —— 而全仓
没有任何地方设置过它。于是两条通道从未相遇:`os serve` 按运维给的根构造
了本地 adapter,`StorageServicePlugin` 在 `kernel:ready` 从 settings 重新
解析,只看到 manifest 的 schema 默认值,就把 adapter 换成了
`./.objectstack/data/uploads`。

所以 `OS_STORAGE_ROOT` 只对一个值生效 —— 恰好等于该默认值的那个,这正是
普通 `pnpm dev` 从没暴露它的原因。其余任何值都是构造完就被丢弃:生产
`/srv/uploads` 被忽略、运维按 backup-restore.mdx 备份到空目录;`dev --fresh`
承诺 tempdir 独占本次运行的全部状态,上传实际落在项目 cwd 且退出后不清理;
每次干净启动都响一条数据丢失级 swap 警告 —— 那条警告是**准确的**,swap
真的发生了,本 commit 不动它,它随 swap 消失而不再响。

修在生产者侧,不在消费者侧加容忍读:`dev.ts` 发布 `OS_STORAGE_LOCAL_ROOT`,
`serve.ts` 经单一通道 `resolveStorageLocalRootEnv` 解析根,并与 `os migrate`
的 storage 引导共用,使 CLI 落字节的位置与 server 完全一致。

`OS_STORAGE_ROOT` 经 `readEnvWithDeprecation('OS_STORAGE_LOCAL_ROOT',
'OS_STORAGE_ROOT')` 保留一个 release,每进程 warn 一次,随后移除。旧名供值
时同时回写到新名 —— 设置服务只查 `OS_STORAGE_LOCAL_ROOT`,没有这一步,旧名
部署会原样保留本单要修的缺陷。

不动 `packages/services/service-storage`:swap 谓词是对的(#4096 已修正),
消费缝归 #5536。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016FNvXhtSdnEGEfLEsMmvxh
@vercel

vercel Bot commented Aug 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 5, 2026 8:17pm

Request Review

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Aug 5, 2026
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli.

21 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/skills-reference.mdx (via packages/cli)
  • content/docs/api/client-sdk.mdx (via @objectstack/cli)
  • content/docs/api/data-flow.mdx (via @objectstack/cli)
  • content/docs/api/environment-routing.mdx (via @objectstack/cli)
  • content/docs/api/error-catalog.mdx (via @objectstack/cli)
  • content/docs/automation/hook-bodies.mdx (via packages/cli)
  • content/docs/deployment/backup-restore.mdx (via @objectstack/cli)
  • content/docs/deployment/cli.mdx (via @objectstack/cli)
  • content/docs/deployment/self-hosting.mdx (via @objectstack/cli)
  • content/docs/deployment/validating-metadata.mdx (via packages/cli)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/cli)
  • content/docs/kernel/runtime-services/data-service.mdx (via packages/cli)
  • content/docs/kernel/runtime-services/index.mdx (via packages/cli)
  • content/docs/permissions/authentication.mdx (via @objectstack/cli)
  • content/docs/plugins/index.mdx (via @objectstack/cli)
  • content/docs/plugins/packages.mdx (via @objectstack/cli)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/cli)
  • content/docs/protocol/kernel/realtime-protocol.mdx (via @objectstack/cli)
  • content/docs/releases/implementation-status.mdx (via @objectstack/cli)
  • content/docs/releases/v16.mdx (via @objectstack/cli)
  • content/docs/releases/v17.mdx (via @objectstack/cli)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

Copy link
Copy Markdown
Contributor Author

CI 红说明(PM,cli 车道):ESLint job 的失败项是 check:engine-double-contract,抱怨的文件 packages/runtime/src/action-execution-calldata-not-found.test.ts 不在本 PR 的 diff 里——它随 #5584 已合入 main,经 merge-ref 混进本 PR 的检查。即 base 侧的红,与本 PR 的改动无关。

解堵热修已派发(#5138 的原 dev,按门禁自身处方把两处 engine double 的 delete() 接上 assertEngineDeleteDispatch);热修合入后本 PR 会 update-branch 重跑 ESLint。auto-merge 保持挂载,其余 checks 照常。


Generated by Claude Code

@baozhoutao
baozhoutao added this pull request to the merge queue Aug 5, 2026
Merged via the queue into main with commit a287d1c Aug 5, 2026
24 of 25 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-4968-storage-root-env-channel branch August 5, 2026 20:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants