@oj-module/cli 0.2.66 → 0.2.67

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 (187) hide show
  1. package/bin/.oj-version +1 -0
  2. package/bin/oj +4 -0
  3. package/devkit/README.md +15 -7
  4. package/devkit/SKILL.md +30 -1
  5. package/devkit/framework-dev.md +10 -4
  6. package/devkit/manual.md +38 -12
  7. package/devkit/publish-walkthrough.md +110 -37
  8. package/devkit/wecom-internal-app.md +7 -2
  9. package/devkit/wecom-multi-corp-links.md +163 -0
  10. package/package.json +2 -2
  11. package/shell-dist/assets/{index.i0kqbxp050.js → index.6alqyx6ae0.js} +21 -1
  12. package/shell-dist/assets/{runtime.1xfsxcajj0.js → runtime.2dgstpjsu0.js} +3449 -3449
  13. package/shell-dist/index.html +4 -4
  14. package/shell-dist/versions.json +3 -3
  15. package/src/__tests__/publish-keygen.test.ts +157 -0
  16. package/src/__tests__/templates-migration-schema-parity.test.ts +540 -0
  17. package/src/__tests__/templates-schema-columns.test.ts +15 -2
  18. package/src/__tests__/templates-tenant-scope.test.ts +6 -2
  19. package/src/bundle.ts +8 -3
  20. package/src/cli.ts +17 -5
  21. package/src/dev.ts +15 -10
  22. package/src/install.ts +11 -4
  23. package/src/keygen.ts +54 -0
  24. package/src/lifecycle/__tests__/bundle.test.ts +21 -3
  25. package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/manifest.yaml +1 -1
  26. package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/migrations/0001__init.sql +2 -2
  27. package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/schema.yaml +5 -2
  28. package/src/lifecycle/__tests__/fixtures/fixture-valid/manifest.yaml +1 -1
  29. package/src/lifecycle/__tests__/manifest-contract.test.ts +75 -3
  30. package/src/lifecycle/__tests__/smoke.test.ts +2 -2
  31. package/src/manifest-contract.ts +91 -8
  32. package/src/market/bootstrap.ts +26 -2
  33. package/src/market/command.ts +1 -0
  34. package/src/market/publish.ts +5 -1
  35. package/src/market/walkthrough.ts +24 -8
  36. package/src/site.ts +2 -0
  37. package/src/usage.ts +8 -4
  38. package/src/vendor.ts +5 -1
  39. package/templates/config.yaml +4 -2
  40. package/templates/init.include.yaml +8 -3
  41. package/templates/openapi.json +460 -1
  42. package/templates/src/app/api/_platform/_lifecycle/compensate.ts +136 -0
  43. package/templates/src/app/api/_platform/_lifecycle/installer.ts +71 -22
  44. package/templates/src/app/api/_platform/_lifecycle/lock.ts +23 -0
  45. package/templates/src/app/api/_platform/_lifecycle/steps/deploy-web.ts +28 -0
  46. package/templates/src/app/api/_platform/_lifecycle/steps/preflight.ts +46 -0
  47. package/templates/src/app/api/_platform/_lifecycle/steps/reload.ts +100 -36
  48. package/templates/src/app/api/_platform/_lifecycle/steps/smoke.ts +7 -15
  49. package/templates/src/app/api/_platform/_lifecycle/text-edit.ts +71 -19
  50. package/templates/src/app/api/_platform/_lifecycle/types.ts +26 -2
  51. package/templates/src/app/api/_platform/_lifecycle/uninstall.ts +93 -14
  52. package/templates/src/app/api/_platform/_shared/service/domain/settings.ts +8 -4
  53. package/templates/src/app/api/_platform/manifest.yaml +1 -0
  54. package/templates/src/app/api/_platform/market/api.ts +235 -10
  55. package/templates/src/app/api/_platform/migrations/0001__baseline.mysql.sql +178 -0
  56. package/templates/src/app/api/_platform/migrations/0001__baseline.pg.sql +173 -0
  57. package/templates/src/app/api/_platform/migrations/0001__baseline.sql +188 -0
  58. package/templates/src/app/api/_platform/migrations/0002__market_release.mysql.sql +17 -0
  59. package/templates/src/app/api/_platform/migrations/0002__market_release.pg.sql +16 -0
  60. package/templates/src/app/api/_platform/migrations/0002__market_release.sql +33 -0
  61. package/templates/src/app/api/_platform/schema.yaml +18 -0
  62. package/templates/src/app/api/_platform/seed.mysql.sql +10 -0
  63. package/templates/src/app/api/_platform/seed.pg.sql +10 -0
  64. package/templates/src/app/api/auth/_shared/wecom/client.ts +24 -4
  65. package/templates/src/app/api/home/migrations/0001__create_home_tables.mysql.sql +14 -0
  66. package/templates/src/app/api/home/migrations/0001__create_home_tables.pg.sql +14 -0
  67. package/templates/src/app/api/home/migrations/0002__tenant.pg.sql +4 -0
  68. package/templates/src/app/api/home/seed.mysql.sql +71 -0
  69. package/templates/src/app/api/home/seed.pg.sql +71 -0
  70. package/templates/src/app/api/notification/_shared/notify.ts +6 -6
  71. package/templates/src/app/api/notification/migrations/0001__create_notifications.mysql.sql +11 -0
  72. package/templates/src/app/api/notification/migrations/0001__create_notifications.pg.sql +11 -0
  73. package/templates/src/app/api/notification/migrations/0002__notifications_user.mysql.sql +8 -0
  74. package/templates/src/app/api/notification/migrations/0003__notifications_broadcast.mysql.sql +21 -0
  75. package/templates/src/app/api/notification/migrations/0003__notifications_broadcast.pg.sql +22 -0
  76. package/templates/src/app/api/notification/migrations/0004__notifications_broadcast_hidden.mysql.sql +10 -0
  77. package/templates/src/app/api/notification/migrations/0004__notifications_broadcast_hidden.pg.sql +11 -0
  78. package/templates/src/app/api/notification/migrations/0005__tenant.pg.sql +6 -0
  79. package/templates/src/app/api/notification/seed.mysql.sql +8 -0
  80. package/templates/src/app/api/notification/seed.pg.sql +11 -0
  81. package/templates/src/app/api/publish/_shared/identity.ts +69 -0
  82. package/templates/src/app/api/publish/api.ts +349 -0
  83. package/templates/src/app/api/publish/manifest.yaml +8 -0
  84. package/templates/src/app/api/publish/migrations/0001__publish_credential.mysql.sql +23 -0
  85. package/templates/src/app/api/publish/migrations/0001__publish_credential.pg.sql +23 -0
  86. package/templates/src/app/api/publish/migrations/0001__publish_credential.sql +39 -0
  87. package/templates/src/app/api/publish/schema.yaml +33 -0
  88. package/templates/src/app/api/system/login-methods/api.ts +2 -1
  89. package/templates/src/app/web/_platform/__client/api.schemas.ts +15 -0
  90. package/templates/src/app/web/_platform/__client/index.ts +25 -1
  91. package/templates/src/app/web/entry.ts +9 -5
  92. package/templates/src/app/web/home/routes.ts +5 -0
  93. package/templates/src/app/web/login/locales/en-US.json +7 -1
  94. package/templates/src/app/web/login/locales/zh-CN.json +7 -1
  95. package/templates/src/app/web/login/pages/login.tsx +79 -86
  96. package/templates/src/app/web/login/routes.ts +54 -1
  97. package/templates/src/app/web/personal-center/locales/en-US.json +1 -0
  98. package/templates/src/app/web/personal-center/locales/zh-CN.json +1 -0
  99. package/templates/src/app/web/personal-center/pages/my-profile/index.tsx +5 -0
  100. package/templates/src/app/web/system/__client/api.schemas.ts +2 -1
  101. package/templates/src/app/web/system/locales/en-US.json +1 -8
  102. package/templates/src/app/web/system/locales/zh-CN.json +1 -8
  103. package/templates/src/app/web/system/pages/settings/index.tsx +17 -4
  104. package/templates/src/app/web/system/routes.ts +0 -6
  105. package/templates/src/oj-market/web/__tests__/CardGrid.test.tsx +33 -0
  106. package/templates/src/oj-market/web/__tests__/DetailDrawer.test.tsx +69 -0
  107. package/templates/src/oj-market/web/__tests__/marketClient.fake.ts +7 -1
  108. package/templates/src/oj-market/web/api/client.ts +3 -0
  109. package/templates/src/oj-market/web/api/defaultClient.ts +5 -0
  110. package/templates/src/oj-market/web/components/CardItem.tsx +44 -1
  111. package/templates/src/oj-market/web/components/DetailDrawer.tsx +17 -1
  112. package/templates/src/oj-market/web/entry.ts +31 -2
  113. package/templates/src/oj-market/web/locales/en-US.json +5 -1
  114. package/templates/src/oj-market/web/locales/zh-CN.json +5 -1
  115. package/templates/src/oj-market/web/publish/__client/api.schemas.ts +61 -0
  116. package/templates/src/oj-market/web/publish/__client/index.ts +164 -0
  117. package/templates/src/oj-market/web/publish/locales/en-US.json +80 -0
  118. package/templates/src/oj-market/web/publish/locales/zh-CN.json +80 -0
  119. package/templates/src/oj-market/web/publish/pages/history.tsx +112 -0
  120. package/templates/src/oj-market/web/publish/pages/index.tsx +196 -0
  121. package/templates/src/oj-market/web/publish/pages/model.ts +52 -0
  122. package/templates/src/oj-market/web/publish/pages/token-modal.tsx +82 -0
  123. package/templates/src/oj-market/web/publish/routes.ts +41 -0
  124. package/templates/src/oj-market/web/types.ts +30 -0
  125. package/templates/tests/lifecycle/compensate.spec.ts +207 -0
  126. package/templates/tests/lifecycle/deploy-smoke.spec.ts +38 -14
  127. package/templates/tests/lifecycle/installer.spec.ts +28 -9
  128. package/templates/tests/lifecycle/market-api.spec.ts +164 -12
  129. package/templates/tests/lifecycle/preflight.spec.ts +68 -0
  130. package/templates/tests/lifecycle/role-title-translate.spec.ts +2 -32
  131. package/templates/tests/lifecycle/text-edit.spec.ts +45 -27
  132. package/templates/tests/lifecycle/uninstall.spec.ts +36 -0
  133. package/templates/tests/publish-credential.spec.ts +354 -0
  134. package/templates/tests/publish-identity.spec.ts +44 -0
  135. package/templates/tests/wecom-client-timeout.spec.ts +33 -3
  136. package/templates/tests/wecom-landing.spec.ts +63 -1
  137. package/templates/tsconfig.paths.json +1 -0
  138. package/templates/web.config.ts +6 -4
  139. package/vendor/host-versions.json +3 -3
  140. package/templates/seed.mysql.sql +0 -6
  141. package/templates/seed.pg.sql +0 -6
  142. package/templates/src/app/api/_platform/migrations/0001__create_users.mysql.sql +0 -9
  143. package/templates/src/app/api/_platform/migrations/0001__create_users.pg.sql +0 -9
  144. package/templates/src/app/api/_platform/migrations/0001__create_users.sql +0 -7
  145. package/templates/src/app/api/_platform/migrations/0002__add_users_avatar_base64.mysql.sql +0 -3
  146. package/templates/src/app/api/_platform/migrations/0002__add_users_avatar_base64.sql +0 -2
  147. package/templates/src/app/api/_platform/migrations/0003__users_source_display_name.mysql.sql +0 -5
  148. package/templates/src/app/api/_platform/migrations/0003__users_source_display_name.sql +0 -5
  149. package/templates/src/app/api/_platform/migrations/0004__sys_rbac.mysql.sql +0 -70
  150. package/templates/src/app/api/_platform/migrations/0004__sys_rbac.pg.sql +0 -67
  151. package/templates/src/app/api/_platform/migrations/0004__sys_rbac.sql +0 -67
  152. package/templates/src/app/api/_platform/migrations/0005__users_must_change.mysql.sql +0 -3
  153. package/templates/src/app/api/_platform/migrations/0005__users_must_change.sql +0 -2
  154. package/templates/src/app/api/_platform/migrations/0006__users_is_forbidden.mysql.sql +0 -3
  155. package/templates/src/app/api/_platform/migrations/0006__users_is_forbidden.sql +0 -3
  156. package/templates/src/app/api/_platform/migrations/0007__users_email.sql +0 -3
  157. package/templates/src/app/api/_platform/migrations/0008__sys_refresh_token.mysql.sql +0 -10
  158. package/templates/src/app/api/_platform/migrations/0008__sys_refresh_token.pg.sql +0 -10
  159. package/templates/src/app/api/_platform/migrations/0008__sys_refresh_token.sql +0 -11
  160. package/templates/src/app/api/_platform/migrations/0009__sys_throttle.mysql.sql +0 -12
  161. package/templates/src/app/api/_platform/migrations/0009__sys_throttle.pg.sql +0 -12
  162. package/templates/src/app/api/_platform/migrations/0009__sys_throttle.sql +0 -13
  163. package/templates/src/app/api/_platform/migrations/0010__sys_solution_route.mysql.sql +0 -17
  164. package/templates/src/app/api/_platform/migrations/0010__sys_solution_route.pg.sql +0 -16
  165. package/templates/src/app/api/_platform/migrations/0010__sys_solution_route.sql +0 -18
  166. package/templates/src/app/api/_platform/migrations/0011__sys_setting_group_label.mysql.sql +0 -2
  167. package/templates/src/app/api/_platform/migrations/0011__sys_setting_group_label.sql +0 -4
  168. package/templates/src/app/api/_platform/migrations/0012__market_history.mysql.sql +0 -13
  169. package/templates/src/app/api/_platform/migrations/0012__market_history.pg.sql +0 -12
  170. package/templates/src/app/api/_platform/migrations/0012__market_history.sql +0 -14
  171. package/templates/src/app/api/_platform/migrations/0013__users_profile.sql +0 -4
  172. package/templates/src/app/api/_platform/migrations/0014__reg_source_seq.mysql.sql +0 -9
  173. package/templates/src/app/api/_platform/migrations/0014__reg_source_seq.pg.sql +0 -7
  174. package/templates/src/app/api/_platform/migrations/0014__reg_source_seq.sql +0 -8
  175. package/templates/src/app/api/_platform/migrations/0015__tenant.mysql.sql +0 -12
  176. package/templates/src/app/api/_platform/migrations/0015__tenant.pg.sql +0 -11
  177. package/templates/src/app/api/_platform/migrations/0015__tenant.sql +0 -44
  178. package/templates/src/app/api/_platform/migrations/0016__org.mysql.sql +0 -41
  179. package/templates/src/app/api/_platform/migrations/0016__org.pg.sql +0 -40
  180. package/templates/src/app/api/_platform/migrations/0016__org.sql +0 -62
  181. package/templates/src/app/api/_platform/migrations/0017__users_employee_no.mysql.sql +0 -7
  182. package/templates/src/app/api/_platform/migrations/0017__users_employee_no.pg.sql +0 -4
  183. package/templates/src/app/api/_platform/migrations/0017__users_employee_no.sql +0 -8
  184. package/templates/src/app/api/_platform/migrations/0018__sys_role_title_key.mysql.sql +0 -3
  185. package/templates/src/app/api/_platform/migrations/0018__sys_role_title_key.pg.sql +0 -3
  186. package/templates/src/app/api/_platform/migrations/0018__sys_role_title_key.sql +0 -8
  187. package/templates/src/app/web/system/pages/login-methods/index.tsx +0 -30
