@oj-module/cli 0.1.17 → 0.1.20

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 (69) hide show
  1. package/devkit/README.md +11 -10
  2. package/devkit/SKILL.md +32 -18
  3. package/devkit/manual.md +318 -131
  4. package/package.json +2 -2
  5. package/shell-dist/assets/contract.js +0 -55
  6. package/shell-dist/assets/runtime.js +206 -212
  7. package/shell-dist/index.html +2 -2
  8. package/shell-dist/versions.json +5 -5
  9. package/src/build.ts +1 -0
  10. package/src/contract/__tests__/check-api.test.ts +74 -0
  11. package/src/contract/__tests__/emit-client.test.ts +132 -0
  12. package/src/contract/__tests__/emit-jsonschema-zod.test.ts +93 -0
  13. package/src/contract/__tests__/mock.test.ts +56 -0
  14. package/src/contract/__tests__/openapi-ir.test.ts +137 -0
  15. package/src/contract/__tests__/run-api.test.ts +67 -0
  16. package/src/contract/check.ts +67 -294
  17. package/src/contract/emit-client.ts +23 -15
  18. package/src/contract/emit-jsonschema-zod.ts +209 -0
  19. package/src/contract/emit-oj-schema.ts +108 -161
  20. package/src/contract/emit-stub.ts +17 -57
  21. package/src/contract/handler-scan.ts +263 -0
  22. package/src/contract/ir.ts +11 -179
  23. package/src/contract/json-schema-sample.ts +72 -0
  24. package/src/contract/mock.ts +28 -100
  25. package/src/contract/openapi-ir.ts +145 -0
  26. package/src/contract/run.ts +71 -120
  27. package/src/dev.ts +20 -18
  28. package/src/init.ts +18 -3
  29. package/src/oj.ts +6 -2
  30. package/src/vendor.ts +8 -4
  31. package/templates/api/config.yaml +1 -0
  32. package/templates/api/src/auth/api.ts +66 -0
  33. package/templates/api/src/notification/_shared/notify.ts +199 -0
  34. package/templates/api/src/notification/api.ts +231 -0
  35. package/templates/api/src/notification/del/api.ts +47 -0
  36. package/templates/api/src/notification/manifest.yaml +3 -1
  37. package/templates/api/src/notification/migrations/0001__create_notifications.sql +1 -1
  38. package/templates/api/src/notification/migrations/0002__notifications_user.sql +9 -0
  39. package/templates/api/src/notification/migrations/0003__notifications_broadcast.sql +22 -0
  40. package/templates/api/src/notification/schema.yaml +23 -1
  41. package/templates/api/src/notification/seed.sql +6 -4
  42. package/templates/api/src/personal-center/upload/api.ts +12 -1
  43. package/templates/env.d.ts +1 -1
  44. package/templates/openapi.json +471 -0
  45. package/templates/web/src/auth/__client/api.schemas.ts +8 -0
  46. package/templates/web/src/auth/__client/api.ts +110 -0
  47. package/templates/web/src/notification/__client/api.schemas.ts +50 -0
  48. package/templates/web/src/notification/{client → __client}/api.ts +29 -29
  49. package/templates/web/src/notification/entry.ts +11 -9
  50. package/templates/web/src/notifications/__client/api.schemas.ts +8 -0
  51. package/templates/web/src/notifications/__client/api.ts +80 -0
  52. package/templates/web/src/personal-center/__client/api.schemas.ts +10 -0
  53. package/templates/web/src/personal-center/__client/api.ts +88 -0
  54. package/templates/web/src/web/__client/api.schemas.ts +8 -0
  55. package/templates/web/src/web/__client/api.ts +110 -0
  56. package/vendor/host-versions.json +5 -5
  57. package/src/contract/emit-meta.ts +0 -175
  58. package/src/contract/emit-schema.ts +0 -133
  59. package/src/contract/evaluate.ts +0 -88
  60. package/templates/api/src/auth/login/api.ts +0 -27
  61. package/templates/api/src/auth/logout/api.ts +0 -10
  62. package/templates/api/src/auth/refresh/api.ts +0 -26
  63. package/templates/api/src/notification/contract.ts +0 -98
  64. package/templates/api/src/notification/notifications/api.ts +0 -24
  65. package/templates/api/src/notification/notifications/clear/api.ts +0 -13
  66. package/templates/api/src/notification/notifications/read/api.ts +0 -19
  67. package/templates/api/src/notification/notifications/read-all/api.ts +0 -13
  68. package/templates/api/src/personal-center/contract.ts +0 -16
  69. package/templates/web/src/notification/client/api.schemas.ts +0 -50
