@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
@@ -1,11 +1,33 @@
1
- # 声明式表结构(§4.2 / D1-C):notification 模块自有表 notifications。
1
+ # 声明式表结构(§4.2 / D1-C):notification 模块自有表。
2
+ # 按人通知 notifications(user_id 归属 + link 深链 + category 类别)+ 广播两表。
2
3
  tables:
3
4
  notifications:
4
5
  pk: id
5
6
  columns:
6
7
  id: {type: integer, autoincrement: true}
8
+ user_id: {type: integer, null: true}
7
9
  avatar: {type: text, null: false}
8
10
  date: {type: text, null: false}
9
11
  is_read: {type: integer, null: false}
10
12
  message: {type: text, null: false}
11
13
  title: {type: text, null: false}
14
+ link: {type: text, null: true}
15
+ category: {type: text, null: false}
16
+ notifications_broadcast:
17
+ pk: id
18
+ columns:
19
+ id: {type: integer, autoincrement: true}
20
+ avatar: {type: text, null: false}
21
+ date: {type: text, null: false}
22
+ message: {type: text, null: false}
23
+ title: {type: text, null: false}
24
+ link: {type: text, null: true}
25
+ category: {type: text, null: false}
26
+ create_time: {type: text, null: false}
27
+ notifications_broadcast_read:
28
+ pk: id
29
+ columns:
30
+ id: {type: integer, autoincrement: true}
31
+ broadcast_id: {type: integer, null: false}
32
+ user_id: {type: integer, null: false}
33
+ read_time: {type: text, null: false}
@@ -1,4 +1,6 @@
1
- -- notifications 模块种子:每次启动重放,必须幂等(INSERT OR IGNORE / 注释里别写分号)。
2
- INSERT OR IGNORE INTO notifications (id, avatar, date, is_read, message, title) VALUES (1, '', '2026-09-01 09:12', 0, '脚手架已就绪,用 admin / 123456 登录开始', '欢迎');
3
- INSERT OR IGNORE INTO notifications (id, avatar, date, is_read, message, title) VALUES (2, '', '2026-09-02 14:30', 0, '编辑 api/src 下的 api.ts 保存即热更,新增模块目录需重启 ojm dev', '开发提示');
4
- INSERT OR IGNORE INTO notifications (id, avatar, date, is_read, message, title) VALUES (3, '', '2026-09-03 18:45', 1, '契约改动后重跑 ojm api 生成前端 client 与文档', '契约');
1
+ -- notification 模块种子(按人真实通知)。user_id=1(admin),link/category 幂等可空/默认。
2
+ -- 按分号朴素切分(S006):语句内不得含分号字面量。
3
+ INSERT OR IGNORE INTO notifications (id, user_id, avatar, date, is_read, message, title, link, category) VALUES (1, 1, '', '2026-09-01 09:12', 0, '脚手架已就绪,用 admin / 123456 登录开始', '欢迎', NULL, 'notice');
4
+ INSERT OR IGNORE INTO notifications (id, user_id, avatar, date, is_read, message, title, link, category) VALUES (2, 1, '', '2026-09-02 14:30', 0, '通知铃支持单条已读、全部已读与清空;广播经 push-broadcast 入 b: 命名空间', '通知铃', NULL, 'notice');
5
+ INSERT OR IGNORE INTO notifications (id, user_id, avatar, date, is_read, message, title, link, category) VALUES (3, 1, '', '2026-09-03 18:45', 1, '契约改动后重跑 ojm api 生成前端 client 与文档', '契约', NULL, 'notice');
6
+ INSERT OR IGNORE INTO notifications (id, user_id, avatar, date, is_read, message, title, link, category) VALUES (4, 1, '', '2026-09-04 08:00', 0, '编辑 api/src 下的 api.ts 保存即热更;handler 组支持四层嵌套(oj v0.1.64)', '开发提示', NULL, 'notice');
@@ -23,7 +23,8 @@ function toBase64(bytes: Uint8Array): string {
23
23
  return out;
24
24
  }
25
25
 
