@oj-module/cli 0.1.16 → 0.1.17

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.
package/devkit/SKILL.md CHANGED
@@ -110,6 +110,9 @@ description: 用 ojm 开发前后端一体工程时使用——新增/修改前
110
110
  | 通知铃 404 `no route matched` | 缺 root 级 `/api/notifications` | 补 `api/src/notifications` 模块 |
111
111
  | `oj serve` 启动即退,报 `config uses removed server.* site keys (v0.1.58): api_prefix` | 用了 oj v0.1.58 起删除的旧站点键(`api_prefix`/`app_path`/`app_prefix`/`static_sites`/`app_spa_fallback`) | 改 `server.api_prefix: /api` 为顶层 `mounts: [{prefix: "/api", api: "src"}]`;迁移完整 YAML 由报错给出 |
112
112
  | 改完 `mounts:` 仍全 404 | `api:` 指向目录不是 oj 启动目录下的 `src` | `mounts[].api` 写 `"src"`,`-c` 指向 `api/config.yaml`(ojm dev/preview 已内置) |
113
+ | `oj serve` 启动即退,报 `module '_shared' missing manifest.yaml` | 把共享后端代码放到了顶层 `api/src/_shared/`(oj 把 `api/src` 顶层目录都当模块,缺 `manifest.yaml`) | 嵌进所属模块目录:`api/src/<模块>/_shared/`,相对导入 |
114
+ | 后端 `import '@oj-module/runtime/...'` 报 `escapes project root` | oj 解析裸 specifier 以 `api/` 为项目根并钳制不越界,`@oj-module/runtime` 符号链接在 `packages/runtime`(项目根外) | 后端原语/类型放 `api/src` 内相对导入;跨模块共享类型仍以 runtime 为单一真源,两端形状手动同步 |
115
+ | 跨用户删/读他人通知不报错却像没生效 | 原语对不存在/非本人的行 `UPDATE 0` 行返回 `ok`,没区分归属 | 原语(`delNotice`/`markRead`)返回 `boolean`/`count`,端点据 `false`/`0` 映射 `json.fail(403, "forbidden")` |
113
116
 
114
117
  ## 手册
115
118
 
package/devkit/manual.md CHANGED
@@ -584,6 +584,47 @@ export default defineModule({
584
584
  });
