create-bubbles 0.1.27 → 0.1.28

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.
Files changed (105) hide show
  1. package/dist/index.js +4 -4
  2. package/package.json +10 -2
  3. package/template-pnpm-vp-monorepo/.changeset/README.md +8 -0
  4. package/template-pnpm-vp-monorepo/.changeset/config.json +11 -0
  5. package/template-pnpm-vp-monorepo/.gitattributes +5 -0
  6. package/template-pnpm-vp-monorepo/.github/workflows/ci.yml +33 -0
  7. package/template-pnpm-vp-monorepo/.github/workflows/docs.yml +55 -0
  8. package/template-pnpm-vp-monorepo/.github/workflows/release.yml +54 -0
  9. package/template-pnpm-vp-monorepo/.vite-hooks/commit-msg +1 -0
  10. package/template-pnpm-vp-monorepo/.vite-hooks/pre-commit +1 -0
  11. package/template-pnpm-vp-monorepo/.vite-hooks/run.mjs +17 -0
  12. package/template-pnpm-vp-monorepo/.vscode/settings.json +23 -0
  13. package/{LICENSE → template-pnpm-vp-monorepo/LICENSE} +2 -2
  14. package/template-pnpm-vp-monorepo/README.md +93 -0
  15. package/template-pnpm-vp-monorepo/_gitignore +43 -0
  16. package/template-pnpm-vp-monorepo/apps/docs/.vitepress/config.mts +69 -0
  17. package/template-pnpm-vp-monorepo/apps/docs/guide/getting-started.md +70 -0
  18. package/template-pnpm-vp-monorepo/apps/docs/guide/release.md +73 -0
  19. package/template-pnpm-vp-monorepo/apps/docs/guide/workspace.md +84 -0
  20. package/template-pnpm-vp-monorepo/apps/docs/index.md +22 -0
  21. package/template-pnpm-vp-monorepo/apps/docs/package.json +23 -0
  22. package/template-pnpm-vp-monorepo/apps/docs/tsconfig.json +7 -0
  23. package/template-pnpm-vp-monorepo/apps/docs/vite.config.ts +12 -0
  24. package/template-pnpm-vp-monorepo/apps/playground/index.html +14 -0
  25. package/template-pnpm-vp-monorepo/apps/playground/package.json +24 -0
  26. package/template-pnpm-vp-monorepo/apps/playground/src/App.vue +214 -0
  27. package/template-pnpm-vp-monorepo/apps/playground/src/env.d.ts +6 -0
  28. package/template-pnpm-vp-monorepo/apps/playground/src/main.ts +6 -0
  29. package/template-pnpm-vp-monorepo/apps/playground/src/style.css +728 -0
  30. package/template-pnpm-vp-monorepo/apps/playground/tsconfig.json +10 -0
  31. package/template-pnpm-vp-monorepo/apps/playground/vite.config.ts +29 -0
  32. package/template-pnpm-vp-monorepo/commitlint.config.mjs +3 -0
  33. package/template-pnpm-vp-monorepo/eslint.config.mjs +22 -0
  34. package/template-pnpm-vp-monorepo/mise.toml +3 -0
  35. package/template-pnpm-vp-monorepo/package.json +40 -0
  36. package/template-pnpm-vp-monorepo/packages/utils/LICENSE +21 -0
  37. package/template-pnpm-vp-monorepo/packages/utils/README.md +26 -0
  38. package/template-pnpm-vp-monorepo/packages/utils/_gitignore +4 -0
  39. package/template-pnpm-vp-monorepo/packages/utils/package.json +42 -0
  40. package/template-pnpm-vp-monorepo/packages/utils/scripts/verify-package.mjs +127 -0
  41. package/template-pnpm-vp-monorepo/packages/utils/src/index.ts +12 -0
  42. package/template-pnpm-vp-monorepo/packages/utils/tests/consumer/commonjs.cts +20 -0
  43. package/template-pnpm-vp-monorepo/packages/utils/tests/consumer/esm.mts +20 -0
  44. package/template-pnpm-vp-monorepo/packages/utils/tests/consumer/tsconfig.json +10 -0
  45. package/template-pnpm-vp-monorepo/packages/utils/tests/index.test.ts +30 -0
  46. package/template-pnpm-vp-monorepo/packages/utils/tsconfig.json +4 -0
  47. package/template-pnpm-vp-monorepo/packages/utils/vite.config.ts +20 -0
  48. package/template-pnpm-vp-monorepo/pnpm-workspace.yaml +27 -0
  49. package/template-pnpm-vp-monorepo/tsconfig/base.json +22 -0
  50. package/template-pnpm-vp-monorepo/tsconfig/node.json +8 -0
  51. package/template-pnpm-vp-monorepo/tsconfig/vue.json +7 -0
  52. package/template-pnpm-vp-monorepo/tsconfig.json +4 -0
  53. package/template-pnpm-vp-monorepo/vite.config.ts +38 -0
  54. package/template-vp-monorepo-react-nestjs/.spaces/01./344/274/201/344/270/232/347/272/247/351/241/271/347/233/256/347/272/247//346/235/203/351/231/220/344/270/216/350/217/234/345/215/225//346/212/200/346/234/257/345/245/221/347/272/246.md +5 -3
  55. package/template-vp-monorepo-react-nestjs/.spaces/04./347/225/214/351/235/242/344/270/216/344/270/273/351/242/230//345/233/275/351/231/205/345/214/226//344/272/244/344/273/230/350/256/260/345/275/225.md +9 -0
  56. package/template-vp-monorepo-react-nestjs/.spaces/04./347/225/214/351/235/242/344/270/216/344/270/273/351/242/230//345/233/275/351/231/205/345/214/226//345/217/221/345/270/203/350/257/204/344/274/260.md +93 -0
  57. package/template-vp-monorepo-react-nestjs/.spaces/04./347/225/214/351/235/242/344/270/216/344/270/273/351/242/230//345/233/275/351/231/205/345/214/226//345/267/245/345/205/267//350/277/220/350/241/214/346/227/266/345/217/221/345/270/203/346/243/200/346/237/245.mjs +340 -0
  58. package/template-vp-monorepo-react-nestjs/.spaces/04./347/225/214/351/235/242/344/270/216/344/270/273/351/242/230//345/233/275/351/231/205/345/214/226//346/265/213/350/257/225/346/212/245/345/221/212.md +28 -6
  59. package/template-vp-monorepo-react-nestjs/.spaces/04./347/225/214/351/235/242/344/270/216/344/270/273/351/242/230//345/233/275/351/231/205/345/214/226//346/265/213/350/257/225/350/257/201/346/215/256/cli-/346/211/253/346/217/217/350/276/271/347/225/214/comment-clean/catalog/en_US.json +4 -0
  60. package/template-vp-monorepo-react-nestjs/.spaces/04./347/225/214/351/235/242/344/270/216/344/270/273/351/242/230//345/233/275/351/231/205/345/214/226//346/265/213/350/257/225/350/257/201/346/215/256/cli-/346/211/253/346/217/217/350/276/271/347/225/214/comment-clean/i18n.config.mjs +9 -0
  61. package/template-vp-monorepo-react-nestjs/.spaces/04./347/225/214/351/235/242/344/270/216/344/270/273/351/242/230//345/233/275/351/231/205/345/214/226//346/265/213/350/257/225/350/257/201/346/215/256/cli-/346/211/253/346/217/217/350/276/271/347/225/214/comment-clean/report.json +16 -0
  62. package/template-vp-monorepo-react-nestjs/.spaces/04./347/225/214/351/235/242/344/270/216/344/270/273/351/242/230//345/233/275/351/231/205/345/214/226//346/265/213/350/257/225/350/257/201/346/215/256/cli-/346/211/253/346/217/217/350/276/271/347/225/214/comment-clean/source/app.ts +2 -0
  63. package/template-vp-monorepo-react-nestjs/.spaces/04./347/225/214/351/235/242/344/270/216/344/270/273/351/242/230//345/233/275/351/231/205/345/214/226//346/265/213/350/257/225/350/257/201/346/215/256/cli-/346/211/253/346/217/217/350/276/271/347/225/214/empty-scan/catalog/en_US.json +4 -0
  64. package/template-vp-monorepo-react-nestjs/.spaces/04./347/225/214/351/235/242/344/270/216/344/270/273/351/242/230//345/233/275/351/231/205/345/214/226//346/265/213/350/257/225/350/257/201/346/215/256/cli-/346/211/253/346/217/217/350/276/271/347/225/214/empty-scan/i18n.config.mjs +9 -0
  65. package/template-vp-monorepo-react-nestjs/.spaces/04./347/225/214/351/235/242/344/270/216/344/270/273/351/242/230//345/233/275/351/231/205/345/214/226//346/265/213/350/257/225/350/257/201/346/215/256/cli-/346/211/253/346/217/217/350/276/271/347/225/214/empty-scan/report.json +19 -0
  66. package/template-vp-monorepo-react-nestjs/.spaces/04./347/225/214/351/235/242/344/270/216/344/270/273/351/242/230//345/233/275/351/231/205/345/214/226//346/265/213/350/257/225/350/257/201/346/215/256/cli-/346/211/253/346/217/217/350/276/271/347/225/214/output.json +84 -0
  67. package/template-vp-monorepo-react-nestjs/.spaces/04./347/225/214/351/235/242/344/270/216/344/270/273/351/242/230//345/233/275/351/231/205/345/214/226//346/265/213/350/257/225/350/257/201/346/215/256/cli-/346/211/253/346/217/217/350/276/271/347/225/214//345/275/223/345/211/215/345/244/215/351/252/214.json +86 -0
  68. package/template-vp-monorepo-react-nestjs/.spaces/04./347/225/214/351/235/242/344/270/216/344/270/273/351/242/230//345/233/275/351/231/205/345/214/226//346/265/213/350/257/225/350/257/201/346/215/256/cli-/346/211/253/346/217/217/350/276/271/347/225/214//346/243/200/346/237/245/347/273/223/346/236/234.json +30 -0
  69. package/template-vp-monorepo-react-nestjs/.spaces/04./347/225/214/351/235/242/344/270/216/344/270/273/351/242/230//345/233/275/351/231/205/345/214/226//346/265/213/350/257/225/350/257/201/346/215/256/cli-/346/211/253/346/217/217/350/276/271/347/225/214//346/263/250/351/207/212/345/233/236/345/275/222.json +40 -0
  70. package/template-vp-monorepo-react-nestjs/.spaces/04./347/225/214/351/235/242/344/270/216/344/270/273/351/242/230//345/233/275/351/231/205/345/214/226//346/265/213/350/257/225/350/257/201/346/215/256//345/214/205/345/217/221/345/270/203/bubblesjs-i18n-core-0.0.0.tgz +0 -0
  71. package/template-vp-monorepo-react-nestjs/.spaces/04./347/225/214/351/235/242/344/270/216/344/270/273/351/242/230//345/233/275/351/231/205/345/214/226//346/265/213/350/257/225/350/257/201/346/215/256//345/214/205/345/217/221/345/270/203/bubblesjs-i18n-react-0.0.0.tgz +0 -0
  72. package/template-vp-monorepo-react-nestjs/.spaces/04./347/225/214/351/235/242/344/270/216/344/270/273/351/242/230//345/233/275/351/231/205/345/214/226//346/265/213/350/257/225/350/257/201/346/215/256//345/214/205/345/217/221/345/270/203/bubblesjs-i18n-vue-0.0.1.tgz +0 -0
  73. package/template-vp-monorepo-react-nestjs/.spaces/04./347/225/214/351/235/242/344/270/216/344/270/273/351/242/230//345/233/275/351/231/205/345/214/226//346/265/213/350/257/225/350/257/201/346/215/256//345/214/205/345/217/221/345/270/203//344/272/247/347/211/251/345/244/215/351/252/214.json +419 -0
  74. package/template-vp-monorepo-react-nestjs/.spaces/04./347/225/214/351/235/242/344/270/216/344/270/273/351/242/230//345/233/275/351/231/205/345/214/226//346/265/213/350/257/225/350/257/201/346/215/256//345/214/205/345/217/221/345/270/203//346/243/200/346/237/245/347/273/223/346/236/234.json +339 -0
  75. package/template-vp-monorepo-react-nestjs/.spaces/04./347/225/214/351/235/242/344/270/216/344/270/273/351/242/230//345/233/275/351/231/205/345/214/226//346/265/213/350/257/225/350/257/201/346/215/256//350/277/220/350/241/214/346/227/266/345/217/221/345/270/203/346/243/200/346/237/245.json +131 -0
  76. package/template-vp-monorepo-react-nestjs/.spaces/04./347/225/214/351/235/242/344/270/216/344/270/273/351/242/230//345/233/275/351/231/205/345/214/226//350/256/276/350/256/241/346/226/271/346/241/210.md +19 -0
  77. package/template-vp-monorepo-react-nestjs/.spaces/04./347/225/214/351/235/242/344/270/216/344/270/273/351/242/230//345/233/275/351/231/205/345/214/226//351/234/200/346/261/202/346/226/207/346/241/243.md +21 -3
  78. package/template-vp-monorepo-react-nestjs/.spaces/06./350/256/244/350/257/201/344/270/216/344/274/232/350/257/235//344/274/232/350/257/235/347/256/241/347/220/206//346/212/200/346/234/257/345/245/221/347/272/246.md +5 -2
  79. package/template-vp-monorepo-react-nestjs/.spaces/06./350/256/244/350/257/201/344/270/216/344/274/232/350/257/235//344/274/232/350/257/235/347/256/241/347/220/206//351/234/200/346/261/202/346/226/207/346/241/243.md +4 -3
  80. package/template-vp-monorepo-react-nestjs/.spaces/06./350/256/244/350/257/201/344/270/216/344/274/232/350/257/235//344/277/256/346/224/271/345/257/206/347/240/201/PRD.md +2 -1
  81. package/template-vp-monorepo-react-nestjs/.spaces/06./350/256/244/350/257/201/344/270/216/344/274/232/350/257/235//344/277/256/346/224/271/345/257/206/347/240/201//346/212/200/346/234/257/345/245/221/347/272/246.md +3 -2
  82. package/template-vp-monorepo-react-nestjs/.spaces/06./350/256/244/350/257/201/344/270/216/344/274/232/350/257/235//344/277/256/346/224/271/345/257/206/347/240/201//346/265/213/350/257/225/346/212/245/345/221/212.md +15 -12
  83. package/template-vp-monorepo-react-nestjs/.spaces/06./350/256/244/350/257/201/344/270/216/344/274/232/350/257/235//344/277/256/346/224/271/345/257/206/347/240/201//350/201/224/350/260/203/350/256/260/345/275/225.md +6 -5
  84. package/template-vp-monorepo-react-nestjs/.spaces/06./350/256/244/350/257/201/344/270/216/344/274/232/350/257/235//344/277/256/346/224/271/345/257/206/347/240/201//350/256/276/350/256/241/346/226/271/346/241/210.md +2 -1
  85. package/template-vp-monorepo-react-nestjs/.spaces/06./350/256/244/350/257/201/344/270/216/344/274/232/350/257/235//344/277/256/346/224/271/345/257/206/347/240/201//351/234/200/346/261/202/346/226/207/346/241/243.md +4 -3
  86. package/template-vp-monorepo-react-nestjs/.spaces/README.md +3 -2
  87. package/template-vp-monorepo-react-nestjs/apps/web/test/{password-actions.spec.ts → password/password-actions.spec.ts} +3 -3
  88. package/template-vp-monorepo-react-nestjs/apps/web/test/{password-reset-action.spec.ts → password/password-reset-action.spec.ts} +4 -2
  89. package/template-vp-monorepo-react-nestjs/apps/web/test/{workspaces-password-entry.spec.tsx → password/workspaces-password-entry.spec.tsx} +7 -7
  90. package/template-vp-monorepo-react-nestjs/packages/i18n/cli/README.md +2 -0
  91. package/template-vp-monorepo-react-nestjs/packages/i18n/cli/src/scanner.test.ts +88 -0
  92. package/template-vp-monorepo-react-nestjs/packages/i18n/cli/src/scanner.ts +34 -11
  93. package/template-vp-monorepo-react-nestjs/packages/i18n/cli/test/command.test.ts +36 -0
  94. package/template-vp-monorepo-react-nestjs/packages/i18n/react/src/use-store.ts +15 -5
  95. package/template-vp-monorepo-react-nestjs/tsconfig.lib.json +1 -1
  96. package/template-vp-react/package.json +5 -5
  97. package/template-vp-vue-eslint-vapor/.vscode/settings.json +53 -0
  98. package/template-vp-vue-eslint-vapor/package.json +1 -0
  99. package/template-vp-vue-eslint-vapor/pnpm-workspace.yaml +0 -24
  100. package/template-vp-vue-eslint-vapor/src/main.ts +2 -3
  101. package/template-vp-vue-eslint-vapor/vite.config.ts +26 -31
  102. package/template-vp-react/.vscode/settings.json +0 -18
  103. /package/{template-vp-vue-eslint-vapor → template-pnpm-vp-monorepo}/.vscode/extensions.json +0 -0
  104. /package/template-vp-monorepo-react-nestjs/apps/server/test/auth/{password-operations.spec.ts → password/password-operations.spec.ts} +0 -0
  105. /package/template-vp-monorepo-react-nestjs/apps/server/test/auth/{password.integration.spec.ts → password/password.integration.spec.ts} +0 -0
