node-resource-utils 0.0.0-stage → 1.1.8
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/LICENSE +21 -0
- package/README.md +35 -2
- package/lib/sync.js +121 -0
- package/package.json +14 -4
- package/resources/.cache/ref.b64 +1 -0
- package/resources/README.md +97 -0
- package/resources/_.env +6 -0
- package/resources/_.env.example +25 -0
- package/resources/_.gitignore +18 -0
- package/resources/_.lintstagedrc.json +9 -0
- package/resources/_.npmrc +5 -0
- package/resources/_.prettierrc +7 -0
- package/resources/commitlint.config.mjs +1 -0
- package/resources/content/docs/README.md +63 -0
- package/resources/content/docs/faq.md +107 -0
- package/resources/content/docs/features/_meta.json +5 -0
- package/resources/content/docs/features/ai.md +71 -0
- package/resources/content/docs/features/appearance.md +47 -0
- package/resources/content/docs/features/feedback.md +79 -0
- package/resources/content/docs/features/files.md +74 -0
- package/resources/content/docs/features/graph.md +66 -0
- package/resources/content/docs/features/inbox.md +61 -0
- package/resources/content/docs/guide/_meta.json +5 -0
- package/resources/content/docs/guide/account.md +76 -0
- package/resources/content/docs/guide/data.md +87 -0
- package/resources/content/docs/guide/navigation.md +81 -0
- package/resources/content/docs/guide/permissions.md +67 -0
- package/resources/content/docs/guide/system.md +92 -0
- package/resources/content/docs/quick-start.md +70 -0
- package/resources/content/docs/shortcuts.md +69 -0
- package/resources/eslint.config.mjs +17 -0
- package/resources/next-env.d.ts +6 -0
- package/resources/next.config.ts +13 -0
- package/resources/postcss.config.mjs +7 -0
- package/resources/prisma/org.config.ts +98 -0
- package/resources/prisma/schema.prisma +336 -0
- package/resources/prisma/seed.ts +721 -0
- package/resources/prisma/seedSafe.ts +347 -0
- package/resources/prisma.config.ts +14 -0
- package/resources/scripts/check-config.mjs +773 -0
- package/resources/scripts/lib/schema-graph.mjs +410 -0
- package/resources/scripts/patch-next-trace.mjs +86 -0
- package/resources/scripts/suggest-relations.mjs +105 -0
- package/resources/src/app/(admin)/ai/page.tsx +469 -0
- package/resources/src/app/(admin)/dashboard/page.tsx +260 -0
- package/resources/src/app/(admin)/demo/employees/page.tsx +508 -0
- package/resources/src/app/(admin)/demo/files/page.tsx +451 -0
- package/resources/src/app/(admin)/demo/loading/page.tsx +513 -0
- package/resources/src/app/(admin)/error.tsx +96 -0
- package/resources/src/app/(admin)/feedback/page.tsx +573 -0
- package/resources/src/app/(admin)/graph/page.tsx +1224 -0
- package/resources/src/app/(admin)/inbox/page.tsx +31 -0
- package/resources/src/app/(admin)/layout.tsx +58 -0
- package/resources/src/app/(admin)/loading.tsx +31 -0
- package/resources/src/app/(admin)/system/depts/page.tsx +415 -0
- package/resources/src/app/(admin)/system/logs/page.tsx +215 -0
- package/resources/src/app/(admin)/system/menus/page.tsx +403 -0
- package/resources/src/app/(admin)/system/roles/page.tsx +433 -0
- package/resources/src/app/(admin)/system/users/page.tsx +620 -0
- package/resources/src/app/api/account-recovery/route.ts +132 -0
- package/resources/src/app/api/ai/act/route.ts +70 -0
- package/resources/src/app/api/ai/ask/route.ts +82 -0
- package/resources/src/app/api/ai/chat/route.ts +404 -0
- package/resources/src/app/api/ai/report/route.ts +164 -0
- package/resources/src/app/api/auth/[...nextauth]/route.ts +3 -0
- package/resources/src/app/api/auth/change-password/route.ts +57 -0
- package/resources/src/app/api/auth/forgot/route.ts +98 -0
- package/resources/src/app/api/auth/register/route.ts +95 -0
- package/resources/src/app/api/auth/register-config/route.ts +51 -0
- package/resources/src/app/api/badges/route.ts +39 -0
- package/resources/src/app/api/depts/[id]/route.ts +145 -0
- package/resources/src/app/api/depts/route.ts +126 -0
- package/resources/src/app/api/docs/export/route.ts +87 -0
- package/resources/src/app/api/docs/parse/route.ts +57 -0
- package/resources/src/app/api/docs/search/route.ts +22 -0
- package/resources/src/app/api/employees/[id]/route.ts +147 -0
- package/resources/src/app/api/employees/export/route.ts +115 -0
- package/resources/src/app/api/employees/import/route.ts +205 -0
- package/resources/src/app/api/employees/route.ts +149 -0
- package/resources/src/app/api/export/route.ts +254 -0
- package/resources/src/app/api/favorites/route.ts +56 -0
- package/resources/src/app/api/feedback/ai/route.ts +31 -0
- package/resources/src/app/api/feedback/approve/route.ts +67 -0
- package/resources/src/app/api/feedback/report/route.ts +213 -0
- package/resources/src/app/api/feedback/route.ts +246 -0
- package/resources/src/app/api/files/[name]/route.ts +82 -0
- package/resources/src/app/api/graph/focus/route.ts +75 -0
- package/resources/src/app/api/graph/node/move/route.ts +48 -0
- package/resources/src/app/api/graph/node/route.ts +274 -0
- package/resources/src/app/api/graph/route.ts +272 -0
- package/resources/src/app/api/health/route.ts +11 -0
- package/resources/src/app/api/inbox/read/route.ts +36 -0
- package/resources/src/app/api/inbox/summary/route.ts +15 -0
- package/resources/src/app/api/logs/route.ts +82 -0
- package/resources/src/app/api/menus/[id]/route.ts +132 -0
- package/resources/src/app/api/menus/mine/route.ts +23 -0
- package/resources/src/app/api/menus/route.ts +89 -0
- package/resources/src/app/api/roles/[id]/menus/route.ts +73 -0
- package/resources/src/app/api/roles/[id]/route.ts +96 -0
- package/resources/src/app/api/roles/route.ts +114 -0
- package/resources/src/app/api/search/route.ts +22 -0
- package/resources/src/app/api/stats/route.ts +92 -0
- package/resources/src/app/api/upload/route.ts +38 -0
- package/resources/src/app/api/users/[id]/password/route.ts +55 -0
- package/resources/src/app/api/users/[id]/route.ts +145 -0
- package/resources/src/app/api/users/route.ts +153 -0
- package/resources/src/app/api/visits/route.ts +48 -0
- package/resources/src/app/change-password/ChangePasswordForm.tsx +174 -0
- package/resources/src/app/change-password/page.tsx +19 -0
- package/resources/src/app/docs/[[...slug]]/page.tsx +56 -0
- package/resources/src/app/docs/layout.tsx +34 -0
- package/resources/src/app/docs/loading.tsx +19 -0
- package/resources/src/app/docs/not-found.tsx +25 -0
- package/resources/src/app/forgot/page.tsx +392 -0
- package/resources/src/app/global-error.tsx +75 -0
- package/resources/src/app/globals.css +235 -0
- package/resources/src/app/layout.tsx +44 -0
- package/resources/src/app/login/page.tsx +267 -0
- package/resources/src/app/page.tsx +14 -0
- package/resources/src/app/register/page.tsx +237 -0
- package/resources/src/auth.ts +215 -0
- package/resources/src/components/AiPanel.tsx +970 -0
- package/resources/src/components/AuthContext.tsx +82 -0
- package/resources/src/components/DataTable.tsx +459 -0
- package/resources/src/components/EChart.tsx +140 -0
- package/resources/src/components/FeedbackAiModal.tsx +476 -0
- package/resources/src/components/FeedbackReportModal.tsx +479 -0
- package/resources/src/components/Markdown.tsx +532 -0
- package/resources/src/components/OntologyGraph.tsx +1196 -0
- package/resources/src/components/Providers.tsx +99 -0
- package/resources/src/components/VisitTracker.tsx +89 -0
- package/resources/src/components/admin/AdminShell.tsx +749 -0
- package/resources/src/components/admin/CountBadge.tsx +67 -0
- package/resources/src/components/admin/GlobalSearch.tsx +509 -0
- package/resources/src/components/admin/InboxBell.tsx +78 -0
- package/resources/src/components/admin/VisitStats.tsx +145 -0
- package/resources/src/components/admin/menu-icons.tsx +56 -0
- package/resources/src/components/ai/AiInputForm.tsx +351 -0
- package/resources/src/components/appearance/AppearanceDrawer.tsx +268 -0
- package/resources/src/components/appearance/AppearanceProvider.tsx +160 -0
- package/resources/src/components/docs/DocArticle.tsx +106 -0
- package/resources/src/components/docs/DocToc.tsx +100 -0
- package/resources/src/components/docs/DocsChrome.tsx +259 -0
- package/resources/src/components/docs/DocsHome.tsx +209 -0
- package/resources/src/components/docs/DocsSearch.tsx +365 -0
- package/resources/src/components/docs/docs.css +1458 -0
- package/resources/src/components/feedback/AsyncButton.tsx +108 -0
- package/resources/src/components/feedback/LoadingOverlay.tsx +125 -0
- package/resources/src/components/feedback/PressableCard.tsx +92 -0
- package/resources/src/components/feedback/README.md +97 -0
- package/resources/src/components/feedback/RouteProgress.tsx +108 -0
- package/resources/src/components/feedback/Skeleton.tsx +249 -0
- package/resources/src/components/feedback/StatCard.tsx +70 -0
- package/resources/src/components/feedback/TaskHint.tsx +153 -0
- package/resources/src/components/feedback/index.ts +93 -0
- package/resources/src/components/feedback/motion.css +204 -0
- package/resources/src/components/feedback/motion.ts +63 -0
- package/resources/src/components/feedback/useCountUp.ts +110 -0
- package/resources/src/components/feedback/usePending.ts +209 -0
- package/resources/src/components/feedback/useRowFlash.ts +40 -0
- package/resources/src/components/inbox/InboxView.tsx +409 -0
- package/resources/src/components/ui/button.tsx +35 -0
- package/resources/src/components/ui/card.tsx +37 -0
- package/resources/src/components/ui/input.tsx +19 -0
- package/resources/src/lib/ai.ts +310 -0
- package/resources/src/lib/aiContext.ts +357 -0
- package/resources/src/lib/aiForm.test.ts +191 -0
- package/resources/src/lib/aiForm.ts +532 -0
- package/resources/src/lib/aiFormTypes.ts +83 -0
- package/resources/src/lib/aiToken.ts +72 -0
- package/resources/src/lib/aiTools.ts +823 -0
- package/resources/src/lib/api.ts +160 -0
- package/resources/src/lib/appInfo.ts +128 -0
- package/resources/src/lib/appearance.ts +643 -0
- package/resources/src/lib/badge-count.ts +146 -0
- package/resources/src/lib/badges.test.ts +58 -0
- package/resources/src/lib/badges.ts +79 -0
- package/resources/src/lib/brand-mark.test.ts +87 -0
- package/resources/src/lib/brand-mark.ts +45 -0
- package/resources/src/lib/docs-shared.test.ts +148 -0
- package/resources/src/lib/docs-shared.ts +422 -0
- package/resources/src/lib/docs.ts +381 -0
- package/resources/src/lib/err-text.ts +21 -0
- package/resources/src/lib/excel.ts +152 -0
- package/resources/src/lib/favorite-path.ts +60 -0
- package/resources/src/lib/favorites.ts +59 -0
- package/resources/src/lib/features.ts +69 -0
- package/resources/src/lib/feedback-access.ts +17 -0
- package/resources/src/lib/feedbackAi.ts +1621 -0
- package/resources/src/lib/formRules.ts +76 -0
- package/resources/src/lib/format.ts +52 -0
- package/resources/src/lib/graph-cascade.ts +304 -0
- package/resources/src/lib/graph-focus.ts +301 -0
- package/resources/src/lib/graph-link.ts +17 -0
- package/resources/src/lib/graph-node-write.ts +150 -0
- package/resources/src/lib/graph-schema.ts +475 -0
- package/resources/src/lib/graph-scope.test.ts +46 -0
- package/resources/src/lib/graph-scope.ts +28 -0
- package/resources/src/lib/http.ts +157 -0
- package/resources/src/lib/inbox-types.ts +57 -0
- package/resources/src/lib/inbox.ts +55 -0
- package/resources/src/lib/menu-service.ts +204 -0
- package/resources/src/lib/notifications.ts +113 -0
- package/resources/src/lib/office.ts +531 -0
- package/resources/src/lib/ontology.ts +277 -0
- package/resources/src/lib/openaiCompat.ts +257 -0
- package/resources/src/lib/pending.ts +98 -0
- package/resources/src/lib/prisma.ts +30 -0
- package/resources/src/lib/rbac.test.ts +106 -0
- package/resources/src/lib/rbac.ts +185 -0
- package/resources/src/lib/search-data.ts +158 -0
- package/resources/src/lib/security-questions.ts +20 -0
- package/resources/src/lib/skin-theme.ts +155 -0
- package/resources/src/lib/system-config.ts +55 -0
- package/resources/src/lib/theme.ts +96 -0
- package/resources/src/lib/todos/feedback.ts +86 -0
- package/resources/src/lib/todos/index.ts +73 -0
- package/resources/src/lib/todos/types.ts +33 -0
- package/resources/src/lib/upload.ts +106 -0
- package/resources/src/lib/utils.test.ts +9 -0
- package/resources/src/lib/utils.ts +6 -0
- package/resources/src/lib/validation.test.ts +297 -0
- package/resources/src/lib/validation.ts +658 -0
- package/resources/src/lib/visit-stats.ts +109 -0
- package/resources/src/lib/visits.ts +111 -0
- package/resources/src/lib/zodField.ts +44 -0
- package/resources/tsconfig.json +41 -0
- package/resources/vitest.config.ts +16 -0
- package/resources/vitest.setup.ts +1 -0
|
@@ -0,0 +1,773 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 系统配置校验 —— 不配置就构建失败。
|
|
3
|
+
*
|
|
4
|
+
* 为什么要有这个:光靠提示词"提醒 AI 改"是靠不住的,AI 会忘、会跳步、
|
|
5
|
+
* 会在别的地方写一堆演示文案。这里在构建前硬检查,
|
|
6
|
+
* 让"交上去还是个演示系统"变得不可能。
|
|
7
|
+
*
|
|
8
|
+
* 检查五类:
|
|
9
|
+
* 系统身份:APP_NAME / APP_DESC / SHOW_DEMO_ACCOUNT_HINT / HOME_STATS
|
|
10
|
+
* 知识图谱:默认本体 / model 在 schema 里存在 / 每个本体有 titleField 与 layer /
|
|
11
|
+
* views 与 relations 引用闭合(视图 id、root、关系两端都对得上)
|
|
12
|
+
* 系统管理:部门管理必须存在(页面 + 增删改查接口 + 菜单),
|
|
13
|
+
* 这一项不走配置,直接查文件 —— 因为 AI 十次有八次会把部门管理漏掉
|
|
14
|
+
* 全局搜索:搜索组件必须还在、且挂在 AdminShell 上、Menu 有 keywords 字段 ——
|
|
15
|
+
* 它是"功能能不能被找到"的唯一入口,删了整个系统的可达性就没了
|
|
16
|
+
* 数据权限:rbac.ts 的三件套、Role.dataScope、员工列表/导出的 deptScope 接线 ——
|
|
17
|
+
* 功能权限只决定"能不能进页面",它才决定"进去能看哪些行"
|
|
18
|
+
* 关系覆盖:视图里同时出现的两个本体,如果 schema 里有外键/桥接表连接,
|
|
19
|
+
* 图上却没有任何路径,直接报错 —— 专治"考试和人员都在图里、却没配关系"。
|
|
20
|
+
* 全局漏配(本体不在视图里)只提示不报错,见 npm run relations。
|
|
21
|
+
*
|
|
22
|
+
* 为什么连引用闭合也要查:图谱是"配置驱动"的,ontology.ts 里写错一个 id
|
|
23
|
+
* (视图引用了不存在的本体、root 不在视图里、关系两端拼错)不会报编译错误,
|
|
24
|
+
* 只会在打开 /graph 时变成红色「图谱加载失败」,而 tsc 完全看不出来。
|
|
25
|
+
*
|
|
26
|
+
* 用法:
|
|
27
|
+
* npm run check:config 手动检查
|
|
28
|
+
* npm run build 自动检查(prebuild 钩子)
|
|
29
|
+
*/
|
|
30
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
31
|
+
import { analyze, suggestSnippet } from './lib/schema-graph.mjs';
|
|
32
|
+
|
|
33
|
+
const NL = '\n';
|
|
34
|
+
const APPINFO = 'src/lib/appInfo.ts';
|
|
35
|
+
const ONTOLOGY = 'src/lib/ontology.ts';
|
|
36
|
+
const SCHEMA = 'prisma/schema.prisma';
|
|
37
|
+
|
|
38
|
+
const errors = [];
|
|
39
|
+
/** 不阻塞构建的"可能漏配"提示(关系漏配、本体缺 link、默认副标题…) */
|
|
40
|
+
const warnings = [];
|
|
41
|
+
|
|
42
|
+
function readOrNull(path) {
|
|
43
|
+
try {
|
|
44
|
+
return readFileSync(path, 'utf8');
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 取 `export const X = ...;` 的值(粗提取,够用) */
|
|
51
|
+
function grab(src, name) {
|
|
52
|
+
const re = new RegExp('export const ' + name + '\\s*(?::[^=]+)?=\\s*([\\s\\S]*?);');
|
|
53
|
+
const m = src.match(re);
|
|
54
|
+
return m ? m[1].trim() : null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* ============================ 1. appInfo.ts ============================ */
|
|
58
|
+
|
|
59
|
+
const appSrc = readOrNull(APPINFO);
|
|
60
|
+
if (!appSrc) {
|
|
61
|
+
console.error(`\n[check-config] 找不到 ${APPINFO}\n`);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const appName = grab(appSrc, 'APP_NAME');
|
|
66
|
+
if (!appName || appName === "'业务管理系统'") {
|
|
67
|
+
errors.push(
|
|
68
|
+
'APP_NAME 还是项目默认值「业务管理系统」。' + NL +
|
|
69
|
+
' → 改成题目里的系统名,例如 \'车辆派遣管理系统\''
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const appDesc = grab(appSrc, 'APP_DESC');
|
|
74
|
+
if (appDesc && /基于\s*Next\.js|antd|Prisma/i.test(appDesc)) {
|
|
75
|
+
errors.push(
|
|
76
|
+
'APP_DESC 还在描述技术栈("基于 Next.js + antd + Prisma"),一眼就是模板。' + NL +
|
|
77
|
+
' → 改成这个系统的业务定位'
|
|
78
|
+
);
|
|
79
|
+
} else if (appDesc && /RBAC|操作审计|数据导出/.test(appDesc)) {
|
|
80
|
+
// 默认值「内置 RBAC 权限、操作审计与数据导出的业务系统」不含技术栈关键词,
|
|
81
|
+
// 只按技术栈判断会漏掉它,于是 meta description 和登录页一直挂着"RBAC"。
|
|
82
|
+
errors.push(
|
|
83
|
+
'APP_DESC 还是基础项目默认的「内置 RBAC 权限、操作审计与数据导出的业务系统」。' + NL +
|
|
84
|
+
' → 改成这个系统的业务定位,例如「车辆申请、审批派车与归队登记」'
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const appSubtitle = grab(appSrc, 'APP_SUBTITLE');
|
|
89
|
+
if (appSubtitle && /RBAC|操作审计|数据导出/.test(appSubtitle)) {
|
|
90
|
+
warnings.push(
|
|
91
|
+
'APP_SUBTITLE 还是基础项目默认文案(侧边栏品牌区和登录页都会显示这一行)' + NL +
|
|
92
|
+
' → 改成一句业务定位,例如「车辆申请、审批派车与归队登记」'
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const demoHint = grab(appSrc, 'SHOW_DEMO_ACCOUNT_HINT');
|
|
97
|
+
if (demoHint === 'true') {
|
|
98
|
+
errors.push(
|
|
99
|
+
'SHOW_DEMO_ACCOUNT_HINT 还是 true,登录页会显示「演示账号:admin / 123456」。' + NL +
|
|
100
|
+
' → 改成 false。这是评委一眼认出"这是演示系统"的最强信号'
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const homeStats = grab(appSrc, 'HOME_STATS');
|
|
105
|
+
if (homeStats && /userCount/.test(homeStats) && /roleCount/.test(homeStats)) {
|
|
106
|
+
errors.push(
|
|
107
|
+
'HOME_STATS 还是「用户总数 / 角色数量 / 员工总数」这类系统自身指标。' + NL +
|
|
108
|
+
' → 换成这个业务最该看的指标,例如「待派车申请 / 今日出车 / 可用车辆」'
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* ============================ 2. ontology.ts ============================ */
|
|
113
|
+
|
|
114
|
+
/** 去掉注释和字符串里的干扰(否则注释掉的示例代码会被误判成真实配置) */
|
|
115
|
+
function stripComments(src) {
|
|
116
|
+
return src
|
|
117
|
+
// 块注释 /* ... */(保留换行,避免行号错乱)
|
|
118
|
+
.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, ''))
|
|
119
|
+
// 行注释 // ...
|
|
120
|
+
.replace(/(^|[^:])\/\/[^\n]*/g, '$1');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const ontoRaw = readOrNull(ONTOLOGY);
|
|
124
|
+
// ★ 必须剥注释:ontology.ts 底部有一整段注释掉的示例配置,
|
|
125
|
+
// 不剥的话会把示例里的 model 名当成真实引用,产生误报。
|
|
126
|
+
const ontoSrc = ontoRaw ? stripComments(ontoRaw) : null;
|
|
127
|
+
if (ontoSrc) {
|
|
128
|
+
// 基础项目默认本体:部门 / 员工 / 角色 / 账号 / 日志
|
|
129
|
+
// ★ user 必须在内:默认配置里就有它,漏掉的话 isDefault 永远算成 false,
|
|
130
|
+
// 这条检查就变成了摆设(改业务前忘了换本体也不会报错)。
|
|
131
|
+
const DEFAULT_IDS = ['department', 'employee', 'role', 'log', 'user'];
|
|
132
|
+
|
|
133
|
+
// ★ 只在 ONTOLOGY 配置对象内部找。文件顶部的 interface GraphView 里也有
|
|
134
|
+
// "relations: string[];",从全文 indexOf 会切到接口声明上(已踩)。
|
|
135
|
+
// 锚点用 ] as EntityDef / RelationDef / GraphView,它们只出现在配置里。
|
|
136
|
+
const cfgAt = ontoSrc.indexOf('export const ONTOLOGY');
|
|
137
|
+
const cfg = cfgAt >= 0 ? ontoSrc.slice(cfgAt) : ontoSrc;
|
|
138
|
+
const entEndAt = cfg.indexOf('] as EntityDef');
|
|
139
|
+
const relEndAt = cfg.indexOf('] as RelationDef');
|
|
140
|
+
const sliceBetween = (startKey, endMarker, from = 0) => {
|
|
141
|
+
const s = cfg.indexOf(startKey, from);
|
|
142
|
+
const e = s >= 0 ? cfg.indexOf(endMarker, s) : -1;
|
|
143
|
+
return s >= 0 && e > s ? cfg.slice(s, e) : '';
|
|
144
|
+
};
|
|
145
|
+
const entBlock = sliceBetween('entities: [', '] as EntityDef');
|
|
146
|
+
const relBlock = sliceBetween('relations: [', '] as RelationDef', entEndAt);
|
|
147
|
+
const viewBlock = sliceBetween('views: [', '] as GraphView', relEndAt);
|
|
148
|
+
|
|
149
|
+
// 每个实体是一个 { ... }(内部没有嵌套对象,subtitleFields 是数组不是对象)
|
|
150
|
+
const entObjs = [...entBlock.matchAll(/\{[^{}]*\}/g)].map((m) => m[0]);
|
|
151
|
+
const entIds = [];
|
|
152
|
+
for (const o of entObjs) {
|
|
153
|
+
const id = o.match(/id:\s*'([^']+)'/)?.[1];
|
|
154
|
+
if (!id) continue;
|
|
155
|
+
entIds.push(id);
|
|
156
|
+
if (!/titleField:\s*'[^']+'/.test(o)) {
|
|
157
|
+
errors.push(
|
|
158
|
+
`本体 ${id} 没写 titleField —— 图谱节点会用 id 当代号,显示成一串 "clx..."。` + NL +
|
|
159
|
+
" → 加 titleField: 'name'(或编号字段)"
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
if (!/layer:\s*\d/.test(o)) {
|
|
163
|
+
errors.push(
|
|
164
|
+
`本体 ${id} 没写 layer —— 分层布局会退化成散乱的力导图。` + NL +
|
|
165
|
+
' → 加 layer: 0/1/2,同层的放同一个值(业务上一般:组织 0 → 资源/人 1 → 单据 2)'
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const isDefault =
|
|
171
|
+
entIds.length > 0 && entIds.every((id) => DEFAULT_IDS.includes(id));
|
|
172
|
+
|
|
173
|
+
if (isDefault) {
|
|
174
|
+
errors.push(
|
|
175
|
+
'ontology.ts 还是基础项目默认的「部门-员工-角色-日志」本体,跟题目无关 —— 知识图谱会显示一堆跟业务不相关的东西。' + NL +
|
|
176
|
+
' → 改成题目的业务本体,例如车辆派遣:部门 / 车辆 / 驾驶员 / 派遣申请' + NL +
|
|
177
|
+
' → 每个 entity 都要给 layer: 0/1/2(数值小的在左)'
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ---- 关系:两端必须指向已声明的本体(写错就是永远画不出来的边) ----
|
|
182
|
+
// 关系对象里 via 有嵌套花括号,不能按 {..} 切;直接抓 id / from / to
|
|
183
|
+
const rels = [...relBlock.matchAll(
|
|
184
|
+
/\{\s*id:\s*'([^']+)'[\s\S]*?from:\s*'([^']+)'[\s\S]*?to:\s*'([^']+)'/g
|
|
185
|
+
)].map((m) => ({ id: m[1], from: m[2], to: m[3] }));
|
|
186
|
+
const relIds = rels.map((r) => r.id);
|
|
187
|
+
for (const r of rels) {
|
|
188
|
+
for (const end of [r.from, r.to]) {
|
|
189
|
+
if (!entIds.includes(end)) {
|
|
190
|
+
errors.push(
|
|
191
|
+
`关系 ${r.id} 的 from/to 指向不存在的本体 ${end} —— 这条边永远画不出来。` + NL +
|
|
192
|
+
' → 改成 entities 里已声明的本体 id'
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ---- 视图:entities / relations / root 必须闭合 ----
|
|
199
|
+
const viewObjs = [...viewBlock.matchAll(/\{[^{}]*\}/g)].map((m) => m[0]);
|
|
200
|
+
if (!viewObjs.length) {
|
|
201
|
+
errors.push(
|
|
202
|
+
'ontology.ts 没有配置 views —— 打开 /graph 只会看到红色「图谱加载失败」。' + NL +
|
|
203
|
+
" → 加一个视图:views: [{ id:'main', label:'业务关系图', entities:[...], relations:[...], root:'xxx' }]"
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
const viewIds = [];
|
|
207
|
+
for (const o of viewObjs) {
|
|
208
|
+
const vid = o.match(/id:\s*'([^']+)'/)?.[1];
|
|
209
|
+
if (!vid) continue;
|
|
210
|
+
viewIds.push(vid);
|
|
211
|
+
const root = o.match(/root:\s*'([^']+)'/)?.[1];
|
|
212
|
+
const list = (k) =>
|
|
213
|
+
(o.match(new RegExp(k + ':\\s*\\[([^\\]]*)\\]'))?.[1] ?? '')
|
|
214
|
+
.split(',')
|
|
215
|
+
.map((s) => s.trim().replace(/^'|'$/g, ''))
|
|
216
|
+
.filter(Boolean);
|
|
217
|
+
const ents = list('entities');
|
|
218
|
+
const relsInView = list('relations');
|
|
219
|
+
if (!ents.length) {
|
|
220
|
+
errors.push(`视图 ${vid} 的 entities 是空的 —— 图上不会有任何节点。`);
|
|
221
|
+
}
|
|
222
|
+
const badEnts = ents.filter((e) => !entIds.includes(e));
|
|
223
|
+
if (badEnts.length) {
|
|
224
|
+
errors.push(`视图 ${vid} 引用了不存在的本体:${badEnts.join(', ')} —— 视图加载会直接报错。`);
|
|
225
|
+
}
|
|
226
|
+
const badRels = relsInView.filter((r) => !relIds.includes(r));
|
|
227
|
+
if (badRels.length) {
|
|
228
|
+
errors.push(`视图 ${vid} 引用了不存在的关系:${badRels.join(', ')}`);
|
|
229
|
+
}
|
|
230
|
+
if (!root) {
|
|
231
|
+
errors.push(`视图 ${vid} 没写 root —— 服务端取不到默认中心节点。`);
|
|
232
|
+
} else if (!ents.includes(root)) {
|
|
233
|
+
errors.push(`视图 ${vid} 的 root「${root}」不在该视图的 entities 里 —— 默认中心节点画不出来。`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// id 不能重复(重复会让视图引用到错误的那个)
|
|
238
|
+
for (const [kind, list] of [
|
|
239
|
+
['本体', entIds],
|
|
240
|
+
['关系', relIds],
|
|
241
|
+
['视图', viewIds],
|
|
242
|
+
]) {
|
|
243
|
+
const dup = [...new Set(list.filter((x, i) => list.indexOf(x) !== i))];
|
|
244
|
+
if (dup.length) errors.push(`${kind} id 重复:${dup.join(', ')}`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ontology 里声明的 model 必须在 schema 里存在,否则查不出数据
|
|
248
|
+
const schemaSrc = readOrNull(SCHEMA);
|
|
249
|
+
if (schemaSrc) {
|
|
250
|
+
const models = new Set(
|
|
251
|
+
[...schemaSrc.matchAll(/^model\s+(\w+)/gm)].map((m) => m[1])
|
|
252
|
+
);
|
|
253
|
+
const usedRe = /model:\s*'([A-Za-z_]\w*)'/g;
|
|
254
|
+
const used = [...ontoSrc.matchAll(usedRe)].map((m) => m[1]);
|
|
255
|
+
const missing = [...new Set(used)].filter((m) => !models.has(m));
|
|
256
|
+
if (missing.length) {
|
|
257
|
+
errors.push(
|
|
258
|
+
'ontology.ts 引用了 schema 中不存在的模型:' + missing.join(', ') + NL +
|
|
259
|
+
' → 要么把 ontology 里的 model 名改成真实的模型名,要么在 schema 里建它(否则图谱查不出节点)'
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/* ============================ 3. 系统管理完整性 ============================ */
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* 部门管理是硬性要求。
|
|
269
|
+
*
|
|
270
|
+
* 为什么不在提示词里说一说就完事:AI 生成"系统管理"时,
|
|
271
|
+
* 十次有八次只做用户 + 角色,把部门漏掉 —— 因为用户管理里有个部门下拉,
|
|
272
|
+
* 看起来"已经覆盖了"。实际上没有部门维护入口,组织结构就是死的。
|
|
273
|
+
* 这里直接查文件存在性,漏了构建就失败。
|
|
274
|
+
*/
|
|
275
|
+
const DEPT_PAGE = 'src/app/(admin)/system/depts/page.tsx';
|
|
276
|
+
const DEPT_API = 'src/app/api/depts/route.ts';
|
|
277
|
+
const DEPT_ITEM_API = 'src/app/api/depts/[id]/route.ts';
|
|
278
|
+
const SEED = 'prisma/seed.ts';
|
|
279
|
+
|
|
280
|
+
const deptPageSrc = readOrNull(DEPT_PAGE);
|
|
281
|
+
if (!deptPageSrc) {
|
|
282
|
+
errors.push(
|
|
283
|
+
`缺少部门管理页面 ${DEPT_PAGE}` + NL +
|
|
284
|
+
' → 系统管理必须有「部门管理」:用户/员工都要挂部门,没有维护入口组织结构就是死的'
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const deptApiSrc = readOrNull(DEPT_API);
|
|
289
|
+
if (!deptApiSrc) {
|
|
290
|
+
errors.push(`缺少部门接口 ${DEPT_API}(至少要有 GET 树 + POST 新增)`);
|
|
291
|
+
} else if (!/export const POST/.test(deptApiSrc)) {
|
|
292
|
+
errors.push(`${DEPT_API} 里没有 POST —— 部门只能查不能建,等于没做`);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const deptItemSrc = readOrNull(DEPT_ITEM_API);
|
|
296
|
+
if (!deptItemSrc) {
|
|
297
|
+
errors.push(`缺少部门更新/删除接口 ${DEPT_ITEM_API}`);
|
|
298
|
+
} else {
|
|
299
|
+
if (!/export const PUT/.test(deptItemSrc)) errors.push(`${DEPT_ITEM_API} 里没有 PUT —— 部门改不了`);
|
|
300
|
+
if (!/export const DELETE/.test(deptItemSrc)) {
|
|
301
|
+
errors.push(`${DEPT_ITEM_API} 里没有 DELETE —— 部门删不了`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const seedSrc = readOrNull(SEED);
|
|
306
|
+
if (!seedSrc) {
|
|
307
|
+
errors.push('找不到 prisma/seed.ts —— 菜单没法初始化');
|
|
308
|
+
} else if (!/name:\s*'部门管理'/.test(seedSrc)) {
|
|
309
|
+
errors.push(
|
|
310
|
+
'prisma/seed.ts 里没有「部门管理」菜单 —— 页面存在也点不进去。' + NL +
|
|
311
|
+
" → 在「系统管理」的 children 里补一条:{ name:'部门管理', path:'/system/depts', icon:'ApartmentOutlined', permCode:'system:dept:list' }"
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/* ============================ 3.1 部门开关 ============================ */
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* 部门管理是"四件套"里唯一被开关控制的一个。
|
|
319
|
+
*
|
|
320
|
+
* 页面、接口、菜单都在,但 features.ts 里 department 还是 false 时,
|
|
321
|
+
* 侧边栏 / 全局搜索 / 菜单管理 / 角色授权四处会一起把它摘掉 ——
|
|
322
|
+
* 交付物上就是"没有部门管理",而前面那些文件检查全都通过了。
|
|
323
|
+
* 这条不查,等于四件套只保住了三件。
|
|
324
|
+
*/
|
|
325
|
+
const FEATURES_FILE = 'src/lib/features.ts';
|
|
326
|
+
const featuresSrc = readOrNull(FEATURES_FILE);
|
|
327
|
+
if (featuresSrc === null) {
|
|
328
|
+
errors.push(
|
|
329
|
+
`缺少 ${FEATURES_FILE} —— 功能开关是这个基础项目的一部分,模块缺失就无从判断哪些该显示` + NL +
|
|
330
|
+
' → 从 node_modules/node-resource-utils/resources 恢复'
|
|
331
|
+
);
|
|
332
|
+
} else if (!/^[ \t]*department:\s*true/m.test(featuresSrc)) {
|
|
333
|
+
errors.push(
|
|
334
|
+
`${FEATURES_FILE} 里 department 还不是 true —— 部门管理会被四处隐藏(侧边栏 / 全局搜索 / 菜单管理 / 角色授权),交付物上看就是"没有部门管理"` + NL +
|
|
335
|
+
' → 系统管理是硬性四件套(部门 / 用户 / 角色 / 菜单),把 department 改成 true'
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/* ============================ 3.2 AI 工具层(能办事,不只是聊天) ============================ */
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* 工具层是"AI 不只是问答"的唯一实现路径。
|
|
343
|
+
*
|
|
344
|
+
* 为什么用文件检查而不是只写在提示词里:
|
|
345
|
+
* 后续 AI 改业务时很容易觉得"快照够用了",把工具循环删掉;
|
|
346
|
+
* 或者嫌确认卡麻烦,让对话接口直接写库。那一瞬间系统就退回成套壳问答。
|
|
347
|
+
* 这里只查存在性与关键调用,不限制业务 —— 工具本来就是按 ontology 自动生成的,
|
|
348
|
+
* 换业务不需要动这里的任何文件。
|
|
349
|
+
*/
|
|
350
|
+
const AI_TOOLS = 'src/lib/aiTools.ts';
|
|
351
|
+
const AI_ACT_API = 'src/app/api/ai/act/route.ts';
|
|
352
|
+
const AI_CHAT_API = 'src/app/api/ai/chat/route.ts';
|
|
353
|
+
const AI_PANEL = 'src/components/AiPanel.tsx';
|
|
354
|
+
|
|
355
|
+
if (!readOrNull(AI_TOOLS)) {
|
|
356
|
+
errors.push(
|
|
357
|
+
`缺少 AI 工具层 ${AI_TOOLS}` + NL +
|
|
358
|
+
' → 它按 ontology 实体 + prisma 字段自动生成 query_ 与 create_ 工具,' + NL +
|
|
359
|
+
' 删掉之后助手只能聊天,不能查数据也不能办事'
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const actSrc = readOrNull(AI_ACT_API);
|
|
364
|
+
if (!actSrc) {
|
|
365
|
+
errors.push(
|
|
366
|
+
`缺少动作执行接口 ${AI_ACT_API}` + NL +
|
|
367
|
+
' → 写操作必须由用户在确认卡上点确认后才执行,不能让对话接口直接写库'
|
|
368
|
+
);
|
|
369
|
+
} else if (!/verifyAction/.test(actSrc)) {
|
|
370
|
+
errors.push(`${AI_ACT_API} 没有校验动作令牌 —— 前端就能改参数,"二次确认"等于没有`);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const chatSrc = readOrNull(AI_CHAT_API);
|
|
374
|
+
if (!chatSrc) {
|
|
375
|
+
errors.push(`缺少统一助手接口 ${AI_CHAT_API}`);
|
|
376
|
+
} else if (!/buildAiTools/.test(chatSrc)) {
|
|
377
|
+
errors.push(
|
|
378
|
+
`${AI_CHAT_API} 没有挂载工具(buildAiTools)` + NL +
|
|
379
|
+
' → 助手只能凭快照回答,"哪个部门用车最多"这类聚合问题算不出来'
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const panelSrc = readOrNull(AI_PANEL);
|
|
384
|
+
if (panelSrc && !/\/api\/ai\/act/.test(panelSrc)) {
|
|
385
|
+
errors.push(`${AI_PANEL} 缺少确认卡(没有调用 /api/ai/act)—— 写操作没有入口,功能没接完`);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/* ============================ 3.5 关系覆盖 ============================ */
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* 视图内的"该连未连"是硬错误:
|
|
392
|
+
* 两个本体都摆进图里了,schema 也有外键或桥接表,图上却连不起来 ——
|
|
393
|
+
* 评委一眼就能看出"考试和人员没关系?不对吧"。
|
|
394
|
+
* 允许两跳可达(A—C—B),因为那至少是连通的;完全不通才报错。
|
|
395
|
+
*/
|
|
396
|
+
const schemaForRel = readOrNull(SCHEMA);
|
|
397
|
+
if (ontoRaw && schemaForRel) {
|
|
398
|
+
const rel = analyze(schemaForRel, ontoRaw);
|
|
399
|
+
|
|
400
|
+
const describe = (l) =>
|
|
401
|
+
l.kind === 'bridge'
|
|
402
|
+
? `桥接表 ${l.bridgeModel}(${l.fromField} / ${l.toField}${l.pure ? ',纯中间表' : `,含 ${l.payload.join('/')}`})`
|
|
403
|
+
: `外键 ${l.aModel}.${l.field}`;
|
|
404
|
+
|
|
405
|
+
for (const v of rel.viewIssues) {
|
|
406
|
+
errors.push(
|
|
407
|
+
`视图「${v.view}」里「${v.a}」和「${v.b}」都在,schema 也有连接(${describe(v.link)}),` + NL +
|
|
408
|
+
' 但图上的关系里没有任何路径把它们连起来 —— 这正是"关系漏配"的典型表现。' + NL +
|
|
409
|
+
` → 补一条关系(可直接粘到 ontology.ts):${suggestSnippet(v.link, rel.entities)}` + NL +
|
|
410
|
+
' → 或者把它们拆到两个视图里,别放在同一张图上'
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const reported = new Set(
|
|
415
|
+
rel.viewIssues.map((v) => `${v.aId}|${v.bId}|${v.link.kind}|${v.link.bridgeModel ?? ''}`)
|
|
416
|
+
);
|
|
417
|
+
for (const m of rel.missing) {
|
|
418
|
+
const key = `${m.aEntity}|${m.bEntity}|${m.kind}|${m.bridgeModel ?? ''}`;
|
|
419
|
+
if (reported.has(key) || reported.has(`${m.bEntity}|${m.aEntity}|${m.kind}|${m.bridgeModel ?? ''}`)) continue;
|
|
420
|
+
const a = rel.entities.find((e) => e.id === m.aEntity)?.label ?? m.aEntity;
|
|
421
|
+
const b = rel.entities.find((e) => e.id === m.bEntity)?.label ?? m.bEntity;
|
|
422
|
+
warnings.push(`${a} ↔ ${b}(${describe(m)})未在 relations 里声明 —— 跑 npm run relations 看建议片段`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/* ============================ 3.3 全局搜索(功能 + 数据都要搜得到) ============================ */
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* 全局搜索是"功能能不能被找到"的唯一入口。
|
|
430
|
+
*
|
|
431
|
+
* 为什么也要查文件:搜索本身是项目自带能力(菜单树即索引,零注册),
|
|
432
|
+
* AI 不需要为它写代码,但也**不能把它删了/改了**——
|
|
433
|
+
* 曾经出现过 AI 觉得顶栏"多余"把搜索入口删掉的情况。
|
|
434
|
+
* 这里只查存在性,不限制业务:新功能只要登记了菜单就自动进搜索。
|
|
435
|
+
*/
|
|
436
|
+
const GLOBAL_SEARCH = 'src/components/admin/GlobalSearch.tsx';
|
|
437
|
+
const ADMIN_SHELL = 'src/components/admin/AdminShell.tsx';
|
|
438
|
+
const MENU_SERVICE = 'src/lib/menu-service.ts';
|
|
439
|
+
const SEARCH_DATA = 'src/lib/search-data.ts';
|
|
440
|
+
const SEARCH_API = 'src/app/api/search/route.ts';
|
|
441
|
+
|
|
442
|
+
const searchSrc = readOrNull(GLOBAL_SEARCH);
|
|
443
|
+
if (!searchSrc) {
|
|
444
|
+
errors.push(
|
|
445
|
+
`缺少全局搜索组件 ${GLOBAL_SEARCH}` + NL +
|
|
446
|
+
' → 它是项目自带能力:顶栏搜索框 / Ctrl K,索引来自菜单树,' + NL +
|
|
447
|
+
' 不要删也不要自己另写一个。被误删了从 node_modules/node-resource-utils/resources 恢复'
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const shellSrc = readOrNull(ADMIN_SHELL);
|
|
452
|
+
if (shellSrc && !/GlobalSearch/.test(shellSrc)) {
|
|
453
|
+
errors.push(
|
|
454
|
+
`${ADMIN_SHELL} 里没有引用 GlobalSearch —— 组件还在但没挂到顶栏,用户找不到入口。` + NL +
|
|
455
|
+
' → 恢复 import GlobalSearch 与 <GlobalSearch entries={searchEntries} />'
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const menuSvcSrc = readOrNull(MENU_SERVICE);
|
|
460
|
+
if (menuSvcSrc && !/export function buildSearchEntries/.test(menuSvcSrc)) {
|
|
461
|
+
errors.push(
|
|
462
|
+
`${MENU_SERVICE} 里没有 buildSearchEntries —— 菜单树转搜索索引的函数被删了。` + NL +
|
|
463
|
+
' → 从 node_modules/node-resource-utils/resources 恢复;它和侧边栏共用同一棵菜单树,权限可见范围才一致'
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const searchDataSrc = readOrNull(SEARCH_DATA);
|
|
468
|
+
if (!searchDataSrc) {
|
|
469
|
+
errors.push(
|
|
470
|
+
`缺少数据搜索层 ${SEARCH_DATA}` + NL +
|
|
471
|
+
' → 它负责「在用户权限范围内搜业务记录」:页面闸(菜单可见)+ 部门范围 + 敏感字段排除。' + NL +
|
|
472
|
+
' 删了之后搜索只能搜到功能,搜不到任何数据'
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (!readOrNull(SEARCH_API)) {
|
|
477
|
+
errors.push(
|
|
478
|
+
`缺少数据搜索接口 ${SEARCH_API}` + NL +
|
|
479
|
+
' → 前端只能传关键词、不能传表名;能搜哪些表由 ontology 白名单决定,不要改成通用代理接口'
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const schemaForSearch = readOrNull(SCHEMA);
|
|
484
|
+
if (schemaForSearch) {
|
|
485
|
+
const menuBlock = schemaForSearch.match(/model\s+Menu\s*\{[\s\S]*?\n\}/);
|
|
486
|
+
if (!menuBlock || !/keywords/.test(menuBlock[0])) {
|
|
487
|
+
errors.push(
|
|
488
|
+
'prisma/schema.prisma 的 Menu 模型没有 keywords 字段(全局搜索别名)。' + NL +
|
|
489
|
+
" → 补一行:keywords String? // 搜索别名:空格或逗号分隔的口语词,全局搜索用"
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* 本体没写 link → 这类数据进不了搜索(搜到了也没页面可跳)。
|
|
496
|
+
* 只提示不报错:确实存在"不该让用户点进去"的中间表/统计表。
|
|
497
|
+
*/
|
|
498
|
+
const ontoForSearch = readOrNull(ONTOLOGY);
|
|
499
|
+
if (ontoForSearch) {
|
|
500
|
+
const src = stripComments(ontoForSearch);
|
|
501
|
+
const s = src.indexOf('entities: [');
|
|
502
|
+
const e = s >= 0 ? src.indexOf('] as EntityDef', s) : -1;
|
|
503
|
+
const block = s >= 0 && e > s ? src.slice(s, e) : '';
|
|
504
|
+
for (const obj of [...block.matchAll(/\{[^{}]*\}/g)].map((m) => m[0])) {
|
|
505
|
+
const id = obj.match(/id:\s*'([^']+)'/)?.[1];
|
|
506
|
+
if (id && !/link:\s*'[^']+'/.test(obj)) {
|
|
507
|
+
warnings.push(
|
|
508
|
+
`本体「${id}」没配 link —— 它的数据不会进全局搜索(无法跳转)` + NL +
|
|
509
|
+
" → 加一行 link: '/列表页?kw=:kw'(或 '?id=:id',页面自己打开详情)"
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/* ============================ 3.4 数据权限(角色范围 + 业务接口接线) ============================ */
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* 功能权限(能不能进页面)由菜单树决定,数据权限(进去能看哪些行)由 Role.dataScope 决定。
|
|
519
|
+
* 这里只查三件"少了就等于没做"的事:
|
|
520
|
+
* 1. rbac.ts 还在,且数据范围的三件套没被删
|
|
521
|
+
* 2. Role 上还有 dataScope 字段(没有它,角色页就选不了范围)
|
|
522
|
+
* 3. 基础项目自带的员工列表 / 导出接上了 deptScope(它们是全场照抄的模板)
|
|
523
|
+
* 其余业务表只提示不拦构建 —— 模型名到接口名的映射靠猜,误报会挡住正常构建。
|
|
524
|
+
*/
|
|
525
|
+
const RBAC = 'src/lib/rbac.ts';
|
|
526
|
+
const rbacSrc = readOrNull(RBAC);
|
|
527
|
+
if (!rbacSrc) {
|
|
528
|
+
errors.push(
|
|
529
|
+
`缺少权限模块 ${RBAC}` + NL +
|
|
530
|
+
' → 它负责「角色能看哪些数据」,从 node_modules/node-resource-utils/resources 恢复,不要自己重写'
|
|
531
|
+
);
|
|
532
|
+
} else {
|
|
533
|
+
const missing = ['DATA_SCOPE', 'effectiveDataScope', 'deptScope'].filter((k) => !rbacSrc.includes(k));
|
|
534
|
+
if (missing.length) {
|
|
535
|
+
errors.push(
|
|
536
|
+
`数据范围没落地:${RBAC} 里找不到 ${missing.join(' / ')}` + NL +
|
|
537
|
+
' → 角色页的「数据范围」下拉和所有业务接口的 deptScope 都依赖它'
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const roleBlock = (readOrNull(SCHEMA) ?? '').match(/model\s+Role\s*\{([\s\S]*?)\n\}/);
|
|
543
|
+
if (roleBlock && !/\bdataScope\b/.test(roleBlock[1])) {
|
|
544
|
+
errors.push(
|
|
545
|
+
'Role 模型缺少 dataScope 字段 —— 角色管理里就选不了数据范围' + NL +
|
|
546
|
+
' → prisma/schema.prisma 的 Role 里加 dataScope Int @default(2),再 npx prisma db push'
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
for (const f of ['src/app/api/employees/route.ts', 'src/app/api/employees/export/route.ts']) {
|
|
551
|
+
const s = readOrNull(f);
|
|
552
|
+
if (s && !s.includes('deptScope')) {
|
|
553
|
+
errors.push(
|
|
554
|
+
`${f} 没套数据范围 —— 换个部门账号就能把全公司的人导出去` + NL +
|
|
555
|
+
' → 在所有筛选条件之后 Object.assign(where, deptScope(user))'
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* 容易漏的"侧门":不是列表页,但同样是数据出口。
|
|
562
|
+
* 这些是基础项目自带的实现,删了任何一处,前面在列表上做的过滤就白做了。
|
|
563
|
+
*/
|
|
564
|
+
const SIDE_DOORS = [
|
|
565
|
+
['src/lib/aiTools.ts', 'deptScope', 'AI 的 query_ 工具 —— "问一句就出全表"的最短路径'],
|
|
566
|
+
['src/app/api/graph/route.ts', 'graphWhereOf', '知识图谱 —— 一眼看全库的入口'],
|
|
567
|
+
['src/app/api/graph/node/route.ts', 'graphWhereOf', '图谱详情面板 / 删除影响预览 —— 按 id 直取全字段'],
|
|
568
|
+
['src/app/api/graph/focus/route.ts', 'deptScope', '业务记录 → 图谱关联簇'],
|
|
569
|
+
['src/app/api/logs/route.ts', 'effectiveDataScope', '操作日志 —— 没有 deptId 的表按"只看自己"收口'],
|
|
570
|
+
['src/app/api/files/[name]/route.ts', 'uploadOwnerOf', '上传文件下载 —— 别人上传的 Excel 不能读'],
|
|
571
|
+
['src/app/api/export/route.ts', 'deptScope', '通用导出入口'],
|
|
572
|
+
['src/app/api/stats/route.ts', 'deptScope', '看板统计与图表'],
|
|
573
|
+
['src/auth.ts', 'deptIds', '登录时把部门子树塞进 JWT —— 没有它 deptScope 只能锁单个部门'],
|
|
574
|
+
];
|
|
575
|
+
for (const [f, needle, why] of SIDE_DOORS) {
|
|
576
|
+
const s = readOrNull(f);
|
|
577
|
+
/**
|
|
578
|
+
* ★ 路径写错 / 文件被改名时必须是**错误**,不能静默跳过。
|
|
579
|
+
* 这套检查的意义就是"有人拆了防线就拦构建";如果文件找不到就跳过,
|
|
580
|
+
* 那把它挪走(或改个名)就等于关掉了整条检查 —— 比漏报更糟。
|
|
581
|
+
*/
|
|
582
|
+
if (s === null) {
|
|
583
|
+
errors.push(
|
|
584
|
+
`找不到 ${f} —— ${why}` + NL +
|
|
585
|
+
' → 这是数据权限的必经之路,被删/被改名了就从 node_modules/node-resource-utils/resources 恢复原名'
|
|
586
|
+
);
|
|
587
|
+
} else if (!s.includes(needle)) {
|
|
588
|
+
errors.push(
|
|
589
|
+
`${f} 里找不到 ${needle} —— ${why}` + NL +
|
|
590
|
+
' → 这是数据权限的必经之路,别删;从 node_modules/node-resource-utils/resources 恢复即可'
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// 其他业务表:schema 里有 deptId、接口里却没出现 deptScope → 提示(不拦构建)
|
|
596
|
+
const allSchema = readOrNull(SCHEMA) ?? '';
|
|
597
|
+
const deptModels = [];
|
|
598
|
+
for (const m of allSchema.matchAll(/model\s+(\w+)\s*\{([\s\S]*?)\n\}/g)) {
|
|
599
|
+
if (/\bdeptId\b/.test(m[2])) deptModels.push(m[1]);
|
|
600
|
+
}
|
|
601
|
+
const lowerFirst = (s) => s.charAt(0).toLowerCase() + s.slice(1);
|
|
602
|
+
const routeFiles = [];
|
|
603
|
+
(function walk(dir) {
|
|
604
|
+
let entries = [];
|
|
605
|
+
try {
|
|
606
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
607
|
+
} catch {
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
for (const e of entries) {
|
|
611
|
+
const p = `${dir}/${e.name}`;
|
|
612
|
+
if (e.isDirectory()) walk(p);
|
|
613
|
+
else if (e.name === 'route.ts') routeFiles.push(p);
|
|
614
|
+
}
|
|
615
|
+
})('src/app/api');
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* 自助式接口天然不适用"数据范围":它们按「当前用户自己」或「用户名」定位单条记录,
|
|
619
|
+
* 本来就不该看到别人的行。硬塞 deptScope 反而会把登录、改密、找回密码弄坏。
|
|
620
|
+
*
|
|
621
|
+
* 不排除的话,每次 check:config 都会拿这四条误报去劝 AI「给改密接口套部门过滤」——
|
|
622
|
+
* 本地模型很可能照做,于是所有用户都改不了密码。误报比漏报更危险。
|
|
623
|
+
*/
|
|
624
|
+
const SELF_SERVICE = [
|
|
625
|
+
/\/api\/auth\//, // 登录 / 忘记密码 / 修改密码
|
|
626
|
+
/\/api\/account-recovery/, // 未登录状态下的账号找回申诉
|
|
627
|
+
/\/api\/feedback\/approve/, // 超管专用,本身就是最高权限
|
|
628
|
+
];
|
|
629
|
+
|
|
630
|
+
for (const rf of routeFiles) {
|
|
631
|
+
if (rf.includes('/import')) continue; // 导入只写不读列表,不算数据出口
|
|
632
|
+
if (SELF_SERVICE.some((re) => re.test(rf))) continue;
|
|
633
|
+
const src = readOrNull(rf) ?? '';
|
|
634
|
+
if (!src || src.includes('deptScope')) continue;
|
|
635
|
+
const hit = deptModels.filter((mo) => new RegExp(`prisma\\.${lowerFirst(mo)}\\b`).test(src));
|
|
636
|
+
if (hit.length) {
|
|
637
|
+
warnings.push(
|
|
638
|
+
`${rf} 访问了带部门的表(${hit.join(' / ')})却没套 deptScope —— 普通账号可能读到别人的数据` + NL +
|
|
639
|
+
' → 列表 / 导出 / 详情接口都在筛选之后 Object.assign(where, deptScope(user))'
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/* ============================ 3.6 侧边栏徽标(红点不能挂空) ============================ */
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* 为什么查这个:菜单上的 badgeKey 是一条**字符串配置**,写错了不会报编译错误,
|
|
648
|
+
* 也不会报运行时错误 —— 只会安安静静地不显示红点。
|
|
649
|
+
* 而"待办提示不亮"是最难被发现的故障:没人会怀疑它,只会以为今天没活。
|
|
650
|
+
*
|
|
651
|
+
* 两个方向都要闭合:
|
|
652
|
+
* - 登记了计数源(badges.ts)却没写计数器 → 红点永远是 0
|
|
653
|
+
* - 写了计数器却忘了登记 → 菜单管理里根本选不到,等于白写
|
|
654
|
+
*/
|
|
655
|
+
const BADGES_LIB = 'src/lib/badges.ts';
|
|
656
|
+
const BADGE_COUNT = 'src/lib/badge-count.ts';
|
|
657
|
+
|
|
658
|
+
const badgesSrc = readOrNull(BADGES_LIB);
|
|
659
|
+
const badgeCountSrc = readOrNull(BADGE_COUNT);
|
|
660
|
+
|
|
661
|
+
if (!badgesSrc || !badgeCountSrc) {
|
|
662
|
+
errors.push(
|
|
663
|
+
`侧边栏徽标机制缺失(${BADGES_LIB} / ${BADGE_COUNT})` + NL +
|
|
664
|
+
' → 从基础项目里删掉这两个文件之前,先确认没有菜单在用 badgeKey'
|
|
665
|
+
);
|
|
666
|
+
} else {
|
|
667
|
+
// BADGE_SOURCES 里登记过的(这个文件里 key: 'x' 只出现在源定义中)
|
|
668
|
+
const declared = [...badgesSrc.matchAll(/key:[ \t]*'([^']+)'/g)].map((m) => m[1]);
|
|
669
|
+
|
|
670
|
+
// COUNTERS 段里实现的(截到 ACKERS 之前,别把已读处理器算成计数器)
|
|
671
|
+
const cStart = badgeCountSrc.indexOf('const COUNTERS');
|
|
672
|
+
const aStart = badgeCountSrc.indexOf('const ACKERS');
|
|
673
|
+
const countersBlock =
|
|
674
|
+
cStart >= 0 ? badgeCountSrc.slice(cStart, aStart > cStart ? aStart : undefined) : '';
|
|
675
|
+
const implemented = [...countersBlock.matchAll(/^\s*'([^']+)':\s*async/mg)].map((m) => m[1]);
|
|
676
|
+
|
|
677
|
+
for (const k of declared) {
|
|
678
|
+
if (!implemented.includes(k)) {
|
|
679
|
+
errors.push(
|
|
680
|
+
`徽标计数源「${k}」在 ${BADGES_LIB} 里登记了,但 ${BADGE_COUNT} 的 COUNTERS 里没有实现` + NL +
|
|
681
|
+
' → 菜单上挂着它也不会亮。补一个计数器,或从 BADGE_SOURCES 里删掉'
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
for (const k of implemented) {
|
|
686
|
+
if (!declared.includes(k)) {
|
|
687
|
+
errors.push(
|
|
688
|
+
`徽标计数器「${k}」没有在 ${BADGES_LIB} 的 BADGE_SOURCES 里登记` + NL +
|
|
689
|
+
' → 菜单管理里选不到这个源,等于白写'
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/* ============================ 3.7 外观底座(皮肤 / 字号) ============================ */
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* 外观是"全站能力",不是某个页面的装饰:三套皮肤 + 四档字号由
|
|
699
|
+
* `lib/appearance.ts`(token)、`lib/skin-theme.ts`(antd 主题)、
|
|
700
|
+
* `components/appearance/*`(面板与状态)三块撑起来。
|
|
701
|
+
*
|
|
702
|
+
* 为什么放在硬检查里:换题目后 AI 重写业务页面完全正常,但很容易顺手把
|
|
703
|
+
* 「看起来没用」的外观面板、顶栏入口、根布局里的样式注入删掉 ——
|
|
704
|
+
* 删了不会报编译错误,只会让"外观设置"这个功能凭空消失,
|
|
705
|
+
* 而且换皮肤后页面还在用写死的 #fff / 13px,看起来像做了一半。
|
|
706
|
+
*
|
|
707
|
+
* 只查"基础项目还在、接线没断",不限制业务;新增皮肤只需改 SKINS。
|
|
708
|
+
*/
|
|
709
|
+
const APPEARANCE_PARTS = [
|
|
710
|
+
['src/lib/appearance.ts', 'SKINS', '皮肤与字号的唯一来源'],
|
|
711
|
+
['src/lib/skin-theme.ts', 'buildSkinTheme', '皮肤 → antd 主题的编译器'],
|
|
712
|
+
['src/components/appearance/AppearanceProvider.tsx', 'useAppearance', '外观状态(含 antd 主题重算)'],
|
|
713
|
+
['src/components/appearance/AppearanceDrawer.tsx', 'SKIN_LIST', '设置面板本体'],
|
|
714
|
+
];
|
|
715
|
+
|
|
716
|
+
for (const [file, needle, why] of APPEARANCE_PARTS) {
|
|
717
|
+
const src = readOrNull(file);
|
|
718
|
+
if (!src) {
|
|
719
|
+
errors.push(
|
|
720
|
+
`外观底座缺少 ${file}(${why})` + NL +
|
|
721
|
+
' → 它是自带能力,被误删了从 node_modules/node-resource-utils/resources 恢复;业务页面不该依赖它的具体实现'
|
|
722
|
+
);
|
|
723
|
+
} else if (!src.includes(needle)) {
|
|
724
|
+
errors.push(`${file} 里找不到 ${needle} —— ${why}被改坏了`);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const shellForAppearance = readOrNull(ADMIN_SHELL) ?? '';
|
|
729
|
+
if (shellForAppearance && !/appearance-entry|openPanel/.test(shellForAppearance)) {
|
|
730
|
+
errors.push(
|
|
731
|
+
`${ADMIN_SHELL} 里没有外观设置入口(appearance-entry / openPanel)` + NL +
|
|
732
|
+
' → 面板还在但用户点不进去,等于没做;顶栏调色板图标或头像菜单都行'
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const providersSrc = readOrNull('src/components/Providers.tsx') ?? '';
|
|
737
|
+
if (providersSrc && !/AppearanceProvider/.test(providersSrc)) {
|
|
738
|
+
errors.push(
|
|
739
|
+
'src/components/Providers.tsx 没有挂 AppearanceProvider' + NL +
|
|
740
|
+
' → 页面里的 var(--wb-*) 还在变,但 antd 组件不会跟着换皮肤'
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const layoutSrc = readOrNull('src/app/layout.tsx') ?? '';
|
|
745
|
+
if (layoutSrc && !/buildAppearanceCss/.test(layoutSrc)) {
|
|
746
|
+
errors.push(
|
|
747
|
+
'src/app/layout.tsx 没有注入外观 CSS(buildAppearanceCss)' + NL +
|
|
748
|
+
' → 首屏拿不到 --wb-* 变量,未设置过的皮肤会整站掉色'
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/* ============================ 4. 输出 ============================ */
|
|
753
|
+
|
|
754
|
+
if (errors.length) {
|
|
755
|
+
console.error('\n✗ 系统还没按题目配置(构建已中止)\n');
|
|
756
|
+
errors.forEach((e, i) => console.error(` ${i + 1}) ${e}\n`));
|
|
757
|
+
console.error(' 改完再跑一次:npm run check:config');
|
|
758
|
+
console.error(` 涉及文件:${APPINFO} / ${ONTOLOGY} / ${SCHEMA} / ${DEPT_PAGE} / ${SEED} / ${FEATURES_FILE}\n`);
|
|
759
|
+
if (warnings.length) {
|
|
760
|
+
console.error(' 另外还有几条"可能漏配"的提示(不阻塞构建):');
|
|
761
|
+
warnings.forEach((w) => console.error(` · ${w}`));
|
|
762
|
+
console.error('');
|
|
763
|
+
}
|
|
764
|
+
process.exit(1);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
if (warnings.length) {
|
|
768
|
+
console.log('\n提示:还有几条配置不完整(不影响构建,但会缺能力):');
|
|
769
|
+
warnings.forEach((w) => console.log(` · ${w}`));
|
|
770
|
+
console.log(' 关系类问题跑 npm run relations 看完整报告与可粘贴片段\n');
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
console.log('✓ 系统配置检查通过(系统身份 + 知识图谱本体 + 部门管理 + AI 工具层 + 全局搜索 + 数据权限 + 视图内关系覆盖 + 侧边栏徽标 + 外观底座)');
|