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.
Files changed (229) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +35 -2
  3. package/lib/sync.js +121 -0
  4. package/package.json +14 -4
  5. package/resources/.cache/ref.b64 +1 -0
  6. package/resources/README.md +97 -0
  7. package/resources/_.env +6 -0
  8. package/resources/_.env.example +25 -0
  9. package/resources/_.gitignore +18 -0
  10. package/resources/_.lintstagedrc.json +9 -0
  11. package/resources/_.npmrc +5 -0
  12. package/resources/_.prettierrc +7 -0
  13. package/resources/commitlint.config.mjs +1 -0
  14. package/resources/content/docs/README.md +63 -0
  15. package/resources/content/docs/faq.md +107 -0
  16. package/resources/content/docs/features/_meta.json +5 -0
  17. package/resources/content/docs/features/ai.md +71 -0
  18. package/resources/content/docs/features/appearance.md +47 -0
  19. package/resources/content/docs/features/feedback.md +79 -0
  20. package/resources/content/docs/features/files.md +74 -0
  21. package/resources/content/docs/features/graph.md +66 -0
  22. package/resources/content/docs/features/inbox.md +61 -0
  23. package/resources/content/docs/guide/_meta.json +5 -0
  24. package/resources/content/docs/guide/account.md +76 -0
  25. package/resources/content/docs/guide/data.md +87 -0
  26. package/resources/content/docs/guide/navigation.md +81 -0
  27. package/resources/content/docs/guide/permissions.md +67 -0
  28. package/resources/content/docs/guide/system.md +92 -0
  29. package/resources/content/docs/quick-start.md +70 -0
  30. package/resources/content/docs/shortcuts.md +69 -0
  31. package/resources/eslint.config.mjs +17 -0
  32. package/resources/next-env.d.ts +6 -0
  33. package/resources/next.config.ts +13 -0
  34. package/resources/postcss.config.mjs +7 -0
  35. package/resources/prisma/org.config.ts +98 -0
  36. package/resources/prisma/schema.prisma +336 -0
  37. package/resources/prisma/seed.ts +721 -0
  38. package/resources/prisma/seedSafe.ts +347 -0
  39. package/resources/prisma.config.ts +14 -0
  40. package/resources/scripts/check-config.mjs +773 -0
  41. package/resources/scripts/lib/schema-graph.mjs +410 -0
  42. package/resources/scripts/patch-next-trace.mjs +86 -0
  43. package/resources/scripts/suggest-relations.mjs +105 -0
  44. package/resources/src/app/(admin)/ai/page.tsx +469 -0
  45. package/resources/src/app/(admin)/dashboard/page.tsx +260 -0
  46. package/resources/src/app/(admin)/demo/employees/page.tsx +508 -0
  47. package/resources/src/app/(admin)/demo/files/page.tsx +451 -0
  48. package/resources/src/app/(admin)/demo/loading/page.tsx +513 -0
  49. package/resources/src/app/(admin)/error.tsx +96 -0
  50. package/resources/src/app/(admin)/feedback/page.tsx +573 -0
  51. package/resources/src/app/(admin)/graph/page.tsx +1224 -0
  52. package/resources/src/app/(admin)/inbox/page.tsx +31 -0
  53. package/resources/src/app/(admin)/layout.tsx +58 -0
  54. package/resources/src/app/(admin)/loading.tsx +31 -0
  55. package/resources/src/app/(admin)/system/depts/page.tsx +415 -0
  56. package/resources/src/app/(admin)/system/logs/page.tsx +215 -0
  57. package/resources/src/app/(admin)/system/menus/page.tsx +403 -0
  58. package/resources/src/app/(admin)/system/roles/page.tsx +433 -0
  59. package/resources/src/app/(admin)/system/users/page.tsx +620 -0
  60. package/resources/src/app/api/account-recovery/route.ts +132 -0
  61. package/resources/src/app/api/ai/act/route.ts +70 -0
  62. package/resources/src/app/api/ai/ask/route.ts +82 -0
  63. package/resources/src/app/api/ai/chat/route.ts +404 -0
  64. package/resources/src/app/api/ai/report/route.ts +164 -0
  65. package/resources/src/app/api/auth/[...nextauth]/route.ts +3 -0
  66. package/resources/src/app/api/auth/change-password/route.ts +57 -0
  67. package/resources/src/app/api/auth/forgot/route.ts +98 -0
  68. package/resources/src/app/api/auth/register/route.ts +95 -0
  69. package/resources/src/app/api/auth/register-config/route.ts +51 -0
  70. package/resources/src/app/api/badges/route.ts +39 -0
  71. package/resources/src/app/api/depts/[id]/route.ts +145 -0
  72. package/resources/src/app/api/depts/route.ts +126 -0
  73. package/resources/src/app/api/docs/export/route.ts +87 -0
  74. package/resources/src/app/api/docs/parse/route.ts +57 -0
  75. package/resources/src/app/api/docs/search/route.ts +22 -0
  76. package/resources/src/app/api/employees/[id]/route.ts +147 -0
  77. package/resources/src/app/api/employees/export/route.ts +115 -0
  78. package/resources/src/app/api/employees/import/route.ts +205 -0
  79. package/resources/src/app/api/employees/route.ts +149 -0
  80. package/resources/src/app/api/export/route.ts +254 -0
  81. package/resources/src/app/api/favorites/route.ts +56 -0
  82. package/resources/src/app/api/feedback/ai/route.ts +31 -0
  83. package/resources/src/app/api/feedback/approve/route.ts +67 -0
  84. package/resources/src/app/api/feedback/report/route.ts +213 -0
  85. package/resources/src/app/api/feedback/route.ts +246 -0
  86. package/resources/src/app/api/files/[name]/route.ts +82 -0
  87. package/resources/src/app/api/graph/focus/route.ts +75 -0
  88. package/resources/src/app/api/graph/node/move/route.ts +48 -0
  89. package/resources/src/app/api/graph/node/route.ts +274 -0
  90. package/resources/src/app/api/graph/route.ts +272 -0
  91. package/resources/src/app/api/health/route.ts +11 -0
  92. package/resources/src/app/api/inbox/read/route.ts +36 -0
  93. package/resources/src/app/api/inbox/summary/route.ts +15 -0
  94. package/resources/src/app/api/logs/route.ts +82 -0
  95. package/resources/src/app/api/menus/[id]/route.ts +132 -0
  96. package/resources/src/app/api/menus/mine/route.ts +23 -0
  97. package/resources/src/app/api/menus/route.ts +89 -0
  98. package/resources/src/app/api/roles/[id]/menus/route.ts +73 -0
  99. package/resources/src/app/api/roles/[id]/route.ts +96 -0
  100. package/resources/src/app/api/roles/route.ts +114 -0
  101. package/resources/src/app/api/search/route.ts +22 -0
  102. package/resources/src/app/api/stats/route.ts +92 -0
  103. package/resources/src/app/api/upload/route.ts +38 -0
  104. package/resources/src/app/api/users/[id]/password/route.ts +55 -0
  105. package/resources/src/app/api/users/[id]/route.ts +145 -0
  106. package/resources/src/app/api/users/route.ts +153 -0
  107. package/resources/src/app/api/visits/route.ts +48 -0
  108. package/resources/src/app/change-password/ChangePasswordForm.tsx +174 -0
  109. package/resources/src/app/change-password/page.tsx +19 -0
  110. package/resources/src/app/docs/[[...slug]]/page.tsx +56 -0
  111. package/resources/src/app/docs/layout.tsx +34 -0
  112. package/resources/src/app/docs/loading.tsx +19 -0
  113. package/resources/src/app/docs/not-found.tsx +25 -0
  114. package/resources/src/app/forgot/page.tsx +392 -0
  115. package/resources/src/app/global-error.tsx +75 -0
  116. package/resources/src/app/globals.css +235 -0
  117. package/resources/src/app/layout.tsx +44 -0
  118. package/resources/src/app/login/page.tsx +267 -0
  119. package/resources/src/app/page.tsx +14 -0
  120. package/resources/src/app/register/page.tsx +237 -0
  121. package/resources/src/auth.ts +215 -0
  122. package/resources/src/components/AiPanel.tsx +970 -0
  123. package/resources/src/components/AuthContext.tsx +82 -0
  124. package/resources/src/components/DataTable.tsx +459 -0
  125. package/resources/src/components/EChart.tsx +140 -0
  126. package/resources/src/components/FeedbackAiModal.tsx +476 -0
  127. package/resources/src/components/FeedbackReportModal.tsx +479 -0
  128. package/resources/src/components/Markdown.tsx +532 -0
  129. package/resources/src/components/OntologyGraph.tsx +1196 -0
  130. package/resources/src/components/Providers.tsx +99 -0
  131. package/resources/src/components/VisitTracker.tsx +89 -0
  132. package/resources/src/components/admin/AdminShell.tsx +749 -0
  133. package/resources/src/components/admin/CountBadge.tsx +67 -0
  134. package/resources/src/components/admin/GlobalSearch.tsx +509 -0
  135. package/resources/src/components/admin/InboxBell.tsx +78 -0
  136. package/resources/src/components/admin/VisitStats.tsx +145 -0
  137. package/resources/src/components/admin/menu-icons.tsx +56 -0
  138. package/resources/src/components/ai/AiInputForm.tsx +351 -0
  139. package/resources/src/components/appearance/AppearanceDrawer.tsx +268 -0
  140. package/resources/src/components/appearance/AppearanceProvider.tsx +160 -0
  141. package/resources/src/components/docs/DocArticle.tsx +106 -0
  142. package/resources/src/components/docs/DocToc.tsx +100 -0
  143. package/resources/src/components/docs/DocsChrome.tsx +259 -0
  144. package/resources/src/components/docs/DocsHome.tsx +209 -0
  145. package/resources/src/components/docs/DocsSearch.tsx +365 -0
  146. package/resources/src/components/docs/docs.css +1458 -0
  147. package/resources/src/components/feedback/AsyncButton.tsx +108 -0
  148. package/resources/src/components/feedback/LoadingOverlay.tsx +125 -0
  149. package/resources/src/components/feedback/PressableCard.tsx +92 -0
  150. package/resources/src/components/feedback/README.md +97 -0
  151. package/resources/src/components/feedback/RouteProgress.tsx +108 -0
  152. package/resources/src/components/feedback/Skeleton.tsx +249 -0
  153. package/resources/src/components/feedback/StatCard.tsx +70 -0
  154. package/resources/src/components/feedback/TaskHint.tsx +153 -0
  155. package/resources/src/components/feedback/index.ts +93 -0
  156. package/resources/src/components/feedback/motion.css +204 -0
  157. package/resources/src/components/feedback/motion.ts +63 -0
  158. package/resources/src/components/feedback/useCountUp.ts +110 -0
  159. package/resources/src/components/feedback/usePending.ts +209 -0
  160. package/resources/src/components/feedback/useRowFlash.ts +40 -0
  161. package/resources/src/components/inbox/InboxView.tsx +409 -0
  162. package/resources/src/components/ui/button.tsx +35 -0
  163. package/resources/src/components/ui/card.tsx +37 -0
  164. package/resources/src/components/ui/input.tsx +19 -0
  165. package/resources/src/lib/ai.ts +310 -0
  166. package/resources/src/lib/aiContext.ts +357 -0
  167. package/resources/src/lib/aiForm.test.ts +191 -0
  168. package/resources/src/lib/aiForm.ts +532 -0
  169. package/resources/src/lib/aiFormTypes.ts +83 -0
  170. package/resources/src/lib/aiToken.ts +72 -0
  171. package/resources/src/lib/aiTools.ts +823 -0
  172. package/resources/src/lib/api.ts +160 -0
  173. package/resources/src/lib/appInfo.ts +128 -0
  174. package/resources/src/lib/appearance.ts +643 -0
  175. package/resources/src/lib/badge-count.ts +146 -0
  176. package/resources/src/lib/badges.test.ts +58 -0
  177. package/resources/src/lib/badges.ts +79 -0
  178. package/resources/src/lib/brand-mark.test.ts +87 -0
  179. package/resources/src/lib/brand-mark.ts +45 -0
  180. package/resources/src/lib/docs-shared.test.ts +148 -0
  181. package/resources/src/lib/docs-shared.ts +422 -0
  182. package/resources/src/lib/docs.ts +381 -0
  183. package/resources/src/lib/err-text.ts +21 -0
  184. package/resources/src/lib/excel.ts +152 -0
  185. package/resources/src/lib/favorite-path.ts +60 -0
  186. package/resources/src/lib/favorites.ts +59 -0
  187. package/resources/src/lib/features.ts +69 -0
  188. package/resources/src/lib/feedback-access.ts +17 -0
  189. package/resources/src/lib/feedbackAi.ts +1621 -0
  190. package/resources/src/lib/formRules.ts +76 -0
  191. package/resources/src/lib/format.ts +52 -0
  192. package/resources/src/lib/graph-cascade.ts +304 -0
  193. package/resources/src/lib/graph-focus.ts +301 -0
  194. package/resources/src/lib/graph-link.ts +17 -0
  195. package/resources/src/lib/graph-node-write.ts +150 -0
  196. package/resources/src/lib/graph-schema.ts +475 -0
  197. package/resources/src/lib/graph-scope.test.ts +46 -0
  198. package/resources/src/lib/graph-scope.ts +28 -0
  199. package/resources/src/lib/http.ts +157 -0
  200. package/resources/src/lib/inbox-types.ts +57 -0
  201. package/resources/src/lib/inbox.ts +55 -0
  202. package/resources/src/lib/menu-service.ts +204 -0
  203. package/resources/src/lib/notifications.ts +113 -0
  204. package/resources/src/lib/office.ts +531 -0
  205. package/resources/src/lib/ontology.ts +277 -0
  206. package/resources/src/lib/openaiCompat.ts +257 -0
  207. package/resources/src/lib/pending.ts +98 -0
  208. package/resources/src/lib/prisma.ts +30 -0
  209. package/resources/src/lib/rbac.test.ts +106 -0
  210. package/resources/src/lib/rbac.ts +185 -0
  211. package/resources/src/lib/search-data.ts +158 -0
  212. package/resources/src/lib/security-questions.ts +20 -0
  213. package/resources/src/lib/skin-theme.ts +155 -0
  214. package/resources/src/lib/system-config.ts +55 -0
  215. package/resources/src/lib/theme.ts +96 -0
  216. package/resources/src/lib/todos/feedback.ts +86 -0
  217. package/resources/src/lib/todos/index.ts +73 -0
  218. package/resources/src/lib/todos/types.ts +33 -0
  219. package/resources/src/lib/upload.ts +106 -0
  220. package/resources/src/lib/utils.test.ts +9 -0
  221. package/resources/src/lib/utils.ts +6 -0
  222. package/resources/src/lib/validation.test.ts +297 -0
  223. package/resources/src/lib/validation.ts +658 -0
  224. package/resources/src/lib/visit-stats.ts +109 -0
  225. package/resources/src/lib/visits.ts +111 -0
  226. package/resources/src/lib/zodField.ts +44 -0
  227. package/resources/tsconfig.json +41 -0
  228. package/resources/vitest.config.ts +16 -0
  229. package/resources/vitest.setup.ts +1 -0