@@ -2,37 +2,40 @@
2
2
 
3
3
  ## 1. 状态与范围
4
4
 
5
- - 状态:验证记录(2026-10-08,验证进行中);本报告尚未形成完整验收结论。
5
+ - 状态:验证记录(2026-10-08);自动化与隔离 PostgreSQL/Redis 专项已执行,浏览器端到端与部署日志链路核验尚未完成,因此不作完整产品验收结论。
6
6
  - 依据:[需求文档](./需求文档.md)、[PRD](./PRD.md)、[技术契约](./技术契约.md)。
7
- - 验证对象为当前工作树中的 Shared、Server 与 React Web 增量;基线提交为 `c7f2bc6`。工作树未提交,后续代码变化须复测受影响项。
7
+ - 验证对象为 Shared、Server 与 React Web 增量;功能实施提交为 `40bc837`,当前 HEAD 为 `42fd708`。测试归档路径与本文状态更新仍有未提交改动;后续代码或测试内容变化须复测受影响项。
8
8
 
9
9
  ## 2. 已执行的自动化
10
10
 
11
11
  | 范围与命令 | 实际结果 | 证据边界 |
12
12
  | --- | --- | --- |
13
- | Server:`vp test test/auth/password-operations.spec.ts` | 12 项通过 | 使用服务替身验证事务失败路径、权限、安全事件和登录并发判定;包含 Fastify inject 的路由响应钩子测试,不等同于真实数据库/Redis 联调 |
14
- | Server:`vp test` | 8 套件、45 项通过;2 套件、23 项因环境门控跳过 | 跳过项不得计为通过;真实依赖专项另记 |
13
+ | Server:`vp test test/auth/password/password-operations.spec.ts`(`apps/server`) | 新路径 1 个文件、12 项通过,退出码 0 | 使用服务替身验证事务失败路径、权限、安全事件和登录并发判定;包含 Fastify inject 的路由响应钩子测试,不等同于真实数据库/Redis 联调 |
14
+ | Server:`vp test`(测试归档后) | 8 套件、45 项通过;3 套件、28 项因环境门控跳过 | 跳过项不得计为通过;其中隔离存储专项另行启用并验证 5 项 |
15
+ | Server:`vp test test/auth/password/password.integration.spec.ts`(`apps/server`),本机隔离 PostgreSQL 和 Redis DB 15 | 新路径 1 个文件、5 项通过,耗时 3.89 秒 | 随机临时数据库与独立 Redis 逻辑库;服务层调用真实存储与会话 Lua,撤销故障使用一次性注入;不等同于浏览器端到端 |
15
16
  | Server:`pnpm exec tsc --noEmit -p tsconfig.json`、`vp run build` | 均执行成功 | 类型检查与构建不能替代运行态验证 |
