@oj-module/cli 0.1.16 → 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 (68) hide show
  1. package/devkit/README.md +11 -10
  2. package/devkit/SKILL.md +35 -18
  3. package/devkit/manual.md +359 -131
  4. package/package.json +2 -2
  5. package/shell-dist/assets/contract.js +0 -55
  6. package/shell-dist/assets/runtime.js +238 -213
  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/api.ts +186 -0
  49. package/templates/web/src/notification/entry.ts +20 -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/{notification/client → personal-center/__client}/api.schemas.ts +2 -14
  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/{notification/client → web/__client}/api.ts +12 -37
  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 -52
  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
@@ -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
+ };
@@ -0,0 +1,186 @@
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 = "/notification";
16
+
17
+ /** 认领身份前缀(契约 apiPrefix,字面等于模块目录名):跨模块认领护栏对比基准 */
18
+ export const MODULE_PREFIX = "/notification";
19
+
20
+ /**
21
+ * 模块 entry 的 onInit 里一行创建(构造即 install):完成 apiPrefix 登记 +
22
+ * scoped request 绑定,返回绑好前缀的端点函数集合。一模块一 client;
23
+ * 重复构造幂等安全(前缀重复登记同值、request 重绑同值)。
24
+ */
25
+ export function createNotificationClient(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 { notificationNotificationsGet, notificationNotificationsReadPost, notificationNotificationsReadAllPost, notificationNotificationsClearPost, notificationPushPost, notificationPushBroadcastPost, notificationDelPost };
37
+ }
38
+
39
+ /**
40
+ * @deprecated 请改用 createNotificationClient(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 里调用 createNotificationClient(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 NotificationNotificationsGetData = z.infer<(typeof schemas)["notificationNotificationsGet"]["data"]>;
68
+
69
+ export async function notificationNotificationsGet(): Promise<NotificationNotificationsGetData> {
70
+ const client = ensureReq();
71
+ try {
72
+ const env = await client.get(`notification/notifications`).json<OjEnvelope<NotificationNotificationsGetData>>();
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 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
+ }
83
+ return data;
84
+ }
85
+ catch (e) {
86
+ throw await toApiError(e);
87
+ }
88
+ }
89
+
90
+ export type NotificationNotificationsReadPostBody = z.input<(typeof schemas)["notificationNotificationsReadPost"]["body"]>;
91
+
92
+ export async function notificationNotificationsReadPost(body: NotificationNotificationsReadPostBody): Promise<unknown> {
93
+ const client = ensureReq();
94
+ try {
95
+ const env = await client.post(`notification/notifications/read`, { json: body }).json<OjEnvelope<unknown>>();
96
+ // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
97
+ if (typeof env.code === "number" && env.code !== 0)
98
+ throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
99
+ const data = env.data as unknown;
100
+ return data;
101
+ }
102
+ catch (e) {
103
+ throw await toApiError(e);
104
+ }
105
+ }
106
+
107
+ export async function notificationNotificationsReadAllPost(): Promise<unknown> {
108
+ const client = ensureReq();
109
+ try {
110
+ const env = await client.post(`notification/notifications/read-all`).json<OjEnvelope<unknown>>();
111
+ // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
112
+ if (typeof env.code === "number" && env.code !== 0)
113
+ throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
114
+ const data = env.data as unknown;
115
+ return data;
116
+ }
117
+ catch (e) {
118
+ throw await toApiError(e);
119
+ }
120
+ }
121
+
122
+ export async function notificationNotificationsClearPost(): Promise<unknown> {
123
+ const client = ensureReq();
124
+ try {
125
+ const env = await client.post(`notification/notifications/clear`).json<OjEnvelope<unknown>>();
126
+ // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
127
+ if (typeof env.code === "number" && env.code !== 0)
128
+ throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
129
+ const data = env.data as unknown;
130
+ return data;
131
+ }
132
+ catch (e) {
133
+ throw await toApiError(e);
134
+ }
135
+ }
136
+
137
+ export type NotificationPushPostBody = z.input<(typeof schemas)["notificationPushPost"]["body"]>;
138
+
139
+ export async function notificationPushPost(body: NotificationPushPostBody): Promise<unknown> {
140
+ const client = ensureReq();
141
+ try {
142
+ const env = await client.post(`notification/push`, { json: body }).json<OjEnvelope<unknown>>();
143
+ // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
144
+ if (typeof env.code === "number" && env.code !== 0)
145
+ throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
146
+ const data = env.data as unknown;
147
+ return data;
148
+ }
149
+ catch (e) {
150
+ throw await toApiError(e);
151
+ }
152
+ }
153
+
154
+ export type NotificationPushBroadcastPostBody = z.input<(typeof schemas)["notificationPushBroadcastPost"]["body"]>;
155
+
156
+ export async function notificationPushBroadcastPost(body: NotificationPushBroadcastPostBody): Promise<unknown> {
157
+ const client = ensureReq();
158
+ try {
159
+ const env = await client.post(`notification/push-broadcast`, { json: body }).json<OjEnvelope<unknown>>();
160
+ // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
161
+ if (typeof env.code === "number" && env.code !== 0)
162
+ throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
163
+ const data = env.data as unknown;
164
+ return data;
165
+ }
166
+ catch (e) {
167
+ throw await toApiError(e);
168
+ }
169
+ }
170
+
171
+ export type NotificationDelPostBody = z.input<(typeof schemas)["notificationDelPost"]["body"]>;
172
+
173
+ export async function notificationDelPost(body: NotificationDelPostBody): Promise<unknown> {
174
+ const client = ensureReq();
175
+ try {
176
+ const env = await client.post(`notification/del`, { json: body }).json<OjEnvelope<unknown>>();
177
+ // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
178
+ if (typeof env.code === "number" && env.code !== 0)
179
+ throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
180
+ const data = env.data as unknown;
181
+ return data;
182
+ }
183
+ catch (e) {
184
+ throw await toApiError(e);
185
+ }
186
+ }
@@ -2,14 +2,15 @@ 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)。
9
9
  *
10
- * 无页面路由;仅通过 onInit 全量接管通知 API(D9 + G4,四方法全必填):
11
- * 把内置 root 级通知读/写收敛到本模块 `/notification` 前缀下的 oj 后端,
12
- * 通知铃的互动(单条已读/全部已读/清空)由此生效。
10
+ * 无页面路由;仅通过 onInit 全量接管通知 API(D9 + G4):把内置 root 级通知读/写
11
+ * 收敛到本模块 `/notification` 前缀下的 oj 后端,通知铃的互动(单条已读/全部已读/
12
+ * 清空)由此生效;发射端三方法(push/pushBroadcast/del)使本模块同时作为跨模块
13
+ * 通知原语的接收端(前端 facade 委托至此)。
13
14
  */
