@oj-module/cli 0.2.49 → 0.2.53

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 (75) hide show
  1. package/devkit/README.md +8 -6
  2. package/devkit/SKILL.md +32 -12
  3. package/devkit/manual.md +6 -3
  4. package/devkit/wecom-daikaifa.md +95 -1
  5. package/devkit/wecom-internal-app.md +750 -0
  6. package/package.json +2 -2
  7. package/shell-dist/assets/{index-FYt9CjFd.js → index-e_Mp2mb5.js} +5 -1
  8. package/shell-dist/assets/runtime.js +24 -3
  9. package/shell-dist/index.html +9 -12
  10. package/shell-dist/versions.json +3 -3
  11. package/src/build.ts +19 -2
  12. package/src/init.ts +5 -4
  13. package/src/shared-deps.ts +27 -9
  14. package/src/site-base.ts +1 -1
  15. package/templates/config.yaml +28 -6
  16. package/templates/init.include.yaml +4 -7
  17. package/templates/openapi.json +2636 -2356
  18. package/templates/src/_platform/api/_shared/service/domain/users.ts +40 -4
  19. package/templates/src/auth/api/_shared/wecom/admin.ts +91 -7
  20. package/templates/src/auth/api/_shared/wecom/client.ts +9 -2
  21. package/templates/src/auth/api/_shared/wecom/config.ts +256 -46
  22. package/templates/src/auth/api/_shared/wecom/contacts.ts +7 -1
  23. package/templates/src/auth/api/_shared/wecom/events.ts +28 -6
  24. package/templates/src/auth/api/_shared/wecom/login.ts +21 -13
  25. package/templates/src/auth/api/_shared/wecom/store.ts +61 -12
  26. package/templates/src/auth/api/_shared/wecom/sync.ts +11 -1
  27. package/templates/src/auth/api/_shared/wecom/tenant-admin.ts +194 -0
  28. package/templates/src/auth/api/migrations/0003__credential_kind.mysql.sql +3 -0
  29. package/templates/src/auth/api/migrations/0003__credential_kind.pg.sql +3 -0
  30. package/templates/src/auth/api/migrations/0003__credential_kind.sql +9 -0
  31. package/templates/src/auth/api/schema.yaml +2 -0
  32. package/templates/src/auth/api/wecom/admin/api.ts +112 -11
  33. package/templates/src/auth/api/wecom/admin/tenant-admin/api.ts +41 -0
  34. package/templates/src/auth/api/wecom/admin/tenant-admin/password/api.ts +66 -0
  35. package/templates/src/auth/api/wecom/api.ts +102 -48
  36. package/templates/src/auth/api/wecom/sync/api.ts +21 -5
  37. package/templates/src/auth/web/__client/api.schemas.ts +22 -1
  38. package/templates/src/auth/web/__client/api.ts +47 -1
  39. package/templates/src/login/web/pages/login.tsx +54 -24
  40. package/templates/src/system/api/settings/api.ts +3 -3
  41. package/templates/src/system/web/entry.ts +4 -4
  42. package/templates/src/system/web/locales/en-US.json +38 -1
  43. package/templates/src/system/web/locales/zh-CN.json +38 -1
  44. package/templates/src/system/web/pages/departments/sync-bar.tsx +32 -7
  45. package/templates/src/system/web/pages/users/index.tsx +7 -4
  46. package/templates/src/system/web/pages/users/roles.tsx +14 -3
  47. package/templates/src/system/web/pages/wecom/index.tsx +292 -45
  48. package/templates/tests/wecom-admin.spec.ts +7 -7
  49. package/templates/tests/wecom-connector.spec.ts +7 -6
  50. package/templates/tests/wecom-corp.spec.ts +523 -0
  51. package/templates/tests/wecom-endpoints.spec.ts +10 -4
  52. package/templates/tests/wecom-org-sync.spec.ts +7 -1
  53. package/templates/tsconfig.paths.json +0 -1
  54. package/templates/web.config.ts +1 -2
  55. package/vendor/host-versions.json +3 -3
  56. package/shell-dist/assets/echarts-charts.js +0 -58571
  57. package/shell-dist/assets/echarts-features.js +0 -24057
  58. package/shell-dist/assets/echarts-for-react.js +0 -704
  59. package/shell-dist/assets/echarts.js +0 -82916
  60. package/shell-dist/assets/editor.js +0 -161457
  61. package/shell-dist/assets/vditor-index-css.js +0 -7
  62. package/shell-dist/assets/vditor.js +0 -15223
  63. package/templates/src/demo/api/manifest.yaml +0 -5
  64. package/templates/src/demo/api/migrations/0001__create_todos.sql +0 -9
  65. package/templates/src/demo/api/migrations/0002__tenant.mysql.sql +0 -2
  66. package/templates/src/demo/api/migrations/0002__tenant.sql +0 -2
  67. package/templates/src/demo/api/schema.yaml +0 -12
  68. package/templates/src/demo/api/seed.sql +0 -5
  69. package/templates/src/demo/api/todos/api.ts +0 -59
  70. package/templates/src/demo/web/__client/api.schemas.ts +0 -20
  71. package/templates/src/demo/web/__client/api.ts +0 -89
  72. package/templates/src/demo/web/entry.ts +0 -42
  73. package/templates/src/demo/web/locales/en-US.json +0 -10
  74. package/templates/src/demo/web/locales/zh-CN.json +0 -10
  75. package/templates/src/demo/web/pages/index.tsx +0 -45