16
- | Web:定向运行 `password-actions.spec.ts`、`password-reset-action.spec.ts`、`workspaces-password-entry.spec.tsx` | 共 5 项通过 | 覆盖成功后清理本地会话、失败时保留会话、入口显示与空工作空间实际页面渲染;未覆盖真实浏览器全过程 |
17
+ | Web:`vp test test/password/password-actions.spec.ts test/password/password-reset-action.spec.ts test/password/workspaces-password-entry.spec.tsx`(`apps/web`) | 新路径 3 个文件、5 项通过 | 覆盖成功后清理本地会话、失败时保留会话、入口显示与空工作空间实际页面渲染;未覆盖真实浏览器全过程 |
17
18
  | Web:`pnpm --filter web build` | 执行成功 | 类型检查与构建不能替代运行态验证 |
18
19
 
19
20
  Web 全量 `pnpm --filter web test` 的 20 个套件中,14 个套件运行通过,62 项测试通过;另 6 个套件在导入既有 `apps/web/src/i18n/instance.ts` 时因 Node 测试环境缺少 `localStorage` 而未进入用例。加临时 `NODE_OPTIONS=--localstorage-file` 后仍复现。改用 `pnpm --filter web exec vp test --environment happy-dom`,19 个套件运行,106 项通过;既有 `test/request.spec.ts` 的 11 项因 happy-dom 的 CORS 限制拒绝本地测试 HTTP 服务而失败。这些运行不能记为 Web 全量通过。
20
21
 
22
+ 本次 Server 与 Shared 改动的 16 个文件定向 `vp fmt --check`、归档后的 5 个密码测试文件定向 `vp fmt --check`,以及 `git diff --check` 均已通过。全目录 `vp check` 初跑存在 79 个格式问题,其中包含本次文件;修复本次后端与共享文件后复跑剩 71 个,集成测试文件当时仍在编辑。全目录检查未通过,其余格式问题尚待单独处理。
23
+
21
24
  ## 3. 验收场景与未覆盖项
22
25
 
23
26
  | 场景 | 当前证据 | 待补验证 |
24
27
  | --- | --- | --- |
