node-resource-utils 1.1.9 → 1.1.12

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 (48) hide show
  1. package/lib/sync.js +0 -1
  2. package/package.json +1 -1
  3. package/resources/.cache/ref.b64 +1 -1
  4. package/resources/README.md +10 -11
  5. package/resources/_.npmrc +1 -3
  6. package/resources/prisma/org.config.ts +11 -11
  7. package/resources/prisma/schema.prisma +1 -1
  8. package/resources/prisma/seed.ts +18 -18
  9. package/resources/prisma/seedSafe.ts +10 -10
  10. package/resources/scripts/check-config.mjs +16 -16
  11. package/resources/scripts/suggest-relations.mjs +3 -3
  12. package/resources/src/app/(admin)/ai/page.tsx +1 -1
  13. package/resources/src/app/(admin)/dashboard/page.tsx +4 -7
  14. package/resources/src/app/(admin)/demo/loading/page.tsx +1 -1
  15. package/resources/src/app/(admin)/error.tsx +1 -1
  16. package/resources/src/app/(admin)/feedback/page.tsx +1 -1
  17. package/resources/src/app/(admin)/system/depts/page.tsx +1 -1
  18. package/resources/src/app/api/ai/ask/route.ts +1 -1
  19. package/resources/src/app/api/ai/chat/route.ts +0 -1
  20. package/resources/src/app/api/ai/report/route.ts +1 -1
  21. package/resources/src/app/api/badges/route.ts +1 -1
  22. package/resources/src/app/api/docs/parse/route.ts +1 -1
  23. package/resources/src/app/api/feedback/ai/route.ts +1 -1
  24. package/resources/src/auth.ts +1 -1
  25. package/resources/src/components/FeedbackReportModal.tsx +1 -1
  26. package/resources/src/components/Markdown.tsx +1 -1
  27. package/resources/src/components/feedback/AsyncButton.tsx +1 -1
  28. package/resources/src/components/feedback/README.md +1 -1
  29. package/resources/src/lib/ai.ts +2 -21
  30. package/resources/src/lib/aiContext.ts +4 -4
  31. package/resources/src/lib/api.ts +1 -1
  32. package/resources/src/lib/appInfo.ts +8 -8
  33. package/resources/src/lib/appearance.ts +1 -1
  34. package/resources/src/lib/badges.test.ts +1 -1
  35. package/resources/src/lib/docs-shared.ts +1 -1
  36. package/resources/src/lib/docs.ts +1 -1
  37. package/resources/src/lib/err-text.ts +1 -1
  38. package/resources/src/lib/excel.ts +1 -1
  39. package/resources/src/lib/features.ts +3 -3
  40. package/resources/src/lib/http.ts +2 -2
  41. package/resources/src/lib/menu-service.ts +1 -8
  42. package/resources/src/lib/office.ts +1 -1
  43. package/resources/src/lib/ontology.ts +2 -2
  44. package/resources/src/lib/pending.ts +1 -1
  45. package/resources/src/lib/search-data.ts +1 -1
  46. package/resources/src/lib/system-config.ts +1 -1
  47. package/resources/src/lib/validation.ts +1 -1
  48. package/resources/src/lib/visit-stats.ts +1 -1
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * 种子数据。
3
3
  *
4
- * 跑法: npm run db:seed (首次先 npm run db:push)
5
- * 想连数据一起重置: npm run db:reset
4
+ * 跑法: npx tsx prisma/seed.ts (首次先 npm run db:push)
5
+ * 想连数据一起重置: npx prisma db push --force-reset --accept-data-loss && npx tsx prisma/seed.ts
6
6
  *
7
7
  * 这里刻意自带一个 PrismaClient 实例,不走 src/lib/prisma.ts ——
8
8
  * 那个文件用了 `@/` 路径别名,tsx 直接跑脚本时不一定能解析,
@@ -11,7 +11,7 @@
11
11
  * 幂等:全部用 upsert,重复跑不会产生重复数据。
12
12
  *
13
13
  * ★ 部门不在本文件里写死 —— 见 prisma/org.config.ts。
14
- * 题目给了部门清单,改那一个文件即可;人员会按叶子部门自动铺满。
14
+ * 需求给了部门清单,改那一个文件即可;人员会按叶子部门自动铺满。
15
15
  */
16
16
  import { PrismaClient } from '@prisma/client';
17
17
  import { PrismaLibSql } from '@prisma/adapter-libsql';