@@ -0,0 +1,50 @@
1
+ /* eslint-disable */
2
+ // 生成物:ojm api 从契约生成,勿手改(改动请改契约文件后重跑 ojm api)
3
+ // AC-D15:仅供 DEV 校验动态 import,生产构建不进产物
4
+ import { z } from "@oj-module/runtime";
5
+
6
+ export const schemas = {
7
+ "notificationNotificationsGet": {
8
+ data: z.array(z.object({
9
+ "id": z.string(),
10
+ "scope": z.enum(["user", "broadcast"]),
11
+ "avatar": z.string(),
12
+ "date": z.string(),
13
+ "isRead": z.boolean().optional(),
14
+ "message": z.string(),
15
+ "title": z.string(),
16
+ "link": z.string().nullable().optional(),
17
+ "category": z.enum(["notice", "pm", "mention"]).optional()
18
+ })),
19
+ },
20
+ "notificationNotificationsReadPost": {
21
+ body: z.object({
22
+ "id": z.string()
23
+ }),
24
+ },
25
+ "notificationPushPost": {
26
+ body: z.object({
27
+ "userId": z.union([z.string(), z.number()]).optional(),
28
+ "title": z.string(),
29
+ "message": z.string(),
30
+ "link": z.string().optional(),
31
+ "avatar": z.string().optional(),
32
+ "category": z.enum(["notice", "pm", "mention"]).optional()
33
+ }),
34
+ },
35
+ "notificationPushBroadcastPost": {
36
+ body: z.object({
37
+ "title": z.string(),
38
+ "message": z.string(),
39
+ "link": z.string().optional(),
40
+ "avatar": z.string().optional(),
41
+ "category": z.enum(["notice", "pm", "mention"]).optional()
42
+ }),
43
+ },
44
+ "notificationDelPost": {
45
+ body: z.object({
46
+ "id": z.string(),
47
+ "userId": z.union([z.string(), z.number()]).optional()
48
+ }),
49
+ },
50
+ };
@@ -33,7 +33,7 @@ export function createNotificationClient(ctx: ModuleContext) {
33
33
  }
34
34
  ctx.register.apiPrefix(API_PREFIX);
35
35
  bindRequest(ctx.utils.request);
36
- return { clearAll, del, fetchNotifications, markAllRead, markRead, push, pushBroadcast };
36
+ return { notificationNotificationsGet, notificationNotificationsReadPost, notificationNotificationsReadAllPost, notificationNotificationsClearPost, notificationPushPost, notificationPushBroadcastPost, notificationDelPost };
37
37
  }
38
38
 