@@ -0,0 +1,277 @@
1
+ /**
2
+ * 本体论(Ontology)配置 —— 知识图谱的"通用引擎"。
3
+ *
4
+ * ★ 这是整套骨架里第二个差异化亮点(第一个是 AI 能力)。
5
+ * 评委打开看到一个能点的知识图谱,基本就记住你了。
6
+ *
7
+ * 设计思路:
8
+ * 图谱画什么,由这个文件里的声明式配置决定,跟代码完全解耦。
9
+ * 所以它**对任何业务系统都通用** —— 接自己的业务时只改这个文件。
10
+ *
11
+ * 三类本体:
12
+ * - entities 本体(节点类型):车辆、司机、部门、用车申请……
13
+ * - relations 关系(边类型):指派、所属、提交、审批……
14
+ * - views 视图:把哪些本体和关系组成一张图
15
+ *
16
+ * ★ views[0] 就是 /graph 默认打开的视图。页面会自动读取 views 并渲染视图下拉,
17
+ * **不需要改 graph/page.tsx** —— 换业务时只改这个文件就够了。
18
+ * views 里引用的 entities / relations / root 必须都是已声明的 id,
19
+ * `npm run check:config` 会在 1 秒内把这些引用错误报出来。
20
+ *
21
+ * 换系统的例子(请销假):
22
+ * entities: Person(人) / LeaveRequest(请假申请) / LeavePlan(休假计划) / LeaveChange(变更申请)
23
+ * relations: 提交(LeaveRequest→Person) / 依据(LeaveRequest→LeavePlan) / 变更(LeaveChange→LeaveRequest)
24
+ */
25
+
26
+ export interface EntityDef {
27
+ /** 本体 id,全图唯一 */
28
+ id: string;
29
+ /** 显示名,比如"车辆" */
30
+ label: string;
31
+ /** Prisma 模型名,节点数据从这里查 */
32
+ model: string;
33
+ /** 节点上显示哪个字段(一般是名称/编号) */
34
+ titleField: string;
35
+ /** 节点上附带显示的字段(最多 2 个,超出会挤) */
36
+ subtitleFields?: string[];
37
+ /**
38
+ * 节点填充色。**必须用浅色**(pastel),文字才看得清。
39
+ * 深色填充 + 深色文字是踩过的坑:节点上的字完全读不出来。
40
+ */
41
+ color: string;
42
+ /** 节点边框色(同色系深一档,视觉更精致) */
43
+ borderColor?: string;
44
+ /**
45
+ * 分层序号:图谱按这个值从左到右排列。
46
+ * 小的在左(如部门),大的在右(如申请/日志)。不填则按数组顺序。
47
+ */
48
+ layer?: number;
49
+ /** 图标名(antd icon,可选) */
50
+ icon?: string;
51
+ /** 点击节点后跳转到哪个页面,用 :id 占位 */
52
+ link?: string;
53
+ }
54
+
55
+ export interface RelationDef {
56
+ id: string;
57
+ /** 关系显示名,比如"指派" */
58
+ label: string;
59
+ /** 起点本体 id */
60
+ from: string;
61
+ /** 终点本体 id */
62
+ to: string;
63
+ /**
64
+ * 关系怎么取数。三种写法:
65
+ * { field: 'driverId' } 外键字段:起点.id -> 终点.id
66
+ * { field: 'driverId', reverse: true } 反向:终点.id -> 起点.id
67
+ * { model: 'DispatchRecord', fromField, toField } 中间表(多对多)
68
+ */
69
+ via:
70
+ | { field: string; reverse?: boolean }
71
+ | { model: string; fromField: string; toField: string };
72
+ }
73
+
74
+ export interface GraphView {
75
+ id: string;
76
+ label: string;
77
+ /** 这个视图包含哪些本体 */
78
+ entities: string[];
79
+ /** 这个视图包含哪些关系 */
80
+ relations: string[];
81
+ /** 默认从哪个本体开始展开 */
82
+ root: string;
83
+ }
84
+
85
+ /**
86
+ * 节点详情面板的可选配置(不写也能用)。
87
+ *
88
+ * 面板的字段是**从 prisma/schema.prisma 反推**的(字段名/类型/注释 → 控件),
89
+ * 所以换业务时通常什么都不用配。只有两种情况需要在这里补:
90
+ * - schema 字段没写注释、又想要好看的中文标签 → fieldLabels
91
+ * - 字段是 String/Int 存枚举(SQLite 没有 enum)→ fieldOptions
92
+ *
93
+ * ★ 注意:fieldLabels / fieldOptions 放在 ONTOLOGY 顶层,**不要塞进 entity 对象里**——
94
+ * scripts/check-config.mjs 用 `\{[^{}]*\}` 切 entity,嵌套对象会让它解析错位。
95
+ */
96
+ export interface FieldOption {
97
+ label: string;
98
+ value: string | number;
99
+ }
100
+
101
+ export interface OntologyConfig {
102
+ entities: EntityDef[];
103
+ relations: RelationDef[];
104
+ views: GraphView[];
105
+ /** { 本体id 或 模型名: { 字段名: 中文标签 } } */
106
+ fieldLabels?: Record<string, Record<string, string>>;
107
+ /** { 本体id 或 模型名: { 字段名: 选项[] } } */
108
+ fieldOptions?: Record<string, Record<string, FieldOption[]>>;
109
+ }
110
+
111
+ /* ==================== 通用能力:业务表 ↔ 知识图谱 ==================== */
112
+
113
+ /**
114
+ * ★ 任意业务列表页都能一键跳进图谱,**图谱页与引擎都不用改**。
115
+ *
116
+ * 1) 列表页放一个按钮(列定义里加一列即可):
117
+ * import { graphHref } from '@/lib/graph-link';
118
+ * <Button href={graphHref('Employee', row.id)}>关联</Button>
119
+ * graphHref 生成 `/graph?src=Employee:<id>`,是纯函数,客户端可直接引。
120
+ *
121
+ * 2) 图谱页读 src → 调 `/api/graph/focus` 解析(src/lib/graph-focus.ts):
122
+ * 模型名 → 本体(entities[].model)
123
+ * → 沿 relations **只往 layer 更小的方向**走,最多 2 跳,不回头
124
+ * 走到 1 个节点 → single:直接定位到该节点并打开详情面板
125
+ * 走到多个节点 → multi :高亮这几个节点之间的连线,其余压暗
126
+ *
127
+ * 3) 反向也能回来:entity.link 里的 `:id` 换成记录主键就是 backLink。
128
+ *
129
+ * 所以接自己的业务时只做三件事:
130
+ * ① prisma 建模型 ② 这里声明 entity + relation(记得给 layer)
131
+ * ③ 列表页加一个 graphHref 按钮
132
+ * 判定 single / multi 完全由本体推导,页面里不要写死"关联谁"。
133
+ */
134
+
135
+ /* ============================ 组织与账号(骨架自带模型) ============================ */
136
+
137
+ export const ONTOLOGY: OntologyConfig = {
138
+ // 默认指向骨架里真实存在的模型,开箱就能看到图。
139
+ // 接自己的业务时换成你的本体即可(注释里有两套完整示例)。
140
+ //
141
+ // ★ 配色原则:**浅色底 + 深色字**。深色底会让节点文字完全读不出来(踩过)。
142
+ entities: [
143
+ {
144
+ id: 'department',
145
+ label: '部门',
146
+ model: 'Department',
147
+ titleField: 'name',
148
+ subtitleFields: [],
149
+ color: '#F2F0E4',
150
+ borderColor: '#D2CBB2',
151
+ layer: 0,
152
+ // link 决定「全局搜索搜到这条数据后点去哪里」:
153
+ // ?id=:id → 页面自己按 id 打开记录(详情弹窗)
154
+ // ?kw=:kw → 列表页按名称过滤(DataTable 已内置支持)
155
+ // 不写 link 的本体不参与数据搜索(搜到了也没地方跳)。
156
+ link: '/system/depts',
157
+ },
158
+ {
159
+ id: 'employee',
160
+ label: '员工',
161
+ model: 'Employee',
162
+ titleField: 'name',
163
+ subtitleFields: ['code', 'position'],
164
+ color: '#E9EFDF',
165
+ borderColor: '#B6C6A1',
166
+ layer: 1,
167
+ link: '/demo/employees?id=:id',
168
+ },
169
+ {
170
+ id: 'role',
171
+ label: '角色',
172
+ model: 'Role',
173
+ titleField: 'name',
174
+ subtitleFields: [],
175
+ color: '#F4EEDF',
176
+ borderColor: '#D8C8A4',
177
+ layer: 2,
178
+ link: '/system/roles?kw=:kw',
179
+ },
180
+ {
181
+ // 账号本身也是"业务记录":一条账号同时挂着部门 + 若干角色,
182
+ // 正好是多实体关联(multi)的通用演示场景。
183
+ id: 'user',
184
+ label: '账号',
185
+ model: 'User',
186
+ titleField: 'username',
187
+ subtitleFields: ['name'],
188
+ color: '#E7EBDB',
189
+ borderColor: '#BFC9AC',
190
+ layer: 3,
191
+ link: '/system/users?kw=:kw',
192
+ },
193
+ {
194
+ id: 'log',
195
+ label: '操作日志',
196
+ model: 'OperationLog',
197
+ titleField: 'module',
198
+ subtitleFields: ['action'],
199
+ color: '#EFEDE6',
200
+ borderColor: '#CFC9BA',
201
+ layer: 3,
202
+ link: '/system/logs?kw=:kw',
203
+ },
204
+ ] as EntityDef[],
205
+
206
+ relations: [
207
+ { id: 'emp_dept', label: '所属部门', from: 'employee', to: 'department', via: { field: 'deptId' } },
208
+ // 部门自引用:parentId → 上级部门。
209
+ // 没有这条,组织树的根节点(比如"总公司")在图上是**孤立的一个方块**:
210
+ // 人只铺在叶子部门,根部门没人指向它,看起来像加载失败。
211
+ { id: 'dept_parent', label: '上级部门', from: 'department', to: 'department', via: { field: 'parentId' } },
212
+ // OperationLog.userId 指向的是账号(User),不是员工(Employee)——别接错模型
213
+ { id: 'log_user', label: '操作人', from: 'log', to: 'user', via: { field: 'userId' } },
214
+ { id: 'user_dept', label: '所属部门', from: 'user', to: 'department', via: { field: 'deptId' } },
215
+ // 中间表写法:UserRole 是桥接表,fromField/toField 指向它自己的两个外键
216
+ {
217
+ id: 'user_role',
218
+ label: '拥有角色',
219
+ from: 'user',
220
+ to: 'role',
221
+ via: { model: 'UserRole', fromField: 'userId', toField: 'roleId' },
222
+ },
223
+ ] as RelationDef[],
224
+
225
+ views: [
226
+ {
227
+ // 默认视图放"部门树 + 人"(4 + 20 = 24 个节点)。
228
+ // ★ 角色不放进这个视图:Employee 上没有 roleId(角色挂在账号上),
229
+ // 放进来就是两三个没有任何边连着的孤立方块 —— 看角色去 access 视图。
230
+ // ★ 不要把 log 放进默认视图:log_user 是 1:N,BFS 会把每个员工的日志全拉进来,
231
+ // 右列一下涨到 20+ 个节点,变成一根柱子(实测踩过)。
232
+ // 需要看日志关联时,让 AI 接业务时自己加第二个视图(views 支持多个)。
233
+ id: 'org',
234
+ label: '组织与人员关系图',
235
+ entities: ['department', 'employee'],
236
+ relations: ['emp_dept', 'dept_parent'],
237
+ root: 'employee',
238
+ },
239
+ {
240
+ // 账号视角:一条账号连着一个部门 + 多个角色。
241
+ // 用户管理页点「关联」走的就是这个视图(single / multi 由 layer 推导)。
242
+ id: 'access',
243
+ label: '账号与权限关系图',
244
+ entities: ['department', 'role', 'user'],
245
+ relations: ['user_dept', 'user_role'],
246
+ root: 'user',
247
+ },
248
+ ] as GraphView[],
249
+
250
+ // 节点详情面板的可选配置(绝大多数情况不用写:字段是从 schema 注释反推的)
251
+ // fieldLabels: { employee: { jobNo: '工号', position: '职位' } },
252
+ // fieldOptions: { employee: { gender: [{ label: '男', value: 'male' }, { label: '女', value: 'female' }] } },
253
+ };
254
+
255
+ /* ============================ 请销假系统(示例,注释掉备用) ============================ */
256
+ /*
257
+ export const ONTOLOGY = {
258
+ entities: [
259
+ { id:'person', label:'员工', model:'User', titleField:'username', color:'#E9EFDF', borderColor:'#B6C6A1', layer:0 },
260
+ { id:'request', label:'请假申请', model:'LeaveRequest', titleField:'reason', color:'#F2F0E4', borderColor:'#D2CBB2', layer:1 },
261
+ { id:'plan', label:'休假计划', model:'LeavePlan', titleField:'name', color:'#F4EEDF', borderColor:'#D8C8A4', layer:1 },
262
+ { id:'change', label:'变更申请', model:'LeaveChange', titleField:'reason', color:'#EFEDE6', borderColor:'#CFC9BA', layer:2 },
263
+ ],
264
+ relations: [
265
+ { id:'submit', label:'提交人', from:'request', to:'person', via:{ field:'userId' } },
266
+ { id:'based_on', label:'依据计划', from:'request', to:'plan', via:{ field:'planId' } },
267
+ { id:'change_of', label:'变更自', from:'change', to:'request', via:{ field:'requestId' } },
268
+ { id:'approver', label:'审批人', from:'request', to:'person', via:{ field:'approverId' } },
269
+ ],
270
+ views: [
271
+ { id:'leave', label:'请销假关系图', entities:['person','request','plan','change'],
272
+ relations:['submit','based_on','change_of','approver'], root:'request' },
273
+ ],
274
+ };
275
+ */
276
+
277
+ export default ONTOLOGY;
@@ -0,0 +1,257 @@
1
+ /**
2
+ * OpenAI 兼容 Provider 统一客户端(服务端专用)。
3
+ *
4
+ * ## 为什么要单独一个文件
5
+ *
6
+ * `src/lib/ai.ts` 是"本地引擎专用"的轻客户端(只打 /chat/completions)。
7
+ * 但实际接入时你可能会遇到两类 provider:
8
+ *
9
+ * 1. **Chat Completions 格式**(最常见)
10
+ * POST {base}/chat/completions
11
+ * 请求 { model, messages:[{role,content}], max_tokens, temperature }
12
+ * 响应 { choices:[{ message:{ content } }] }
13
+ *
14
+ * 2. **Responses 格式**(OpenAI 新接口,DeepSeek/Qwen 部分网关也支持)
15
+ * POST {base}/responses
16
+ * 请求 { model, input: "..."/[...], max_output_tokens, temperature }
17
+ * 响应 { output:[{ content:[{ type:'output_text', text }] }], output_text? }
18
+ *
19
+ * 这个文件把两者的差异吃掉,对外只暴露一个 `compatInvoke()`。
20
+ * **零新增依赖**(不像 openai SDK 会多装一堆包),离线环境最稳。
21
+ *
22
+ * ## 配置(环境变量)
23
+ *
24
+ * ```
25
+ * OPENAI_BASE_URL https://api.openai.com/v1 或 http://127.0.0.1:8092/v1
26
+ * OPENAI_API_KEY sk-xxx(本地引擎随便填)
27
+ * OPENAI_MODEL gpt-4o-mini / qwen3.8-27b-gsq / ...
28
+ * OPENAI_API_MODE chat | responses | auto (默认 auto:先试 chat,404/400 再试 responses)
29
+ * ```
30
+ *
31
+ * 和 `ai.ts` 的关系:`ai.ts` 优先读 `AI_BASE_URL/AI_MODEL`,
32
+ * 这里优先读 `OPENAI_BASE_URL/OPENAI_MODEL`;两者都兼容,谁配了用谁。
33
+ */
34
+
35
+ /* ============================ 1. 配置 ============================ */
36
+
37
+ export const COMPAT_BASE_URL =
38
+ process.env.OPENAI_BASE_URL ?? process.env.AI_BASE_URL ?? 'http://127.0.0.1:8092/v1';
39
+
40
+ export const COMPAT_API_KEY =
41
+ process.env.OPENAI_API_KEY ?? process.env.AI_API_KEY ?? 'local';
42
+
43
+ export const COMPAT_MODEL =
44
+ process.env.OPENAI_MODEL ?? process.env.AI_MODEL ?? 'qwen3.8-27b-gsq';
45
+
46
+ /** 接口形态。auto = 先 chat.completions,失败再试 responses */
47
+ export type ApiMode = 'chat' | 'responses' | 'auto';
48
+ export const COMPAT_MODE: ApiMode =
49
+ (process.env.OPENAI_API_MODE as ApiMode) ?? 'auto';
50
+
51
+ export interface CompatMessage {
52
+ role: 'system' | 'user' | 'assistant';
53
+ content: string;
54
+ }
55
+
56
+ export interface CompatOptions {
57
+ system?: string;
58
+ messages: CompatMessage[];
59
+ maxTokens?: number;
60
+ temperature?: number;
61
+ timeoutMs?: number;
62
+ /** 强制指定本次用哪种接口;不填用全局 COMPAT_MODE */
63
+ mode?: ApiMode;
64
+ }
65
+
66
+ export class CompatError extends Error {
67
+ constructor(
68
+ message: string,
69
+ readonly hint?: string,
70
+ readonly status?: number
71
+ ) {
72
+ super(message);
73
+ this.name = 'CompatError';
74
+ }
75
+ }
76
+
77
+ /* ============================ 2. 底层:两种接口各一个函数 ============================ */
78
+
79
+ /** 拼接 endpoint,兼容 base 带不带 /v1 */
80
+ function endpoint(path: 'chat/completions' | 'responses'): string {
81
+ const base = COMPAT_BASE_URL.replace(/\/+$/, '');
82
+ return `${base}/${path}`;
83
+ }
84
+
85
+ function headers(): Record<string, string> {
86
+ const h: Record<string, string> = { 'Content-Type': 'application/json' };
87
+ // 本地引擎常常不校验 key,但 OpenAI 兼容端点要求这个头存在
88
+ if (COMPAT_API_KEY) h.Authorization = `Bearer ${COMPAT_API_KEY}`;
89
+ return h;
90
+ }
91
+
92
+ /** Chat Completions 形态 */
93
+ async function callChatCompletions(opts: CompatOptions): Promise<string> {
94
+ const { system, messages, maxTokens = 1024, temperature = 0.4, timeoutMs = 120_000 } = opts;
95
+
96
+ const body = {
97
+ model: COMPAT_MODEL,
98
+ messages: system ? [{ role: 'system', content: system }, ...messages] : messages,
99
+ // 新旧字段都带上,不同网关认的不一样
100
+ max_tokens: maxTokens,
101
+ max_completion_tokens: maxTokens,
102
+ temperature,
103
+ stream: false,
104
+ };
105
+
106
+ const ctrl = new AbortController();
107
+ const timer = setTimeout(() => ctrl.abort(), timeoutMs);
108
+ try {
109
+ const res = await fetch(endpoint('chat/completions'), {
110
+ method: 'POST',
111
+ headers: headers(),
112
+ body: JSON.stringify(body),
113
+ signal: ctrl.signal,
114
+ cache: 'no-store',
115
+ });
116
+ if (!res.ok) {
117
+ const t = await res.text().catch(() => '');
118
+ throw new CompatError(
119
+ `Chat Completions 返回 ${res.status}`,
120
+ t.slice(0, 300),
121
+ res.status
122
+ );
123
+ }
124
+ const data = (await res.json()) as {
125
+ choices?: { message?: { content?: string } }[];
126
+ };
127
+ const text = data.choices?.[0]?.message?.content ?? '';
128
+ if (!text.trim()) throw new CompatError('Chat Completions 返回空内容');
129
+ return text.trim();
130
+ } finally {
131
+ clearTimeout(timer);
132
+ }
133
+ }
134
+
135
+ /** Responses 形态 */
136
+ async function callResponses(opts: CompatOptions): Promise<string> {
137
+ const { system, messages, maxTokens = 1024, temperature = 0.4, timeoutMs = 120_000 } = opts;
138
+
139
+ // Responses 的 input 既可以是字符串,也可以是消息数组
140
+ const input = [
141
+ ...(system ? [{ role: 'system', content: system }] : []),
142
+ ...messages.map((m) => ({ role: m.role, content: m.content })),
143
+ ];
144
+
145
+ const body = {
146
+ model: COMPAT_MODEL,
147
+ input,
148
+ max_output_tokens: maxTokens,
149
+ temperature,
150
+ // 显式关掉 reasoning,避免有的网关默认开思考导致超时
151
+ reasoning: { effort: 'none' },
152
+ };
153
+
154
+ const ctrl = new AbortController();
155
+ const timer = setTimeout(() => ctrl.abort(), timeoutMs);
156
+ try {
157
+ const res = await fetch(endpoint('responses'), {
158
+ method: 'POST',
159
+ headers: headers(),
160
+ body: JSON.stringify(body),
161
+ signal: ctrl.signal,
162
+ cache: 'no-store',
163
+ });
164
+ if (!res.ok) {
165
+ const t = await res.text().catch(() => '');
166
+ throw new CompatError(`Responses 返回 ${res.status}`, t.slice(0, 300), res.status);
167
+ }
168
+ const data = (await res.json()) as {
169
+ output_text?: string;
170
+ output?: {
171
+ type?: string;
172
+ content?: { type?: string; text?: string }[];
173
+ }[];
174
+ };
175
+
176
+ // 优先用顶层 output_text(网关通常会给),否则从 output 里拼
177
+ if (data.output_text?.trim()) return data.output_text.trim();
178
+
179
+ const parts: string[] = [];
180
+ for (const item of data.output ?? []) {
181
+ for (const c of item.content ?? []) {
182
+ if (c?.type === 'output_text' && c.text) parts.push(c.text);
183
+ else if (c?.text) parts.push(c.text);
184
+ }
185
+ }
186
+ const text = parts.join('\n').trim();
187
+ if (!text) throw new CompatError('Responses 返回空内容');
188
+ return text;
189
+ } finally {
190
+ clearTimeout(timer);
191
+ }
192
+ }
193
+
194
+ /* ============================ 3. 对外统一入口 ============================ */
195
+
196
+ /**
197
+ * 调一次 OpenAI 兼容 provider,返回纯文本。
198
+ *
199
+ * auto 模式下的策略:先试 chat/completions;
200
+ * 如果返回 404 / 400(说明这个 provider 没有该端点或不认这个 body),
201
+ * 再试 responses。两个都失败就抛出**包含两次原因**的错误,
202
+ * 方便你判断到底是配置错了还是 provider 不支持。
203
+ */
204
+ export async function compatInvoke(opts: CompatOptions): Promise<string> {
205
+ const mode = opts.mode ?? COMPAT_MODE;
206
+ const errors: string[] = [];
207
+
208
+ const tryOne = async (m: 'chat' | 'responses') => {
209
+ try {
210
+ return m === 'chat' ? await callChatCompletions(opts) : await callResponses(opts);
211
+ } catch (e) {
212
+ const ce = e as CompatError;
213
+ const detail = ce.hint ? `${ce.message}(${ce.hint})` : ce.message;
214
+ errors.push(`${m === 'chat' ? 'chat/completions' : 'responses'}: ${detail}`);
215
+ return null;
216
+ }
217
+ };
218
+
219
+ if (mode === 'chat' || mode === 'responses') {
220
+ const r = await tryOne(mode);
221
+ if (r) return r;
222
+ throw new CompatError(
223
+ `调用 ${mode} 接口失败`,
224
+ composeHint(errors.join('\n'))
225
+ );
226
+ }
227
+
228
+ // auto
229
+ const a = await tryOne('chat');
230
+ if (a) return a;
231
+ const b = await tryOne('responses');
232
+ if (b) return b;
233
+
234
+ throw new CompatError('两种接口形态都调用失败', composeHint(errors.join('\n')));
235
+ }
236
+
237
+ function composeHint(detail: string): string {
238
+ return (
239
+ `${detail}\n\n` +
240
+ `请检查:1) OPENAI_BASE_URL 是否以 /v1 结尾;2) OPENAI_MODEL 是否与 provider 一致;` +
241
+ `3) 该 provider 是否支持 chat/completions 或 responses;` +
242
+ `4) 可用 OPENAI_API_MODE=chat 或 responses 强制指定。`
243
+ );
244
+ }
245
+
246
+ /** 探测 provider 支持哪种接口,返回可用列表(用于设置页/自检) */
247
+ export async function detectApiModes(): Promise<{ chat: boolean; responses: boolean }> {
248
+ const probe: CompatOptions = {
249
+ messages: [{ role: 'user', content: 'hi' }],
250
+ maxTokens: 8,
251
+ temperature: 0,
252
+ timeoutMs: 30_000,
253
+ };
254
+ const chat = (await callChatCompletions(probe).then(() => true).catch(() => false)) as boolean;
255
+ const responses = (await callResponses(probe).then(() => true).catch(() => false)) as boolean;
256
+ return { chat, responses };
257
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * 全局等待任务登记表(等待反馈体系的「大脑」)。
3
+ *
4
+ * 解决的问题:比赛出的题不可预知,页面里任何一个耗时动作都可能让人干瞪眼。
5
+ * 与其给每个按钮/每个页面各写一套 loading,不如让**所有耗时动作都往这里登记一条**,
6
+ * 由全局浮层统一兜底 —— 即使写页面的人忘了加 spinner,用户也不会看到"点了没反应"。
7
+ *
8
+ * 设计取舍:
9
+ * - 不用 zustand:模块级 store 在 SSR 下会被所有请求共享,容易串数据。
10
+ * 这里用「不可变数组 + 订阅者 + useSyncExternalStore」,服务端快照恒为空,天然安全。
11
+ * - 不做定时器轮询:只记录 startedAt,"已经等了多久"由展示组件自己算,
12
+ * 没人看的时候不产生任何 setInterval。
13
+ */
14
+
15
+ export type PendingKind = 'request' | 'task';
16
+
17
+ export type PendingTask = {
18
+ id: string;
19
+ /** 展示给用户的人话,例如「正在生成导出文件…」 */
20
+ label: string;
21
+ /** request = 拦截器自动登记;task = 组件显式登记(AsyncButton / useAsyncTask) */
22
+ kind: PendingKind;
23
+ /** 毫秒时间戳,展示层用它算已耗时 */
24
+ startedAt: number;
25
+ };
26
+
27
+ /**
28
+ * 三个阈值,全站统一。改这里 = 改所有地方的等待手感。
29
+ *
30
+ * 为什么是这三个数:
31
+ * - 200ms:低于这个时间的操作,人几乎察觉不到,闪一下 spinner 反而像"抖了一下"
32
+ * - 3s :超过 3 秒还没有任何反馈,用户就开始怀疑"我是不是没点上"
33
+ * - 8s :超过 8 秒必须给一句解释,否则用户会去点第二次(于是重复提交)
34
+ */
35
+ export const WAIT = {
36
+ /** 局部 spinner 延迟出现,避免快请求闪烁 */
37
+ LOCAL_DELAY: 200,
38
+ /** 全局浮层出现阈值 */
39
+ GLOBAL_APPEAR: 3000,
40
+ /** 文案升级为「较慢,仍在继续」的阈值 */
41
+ SLOW: 8000,
42
+ } as const;
43
+
44
+ /** 服务端与首帧都返回同一个常量引用,useSyncExternalStore 才不会无限重渲染 */
45
+ const EMPTY: PendingTask[] = [];
46
+
47
+ let tasks: PendingTask[] = EMPTY;
48
+ let seq = 0;
49
+ const listeners = new Set<() => void>();
50
+
51
+ function emit() {
52
+ // 复制一份再遍历:监听器里调用 endTask 会改 listeners,直接遍历会抛
53
+ for (const fn of [...listeners]) fn();
54
+ }
55
+
56
+ /** 登记一个耗时动作,返回用于注销的 id */
57
+ export function beginTask(label: string, kind: PendingKind = 'task'): string {
58
+ seq += 1;
59
+ const id = `wb-task-${seq}`;
60
+ tasks = [...tasks, { id, label, kind, startedAt: Date.now() }];
61
+ emit();
62
+ return id;
63
+ }
64
+
65
+ /** 注销。重复注销 / 传不存在的 id 都安全(并发请求各自 end,不会互相顶掉) */
66
+ export function endTask(id: string | undefined | null): void {
67
+ if (!id) return;
68
+ if (!tasks.some((t) => t.id === id)) return;
69
+ tasks = tasks.filter((t) => t.id !== id);
70
+ emit();
71
+ }
72
+
73
+ /** 页面卸载 / 登出时清空,避免残留任务把浮层永久钉在屏幕上 */
74
+ export function clearTasks(): void {
75
+ if (!tasks.length) return;
76
+ tasks = EMPTY;
77
+ emit();
78
+ }
79
+
80
+ export function subscribeTasks(listener: () => void): () => void {
81
+ listeners.add(listener);
82
+ return () => {
83
+ listeners.delete(listener);
84
+ };
85
+ }
86
+
87
+ export function getTasksSnapshot(): PendingTask[] {
88
+ return tasks;
89
+ }
90
+
91
+ export function getTasksServerSnapshot(): PendingTask[] {
92
+ return EMPTY;
93
+ }
94
+
95
+ /** 给非 React 场景(拦截器、原生事件)读当前任务数用 */
96
+ export function pendingCount(): number {
97
+ return tasks.length;
98
+ }
@@ -0,0 +1,30 @@
1
+ import { PrismaClient } from '@prisma/client';
2
+ import { PrismaLibSql } from '@prisma/adapter-libsql';
3
+
4
+ /**
5
+ * Prisma 7 的用法和 6 有两个硬性差别,照着老写法会直接跑不起来:
6
+ *
7
+ * 1. 连接串不能写在 schema.prisma 的 datasource 里(会报 P1012),
8
+ * 要放到项目根目录的 prisma.config.ts,供 migrate / db push 使用。
9
+ * 2. PrismaClient 必须传 driver adapter —— Rust 查询引擎已被移除,
10
+ * 运行时靠 JS 侧的查询编译器 + 数据库驱动。
11
+ *
12
+ * 这里选 libSQL 而不是 better-sqlite3,是为了内网离线:
13
+ * libSQL 的原生二进制以「普通 npm 平台包」(@libsql/win32-x64-msvc)分发,
14
+ * 安装时不需要任何联网下载或本地编译;better-sqlite3 则要靠
15
+ * prebuild-install 去 GitHub 取二进制,离线环境下拿不到。
16
+ */
17
+ const adapter = new PrismaLibSql({
18
+ url: process.env.DATABASE_URL ?? 'file:./dev.db',
19
+ });
20
+
21
+ const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient };
22
+
23
+ export const prisma =
24
+ globalForPrisma.prisma ??
25
+ new PrismaClient({
26
+ adapter,
27
+ log: process.env.NODE_ENV === 'development' ? ['warn', 'error'] : ['error'],
28
+ });
29
+
30
+ if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;