25
- | 无工作空间用户进入本人改密、成功后回登录页 | Web 页面与会话清理定向自动化 | 真实浏览器和后端联动 |
26
- | 旧密码错误、密码复用、输入不合法时无密码变更 | Server 定向自动化 | 真实 PostgreSQL 写入前后比对 |
27
- | 平台管理员重置他人、自重置拒绝、越权拒绝、目标不存在 | Server 定向自动化及 Web 入口定向自动化 | 真实权限数据和 HTTP 联动 |
28
- | 撤销目标 Web、桌面端、移动端会话,管理员自身会话保持 | 服务替身验证撤销调用边界 | 真实 Redis 三终端会话验证 |
29
- | Redis 撤销失败时密码和成功审计回滚,旧密码并发登录不能重新建会话 | Server 定向自动化 | 真实 PostgreSQL/Redis 故障与并发验证 |
28
+ | 无工作空间用户进入本人改密、成功后回登录页 | Web 页面与会话清理定向自动化;真实存储验证旧密码登录失败、新密码登录成功 | 真实浏览器和后端全过程联动 |
29
+ | 旧密码错误、密码复用、输入不合法时无密码变更 | Server 定向自动化覆盖旧密码错误、复用与未知字段清洗;真实存储专项验证撤销故障时摘要原值 | 长度、确认密码及其他输入失败逐项进行真实 HTTP 与数据库核对 |
30
+ | 平台管理员重置他人、自重置拒绝、越权拒绝、目标不存在 | Server 定向自动化;真实存储专项验证重置他人和自重置拒绝 | 非平台管理员、目标不存在的真实权限数据与 HTTP 联动;锁定/停用账号状态保持 |
31
+ | 撤销目标 Web、桌面端、移动端会话,管理员自身会话保持 | 真实 Redis 三终端 Lua 校验:本人和重置目标旧会话失效,管理员 Web 会话有效 | 真实浏览器与其他端客户端交互 |
32
+ | Redis 撤销失败时密码和成功审计回滚,旧密码并发登录不能重新建会话 | 真实 PostgreSQL 验证一次性注入的撤销故障会回滚摘要及成功审计,原会话有效;Server 定向自动化验证登录摘要复核 | 真实并发时序与运行态 Redis 停机故障 |
30
33
  | 响应、安全事件、管理审计和普通日志无密码明文或摘要 | Server 定向自动化覆盖固定安全事件与错误日志,代码审查已发现并修复两处问题 | 运行态日志、响应与审计数据抽检;部署日志保留期限与检索能力确认 |
31
34
 
32
- 真实 PostgreSQL/Redis 专项、浏览器端到端场景及运行态日志/审计抽检尚待记录,不能据当前自动化结果认定需求第 6 节全部通过。相关前后端核对见[联调记录](./联调记录.md)。
35
+ 浏览器端到端场景、运行态安全事件/普通日志抽检,以及部署日志保留和检索能力尚待记录;不能据当前自动化与服务层集成结果认定[需求文档](./需求文档.md)第 6 节全部通过。相关前后端核对见[联调记录](./联调记录.md)。
33
36
 
34
37
  ## 4. 变更记录
35
38
 
36
39
  | 日期 | 摘要 | 影响范围 | 原因 |
37
40
  | --- | --- | --- | --- |
38
- | 2026-10-08 | 建立自动化与未覆盖场景记录,区分测试环境加载失败和真实依赖验证 | Shared、Server、Web、认证与会话 | 避免将代码级测试或构建结果误记为完整业务验收 |
41
+ | 2026-10-08 | 记录定向/全量自动化、隔离 PostgreSQL/Redis 专项、测试环境失败与未覆盖场景 | Shared、Server、Web、认证与会话 | 区分代码级、真实依赖及浏览器和部署验证边界 |
@@ -2,20 +2,21 @@
2
2
 
3
3
  ## 1. 状态与依据
4
4
 
5
- - 状态:验证记录(2026-10-08,联调进行中);目前记录前后端契约核对,尚未形成运行态完整联调结论。
5
+ - 状态:验证记录(2026-10-08);已完成静态契约核对和服务层真实 PostgreSQL/Redis 专项,尚无浏览器端到端联调证据。
6
6
  - 需求与用户流程见[需求文档](./需求文档.md)和[PRD](./PRD.md);请求、响应、权限及失败语义见[技术契约](./技术契约.md)。执行结果见[测试报告](./测试报告.md)。
7
7
 
8
8
  ## 2. 已核对的接口衔接
9
9
 
10
10
  | 场景 | 当前代码衔接 | 验证边界 |
11
11
  | --- | --- | --- |
12
- | 本人改密 | Web `changePassword` 提交共享 `ChangePasswordRequest` 到 `POST /auth/change-password`;Server 使用当前会话 `userId` 处理并返回 `ChangePasswordResult` | 已完成源码与类型路径核对,运行态 HTTP 联动待记录 |
13
- | 平台管理员重置 | Web 平台账号管理提交共享 `ResetAccountPasswordRequest` 到 `POST /platform/accounts/:userId/reset-password`;Server 路由要求平台操作权限与内置管理员身份 | 已完成源码与类型路径核对,真实权限数据的 HTTP 联动待记录 |
12
+ | 本人改密 | Web `changePassword` 提交共享 `ChangePasswordRequest` 到 `POST /auth/change-password`;Server 使用当前会话 `userId` 处理并返回 `ChangePasswordResult` | 已完成源码与类型路径核对;服务层真实存储验证新旧密码登录和三终端撤销,运行态 HTTP 联动待记录 |
13
+ | 平台管理员重置 | Web 平台账号管理提交共享 `ResetAccountPasswordRequest` 到 `POST /platform/accounts/:userId/reset-password`;Server 路由要求平台操作权限与内置管理员身份 | 已完成源码与类型路径核对;服务层真实存储验证目标撤销、操作者会话及成功审计,运行态 HTTP 联动待记录 |
14
14
  | 成功与失败界面 | 本人成功后删除本地令牌并跳转登录页,重置成功后仅提示线下告知;失败保留仍有效的会话且不显示成功 | 定向 Web 自动化已执行,真实浏览器全过程待记录 |
15
15
 
16
16
  ## 3. 联调发现与待确认
17
17
 
18
- - 安全审查发现管理员重置请求的未知字段名可能进入 400 响应,以及重置时认证存储/审计故障可能返回与契约不符的 500。Server 已针对这两项修改,并在定向自动化中复测;真实依赖故障路径仍待验证。
18
+ - 安全审查发现管理员重置请求的未知字段名可能进入 400 响应,以及重置时认证存储/审计故障可能返回与契约不符的 500。Server 已针对这两项修改,并在定向自动化中复测;隔离存储专项已验证一次性注入撤销故障的数据库回滚,运行态 Redis 停机故障尚未执行。
19
+ - 隔离 PostgreSQL/Redis 5 项专项验证了本人改密和管理员重置成功路径、三终端旧会话撤销、管理员会话保留、一次性注入的撤销故障回滚与自重置拒绝。此专项直接调用服务,不代表已经通过浏览器到后端的运行态全链路。
19
20
  - Web 全量测试受既有 Node `localStorage` 加载条件和 happy-dom 本地 HTTP CORS 限制影响;具体运行数字和边界见[测试报告](./测试报告.md)。
20
21
  - 当前尚无真实浏览器端到端证据;部署日志的实际保留期限和检索能力也尚未核验。
21
22
 
@@ -23,4 +24,4 @@
23
24
 
24
25
  | 日期 | 摘要 | 影响范围 | 原因 |
25
26
  | --- | --- | --- | --- |
26
- | 2026-10-08 | 建立共享类型、Web 请求与 Server 路由的衔接核对及待联调边界 | Shared、Web、Server | 区分源码契约核对和真实运行态联调 |
27
+ | 2026-10-08 | 核对共享类型、Web 请求与 Server 路由,记录隔离存储专项和待联调边界 | Shared、Web、Server、PostgreSQL、Redis | 区分源码契约、服务层真实依赖和浏览器运行态联调 |
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## 1. 状态与来源
4
4
 