@@ -1,6 +1,6 @@
1
1
  import { request, useUserStore } from "@oj-module/runtime";
2
2
  import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
3
- import { App, Button, Card, Descriptions, Popconfirm, QRCode, Space, Table, Tag, Typography } from "antd";
3
+ import { Alert, App, Button, Card, Descriptions, Input, Modal, Popconfirm, QRCode, Space, Table, Tag, Typography } from "antd";
4
4
  import { useEffect, useState } from "react";
5
5
  import { useTranslation } from "react-i18next";
6
6
 
@@ -16,7 +16,9 @@ interface AppRow {
16
16
  env: string
17
17
  corp_name: string | null
18
18
  status: "active" | "disabled"
19
- /** 0=管理员授权 / 1=成员授权:成员授权下这家企业的应用做不了组织架构同步 */
19
+ /** 应用来源:suite=代开发 / corp=企业内部应用(自建) */
20
+ credential_kind: "suite" | "corp"
21
+ /** 0=管理员授权 / 1=成员授权:成员授权下这家企业的应用做不了组织架构同步;自建行恒 0 */
20
22
  auth_mode: number
21
23
  installed_at: number
22
24
  update_time: number
@@ -28,6 +30,33 @@ interface InstallInfo {
28
30
  expiresIn: number
29
31
  }
30
32
 
33
+ /** GET /auth/wecom/admin/overview:本部署配了哪几套凭据、回调地址填什么 */
34
+ interface Overview {
35
+ kinds: Array<"suite" | "corp">
36
+ configError: string | null
37
+ callbackUrl: string | null
38
+ corpId: string | null
39
+ agentId: string | null
40
+ corpCallbackConfigured: boolean
41
+ }
42
+
43
+ interface ConnectResult {
44
+ corpId: string
45
+ agentId: string
46
+ departments: number
47
+ members: number
48
+ }
49
+
50
+ /** GET /auth/wecom/admin/tenant-admin:某企业的租户管理员状态 */
51
+ interface TenantAdminState {
52
+ corpId: string
53
+ username: string
54
+ /** 该租户是否已有一个握着 admin 角色的账号 */
55
+ exists: boolean
56
+ /** 是否须改密(开通后恒 true,直到本人改过一次) */
57
+ mustChange: boolean
58
+ }
59
+
31
60
  /**
32
61
  * 端点不在本模块的生成 client 里(`__client` 按模块切分),故直接用 runtime 的 ky 出口。
33
62
  * 路径不带前导 `/`:与 request 的 baseURL 拼接口径一致(fetchLogin 同款)。
@@ -37,6 +66,11 @@ async function fetchApps(): Promise<AppRow[]> {
37
66
  return env.data ?? [];
38
67
  }
39
68
 
69
+ async function fetchOverview(): Promise<Overview> {
70
+ const env = await request.get("auth/wecom/admin/overview").json<{ code: number, data?: Overview }>();
71
+ return env.data ?? { kinds: [], configError: null, callbackUrl: null, corpId: null, agentId: null, corpCallbackConfigured: false };
72
+ }
73
+
40
74
  async function setStatus(input: { corpId: string, agentId: string, status: "active" | "disabled" }): Promise<{ updated: number, revokedSessions: number }> {
41
75
  const env = await request.post("auth/wecom/admin/status", { json: input }).json<{ code: number, data?: { updated: number, revokedSessions: number } }>();
42
76
  return env.data ?? { updated: 0, revokedSessions: 0 };
@@ -49,12 +83,37 @@ async function createInstall(): Promise<InstallInfo> {
49
83
  return env.data;
50
84
  }
51
85
 
86
+ async function connectCorp(): Promise<ConnectResult> {
87
+ const env = await request.post("auth/wecom/admin/connect", { json: {} }).json<{ code: number, data?: ConnectResult }>();
88
+ if (!env.data)
89
+ throw new Error("企业微信未返回接入结果");
90
+ return env.data;
91
+ }
92
+
93
+ async function fetchTenantAdmin(corpId: string): Promise<TenantAdminState> {
94
+ const env = await request.get("auth/wecom/admin/tenant-admin", { searchParams: { corpId } }).json<{ code: number, data?: TenantAdminState }>();
95
+ return env.data ?? { corpId, username: "admin", exists: false, mustChange: false };
96
+ }
97
+
52
98
  /**
53
- * 企微代开发后台(服务商运营面,superadmin 独占):
54
- * · 已授权企业 × 应用清单(同一企业测试/正式两个应用各一行);
55
- * · 启停(停用企业最后一个应用时后端同时吊销该企业会话);
56
- * · 开通入口(点「生成开通链接」→ 后端调企微 get_customized_auth_url 换 qrcode_url,
57
- * 企业管理员扫/开即授权,授权成功由 create_auth 回调落库)。
99
+ * 重设租户管理员口令。
100
+ *
101
+ * 口令**不由系统生成**:让 superadmin 自己输入,他知道这个值 → 直接告知对方即可。
102
+ * 后端只存 bcrypt,没有任何可逆副本,所以「系统生成后回显」这条路本来也走不通。
103
+ */
104
+ async function resetTenantAdminPassword(input: { corpId: string, password: string }): Promise<TenantAdminState> {
105
+ const env = await request.post("auth/wecom/admin/tenant-admin/password", { json: input }).json<{ code: number, data?: TenantAdminState }>();
106
+ if (!env.data)
107
+ throw new Error("企业微信未返回重设结果");
108
+ return env.data;
109
+ }
110
+
111
+ /**
112
+ * 企微后台(superadmin 独占),两块卡片按「本部署配了哪套凭据」决定显不显示:
113
+ * · 代开发:已授权企业 × 应用清单、启停、开通入口(点「生成开通链接」→ 后端调
114
+ * get_customized_auth_url 换 qrcode_url,企业管理员扫/开即授权,由 create_auth 回调落库);
115
+ * · 企业内部应用(自建):本企业的那一份凭据 + 回调地址 + 「验证并接入」。
116
+ * 两套可以同时启用(`wecom_allow_coexist: 'true'`),此时两张卡片并列,清单里按「来源」区分。
58
117
  */
59
118
  export default function WecomAdminPage() {
60
119
  const { t } = useTranslation();
@@ -83,6 +142,14 @@ export default function WecomAdminPage() {
83
142
  retry: false,
84
143
  });
85
144
 
145
+ // 概览读的是**部署常量**而不是库:配置错的时候清单可能是空的,而这里必须还能说清「缺什么」
146
+ const overviewQuery = useQuery({
147
+ queryKey: ["system", "wecom-overview"],
148
+ queryFn: fetchOverview,
149
+ enabled: isSuper,
150
+ retry: false,
151
+ });
152
+
86
153
  const statusMut = useMutation({
87
154
  mutationFn: setStatus,
88
155
  onSuccess: (r) => {
@@ -106,9 +173,92 @@ export default function WecomAdminPage() {
106
173
  onError: e => message.error(explainError(e, t)),
107
174
  });
108
175
 
176
+ const connectMut = useMutation({
177
+ mutationFn: connectCorp,
178
+ onSuccess: (r) => {
179
+ message.success(t("system:wecom.corp.connectOk", { depts: r.departments, members: r.members }));
180
+ queryClient.invalidateQueries({ queryKey: ["system", "wecom-apps"] });
181
+ queryClient.invalidateQueries({ queryKey: ["system", "wecom-overview"] });
182
+ },
183
+ onError: e => message.error(explainError(e, t)),
184
+ });
185
+
186
+ // 企业清单按 corp_id 去重:租户键就是 corp_id,同一企业的多个应用共用一个租户管理员
187
+ const corps = [...new Map((appsQuery.data ?? []).map(r => [r.corp_id, { corpId: r.corp_id, name: r.corp_name }])).values()];
188
+
189
+ // 租户管理员状态:一个企业一条查询并并发取(企业数量级很小,不值得为它加批量端点)
190
+ const tenantAdminQuery = useQuery({
191
+ queryKey: ["system", "wecom-tenant-admin", corps.map(c => c.corpId).join(",")],
192
+ queryFn: async () => {
193
+ const out: Record<string, TenantAdminState> = {};
194
+ await Promise.all(corps.map(async c => {
195
+ out[c.corpId] = await fetchTenantAdmin(c.corpId);
196
+ }));
197
+ return out;
198
+ },
199
+ enabled: isSuper && corps.length > 0,
200
+ retry: false,
201
+ });
202
+
203
+ const [resetTarget, setResetTarget] = useState<{ corpId: string, name: string | null } | null>(null);
204
+ const [resetPwd, setResetPwd] = useState("");
205
+
206
+ const resetMut = useMutation({
207
+ mutationFn: resetTenantAdminPassword,
208
+ onSuccess: () => {
209
+ message.success(t("system:wecom.tenantAdmin.resetOk"));
210
+ setResetTarget(null);
211
+ setResetPwd("");
212
+ queryClient.invalidateQueries({ queryKey: ["system", "wecom-tenant-admin"] });
213
+ },
214
+ onError: e => message.error(explainError(e, t)),
215
+ });
216
+
217
+ const tenantAdminColumns = [
218
+ {
219
+ title: t("system:wecom.col.corp"),
220
+ dataIndex: "corpId",
221
+ render: (_: string, row: { corpId: string, name: string | null }) => (
222
+ <div>
223
+ <div>{row.corpId}</div>
224
+ {row.name && <Typography.Text type="secondary">{row.name}</Typography.Text>}
225
+ </div>
226
+ ),
227
+ },
228
+ {
229
+ title: t("system:wecom.tenantAdmin.username"),
230
+ dataIndex: "username",
231
+ render: (_: unknown, row: { corpId: string }) => tenantAdminQuery.data?.[row.corpId]?.username ?? "-",
232
+ },
233
+ {
234
+ title: t("system:wecom.tenantAdmin.state"),
235
+ dataIndex: "exists",
236
+ render: (_: unknown, row: { corpId: string }) => {
237
+ const s = tenantAdminQuery.data?.[row.corpId];
238
+ return s?.exists
239
+ ? <Tag color="green">{t("system:wecom.tenantAdmin.exists")}</Tag>
240
+ : <Tag color="orange">{t("system:wecom.tenantAdmin.missing")}</Tag>;
241
+ },
242
+ },
243
+ {
244
+ title: t("system:wecom.tenantAdmin.action"),
245
+ key: "action",
246
+ render: (_: unknown, row: { corpId: string, name: string | null }) => (
247
+ <Button size="small" onClick={() => { setResetTarget(row); setResetPwd(""); }}>
248
+ {t("system:wecom.tenantAdmin.reset")}
249
+ </Button>
250
+ ),
251
+ },
252
+ ];
253
+
109
254
  if (!isSuper)
110
255
  return <ForbiddenPanel t={t} />;
111
256
 
257
+ const overview = overviewQuery.data;
258
+ const kinds = overview?.kinds ?? [];
259
+ const hasSuite = kinds.includes("suite");
260
+ const hasCorp = kinds.includes("corp");
261
+
112
262
  const columns = [
113
263
  {
114
264
  title: t("system:wecom.col.corp"),
@@ -120,16 +270,26 @@ export default function WecomAdminPage() {
120
270
  </div>
121
271
  ),
122
272
  },
273
+ {
274
+ // 来源列:共存部署里清单会同时出现两类行,不标出来就分不清「这行是谁装进来的」
275
+ title: t("system:wecom.col.source"),
276
+ dataIndex: "credential_kind",
277
+ render: (v: AppRow["credential_kind"]) => (v === "corp"
278
+ ? <Tag color="blue">{t("system:wecom.source.corp")}</Tag>
279
+ : <Tag>{t("system:wecom.source.suite")}</Tag>),
280
+ },
123
281
  { title: t("system:wecom.col.agent"), dataIndex: "agent_id" },
124
282
  { title: t("system:wecom.col.env"), dataIndex: "env", render: (v: string) => <Tag>{v}</Tag> },
125
283
  {
126
284
  // 权限三态:成员授权是「装了也同步不了」的硬约束(企微侧通讯录接口一律 60011),
127
- // 摆在清单上,服务商不用点进去试一遍才知道
285
+ // 摆在清单上,服务商不用点进去试一遍才知道。自建应用没有这个概念,显示空值。
128
286
  title: t("system:wecom.col.authMode"),
129
287
  dataIndex: "auth_mode",
130
- render: (v: number) => (v === 0
131
- ? <Tag color="green">{t("system:wecom.authModeAdmin")}</Tag>
132
- : <Tag color="orange">{t("system:wecom.authModeMember")}</Tag>),
288
+ render: (v: number, row: AppRow) => (row.credential_kind === "corp"
289
+ ? <Typography.Text type="secondary">{t("system:wecom.authModeNone")}</Typography.Text>
290
+ : v === 0
291
+ ? <Tag color="green">{t("system:wecom.authModeAdmin")}</Tag>
292
+ : <Tag color="orange">{t("system:wecom.authModeMember")}</Tag>),
133
293
  },
134
294
  {
135
295
  title: t("system:wecom.col.status"),
@@ -166,41 +326,128 @@ export default function WecomAdminPage() {
166
326
 
167
327
  return (
168
328
  <Space direction="vertical" size="middle" style={{ width: "100%" }}>
169
- <Card
170
- title={t("system:wecom.title")}
171
- extra={(
172
- <Button type="primary" loading={installMut.isPending} onClick={() => installMut.mutate()}>
173
- {t("system:wecom.genInstall")}
174
- </Button>
175
- )}
329
+ {overview?.configError && (
330
+ <Alert
331
+ type="error"
332
+ showIcon
333
+ message={t("system:wecom.configErrorTitle")}
334
+ description={overview.configError}
335
+ />
336
+ )}
337
+ {hasSuite && hasCorp && (
338
+ <Alert type="info" showIcon message={t("system:wecom.coexistHint")} />
339
+ )}
340
+ {hasSuite && (
341
+ <Card
342
+ title={t("system:wecom.title")}
343
+ extra={(
344
+ <Button type="primary" loading={installMut.isPending} onClick={() => installMut.mutate()}>
345
+ {t("system:wecom.genInstall")}
346
+ </Button>
347
+ )}
348
+ >
349
+ <Typography.Paragraph type="secondary">{t("system:wecom.hint")}</Typography.Paragraph>
350
+ {install && (
351
+ <div style={{ display: "flex", gap: 24, alignItems: "flex-start", flexWrap: "wrap" }}>
352
+ {/* 二维码用 antd 内置 QRCode(同一份共享 antd,不额外引二维码依赖):
353
+ 企业管理员多半在手机上收通知,扫码比复制链接快 */}
354
+ <QRCode
355
+ value={install.installUrl}
356
+ size={160}
357
+ type="svg"
358
+ bordered
359
+ status={expired ? "expired" : "active"}
360
+ onRefresh={() => installMut.mutate()}
361
+ />
362
+ <Descriptions column={1} size="small" bordered style={{ flex: 1, minWidth: 320 }}>
363
+ <Descriptions.Item label={t("system:wecom.installUrl")}>
364
+ <Typography.Text copyable={{ text: install.installUrl }}>{install.installUrl}</Typography.Text>
365
+ </Descriptions.Item>
366
+ <Descriptions.Item label={t("system:wecom.callbackUrl")}>
367
+ <Typography.Text copyable={{ text: install.callbackUrl }}>{install.callbackUrl}</Typography.Text>
368
+ </Descriptions.Item>
369
+ <Descriptions.Item label={t("system:wecom.expiresIn")}>
370
+ {t("system:wecom.expiresValue", { count: Math.round(install.expiresIn / 60) })}
371
+ </Descriptions.Item>
372
+ </Descriptions>
373
+ </div>
374
+ )}
375
+ </Card>
376
+ )}
377
+ {hasCorp && (
378
+ <Card
379
+ title={t("system:wecom.corp.title")}
380
+ extra={(
381
+ <Popconfirm
382
+ title={t("system:wecom.corp.connectConfirm")}
383
+ onConfirm={() => connectMut.mutate()}
384
+ >
385
+ <Button type="primary" loading={connectMut.isPending}>
386
+ {t("system:wecom.corp.connect")}
387
+ </Button>
388
+ </Popconfirm>
389
+ )}
390
+ >
391
+ <Typography.Paragraph type="secondary">{t("system:wecom.corp.hint")}</Typography.Paragraph>
392
+ <Descriptions column={1} size="small" bordered>
393
+ <Descriptions.Item label={t("system:wecom.corp.corpId")}>
394
+ <Typography.Text>{overview?.corpId ?? "-"}</Typography.Text>
395
+ </Descriptions.Item>
396
+ <Descriptions.Item label={t("system:wecom.corp.agentId")}>
397
+ <Typography.Text>{overview?.agentId ?? "-"}</Typography.Text>
398
+ </Descriptions.Item>
399
+ <Descriptions.Item label={t("system:wecom.corp.callbackUrl")}>
400
+ {overview?.callbackUrl
401
+ ? <Typography.Text copyable={{ text: overview.callbackUrl }}>{overview.callbackUrl}</Typography.Text>
402
+ : "-"}
403
+ </Descriptions.Item>
404
+ <Descriptions.Item label={t("system:wecom.corp.callbackConfigured")}>
405
+ {overview?.corpCallbackConfigured
406
+ ? <Tag color="green">{t("system:wecom.corp.callbackYes")}</Tag>
407
+ : <Tag color="orange">{t("system:wecom.corp.callbackNo")}</Tag>}
408
+ </Descriptions.Item>
409
+ </Descriptions>
410
+ <Typography.Paragraph type="secondary" style={{ marginTop: 12, marginBottom: 0 }}>
411
+ {t("system:wecom.corp.callbackHint")}
412
+ </Typography.Paragraph>
413
+ <Typography.Paragraph type="warning" style={{ marginBottom: 0 }}>
414
+ {t("system:wecom.corp.connectHint")}
415
+ </Typography.Paragraph>
416
+ </Card>
417
+ )}
418
+ {corps.length > 0 && (
419
+ <Card title={t("system:wecom.tenantAdmin.title")}>
420
+ <Typography.Paragraph type="secondary">
421
+ {t("system:wecom.tenantAdmin.hint")}
422
+ </Typography.Paragraph>
423
+ <Table
424
+ rowKey={row => row.corpId}
425
+ size="small"
426
+ loading={tenantAdminQuery.isLoading}
427
+ dataSource={corps}
428
+ columns={tenantAdminColumns}
429
+ pagination={false}
430
+ />
431
+ </Card>
432
+ )}
433
+ <Modal
434
+ open={resetTarget != null}
435
+ title={t("system:wecom.tenantAdmin.resetTitle")}
436
+ onCancel={() => { setResetTarget(null); setResetPwd(""); }}
437
+ onOk={() => resetMut.mutate({ corpId: resetTarget!.corpId, password: resetPwd })}
438
+ confirmLoading={resetMut.isPending}
439
+ okButtonProps={{ disabled: resetPwd.length < 6 }}
440
+ destroyOnHidden
176
441
  >
177
- <Typography.Paragraph type="secondary">{t("system:wecom.hint")}</Typography.Paragraph>
178
- {install && (
179
- <div style={{ display: "flex", gap: 24, alignItems: "flex-start", flexWrap: "wrap" }}>
180
- {/* 二维码用 antd 内置 QRCode(同一份共享 antd,不额外引二维码依赖):
181
- 企业管理员多半在手机上收通知,扫码比复制链接快 */}
182
- <QRCode
183
- value={install.installUrl}
184
- size={160}
185
- type="svg"
186
- bordered
187
- status={expired ? "expired" : "active"}
188
- onRefresh={() => installMut.mutate()}
189
- />
190
- <Descriptions column={1} size="small" bordered style={{ flex: 1, minWidth: 320 }}>
191
- <Descriptions.Item label={t("system:wecom.installUrl")}>
192
- <Typography.Text copyable={{ text: install.installUrl }}>{install.installUrl}</Typography.Text>
193
- </Descriptions.Item>
194
- <Descriptions.Item label={t("system:wecom.callbackUrl")}>
195
- <Typography.Text copyable={{ text: install.callbackUrl }}>{install.callbackUrl}</Typography.Text>
196
- </Descriptions.Item>
197
- <Descriptions.Item label={t("system:wecom.expiresIn")}>
198
- {t("system:wecom.expiresValue", { count: Math.round(install.expiresIn / 60) })}
199
- </Descriptions.Item>
200
- </Descriptions>
201
- </div>
202
- )}
203
- </Card>
442
+ <Typography.Paragraph type="secondary">
443
+ {t("system:wecom.tenantAdmin.resetHint", { corpId: resetTarget?.corpId ?? "" })}
444
+ </Typography.Paragraph>
445
+ <Input.Password
446
+ value={resetPwd}
447
+ onChange={e => setResetPwd(e.target.value)}
448
+ placeholder={t("system:wecom.tenantAdmin.resetPlaceholder")}
449
+ />
450
+ </Modal>
204
451
  <Card title={t("system:wecom.appsTitle")}>
205
452
  <Typography.Paragraph type="secondary" style={{ marginBottom: 12 }}>
206
453
  {t("system:wecom.authModeHint")}
@@ -15,7 +15,7 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
15
15
  import { assertSuperAdmin, invalidateAllUsers } from "../src/_platform/api/_shared/service/infra/access";
16
16
  import { listApps, setAppStatus } from "../src/auth/api/_shared/wecom/admin";
17
17
  import { createWecomClient, runtimeCache, runtimeHttp } from "../src/auth/api/_shared/wecom/client";
18
- import { readWecomConfig } from "../src/auth/api/_shared/wecom/config";
18
+ import { readSuiteConfig } from "../src/auth/api/_shared/wecom/config";
19
19
  import { installDb } from "./fixture";
20
20
 
21
21
  const g = globalThis as any;
@@ -123,7 +123,7 @@ function fakeClient(): { client: any, invalidated: string[] } {
123
123
  const deps = { store: { encrypt: (p: string) => `gcm(${p})`, decrypt: (b: string) => b, now: () => NOW } };
124
124
 
125
125
  /** createInstall 的薄包装:省掉动态 import 的样板 */
126
- async function createInstallForTest(cfg: ReturnType<typeof readWecomConfig>, client: ReturnType<typeof createWecomClient>, state: string) {
126
+ async function createInstallForTest(cfg: ReturnType<typeof readSuiteConfig>, client: ReturnType<typeof createWecomClient>, state: string) {
127
127
  const { createInstall } = await import("../src/auth/api/_shared/wecom/admin");
128
128
  return createInstall(cfg, client, state);
129
129
  }
@@ -274,7 +274,7 @@ describe("P3-C:开通入口(createInstall,代开发 get_customized_auth_ur
274
274
 
275
275
  it("provider_token → get_customized_auth_url → qrcode_url(即开通链接),且绝不调 get_pre_auth_code", async () => {
276
276
  const { calls, bodies } = stubUpstream();
277
- const cfg = readWecomConfig();
277
+ const cfg = readSuiteConfig();
278
278
  const client = createWecomClient({ http: runtimeHttp(), cache: runtimeCache() });
279
279
 
280
280
  const r = await createInstallForTest(cfg, client, "st_1");
@@ -297,7 +297,7 @@ describe("P3-C:开通入口(createInstall,代开发 get_customized_auth_ur
297
297
 
298
298
  it("调用方没传 state 时仍发默认 state(避免 40058 state missing)", async () => {
299
299
  const { bodies } = stubUpstream();
300
- const cfg = readWecomConfig();
300
+ const cfg = readSuiteConfig();
301
301
  const client = createWecomClient({ http: runtimeHttp(), cache: runtimeCache() });
302
302
  await createInstallForTest(cfg, client, "");
303
303
  const body = bodies.find(b => b && b.templateid_list) as any;
@@ -307,14 +307,14 @@ describe("P3-C:开通入口(createInstall,代开发 get_customized_auth_ur
307
307
 
308
308
  it("缺 provider 凭证(wecom_provider_id / wecom_provider_secret)→ 400 并点名缺哪两个 vars", async () => {
309
309
  installVars({ wecom_provider_id: "", wecom_provider_secret: "" });
310
- const cfg = readWecomConfig();
310
+ const cfg = readSuiteConfig();
311
311
  const client = createWecomClient({ http: runtimeHttp(), cache: runtimeCache() });
312
312
  await expect(createInstallForTest(cfg, client, "")).rejects.toMatchObject({ status: 400 });
313
313
  });
314
314
 
315
315
  it("上游 errcode(如 provider_secret 错)→ 502(可重试语义,不是 500)", async () => {
316
316
  g.fetch = async () => ({ ok: true, json: async () => ({ errcode: 40001, errmsg: "invalid provider secret" }) });
317
- const cfg = readWecomConfig();
317
+ const cfg = readSuiteConfig();
318
318
  const client = createWecomClient({ http: runtimeHttp(), cache: runtimeCache() });
319
319
  await expect(createInstallForTest(cfg, client, "")).rejects.toMatchObject({ status: 502 });
320
320
  });
@@ -330,7 +330,7 @@ describe("P3-C:开通入口(createInstall,代开发 get_customized_auth_ur
330
330
  throw new Error(`stub fetch 未预期:${url}`);
331
331
  },
332
332
  });
333
- const cfg = readWecomConfig();
333
+ const cfg = readSuiteConfig();
334
334
  const client = createWecomClient({ http: runtimeHttp(), cache: runtimeCache() });
335
335
  await expect(createInstallForTest(cfg, client, "st_1")).rejects.toMatchObject({
336
336
  status: 502,
@@ -21,7 +21,7 @@ import {
21
21
  eventUrlOf,
22
22
  loginRedirectOf,
23
23
  readCallbackConfig,
24
- readWecomConfig,
24
+ readSuiteConfig,
25
25
  siteRootOf,
26
26
  } from "../src/auth/api/_shared/wecom/config";
27
27
  import { envOfState, handleSuiteEvent } from "../src/auth/api/_shared/wecom/events";
@@ -105,6 +105,7 @@ function profile(suiteCorpAgent: { corp?: string, agent?: string, env?: string,
105
105
  agentId: suiteCorpAgent.agent ?? "100001",
106
106
  env: suiteCorpAgent.env ?? "prod",
107
107
  corpName: "测试企业",
108
+ credentialKind: "suite" as const,
108
109
  permanentCode: suiteCorpAgent.code ?? "perm_a",
109
110
  };
110
111
  }
@@ -757,7 +758,7 @@ describe("P1-J:部署常量读取(缺项一次报全 / 密钥格式 fail-fas
757
758
  installVars({ wecom_suite_id: "", wecom_callback_token: "", wecom_enc_key: "" });
758
759
  const msg = (() => {
759
760
  try {
760
- readWecomConfig();
761
+ readSuiteConfig();
761
762
  return "";
762
763
  }
763
764
  catch (e) {
@@ -789,7 +790,7 @@ describe("P1-J:部署常量读取(缺项一次报全 / 密钥格式 fail-fas
789
790
 
790
791
  it("baseUrl 去尾斜杠后拼出两个企微侧地址(回调 URL 要原样填进后台)", () => {
791
792
  installVars();
792
- const cfg = readWecomConfig();
793
+ const cfg = readSuiteConfig();
793
794
  expect(cfg.baseUrl).toBe("https://site.example.com/api/auth/wecom");
794
795
  // 不带 callback_format=json:官方文档只给「对外收款」写了这个参数,代开发的通用回调配置只有 XML
795
796
  expect(eventUrlOf(cfg)).toBe("https://site.example.com/api/auth/wecom/event");
@@ -799,7 +800,7 @@ describe("P1-J:部署常量读取(缺项一次报全 / 密钥格式 fail-fas
799
800
 
800
801
  it("子路径部署:站点根保留子路径(302 回跳必须落 /oj/login,不是 /login)", () => {
801
802
  installVars({ wecom_base_url: "https://site.example.com/oj/api/auth/wecom" });
802
- const cfg = readWecomConfig();
803
+ const cfg = readSuiteConfig();
803
804
  expect(siteRootOf(cfg)).toBe("https://site.example.com/oj");
804
805
  expect(loginLandingUrl({ origin: siteRootOf(cfg), ticket: "tk", tenant: "ww_corp_a" }))
805
806
  .toBe("https://site.example.com/oj/login?wecom_ticket=tk&tenant=ww_corp_a");
@@ -807,12 +808,12 @@ describe("P1-J:部署常量读取(缺项一次报全 / 密钥格式 fail-fas
807
808
 
808
809
  it("baseUrl 后缀不对 → 400(宁可拒,也不猜站点根拼出错误回跳)", () => {
809
810
  installVars({ wecom_base_url: "https://site.example.com/api/wecom" });
810
- expect(() => siteRootOf(readWecomConfig())).toThrow(/必须以 \/api\/auth\/wecom 结尾/);
811
+ expect(() => siteRootOf(readSuiteConfig())).toThrow(/必须以 \/api\/auth\/wecom 结尾/);
811
812
  });
812
813
 
813
814
  it("baseUrl 非绝对地址 → 400(相对路径会让 redirect_uri 与回调 URL 双双失效)", () => {
814
815
  installVars({ wecom_base_url: "/api/auth/wecom" });
815
- expect(() => readWecomConfig()).toThrow(/绝对地址/);
816
+ expect(() => readSuiteConfig()).toThrow(/绝对地址/);
816
817
  });
817
818
 
818
819
  it("wecom_enc_key 只收 16/32 字节的 hex 或 base64(AES-192 会在加密时才炸,故在读配置处拦住)", () => {