39
39
  /**
@@ -64,14 +64,22 @@ async function toApiError(e: unknown): Promise<unknown> {
64
64
  return e;
65
65
  }
66
66
 
67
- export async function clearAll(): Promise<unknown> {
67
+ export type NotificationNotificationsGetData = z.infer<(typeof schemas)["notificationNotificationsGet"]["data"]>;
68
+
69
+ export async function notificationNotificationsGet(): Promise<NotificationNotificationsGetData> {
68
70
  const client = ensureReq();
69
71
  try {
70
- const env = await client.post(`notification/notifications/clear`).json<OjEnvelope<unknown>>();
72
+ const env = await client.get(`notification/notifications`).json<OjEnvelope<NotificationNotificationsGetData>>();
71
73
  // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
72
74
  if (typeof env.code === "number" && env.code !== 0)
73
75
  throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
74
- const data = env.data as unknown;
76
+ const data = env.data as NotificationNotificationsGetData;
77
+ if (import.meta.env.DEV) {
78
+ const { schemas } = await import("./api.schemas");
79
+ const r = schemas["notificationNotificationsGet"].data.safeParse(data);
80
+ if (!r.success)
81
+ throw new ContractApiError(-1, `[契约违例] notificationNotificationsGet 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
82
+ }
75
83
  return data;
76
84
  }
77
85
  catch (e) {
@@ -79,12 +87,12 @@ export async function clearAll(): Promise<unknown> {
79
87
  }
80
88
  }
81
89
 
82
- export type DelBody = z.input<(typeof schemas)["del"]["body"]>;
90
+ export type NotificationNotificationsReadPostBody = z.input<(typeof schemas)["notificationNotificationsReadPost"]["body"]>;
83
91
 
84
- export async function del(body: DelBody): Promise<unknown> {
92
+ export async function notificationNotificationsReadPost(body: NotificationNotificationsReadPostBody): Promise<unknown> {
85
93
  const client = ensureReq();
86
94
  try {
87
- const env = await client.post(`notification/del`, { json: body }).json<OjEnvelope<unknown>>();
95
+ const env = await client.post(`notification/notifications/read`, { json: body }).json<OjEnvelope<unknown>>();
88
96
  // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
89
97
  if (typeof env.code === "number" && env.code !== 0)
90
98
  throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
@@ -96,22 +104,14 @@ export async function del(body: DelBody): Promise<unknown> {
96
104
  }
97
105
  }
98
106
 
99
- export type FetchNotificationsData = z.infer<(typeof schemas)["fetchNotifications"]["data"]>;
100
-
101
- export async function fetchNotifications(): Promise<FetchNotificationsData> {
107
+ export async function notificationNotificationsReadAllPost(): Promise<unknown> {
102
108
  const client = ensureReq();
103
109
  try {
104
- const env = await client.get(`notification/notifications`).json<OjEnvelope<FetchNotificationsData>>();
110
+ const env = await client.post(`notification/notifications/read-all`).json<OjEnvelope<unknown>>();
105
111
  // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
106
112
  if (typeof env.code === "number" && env.code !== 0)
107
113
  throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
108
- const data = env.data as FetchNotificationsData;
109
- if (import.meta.env.DEV) {
110
- const { schemas } = await import("./api.schemas");
111
- const r = schemas.fetchNotifications.data.safeParse(data);
112
- if (!r.success)
113
- throw new ContractApiError(-1, `[契约违例] fetchNotifications 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
114
- }
114
+ const data = env.data as unknown;
115
115
  return data;
116
116
  }
117
117
  catch (e) {
@@ -119,10 +119,10 @@ export async function fetchNotifications(): Promise<FetchNotificationsData> {
119
119
  }
120
120
  }
121
121
 
122
- export async function markAllRead(): Promise<unknown> {
122
+ export async function notificationNotificationsClearPost(): Promise<unknown> {
123
123
  const client = ensureReq();
124
124
  try {
125
- const env = await client.post(`notification/notifications/read-all`).json<OjEnvelope<unknown>>();
125
+ const env = await client.post(`notification/notifications/clear`).json<OjEnvelope<unknown>>();
126
126
  // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
127
127
  if (typeof env.code === "number" && env.code !== 0)
128
128
  throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
@@ -134,12 +134,12 @@ export async function markAllRead(): Promise<unknown> {
134
134
  }
135
135
  }
136
136
 
137
- export type MarkReadBody = z.input<(typeof schemas)["markRead"]["body"]>;
137
+ export type NotificationPushPostBody = z.input<(typeof schemas)["notificationPushPost"]["body"]>;
138
138
 
139
- export async function markRead(body: MarkReadBody): Promise<unknown> {
139
+ export async function notificationPushPost(body: NotificationPushPostBody): Promise<unknown> {
140
140
  const client = ensureReq();
141
141
  try {
142
- const env = await client.post(`notification/notifications/read`, { json: body }).json<OjEnvelope<unknown>>();
142
+ const env = await client.post(`notification/push`, { json: body }).json<OjEnvelope<unknown>>();
143
143
  // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
144
144
  if (typeof env.code === "number" && env.code !== 0)
145
145
  throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
@@ -151,12 +151,12 @@ export async function markRead(body: MarkReadBody): Promise<unknown> {
151
151
  }
152
152
  }
153
153
 
154
- export type PushBody = z.input<(typeof schemas)["push"]["body"]>;
154
+ export type NotificationPushBroadcastPostBody = z.input<(typeof schemas)["notificationPushBroadcastPost"]["body"]>;
155
155
 
156
- export async function push(body: PushBody): Promise<unknown> {
156
+ export async function notificationPushBroadcastPost(body: NotificationPushBroadcastPostBody): Promise<unknown> {
157
157
  const client = ensureReq();
158
158
  try {
159
- const env = await client.post(`notification/push`, { json: body }).json<OjEnvelope<unknown>>();
159
+ const env = await client.post(`notification/push-broadcast`, { json: body }).json<OjEnvelope<unknown>>();
160
160
  // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
161
161
  if (typeof env.code === "number" && env.code !== 0)
162
162
  throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
@@ -168,12 +168,12 @@ export async function push(body: PushBody): Promise<unknown> {
168
168
  }
169
169
  }
170
170
 
171
- export type PushBroadcastBody = z.input<(typeof schemas)["pushBroadcast"]["body"]>;
171
+ export type NotificationDelPostBody = z.input<(typeof schemas)["notificationDelPost"]["body"]>;
172
172
 
173
- export async function pushBroadcast(body: PushBroadcastBody): Promise<unknown> {
173
+ export async function notificationDelPost(body: NotificationDelPostBody): Promise<unknown> {
174
174
  const client = ensureReq();
175
175
  try {
176
- const env = await client.post(`notification/push-broadcast`, { json: body }).json<OjEnvelope<unknown>>();
176
+ const env = await client.post(`notification/del`, { json: body }).json<OjEnvelope<unknown>>();
177
177
  // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
178
178
  if (typeof env.code === "number" && env.code !== 0)
179
179
  throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
@@ -2,7 +2,7 @@ import type { NotificationsApiProvider } from "@oj-module/runtime";
2
2
 
3
3
  import { defineModule } from "@oj-module/runtime";
4
4
 
5
- import { createNotificationClient } from "./client/api";
5
+ import { createNotificationClient } from "./__client/api";
6
6
 
7
7
  /**
8
8
  * notification 模块(G5 模板形态,对齐 apps/playground-oj/web/src/notification)。
@@ -21,23 +21,25 @@ export default defineModule({
21
21
  lifecycle: {
22
22
  async onInit(ctx) {
23
23
  // 构造即接线:createNotificationClient 登记契约前缀 /notification
24
- // (唯一真源在 client/api.ts 的 API_PREFIX)+ 绑定圈好前缀的 scoped
24
+ // (唯一真源在 __client/api.ts 的 API_PREFIX)+ 绑定圈好前缀的 scoped
25
25
  // request(越界/穿越发请求前即拒绝,前缀收敛 D11),返回的 client
26
26
  // 方法都自动带 /api/notification/ 前缀。
27
27
  const notificationClient = createNotificationClient(ctx);
28
28
  const provider: NotificationsApiProvider = {
29
- fetchNotifications: () => notificationClient.fetchNotifications(),
29
+ // 端点函数名由 oj operationId 派生(camelCase 子路径 + 方法),
30
+ // 见 __client/api.ts;provider 方法名(fetchNotifications/markRead…)不变。
31
+ fetchNotifications: () => notificationClient.notificationNotificationsGet(),
30
32
  // id 为命名空间串("u:<id>" / "b:<id>"),原样透传
31
- markRead: id => notificationClient.markRead({ id: String(id) }).then(() => {}),
32
- markAllRead: () => notificationClient.markAllRead().then(() => {}),
33
- clearAll: () => notificationClient.clearAll().then(() => {}),
33
+ markRead: id => notificationClient.notificationNotificationsReadPost({ id: String(id) }).then(() => {}),
34
+ markAllRead: () => notificationClient.notificationNotificationsReadAllPost().then(() => {}),
35
+ clearAll: () => notificationClient.notificationNotificationsClearPost().then(() => {}),
34
36
  push: input => notificationClient
35
- .push({ userId: input.userId, title: input.title, message: input.message, link: input.link, avatar: input.avatar, category: input.category })
37
+ .notificationPushPost({ userId: input.userId, title: input.title, message: input.message, link: input.link, avatar: input.avatar, category: input.category })
36
38
  .then(r => String((r as { id: string }).id)),
37
39
  pushBroadcast: input => notificationClient
38
- .pushBroadcast({ title: input.title, message: input.message, link: input.link, avatar: input.avatar, category: input.category })
40
+ .notificationPushBroadcastPost({ title: input.title, message: input.message, link: input.link, avatar: input.avatar, category: input.category })
39
41
  .then(r => String((r as { id: string }).id)),
40
- del: (id, userId) => notificationClient.del({ id: String(id), userId }).then(() => {}),
42
+ del: (id, userId) => notificationClient.notificationDelPost({ id: String(id), userId }).then(() => {}),
41
43
  };
42
44
  ctx.register.notificationsApi(provider);
43
45
  },
@@ -0,0 +1,8 @@
1
+ /* eslint-disable */
2
+ // 生成物:ojm api 从契约生成,勿手改(改动请改契约文件后重跑 ojm api)
3
+ // AC-D15:仅供 DEV 校验动态 import,生产构建不进产物
4
+ import { z } from "@oj-module/runtime";
5
+
6
+ export const schemas = {
7
+
8
+ };
@@ -0,0 +1,80 @@
1
+ /* eslint-disable */
2
+ // 生成物:ojm api 从契约生成,勿手改(改动请改契约文件后重跑 ojm api)
3
+ import { ContractApiError } from "@oj-module/runtime/contract/errors";
4
+ import type { ScopedRequestLike } from "@oj-module/runtime/contract/errors";
5
+ import type { ModuleContext } from "@oj-module/runtime";
6
+ import type { z } from "@oj-module/runtime";
7
+ import type { schemas } from "./api.schemas";
8
+
9
+ /** oj 信封(AC-D16):code=0 成功;非 0 时 HTTP status=code,由 toApiError 归一为 ContractApiError */
10
+ interface OjEnvelope<T> { code: number, msg?: string, data?: T }
11
+
12
+ let req: ScopedRequestLike | undefined;
13
+
14
+ /** 线上 API 前缀(ojm api 从契约抽取,唯一真源,勿手改):请求基址 + 前缀登记用 */
15
+ export const API_PREFIX = "/notifications";
16
+
17
+ /** 认领身份前缀(契约 apiPrefix,字面等于模块目录名):跨模块认领护栏对比基准 */
18
+ export const MODULE_PREFIX = "/notifications";
19
+
20
+ /**
21
+ * 模块 entry 的 onInit 里一行创建(构造即 install):完成 apiPrefix 登记 +
22
+ * scoped request 绑定,返回绑好前缀的端点函数集合。一模块一 client;
23
+ * 重复构造幂等安全(前缀重复登记同值、request 重绑同值)。
24
+ */
25
+ export function createNotificationsClient(ctx: ModuleContext) {
26
+ // 跨模块认领护栏(DEV):工厂知道自己该属于谁(MODULE_PREFIX = 契约 apiPrefix),
27
+ // ctx 又带 module.name——名实不符说明别的模块在替本模块创建 client,会共享 request
28
+ // 单槽、卸载互相耦合;跨模块需求应由本模块创建后经 provider 暴露。
29
+ // 注意对比基准是 MODULE_PREFIX(目录名)而非 API_PREFIX——D-M8 映射时线上前缀
30
+ // 属于别的命名空间,认领身份仍按目录名。
31
+ if (import.meta.env.DEV && ctx.module.name !== MODULE_PREFIX.slice(1)) {
32
+ console.warn(`[ojm-api] 模块 "${ctx.module.name}" 正在认领 ${MODULE_PREFIX} 的 client——跨模块认领会共享 request 单槽、卸载互相耦合;应由 "${MODULE_PREFIX.slice(1)}" 模块在自身 onInit 创建,跨模块需求经 provider 暴露。`);
33
+ }
34
+ ctx.register.apiPrefix(API_PREFIX);
35
+ bindRequest(ctx.utils.request);
36
+ return { notificationsGet };
37
+ }
38
+
39
+ /**
40
+ * @deprecated 请改用 createNotificationsClient(ctx)——工厂构造即完成
41
+ * 前缀登记 + request 绑定,不会出现「登记了前缀没绑 request」的半接线状态。
42
+ */
43
+ export function bindRequest(r: ScopedRequestLike): void {
44
+ req = r;
45
+ }
46
+
47
+ function ensureReq(): ScopedRequestLike {
48
+ if (!req)
49
+ throw new ContractApiError(-1, "[ojm-api] 请求未绑定——请在模块 entry.ts 的 onInit 里调用 createNotificationsClient(ctx)(或 bindRequest(ctx.utils.request))。");
50
+ return req;
51
+ }
52
+
53
+ /** ky HTTPError → ContractApiError(错误体为信封时取 code/msg);契约违例与原错误原样透传 */
54
+ async function toApiError(e: unknown): Promise<unknown> {
55
+ if (e instanceof ContractApiError)
56
+ return e;
57
+ // ky 2.x 构造 HTTPError 时已用 getReader() 消费掉原 response 体(res.clone().json() 按规范抛 TypeError,
58
+ // 体方法一律失效),解析好的错误体在公开字段 error.data——故读 error.data,勿改回 clone().json()。
59
+ const err = e as { data?: { code?: unknown, msg?: string }, response?: Response } | null;
60
+ const res = err?.response;
61
+ const d = err?.data;
62
+ if (res instanceof Response && d && typeof d.code === "number")
63
+ return new ContractApiError(d.code, d.msg ?? res.statusText);
64
+ return e;
65
+ }
66
+
67
+ export async function notificationsGet(): Promise<unknown> {
68
+ const client = ensureReq();
69
+ try {
70
+ const env = await client.get(`notifications`).json<OjEnvelope<unknown>>();
71
+ // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
72
+ if (typeof env.code === "number" && env.code !== 0)
73
+ throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
74
+ const data = env.data as unknown;
75
+ return data;
76
+ }
77
+ catch (e) {
78
+ throw await toApiError(e);
79
+ }
80
+ }
@@ -0,0 +1,10 @@
1
+ /* eslint-disable */
2
+ // 生成物:ojm api 从契约生成,勿手改(改动请改契约文件后重跑 ojm api)
3
+ // AC-D15:仅供 DEV 校验动态 import,生产构建不进产物
4
+ import { z } from "@oj-module/runtime";
5
+
6
+ export const schemas = {
7
+ "personalCenterUploadPost": {
8
+ data: z.string(),
9
+ },
10
+ };
@@ -0,0 +1,88 @@
1
+ /* eslint-disable */
2
+ // 生成物:ojm api 从契约生成,勿手改(改动请改契约文件后重跑 ojm api)
3
+ import { ContractApiError } from "@oj-module/runtime/contract/errors";
4
+ import type { ScopedRequestLike } from "@oj-module/runtime/contract/errors";
5
+ import type { ModuleContext } from "@oj-module/runtime";
6
+ import type { z } from "@oj-module/runtime";
7
+ import type { schemas } from "./api.schemas";
8
+
9
+ /** oj 信封(AC-D16):code=0 成功;非 0 时 HTTP status=code,由 toApiError 归一为 ContractApiError */
10
+ interface OjEnvelope<T> { code: number, msg?: string, data?: T }
11
+
12
+ let req: ScopedRequestLike | undefined;
13
+
14
+ /** 线上 API 前缀(ojm api 从契约抽取,唯一真源,勿手改):请求基址 + 前缀登记用 */
15
+ export const API_PREFIX = "/personal-center";
16
+
17
+ /** 认领身份前缀(契约 apiPrefix,字面等于模块目录名):跨模块认领护栏对比基准 */
18
+ export const MODULE_PREFIX = "/personal-center";
19
+
20
+ /**
21
+ * 模块 entry 的 onInit 里一行创建(构造即 install):完成 apiPrefix 登记 +
22
+ * scoped request 绑定,返回绑好前缀的端点函数集合。一模块一 client;
23
+ * 重复构造幂等安全(前缀重复登记同值、request 重绑同值)。
24
+ */
25
+ export function createPersonalCenterClient(ctx: ModuleContext) {
26
+ // 跨模块认领护栏(DEV):工厂知道自己该属于谁(MODULE_PREFIX = 契约 apiPrefix),
27
+ // ctx 又带 module.name——名实不符说明别的模块在替本模块创建 client,会共享 request
28
+ // 单槽、卸载互相耦合;跨模块需求应由本模块创建后经 provider 暴露。
29
+ // 注意对比基准是 MODULE_PREFIX(目录名)而非 API_PREFIX——D-M8 映射时线上前缀
30
+ // 属于别的命名空间,认领身份仍按目录名。
31
+ if (import.meta.env.DEV && ctx.module.name !== MODULE_PREFIX.slice(1)) {
32
+ console.warn(`[ojm-api] 模块 "${ctx.module.name}" 正在认领 ${MODULE_PREFIX} 的 client——跨模块认领会共享 request 单槽、卸载互相耦合;应由 "${MODULE_PREFIX.slice(1)}" 模块在自身 onInit 创建,跨模块需求经 provider 暴露。`);
33
+ }
34
+ ctx.register.apiPrefix(API_PREFIX);
35
+ bindRequest(ctx.utils.request);
36
+ return { personalCenterUploadPost };
37
+ }
38
+
39
+ /**
40
+ * @deprecated 请改用 createPersonalCenterClient(ctx)——工厂构造即完成
41
+ * 前缀登记 + request 绑定,不会出现「登记了前缀没绑 request」的半接线状态。
42
+ */
43
+ export function bindRequest(r: ScopedRequestLike): void {
44
+ req = r;
45
+ }
46
+
47
+ function ensureReq(): ScopedRequestLike {
48
+ if (!req)
49
+ throw new ContractApiError(-1, "[ojm-api] 请求未绑定——请在模块 entry.ts 的 onInit 里调用 createPersonalCenterClient(ctx)(或 bindRequest(ctx.utils.request))。");
50
+ return req;
51
+ }
52
+
53
+ /** ky HTTPError → ContractApiError(错误体为信封时取 code/msg);契约违例与原错误原样透传 */
54
+ async function toApiError(e: unknown): Promise<unknown> {
55
+ if (e instanceof ContractApiError)
56
+ return e;
57
+ // ky 2.x 构造 HTTPError 时已用 getReader() 消费掉原 response 体(res.clone().json() 按规范抛 TypeError,
58
+ // 体方法一律失效),解析好的错误体在公开字段 error.data——故读 error.data,勿改回 clone().json()。
59
+ const err = e as { data?: { code?: unknown, msg?: string }, response?: Response } | null;
60
+ const res = err?.response;
61
+ const d = err?.data;
62
+ if (res instanceof Response && d && typeof d.code === "number")
63
+ return new ContractApiError(d.code, d.msg ?? res.statusText);
64
+ return e;
65
+ }
66
+
67
+ export type PersonalCenterUploadPostData = z.infer<(typeof schemas)["personalCenterUploadPost"]["data"]>;
68
+
69
+ export async function personalCenterUploadPost(): Promise<PersonalCenterUploadPostData> {
70
+ const client = ensureReq();
71
+ try {
72
+ const env = await client.post(`personal-center/upload`).json<OjEnvelope<PersonalCenterUploadPostData>>();
73
+ // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
74
+ if (typeof env.code === "number" && env.code !== 0)
75
+ throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
76
+ const data = env.data as PersonalCenterUploadPostData;
77
+ if (import.meta.env.DEV) {
78
+ const { schemas } = await import("./api.schemas");
79
+ const r = schemas["personalCenterUploadPost"].data.safeParse(data);
80
+ if (!r.success)
81
+ throw new ContractApiError(-1, `[契约违例] personalCenterUploadPost 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
82
+ }
83
+ return data;
84
+ }
85
+ catch (e) {
86
+ throw await toApiError(e);
87
+ }
88
+ }
@@ -0,0 +1,8 @@
1
+ /* eslint-disable */
2
+ // 生成物:ojm api 从契约生成,勿手改(改动请改契约文件后重跑 ojm api)
3
+ // AC-D15:仅供 DEV 校验动态 import,生产构建不进产物
4
+ import { z } from "@oj-module/runtime";
5
+
6
+ export const schemas = {
7
+
8
+ };
@@ -0,0 +1,110 @@
1
+ /* eslint-disable */
2
+ // 生成物:ojm api 从契约生成,勿手改(改动请改契约文件后重跑 ojm api)
3
+ import { ContractApiError } from "@oj-module/runtime/contract/errors";
4
+ import type { ScopedRequestLike } from "@oj-module/runtime/contract/errors";
5
+ import type { ModuleContext } from "@oj-module/runtime";
6
+ import type { z } from "@oj-module/runtime";
7
+ import type { schemas } from "./api.schemas";
8
+
9
+ /** oj 信封(AC-D16):code=0 成功;非 0 时 HTTP status=code,由 toApiError 归一为 ContractApiError */
10
+ interface OjEnvelope<T> { code: number, msg?: string, data?: T }
11
+
12
+ let req: ScopedRequestLike | undefined;
13
+
14
+ /** 线上 API 前缀(ojm api 从契约抽取,唯一真源,勿手改):请求基址 + 前缀登记用 */
15
+ export const API_PREFIX = "/web";
16
+
17
+ /** 认领身份前缀(契约 apiPrefix,字面等于模块目录名):跨模块认领护栏对比基准 */
18
+ export const MODULE_PREFIX = "/web";
19
+
20
+ /**
21
+ * 模块 entry 的 onInit 里一行创建(构造即 install):完成 apiPrefix 登记 +
22
+ * scoped request 绑定,返回绑好前缀的端点函数集合。一模块一 client;
23
+ * 重复构造幂等安全(前缀重复登记同值、request 重绑同值)。
24
+ */
25
+ export function createWebClient(ctx: ModuleContext) {
26
+ // 跨模块认领护栏(DEV):工厂知道自己该属于谁(MODULE_PREFIX = 契约 apiPrefix),
27
+ // ctx 又带 module.name——名实不符说明别的模块在替本模块创建 client,会共享 request
28
+ // 单槽、卸载互相耦合;跨模块需求应由本模块创建后经 provider 暴露。
29
+ // 注意对比基准是 MODULE_PREFIX(目录名)而非 API_PREFIX——D-M8 映射时线上前缀
30
+ // 属于别的命名空间,认领身份仍按目录名。
31
+ if (import.meta.env.DEV && ctx.module.name !== MODULE_PREFIX.slice(1)) {
32
+ console.warn(`[ojm-api] 模块 "${ctx.module.name}" 正在认领 ${MODULE_PREFIX} 的 client——跨模块认领会共享 request 单槽、卸载互相耦合;应由 "${MODULE_PREFIX.slice(1)}" 模块在自身 onInit 创建,跨模块需求经 provider 暴露。`);
33
+ }
34
+ ctx.register.apiPrefix(API_PREFIX);
35
+ bindRequest(ctx.utils.request);
36
+ return { webGetAsyncRoutesGet, webHelloGet, webUserInfoGet };
37
+ }
38
+
39
+ /**
40
+ * @deprecated 请改用 createWebClient(ctx)——工厂构造即完成
41
+ * 前缀登记 + request 绑定,不会出现「登记了前缀没绑 request」的半接线状态。
42
+ */
43
+ export function bindRequest(r: ScopedRequestLike): void {
44
+ req = r;
45
+ }
46
+
47
+ function ensureReq(): ScopedRequestLike {
48
+ if (!req)
49
+ throw new ContractApiError(-1, "[ojm-api] 请求未绑定——请在模块 entry.ts 的 onInit 里调用 createWebClient(ctx)(或 bindRequest(ctx.utils.request))。");
50
+ return req;
51
+ }
52
+
53
+ /** ky HTTPError → ContractApiError(错误体为信封时取 code/msg);契约违例与原错误原样透传 */
54
+ async function toApiError(e: unknown): Promise<unknown> {
55
+ if (e instanceof ContractApiError)
56
+ return e;
57
+ // ky 2.x 构造 HTTPError 时已用 getReader() 消费掉原 response 体(res.clone().json() 按规范抛 TypeError,
58
+ // 体方法一律失效),解析好的错误体在公开字段 error.data——故读 error.data,勿改回 clone().json()。
59
+ const err = e as { data?: { code?: unknown, msg?: string }, response?: Response } | null;
60
+ const res = err?.response;
61
+ const d = err?.data;
62
+ if (res instanceof Response && d && typeof d.code === "number")
63
+ return new ContractApiError(d.code, d.msg ?? res.statusText);
64
+ return e;
65
+ }
66
+
67
+ export async function webGetAsyncRoutesGet(): Promise<unknown> {
68
+ const client = ensureReq();
69
+ try {
70
+ const env = await client.get(`web/get-async-routes`).json<OjEnvelope<unknown>>();
71
+ // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
72
+ if (typeof env.code === "number" && env.code !== 0)
73
+ throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
74
+ const data = env.data as unknown;
75
+ return data;
76
+ }
77
+ catch (e) {
78
+ throw await toApiError(e);
79
+ }
80
+ }
81
+
82
+ export async function webHelloGet(): Promise<unknown> {
83
+ const client = ensureReq();
84
+ try {
85
+ const env = await client.get(`web/hello`).json<OjEnvelope<unknown>>();
86
+ // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
87
+ if (typeof env.code === "number" && env.code !== 0)
88
+ throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
89
+ const data = env.data as unknown;
90
+ return data;
91
+ }
92
+ catch (e) {
93
+ throw await toApiError(e);
94
+ }
95
+ }
96
+
97
+ export async function webUserInfoGet(): Promise<unknown> {
98
+ const client = ensureReq();
99
+ try {
100
+ const env = await client.get(`web/user-info`).json<OjEnvelope<unknown>>();
101
+ // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
102
+ if (typeof env.code === "number" && env.code !== 0)
103
+ throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
104
+ const data = env.data as unknown;
105
+ return data;
106
+ }
107
+ catch (e) {
108
+ throw await toApiError(e);
109
+ }
110
+ }
@@ -8,11 +8,11 @@
8
8
  "react-router": "7.18.2",
9
9
  "react-router/dom": "7.18.2",
10
10
  "@tanstack/react-query": "5.102.8",
11
- "@oj-module/runtime": "0.1.17",
12
- "@oj-module/runtime/contract": "0.1.17",
13
- "@oj-module/runtime/contract/errors": "0.1.17",
14
- "@plane/kit": "0.1.17",
15
- "@plane/editor": "0.1.17",
11
+ "@oj-module/runtime": "0.1.20",
12
+ "@oj-module/runtime/contract": "0.1.20",
13
+ "@oj-module/runtime/contract/errors": "0.1.20",
14
+ "@plane/kit": "0.1.20",
15
+ "@plane/editor": "0.1.20",
16
16
  "antd": "6.6.1",
17
17
  "antd/locale/en_US": "6.6.1",
18
18
  "antd/locale/zh_CN": "6.6.1",