5
- - 状态:待实施;本文描述 2026-10-08 确定的实施设计,不表示当前接口已经上线。
5
+ - 状态:当前有效设计;本方案已在当前工作树实现,自动化与隔离存储专项结果及未覆盖边界见[测试报告](./测试报告.md),不代表已经部署上线。
6
6
  - 业务边界以[需求文档](./需求文档.md)为准,用户流程以[PRD](./PRD.md)为准,字段与错误以[技术契约](./技术契约.md)为准。
7
7
  - 沿用当前全局 `users` 账号、Argon2id 密码摘要和 Redis Session;不引入工作空间级密码、临时密码或 Refresh Token。
8
8
 
@@ -50,4 +50,5 @@
50
50
 
51
51
  | 日期 | 摘要 | 影响范围 | 原因 |
52
52
  | --- | --- | --- | --- |
53
+ | 2026-10-08 | 同步当前工作树实现状态与验证边界 | Auth、平台账号、Web、Redis Session | 区分代码实现和未完成的浏览器、部署验收 |
53
54
  | 2026-10-08 | 确定入口、权限、事务内全端撤销、登录并发校验及安全记录边界 | Auth、平台账号、Web、Redis Session | 将已确认业务规则落实为可实现且可验证的方案 |
@@ -2,13 +2,13 @@
2
2
 
3
3
  ## 1. 文档状态
4
4
 
5
- - 状态:实施中;以下业务规则已确认,本功能尚待代码实现与验收完成。
5
+ - 状态:当前有效需求;功能已在当前工作树实现,自动化与隔离存储专项已执行,浏览器端到端及部署日志链路核验仍待补充,详见[测试报告](./测试报告.md)。
6
6
  - 整理日期:2026-09-30。
7
- - 当前认证与会话基线见[会话管理需求文档](../会话管理/需求文档.md)和[会话管理技术契约](../会话管理/技术契约.md)。本功能的实施依据另见[PRD](./PRD.md)、[设计方案](./设计方案.md)和[技术契约](./技术契约.md);拟新增能力在验收完成前不是现行接口。
7
+ - 当前认证与会话基线见[会话管理需求文档](../会话管理/需求文档.md)和[会话管理技术契约](../会话管理/技术契约.md)。本功能的实现依据另见[PRD](./PRD.md)、[设计方案](./设计方案.md)和[技术契约](./技术契约.md);已执行与未覆盖的验证见[测试报告](./测试报告.md)和[联调记录](./联调记录.md)。
8
8
 
9
9
  ## 2. 背景与目标
10
10
 
11
- 当前用户注册后无法自行修改密码,管理员也无法为忘记密码的用户重置密码。账号是跨工作空间使用的全局身份;密码变更必须作用于全局账号,并及时使旧登录状态失效。
11
+ 此前用户注册后无法自行修改密码,管理员也无法为忘记密码的用户重置密码。账号是跨工作空间使用的全局身份;密码变更必须作用于全局账号,并及时使旧登录状态失效。
12
12
 
13
13
  本功能让已登录用户验证旧密码后修改自己的密码,让平台管理员为其他全局账号设定新密码,同时保护密码内容与各终端会话。
14
14
 
@@ -59,5 +59,6 @@
59
59
 
60
60
  | 日期 | 摘要 | 影响范围 | 原因 |
61
61
  | --- | --- | --- | --- |
62
+ | 2026-10-08 | 标明当前工作树已实现双入口与后端处理,并关联实际验证和未覆盖边界 | 全局账号、认证、平台账号管理、Web 与会话 | 使需求状态与实现同步,避免把未执行的浏览器和部署验证写成通过 |
62
63
  | 2026-10-08 | 形成 PRD、设计与技术契约,明确接口和安全事件;保留周期作为交付待确认项 | 全局账号、认证、平台账号管理、Web 与会话 | 为已确认需求建立实施和联调依据,避免把部署状态写成已验证事实 |
63
64
  | 2026-09-30 | 确认本人改密与平台管理员重置、密码规则及全端会话撤销,建立待实施需求 | 全局账号、认证、平台账号管理与会话 | 补齐现有账号密码维护能力,并明确凭据与会话的安全边界 |
@@ -15,7 +15,7 @@
15
15
  | [03.任务队列](./03.任务队列/) | BullMQ 公共队列 | 公共队列基础能力已实现;具体业务任务另行接入 |
16
16
  | [04.界面与主题](./04.界面与主题/) | 主题说明、UI 截图、视觉验证证据与工具、前端国际化 | 已归档验证产物;国际化已统一联动 antd、Pro Components 与 Day.js,不等同于业务验收 |
17
17
  | [05.运行维护](./05.运行维护/) | 平台操作、数据库迁移、备份与恢复工具 | 随当前实现维护;备份文件按敏感数据处理 |
18
- | [06.认证与会话](./06.认证与会话/) | 登录、终端会话、修改密码、令牌演进与实时撤权边界 | 当前 Redis Session 基线已记录;修改密码实施中,后续令牌演进必须先确认迁移契约 |
18
+ | [06.认证与会话](./06.认证与会话/) | 登录、终端会话、修改密码、令牌演进与实时撤权边界 | 当前 Redis Session 与修改密码增量已在工作树实现;浏览器和部署验证待补,后续令牌演进必须先确认迁移契约 |
19
19
  | [07.API与错误](./07.API与错误/) | Server 成功响应、失败响应、请求关联 ID | 当前有效契约 |
20
20
 
21
21
  ## 主要功能与产物导航
@@ -32,7 +32,7 @@
32
32
  | 前端国际化 | [需求文档](./04.界面与主题/国际化/需求文档.md)、[设计方案](./04.界面与主题/国际化/设计方案.md)、[测试报告](./04.界面与主题/国际化/测试报告.md)、[交付记录](./04.界面与主题/国际化/交付记录.md) | 语言切换统一驱动应用词条、antd、Pro Components、Day.js 与 `<html lang>` |
33
33
  | 平台运行维护 | [平台操作手册](./05.运行维护/平台操作手册.md)、[数据库迁移指南](./05.运行维护/数据库迁移/操作指南.md) | [备份与恢复材料](./05.运行维护/数据库迁移/备份/);其中数据库转储按敏感数据管理 |
34
34
  | 认证与会话 | [需求文档](./06.认证与会话/会话管理/需求文档.md)、[技术契约](./06.认证与会话/会话管理/技术契约.md) | 当前 Session 基线与后续演进边界 |
35
- | 修改密码 | [需求文档](./06.认证与会话/修改密码/需求文档.md)、[PRD](./06.认证与会话/修改密码/PRD.md)、[设计方案](./06.认证与会话/修改密码/设计方案.md)、[技术契约](./06.认证与会话/修改密码/技术契约.md) | 实施中:本人改密、平台管理员重置及全端会话撤销;保留期限待交付核验 |
35
+ | 修改密码 | [需求文档](./06.认证与会话/修改密码/需求文档.md)、[PRD](./06.认证与会话/修改密码/PRD.md)、[设计方案](./06.认证与会话/修改密码/设计方案.md)、[技术契约](./06.认证与会话/修改密码/技术契约.md) | [联调记录](./06.认证与会话/修改密码/联调记录.md)、[测试报告](./06.认证与会话/修改密码/测试报告.md);工作树已实现且完成隔离存储专项,浏览器端到端与部署日志核验待补 |
36
36
  | API 与错误 | [技术契约](./07.API与错误/技术契约.md) | 成功响应、失败响应与请求关联 ID |
37
37
 
38
38
  ## 从 Codex 历史提炼的当前决策
@@ -80,6 +80,7 @@
80
80
 
81
81
  | 日期 | 摘要 | 影响范围 | 原因 |
82
82
  | ---------- | ---------------------------------------------------------------------------------------------- | -------------------------------------- | ----------------------------------------------------------------------------- |
