hydro-ai-helper 1.8.5

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 (182) hide show
  1. package/.eslintrc.json +31 -0
  2. package/.github/workflows/sync-releases-to-gitee.yml +119 -0
  3. package/.github/workflows/sync-to-gitee.yml +53 -0
  4. package/.vercelignore +31 -0
  5. package/CONTRIBUTING.md +165 -0
  6. package/README.md +273 -0
  7. package/TROUBLESHOOTING.md +355 -0
  8. package/api/badge-active.ts +88 -0
  9. package/api/badge-installs.ts +75 -0
  10. package/api/report.ts +242 -0
  11. package/assets/screenshots/1.png +0 -0
  12. package/assets/screenshots/2.png +0 -0
  13. package/assets/screenshots/3.png +0 -0
  14. package/assets/screenshots/4.png +0 -0
  15. package/assets/screenshots/5.png +0 -0
  16. package/assets/screenshots/6.png +0 -0
  17. package/assets/screenshots/7.png +0 -0
  18. package/dist/__tests__/__mocks__/hydrooj.js +29 -0
  19. package/dist/__tests__/__mocks__/hydrooj.js.map +1 -0
  20. package/dist/constants/jailbreakRules.js +29 -0
  21. package/dist/constants/jailbreakRules.js.map +1 -0
  22. package/dist/constants/permissions.js +42 -0
  23. package/dist/constants/permissions.js.map +1 -0
  24. package/dist/handlers/adminConfigHandler.js +347 -0
  25. package/dist/handlers/adminConfigHandler.js.map +1 -0
  26. package/dist/handlers/adminHandler.js +378 -0
  27. package/dist/handlers/adminHandler.js.map +1 -0
  28. package/dist/handlers/analyticsHandler.js +433 -0
  29. package/dist/handlers/analyticsHandler.js.map +1 -0
  30. package/dist/handlers/dashboardHandler.js +22 -0
  31. package/dist/handlers/dashboardHandler.js.map +1 -0
  32. package/dist/handlers/exportHandler.js +118 -0
  33. package/dist/handlers/exportHandler.js.map +1 -0
  34. package/dist/handlers/studentHandler.js +430 -0
  35. package/dist/handlers/studentHandler.js.map +1 -0
  36. package/dist/handlers/teacherHandler.js +246 -0
  37. package/dist/handlers/teacherHandler.js.map +1 -0
  38. package/dist/handlers/testHandler.js +27 -0
  39. package/dist/handlers/testHandler.js.map +1 -0
  40. package/dist/handlers/updateHandler.js +84 -0
  41. package/dist/handlers/updateHandler.js.map +1 -0
  42. package/dist/handlers/versionHandler.js +61 -0
  43. package/dist/handlers/versionHandler.js.map +1 -0
  44. package/dist/index.d.ts +22 -0
  45. package/dist/index.d.ts.map +1 -0
  46. package/dist/index.js +151 -0
  47. package/dist/index.js.map +1 -0
  48. package/dist/lib/crypto.js +110 -0
  49. package/dist/lib/crypto.js.map +1 -0
  50. package/dist/lib/httpHelpers.js +61 -0
  51. package/dist/lib/httpHelpers.js.map +1 -0
  52. package/dist/lib/queryHelpers.js +81 -0
  53. package/dist/lib/queryHelpers.js.map +1 -0
  54. package/dist/models/aiConfig.js +255 -0
  55. package/dist/models/aiConfig.js.map +1 -0
  56. package/dist/models/conversation.js +209 -0
  57. package/dist/models/conversation.js.map +1 -0
  58. package/dist/models/jailbreakLog.js +68 -0
  59. package/dist/models/jailbreakLog.js.map +1 -0
  60. package/dist/models/message.js +174 -0
  61. package/dist/models/message.js.map +1 -0
  62. package/dist/models/pluginInstall.js +95 -0
  63. package/dist/models/pluginInstall.js.map +1 -0
  64. package/dist/models/rateLimitRecord.js +54 -0
  65. package/dist/models/rateLimitRecord.js.map +1 -0
  66. package/dist/models/versionCache.js +89 -0
  67. package/dist/models/versionCache.js.map +1 -0
  68. package/dist/services/effectivenessService.js +131 -0
  69. package/dist/services/effectivenessService.js.map +1 -0
  70. package/dist/services/exportService.js +152 -0
  71. package/dist/services/exportService.js.map +1 -0
  72. package/dist/services/migrationService.js +90 -0
  73. package/dist/services/migrationService.js.map +1 -0
  74. package/dist/services/openaiClient.js +412 -0
  75. package/dist/services/openaiClient.js.map +1 -0
  76. package/dist/services/promptService.js +467 -0
  77. package/dist/services/promptService.js.map +1 -0
  78. package/dist/services/rateLimitService.js +108 -0
  79. package/dist/services/rateLimitService.js.map +1 -0
  80. package/dist/services/telemetryService.js +173 -0
  81. package/dist/services/telemetryService.js.map +1 -0
  82. package/dist/services/updateService.js +388 -0
  83. package/dist/services/updateService.js.map +1 -0
  84. package/dist/services/versionService.js +289 -0
  85. package/dist/services/versionService.js.map +1 -0
  86. package/dist/types/hydrooj.js +7 -0
  87. package/dist/types/hydrooj.js.map +1 -0
  88. package/dist/types/mongo.js +7 -0
  89. package/dist/types/mongo.js.map +1 -0
  90. package/dist/utils/domainHelper.js +16 -0
  91. package/dist/utils/domainHelper.js.map +1 -0
  92. package/dist/utils/mongo.js +16 -0
  93. package/dist/utils/mongo.js.map +1 -0
  94. package/frontend/admin/ConfigPanel.tsx +1379 -0
  95. package/frontend/admin/VersionBadge.tsx +655 -0
  96. package/frontend/ai_helper.page.tsx +52 -0
  97. package/frontend/ai_helper_admin_config.page.tsx +47 -0
  98. package/frontend/ai_helper_analytics.page.tsx +52 -0
  99. package/frontend/ai_helper_conversation_detail.page.tsx +55 -0
  100. package/frontend/ai_helper_conversations.page.tsx +56 -0
  101. package/frontend/components/AIHelperDashboard.tsx +125 -0
  102. package/frontend/problem_detail.page.tsx +395 -0
  103. package/frontend/student/AIAssistantPanel.tsx +1839 -0
  104. package/frontend/student/AIChatPanel.tsx +944 -0
  105. package/frontend/student/ThreeColumnLayout.tsx +240 -0
  106. package/frontend/teacher/AnalyticsPage.tsx +614 -0
  107. package/frontend/teacher/ConversationDetail.tsx +504 -0
  108. package/frontend/teacher/ConversationList.tsx +568 -0
  109. package/frontend/teacher/ExportDialog.tsx +291 -0
  110. package/frontend/test.page.ts +88 -0
  111. package/frontend/utils/domainUtils.ts +37 -0
  112. package/index.js +6 -0
  113. package/jest.config.js +17 -0
  114. package/lib/mongodb.ts +60 -0
  115. package/locales/en.yaml +4 -0
  116. package/locales/zh.yaml +77 -0
  117. package/package.json +48 -0
  118. package/public/.gitkeep +0 -0
  119. package/public/README.md +1 -0
  120. package/src/__tests__/__mocks__/hydrooj.ts +27 -0
  121. package/src/__tests__/services/exportService.test.ts +156 -0
  122. package/src/__tests__/services/promptService.test.ts +141 -0
  123. package/src/__tests__/services/rateLimitService.test.ts +145 -0
  124. package/src/__tests__/utils/domainHelper.test.ts +47 -0
  125. package/src/constants/jailbreakRules.ts +28 -0
  126. package/src/constants/permissions.ts +43 -0
  127. package/src/handlers/adminConfigHandler.ts +363 -0
  128. package/src/handlers/adminHandler.ts +427 -0
  129. package/src/handlers/analyticsHandler.ts +517 -0
  130. package/src/handlers/dashboardHandler.ts +20 -0
  131. package/src/handlers/exportHandler.ts +134 -0
  132. package/src/handlers/studentHandler.ts +483 -0
  133. package/src/handlers/teacherHandler.ts +317 -0
  134. package/src/handlers/testHandler.ts +25 -0
  135. package/src/handlers/updateHandler.ts +85 -0
  136. package/src/handlers/versionHandler.ts +62 -0
  137. package/src/index.ts +198 -0
  138. package/src/lib/crypto.ts +125 -0
  139. package/src/lib/httpHelpers.ts +75 -0
  140. package/src/lib/queryHelpers.ts +108 -0
  141. package/src/models/aiConfig.ts +375 -0
  142. package/src/models/conversation.ts +328 -0
  143. package/src/models/jailbreakLog.ts +108 -0
  144. package/src/models/message.ts +246 -0
  145. package/src/models/pluginInstall.ts +138 -0
  146. package/src/models/rateLimitRecord.ts +81 -0
  147. package/src/models/versionCache.ts +127 -0
  148. package/src/services/effectivenessService.ts +161 -0
  149. package/src/services/exportService.ts +191 -0
  150. package/src/services/migrationService.ts +111 -0
  151. package/src/services/openaiClient.ts +548 -0
  152. package/src/services/promptService.ts +573 -0
  153. package/src/services/rateLimitService.ts +122 -0
  154. package/src/services/telemetryService.ts +218 -0
  155. package/src/services/updateService.ts +441 -0
  156. package/src/services/versionService.ts +351 -0
  157. package/src/types/hydrooj-extensions.d.ts +55 -0
  158. package/src/types/hydrooj.ts +38 -0
  159. package/src/types/mongo.ts +23 -0
  160. package/src/utils/domainHelper.ts +16 -0
  161. package/src/utils/mongo.ts +15 -0
  162. package/templates/ai-helper/admin_config.html +8 -0
  163. package/templates/ai-helper/analytics.html +8 -0
  164. package/templates/ai-helper/dashboard.html +8 -0
  165. package/templates/ai-helper/teacher_conversation_detail.html +8 -0
  166. package/templates/ai-helper/teacher_conversations.html +8 -0
  167. package/tests/integration/README.md +86 -0
  168. package/tests/integration/curl/01-admin-config-set.sh +56 -0
  169. package/tests/integration/curl/02-student-chat.sh +67 -0
  170. package/tests/integration/curl/03-teacher-list.sh +52 -0
  171. package/tests/integration/curl/04-teacher-detail.sh +37 -0
  172. package/tests/integration/curl/05-export.sh +49 -0
  173. package/tests/integration/curl/06-analytics.sh +43 -0
  174. package/tests/performance/README.md +85 -0
  175. package/tests/performance/data/prepare-sample-data.md +57 -0
  176. package/tests/performance/scripts/export-csv-k6.js +41 -0
  177. package/tests/performance/scripts/student-chat-k6.js +44 -0
  178. package/tests/performance/scripts/teacher-conversations-k6.js +33 -0
  179. package/tsconfig.json +42 -0
  180. package/tsconfig.vercel.json +27 -0
  181. package/types/hydrooj/index.d.ts +73 -0
  182. package/vercel.json +3 -0