@@ -15,6 +15,8 @@
15
15
 
16
16
  const API_BASE = "https://qyapi.weixin.qq.com";
17
17
  const TIMEOUT_MS = 5000;
18
+ /** 软超时的检查步长:主链路一结束,最坏再挂一个步长就收尾 */
19
+ const TIMEOUT_STEP_MS = 50;
18
20
  const TOKEN_SKEW_SECS = 300;
19
21
 
20
22
  export interface WecomHttp {
@@ -37,6 +39,14 @@ export interface WecomClientDeps {
37
39
  * 软超时:服务端 isolate 无 `setTimeout` / `AbortSignal` 全局,不能用 `AbortSignal.timeout`。
38
40
  * 这里用运行时注入的 `tasks.sleep(ms)` 起一个「到点就 reject」的竞态;fetch 不被真正 abort,
39
41
  * 只是不再等它(后台连接最终由 OS/池回收)。`tasks` 不可用时降级为原 promise(无超时)。
42
+ *
43
+ * **必须短步长**:oj 运行时要等请求上下文里所有未完成的异步任务(含 `tasks.sleep`)
44
+ * 结束才 flush 响应。早期实现挂一条 `sleep(5000)` 做竞态,上游 100ms 就返回时那条
45
+ * sleep 仍在飞,于是每个调企微接口的请求都被白拖到 5s 才发得出响应——登录回调
46
+ * `/api/auth/wecom/callback` 实测恒定 5011ms,正是企微工作台进入 5-6s 白屏的来源。
47
+ * 改成按 `TIMEOUT_STEP_MS` 逐段 sleep 并检查 settled 后,主链路一结束、最多再挂
48
+ * 一个步长即收尾(<=50ms),超时语义不变。
49
+ *
40
50
  * 输家 promise 在 race 结束后仍可能 reject,挂 `.catch` 防 unhandled rejection。
41
51
  */
42
52
  function withSoftTimeout<T>(p: Promise<T>, ms: number): Promise<T> {
@@ -44,11 +54,21 @@ function withSoftTimeout<T>(p: Promise<T>, ms: number): Promise<T> {
44
54
  const sleep = g.tasks?.sleep;
45
55
  if (typeof sleep !== "function")
46
56
  return p;
47
- const timeout = sleep(ms).then(
48
- () => { throw new Error(`超时 ${ms}ms`); },
49
- () => undefined as never, // sleep 自身异常不中断主链路(返回 never 以免 race 结果退化为 void | T)
50
- );
57
+ let settled = false;
58
+ const deadline = Date.now() + ms;
59
+ const timeout = (async () => {
60
+ for (;;) {
61
+ const left = deadline - Date.now();
62
+ if (settled || left <= 0)
63
+ break;
64
+ await sleep(Math.min(left, TIMEOUT_STEP_MS));
65
+ }
66
+ if (!settled)
67
+ throw new Error(`超时 ${ms}ms`);
68
+ return undefined as never;
69
+ })();
51
70
  return Promise.race([p, timeout]).finally(() => {
71
+ settled = true;
52
72
  // 输家 promise 在 race 结束后仍可能 reject,挂 catch 防 unhandled rejection
53
73
  timeout.catch(() => {});
54
74
  });
@@ -0,0 +1,14 @@
1
+ -- 0001(mysql 方言):home 两表建表。口径同基础件。
2
+ -- 方言差异:AUTOINCREMENT → BIGINT AUTO_INCREMENT;TEXT 不能带字面默认值 → 收窄 VARCHAR(255)。
3
+ CREATE TABLE IF NOT EXISTS home_pie (
4
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
5
+ category VARCHAR(255) NOT NULL DEFAULT '',
6
+ value INTEGER NOT NULL DEFAULT 0
7
+ );
8
+
9
+ CREATE TABLE IF NOT EXISTS home_line (
10
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
11
+ day INTEGER NOT NULL DEFAULT 0,
12
+ value INTEGER NOT NULL DEFAULT 0
13
+ );
14
+
@@ -0,0 +1,14 @@
1
+ -- 0001(postgres 方言):home 两表建表。口径同基础件。
2
+ -- 方言差异:AUTOINCREMENT → BIGSERIAL(SERIAL 系允许 seed 显式写 id);TEXT 在 pg 合法且可带默认值。
3
+ CREATE TABLE IF NOT EXISTS home_pie (
4
+ id BIGSERIAL PRIMARY KEY,
5
+ category TEXT NOT NULL DEFAULT '',
6
+ value INTEGER NOT NULL DEFAULT 0
7
+ );
8
+
9
+ CREATE TABLE IF NOT EXISTS home_line (
10
+ id BIGSERIAL PRIMARY KEY,
11
+ day INTEGER NOT NULL DEFAULT 0,
12
+ value INTEGER NOT NULL DEFAULT 0
13
+ );
14
+
@@ -0,0 +1,4 @@
1
+ -- 0002__tenant(postgres 方言):首页图表两表租户化。TEXT 在 pg 合法,与基础件同语法。
2
+ ALTER TABLE home_pie ADD COLUMN tenant_id TEXT NOT NULL DEFAULT 'default';
3
+ ALTER TABLE home_line ADD COLUMN tenant_id TEXT NOT NULL DEFAULT 'default';
4
+
@@ -0,0 +1,71 @@
1
+ -- 方言覆盖(mysql):INSERT OR IGNORE → INSERT IGNORE,语句与基础件逐条对应。
2
+ -- home 模块种子(P4-1)。按分号朴素切分(S006)。
3
+ -- home_pie:五分类(code 对齐前端 DATA_KEY),幂等 INSERT OR IGNORE。
4
+ INSERT IGNORE INTO home_pie (category, value) VALUES ('electronics', 320);
5
+ INSERT IGNORE INTO home_pie (category, value) VALUES ('home_goods', 210);
6
+ INSERT IGNORE INTO home_pie (category, value) VALUES ('apparel_accessories', 180);
7
+ INSERT IGNORE INTO home_pie (category, value) VALUES ('food_beverages', 150);
8
+ INSERT IGNORE INTO home_pie (category, value) VALUES ('beauty_skincare', 95);
9
+
10
+ -- home_line:日窗口序列(day = epoch-day,种子围绕"当前日期"生成,约 60 天窗口)。
11
+ -- 刻意缺 today-30 / today-12 两天,演示"缺哪天补哪天"——handler 在窗口内对缺失日补 0;
12
+ -- 若整个窗口无 seed(seed 随时间变旧),handler 回落确定式趋势兜底(懒补数)。
13
+ INSERT IGNORE INTO home_line (day, value) VALUES (20640, 181);
14
+ INSERT IGNORE INTO home_line (day, value) VALUES (20641, 190);
15
+ INSERT IGNORE INTO home_line (day, value) VALUES (20642, 199);
16
+ INSERT IGNORE INTO home_line (day, value) VALUES (20643, 208);
17
+ INSERT IGNORE INTO home_line (day, value) VALUES (20644, 217);
18
+ INSERT IGNORE INTO home_line (day, value) VALUES (20645, 226);
19
+ INSERT IGNORE INTO home_line (day, value) VALUES (20646, 235);
20
+ INSERT IGNORE INTO home_line (day, value) VALUES (20647, 244);
21
+ INSERT IGNORE INTO home_line (day, value) VALUES (20648, 253);
22
+ INSERT IGNORE INTO home_line (day, value) VALUES (20649, 262);
23
+ INSERT IGNORE INTO home_line (day, value) VALUES (20650, 271);
24
+ INSERT IGNORE INTO home_line (day, value) VALUES (20651, 280);
25
+ INSERT IGNORE INTO home_line (day, value) VALUES (20652, 289);
26
+ INSERT IGNORE INTO home_line (day, value) VALUES (20653, 298);
27
+ INSERT IGNORE INTO home_line (day, value) VALUES (20654, 100);
28
+ INSERT IGNORE INTO home_line (day, value) VALUES (20655, 109);
29
+ INSERT IGNORE INTO home_line (day, value) VALUES (20656, 118);
30
+ INSERT IGNORE INTO home_line (day, value) VALUES (20657, 127);
31
+ INSERT IGNORE INTO home_line (day, value) VALUES (20658, 136);
32
+ INSERT IGNORE INTO home_line (day, value) VALUES (20659, 145);
33
+ INSERT IGNORE INTO home_line (day, value) VALUES (20660, 154);
34
+ INSERT IGNORE INTO home_line (day, value) VALUES (20661, 163);
35
+ INSERT IGNORE INTO home_line (day, value) VALUES (20662, 172);
36
+ INSERT IGNORE INTO home_line (day, value) VALUES (20663, 181);
37
+ INSERT IGNORE INTO home_line (day, value) VALUES (20664, 190);
38
+ INSERT IGNORE INTO home_line (day, value) VALUES (20665, 199);
39
+ INSERT IGNORE INTO home_line (day, value) VALUES (20666, 208);
40
+ INSERT IGNORE INTO home_line (day, value) VALUES (20667, 217);
41
+ INSERT IGNORE INTO home_line (day, value) VALUES (20668, 226);
42
+ INSERT IGNORE INTO home_line (day, value) VALUES (20669, 235);
43
+ INSERT IGNORE INTO home_line (day, value) VALUES (20671, 253);
44
+ INSERT IGNORE INTO home_line (day, value) VALUES (20672, 262);
45
+ INSERT IGNORE INTO home_line (day, value) VALUES (20673, 271);
46
+ INSERT IGNORE INTO home_line (day, value) VALUES (20674, 280);
47
+ INSERT IGNORE INTO home_line (day, value) VALUES (20675, 289);
48
+ INSERT IGNORE INTO home_line (day, value) VALUES (20676, 298);
49
+ INSERT IGNORE INTO home_line (day, value) VALUES (20677, 100);
50
+ INSERT IGNORE INTO home_line (day, value) VALUES (20678, 109);
51
+ INSERT IGNORE INTO home_line (day, value) VALUES (20679, 118);
52
+ INSERT IGNORE INTO home_line (day, value) VALUES (20680, 127);
53
+ INSERT IGNORE INTO home_line (day, value) VALUES (20681, 136);
54
+ INSERT IGNORE INTO home_line (day, value) VALUES (20682, 145);
55
+ INSERT IGNORE INTO home_line (day, value) VALUES (20683, 154);
56
+ INSERT IGNORE INTO home_line (day, value) VALUES (20684, 163);
57
+ INSERT IGNORE INTO home_line (day, value) VALUES (20685, 172);
58
+ INSERT IGNORE INTO home_line (day, value) VALUES (20686, 181);
59
+ INSERT IGNORE INTO home_line (day, value) VALUES (20687, 190);
60
+ INSERT IGNORE INTO home_line (day, value) VALUES (20689, 208);
61
+ INSERT IGNORE INTO home_line (day, value) VALUES (20690, 217);
62
+ INSERT IGNORE INTO home_line (day, value) VALUES (20691, 226);
63
+ INSERT IGNORE INTO home_line (day, value) VALUES (20692, 235);
64
+ INSERT IGNORE INTO home_line (day, value) VALUES (20693, 244);
65
+ INSERT IGNORE INTO home_line (day, value) VALUES (20694, 253);
66
+ INSERT IGNORE INTO home_line (day, value) VALUES (20695, 262);
67
+ INSERT IGNORE INTO home_line (day, value) VALUES (20696, 271);
68
+ INSERT IGNORE INTO home_line (day, value) VALUES (20697, 280);
69
+ INSERT IGNORE INTO home_line (day, value) VALUES (20698, 289);
70
+ INSERT IGNORE INTO home_line (day, value) VALUES (20699, 298);
71
+ INSERT IGNORE INTO home_line (day, value) VALUES (20700, 100);
@@ -0,0 +1,71 @@
1
+ -- 方言覆盖(postgres):INSERT OR IGNORE → ON CONFLICT DO NOTHING,语句与基础件逐条对应。
2
+ -- home 模块种子(P4-1)。按分号朴素切分(S006)。
3
+ -- home_pie:五分类(code 对齐前端 DATA_KEY),幂等 INSERT OR IGNORE。
4
+ INSERT INTO home_pie (category, value) VALUES ('electronics', 320) ON CONFLICT DO NOTHING;
5
+ INSERT INTO home_pie (category, value) VALUES ('home_goods', 210) ON CONFLICT DO NOTHING;
6
+ INSERT INTO home_pie (category, value) VALUES ('apparel_accessories', 180) ON CONFLICT DO NOTHING;
7
+ INSERT INTO home_pie (category, value) VALUES ('food_beverages', 150) ON CONFLICT DO NOTHING;
8
+ INSERT INTO home_pie (category, value) VALUES ('beauty_skincare', 95) ON CONFLICT DO NOTHING;
9
+
10
+ -- home_line:日窗口序列(day = epoch-day,种子围绕"当前日期"生成,约 60 天窗口)。
11
+ -- 刻意缺 today-30 / today-12 两天,演示"缺哪天补哪天"——handler 在窗口内对缺失日补 0;
12
+ -- 若整个窗口无 seed(seed 随时间变旧),handler 回落确定式趋势兜底(懒补数)。
13
+ INSERT INTO home_line (day, value) VALUES (20640, 181) ON CONFLICT DO NOTHING;
14
+ INSERT INTO home_line (day, value) VALUES (20641, 190) ON CONFLICT DO NOTHING;
15
+ INSERT INTO home_line (day, value) VALUES (20642, 199) ON CONFLICT DO NOTHING;
16
+ INSERT INTO home_line (day, value) VALUES (20643, 208) ON CONFLICT DO NOTHING;
17
+ INSERT INTO home_line (day, value) VALUES (20644, 217) ON CONFLICT DO NOTHING;
18
+ INSERT INTO home_line (day, value) VALUES (20645, 226) ON CONFLICT DO NOTHING;
19
+ INSERT INTO home_line (day, value) VALUES (20646, 235) ON CONFLICT DO NOTHING;
20
+ INSERT INTO home_line (day, value) VALUES (20647, 244) ON CONFLICT DO NOTHING;
21
+ INSERT INTO home_line (day, value) VALUES (20648, 253) ON CONFLICT DO NOTHING;
22
+ INSERT INTO home_line (day, value) VALUES (20649, 262) ON CONFLICT DO NOTHING;
23
+ INSERT INTO home_line (day, value) VALUES (20650, 271) ON CONFLICT DO NOTHING;
24
+ INSERT INTO home_line (day, value) VALUES (20651, 280) ON CONFLICT DO NOTHING;
25
+ INSERT INTO home_line (day, value) VALUES (20652, 289) ON CONFLICT DO NOTHING;
26
+ INSERT INTO home_line (day, value) VALUES (20653, 298) ON CONFLICT DO NOTHING;
27
+ INSERT INTO home_line (day, value) VALUES (20654, 100) ON CONFLICT DO NOTHING;
28
+ INSERT INTO home_line (day, value) VALUES (20655, 109) ON CONFLICT DO NOTHING;
29
+ INSERT INTO home_line (day, value) VALUES (20656, 118) ON CONFLICT DO NOTHING;
30
+ INSERT INTO home_line (day, value) VALUES (20657, 127) ON CONFLICT DO NOTHING;
31
+ INSERT INTO home_line (day, value) VALUES (20658, 136) ON CONFLICT DO NOTHING;
32
+ INSERT INTO home_line (day, value) VALUES (20659, 145) ON CONFLICT DO NOTHING;
33
+ INSERT INTO home_line (day, value) VALUES (20660, 154) ON CONFLICT DO NOTHING;
34
+ INSERT INTO home_line (day, value) VALUES (20661, 163) ON CONFLICT DO NOTHING;
35
+ INSERT INTO home_line (day, value) VALUES (20662, 172) ON CONFLICT DO NOTHING;
36
+ INSERT INTO home_line (day, value) VALUES (20663, 181) ON CONFLICT DO NOTHING;
37
+ INSERT INTO home_line (day, value) VALUES (20664, 190) ON CONFLICT DO NOTHING;
38
+ INSERT INTO home_line (day, value) VALUES (20665, 199) ON CONFLICT DO NOTHING;
39
+ INSERT INTO home_line (day, value) VALUES (20666, 208) ON CONFLICT DO NOTHING;
40
+ INSERT INTO home_line (day, value) VALUES (20667, 217) ON CONFLICT DO NOTHING;
41
+ INSERT INTO home_line (day, value) VALUES (20668, 226) ON CONFLICT DO NOTHING;
42
+ INSERT INTO home_line (day, value) VALUES (20669, 235) ON CONFLICT DO NOTHING;
43
+ INSERT INTO home_line (day, value) VALUES (20671, 253) ON CONFLICT DO NOTHING;
44
+ INSERT INTO home_line (day, value) VALUES (20672, 262) ON CONFLICT DO NOTHING;
45
+ INSERT INTO home_line (day, value) VALUES (20673, 271) ON CONFLICT DO NOTHING;
46
+ INSERT INTO home_line (day, value) VALUES (20674, 280) ON CONFLICT DO NOTHING;
47
+ INSERT INTO home_line (day, value) VALUES (20675, 289) ON CONFLICT DO NOTHING;
48
+ INSERT INTO home_line (day, value) VALUES (20676, 298) ON CONFLICT DO NOTHING;
49
+ INSERT INTO home_line (day, value) VALUES (20677, 100) ON CONFLICT DO NOTHING;
50
+ INSERT INTO home_line (day, value) VALUES (20678, 109) ON CONFLICT DO NOTHING;
51
+ INSERT INTO home_line (day, value) VALUES (20679, 118) ON CONFLICT DO NOTHING;
52
+ INSERT INTO home_line (day, value) VALUES (20680, 127) ON CONFLICT DO NOTHING;
53
+ INSERT INTO home_line (day, value) VALUES (20681, 136) ON CONFLICT DO NOTHING;
54
+ INSERT INTO home_line (day, value) VALUES (20682, 145) ON CONFLICT DO NOTHING;
55
+ INSERT INTO home_line (day, value) VALUES (20683, 154) ON CONFLICT DO NOTHING;
56
+ INSERT INTO home_line (day, value) VALUES (20684, 163) ON CONFLICT DO NOTHING;
57
+ INSERT INTO home_line (day, value) VALUES (20685, 172) ON CONFLICT DO NOTHING;
58
+ INSERT INTO home_line (day, value) VALUES (20686, 181) ON CONFLICT DO NOTHING;
59
+ INSERT INTO home_line (day, value) VALUES (20687, 190) ON CONFLICT DO NOTHING;
60
+ INSERT INTO home_line (day, value) VALUES (20689, 208) ON CONFLICT DO NOTHING;
61
+ INSERT INTO home_line (day, value) VALUES (20690, 217) ON CONFLICT DO NOTHING;
62
+ INSERT INTO home_line (day, value) VALUES (20691, 226) ON CONFLICT DO NOTHING;
63
+ INSERT INTO home_line (day, value) VALUES (20692, 235) ON CONFLICT DO NOTHING;
64
+ INSERT INTO home_line (day, value) VALUES (20693, 244) ON CONFLICT DO NOTHING;
65
+ INSERT INTO home_line (day, value) VALUES (20694, 253) ON CONFLICT DO NOTHING;
66
+ INSERT INTO home_line (day, value) VALUES (20695, 262) ON CONFLICT DO NOTHING;
67
+ INSERT INTO home_line (day, value) VALUES (20696, 271) ON CONFLICT DO NOTHING;
68
+ INSERT INTO home_line (day, value) VALUES (20697, 280) ON CONFLICT DO NOTHING;
69
+ INSERT INTO home_line (day, value) VALUES (20698, 289) ON CONFLICT DO NOTHING;
70
+ INSERT INTO home_line (day, value) VALUES (20699, 298) ON CONFLICT DO NOTHING;
71
+ INSERT INTO home_line (day, value) VALUES (20700, 100) ON CONFLICT DO NOTHING;
@@ -69,11 +69,9 @@ function splitId(id: string): [scope: "u" | "b", raw: string] {
69
69
  function mapItem(r: Row): NoticeItem {
70
70
  const scope = String(r.scope);
71
71
  const isBroadcast = scope === "broadcast";
72
- // UNION 结果列名取首分支(userQ)=> 位置 4 名为 is_read。按人项此处即存储整数列;
73
- // 广播项此处实际承载回执 read_time(非空即已读,与原始 SQL 的 (read_time IS NOT NULL AND <> '') 等价)。
74
- const isRead = isBroadcast
75
- ? (r.is_read != null && String(r.is_read) !== "")
76
- : Number(r.is_read) === 1;
72
+ // UNION 结果列名取首分支(userQ)=> 位置 4 名为 is_read,两分支都输出整数 0/1
73
+ // (按人项取存储列,广播项由回执 EXISTS 推导),故统一按整数判已读。
74
+ const isRead = Number(r.is_read) === 1;
77
75
  return {
78
76
  id: (isBroadcast ? "b:" : "u:") + String(r.id),
79
77
  scope: isBroadcast ? "broadcast" : "user",
@@ -172,7 +170,9 @@ export async function noticesFor(
172
170
  "b.id",
173
171
  "b.avatar",
174
172
  "b.date",
175
- "r.read_time",
173
+ // 第 4 列与首分支 is_read 同型(整数):pg 的 UNION 不允许 integer/text 混排,
174
+ // sqlite/mysql 会隐式放宽;回执存在即已读 → 0/1,mapItem 统一按 Number(...) === 1 判定。
175
+ { case: { when: [{ cond: { field: "r.read_time", op: "isnull" }, then: 0 }], else: 1 }, as: "is_read" },
176
176
  "b.message",
177
177
  "b.title",
178
178
  "b.link",
@@ -0,0 +1,11 @@
1
+ -- 0001(mysql 方言):通知表建表。口径同基础件。
2
+ -- 方言差异:AUTOINCREMENT → BIGINT AUTO_INCREMENT;TEXT 不能带字面默认值 → 短文本收窄 VARCHAR,长文本用括号默认值。
3
+ CREATE TABLE IF NOT EXISTS notifications (
4
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
5
+ avatar VARCHAR(255) NOT NULL DEFAULT '',
6
+ `date` VARCHAR(64) NOT NULL DEFAULT '',
7
+ is_read INTEGER NOT NULL DEFAULT 0,
8
+ message TEXT NOT NULL DEFAULT (''),
9
+ title VARCHAR(255) NOT NULL DEFAULT ''
10
+ );
11
+
@@ -0,0 +1,11 @@
1
+ -- 0001(postgres 方言):通知表建表。口径同基础件。
2
+ -- 方言差异:AUTOINCREMENT → BIGSERIAL(SERIAL 系允许 seed 显式写 id);TEXT 在 pg 合法且可带默认值。
3
+ CREATE TABLE IF NOT EXISTS notifications (
4
+ id BIGSERIAL PRIMARY KEY,
5
+ avatar TEXT NOT NULL DEFAULT '',
6
+ date TEXT NOT NULL DEFAULT '',
7
+ is_read INTEGER NOT NULL DEFAULT 0,
8
+ message TEXT NOT NULL DEFAULT '',
9
+ title TEXT NOT NULL DEFAULT ''
10
+ );
11
+
@@ -0,0 +1,8 @@
1
+ -- 0002(mysql 方言):按人真实通知表演进。
2
+ -- 方言差异:TEXT 不能带字面默认值 → category 收窄 VARCHAR(255);MySQL 不支持 CREATE INDEX IF NOT EXISTS,直接建索引。
3
+ ALTER TABLE notifications ADD COLUMN user_id INTEGER;
4
+ ALTER TABLE notifications ADD COLUMN link TEXT;
5
+ ALTER TABLE notifications ADD COLUMN category VARCHAR(255) NOT NULL DEFAULT 'notice';
6
+ CREATE INDEX idx_notifications_user ON notifications (user_id, is_read);
7
+ UPDATE notifications SET user_id = 1, category = 'notice' WHERE user_id IS NULL;
8
+
@@ -0,0 +1,21 @@
1
+ -- 0003(mysql 方言):广播表 + 已读回执表。口径同基础件。
2
+ -- 方言差异:AUTOINCREMENT → BIGINT AUTO_INCREMENT;CREATE INDEX IF NOT EXISTS 不被 MySQL 支持,索引写进建表语句。
3
+ CREATE TABLE IF NOT EXISTS notifications_broadcast (
4
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
5
+ avatar VARCHAR(255) NOT NULL DEFAULT '',
6
+ `date` VARCHAR(64) NOT NULL DEFAULT '',
7
+ message TEXT NOT NULL DEFAULT (''),
8
+ title VARCHAR(255) NOT NULL DEFAULT '',
9
+ link TEXT,
10
+ category VARCHAR(255) NOT NULL DEFAULT 'notice',
11
+ create_time VARCHAR(64) NOT NULL DEFAULT ''
12
+ );
13
+
14
+ CREATE TABLE IF NOT EXISTS notifications_broadcast_read (
15
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
16
+ broadcast_id BIGINT NOT NULL,
17
+ user_id BIGINT NOT NULL,
18
+ read_time VARCHAR(64) NOT NULL DEFAULT '',
19
+ KEY idx_broadcast_read (user_id, broadcast_id)
20
+ );
21
+
@@ -0,0 +1,22 @@
1
+ -- 0003(postgres 方言):广播表 + 已读回执表。口径同基础件。
2
+ -- 方言差异:AUTOINCREMENT → BIGSERIAL(SERIAL 系允许 seed 显式写 id);其余 DDL 与基础件同语法。
3
+ CREATE TABLE IF NOT EXISTS notifications_broadcast (
4
+ id BIGSERIAL PRIMARY KEY,
5
+ avatar TEXT NOT NULL DEFAULT '',
6
+ date TEXT NOT NULL DEFAULT '',
7
+ message TEXT NOT NULL DEFAULT '',
8
+ title TEXT NOT NULL DEFAULT '',
9
+ link TEXT,
10
+ category TEXT NOT NULL DEFAULT 'notice',
11
+ create_time TEXT NOT NULL DEFAULT ''
12
+ );
13
+
14
+ CREATE TABLE IF NOT EXISTS notifications_broadcast_read (
15
+ id BIGSERIAL PRIMARY KEY,
16
+ broadcast_id BIGINT NOT NULL,
17
+ user_id BIGINT NOT NULL,
18
+ read_time TEXT NOT NULL DEFAULT ''
19
+ );
20
+
21
+ CREATE INDEX IF NOT EXISTS idx_broadcast_read ON notifications_broadcast_read (user_id, broadcast_id);
22
+
@@ -0,0 +1,10 @@
1
+ -- 0004(mysql 方言):广播隐藏标记表。口径同基础件。
2
+ -- 方言差异:AUTOINCREMENT → BIGINT AUTO_INCREMENT;CREATE INDEX IF NOT EXISTS 不被 MySQL 支持,索引写进建表语句。
3
+ CREATE TABLE IF NOT EXISTS notifications_broadcast_hidden (
4
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
5
+ broadcast_id BIGINT NOT NULL,
6
+ user_id BIGINT NOT NULL,
7
+ create_time VARCHAR(64) NOT NULL DEFAULT '',
8
+ KEY idx_broadcast_hidden (user_id, broadcast_id)
9
+ );
10
+
@@ -0,0 +1,11 @@
1
+ -- 0004(postgres 方言):广播隐藏标记表。口径同基础件。
2
+ -- 方言差异:AUTOINCREMENT → BIGSERIAL(SERIAL 系允许 seed 显式写 id);其余 DDL 与基础件同语法。
3
+ CREATE TABLE IF NOT EXISTS notifications_broadcast_hidden (
4
+ id BIGSERIAL PRIMARY KEY,
5
+ broadcast_id BIGINT NOT NULL,
6
+ user_id BIGINT NOT NULL,
7
+ create_time TEXT NOT NULL DEFAULT ''
8
+ );
9
+
10
+ CREATE INDEX IF NOT EXISTS idx_broadcast_hidden ON notifications_broadcast_hidden (user_id, broadcast_id);
11
+
@@ -0,0 +1,6 @@
1
+ -- 0005__tenant(postgres 方言):通知四表租户化。TEXT 在 pg 合法,与基础件同语法。
2
+ ALTER TABLE notifications ADD COLUMN tenant_id TEXT NOT NULL DEFAULT 'default';
3
+ ALTER TABLE notifications_broadcast ADD COLUMN tenant_id TEXT NOT NULL DEFAULT 'default';
4
+ ALTER TABLE notifications_broadcast_read ADD COLUMN tenant_id TEXT NOT NULL DEFAULT 'default';
5
+ ALTER TABLE notifications_broadcast_hidden ADD COLUMN tenant_id TEXT NOT NULL DEFAULT 'default';
6
+
@@ -0,0 +1,8 @@
1
+ -- 方言覆盖(mysql):INSERT OR IGNORE → INSERT IGNORE,语句与基础件逐条对应。
2
+ -- notification 模块种子(按人真实通知)。user_id=1(admin),link/category 幂等可空/默认。
3
+ -- avatar 给真实 URL:铃铛按 avatar 渲染头像,空串会破图(空串仅作为运行时兜底,不作为种子值)。
4
+ -- 按分号朴素切分(S006):语句内不得含分号字面量。
5
+ INSERT IGNORE INTO notifications (id, user_id, avatar, date, is_read, message, title, link, category) VALUES (1, 1, 'https://avatar.vercel.sh/seed-welcome.svg?text=W', '2026-09-01 09:12', 0, '脚手架已就绪,用 admin / aA123456 登录开始', '欢迎', NULL, 'notice');
6
+ INSERT IGNORE INTO notifications (id, user_id, avatar, date, is_read, message, title, link, category) VALUES (2, 1, 'https://avatar.vercel.sh/seed-bell.svg?text=B', '2026-09-02 14:30', 0, '通知铃支持单条已读、全部已读与清空;广播经 push-broadcast 入 b: 命名空间', '通知铃', NULL, 'notice');
7
+ INSERT IGNORE INTO notifications (id, user_id, avatar, date, is_read, message, title, link, category) VALUES (3, 1, 'https://avatar.vercel.sh/seed-contract.svg?text=C', '2026-09-03 18:45', 1, '契约改动后重跑 ojm api 生成前端 client 与文档', '契约', NULL, 'notice');
8
+ INSERT IGNORE INTO notifications (id, user_id, avatar, date, is_read, message, title, link, category) VALUES (4, 1, 'https://avatar.vercel.sh/seed-dev.svg?text=D', '2026-09-04 08:00', 0, '编辑 api/src 下的 api.ts 保存即热更;handler 组支持四层嵌套(oj v0.1.64)', '开发提示', NULL, 'notice');
@@ -0,0 +1,11 @@
1
+ -- 方言覆盖(postgres):INSERT OR IGNORE → ON CONFLICT DO NOTHING,语句与基础件逐条对应。
2
+ -- notification 模块种子(按人真实通知)。user_id=1(admin),link/category 幂等可空/默认。
3
+ -- avatar 给真实 URL:铃铛按 avatar 渲染头像,空串会破图(空串仅作为运行时兜底,不作为种子值)。
4
+ -- 按分号朴素切分(S006):语句内不得含分号字面量。
5
+ INSERT INTO notifications (id, user_id, avatar, date, is_read, message, title, link, category) VALUES (1, 1, 'https://avatar.vercel.sh/seed-welcome.svg?text=W', '2026-09-01 09:12', 0, '脚手架已就绪,用 admin / aA123456 登录开始', '欢迎', NULL, 'notice') ON CONFLICT DO NOTHING;
6
+ INSERT INTO notifications (id, user_id, avatar, date, is_read, message, title, link, category) VALUES (2, 1, 'https://avatar.vercel.sh/seed-bell.svg?text=B', '2026-09-02 14:30', 0, '通知铃支持单条已读、全部已读与清空;广播经 push-broadcast 入 b: 命名空间', '通知铃', NULL, 'notice') ON CONFLICT DO NOTHING;
7
+ INSERT INTO notifications (id, user_id, avatar, date, is_read, message, title, link, category) VALUES (3, 1, 'https://avatar.vercel.sh/seed-contract.svg?text=C', '2026-09-03 18:45', 1, '契约改动后重跑 ojm api 生成前端 client 与文档', '契约', NULL, 'notice') ON CONFLICT DO NOTHING;
8
+ INSERT INTO notifications (id, user_id, avatar, date, is_read, message, title, link, category) VALUES (4, 1, 'https://avatar.vercel.sh/seed-dev.svg?text=D', '2026-09-04 08:00', 0, '编辑 api/src 下的 api.ts 保存即热更;handler 组支持四层嵌套(oj v0.1.64)', '开发提示', NULL, 'notice') ON CONFLICT DO NOTHING;
9
+
10
+ -- 显式写 id 不会推进 BIGSERIAL 序列,补一次 setval 免新增行撞主键(幂等,每次启动按当前最大 id 校正)。
11
+ SELECT setval(pg_get_serial_sequence('notifications', 'id'), GREATEST((SELECT MAX(id) FROM notifications), 1));
@@ -0,0 +1,69 @@
1
+ // 发布身份的推导规则 —— **唯一真源**。
2
+ //
3
+ // 为什么只在服务端算:`key_id` 直接决定 `~/.ojm/keys/<id>/` 的目录名与签名身份,
4
+ // 同一规则若在 `packages/cli` 与模板工程(互不可 import 的两棵树)各写一份,必然漂移,
5
+ // 表现为「CLI 建了 demo-solution.wecom-everpan,服务端记录成 demo-solution.wecom-everpan-x」。
6
+ // 故 key_id 在**申请时**由本文件算好落库,兑换接口原样下发,CLI 零推导。
7
+ //
8
+ // 规则与约束:
9
+ // · 产品名 = 解决方案名,遵循 NAME_RE(小写字母开头,仅 [a-z0-9-],≤64)
10
+ // · 人名段 = username 归一化:小写 → 非 [a-z0-9-] 一律替换为 '-' → 折叠连续 '-' → 去首尾 '-'
11
+ // (`wecom:everpan` → `wecom-everpan`,目录名不出现冒号;比对也无需回到裸人名)
12
+ // · key_id = `<product>.<person>`,必须满足 C2-37 的 key_id 形状
13
+ // · 归一化后为空(例如纯中文用户名)→ 退化为 `u<uid>`,保证仍满足 key_id 形状
14
+
15
+ /** C2-37 key_id 形状:小写、≥2 段、仅 [a-z0-9-](与 packages/cli/src/keygen.ts 的 isValidKeyId 同规则) */
16
+ export const KEY_ID_RE = /^[a-z][a-z0-9-]*(?:\.[a-z0-9-]+)+$/;
17
+
18
+ /** 解决方案名规则(与 packages/cli/src/manifest-contract.ts 的 NAME_RE 同规则) */
19
+ export const PRODUCT_RE = /^[a-z][a-z0-9-]{0,63}$/;
20
+
21
+ /**
22
+ * 保留名:产品名占用它们会让人误以为是框架/平台自身发布的包。
23
+ * 与 manifest-contract 的 RESERVED_NAMES 取交集(`_platform` / `_system` 等带下划线的
24
+ * 本就过不了 PRODUCT_RE,不必重复列)。
25
+ */
26
+ export const RESERVED_PRODUCTS = new Set(["oj", "ojm", "system", "auth", "market", "framework", "health", "plugins", "registry"]);
27
+
28
+ /** 凭证有效期:7 天(一次性令牌的暴露窗口) */
29
+ export const CREDENTIAL_TTL_SECS = 7 * 24 * 3600;
30
+
31
+ export interface IdentityInput {
32
+ product: string
33
+ username: string
34
+ uid: number
35
+ }
36
+
37
+ /** username → key_id 的人名段 */
38
+ export function encodePerson(username: string, uid: number): string {
39
+ const normalized = username
40
+ .toLowerCase()
41
+ .replace(/[^a-z0-9-]/g, "-")
42
+ .replace(/-+/g, "-")
43
+ .replace(/^-|-$/g, "");
44
+ return normalized === "" ? `u${uid}` : normalized;
45
+ }
46
+
47
+ /** 产品名 + 用户名 → key_id(不做校验,调用方先用 assertProduct 收口) */
48
+ export function deriveKeyId(input: IdentityInput): string {
49
+ return `${input.product}.${encodePerson(input.username, input.uid)}`;
50
+ }
51
+
52
+ /** 产品名校验:形状 + 保留名。错误信息给可用示例,避免用户反复试错 */
53
+ export function assertProduct(value: unknown): string {
54
+ const product = typeof value === "string" ? value.trim() : "";
55
+ if (product === "")
56
+ throw Object.assign(new Error("产品名不能为空:填你要发布的解决方案名,例如 demo-solution"), { status: 400 });
57
+ if (!PRODUCT_RE.test(product))
58
+ throw Object.assign(new Error("产品名只能是小写字母开头的 [a-z0-9-],最长 64 个字符,例如 demo-solution"), { status: 400 });
59
+ if (RESERVED_PRODUCTS.has(product))
60
+ throw Object.assign(new Error(`产品名 ${product} 是框架保留名,请换成你的解决方案名`), { status: 400 });
61
+ return product;
62
+ }
63
+
64
+ /** 派生结果自检:理论上必然成立,不成立说明归一化有 bug,宁可 500 也不发一个畸形身份 */
65
+ export function assertKeyId(keyId: string): string {
66
+ if (!KEY_ID_RE.test(keyId))
67
+ throw new Error(`内部错误:推导出的发布身份 ${keyId} 不符合 key_id 规则,请联系管理员`);
68
+ return keyId;
69
+ }