26
- export default {
26
+ // POST /api/personal-center/upload —— 头像上传(multipart→base64,P4-3,D7/F4)。
27
+ const handler = {
27
28
  async post() {
28
29
  const uid = http.user?.id;
29
30
  if (!uid) {
@@ -51,3 +52,13 @@ export default {
51
52
  }
52
53
  },
53
54
  };
55
+
56
+ // 出参(data payload)= 上传后返回的资源 URL(data: URL)。body 是 multipipart,
57
+ // 不在 .schema 白名单内(前端走 antd Upload 直连,不经 JSON client),故此处只声明出参。
58
+ // oj v0.1.72 起出参走平级 .response(与 .schema 同级,动词级优先于组级),
59
+ // .schema 只承载入参(params/query/body)。
60
+ handler.post.response = {
61
+ type: "string",
62
+ };
63
+
64
+ export default handler;
@@ -1,6 +1,6 @@
1
1
  // 前端构建期全局类型(Vite 语义)。
2
2
  //
3
- // `ojm api` 生成的 client(web/src/<模块>/client/api.ts)会读
3
+ // `ojm api` 生成的 client(web/src/<模块>/__client/api.ts)会读
4
4
  // `import.meta.env.DEV` 做开发期响应校验;脚手架不直接依赖 vite,
5
5
  // 故自带最小声明,避免 `pnpm typecheck` 对生成物报 TS2339
6
6
  // "Property 'env' does not exist on type 'ImportMeta'"。
@@ -0,0 +1,471 @@
1
+ {
2
+ "openapi": "3.1.0",
3
+ "info": {
4
+ "title": "oj API",
5
+ "version": "0.1.0"
6
+ },
7
+ "paths": {
8
+ "/api/auth/login": {
9
+ "post": {
10
+ "operationId": "auth_login_post",
11
+ "summary": "POST api/auth/login",
12
+ "x-oj-file": "auth/api.ts",
13
+ "tags": [
14
+ "auth"
15
+ ],
16
+ "responses": {
17
+ "200": {
18
+ "description": "OK"
19
+ }
20
+ }
21
+ }
22
+ },
23
+ "/api/auth/logout": {
24
+ "post": {
25
+ "operationId": "auth_logout_post",
26
+ "summary": "POST api/auth/logout",
27
+ "x-oj-file": "auth/api.ts",
28
+ "tags": [
29
+ "auth"
30
+ ],
31
+ "responses": {
32
+ "200": {
33
+ "description": "OK"
34
+ }
35
+ }
36
+ }
37
+ },
38
+ "/api/auth/refresh": {
39
+ "post": {
40
+ "operationId": "auth_refresh_post",
41
+ "summary": "POST api/auth/refresh",
42
+ "x-oj-file": "auth/api.ts",
43
+ "tags": [
44
+ "auth"
45
+ ],
46
+ "responses": {
47
+ "200": {
48
+ "description": "OK"
49
+ }
50
+ }
51
+ }
52
+ },
53
+ "/api/notification/notifications": {
54
+ "get": {
55
+ "operationId": "notification_notifications_get",
56
+ "summary": "GET api/notification/notifications",
57
+ "x-oj-file": "notification/api.ts",
58
+ "tags": [
59
+ "notification"
60
+ ],
61
+ "responses": {
62
+ "200": {
63
+ "description": "OK",
64
+ "x-oj-response-enforced": false,
65
+ "content": {
66
+ "application/json": {
67
+ "schema": {
68
+ "type": "object",
69
+ "required": [
70
+ "code",
71
+ "msg",
72
+ "data"
73
+ ],
74
+ "properties": {
75
+ "code": {
76
+ "type": "integer"
77
+ },
78
+ "msg": {
79
+ "type": "string"
80
+ },
81
+ "data": {
82
+ "type": "array",
83
+ "items": {
84
+ "type": "object",
85
+ "required": [
86
+ "id",
87
+ "scope",
88
+ "avatar",
89
+ "date",
90
+ "message",
91
+ "title"
92
+ ],
93
+ "properties": {
94
+ "id": {
95
+ "type": "string"
96
+ },
97
+ "scope": {
98
+ "type": "string",
99
+ "enum": [
100
+ "user",
101
+ "broadcast"
102
+ ]
103
+ },
104
+ "avatar": {
105
+ "type": "string"
106
+ },
107
+ "date": {
108
+ "type": "string"
109
+ },
110
+ "isRead": {
111
+ "type": "boolean"
112
+ },
113
+ "message": {
114
+ "type": "string"
115
+ },
116
+ "title": {
117
+ "type": "string"
118
+ },
119
+ "link": {
120
+ "type": [
121
+ "string",
122
+ "null"
123
+ ]
124
+ },
125
+ "category": {
126
+ "type": "string",
127
+ "enum": [
128
+ "notice",
129
+ "pm",
130
+ "mention"
131
+ ]
132
+ }
133
+ }
134
+ }
135
+ }
136
+ }
137
+ }
138
+ }
139
+ }
140
+ }
141
+ }
142
+ }
143
+ },
144
+ "/api/notification/notifications/read": {
145
+ "post": {
146
+ "operationId": "notification_notifications_read_post",
147
+ "summary": "POST api/notification/notifications/read",
148
+ "x-oj-file": "notification/api.ts",
149
+ "tags": [
150
+ "notification"
151
+ ],
152
+ "requestBody": {
153
+ "required": true,
154
+ "content": {
155
+ "application/json": {
156
+ "schema": {
157
+ "type": "object",
158
+ "required": [
159
+ "id"
160
+ ],
161
+ "properties": {
162
+ "id": {
163
+ "type": "string"
164
+ }
165
+ }
166
+ }
167
+ }
168
+ }
169
+ },
170
+ "responses": {
171
+ "200": {
172
+ "description": "OK"
173
+ }
174
+ }
175
+ }
176
+ },
177
+ "/api/notification/notifications/read-all": {
178
+ "post": {
179
+ "operationId": "notification_notifications_read_all_post",
180
+ "summary": "POST api/notification/notifications/read-all",
181
+ "x-oj-file": "notification/api.ts",
182
+ "tags": [
183
+ "notification"
184
+ ],
185
+ "responses": {
186
+ "200": {
187
+ "description": "OK"
188
+ }
189
+ }
190
+ }
191
+ },
192
+ "/api/notification/notifications/clear": {
193
+ "post": {
194
+ "operationId": "notification_notifications_clear_post",
195
+ "summary": "POST api/notification/notifications/clear",
196
+ "x-oj-file": "notification/api.ts",
197
+ "tags": [
198
+ "notification"
199
+ ],
200
+ "responses": {
201
+ "200": {
202
+ "description": "OK"
203
+ }
204
+ }
205
+ }
206
+ },
207
+ "/api/notification/push": {
208
+ "post": {
209
+ "operationId": "notification_push_post",
210
+ "summary": "POST api/notification/push",
211
+ "x-oj-file": "notification/api.ts",
212
+ "tags": [
213
+ "notification"
214
+ ],
215
+ "requestBody": {
216
+ "required": true,
217
+ "content": {
218
+ "application/json": {
219
+ "schema": {
220
+ "type": "object",
221
+ "required": [
222
+ "title",
223
+ "message"
224
+ ],
225
+ "properties": {
226
+ "userId": {
227
+ "type": [
228
+ "string",
229
+ "number"
230
+ ]
231
+ },
232
+ "title": {
233
+ "type": "string"
234
+ },
235
+ "message": {
236
+ "type": "string"
237
+ },
238
+ "link": {
239
+ "type": "string"
240
+ },
241
+ "avatar": {
242
+ "type": "string"
243
+ },
244
+ "category": {
245
+ "type": "string",
246
+ "enum": [
247
+ "notice",
248
+ "pm",
249
+ "mention"
250
+ ]
251
+ }
252
+ }
253
+ }
254
+ }
255
+ }
256
+ },
257
+ "responses": {
258
+ "200": {
259
+ "description": "OK"
260
+ }
261
+ }
262
+ }
263
+ },
264
+ "/api/notification/push-broadcast": {
265
+ "post": {
266
+ "operationId": "notification_push_broadcast_post",
267
+ "summary": "POST api/notification/push-broadcast",
268
+ "x-oj-file": "notification/api.ts",
269
+ "tags": [
270
+ "notification"
271
+ ],
272
+ "requestBody": {
273
+ "required": true,
274
+ "content": {
275
+ "application/json": {
276
+ "schema": {
277
+ "type": "object",
278
+ "required": [
279
+ "title",
280
+ "message"
281
+ ],
282
+ "properties": {
283
+ "title": {
284
+ "type": "string"
285
+ },
286
+ "message": {
287
+ "type": "string"
288
+ },
289
+ "link": {
290
+ "type": "string"
291
+ },
292
+ "avatar": {
293
+ "type": "string"
294
+ },
295
+ "category": {
296
+ "type": "string",
297
+ "enum": [
298
+ "notice",
299
+ "pm",
300
+ "mention"
301
+ ]
302
+ }
303
+ }
304
+ }
305
+ }
306
+ }
307
+ },
308
+ "responses": {
309
+ "200": {
310
+ "description": "OK"
311
+ }
312
+ }
313
+ }
314
+ },
315
+ "/api/notification/del": {
316
+ "post": {
317
+ "operationId": "notification_del_post",
318
+ "summary": "POST api/notification/del",
319
+ "x-oj-file": "notification/del/api.ts",
320
+ "tags": [
321
+ "notification"
322
+ ],
323
+ "requestBody": {
324
+ "required": true,
325
+ "content": {
326
+ "application/json": {
327
+ "schema": {
328
+ "type": "object",
329
+ "required": [
330
+ "id"
331
+ ],
332
+ "properties": {
333
+ "id": {
334
+ "type": "string"
335
+ },
336
+ "userId": {
337
+ "type": [
338
+ "string",
339
+ "number"
340
+ ]
341
+ }
342
+ }
343
+ }
344
+ }
345
+ }
346
+ },
347
+ "responses": {
348
+ "200": {
349
+ "description": "OK"
350
+ }
351
+ }
352
+ }
353
+ },
354
+ "/api/notifications": {
355
+ "get": {
356
+ "operationId": "notifications_get",
357
+ "summary": "GET api/notifications",
358
+ "x-oj-file": "notifications/api.ts",
359
+ "tags": [
360
+ "notifications"
361
+ ],
362
+ "responses": {
363
+ "200": {
364
+ "description": "OK"
365
+ }
366
+ }
367
+ }
368
+ },
369
+ "/api/personal-center/upload": {
370
+ "post": {
371
+ "operationId": "personal_center_upload_post",
372
+ "summary": "POST api/personal-center/upload",
373
+ "x-oj-file": "personal-center/upload/api.ts",
374
+ "tags": [
375
+ "personal-center"
376
+ ],
377
+ "responses": {
378
+ "200": {
379
+ "description": "OK",
380
+ "x-oj-response-enforced": false,
381
+ "content": {
382
+ "application/json": {
383
+ "schema": {
384
+ "type": "object",
385
+ "required": [
386
+ "code",
387
+ "msg",
388
+ "data"
389
+ ],
390
+ "properties": {
391
+ "code": {
392
+ "type": "integer"
393
+ },
394
+ "msg": {
395
+ "type": "string"
396
+ },
397
+ "data": {
398
+ "type": "string"
399
+ }
400
+ }
401
+ }
402
+ }
403
+ }
404
+ }
405
+ }
406
+ }
407
+ },
408
+ "/api/web/get-async-routes": {
409
+ "get": {
410
+ "operationId": "web_get_async_routes_get",
411
+ "summary": "GET api/web/get-async-routes",
412
+ "x-oj-file": "web/get-async-routes/api.ts",
413
+ "tags": [
414
+ "web"
415
+ ],
416
+ "responses": {
417
+ "200": {
418
+ "description": "OK"
419
+ }
420
+ }
421
+ }
422
+ },
423
+ "/api/web/hello": {
424
+ "get": {
425
+ "operationId": "web_hello_get",
426
+ "summary": "GET api/web/hello",
427
+ "x-oj-file": "web/hello/api.ts",
428
+ "tags": [
429
+ "web"
430
+ ],
431
+ "responses": {
432
+ "200": {
433
+ "description": "OK"
434
+ }
435
+ }
436
+ }
437
+ },
438
+ "/api/web/user-info": {
439
+ "get": {
440
+ "operationId": "web_user_info_get",
441
+ "summary": "GET api/web/user-info",
442
+ "x-oj-file": "web/user-info/api.ts",
443
+ "tags": [
444
+ "web"
445
+ ],
446
+ "responses": {
447
+ "200": {
448
+ "description": "OK"
449
+ }
450
+ }
451
+ }
452
+ }
453
+ },
454
+ "tags": [
455
+ {
456
+ "name": "auth"
457
+ },
458
+ {
459
+ "name": "notification"
460
+ },
461
+ {
462
+ "name": "notifications"
463
+ },
464
+ {
465
+ "name": "personal-center"
466
+ },
467
+ {
468
+ "name": "web"
469
+ }
470
+ ]
471
+ }
@@ -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 = "/auth";
16
+
17
+ /** 认领身份前缀(契约 apiPrefix,字面等于模块目录名):跨模块认领护栏对比基准 */
18
+ export const MODULE_PREFIX = "/auth";
19
+
20
+ /**
21
+ * 模块 entry 的 onInit 里一行创建(构造即 install):完成 apiPrefix 登记 +
22
+ * scoped request 绑定,返回绑好前缀的端点函数集合。一模块一 client;
23
+ * 重复构造幂等安全(前缀重复登记同值、request 重绑同值)。
24
+ */
25
+ export function createAuthClient(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 { authLoginPost, authLogoutPost, authRefreshPost };
37
+ }
38
+
39
+ /**
40
+ * @deprecated 请改用 createAuthClient(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 里调用 createAuthClient(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 authLoginPost(): Promise<unknown> {
68
+ const client = ensureReq();
69
+ try {
70
+ const env = await client.post(`auth/login`).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 authLogoutPost(): Promise<unknown> {
83
+ const client = ensureReq();
84
+ try {
85
+ const env = await client.post(`auth/logout`).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 authRefreshPost(): Promise<unknown> {
98
+ const client = ensureReq();
99
+ try {
100
+ const env = await client.post(`auth/refresh`).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
+ }