@@ -0,0 +1,317 @@
1
+ /**
2
+ * 教师端 Handler
3
+ * 处理教师查看对话列表和详情的请求
4
+ */
5
+
6
+ import { Handler, PRIV, db } from 'hydrooj';
7
+ import { ConversationModel } from '../models/conversation';
8
+ import { MessageModel } from '../models/message';
9
+ import { ObjectId } from '../utils/mongo';
10
+ import { setJsonResponse, setErrorResponse, setTemplateResponse, expectsJson } from '../lib/httpHelpers';
11
+ import { parsePaginationParams } from '../lib/queryHelpers';
12
+ import { getDomainId } from '../utils/domainHelper';
13
+ import type { UserDocument } from '../types/hydrooj';
14
+
15
+ /**
16
+ * 批量获取用户名映射
17
+ * @param uids 用户 ID 数组
18
+ * @returns uid -> uname 映射表
19
+ */
20
+ async function getUserNameMap(uids: number[]): Promise<Map<number, string>> {
21
+ const uniqueUids = [...new Set(uids)];
22
+ const userMap = new Map<number, string>();
23
+
24
+ if (uniqueUids.length === 0) {
25
+ return userMap;
26
+ }
27
+
28
+ try {
29
+ // 直接使用 HydroOJ db 访问 user 集合
30
+ const userColl = db.collection('user');
31
+ const users = await userColl.find({ _id: { $in: uniqueUids } }).toArray();
32
+ for (const user of users) {
33
+ const u = user as UserDocument;
34
+ userMap.set(u._id, u.uname || '已删除用户');
35
+ }
36
+ // 对于不存在的用户,设置默认值
37
+ for (const uid of uniqueUids) {
38
+ if (!userMap.has(uid)) {
39
+ userMap.set(uid, '已删除用户');
40
+ }
41
+ }
42
+ } catch (err) {
43
+ console.error('[AI Helper] Failed to fetch user names:', err);
44
+ // 出错时全部使用默认值
45
+ for (const uid of uniqueUids) {
46
+ userMap.set(uid, '已删除用户');
47
+ }
48
+ }
49
+
50
+ return userMap;
51
+ }
52
+
53
+ /**
54
+ * 对话概要接口
55
+ */
56
+ interface ConversationSummary {
57
+ _id: string;
58
+ userId: number;
59
+ userName: string;
60
+ classId?: string;
61
+ problemId: string;
62
+ problemUrl?: string; // T031: 题目详情页 URL
63
+ startTime: string;
64
+ endTime: string;
65
+ messageCount: number;
66
+ isEffective: boolean;
67
+ tags: string[];
68
+ teacherNote?: string;
69
+ metadata?: {
70
+ problemTitle?: string;
71
+ problemContent?: string;
72
+ };
73
+ firstMessageSummary?: string; // T048: 学生首条消息摘要(前100字)
74
+ }
75
+
76
+ /**
77
+ * T031/T032: 生成题目详情页 URL
78
+ * @param domainId 域 ID
79
+ * @param problemId 题目 ID
80
+ * @returns 题目详情页 URL
81
+ */
82
+ function buildProblemUrl(domainId: string, problemId: string): string {
83
+ if (!problemId) return '';
84
+ // 域前缀格式:/d/:domainId/p/:problemId
85
+ // 主站格式:/p/:problemId
86
+ if (domainId && domainId !== 'system') {
87
+ return `/d/${domainId}/p/${problemId}`;
88
+ }
89
+ return `/p/${problemId}`;
90
+ }
91
+
92
+ /**
93
+ * ConversationListHandler - 获取对话列表
94
+ * GET /ai-helper/conversations
95
+ * GET /d/:domainId/ai-helper/conversations
96
+ */
97
+ export class ConversationListHandler extends Handler {
98
+ async get() {
99
+ try {
100
+ const wantJson = expectsJson(this);
101
+ const conversationModel: ConversationModel = this.ctx.get('conversationModel');
102
+
103
+ // 获取当前域 ID(用于域隔离)
104
+ const domainId = getDomainId(this);
105
+
106
+ // 从查询参数读取筛选条件
107
+ const {
108
+ startDate,
109
+ endDate,
110
+ problemId,
111
+ classId,
112
+ userId,
113
+ page = '1',
114
+ limit = '50'
115
+ } = this.request.query;
116
+
117
+ // 解析分页参数
118
+ const { page: pageNum, limit: limitNum } = parsePaginationParams(page as string, limit as string);
119
+
120
+ // 构造筛选条件(始终包含 domainId 以实现域隔离)
121
+ const filters: {
122
+ domainId?: string;
123
+ startDate?: string;
124
+ endDate?: string;
125
+ problemId?: string;
126
+ classId?: string;
127
+ userId?: number;
128
+ } = {
129
+ domainId // 域隔离:只查询当前域的对话
130
+ };
131
+
132
+ if (startDate) {
133
+ filters.startDate = startDate as string;
134
+ }
135
+
136
+ if (endDate) {
137
+ filters.endDate = endDate as string;
138
+ }
139
+
140
+ if (problemId) {
141
+ filters.problemId = problemId as string;
142
+ }
143
+
144
+ if (classId) {
145
+ filters.classId = classId as string;
146
+ }
147
+
148
+ if (userId) {
149
+ filters.userId = parseInt(userId as string, 10);
150
+ }
151
+
152
+ // 查询对话列表(已通过 domainId 实现域隔离)
153
+ const { conversations, total } = await conversationModel.findByFilters(
154
+ filters,
155
+ pageNum,
156
+ limitNum
157
+ );
158
+
159
+ // 批量获取用户名(避免 N+1 查询)
160
+ const uids = conversations.map(conv => conv.userId);
161
+ const userNameMap = await getUserNameMap(uids);
162
+
163
+ // T048: 批量获取每个会话的第一条学生消息
164
+ const messageModel: MessageModel = this.ctx.get('messageModel');
165
+ const conversationIds = conversations.map(conv => conv._id);
166
+ const firstMessagesMap = await messageModel.findFirstStudentMessagesForConversations(conversationIds);
167
+
168
+ // T031: 转换为响应格式(包含 problemUrl 和 firstMessageSummary)
169
+ const conversationSummaries: ConversationSummary[] = conversations.map(conv => {
170
+ const convIdStr = conv._id.toString();
171
+ const firstMsg = firstMessagesMap.get(convIdStr);
172
+ // T048: 截取前100字作为摘要
173
+ let firstMessageSummary: string | undefined;
174
+ if (firstMsg?.content) {
175
+ firstMessageSummary = firstMsg.content.length > 100
176
+ ? firstMsg.content.substring(0, 100) + '...'
177
+ : firstMsg.content;
178
+ }
179
+
180
+ return {
181
+ _id: convIdStr,
182
+ userId: conv.userId,
183
+ userName: userNameMap.get(conv.userId) || '已删除用户',
184
+ classId: conv.classId,
185
+ problemId: conv.problemId,
186
+ problemUrl: buildProblemUrl(domainId, conv.problemId), // T031: 题目链接
187
+ startTime: conv.startTime.toISOString(),
188
+ endTime: conv.endTime.toISOString(),
189
+ messageCount: conv.messageCount,
190
+ isEffective: conv.isEffective,
191
+ tags: conv.tags,
192
+ teacherNote: conv.teacherNote,
193
+ metadata: conv.metadata,
194
+ firstMessageSummary // T048: 学生问题摘要
195
+ };
196
+ });
197
+
198
+ if (wantJson) {
199
+ setJsonResponse(this, {
200
+ conversations: conversationSummaries,
201
+ total,
202
+ page: pageNum,
203
+ limit: limitNum
204
+ });
205
+ return;
206
+ }
207
+
208
+ setTemplateResponse(this, 'ai-helper/teacher_conversations.html', {
209
+ total,
210
+ page: pageNum,
211
+ limit: limitNum
212
+ });
213
+ } catch (err) {
214
+ console.error('[AI Helper] ConversationListHandler error:', err);
215
+ setErrorResponse(this, 'INTERNAL_ERROR', err instanceof Error ? err.message : '服务器内部错误', 500);
216
+ }
217
+ }
218
+ }
219
+
220
+ /**
221
+ * ConversationDetailHandler - 获取对话详情
222
+ * GET /ai-helper/conversations/:id
223
+ * GET /d/:domainId/ai-helper/conversations/:id
224
+ */
225
+ export class ConversationDetailHandler extends Handler {
226
+ async get({ id }: { id: string }) {
227
+ try {
228
+ const wantJson = expectsJson(this);
229
+ const conversationModel: ConversationModel = this.ctx.get('conversationModel');
230
+ const messageModel: MessageModel = this.ctx.get('messageModel');
231
+
232
+ // 获取当前域 ID(用于域隔离验证)
233
+ const domainId = getDomainId(this);
234
+
235
+ if (!ObjectId.isValid(id)) {
236
+ setErrorResponse(this, 'INVALID_ID', '无效的会话 ID');
237
+ return;
238
+ }
239
+
240
+ // 查询会话详情
241
+ const conversation = await conversationModel.findById(id);
242
+
243
+ if (!conversation) {
244
+ setErrorResponse(this, 'NOT_FOUND', '会话不存在', 404);
245
+ return;
246
+ }
247
+
248
+ // 域隔离验证:确保对话属于当前域
249
+ // 特殊处理:
250
+ // 1. 对话的 domainId 为 'system'(历史数据迁移默认值)时,允许任何域访问
251
+ // 2. 当前访问域为 'system'(主站访问)时,允许访问所有域的对话
252
+ const conversationDomain = conversation.domainId || 'system';
253
+ if (conversationDomain !== domainId && conversationDomain !== 'system' && domainId !== 'system') {
254
+ setErrorResponse(this, 'FORBIDDEN', '无权访问此对话', 403);
255
+ return;
256
+ }
257
+
258
+ // 查询会话的所有消息 (按时间升序)
259
+ const messages = await messageModel.findByConversationId(id);
260
+
261
+ // 转换消息格式
262
+ const messagesFormatted = messages.map(msg => ({
263
+ _id: msg._id.toString(),
264
+ role: msg.role,
265
+ content: msg.content,
266
+ timestamp: msg.timestamp.toISOString(),
267
+ questionType: msg.questionType,
268
+ attachedCode: msg.attachedCode,
269
+ attachedError: msg.attachedError,
270
+ metadata: msg.metadata
271
+ }));
272
+
273
+ // 获取用户名
274
+ const userNameMap = await getUserNameMap([conversation.userId]);
275
+ const userName = userNameMap.get(conversation.userId) || '已删除用户';
276
+
277
+ // T032: 对话详情响应(包含 problemUrl)
278
+ const conversationData = {
279
+ _id: conversation._id.toString(),
280
+ userId: conversation.userId,
281
+ userName,
282
+ classId: conversation.classId,
283
+ problemId: conversation.problemId,
284
+ problemUrl: buildProblemUrl(domainId, conversation.problemId), // T032: 题目链接
285
+ startTime: conversation.startTime.toISOString(),
286
+ endTime: conversation.endTime.toISOString(),
287
+ messageCount: conversation.messageCount,
288
+ isEffective: conversation.isEffective,
289
+ tags: conversation.tags,
290
+ teacherNote: conversation.teacherNote,
291
+ metadata: conversation.metadata
292
+ };
293
+
294
+ if (wantJson) {
295
+ setJsonResponse(this, {
296
+ conversation: conversationData,
297
+ messages: messagesFormatted
298
+ });
299
+ return;
300
+ }
301
+
302
+ setTemplateResponse(this, 'ai-helper/teacher_conversation_detail.html', {
303
+ conversation: conversationData,
304
+ messages: messagesFormatted
305
+ });
306
+ } catch (err) {
307
+ console.error('[AI Helper] ConversationDetailHandler error:', err);
308
+ setErrorResponse(this, 'INTERNAL_ERROR', err instanceof Error ? err.message : '服务器内部错误', 500);
309
+ }
310
+ }
311
+ }
312
+
313
+ // 导出路由权限配置
314
+ // 使用 PRIV.PRIV_EDIT_SYSTEM (root-only 权限)
315
+ // AI 对话数据敏感,目前仅允许系统管理员访问
316
+ export const ConversationListHandlerPriv = PRIV.PRIV_EDIT_SYSTEM;
317
+ export const ConversationDetailHandlerPriv = PRIV.PRIV_EDIT_SYSTEM;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * 测试路由 Handler
3
+ * 用于验证插件加载和路由注册是否成功
4
+ */
5
+
6
+ import { Handler, PRIV } from 'hydrooj';
7
+
8
+ /**
9
+ * Hello 测试 Handler
10
+ * GET /ai-helper/hello - 返回插件状态信息
11
+ */
12
+ export class HelloHandler extends Handler {
13
+ async get() {
14
+ this.response.body = {
15
+ message: 'AI Helper Plugin Loaded',
16
+ version: '0.1.0',
17
+ timestamp: new Date().toISOString(),
18
+ status: 'ok'
19
+ };
20
+ this.response.type = 'application/json';
21
+ }
22
+ }
23
+
24
+ // 导出路由权限配置(最低权限 - 任何人可访问)
25
+ export const HelloHandlerPriv = PRIV.PRIV_NONE;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * UpdateHandler - 插件更新 API 处理器
3
+ *
4
+ * 提供插件在线更新接口:
5
+ * GET /ai-helper/admin/update/info - 获取更新信息
6
+ * POST /ai-helper/admin/update - 执行更新
7
+ */
8
+
9
+ import { Handler, PRIV } from 'hydrooj';
10
+ import { UpdateService } from '../services/updateService';
11
+ import { setJsonResponse, setErrorResponse } from '../lib/httpHelpers';
12
+
13
+ /**
14
+ * UpdateInfoHandler - 获取更新信息
15
+ * GET /ai-helper/admin/update/info
16
+ *
17
+ * 响应:
18
+ * {
19
+ * pluginPath: string, // 插件安装路径
20
+ * isValid: boolean, // 路径是否有效
21
+ * message: string // 验证消息
22
+ * }
23
+ */
24
+ export class UpdateInfoHandler extends Handler {
25
+ async get() {
26
+ try {
27
+ const updateService = new UpdateService();
28
+ const info = updateService.getPluginInfo();
29
+
30
+ setJsonResponse(this, info);
31
+ } catch (err) {
32
+ console.error('[UpdateInfoHandler] Error:', err);
33
+ setErrorResponse(this, 'UPDATE_INFO_FAILED', err instanceof Error ? err.message : '获取更新信息失败', 500);
34
+ }
35
+ }
36
+ }
37
+
38
+ // 导出路由权限配置 - root-only
39
+ export const UpdateInfoHandlerPriv = PRIV.PRIV_EDIT_SYSTEM;
40
+
41
+ /**
42
+ * UpdateHandler - 执行更新
43
+ * POST /ai-helper/admin/update
44
+ *
45
+ * 响应:
46
+ * {
47
+ * success: boolean,
48
+ * step: string,
49
+ * message: string,
50
+ * logs: string[],
51
+ * pluginPath?: string,
52
+ * error?: string
53
+ * }
54
+ */
55
+ export class UpdateHandler extends Handler {
56
+ async post() {
57
+ try {
58
+ const updateService = new UpdateService();
59
+
60
+ // 收集所有日志
61
+ const allLogs: string[] = [];
62
+
63
+ // 执行更新
64
+ const result = await updateService.performUpdate((step, log) => {
65
+ allLogs.push(`[${step}] ${log}`);
66
+ console.log(`[UpdateHandler] ${step}: ${log}`);
67
+ });
68
+
69
+ setJsonResponse(this, {
70
+ success: result.success,
71
+ step: result.step,
72
+ message: result.message,
73
+ logs: result.logs,
74
+ pluginPath: result.pluginPath,
75
+ error: result.error
76
+ });
77
+ } catch (err) {
78
+ console.error('[UpdateHandler] Error:', err);
79
+ setErrorResponse(this, 'UPDATE_FAILED', err instanceof Error ? err.message : '更新失败', 500);
80
+ }
81
+ }
82
+ }
83
+
84
+ // 导出路由权限配置 - root-only
85
+ export const UpdateHandlerPriv = PRIV.PRIV_EDIT_SYSTEM;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Version Handler - 版本检测 API 处理器
3
+ *
4
+ * T051: 提供版本检测接口
5
+ * GET /ai-helper/version/check - 检查版本更新
6
+ */
7
+
8
+ import { Handler, PRIV } from 'hydrooj';
9
+ import { VersionService } from '../services/versionService';
10
+ import { setJsonResponse, setErrorResponse } from '../lib/httpHelpers';
11
+
12
+ /**
13
+ * VersionCheckHandler - 检查版本更新
14
+ * GET /ai-helper/version/check
15
+ * GET /d/:domainId/ai-helper/version/check
16
+ *
17
+ * 查询参数:
18
+ * - refresh: 是否强制刷新(忽略缓存),默认 false
19
+ *
20
+ * 响应:
21
+ * {
22
+ * currentVersion: string, // 当前安装版本
23
+ * latestVersion: string, // 远程最新版本
24
+ * hasUpdate: boolean, // 是否有可用更新
25
+ * releaseUrl: string, // 发布页面 URL
26
+ * releaseNotes?: string, // 更新说明(如有)
27
+ * checkedAt: string, // 检查时间(ISO 格式)
28
+ * fromCache: boolean // 是否来自缓存
29
+ * }
30
+ */
31
+ export class VersionCheckHandler extends Handler {
32
+ async get() {
33
+ try {
34
+ const versionService: VersionService = this.ctx.get('versionService');
35
+
36
+ // 检查是否强制刷新
37
+ const refresh = this.request.query.refresh === 'true';
38
+
39
+ // 检查版本更新
40
+ const result = await versionService.checkForUpdates(refresh);
41
+
42
+ setJsonResponse(this, {
43
+ currentVersion: result.currentVersion,
44
+ latestVersion: result.latestVersion,
45
+ hasUpdate: result.hasUpdate,
46
+ releaseUrl: result.releaseUrl,
47
+ releaseNotes: result.releaseNotes,
48
+ checkedAt: result.checkedAt.toISOString(),
49
+ fromCache: result.fromCache,
50
+ source: result.source
51
+ });
52
+ } catch (err) {
53
+ console.error('[VersionCheckHandler] Error:', err);
54
+ setErrorResponse(this, 'VERSION_CHECK_FAILED', err instanceof Error ? err.message : '版本检查失败', 500);
55
+ }
56
+ }
57
+ }
58
+
59
+ // 导出路由权限配置
60
+ // 使用 PRIV.PRIV_EDIT_SYSTEM (root-only 权限)
61
+ // 版本检查仅限管理员访问
62
+ export const VersionCheckHandlerPriv = PRIV.PRIV_EDIT_SYSTEM;
package/src/index.ts ADDED
@@ -0,0 +1,198 @@
1
+ /**
2
+ * HydroOJ AI Learning Assistant Plugin
3
+ *
4
+ * 教学优先的 AI 辅助学习插件
5
+ * - 引导式学习,不提供完整代码
6
+ * - 对话记录可追踪
7
+ * - 符合教学研究需求
8
+ */
9
+
10
+ console.log('[AI-Helper] Loading plugin...');
11
+
12
+ import { Context, definePlugin, Schema } from 'hydrooj';
13
+ console.log('[AI-Helper] hydrooj imports OK');
14
+
15
+ import { HelloHandler, HelloHandlerPriv } from './handlers/testHandler';
16
+ console.log('[AI-Helper] testHandler OK');
17
+
18
+ import { ChatHandler, ChatHandlerPriv, ProblemStatusHandler, ProblemStatusHandlerPriv } from './handlers/studentHandler';
19
+ console.log('[AI-Helper] studentHandler OK');
20
+
21
+ import {
22
+ ConversationListHandler,
23
+ ConversationListHandlerPriv,
24
+ ConversationDetailHandler,
25
+ ConversationDetailHandlerPriv
26
+ } from './handlers/teacherHandler';
27
+ console.log('[AI-Helper] teacherHandler OK');
28
+
29
+ import { AnalyticsHandler, AnalyticsHandlerPriv } from './handlers/analyticsHandler';
30
+ console.log('[AI-Helper] analyticsHandler OK');
31
+
32
+ import { AdminConfigHandler, AdminConfigHandlerPriv } from './handlers/adminConfigHandler';
33
+ console.log('[AI-Helper] adminConfigHandler OK');
34
+
35
+ import { AIHelperDashboardHandler, AIHelperDashboardHandlerPriv } from './handlers/dashboardHandler';
36
+ console.log('[AI-Helper] dashboardHandler OK');
37
+
38
+ import { ExportHandler, ExportHandlerPriv } from './handlers/exportHandler';
39
+ console.log('[AI-Helper] exportHandler OK');
40
+
41
+ import { TestConnectionHandler, TestConnectionHandlerPriv, FetchModelsHandler, FetchModelsHandlerPriv } from './handlers/adminHandler';
42
+ console.log('[AI-Helper] adminHandler OK');
43
+
44
+ import { VersionCheckHandler, VersionCheckHandlerPriv } from './handlers/versionHandler';
45
+ console.log('[AI-Helper] versionHandler OK');
46
+
47
+ import { UpdateInfoHandler, UpdateInfoHandlerPriv, UpdateHandler, UpdateHandlerPriv } from './handlers/updateHandler';
48
+ console.log('[AI-Helper] updateHandler OK');
49
+
50
+ import { ConversationModel } from './models/conversation';
51
+ import { MessageModel } from './models/message';
52
+ import { RateLimitRecordModel } from './models/rateLimitRecord';
53
+ import { AIConfigModel } from './models/aiConfig';
54
+ import { JailbreakLogModel } from './models/jailbreakLog';
55
+ import { VersionCacheModel } from './models/versionCache';
56
+ import { PluginInstallModel } from './models/pluginInstall';
57
+ console.log('[AI-Helper] models OK');
58
+
59
+ import { MigrationService } from './services/migrationService';
60
+ import { VersionService } from './services/versionService';
61
+ import { TelemetryService } from './services/telemetryService';
62
+ console.log('[AI-Helper] services OK');
63
+
64
+ console.log('[AI-Helper] All imports completed successfully');
65
+
66
+ /**
67
+ * 插件配置接口
68
+ */
69
+ export interface AIHelperConfig {
70
+ // 未来可在此添加配置项
71
+ }
72
+
73
+ /**
74
+ * 插件入口函数
75
+ * @param ctx HydroOJ Context
76
+ * @param config 插件配置
77
+ */
78
+ const configSchema = Schema.object({}).description('AI 助手插件配置(预留)').default({});
79
+
80
+ const aiHelperPlugin = definePlugin<AIHelperConfig>({
81
+ name: 'hydro-ai-helper',
82
+ schema: configSchema,
83
+ async apply(ctx: Context) {
84
+ // 初始化数据库模型
85
+ const db = ctx.db;
86
+ const conversationModel = new ConversationModel(db);
87
+ const messageModel = new MessageModel(db);
88
+ const rateLimitRecordModel = new RateLimitRecordModel(db);
89
+ const aiConfigModel = new AIConfigModel(db);
90
+ const jailbreakLogModel = new JailbreakLogModel(db);
91
+ const versionCacheModel = new VersionCacheModel(db);
92
+ const pluginInstallModel = new PluginInstallModel(db);
93
+
94
+ // 创建数据库索引
95
+ await conversationModel.ensureIndexes();
96
+ await messageModel.ensureIndexes();
97
+ await rateLimitRecordModel.ensureIndexes();
98
+ await aiConfigModel.ensureIndexes();
99
+ await jailbreakLogModel.ensureIndexes();
100
+ await versionCacheModel.ensureIndexes();
101
+ await pluginInstallModel.ensureIndexes();
102
+
103
+ // 执行数据迁移(为历史数据添加 domainId)
104
+ const migrationService = new MigrationService(db);
105
+ await migrationService.runAllMigrations();
106
+
107
+ // 初始化插件安装记录
108
+ const packageJson = require('../package.json');
109
+ const currentVersion = packageJson.version || '1.8.0';
110
+ await pluginInstallModel.createIfMissing(currentVersion);
111
+
112
+ // 将模型实例注入到 ctx 中,供 Handler 使用
113
+ ctx.provide('conversationModel', conversationModel);
114
+ ctx.provide('messageModel', messageModel);
115
+ ctx.provide('rateLimitRecordModel', rateLimitRecordModel);
116
+ ctx.provide('aiConfigModel', aiConfigModel);
117
+ ctx.provide('jailbreakLogModel', jailbreakLogModel);
118
+ ctx.provide('versionCacheModel', versionCacheModel);
119
+ ctx.provide('pluginInstallModel', pluginInstallModel);
120
+
121
+ // 初始化版本服务
122
+ const versionService = new VersionService(versionCacheModel);
123
+ ctx.provide('versionService', versionService);
124
+
125
+ // 初始化遥测服务(延迟 5 秒启动,避免阻塞插件加载)
126
+ const telemetryService = new TelemetryService(pluginInstallModel, conversationModel);
127
+ setTimeout(() => {
128
+ telemetryService.init().catch(err => {
129
+ console.error('[AI-Helper] Telemetry service initialization failed:', err);
130
+ });
131
+ }, 5000);
132
+
133
+ // 注册测试路由
134
+ // GET /ai-helper/hello - 返回插件状态
135
+ ctx.Route('ai_helper_hello', '/ai-helper/hello', HelloHandler, HelloHandlerPriv);
136
+
137
+ // 注册学生端对话路由 (支持域前缀)
138
+ // POST /ai-helper/chat - 学生提交问题获得 AI 回答
139
+ ctx.Route('ai_helper_chat', '/ai-helper/chat', ChatHandler, ChatHandlerPriv);
140
+ // 域前缀路由: /d/:domainId/ai-helper/chat
141
+ ctx.Route('ai_helper_chat_domain', '/d/:domainId/ai-helper/chat', ChatHandler, ChatHandlerPriv);
142
+
143
+ // GET /ai-helper/problem-status/:problemId - 查询用户在该题的提交状态(是否已 AC)
144
+ ctx.Route('ai_helper_problem_status', '/ai-helper/problem-status/:problemId', ProblemStatusHandler, ProblemStatusHandlerPriv);
145
+ ctx.Route('ai_helper_problem_status_domain', '/d/:domainId/ai-helper/problem-status/:problemId', ProblemStatusHandler, ProblemStatusHandlerPriv);
146
+
147
+ // 注册教师端路由 (支持域前缀)
148
+ // 当前设计:AI 学习助手对话统计非常敏感,仅允许 root 访问。
149
+ // 注意:这里使用的是 root-only 的系统权限(PRIV.PRIV_EDIT_SYSTEM),普通老师也无权访问。
150
+ // TODO(如需求变更): 未来若有专门的教师统计角色,再考虑降低权限。
151
+
152
+ // 注入控制面板菜单项(统一入口)
153
+ ctx.injectUI('ControlPanel', 'ai_helper');
154
+
155
+ // AI 助手统一入口路由
156
+ ctx.Route('ai_helper', '/ai-helper', AIHelperDashboardHandler, AIHelperDashboardHandlerPriv);
157
+ ctx.Route('ai_helper_domain', '/d/:domainId/ai-helper', AIHelperDashboardHandler, AIHelperDashboardHandlerPriv);
158
+
159
+ // GET /ai-helper/conversations - 获取对话列表
160
+ ctx.Route('ai_helper_conversations', '/ai-helper/conversations', ConversationListHandler, ConversationListHandlerPriv);
161
+ ctx.Route('ai_helper_conversations_domain', '/d/:domainId/ai-helper/conversations', ConversationListHandler, ConversationListHandlerPriv);
162
+
163
+ // GET /ai-helper/conversations/:id - 获取对话详情
164
+ ctx.Route('ai_helper_conversation_detail', '/ai-helper/conversations/:id', ConversationDetailHandler, ConversationDetailHandlerPriv);
165
+ ctx.Route('ai_helper_conversation_detail_domain', '/d/:domainId/ai-helper/conversations/:id', ConversationDetailHandler, ConversationDetailHandlerPriv);
166
+
167
+ // GET /ai-helper/analytics - AI 使用统计页面
168
+ ctx.Route('ai_helper_analytics', '/ai-helper/analytics', AnalyticsHandler, AnalyticsHandlerPriv);
169
+ ctx.Route('ai_helper_analytics_domain', '/d/:domainId/ai-helper/analytics', AnalyticsHandler, AnalyticsHandlerPriv);
170
+
171
+ // GET /ai-helper/admin/config - AI 配置页面 & JSON API(通过 Accept 头区分)
172
+ ctx.Route('ai_helper_admin_config', '/ai-helper/admin/config', AdminConfigHandler, AdminConfigHandlerPriv);
173
+
174
+ // GET /ai-helper/export - 数据导出 API
175
+ ctx.Route('ai_helper_export', '/ai-helper/export', ExportHandler, ExportHandlerPriv);
176
+ ctx.Route('ai_helper_export_domain', '/d/:domainId/ai-helper/export', ExportHandler, ExportHandlerPriv);
177
+
178
+ // POST /ai-helper/admin/test-connection - 测试连接
179
+ ctx.Route('ai_helper_admin_test_connection', '/ai-helper/admin/test-connection', TestConnectionHandler, TestConnectionHandlerPriv);
180
+
181
+ // POST /ai-helper/admin/fetch-models - 获取可用模型列表
182
+ ctx.Route('ai_helper_admin_fetch_models', '/ai-helper/admin/fetch-models', FetchModelsHandler, FetchModelsHandlerPriv);
183
+
184
+ // T052: GET /ai-helper/version/check - 版本检测
185
+ ctx.Route('ai_helper_version_check', '/ai-helper/version/check', VersionCheckHandler, VersionCheckHandlerPriv);
186
+ ctx.Route('ai_helper_version_check_domain', '/d/:domainId/ai-helper/version/check', VersionCheckHandler, VersionCheckHandlerPriv);
187
+
188
+ // 插件更新路由
189
+ // GET /ai-helper/admin/update/info - 获取更新信息
190
+ ctx.Route('ai_helper_update_info', '/ai-helper/admin/update/info', UpdateInfoHandler, UpdateInfoHandlerPriv);
191
+ // POST /ai-helper/admin/update - 执行更新
192
+ ctx.Route('ai_helper_update', '/ai-helper/admin/update', UpdateHandler, UpdateHandlerPriv);
193
+ }
194
+ });
195
+
196
+ export const Config = configSchema;
197
+ export const apply = aiHelperPlugin.apply;
198
+ export default aiHelperPlugin;