83
+ | 2026-10-08 | 同步修改密码当前工作树实现状态,补充联调与测试报告导航及未覆盖边界 | 认证与会话目录索引 | 使目录状态与代码和实际验证证据一致 |
83
84
  | 2026-10-08 | 补充修改密码 PRD、设计和技术契约导航,标明实施中及日志保留待核验 | 认证与会话目录索引 | 在开发前固定正式实施依据并保持状态真实 |
84
85
  | 2026-09-30 | 新增修改密码待实施需求导航,同步认证与会话主题状态 | 认证与会话目录索引 | 区分已确认的密码维护需求与当前尚未实现的接口 |
85
86
  | 2026-09-18 | 清理原始未完会话正文,记录企业层级实现偏差,并将企业邀请纠偏为“只确认产品入口、具体协议待确认” | 企业/项目正式文档索引、迁移与交付状态 | 现有邀请实现不能反向定义产品协议;消除索引与现行需求、PRD、技术契约之间的冲突 |
@@ -2,10 +2,10 @@ import { describe, expect, it, vi } from 'vite-plus/test'
2
2
  import type { ChangePasswordRequest, ChangePasswordResult } from 'shared/types'
3
3
 
4
4
  const session = vi.hoisted(() => ({ clear: vi.fn(), remove: vi.fn() }))
5
- vi.mock('../src/utils/request/workspace', () => ({ clearWorkspaceRequests: session.clear }))
6
- vi.mock('../src/utils/storage/cookie', () => ({ cookie: { remove: session.remove } }))
5
+ vi.mock('../../src/utils/request/workspace', () => ({ clearWorkspaceRequests: session.clear }))
6
+ vi.mock('../../src/utils/storage/cookie', () => ({ cookie: { remove: session.remove } }))
7
7
 
8
- import { changePasswordAndClearSession } from '../src/components/PasswordActions/change-password-session'
8
+ import { changePasswordAndClearSession } from '../../src/components/PasswordActions/change-password-session'
9
9
 