14
15
  export default defineModule({
15
16
  name: "notification",
@@ -20,15 +21,25 @@ export default defineModule({
20
21
  lifecycle: {
21
22
  async onInit(ctx) {
22
23
  // 构造即接线:createNotificationClient 登记契约前缀 /notification
23
- // (唯一真源在 client/api.ts 的 API_PREFIX)+ 绑定圈好前缀的 scoped
24
+ // (唯一真源在 __client/api.ts 的 API_PREFIX)+ 绑定圈好前缀的 scoped
24
25
  // request(越界/穿越发请求前即拒绝,前缀收敛 D11),返回的 client
25
26
  // 方法都自动带 /api/notification/ 前缀。
26
27
  const notificationClient = createNotificationClient(ctx);
27
28
  const provider: NotificationsApiProvider = {
28
- fetchNotifications: () => notificationClient.fetchNotifications(),
29
- markRead: id => notificationClient.markRead({ id: Number(id) }).then(() => {}),
30
- markAllRead: () => notificationClient.markAllRead().then(() => {}),
31
- clearAll: () => notificationClient.clearAll().then(() => {}),
29
+ // 端点函数名由 oj operationId 派生(camelCase 子路径 + 方法),
30
+ // 见 __client/api.ts;provider 方法名(fetchNotifications/markRead…)不变。
31
+ fetchNotifications: () => notificationClient.notificationNotificationsGet(),
32
+ // id 为命名空间串("u:<id>" / "b:<id>"),原样透传
33
+ markRead: id => notificationClient.notificationNotificationsReadPost({ id: String(id) }).then(() => {}),
34
+ markAllRead: () => notificationClient.notificationNotificationsReadAllPost().then(() => {}),
35
+ clearAll: () => notificationClient.notificationNotificationsClearPost().then(() => {}),
36
+ push: input => notificationClient
37
+ .notificationPushPost({ userId: input.userId, title: input.title, message: input.message, link: input.link, avatar: input.avatar, category: input.category })
38
+ .then(r => String((r as { id: string }).id)),
39
+ pushBroadcast: input => notificationClient
40
+ .notificationPushBroadcastPost({ title: input.title, message: input.message, link: input.link, avatar: input.avatar, category: input.category })
41
+ .then(r => String((r as { id: string }).id)),
42
+ del: (id, userId) => notificationClient.notificationDelPost({ id: String(id), userId }).then(() => {}),
32
43
  };
33
44
  ctx.register.notificationsApi(provider);
34
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
+ }
@@ -4,19 +4,7 @@
4
4
  import { z } from "@oj-module/runtime";
5
5
 
6
6
  export const schemas = {
7
- fetchNotifications: {
8
- data: z.array(z.object({
9
- id: z.number(),
10
- avatar: z.string(),
11
- date: z.string(),
12
- isRead: z.boolean().optional(),
13
- message: z.string(),
14
- title: z.string(),
15
- })),
16
- },
17
- markRead: {
18
- body: z.object({
19
- id: z.number(),
20
- }),
7
+ "personalCenterUploadPost": {
8
+ data: z.string(),
21
9
  },
22
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
+ };
@@ -12,17 +12,17 @@ interface OjEnvelope<T> { code: number, msg?: string, data?: T }
12
12
  let req: ScopedRequestLike | undefined;
13
13
 
14
14
  /** 线上 API 前缀(ojm api 从契约抽取,唯一真源,勿手改):请求基址 + 前缀登记用 */
15
- export const API_PREFIX = "/notification";
15
+ export const API_PREFIX = "/web";
16
16
 
17
17
  /** 认领身份前缀(契约 apiPrefix,字面等于模块目录名):跨模块认领护栏对比基准 */
18
- export const MODULE_PREFIX = "/notification";
18
+ export const MODULE_PREFIX = "/web";
19
19
 
20
20
  /**
21
21
  * 模块 entry 的 onInit 里一行创建(构造即 install):完成 apiPrefix 登记 +
22
22
  * scoped request 绑定,返回绑好前缀的端点函数集合。一模块一 client;
23
23
  * 重复构造幂等安全(前缀重复登记同值、request 重绑同值)。
24
24
  */
25
- export function createNotificationClient(ctx: ModuleContext) {
25
+ export function createWebClient(ctx: ModuleContext) {
26
26
  // 跨模块认领护栏(DEV):工厂知道自己该属于谁(MODULE_PREFIX = 契约 apiPrefix),
27
27
  // ctx 又带 module.name——名实不符说明别的模块在替本模块创建 client,会共享 request
28
28
  // 单槽、卸载互相耦合;跨模块需求应由本模块创建后经 provider 暴露。
@@ -33,11 +33,11 @@ export function createNotificationClient(ctx: ModuleContext) {
33
33
  }
34
34
  ctx.register.apiPrefix(API_PREFIX);
35
35
  bindRequest(ctx.utils.request);
36
- return { clearAll, fetchNotifications, markAllRead, markRead };
36
+ return { webGetAsyncRoutesGet, webHelloGet, webUserInfoGet };
37
37
  }
38
38
 
39
39
  /**
40
- * @deprecated 请改用 createNotificationClient(ctx)——工厂构造即完成
40
+ * @deprecated 请改用 createWebClient(ctx)——工厂构造即完成
41
41
  * 前缀登记 + request 绑定,不会出现「登记了前缀没绑 request」的半接线状态。
42
42
  */
43
43
  export function bindRequest(r: ScopedRequestLike): void {
@@ -46,7 +46,7 @@ export function bindRequest(r: ScopedRequestLike): void {
46
46
 
47
47
  function ensureReq(): ScopedRequestLike {
48
48
  if (!req)
49
- throw new ContractApiError(-1, "[ojm-api] 请求未绑定——请在模块 entry.ts 的 onInit 里调用 createNotificationClient(ctx)(或 bindRequest(ctx.utils.request))。");
49
+ throw new ContractApiError(-1, "[ojm-api] 请求未绑定——请在模块 entry.ts 的 onInit 里调用 createWebClient(ctx)(或 bindRequest(ctx.utils.request))。");
50
50
  return req;
51
51
  }
52
52
 
@@ -64,10 +64,10 @@ async function toApiError(e: unknown): Promise<unknown> {
64
64
  return e;
65
65
  }
66
66
 
67
- export async function clearAll(): Promise<unknown> {
67
+ export async function webGetAsyncRoutesGet(): Promise<unknown> {
68
68
  const client = ensureReq();
69
69
  try {
70
- const env = await client.post(`notification/notifications/clear`).json<OjEnvelope<unknown>>();
70
+ const env = await client.get(`web/get-async-routes`).json<OjEnvelope<unknown>>();
71
71
  // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
72
72
  if (typeof env.code === "number" && env.code !== 0)
73
73
  throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
@@ -79,33 +79,10 @@ export async function clearAll(): Promise<unknown> {
79
79
  }
80
80
  }
81
81
 
82
- export type FetchNotificationsData = z.infer<(typeof schemas)["fetchNotifications"]["data"]>;
83
-
84
- export async function fetchNotifications(): Promise<FetchNotificationsData> {
85
- const client = ensureReq();
86
- try {
87
- const env = await client.get(`notification/notifications`).json<OjEnvelope<FetchNotificationsData>>();
88
- // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
89
- if (typeof env.code === "number" && env.code !== 0)
90
- throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
91
- const data = env.data as FetchNotificationsData;
92
- if (import.meta.env.DEV) {
93
- const { schemas } = await import("./api.schemas");
94
- const r = schemas.fetchNotifications.data.safeParse(data);
95
- if (!r.success)
96
- throw new ContractApiError(-1, `[契约违例] fetchNotifications 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
97
- }
98
- return data;
99
- }
100
- catch (e) {
101
- throw await toApiError(e);
102
- }
103
- }
104
-
105
- export async function markAllRead(): Promise<unknown> {
82
+ export async function webHelloGet(): Promise<unknown> {
106
83
  const client = ensureReq();
107
84
  try {
108
- const env = await client.post(`notification/notifications/read-all`).json<OjEnvelope<unknown>>();
85
+ const env = await client.get(`web/hello`).json<OjEnvelope<unknown>>();
109
86
  // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
110
87
  if (typeof env.code === "number" && env.code !== 0)
111
88
  throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
@@ -117,12 +94,10 @@ export async function markAllRead(): Promise<unknown> {
117
94
  }
118
95
  }
119
96
 
120
- export type MarkReadBody = z.input<(typeof schemas)["markRead"]["body"]>;
121
-
122
- export async function markRead(body: MarkReadBody): Promise<unknown> {
97
+ export async function webUserInfoGet(): Promise<unknown> {
123
98
  const client = ensureReq();
124
99
  try {
125
- const env = await client.post(`notification/notifications/read`, { json: body }).json<OjEnvelope<unknown>>();
100
+ const env = await client.get(`web/user-info`).json<OjEnvelope<unknown>>();
126
101
  // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
127
102
  if (typeof env.code === "number" && env.code !== 0)
128
103
  throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
@@ -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.16",
12
- "@oj-module/runtime/contract": "0.1.16",
13
- "@oj-module/runtime/contract/errors": "0.1.16",
14
- "@plane/kit": "0.1.16",
15
- "@plane/editor": "0.1.16",
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",