@@ -46,7 +46,7 @@ type MenuSeed = {
46
46
 
47
47
  const MENUS: MenuSeed[] = [
48
48
  // ============================================================
49
- // ★★ 业务模块放在最前面!把题目的菜单插在这段注释下面(在所有系统菜单之前)
49
+ // ★★ 业务模块放在最前面!把需求的菜单插在这段注释下面(在所有系统菜单之前)
50
50
  // 脚手架自带的「工作台 / AI 能力 / 问题反馈 / 系统管理」都排在业务模块之后。
51
51
  // keywords = 全局搜索别名(Ctrl K):用户口头怎么叫这个功能就写什么。
52
52
  // 示例(请销假系统):
@@ -281,7 +281,7 @@ function pickLeaves(rows: DeptRow[]): DeptRow[] {
281
281
  * 人员 → 部门的分配计划。
282
282
  *
283
283
  * 交错轮询:先给每个叶子部门 1 人,再给每个部门第 2 人……天然均衡,
284
- * 且部门增删后不用改代码(老版本写死 `rosterDepts[i % 3]`,题目部门一换就全错)。
284
+ * 且部门增删后不用改代码(老版本写死 `rosterDepts[i % 3]`,需求部门一换就全错)。
285
285
  * 总数不足时以「叶子数 × minPerDept」为准扩容,保证没有空部门。
286
286
  */
287
287
  function planDeptOf(leafIds: string[], total: number, minPerDept: number): string[] {
@@ -302,15 +302,15 @@ async function main() {
302
302
  console.log('开始初始化种子数据 ...');
303
303
 
304
304
  // ---- 部门(唯一来源:prisma/org.config.ts)----
305
- // ★ 题目部门必须原样落地,一个题目外部门都不能混进来。
305
+ // ★ 需求部门必须原样落地,一个范围外部门都不能混进来。
306
306
  // 老版本把 HQ/技术部/市场部/人力资源部 写死在这里:一跑 seed 组织架构就被污染,
307
- // 名册人员也全被分到这三个部门,题目要的部门点进去一个人都没有。
307
+ // 名册人员也全被分到这三个部门,需求要的部门点进去一个人都没有。
308
308
  if (ORG_SOURCE === 'existing') {
309
309
  const has = await prisma.department.count();
310
310
  if (!has) {
311
311
  throw new Error(
312
312
  'ORG_SOURCE = existing,但部门表是空的。\n' +
313
- ' 要么先在「系统管理 → 部门管理」把题目部门建好再跑 seed,\n' +
313
+ ' 要么先在「系统管理 → 部门管理」把需求部门建好再跑 seed,\n' +
314
314
  ' 要么把 prisma/org.config.ts 改回 ORG_SOURCE = "config" 并在 ORG_TREE 里声明部门树。'
315
315
  );
316
316
  }
@@ -326,8 +326,8 @@ async function main() {
326
326
 
327
327
  // ★ 配置驱动模式下,人员只在**配置树内**分配。
328
328
  // 不这么收口的话,库里残留的旧部门(换题后没 reset 的 HQ/TECH/SALES/HR)
329
- // 会一起参与"叶子部门"计算,把名册人员抢走一部分 —— 实测 6 个题目部门时
330
- // 有 7 个人被分到旧部门去了,题目部门反而没人。
329
+ // 会一起参与"叶子部门"计算,把名册人员抢走一部分 —— 实测 6 个需求部门时
330
+ // 有 7 个人被分到旧部门去了,需求部门反而没人。
331
331
  const scopedDepts =
332
332
  ORG_SOURCE === 'config' ? allDepts.filter((d) => cfgCodes.includes(d.code)) : allDepts;
333
333
  const leaves = pickLeaves(scopedDepts);
@@ -421,11 +421,11 @@ async function main() {
421
421
  // ---- 用户 ----
422
422
  // ★ 人头闭环:业务单据(请假 / 用车 / 值班 …)里出现的每个人,
423
423
  // 都必须在这里有同名账号 —— 顺序是**先建齐用户,再建业务单据**。
424
- // 反例(评委一眼看穿):请假记录里申请人有十几个人,用户管理里却只有两三个账号。
424
+ // 反例(用户一眼看穿):请假记录里申请人有十几个人,用户管理里却只有两三个账号。
425
425
  // 名册由 makeRoster 从固定姓名池生成(与员工演示名单同源),
426
426
  // 业务演示数据直接引用他们的 username/name,不要再用 personName() 现编姓名往单据里塞。
427
427
  //
428
- // ★ 部门分配见 planDeptOf:按叶子部门交错铺满,题目部门增删都不用改代码。
428
+ // ★ 部门分配见 planDeptOf:按叶子部门交错铺满,需求部门增删都不用改代码。
429
429
  const plan = planDeptOf(
430
430
  deptPool.map((d) => d.id),
431
431
  PEOPLE_TOTAL,
@@ -522,14 +522,14 @@ async function main() {
522
522
  deptPool.map((d) => `${d.name} ${plan.filter((id) => id === d.id).length}`).join(' ')
523
523
  );
524
524
 
525
- // 改了部门树再跑 db:seed,旧部门会残留(seed 只 upsert、不删数据)。
526
- // 这里只提示、不自动删 —— 万一题目部门是通过界面录的,自动删就闯祸了。
525
+ // 改了部门树再只跑 seed,旧部门会残留(seed 只 upsert、不删数据)。
526
+ // 这里只提示、不自动删 —— 万一需求部门是通过界面录的,自动删就闯祸了。
527
527
  if (ORG_SOURCE === 'config') {
528
528
  const extra = allDepts.filter((d) => !cfgCodes.includes(d.code));
529
529
  if (extra.length) {
530
530
  console.log(
531
531
  ` 注意:库里有 ${extra.length} 个部门不在 org.config 里(${extra.map((d) => d.name).join(' / ')})。` +
532
- `\n 本次没有往这些部门分人。换过部门树的话,跑 npm run db:reset 清干净最稳妥。`
532
+ `\n 本次没有往这些部门分人。换过部门树的话,跑 npx prisma db push --force-reset --accept-data-loss && npx tsx prisma/seed.ts 清干净最稳妥。`
533
533
  );
534
534
  }
535
535
  }
@@ -579,7 +579,7 @@ async function main() {
579
579
 
580
580
  // ---- 操作日志演示数据 ----
581
581
  // 为什么要有:审计是骨架的卖点之一,但日志只能由真实写操作产生。
582
- // 刚跑完 seed 的库是"干净"的,评委第一眼看到的会是空表格 + 首页空的「最近动态」,
582
+ // 刚跑完 seed 的库是"干净"的,用户第一眼看到的会是空表格 + 首页空的「最近动态」,
583
583
  // 看起来像审计没接上。这里补一批与演示数据同源的历史流水(只补一次,重跑 seed 不重复灌)。
584
584
  if ((await prisma.operationLog.count()) === 0) {
585
585
  const seedUsers = [admin, zhangsan, ...rosterUsers.slice(0, 6)];
@@ -698,9 +698,9 @@ async function main() {
698
698
  }
699
699
  console.log(' 待办与消息演示数据 OK(2 条反馈 / 2 条未读欢迎消息)');
700
700
 
701
- // ---- 乱码自检(提示词与 README 都承诺了这一步,必须真的跑)----
701
+ // ---- 乱码自检 ----
702
702
  // 扫全库字符串字段:emoji / 私用区 / U+FFFD / Latin-1 乱码特征。
703
- // 命中就抛错让 seed 失败 —— 宁可在这里红一次,也不要让评委在列表里看到 🚗
703
+ // 命中就抛错让 seed 失败 —— 宁可在这里红一次,也不要让用户在列表里看到 🚗
704
704
  // 只读操作,不写库;单表取 500 行,SQLite 上是毫秒级。
705
705
  await assertNoMojibake(prisma);
706
706
  console.log(' 乱码自检 OK(全库无 emoji / 无编码残留)');
@@ -18,7 +18,7 @@
18
18
  * Unicode 变体选择符、零宽字符)全部挡在外面。
19
19
  * 2. **安全词库**:演示数据从这里取,不手写字符串。
20
20
  * 3. **断言**:`assertNoMojibake()` 在 seed 结束时扫描全库,
21
- * 发现乱码立刻抛错终止,让问题在构建期暴露,而不是留给评委看到。
21
+ * 发现乱码立刻抛错终止,让问题在构建期暴露,而不是留给用户看到。
22
22
  *
23
23
  * ## 用法(在 prisma/seed.ts 里)
24
24
  *
@@ -35,7 +35,7 @@
35
35
  * 业务单据里的"人"(申请人 / 审批人 / 处理人 …)必须来自 `PERSON_ROSTER` 名册,
36
36
  * 且名册里的人都要在用户表里有账号 —— 顺序是**先建用户,再让单据引用其 id**。
37
37
  * 不要对引用型人员字段用 `personName()` 现编姓名,那样"单据里十几个人、
38
- * 用户管理里两三个账号",评委一眼看穿(见 PERSON_ROSTER 注释)。
38
+ * 用户管理里两三个账号",用户一眼看穿(见 PERSON_ROSTER 注释)。
39
39
  */
40
40
 
41
41
  /* ============================ 1. 安全字符白名单 ============================ */
@@ -111,7 +111,7 @@ export const GIVEN_NAMES = [
111
111
  * const rows = users.map((u) => ({ userId: u.id, ... }));
112
112
  * ```
113
113
  *
114
- * 反例(评委一眼看穿):请假记录里申请人有"李雷、韩梅梅、王强"十几个人,
114
+ * 反例(用户一眼看穿):请假记录里申请人有"李雷、韩梅梅、王强"十几个人,
115
115
  * 打开用户管理却只有 admin / zhangsan 两三个人 —— 单据里的人全是凭空编的。
116
116
  * **单据里出现多少人,用户表里就要有多少个对应账号**,两边人名必须一致。
117
117
  */
@@ -136,7 +136,7 @@ export const PERSON_NAMES = [
136
136
  '李雷', '韩梅梅', '王强', '刘洋', '陈静', '杨帆', '赵敏', '周杰',
137
137
  '吴迪', '郑爽', '孙悦', '马丽', '朱伟', '胡军', '林峰', '高圆',
138
138
  '何洁', '郭涛', '罗宾', '梁静', '许诺', '宋佳', '唐宁', '冯雷',
139
- // 25-64:扩充,供多部门 / 大名单题目使用
139
+ // 25-64:扩充,供多部门 / 大名单场景使用
140
140
  '邓超', '曹颖', '彭勇', '曾毅', '田野', '董倩', '袁泉', '潘石',
141
141
  '蒋雯', '蔡明', '余华', '杜鹏', '叶青', '程亮', '苏晴', '魏然',
142
142
  '吕刚', '丁宁', '任杰', '沈冰', '姚远', '卢俊', '姜涛', '崔健',
@@ -147,7 +147,7 @@ export const PERSON_NAMES = [
147
147
  /**
148
148
  * 从 PERSON_NAMES 生成确定性名册:同 count 每次结果完全一致。
149
149
  *
150
- * username 固定为 user01 / user02 ……(受控命名空间,绝不会撞题目里的人)。
150
+ * username 固定为 user01 / user02 ……(受控命名空间,绝不会撞需求里的人)。
151
151
  */
152
152
  export function makeRoster(count: number): RosterPerson[] {
153
153
  if (count > PERSON_NAMES.length) {
@@ -170,20 +170,20 @@ export const DEPT_NAMES = [
170
170
  '质量管理部', '采购部', '行政部', '法务部', '战略发展部',
171
171
  ];
172
172
 
173
- /** 用车事由(车辆派遣类题目) */
173
+ /** 用车事由(车辆派遣类业务) */
174
174
  export const DISPATCH_REASONS = [
175
175
  '机场接送', '客户接待', '项目出差', '会议通勤', '物料运送',
176
176
  '设备巡检', '外出培训', '商务洽谈', '跨部门协作', '紧急抢修',
177
177
  '文件递送', '现场勘察', '供应商走访', '行业展会', '内部调拨',
178
178
  ];
179
179
 
180
- /** 请假事由(请销假类题目) */
180
+ /** 请假事由(请销假类业务) */
181
181
  export const LEAVE_REASONS = [
182
182
  '个人事务', '探亲', '就医', '婚假', '产假', '陪产假',
183
183
  '年假', '调休', '子女入学', '家中急事', '外出考试', '身体不适',
184
184
  ];
185
185
 
186
- /** 值班事项(值班管理类题目) */
186
+ /** 值班事项(值班管理类业务) */
187
187
  export const DUTY_ITEMS = [
188
188
  '机房巡检完成,设备运行正常', '接待来访人员三名并登记',
189
189
  '处理服务器告警一次,已恢复', '巡查消防通道,无异常',
@@ -258,7 +258,7 @@ const MOJIBAKE_RUN_RE = /[\u00A0-\u024F]{2,}/;
258
258
  * ★ 这里刻意用**黑名单**而不是"白名单放行"。
259
259
  * 旧写法是「不在允许列表里的字符一律报错」,实测会误杀正常中文标点:
260
260
  * 「」『』(U+300C..300F)、×、①、㎡ 都被判成乱码,
261
- * 于是自检一旦接进 seed,`npm run db:seed` 直接失败 —— 这也是它长期没被接线的真实原因。
261
+ * 于是自检一旦接进 seed,`npx tsx prisma/seed.ts` 直接失败 —— 这也是它长期没被接线的真实原因。
262
262
  * 黑名单只拦"肯定是错的"那一类,不会因为作者用了书名号就把整库数据判死。
263
263
  *
264
264
  * 2600–27BF(★☆✓ 这类常用符号)故意不拦:中文文案里出现它们是正常的。
@@ -276,7 +276,7 @@ export interface MojibakeHit {
276
276
  * 扫描所有指定的表,找出乱码字段。
277
277
  *
278
278
  * 用法:seed 末尾调用 `await assertNoMojibake(prisma)`,有乱码直接抛错,
279
- * 让 `npm run db:seed` 失败 —— 问题在构建期暴露,而不是等评委点开列表。
279
+ * 让 `npx tsx prisma/seed.ts` 失败 —— 问题在构建期暴露,而不是等用户点开列表。
280
280
  */
281
281
  export async function findMojibake(
282
282
  // 参数故意用 unknown:调用方传的是 PrismaClient,具体类型随 schema 变化,
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * 系统配置校验 —— 不配置就构建失败。
3
3
  *
4
- * 为什么要有这个:光靠提示词"提醒 AI 改"是靠不住的,AI 会忘、会跳步、
4
+ * 为什么要有这个:只靠口头提醒 AI 改并不可靠,AI 会忘、会跳步、
5
5
  * 会在别的地方写一堆演示文案。这里在构建前硬检查,
6
- * 让"交上去还是个演示系统"变得不可能。
6
+ * 让"上线后还是个演示系统"变得不可能。
7
7
  *
8
8
  * 检查五类:
9
9
  * 系统身份:APP_NAME / APP_DESC / SHOW_DEMO_ACCOUNT_HINT / HOME_STATS
@@ -17,14 +17,14 @@
17
17
  * 功能权限只决定"能不能进页面",它才决定"进去能看哪些行"
18
18
  * 关系覆盖:视图里同时出现的两个本体,如果 schema 里有外键/桥接表连接,
19
19
  * 图上却没有任何路径,直接报错 —— 专治"考试和人员都在图里、却没配关系"。
20
- * 全局漏配(本体不在视图里)只提示不报错,见 npm run relations。
20
+ * 全局漏配(本体不在视图里)只提示不报错,见 node scripts/suggest-relations.mjs。
21
21
  *
22
22
  * 为什么连引用闭合也要查:图谱是"配置驱动"的,ontology.ts 里写错一个 id
23
23
  * (视图引用了不存在的本体、root 不在视图里、关系两端拼错)不会报编译错误,
24
24
  * 只会在打开 /graph 时变成红色「图谱加载失败」,而 tsc 完全看不出来。
25
25
  *
26
26
  * 用法:
27
- * npm run check:config 手动检查
27
+ * node scripts/check-config.mjs 手动检查
28
28
  * npm run build 自动检查(prebuild 钩子)
29
29
  */
30
30
  import { readFileSync, readdirSync } from 'node:fs';
@@ -66,7 +66,7 @@ const appName = grab(appSrc, 'APP_NAME');
66
66
  if (!appName || appName === "'业务管理系统'") {
67
67
  errors.push(
68
68
  'APP_NAME 还是项目默认值「业务管理系统」。' + NL +
69
- ' → 改成题目里的系统名,例如 \'车辆派遣管理系统\''
69
+ ' → 改成需求里的系统名,例如 \'车辆派遣管理系统\''
70
70
  );
71
71
  }
72
72
 
@@ -97,7 +97,7 @@ const demoHint = grab(appSrc, 'SHOW_DEMO_ACCOUNT_HINT');
97
97
  if (demoHint === 'true') {
98
98
  errors.push(
99
99
  'SHOW_DEMO_ACCOUNT_HINT 还是 true,登录页会显示「演示账号:admin / 123456」。' + NL +
100
- ' → 改成 false。这是评委一眼认出"这是演示系统"的最强信号'
100
+ ' → 改成 false。这是用户一眼认出"这是演示系统"的最强信号'
101
101
  );
102
102
  }
103
103
 
@@ -172,8 +172,8 @@ if (ontoSrc) {
172
172
 
173
173
  if (isDefault) {
174
174
  errors.push(
175
- 'ontology.ts 还是基础项目默认的「部门-员工-角色-日志」本体,跟题目无关 —— 知识图谱会显示一堆跟业务不相关的东西。' + NL +
176
- ' → 改成题目的业务本体,例如车辆派遣:部门 / 车辆 / 驾驶员 / 派遣申请' + NL +
175
+ 'ontology.ts 还是基础项目默认的「部门-员工-角色-日志」本体,跟需求无关 —— 知识图谱会显示一堆跟业务不相关的东西。' + NL +
176
+ ' → 改成需求的业务本体,例如车辆派遣:部门 / 车辆 / 驾驶员 / 派遣申请' + NL +
177
177
  ' → 每个 entity 都要给 layer: 0/1/2(数值小的在左)'
178
178
  );
179
179
  }
@@ -267,7 +267,7 @@ if (ontoSrc) {
267
267
  /**
268
268
  * 部门管理是硬性要求。
269
269
  *
270
- * 为什么不在提示词里说一说就完事:AI 生成"系统管理"时,
270
+ * 为什么不能只做说明:AI 生成"系统管理"时,
271
271
  * 十次有八次只做用户 + 角色,把部门漏掉 —— 因为用户管理里有个部门下拉,
272
272
  * 看起来"已经覆盖了"。实际上没有部门维护入口,组织结构就是死的。
273
273
  * 这里直接查文件存在性,漏了构建就失败。
@@ -341,7 +341,7 @@ if (featuresSrc === null) {
341
341
  /**
342
342
  * 工具层是"AI 不只是问答"的唯一实现路径。
343
343
  *
344
- * 为什么用文件检查而不是只写在提示词里:
344
+ * 为什么用文件检查而不是只做说明:
345
345
  * 后续 AI 改业务时很容易觉得"快照够用了",把工具循环删掉;
346
346
  * 或者嫌确认卡麻烦,让对话接口直接写库。那一瞬间系统就退回成套壳问答。
347
347
  * 这里只查存在性与关键调用,不限制业务 —— 工具本来就是按 ontology 自动生成的,
@@ -390,7 +390,7 @@ if (panelSrc && !/\/api\/ai\/act/.test(panelSrc)) {
390
390
  /**
391
391
  * 视图内的"该连未连"是硬错误:
392
392
  * 两个本体都摆进图里了,schema 也有外键或桥接表,图上却连不起来 ——
393
- * 评委一眼就能看出"考试和人员没关系?不对吧"。
393
+ * 用户一眼就能看出"考试和人员没关系?不对吧"。
394
394
  * 允许两跳可达(A—C—B),因为那至少是连通的;完全不通才报错。
395
395
  */
396
396
  const schemaForRel = readOrNull(SCHEMA);
@@ -419,7 +419,7 @@ if (ontoRaw && schemaForRel) {
419
419
  if (reported.has(key) || reported.has(`${m.bEntity}|${m.aEntity}|${m.kind}|${m.bridgeModel ?? ''}`)) continue;
420
420
  const a = rel.entities.find((e) => e.id === m.aEntity)?.label ?? m.aEntity;
421
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 看建议片段`);
422
+ warnings.push(`${a} ↔ ${b}(${describe(m)})未在 relations 里声明 —— 跑 node scripts/suggest-relations.mjs 看建议片段`);
423
423
  }
424
424
  }
425
425
 
@@ -699,7 +699,7 @@ if (!badgesSrc || !badgeCountSrc) {
699
699
  * `lib/appearance.ts`(token)、`lib/skin-theme.ts`(antd 主题)、
700
700
  * `components/appearance/*`(面板与状态)三块撑起来。
701
701
  *
702
- * 为什么放在硬检查里:换题目后 AI 重写业务页面完全正常,但很容易顺手把
702
+ * 为什么放在硬检查里:换需求后 AI 重写业务页面完全正常,但很容易顺手把
703
703
  * 「看起来没用」的外观面板、顶栏入口、根布局里的样式注入删掉 ——
704
704
  * 删了不会报编译错误,只会让"外观设置"这个功能凭空消失,
705
705
  * 而且换皮肤后页面还在用写死的 #fff / 13px,看起来像做了一半。
@@ -752,9 +752,9 @@ if (layoutSrc && !/buildAppearanceCss/.test(layoutSrc)) {
752
752
  /* ============================ 4. 输出 ============================ */
753
753
 
754
754
  if (errors.length) {
755
- console.error('\n✗ 系统还没按题目配置(构建已中止)\n');
755
+ console.error('\n✗ 系统还没按需求配置(构建已中止)\n');
756
756
  errors.forEach((e, i) => console.error(` ${i + 1}) ${e}\n`));
757
- console.error(' 改完再跑一次:npm run check:config');
757
+ console.error(' 改完再跑一次:node scripts/check-config.mjs');
758
758
  console.error(` 涉及文件:${APPINFO} / ${ONTOLOGY} / ${SCHEMA} / ${DEPT_PAGE} / ${SEED} / ${FEATURES_FILE}\n`);
759
759
  if (warnings.length) {
760
760
  console.error(' 另外还有几条"可能漏配"的提示(不阻塞构建):');
@@ -767,7 +767,7 @@ if (errors.length) {
767
767
  if (warnings.length) {
768
768
  console.log('\n提示:还有几条配置不完整(不影响构建,但会缺能力):');
769
769
  warnings.forEach((w) => console.log(` · ${w}`));
770
- console.log(' 关系类问题跑 npm run relations 看完整报告与可粘贴片段\n');
770
+ console.log(' 关系类问题跑 node scripts/suggest-relations.mjs 看完整报告与可粘贴片段\n');
771
771
  }
772
772
 
773
773
  console.log('✓ 系统配置检查通过(系统身份 + 知识图谱本体 + 部门管理 + AI 工具层 + 全局搜索 + 数据权限 + 视图内关系覆盖 + 侧边栏徽标 + 外观底座)');
@@ -3,8 +3,8 @@
3
3
  * 关系挖掘器 —— 把 schema 里"物理存在但 ontology 没写"的关系找出来。
4
4
  *
5
5
  * 用法:
6
- * npm run relations 列出候选关系、漏配项、桥接表,并给出可粘贴的 TS 片段
7
- * npm run relations -- --strict 只要有"视图内该连未连"就退出码 1(CI 用)
6
+ * node scripts/suggest-relations.mjs 列出候选关系、漏配项、桥接表,并给出可粘贴的 TS 片段
7
+ * node scripts/suggest-relations.mjs -- --strict 只要有"视图内该连未连"就退出码 1(CI 用)
8
8
  *
9
9
  * 为什么需要它:
10
10
  * 图谱关系是手写的,人只会写"想得到的"。最典型的漏配是
@@ -82,7 +82,7 @@ if (indirectNote.length) {
82
82
  console.log('── 只能间接到达(两跳内可连通,不报错)──');
83
83
  for (const n of indirectNote) {
84
84
  console.log(` · 视图「${n.view}」:${n.a} ↔ ${n.b} 隔着 1 个节点,${n.hops} 跳可达`);
85
- console.log(` 如果评委要想看它们的关系,建议补一条 via.model 直连`);
85
+ console.log(` 如果要看它们的关系,建议补一条 via.model 直连`);
86
86
  }
87
87
  console.log('');
88
88
  }
@@ -24,7 +24,7 @@ const { RangePicker } = DatePicker;
24
24
  * 功能没变:左边生成报告,右边一个统一助手(数据问答 + 使用答疑)。
25
25
  */
26
26
 
27
- /** 助手空白态的引导问题。接业务时换成题目里的真实问题(数据类 / 使用类 / 办事类各一条) */
27
+ /** 助手空白态的引导问题。接业务时换成需求里的真实问题(数据类 / 使用类 / 办事类各一条) */
28
28
  const ASK_SAMPLES = [
29
29
  '这个月新增了哪些数据?分别是什么类型',
30
30
  '当前数据里有哪些明显的问题或风险?',
@@ -139,10 +139,7 @@ export default function DashboardPage() {
139
139
  );
140
140
 
141
141
  const cards = useMemo(() => {
142
- // ★ 未配置(APP_NAME 还是默认值)时走脚手架自带的 5 张卡,保留下钻链接,
143
- // 保证"刚解压就能演示";一旦按题目配过,就完全由 appInfo.ts 的 HOME_STATS 决定。
144
- // 以前这里写死卡片、HOME_STATS 只是摆设 —— 提示词让 AI 改 appInfo,
145
- // 改完首页却一点没变,check:config 还照样通过,等于白改。
142
+ // ★ 未配置时展示默认卡片;配置后完全由 appInfo.ts 的 HOME_STATS 决定。
146
143
  if (!IS_CONFIGURED) {
147
144
  return [
148
145
  { title: '用户总数', value: data?.userCount, icon: <TeamOutlined />, color: palette[0], href: '/system/users', suffix: undefined },
@@ -191,7 +188,7 @@ export default function DashboardPage() {
191
188
  </div>
192
189
  <div style={{ fontSize: fs(12), color: MILITARY.textMuted }}>统计口径:当前数据库全量数据</div>
193
190
  </div>
194
- {/* ★ 未配置警告:APP_NAME 还是默认值,说明还没按题目配置。
191
+ {/* ★ 未配置警告:APP_NAME 还是默认值,说明还没按需求配置。
195
192
  目的是让"忘了改"不可能被忽略。改完 appInfo.ts 后自动消失。 */}
196
193
  {!IS_CONFIGURED && (
197
194
  <Alert
@@ -202,11 +199,11 @@ export default function DashboardPage() {
202
199
  description={
203
200
  <div>
204
201
  修改 <Typography.Text code>src/lib/appInfo.ts</Typography.Text>:
205
- 把 <Typography.Text code>APP_NAME</Typography.Text> 改成题目里的系统名、
202
+ 把 <Typography.Text code>APP_NAME</Typography.Text> 改成需求里的系统名、
206
203
  <Typography.Text code>HOME_STATS</Typography.Text> 换成业务指标、
207
204
  把本页图表换成业务相关的内容、
208
205
  把 <Typography.Text code>SHOW_DEMO_ACCOUNT_HINT</Typography.Text> 设为 false。
209
- 然后运行 <Typography.Text code>npm run check:config</Typography.Text> 校验。
206
+ 然后运行 <Typography.Text code>node scripts/check-config.mjs</Typography.Text> 校验。
210
207
  </div>
211
208
  }
212
209
  />
@@ -25,7 +25,7 @@ const { Paragraph, Text, Title } = Typography;
25
25
  /**
26
26
  * 「等待与反馈」样板间。
27
27
  *
28
- * 这个页面不解决业务问题,它解决的是**比赛现场的一个固定问题**:
28
+ * 这个页面不解决业务问题,它解决的是**演示现场的一个固定问题**:
29
29
  * 出的题不确定,但"点了一下要等几秒"这件事一定会出现。
30
30
  * 所以把各种等待形态集中在这里,新题来了照抄对应的一块就行。
31
31
  *
@@ -13,7 +13,7 @@ import { fs, MILITARY } from '@/lib/theme';
13
13
  * 明确说清"没打开"、给出可能的两个原因、并且**保留重试按钮**。
14
14
  *
15
15
  * 注意:错误信息做脱敏 —— 生产环境只给摘要(digest),
16
- * 开发环境才显示原始 message,避免把堆栈/路径泄漏给评委看到。
16
+ * 开发环境才显示原始 message,避免把堆栈/路径泄漏给用户看到。
17
17
  */
18
18
  export default function AdminError({
19
19
  error,
@@ -24,7 +24,7 @@ import { formRules } from '@/lib/formRules';
24
24
  * 问题反馈页(骨架内置)。
25
25
  *
26
26
  * ★ 为什么必须有这个模块:
27
- * 比赛演示时评委最常问的一句是「如果有问题怎么反馈」。
27
+ * 演示时用户最常问的一句是「如果有问题怎么反馈」。
28
28
  * 有一个能提交、能跟踪状态、管理员能回复的反馈闭环,
29
29
  * 比任何功能都更能体现"这是个能上线的系统"。
30
30
  *
@@ -19,7 +19,7 @@ import { fs, MILITARY } from '@/lib/theme';
19
19
  * 为什么它不能省:
20
20
  * 用户、员工、以及绝大多数业务单据都要挂部门。
21
21
  * 只做用户管理不做部门管理,等于把"组织结构"这个地基留空,
22
- * 评委只需要问一句"部门在哪维护"就露馅。
22
+ * 用户只需要问一句"部门在哪维护"就露馅。
23
23
  *
24
24
  * 实现要点:
25
25
  * - 树形表格(不是分页列表):组织结构必须能一眼看出层级
@@ -37,7 +37,7 @@ export const POST = withRoute(async (req) => {
37
37
  });
38
38
  if (!ctx.snapshot.length) {
39
39
  throw new BizError(
40
- '库里没有可查询的业务数据。请先补充演示数据(npm run db:seed),或在 ontology.ts 里声明业务表。',
40
+ '库里没有可查询的业务数据。请先补充演示数据(npx tsx prisma/seed.ts),或在 ontology.ts 里声明业务表。',
41
41
  CODE.BAD_REQUEST
42
42
  );
43
43
  }
@@ -386,7 +386,6 @@ export async function POST(req: Request) {
386
386
  });
387
387
  }
388
388
 
389
- /** 供调试:返回助手此刻看到的系统提示词(不含敏感数据以外的内容) */
390
389
  export async function GET() {
391
390
  const user = await currentUser();
392
391
  if (!user) return Response.json({ error: '请先登录' }, { status: 401 });
@@ -7,7 +7,7 @@ import { aiChat, cleanAiText } from '@/lib/ai';
7
7
  * 「数据 → 报告」生成器 —— 这是整套骨架里最有差异化的一块。
8
8
  *
9
9
  * 别的队伍在用大模型写代码,你把**大模型用进了产品里**。
10
- * 评委打开系统看到一个「一键生成报告」,印象分直接拉开差距。
10
+ * 用户打开系统看到一个「一键生成报告」,印象分直接拉开差距。
11
11
  *
12
12
  * 用法(模型接业务时只要调这一个接口):
13
13
  * POST /api/ai/report
@@ -15,7 +15,7 @@ export const dynamic = 'force-dynamic';
15
15
  * 为什么不建 SSE / WebSocket 推送:
16
16
  * 待办数字的变化频率是以"分钟"计的,不是聊天消息。60 秒轮询 + 回到前台刷新,
17
17
  * 代价是一个几乎不耗资源的 count 查询,换来的是"断线、多标签页、休眠唤醒"
18
- * 全都不用额外处理。这套骨架的定位是比赛演示与内网部署,简单可靠优先。
18
+ * 全都不用额外处理。这套骨架的定位是业务演示与内网部署,简单可靠优先。
19
19
  *
20
20
  * 为什么 path 而不是 menuId:菜单重跑 seed 会换 id,path 稳定;
21
21
  * 而且前端菜单树里天然有 path,不用为徽标额外传一份菜单 id 映射。
@@ -14,7 +14,7 @@ const ALLOWED = ['.doc', '.docx', '.xls', '.xlsx', '.pdf', '.txt', '.md', '.csv'
14
14
  /**
15
15
  * POST /api/docs/parse —— 上传并解析文档。
16
16
  *
17
- * 这是给评委看的核心亮点,所以策略是「宁可降级也不报错」:
17
+ * 这是给用户看的核心亮点,所以策略是「宁可降级也不报错」:
18
18
  * LibreOffice 缺失、pdf-parse 缺失都只影响对应格式的深度解析,
19
19
  * 接口本身永远返回结构化结果 + 明确的提示文本。
20
20
  */
@@ -14,7 +14,7 @@ export const dynamic = 'force-dynamic';
14
14
  * ★ 这个接口**永远不返回 404 / 500**:
15
15
  * 反馈不存在、模型不可用、业务规则冲突,全部转成
16
16
  * `code: 0 + outcome` 的结构化结果,由前端在弹窗里展示分析与提示。
17
- * 反馈页因此不会出现红色报错弹窗,评委点任何一条都能得到回应。
17
+ * 反馈页因此不会出现红色报错弹窗,用户点任何一条都能得到回应。
18
18
  */
19
19
  export const POST = withRoute(async (req: NextRequest) => {
20
20
  const user = await currentUser();
@@ -10,7 +10,7 @@ import { prisma } from '@/lib/prisma';
10
10
  * - JWT 会话(非数据库会话):内网单机、无横向扩容需求,JWT 省一次库往返,
11
11
  * 刷新页面也不掉登录态。
12
12
  * - 权限集合(permCodes)在登录时一次算好塞进 JWT —— 之后每次判权限不用再查库。
13
- * 代价是改权限要重新登录才生效;演示/比赛场景完全够用。
13
+ * 代价是改权限要重新登录才生效;演示场景完全够用。
14
14
  * - 密码一律 bcrypt 比对,绝不存明文。
15
15
  */
16
16
 
@@ -22,7 +22,7 @@ import { fs, MILITARY } from '@/lib/theme';
22
22
  * 深军绿封面 + 统计条 + 白纸正文 + 底部操作栏。
23
23
  *
24
24
  * 为什么不用普通 Modal 直接塞一段文字:
25
- * 这是比赛演示的亮点功能,评委第一眼看到的是排版。
25
+ * 这是演示的亮点功能,用户第一眼看到的是排版。
26
26
  * 封面、统计卡、纸张层次能把「AI 真的读了数据」这件事讲清楚。
27
27
  */
28
28
 
@@ -9,7 +9,7 @@ import { headingId } from '@/lib/docs-shared';
9
9
  * 轻量 Markdown 渲染器(无第三方依赖)。
10
10
  *
11
11
  * 为什么自己写:
12
- * 比赛环境是离线内网,装不了 react-markdown / marked;
12
+ * 运行环境是离线内网,装不了 react-markdown / marked;
13
13
  * 而模型的回答天然是 Markdown,直接当纯文本显示会看到一堆 ** 和 | 表格。
14
14
  *
15
15
  * 支持:标题 / 段落 / 有序无序列表 / 代码块 / 行内代码 / 粗斜体 / 删除线 /
@@ -9,7 +9,7 @@ import { fs } from '@/lib/theme';
9
9
  /**
10
10
  * 会自己交代进度的按钮。
11
11
  *
12
- * 比赛里最常写的一行代码是:
12
+ * 业务页面里最常写的一行代码是:
13
13
  * <Button onClick={async () => { await axios.post(...) }}>提交</Button>
14
14
  * 它有三个隐患:不防重复点击、慢的时候没有任何反馈、失败只报 console。
15
15
  *
@@ -1,6 +1,6 @@
1
1
  # 反馈底座(feedback)
2
2
 
3
- 比赛每道新题都要重做一遍页面,但"用户点了之后有没有反应"这件事是**通用**的。
3
+ 每做一个新模块都要重做一遍页面,但"用户点了之后有没有反应"这件事是**通用**的。
4
4
  这个目录把它做成了一层能力:换题时不用重新设计动效,照着下面的样板接就行。
5
5
 
6
6
  ## 两条铁律
@@ -1,25 +1,6 @@
1
- /**
2
- * 本地大模型客户端(服务端专用)。
3
- *
4
- * 为什么要有这个:比赛本身就是"用 AI 造系统",
5
- * 如果造出来的系统里**没有真正的 AI 功能**,就和其他队伍站在同一起跑线上了。
6
- * 把"调本地模型"这件事在骨架里封好,模型只需要把业务数据拼进 prompt。
7
- *
8
- * 约定:
9
- * - 地址/模型名走环境变量,换机器不用改代码
10
- * - 只在服务端调用(API Key 不能进浏览器)
11
- * - 统一超时 + 统一错误 → 面向用户的中文提示
12
- */
1
+ /** 本地大模型客户端(服务端专用)。 */
13
2
 
14
- /**
15
- * 配置来源:`AI_*` 优先,`OPENAI_*` 兜底。
16
- *
17
- * ★ 为什么两套都认:提示词里让 AI 去 `.env` 找 `OPENAI_BASE_URL / OPENAI_API_KEY / OPENAI_MODEL`
18
- * (这是业界最常见的叫法),而骨架一开始只读 `AI_*`。两边对不上时,用户按提示词
19
- * 填了 `OPENAI_*`,`/api/ai/chat` 却仍走默认地址 —— 表现为"配了也没用",且没有任何报错。
20
- * `openaiCompat.ts` 早就是两套都认的写法,这里补一致,避免同一个模型有两个入口
21
- * 却有两条不同的配置路径。
22
- */
3
+ /** 配置来源:`AI_*` 优先,`OPENAI_*` 兜底。 */
23
4
  const pickEnv = (...names: string[]): string | undefined => {
24
5
  for (const n of names) {
25
6
  const v = process.env[n];
@@ -7,7 +7,7 @@ import { APP_NAME } from '@/lib/appInfo';
7
7
  *
8
8
  * 为什么要有这个文件:
9
9
  * 1. 模型不知道"现在"是几号 —— 问"这个月有几条申请"它只能瞎猜。
10
- * 这里把北京时间、"本月/上月"的具体区间一起塞进 system prompt。
10
+ * 这里把北京时间、"本月/上月"的具体区间一起加入模型上下文。
11
11
  * 2. 一个对话框要同时回答"系统怎么用"和"库里有多少条",
12
12
  * 就必须每次都把业务数据快照带上,而不是分两个接口。
13
13
  * 3. /api/ai/chat(流式)和 /api/ai/ask(非流式)共用同一套上下文,避免两边漂移。
@@ -29,10 +29,10 @@ const BLOCKED = new Set(['User', 'Role', 'Menu', 'UserRole', 'RoleMenu']);
29
29
  export const ROWS_PER_TABLE = 6;
30
30
  /** 单个字段值最长多少字符 */
31
31
  const MAX_FIELD_LEN = 40;
32
- /** 快照总字符上限(防止 prompt 爆掉) */
32
+ /** 快照总字符上限 */
33
33
  const MAX_SNAPSHOT_CHARS = 7000;
34
34
 
35
- /** 北京时间固定 UTC+8(比赛机器在国内,不依赖服务器时区设置) */
35
+ /** 北京时间固定 UTC+8(部署机器在国内,不依赖服务器时区设置) */
36
36
  const TZ_OFFSET_MS = 8 * 3600_000;
37
37
  const WEEKDAYS = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
38
38
 
@@ -278,7 +278,7 @@ export interface AssistantOptions {
278
278
  }
279
279
 
280
280
  /**
281
- * 组装统一助手的 system prompt。
281
+ * 组装统一助手上下文。
282
282
  *
283
283
  * extra 是调用方追加的角色说明(会被截断,防止把上下文挤爆)。
284
284
  * withData=false 时只要时间不要数据(比如纯使用答疑)。
@@ -9,7 +9,7 @@ import { ZodError } from 'zod';
9
9
  * { code: 400xx, data: null, message: '...' } 失败
10
10
  *
11
11
  * 这样做的直接好处:任何接口忘记 try/catch 都不会把 500 堆栈泄漏给前端,
12
- * 评委点到的永远是干净的提示语,而不是一屏红字。
12
+ * 用户点到的永远是干净的提示语,而不是一屏红字。
13
13
  */
14
14
  export type ApiOk<T> = { code: 0; data: T; message: string };
15
15
  export type ApiErr = { code: number; data: null; message: string };