585
585
  ```
586
586
 
587
+ **⑤ 跨模块发射原语(任何后端模块都能推通知)**
588
+
589
+ 通知不是「只有通知模块自己用」的功能——下单、审核、@我等其它模块也需要给某个人推一条提醒。
590
+ 把发射逻辑收敛成 `api/src/notification/_shared/notify.ts` 里的纯函数原语,其它模块**相对导入直接调用,
591
+ 无需走 HTTP**:
592
+
593
+ ```ts
594
+ // 在其它模块的 handler 里
595
+ import { pushNotice } from "../../notification/_shared/notify";
596
+
597
+ export default {
598
+ async post() {
599
+ // ...业务逻辑...
600
+ const id = await pushNotice({ userId: 2, title: "你的订单已发货", message: "单号 SO123" });
601
+ json.ok({ id }); // 返回命名空间 id "u:<id>"
602
+ },
603
+ };
604
+ ```
605
+
606
+ 原语清单(`_shared/notify.ts`,设计文档 §4.3 / §6):
607
+
608
+ | 原语 | 作用 | 返回 |
609
+ |---|---|---|
610
+ | `pushNotice({ userId, title, message, link?, avatar?, category? })` | 给指定用户推一条按人通知(一行 `notifications`) | 命名空间 id `"u:<id>"` |
611
+ | `pushBroadcastNotice({ title, message, ... })` | 广播一条(一行 `notifications_broadcast`,全员可见、一行存储) | 命名空间 id `"b:<id>"` |
612
+ | `delNotice(id, userId)` | 删按人项(校验归属,非本人返回 `false`);广播项仅删本人回执 | `boolean`(是否命中) |
613
+ | `noticesFor(userId)` | 合并列表:广播 LEFT JOIN 回执算 `is_read` UNION 按人 `WHERE user_id=?` | `NoticeItem[]` |
614
+ | `markRead(id, userId)` | 按人项校验归属(非本人 `false` → 端点映射 403);广播项 upsert 回执 | `boolean` |
615
+ | `markAllRead(userId)` / `clearNotices(userId)` | 全部已读 / 清空(按当前用户) | `void` |
616
+
617
+ 约定:
618
+ - **原语不做业务判断**(不判空间/角色/可见性),收件人过滤是发送方责任(`pushNotice` 的 `userId` 谁都能传,按人隔离靠存储 `user_id` 保证)。
619
+ - **后端不能 `import '@oj-module/runtime'`**:oj 解析裸 specifier 以 `api/` 为项目根并钳制不越界(越界报
620
+ `escapes project root`);`@oj-module/runtime` 符号链接在 `packages/runtime`,属项目根之外。原语必须落在
621
+ `api/src` 内(相对导入);类型与 runtime 的 `@oj-module/runtime/notify` 保持同形即可,共享契约类型单一真源在 runtime。
622
+ - **`_shared` 必须嵌进所属模块目录**(`api/src/notification/_shared/`),不能放 `api/src/_shared/`——oj 把
623
+ `api/src` 的**顶层**目录都当模块(须有 `manifest.yaml`),顶层 `_shared` 会被当成缺 manifest 的模块,
624
+ `oj serve` 直接退(`module '_shared' missing manifest.yaml`)。
625
+ - **归属校验返回 403**:跨用户 `del` / `read` 他人按人项,原语返回 `false`,端点 `json.fail(403, "forbidden")`——
626
+ 与「按人隔离」对称(A 推给 B 后 A 不可见、B 不可删/读 A 的)。
627
+
587
628
  **验证**:`pnpm dev` → `admin / 123456` 登录 → 右上角铃铛有蓝点 → 点开 →
588
629
  点任意一条蓝点消失 → 「全部已读」全清 → 「清空」列表变空。
589
630
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@oj-module/cli",
3
3
  "type": "module",
4
- "version": "0.1.16",
4
+ "version": "0.1.17",
5
5
  "description": "React Antd Admin 模块工程命令行工具(含预构建宿主 shell-dist)",
6
6
  "license": "MIT",
7
7
  "exports": {
@@ -49,7 +49,7 @@
49
49
  ],
50
50
  "dependencies": {
51
51
  "@faker-js/faker": "^10.4.0",
52
- "@oj-module/runtime": "0.1.16",
52
+ "@oj-module/runtime": "0.1.17",
53
53
  "@redocly/cli": "^2.51.0",
54
54
  "@tailwindcss/vite": "^4.3.0",
55
55
  "esbuild": "^0.28.2",
@@ -122,6 +122,36 @@ function unregisterApiProviders(moduleName) {
122
122
  var systemCurrent, notificationsCurrent, uploadCurrent, routesCurrent;
123
123
  var init_api_provider = __esmMin((() => {}));
124
124
  //#endregion
125
+ //#region src/api/notify/index.ts
126
+ init_api_provider();
127
+ var NOT_REGISTERED = "NotificationsBackendNotRegistered";
128
+ function requirePushProvider() {
129
+ const p = getNotificationsApiProvider();
130
+ if (!p || typeof p.push !== "function" || typeof p.pushBroadcast !== "function" || typeof p.del !== "function") throw new Error(`[notify] ${NOT_REGISTERED}:未注册 notificationsApi provider 的发射方法(push/pushBroadcast/del)。`);
131
+ return p;
132
+ }
133
+ /**
134
+ * 跨模块通知发射原语(前端侧 facade)。委托给已注册 notificationsApi provider 的
135
+ * 发射方法(经 HTTP 落到后端通知模块)。后端模块请改用 `api/src/_shared/notify`
136
+ * (直接 db 写入两表)——那是真正的服务端原语。
137
+ *
138
+ * 语义:原语不做任何业务判断(不判空间/角色/可见性),收件人过滤是发送方责任。
139
+ */
140
+ var notify = {
141
+ /** 给指定用户推一条按人通知,返回命名空间 id "u:<id>" */
142
+ push(input) {
143
+ return requirePushProvider().push(input);
144
+ },
145
+ /** 广播一条通知(全员可见,一行存储),返回命名空间 id "b:<id>" */
146
+ pushBroadcast(input) {
147
+ return requirePushProvider().pushBroadcast(input);
148
+ },
149
+ /** 删除通知:按人项校验归属(非本人删自己);广播项仅删本人回执 */
150
+ del(id, userId) {
151
+ return requirePushProvider().del(id, userId);
152
+ }
153
+ };
154
+ //#endregion
125
155
  //#region src/router/extra-info/route-path.ts
126
156
  /**
127
157
  * 判断 pathname 是否为登录页(basename 感知,P0)。
@@ -225,10 +255,10 @@ function getAppInfo() {
225
255
  return {
226
256
  "pkg": {
227
257
  "name": "@oj-module/runtime",
228
- "version": "0.1.16",
258
+ "version": "0.1.17",
229
259
  "license": "MIT"
230
260
  },
231
- "lastBuildTime": "2026-10-13 10:35:26"
261
+ "lastBuildTime": "2026-10-13 22:01:51"
232
262
  };
233
263
  }
234
264
  var init_get_app_info = __esmMin((() => {}));
@@ -4429,6 +4459,7 @@ var init_notification = __esmMin((() => {
4429
4459
  const handleClick = (item) => {
4430
4460
  if (readOnly) return;
4431
4461
  onEventChange && onEventChange("read", item);
4462
+ if (item.link) window.open(item.link, "_blank", "noopener");
4432
4463
  };
4433
4464
  dot = useMemo(() => {
4434
4465
  return !!notifications?.filter((item) => !item?.isRead).length;
@@ -27416,4 +27447,4 @@ init_ri();
27416
27447
  init_menu_icons();
27417
27448
  init_tree();
27418
27449
  //#endregion
27419
- export { AccessControl, AccessControlRoles, AntdApp, BasicButton, BasicContent, BasicTable, EmbeddedIcon, ErrorBoundary, ExternalIcon, FormAvatarItem, FormTreeItem, Iframe, LayoutCenterIcon, LayoutEffects, LayoutLeftIcon, LayoutRightIcon, MixedNavigationIcon, OutsidePageIcon, PageError, ProfileCardIcon, RiAccountCircleLine, RiContrastFill, RiFullscreenExitLine, RiFullscreenLine, RiMailCheckLine, RiMoonIcon, RiReactjsLine, RiSunIcon, RiUserSettingsLine, ServerErrorIcon, SideNavigationIcon, TopNavigationIcon, TwoColumnNavigationIcon, accessControlCodes, defineModule, fetchAddMenuItem, fetchAddRoleItem, fetchAsyncRoutes, fetchDeleteMenuItem, fetchDeleteRoleItem, fetchLogin, fetchLogout, fetchMenuByRoleId, fetchMenuList, fetchRefreshToken, fetchRoleList, fetchRoleMenu, fetchUpdateMenuItem, fetchUpdateRoleItem, fetchUserInfo, filterTree, getAllExpandedKeys, getAppInfo, getBooleanOptions, getModule, getModules, getRedirectPath, getRegisteredApiPrefix, getRegisteredStore, getRoutes, getYesNoOptions, handleTree, loadAll, mapTree, menuIcons, permissionPrefix, setupI18n, traverseTreeValues, unloadModule, useAccess, useAuthStore, usePreferences, useSlotNodes, useTabsStore, useUserStore, external_exports as z };
27450
+ export { AccessControl, AccessControlRoles, AntdApp, BasicButton, BasicContent, BasicTable, EmbeddedIcon, ErrorBoundary, ExternalIcon, FormAvatarItem, FormTreeItem, Iframe, LayoutCenterIcon, LayoutEffects, LayoutLeftIcon, LayoutRightIcon, MixedNavigationIcon, OutsidePageIcon, PageError, ProfileCardIcon, RiAccountCircleLine, RiContrastFill, RiFullscreenExitLine, RiFullscreenLine, RiMailCheckLine, RiMoonIcon, RiReactjsLine, RiSunIcon, RiUserSettingsLine, ServerErrorIcon, SideNavigationIcon, TopNavigationIcon, TwoColumnNavigationIcon, accessControlCodes, defineModule, fetchAddMenuItem, fetchAddRoleItem, fetchAsyncRoutes, fetchDeleteMenuItem, fetchDeleteRoleItem, fetchLogin, fetchLogout, fetchMenuByRoleId, fetchMenuList, fetchRefreshToken, fetchRoleList, fetchRoleMenu, fetchUpdateMenuItem, fetchUpdateRoleItem, fetchUserInfo, filterTree, getAllExpandedKeys, getAppInfo, getBooleanOptions, getModule, getModules, getRedirectPath, getRegisteredApiPrefix, getRegisteredStore, getRoutes, getYesNoOptions, handleTree, loadAll, mapTree, menuIcons, notify, permissionPrefix, setupI18n, traverseTreeValues, unloadModule, useAccess, useAuthStore, usePreferences, useSlotNodes, useTabsStore, useUserStore, external_exports as z };
@@ -5,9 +5,9 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <link rel="icon" href="/favicon.ico" />
7
7
  <title>React Antd Admin · Shell</title>
8
- <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' https://modules.cdn.example.com 'wasm-unsafe-eval' 'nonce-aHFPg951dQXbJx2TRjf5iw=='; script-src-attr 'none'; connect-src 'self' data: https://modules.cdn.example.com; style-src 'self' https://modules.cdn.example.com 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; frame-src https://ant.design https://react.dev https://condorheroblog.github.io; form-action 'none'; base-uri 'none'; object-src 'none'; upgrade-insecure-requests" />
8
+ <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' https://modules.cdn.example.com 'wasm-unsafe-eval' 'nonce-cEEui3MttOAqCTHlBg5Xog=='; script-src-attr 'none'; connect-src 'self' data: https://modules.cdn.example.com; style-src 'self' https://modules.cdn.example.com 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; frame-src https://ant.design https://react.dev https://condorheroblog.github.io; form-action 'none'; base-uri 'none'; object-src 'none'; upgrade-insecure-requests" />
9
9
 
10
- <script type="importmap" nonce="aHFPg951dQXbJx2TRjf5iw==">{
10
+ <script type="importmap" nonce="cEEui3MttOAqCTHlBg5Xog==">{
11
11
  "imports": {
12
12
  "react": "/assets/react.js",
13
13
  "react/jsx-runtime": "/assets/jsx-runtime.js",
@@ -7,11 +7,11 @@
7
7
  "react-router": "7.18.2",
8
8
  "react-router/dom": "7.18.2",
9
9
  "@tanstack/react-query": "5.102.8",
10
- "@oj-module/runtime": "0.1.16",
11
- "@oj-module/runtime/contract": "0.1.16",
12
- "@oj-module/runtime/contract/errors": "0.1.16",
13
- "@plane/kit": "0.1.16",
14
- "@plane/editor": "0.1.16",
10
+ "@oj-module/runtime": "0.1.17",
11
+ "@oj-module/runtime/contract": "0.1.17",
12
+ "@oj-module/runtime/contract/errors": "0.1.17",
13
+ "@plane/kit": "0.1.17",
14
+ "@plane/editor": "0.1.17",
15
15
  "antd": "6.6.1",
16
16
  "antd/locale/en_US": "6.6.1",
17
17
  "antd/locale/zh_CN": "6.6.1",
@@ -3,17 +3,20 @@ import { defineApi, z } from "@oj-module/runtime/contract";
3
3
  /**
4
4
  * notification 模块契约(uni-dev 形态,apiPrefix 字面等于 "/notification",AC-D9)。
5
5
  * 通知读写端点——前端经 ctx.register.notificationsApi 全量接管内置通知 API(D9 + G4):
6
- * 读 + 单条已读 + 全部已读 + 清空,四端点全必填。
6
+ * 读 + 单条已读 + 全部已读 + 清空,四端点全必填;另含发射原语端点 push/push-broadcast/del。
7
7
  */
8
8
 
9
- /** 通知项 */
9
+ /** 通知项(id 为命名空间串:"u:<id>" 按人 / "b:<id>" 广播) */
10
10
  const notificationItem = z.object({
11
- id: z.number(),
11
+ id: z.string(),
12
+ scope: z.enum(["user", "broadcast"]),
12
13
  avatar: z.string(),
13
14
  date: z.string(),
14
15
  isRead: z.boolean().optional(),
15
16
  message: z.string(),
16
17
  title: z.string(),
18
+ link: z.string().nullable().optional(),
19
+ category: z.enum(["notice", "pm", "mention"]).optional(),
17
20
  });
18
21
 
19
22
  /* 通知列表 */
@@ -21,7 +24,7 @@ export const fetchNotifications = defineApi({
21
24
  apiPrefix: "/notification",
22
25
  route: "/notifications",
23
26
  data: z.array(notificationItem),
24
- description: "通知列表",
27
+ description: "通知列表(按人 + 广播合并)",
25
28
  });
26
29
 
27
30
  /* 单条已读 */
@@ -30,7 +33,7 @@ export const markRead = defineApi({
30
33
  route: "/notifications/read",
31
34
  method: "POST",
32
35
  body: z.object({
33
- id: z.number(),
36
+ id: z.string(),
34
37
  }),
35
38
  description: "单条通知已读",
36
39
  });
@@ -50,3 +53,46 @@ export const clearAll = defineApi({
50
53
  method: "POST",
51
54
  description: "清空通知",
52
55
  });
56
+
57
+ /* 推按人通知(发射原语 HTTP 入口) */
58
+ export const push = defineApi({
59
+ apiPrefix: "/notification",
60
+ route: "/push",
61
+ method: "POST",
62
+ body: z.object({
63
+ userId: z.union([z.string(), z.number()]).optional(),
64
+ title: z.string(),
65
+ message: z.string(),
66
+ link: z.string().optional(),
67
+ avatar: z.string().optional(),
68
+ category: z.enum(["notice", "pm", "mention"]).optional(),
69
+ }),
70
+ description: "推按人通知",
71
+ });
72
+
73
+ /* 广播通知(全员可见,一行存储) */
74
+ export const pushBroadcast = defineApi({
75
+ apiPrefix: "/notification",
76
+ route: "/push-broadcast",
77
+ method: "POST",
78
+ body: z.object({
79
+ title: z.string(),
80
+ message: z.string(),
81
+ link: z.string().optional(),
82
+ avatar: z.string().optional(),
83
+ category: z.enum(["notice", "pm", "mention"]).optional(),
84
+ }),
85
+ description: "广播通知",
86
+ });
87
+
88
+ /* 删除通知(按人项校验归属,非本人 403;广播项仅删本人回执) */
89
+ export const del = defineApi({
90
+ apiPrefix: "/notification",
91
+ route: "/del",
92
+ method: "POST",
93
+ body: z.object({
94
+ id: z.string(),
95
+ userId: z.union([z.string(), z.number()]).optional(),
96
+ }),
97
+ description: "删除通知",
98
+ });
@@ -4,19 +4,47 @@
4
4
  import { z } from "@oj-module/runtime";
5
5
 
6
6
  export const schemas = {
7
+ del: {
8
+ body: z.object({
9
+ id: z.string(),
10
+ userId: z.union([z.string(), z.number()]).optional(),
11
+ }),
12
+ },
7
13
  fetchNotifications: {
8
14
  data: z.array(z.object({
9
- id: z.number(),
15
+ id: z.string(),
16
+ scope: z.enum(["user","broadcast"]),
10
17
  avatar: z.string(),
11
18
  date: z.string(),
12
19
  isRead: z.boolean().optional(),
13
20
  message: z.string(),
14
21
  title: z.string(),
22
+ link: z.string().nullable().optional(),
23
+ category: z.enum(["notice","pm","mention"]).optional(),
15
24
  })),
16
25
  },
17
26
  markRead: {
18
27
  body: z.object({
19
- id: z.number(),
28
+ id: z.string(),
29
+ }),
30
+ },
31
+ push: {
32
+ body: z.object({
33
+ userId: z.union([z.string(), z.number()]).optional(),
34
+ title: z.string(),
35
+ message: z.string(),
36
+ link: z.string().optional(),
37
+ avatar: z.string().optional(),
38
+ category: z.enum(["notice","pm","mention"]).optional(),
39
+ }),
40
+ },
41
+ pushBroadcast: {
42
+ body: z.object({
43
+ title: z.string(),
44
+ message: z.string(),
45
+ link: z.string().optional(),
46
+ avatar: z.string().optional(),
47
+ category: z.enum(["notice","pm","mention"]).optional(),
20
48
  }),
21
49
  },
22
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, fetchNotifications, markAllRead, markRead };
36
+ return { clearAll, del, fetchNotifications, markAllRead, markRead, push, pushBroadcast };
37
37
  }
38
38
 
39
39
  /**
@@ -79,6 +79,23 @@ export async function clearAll(): Promise<unknown> {
79
79
  }
80
80
  }
81
81
 
82
+ export type DelBody = z.input<(typeof schemas)["del"]["body"]>;
83
+
84
+ export async function del(body: DelBody): Promise<unknown> {
85
+ const client = ensureReq();
86
+ try {
87
+ const env = await client.post(`notification/del`, { json: body }).json<OjEnvelope<unknown>>();
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 unknown;
92
+ return data;
93
+ }
94
+ catch (e) {
95
+ throw await toApiError(e);
96
+ }
97
+ }
98
+
82
99
  export type FetchNotificationsData = z.infer<(typeof schemas)["fetchNotifications"]["data"]>;
83
100
 
84
101
  export async function fetchNotifications(): Promise<FetchNotificationsData> {
@@ -133,3 +150,37 @@ export async function markRead(body: MarkReadBody): Promise<unknown> {
133
150
  throw await toApiError(e);
134
151
  }
135
152
  }
153
+
154
+ export type PushBody = z.input<(typeof schemas)["push"]["body"]>;
155
+
156
+ export async function push(body: PushBody): Promise<unknown> {
157
+ const client = ensureReq();
158
+ try {
159
+ const env = await client.post(`notification/push`, { 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 PushBroadcastBody = z.input<(typeof schemas)["pushBroadcast"]["body"]>;
172
+
173
+ export async function pushBroadcast(body: PushBroadcastBody): Promise<unknown> {
174
+ const client = ensureReq();
175
+ try {
176
+ const env = await client.post(`notification/push-broadcast`, { 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
+ }
@@ -7,9 +7,10 @@ import { createNotificationClient } from "./client/api";
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",
@@ -26,9 +27,17 @@ export default defineModule({
26
27
  const notificationClient = createNotificationClient(ctx);
27
28
  const provider: NotificationsApiProvider = {
28
29
  fetchNotifications: () => notificationClient.fetchNotifications(),
29
- markRead: id => notificationClient.markRead({ id: Number(id) }).then(() => {}),
30
+ // id 为命名空间串("u:<id>" / "b:<id>"),原样透传
31
+ markRead: id => notificationClient.markRead({ id: String(id) }).then(() => {}),
30
32
  markAllRead: () => notificationClient.markAllRead().then(() => {}),
31
33
  clearAll: () => notificationClient.clearAll().then(() => {}),
34
+ push: input => notificationClient
35
+ .push({ userId: input.userId, title: input.title, message: input.message, link: input.link, avatar: input.avatar, category: input.category })
36
+ .then(r => String((r as { id: string }).id)),
37
+ pushBroadcast: input => notificationClient
38
+ .pushBroadcast({ title: input.title, message: input.message, link: input.link, avatar: input.avatar, category: input.category })
39
+ .then(r => String((r as { id: string }).id)),
40
+ del: (id, userId) => notificationClient.del({ id: String(id), userId }).then(() => {}),
32
41
  };
33
42
  ctx.register.notificationsApi(provider);
34
43
  },
@@ -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.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",
16
16
  "antd": "6.6.1",
17
17
  "antd/locale/en_US": "6.6.1",
18
18
  "antd/locale/zh_CN": "6.6.1",