10
10
  const input: ChangePasswordRequest = {
11
11
  oldPassword: 'old-example',
@@ -1,7 +1,7 @@
1
1
  import type { I18nState } from '@bubblesjs/i18n-core'
2
2
  import { describe, expect, it, vi } from 'vite-plus/test'
3
3
  import type { AccountRecord } from 'shared/types'
4
- import { createMemberTableColumns } from '../src/pages/access/components/MemberTable'
4
+ import { createMemberTableColumns } from '../../src/pages/access/components/MemberTable'
5
5
 
6
6
  const tr = ((key: string) => key) as I18nState['tr']
7
7
  const target: AccountRecord = {
@@ -43,7 +43,9 @@ function resetAction(options: { currentUserId: string; canResetPassword: boolean
43
43
  const actions = render?.(undefined, target).props.children ?? []
44
44
  const button = actions.find(
45
45
  (item): item is { props: { children: string; onClick: () => void } } =>
46
- typeof item === 'object' && item !== null && 'props' in item &&
46
+ typeof item === 'object' &&
47
+ item !== null &&
48
+ 'props' in item &&
47
49
  (item as { props: { children?: unknown } }).props.children === '重置密码',
48
50
  )
49
51
  return { button, onResetPassword }
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vite-plus/test'
4
4
 
5
- vi.mock('../src/pages/workspaces/state', () => ({
5
+ vi.mock('../../src/pages/workspaces/state', () => ({
6
6
  getWorkspaceState: () => ({
7
7
  workspaces: {
8
8
  user: { id: 'user-without-workspace', name: '无空间用户' },
@@ -10,14 +10,14 @@ vi.mock('../src/pages/workspaces/state', () => ({
10
10
  },
11
11
  }),
12
12
  }))
13
- vi.mock('../src/api/auth', () => ({ logout: vi.fn() }))
14
- vi.mock('../src/utils/request/workspace', () => ({ clearWorkspaceRequests: vi.fn() }))
15
- vi.mock('../src/components/Brand/Brand', () => ({ default: () => <span>万物</span> }))
16
- vi.mock('../src/components/LocaleSwitch/LocaleSwitch', () => ({ default: () => null }))
13
+ vi.mock('../../src/api/auth', () => ({ logout: vi.fn() }))
14
+ vi.mock('../../src/utils/request/workspace', () => ({ clearWorkspaceRequests: vi.fn() }))
15
+ vi.mock('../../src/components/Brand/Brand', () => ({ default: () => <span>万物</span> }))
16
+ vi.mock('../../src/components/LocaleSwitch/LocaleSwitch', () => ({ default: () => null }))
17
17
  vi.mock('@bubblesjs/i18n-react', () => ({
18
18
  useI18n: () => ({ tr: (key: string) => key }),
19
19
  }))
20
- vi.mock('../src/components/PasswordActions/ChangePasswordDialog', () => ({
20
+ vi.mock('../../src/components/PasswordActions/ChangePasswordDialog', () => ({
21
21
  /** 模拟改密弹窗的引用接口,以验证页面入口能实际打开它。 */
22
22
  default: function PasswordDialog({ ref }: { ref: Ref<{ show: () => void; hide: () => void }> }) {
23
23
  const [open, setOpen] = useState(false)
@@ -27,7 +27,7 @@ vi.mock('../src/components/PasswordActions/ChangePasswordDialog', () => ({
27
27
  }))
28
28
 
29
29
  import { App } from 'antd'
30
- import WorkspacesPage from '../src/pages/workspaces'
30
+ import WorkspacesPage from '../../src/pages/workspaces'
31
31
 
32
32
  let host: HTMLDivElement
33
33
  let root: ReturnType<typeof createRoot>
@@ -53,6 +53,8 @@ tr(`你好 ${name}`)
53
53
 
54
54
  扫描器故意不依赖 AST,按文本识别静态调用,因此不区分具体编程语言。对应的取舍是:注释或普通字符串中出现完整的 `tr('key')` 文本时,也会被识别。
55
55
 
56
+ 调用名与 `(`、`(` 与静态字符串、静态字符串与 `,` 或 `)` 之间允许空白、`//` 行注释和 `/* ... */` 块注释,例如 `tr(/* 说明 */ '保存' /* 说明 */)`。字符串内部的注释符号按词条正文保留;注释后是动态表达式时仍不提取。
57
+
56
58
  ## 命令
57
59
 
58
60
  ```bash
@@ -21,6 +21,94 @@ describe('scanSource', () => {
21
21
  expect(scanSource(source)).toEqual([{ key: '你好 {name}', index: 16, line: 1, column: 17 }])
22
22
  })
23
23
 
24
+ it.each([
25
+ { boundary: 'call name', source: "tr/* 调用说明 */('保存')" },
26
+ { boundary: 'opening parenthesis', source: "tr(/* 参数说明 */ '保存')" },
27
+ { boundary: 'closing parenthesis', source: "tr('保存' /* 词条说明 */)" },
28
+ { boundary: 'comma', source: "tr('保存' /* 词条说明 */, { name })" },
29
+ {
30
+ boundary: 'adjacent mixed comments',
31
+ source: [
32
+ 'tr/**/// 调用说明',
33
+ '(/* 参数说明 */ /**/ // 参数说明',
34
+ " '保存' /**/ // 词条说明",
35
+ ' /* 词条说明 */)',
36
+ ].join('\n'),
37
+ },
38
+ ])('allows comments at the $boundary boundary', ({ source }) => {
39
+ expect(scanSource(source)).toEqual([{ key: '保存', index: 0, line: 1, column: 1 }])
40
+ })
41
+
42
+ it.each([
43
+ { name: 'LF', lineEnding: '\n' },
44
+ { name: 'CR', lineEnding: '\r' },
45
+ { name: 'CRLF', lineEnding: '\r\n' },
46
+ { name: 'U+2028', lineEnding: '\u2028' },
47
+ { name: 'U+2029', lineEnding: '\u2029' },
48
+ ])('ends line comments at $name and preserves source positions', ({ lineEnding }) => {
49
+ const source = [
50
+ 'tr // 调用说明',
51
+ ' (// 参数说明',
52
+ " 'first' // 词条说明",
53
+ ' )',
54
+ " tr('next')",
55
+ ].join(lineEnding)
56
+
57
+ expect(scanSource(source)).toEqual([
58
+ { key: 'first', index: 0, line: 1, column: 1 },
59
+ { key: 'next', index: source.indexOf("tr('next')"), line: 5, column: 3 },
60
+ ])
61
+ })
62
+
63
+ it('preserves comment markers inside static strings', () => {
64
+ const source = [
65
+ "tr('https://example.test/path/*说明*/')",
66
+ 'tr("/* 开头 */ // 结尾")',
67
+ 'tr(`// 第一行\n/* 第二行 */`)',
68
+ ].join('\n')
69
+
70
+ expect(scanSource(source).map(({ key }) => key)).toEqual([
71
+ 'https://example.test/path/*说明*/',
72
+ '/* 开头 */ // 结尾',
73
+ '// 第一行\n/* 第二行 */',
74
+ ])
75
+ })
76
+
77
+ it('continues extracting complete calls inside comments and ordinary strings', () => {
78
+ const source = [
79
+ "// tr('行注释')",
80
+ "/* tr('块注释') */",
81
+ 'const example = "tr(\'字符串\')"',
82
+ ].join('\r\n')
83
+
84
+ expect(scanSource(source)).toEqual([
85
+ { key: '行注释', index: 3, line: 1, column: 4 },
86
+ { key: '块注释', index: source.indexOf("tr('块注释')"), line: 2, column: 4 },
87
+ { key: '字符串', index: source.indexOf("tr('字符串')"), line: 3, column: 18 },
88
+ ])
89
+ })
90
+
91
+ it('still rejects dynamic expressions around comments', () => {
92
+ const source = [
93
+ 'tr(/* 参数说明 */ key)',
94
+ "tr('a' /* 词条说明 */ + value)",
95
+ "tr('a' // 词条说明\n + value)",
96
+ "tr('a' /* 词条说明 */.trim())",
97
+ "tr('a' /* 词条说明 */ / divisor)",
98
+ 'tr(`hello ${name}` /* 词条说明 */)',
99
+ ].join('\n')
100
+
101
+ expect(scanSource(source)).toEqual([])
102
+ })
103
+
104
+ it.each([
105
+ { boundary: 'call name', source: "tr/* 调用说明 ('保存')" },
106
+ { boundary: 'opening parenthesis', source: "tr(/* 参数说明 '保存')" },
107
+ { boundary: 'static string', source: "tr('保存' /* 词条说明)" },
108
+ ])('rejects an unterminated block comment after the $boundary', ({ source }) => {
109
+ expect(scanSource(source)).toEqual([])
110
+ })
111
+
24
112
  it('decodes common escapes without evaluating source code', () => {
25
113
  const source = String.raw`tr('It\'s\n中\t\x41\u6587\u{1F600}')`
26
114
 
@@ -21,9 +21,10 @@ interface ParsedEscape {
21
21
 
22
22
  const identifierContinuePattern = /[\p{ID_Continue}_$]/u
23
23
  const whitespacePattern = /\s/u
24
+ const lineTerminatorPattern = /[\n\r\u2028\u2029]/u
24
25
  const hexPattern = /^[\dA-Fa-f]+$/
25
26
 
26
- /** 扫描文本中的翻译调用及其静态字符串参数,返回词条与从 1 开始的行列位置;跳过插值模板和动态表达式。 */
27
+ /** 扫描文本中的翻译调用及其静态字符串参数,允许调用边界的空白与注释;返回原始源码行列位置,跳过插值模板和动态表达式。 */
27
28
  export function scanSource(source: string, options: ScanOptions = {}): MessageOccurrence[] {
28
29
  const callNames = normalizeCallNames(options.callNames)
29
30
  const occurrences: MessageOccurrence[] = []
@@ -44,12 +45,12 @@ export function scanSource(source: string, options: ScanOptions = {}): MessageOc
44
45
  continue
45
46
  }
46
47
 
47
- let cursor = skipWhitespace(source, index + callName.length)
48
+ let cursor = skipWhitespaceAndComments(source, index + callName.length)
48
49
  if (source[cursor] !== '(') {
49
50
  continue
50
51
  }
51
52
 
52
- cursor = skipWhitespace(source, cursor + 1)
53
+ cursor = skipWhitespaceAndComments(source, cursor + 1)
53
54
  const quote = source[cursor]
54
55
  if (quote !== "'" && quote !== '"' && quote !== '`') {
55
56
  continue
@@ -60,7 +61,7 @@ export function scanSource(source: string, options: ScanOptions = {}): MessageOc
60
61
  continue
61
62
  }
62
63
 
63
- const nextTokenIndex = skipWhitespace(source, parsed.endIndex)
64
+ const nextTokenIndex = skipWhitespaceAndComments(source, parsed.endIndex)
64
65
  const nextToken = source[nextTokenIndex]
65
66
  if (nextToken !== ',' && nextToken !== ')') {
66
67
  continue
@@ -114,11 +115,33 @@ function isIdentifierContinue(character: string | undefined): boolean {
114
115
  )
115
116
  }
116
117
 
117
- /** 从指定位置跳过空白字符,返回下一个非空白字符的索引。 */
118
- function skipWhitespace(source: string, startIndex: number): number {
118
+ /** 跳过调用边界的空白、行注释和块注释;未闭合块注释返回源码末尾,使当前候选匹配失败。 */
119
+ function skipWhitespaceAndComments(source: string, startIndex: number): number {
119
120
  let index = startIndex
120
- while (index < source.length && whitespacePattern.test(source[index] ?? '')) {
121
- index += 1
121
+ while (index < source.length) {
122
+ if (whitespacePattern.test(source[index] ?? '')) {
123
+ index += 1
124
+ continue
125
+ }
126
+
127
+ if (source.startsWith('//', index)) {
128
+ index += 2
129
+ while (index < source.length && !lineTerminatorPattern.test(source[index] ?? '')) {
130
+ index += 1
131
+ }
132
+ continue
133
+ }
134
+
135
+ if (source.startsWith('/*', index)) {
136
+ const closingIndex = source.indexOf('*/', index + 2)
137
+ if (closingIndex === -1) {
138
+ return source.length
139
+ }
140
+ index = closingIndex + 2
141
+ continue
142
+ }
143
+
144
+ break
122
145
  }
123
146
  return index
124
147
  }
@@ -131,7 +154,7 @@ function parseString(
131
154
  ): ParsedString | undefined {
132
155
  let value = ''
133
156
 
134
- for (let index = startIndex + 1; index < source.length; ) {
157
+ for (let index = startIndex + 1; index < source.length;) {
135
158
  const character = source[index]
136
159
 
137
160
  if (character === quote) {
@@ -258,7 +281,7 @@ function parseCodePointEscape(source: string, slashIndex: number): ParsedEscape
258
281
  }
259
282
  }
260
283
 
261
- /** 收集每行起始偏移,兼容 LF、CRLF 和 CR 换行。 */
284
+ /** 收集每行起始偏移,兼容 LF、CRLF、CR 以及 Unicode 行和段落分隔符。 */
262
285
  function collectLineStarts(source: string): number[] {
263
286
  const starts = [0]
264
287
 
@@ -268,7 +291,7 @@ function collectLineStarts(source: string): number[] {
268
291
  index += 1
269
292
  }
270
293
  starts.push(index + 1)
271
- } else if (source[index] === '\n') {
294
+ } else if (lineTerminatorPattern.test(source[index] ?? '')) {
272
295
  starts.push(index + 1)
273
296
  }
274
297
  }
@@ -62,6 +62,42 @@ describe('runCli', () => {
62
62
  expect(output.join('\n')).toContain('Project web: 1 source files, 2 keys.')
63
63
  })
64
64
 
65
+ it('keeps translations for calls with comments when cleaning catalogs', async () => {
66
+ const root = temporaryDirectory.path
67
+ await mkdir(join(root, 'src'), { recursive: true })
68
+ await mkdir(join(root, 'locales'), { recursive: true })
69
+ await writeFile(
70
+ join(root, 'src', 'app.ts'),
71
+ "tr('正常')\ntr(/* 参数说明 */ '保留' /* 结束说明 */)\n",
72
+ 'utf8',
73
+ )
74
+ await writeFile(
75
+ join(root, 'locales', 'en_US.json'),
76
+ JSON.stringify({ 正常: 'Normal', 保留: 'Keep translation', 废弃: 'Remove translation' }),
77
+ 'utf8',
78
+ )
79
+ await writeFile(join(root, 'locales', 'zh_CN.json'), '{}\n', 'utf8')
80
+ await writeConfig(root)
81
+
82
+ await expect(runCli(['sync', '--clean'], { cwd: root, stdout: () => undefined })).resolves.toBe(
83
+ 0,
84
+ )
85
+ expect(JSON.parse(await readFile(join(root, 'locales', 'en_US.json'), 'utf8'))).toEqual({
86
+ 正常: 'Normal',
87
+ 保留: 'Keep translation',
88
+ })
89
+ const report = JSON.parse(await readFile(join(root, '.reports', 'i18n.json'), 'utf8'))
90
+ expect(report.files).toEqual(
91
+ expect.arrayContaining([
92
+ expect.objectContaining({
93
+ locale: 'en_US',
94
+ deleted: ['废弃'],
95
+ unchangedCount: 2,
96
+ }),
97
+ ]),
98
+ )
99
+ })
100
+
65
101
  it('checks without writing and can fail on unused keys', async () => {
66
102
  const root = temporaryDirectory.path
67
103
  await mkdir(join(root, 'src'), { recursive: true })
@@ -1,5 +1,5 @@
1
- import { useSyncExternalStore } from 'react'
2
1
  import type { I18nState, I18nStore } from '@bubblesjs/i18n-core'
2
+ import { useMemo, useSyncExternalStore } from 'react'
3
3
 
4
4
  /** 订阅国际化容器,并返回选择器提取的状态。 */
5
5
  export function useI18nStore<T>(store: I18nStore, selector: (state: I18nState) => T): T
@@ -10,8 +10,18 @@ export function useI18nStore<T>(
10
10
  store: I18nStore,
11
11
  selector?: (state: I18nState) => T,
12
12
  ): T | I18nState {
13
- if (selector) {
14
- return useSyncExternalStore(store.subscribe, () => selector(store.getState()))
15
- }
16
- return useSyncExternalStore(store.subscribe, store.getState)
13
+ const getSnapshot = useMemo(() => {
14
+ let cache: { state: I18nState; value: T | I18nState } | undefined
15
+ return () => {
16
+ const state = store.getState()
17
+ if (cache && Object.is(cache.state, state)) {
18
+ return cache.value
19
+ }
20
+ const value = selector ? selector(state) : state
21
+ cache = { state, value }
22
+ return value
23
+ }
24
+ }, [store, selector])
25
+
26
+ return useSyncExternalStore(store.subscribe, getSnapshot)
17
27
  }
@@ -2,7 +2,7 @@
2
2
  "extends": "./tsconfig.bundler.json",
3
3
  "compilerOptions": {
4
4
  "target": "ES2020",
5
- "lib": ["ES2020"],
5
+ "lib": ["ES2020", "DOM"],
6
6
  "types": [],
7
7
  "useDefineForClassFields": true,
8
8
  "noUncheckedIndexedAccess": true
@@ -26,14 +26,14 @@
26
26
  "devDependencies": {
27
27
  "@commitlint/cli": "^21.2.3",
28
28
  "@commitlint/config-conventional": "^21.2.3",
29
- "@types/node": "^26.6.2",
29
+ "@types/node": "^26.6.4",
30
30
  "@types/react": "^19.3.0",
31
- "@types/react-dom": "^19.2.7",
32
- "@vitejs/plugin-react": "^6.1.1",
31
+ "@types/react-dom": "^19.3.0",
32
+ "@vitejs/plugin-react": "^6.1.2",
33
33
  "typescript": "~7.0.2",
34
34
  "unplugin-auto-import": "^21.1.0",
35
- "vite": "npm:@voidzero-dev/vite-plus-core@0.3.3",
35
+ "vite": "npm:@voidzero-dev/vite-plus-core@1.1.0",
36
36
  "vite-plugin-svgr": "^5.2.0",
37
- "vite-plus": "0.3.3"
37
+ "vite-plus": "1.1.0"
38
38
  }
39
39
  }
@@ -0,0 +1,53 @@
1
+ {
2
+ // Disable the default formatter, use eslint instead
3
+ "biome.enabled": false,
4
+ "oxc.enable": false,
5
+ "prettier.enable": false,
6
+ "editor.formatOnSave": false,
7
+
8
+
9
+ // Auto fix
10
+ "editor.codeActionsOnSave": {
11
+ "source.fixAll.eslint": "explicit",
12
+ "source.organizeImports": "never"
13
+ },
14
+
15
+ // Silent the stylistic rules in your IDE, but still auto fix them
16
+ "eslint.rules.customizations": [
17
+ { "rule": "style/*", "severity": "off", "fixable": true },
18
+ { "rule": "format/*", "severity": "off", "fixable": true },
19
+ { "rule": "*-indent", "severity": "off", "fixable": true },
20
+ { "rule": "*-spacing", "severity": "off", "fixable": true },
21
+ { "rule": "*-spaces", "severity": "off", "fixable": true },
22
+ { "rule": "*-order", "severity": "off", "fixable": true },
23
+ { "rule": "*-dangle", "severity": "off", "fixable": true },
24
+ { "rule": "*-newline", "severity": "off", "fixable": true },
25
+ { "rule": "*quotes", "severity": "off", "fixable": true },
26
+ { "rule": "*semi", "severity": "off", "fixable": true }
27
+ ],
28
+
29
+ // Enable eslint for all supported languages
30
+ "eslint.validate": [
31
+ "javascript",
32
+ "javascriptreact",
33
+ "typescript",
34
+ "typescriptreact",
35
+ "vue",
36
+ "html",
37
+ "markdown",
38
+ "json",
39
+ "jsonc",
40
+ "yaml",
41
+ "toml",
42
+ "xml",
43
+ "gql",
44
+ "graphql",
45
+ "astro",
46
+ "svelte",
47
+ "css",
48
+ "less",
49
+ "scss",
50
+ "pcss",
51
+ "postcss"
52
+ ]
53
+ }
@@ -16,6 +16,7 @@
16
16
  "@antdv-next/icons": "^1.1.2",
17
17
  "alova": "^3.5.5",
18
18
  "antdv-next": "^1.6.0",
19
+ "antdv-next-pro": "^0.1.3",
19
20
  "axios": "^1.20.0",
20
21
  "dayjs": "^1.11.23",
21
22
  "echarts": "^6.1.0",