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,351 @@
1
+ /**
2
+ * Version Service - 版本检测服务
3
+ *
4
+ * 提供插件版本检测功能:
5
+ * - 获取当前安装版本
6
+ * - 同时从 Gitee 和 GitHub 仓库检查最新版本
7
+ * - 取两个仓库中更新的版本为准
8
+ * - 语义化版本比较
9
+ * - 24 小时缓存机制
10
+ */
11
+
12
+ import axios from 'axios';
13
+ import { VersionCacheModel, type VersionCache, VERSION_CACHE_TTL_MS } from '../models/versionCache';
14
+
15
+ /**
16
+ * 仓库配置(按优先级排序)
17
+ */
18
+ const REPO_CONFIGS = [
19
+ {
20
+ name: 'Gitee',
21
+ owner: 'alture',
22
+ repo: 'hydro-ai-helper',
23
+ branch: 'main',
24
+ packageJsonUrl: 'https://gitee.com/alture/hydro-ai-helper/raw/main/package.json',
25
+ releasesUrl: 'https://gitee.com/alture/hydro-ai-helper/releases'
26
+ },
27
+ {
28
+ name: 'GitHub',
29
+ owner: 'AltureT',
30
+ repo: 'hydro-ai-helper',
31
+ branch: 'main',
32
+ packageJsonUrl: 'https://raw.githubusercontent.com/AltureT/hydro-ai-helper/main/package.json',
33
+ releasesUrl: 'https://github.com/AltureT/hydro-ai-helper/releases'
34
+ }
35
+ ];
36
+
37
+ /**
38
+ * 单个仓库的版本信息
39
+ */
40
+ interface RepoVersionInfo {
41
+ name: string;
42
+ version: string;
43
+ releasesUrl: string;
44
+ latency: number;
45
+ }
46
+
47
+ /**
48
+ * 版本检查结果接口
49
+ */
50
+ export interface VersionCheckResult {
51
+ currentVersion: string;
52
+ latestVersion: string;
53
+ hasUpdate: boolean;
54
+ releaseUrl: string;
55
+ releaseNotes?: string;
56
+ checkedAt: Date;
57
+ fromCache: boolean;
58
+ source?: string; // 版本来源(Gitee/GitHub)
59
+ }
60
+
61
+ /**
62
+ * 版本服务类
63
+ */
64
+ export class VersionService {
65
+ private versionCacheModel: VersionCacheModel;
66
+ private currentVersion: string;
67
+ private static CACHE_KEY = 'latest_version';
68
+
69
+ constructor(versionCacheModel: VersionCacheModel) {
70
+ this.versionCacheModel = versionCacheModel;
71
+ // 从 package.json 读取当前版本(编译时嵌入)
72
+ this.currentVersion = this.readCurrentVersion();
73
+ }
74
+
75
+ /**
76
+ * T047: 获取当前安装版本
77
+ * @returns 当前版本号字符串
78
+ */
79
+ getCurrentVersion(): string {
80
+ return this.currentVersion;
81
+ }
82
+
83
+ /**
84
+ * 从 package.json 读取当前版本
85
+ * 在运行时使用 require 读取
86
+ */
87
+ private readCurrentVersion(): string {
88
+ try {
89
+ // 尝试从插件���录读取 package.json
90
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
91
+ const pkg = require('../../package.json');
92
+ return pkg.version || '0.0.0';
93
+ } catch (err) {
94
+ console.error('[VersionService] Failed to read package.json:', err);
95
+ return '0.0.0';
96
+ }
97
+ }
98
+
99
+ /**
100
+ * 从单个仓库获取版本信息
101
+ * @param config 仓库配置
102
+ * @returns 版本信息或 null(失败时)
103
+ */
104
+ private async fetchVersionFromRepo(config: typeof REPO_CONFIGS[0]): Promise<RepoVersionInfo | null> {
105
+ const startTime = Date.now();
106
+ try {
107
+ const response = await axios.get(config.packageJsonUrl, {
108
+ timeout: 10000,
109
+ headers: {
110
+ 'Accept': 'application/json'
111
+ }
112
+ });
113
+
114
+ const latency = Date.now() - startTime;
115
+ const packageJson = response.data;
116
+
117
+ let version: string;
118
+ if (typeof packageJson === 'object' && packageJson.version) {
119
+ version = packageJson.version;
120
+ } else if (typeof packageJson === 'string') {
121
+ const parsed = JSON.parse(packageJson);
122
+ version = parsed.version || '0.0.0';
123
+ } else {
124
+ return null;
125
+ }
126
+
127
+ return {
128
+ name: config.name,
129
+ version,
130
+ releasesUrl: config.releasesUrl,
131
+ latency
132
+ };
133
+ } catch (err) {
134
+ console.error(`[VersionService] Failed to fetch from ${config.name}:`, err);
135
+ return null;
136
+ }
137
+ }
138
+
139
+ /**
140
+ * 从所有仓库获取版本,取最新的
141
+ * @returns 最新版本信息
142
+ */
143
+ private async fetchLatestVersionFromAllRepos(): Promise<{
144
+ version: string;
145
+ source: string;
146
+ releasesUrl: string;
147
+ }> {
148
+ console.log('[VersionService] Checking versions from all repositories...');
149
+
150
+ // 并行获取所有仓库的版本
151
+ const results = await Promise.all(
152
+ REPO_CONFIGS.map(config => this.fetchVersionFromRepo(config))
153
+ );
154
+
155
+ // 过滤掉失败的结果
156
+ const validResults = results.filter((r): r is RepoVersionInfo => r !== null);
157
+
158
+ if (validResults.length === 0) {
159
+ throw new Error('All repositories failed to respond');
160
+ }
161
+
162
+ // 打印获取到的版本
163
+ for (const result of validResults) {
164
+ console.log(`[VersionService] ${result.name}: v${result.version} (${result.latency}ms)`);
165
+ }
166
+
167
+ // 找出最新版本
168
+ let latest = validResults[0];
169
+ for (let i = 1; i < validResults.length; i++) {
170
+ if (this.isNewerVersion(validResults[i].version, latest.version)) {
171
+ latest = validResults[i];
172
+ }
173
+ }
174
+
175
+ console.log(`[VersionService] Latest version: v${latest.version} from ${latest.name}`);
176
+
177
+ return {
178
+ version: latest.version,
179
+ source: latest.name,
180
+ releasesUrl: latest.releasesUrl
181
+ };
182
+ }
183
+
184
+ /**
185
+ * T048: 从仓库检查最新版本
186
+ * @param forceRefresh 是否强制刷新(忽略缓存)
187
+ * @returns 版本检查结果
188
+ */
189
+ async checkForUpdates(forceRefresh = false): Promise<VersionCheckResult> {
190
+ // T050: 检查缓存
191
+ if (!forceRefresh) {
192
+ const cached = await this.getCachedVersion();
193
+ if (cached) {
194
+ return {
195
+ currentVersion: this.currentVersion,
196
+ latestVersion: cached.latestVersion,
197
+ hasUpdate: cached.hasUpdate,
198
+ releaseUrl: cached.releaseUrl || REPO_CONFIGS[0].releasesUrl,
199
+ releaseNotes: cached.releaseNotes,
200
+ checkedAt: cached.checkedAt,
201
+ fromCache: true,
202
+ source: (cached as any).source
203
+ };
204
+ }
205
+ }
206
+
207
+ // 从所有仓库获取最新版本
208
+ try {
209
+ const { version: latestVersion, source, releasesUrl } = await this.fetchLatestVersionFromAllRepos();
210
+ const hasUpdate = this.isNewerVersion(latestVersion, this.currentVersion);
211
+
212
+ // 更新缓存
213
+ await this.updateCache({
214
+ currentVersion: this.currentVersion,
215
+ latestVersion,
216
+ hasUpdate,
217
+ releaseUrl: releasesUrl,
218
+ source
219
+ });
220
+
221
+ return {
222
+ currentVersion: this.currentVersion,
223
+ latestVersion,
224
+ hasUpdate,
225
+ releaseUrl: releasesUrl,
226
+ checkedAt: new Date(),
227
+ fromCache: false,
228
+ source
229
+ };
230
+ } catch (err) {
231
+ console.error('[VersionService] Failed to check for updates:', err);
232
+
233
+ // 如果请求失败,尝试返回过期的缓存数据
234
+ const expiredCache = await this.versionCacheModel.get(VersionService.CACHE_KEY);
235
+ if (expiredCache) {
236
+ return {
237
+ currentVersion: this.currentVersion,
238
+ latestVersion: expiredCache.latestVersion,
239
+ hasUpdate: expiredCache.hasUpdate,
240
+ releaseUrl: expiredCache.releaseUrl || REPO_CONFIGS[0].releasesUrl,
241
+ checkedAt: expiredCache.checkedAt,
242
+ fromCache: true,
243
+ source: (expiredCache as any).source
244
+ };
245
+ }
246
+
247
+ // 完全失败时返回无更新
248
+ return {
249
+ currentVersion: this.currentVersion,
250
+ latestVersion: this.currentVersion,
251
+ hasUpdate: false,
252
+ releaseUrl: REPO_CONFIGS[0].releasesUrl,
253
+ checkedAt: new Date(),
254
+ fromCache: false
255
+ };
256
+ }
257
+ }
258
+
259
+ /**
260
+ * T049: 语义化版本比较
261
+ * 判断 version1 是否比 version2 新
262
+ * @param version1 待比较版本
263
+ * @param version2 基准版本
264
+ * @returns true 如果 version1 > version2
265
+ */
266
+ isNewerVersion(version1: string, version2: string): boolean {
267
+ const v1Parts = this.parseVersion(version1);
268
+ const v2Parts = this.parseVersion(version2);
269
+
270
+ for (let i = 0; i < 3; i++) {
271
+ if (v1Parts[i] > v2Parts[i]) return true;
272
+ if (v1Parts[i] < v2Parts[i]) return false;
273
+ }
274
+
275
+ return false; // 版本相同
276
+ }
277
+
278
+ /**
279
+ * 解析版本号为数字数组
280
+ * @param version 版本字符串(如 "1.2.3")
281
+ * @returns [major, minor, patch]
282
+ */
283
+ private parseVersion(version: string): [number, number, number] {
284
+ // 移除前缀 v 或 V
285
+ const clean = version.replace(/^[vV]/, '');
286
+
287
+ // 移除预发布标签(如 -beta.1)
288
+ const base = clean.split('-')[0];
289
+
290
+ // 解析主版本号
291
+ const parts = base.split('.').map(p => parseInt(p, 10) || 0);
292
+
293
+ return [
294
+ parts[0] || 0,
295
+ parts[1] || 0,
296
+ parts[2] || 0
297
+ ];
298
+ }
299
+
300
+ /**
301
+ * T050: 获取缓存的版本信息(24小时有效)
302
+ * @returns 缓存数据或 null
303
+ */
304
+ private async getCachedVersion(): Promise<VersionCache | null> {
305
+ const cache = await this.versionCacheModel.get(VersionService.CACHE_KEY);
306
+
307
+ if (!cache) {
308
+ return null;
309
+ }
310
+
311
+ // 检查缓存是否在 TTL 内
312
+ const now = new Date().getTime();
313
+ const checkedAt = new Date(cache.checkedAt).getTime();
314
+
315
+ if (now - checkedAt > VERSION_CACHE_TTL_MS) {
316
+ return null; // 缓存过期
317
+ }
318
+
319
+ return cache;
320
+ }
321
+
322
+ /**
323
+ * 更新版本缓存
324
+ * @param data 版本数据
325
+ */
326
+ private async updateCache(data: {
327
+ currentVersion: string;
328
+ latestVersion: string;
329
+ hasUpdate: boolean;
330
+ releaseUrl?: string;
331
+ releaseNotes?: string;
332
+ source?: string;
333
+ }): Promise<void> {
334
+ await this.versionCacheModel.set({
335
+ key: VersionService.CACHE_KEY,
336
+ currentVersion: data.currentVersion,
337
+ latestVersion: data.latestVersion,
338
+ hasUpdate: data.hasUpdate,
339
+ releaseUrl: data.releaseUrl,
340
+ releaseNotes: data.releaseNotes,
341
+ checkedAt: new Date()
342
+ });
343
+ }
344
+
345
+ /**
346
+ * 清除版本缓存(用于测试或强制刷新)
347
+ */
348
+ async clearCache(): Promise<void> {
349
+ await this.versionCacheModel.delete(VersionService.CACHE_KEY);
350
+ }
351
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * HydroOJ 扩展类型定义
3
+ * 补充 hydrooj 包中未完整导出的类型
4
+ */
5
+
6
+ declare module 'hydrooj' {
7
+ /**
8
+ * 题目文档接口(简化版)
9
+ */
10
+ export interface ProblemDoc {
11
+ _id: import('mongodb').ObjectId;
12
+ docType: 10;
13
+ docId: number;
14
+ pid: string;
15
+ domainId: string;
16
+ title: string;
17
+ content: string | Record<string, string>;
18
+ owner: number;
19
+ nSubmit?: number;
20
+ nAccept?: number;
21
+ difficulty?: number;
22
+ tag?: string[];
23
+ hidden?: boolean;
24
+ html?: boolean;
25
+ }
26
+
27
+ /**
28
+ * ProblemModel 类型定义
29
+ */
30
+ export const ProblemModel: {
31
+ /**
32
+ * 获取题目
33
+ * @param domainId 域 ID
34
+ * @param pid 题目 ID(数字或字符串)
35
+ * @param projection 投影字段(可选)
36
+ */
37
+ get(
38
+ domainId: string,
39
+ pid: string | number,
40
+ projection?: (keyof ProblemDoc)[]
41
+ ): Promise<ProblemDoc | null>;
42
+
43
+ /**
44
+ * 检查用户是否可以查看题目
45
+ */
46
+ canViewBy(pdoc: ProblemDoc, user: { _id: number }): boolean;
47
+
48
+ /** 公开字段投影 */
49
+ PROJECTION_PUBLIC: (keyof ProblemDoc)[];
50
+ /** 列表字段投影 */
51
+ PROJECTION_LIST: (keyof ProblemDoc)[];
52
+ /** 基础字段投影 */
53
+ PROJECTION_BASE: (keyof ProblemDoc)[];
54
+ };
55
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * HydroOJ 扩展类型定义
3
+ * 用于补充 HydroOJ 未导出的内部类型
4
+ */
5
+
6
+ import type { Handler } from 'hydrooj';
7
+
8
+ /**
9
+ * HydroOJ Problem 文档接口
10
+ */
11
+ export interface ProblemDocument {
12
+ docId: number;
13
+ domainId: string;
14
+ docType: number;
15
+ title?: string;
16
+ }
17
+
18
+ /**
19
+ * HydroOJ User 文档接口
20
+ */
21
+ export interface UserDocument {
22
+ _id: number;
23
+ uname?: string;
24
+ }
25
+
26
+ /**
27
+ * Handler 的 domain 上下文接口
28
+ */
29
+ export interface DomainContext {
30
+ _id: string;
31
+ }
32
+
33
+ /**
34
+ * 带有 domain 上下文的 Handler 类型
35
+ */
36
+ export interface HandlerWithDomain extends Handler {
37
+ domain?: DomainContext;
38
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * MongoDB 查询类型定义
3
+ * 为 MongoDB 查询和更新操作提供更精确的类型
4
+ */
5
+
6
+ import type { Filter, UpdateFilter } from 'mongodb';
7
+
8
+ /**
9
+ * MongoDB 查询条件类型
10
+ * 用于动态构建查询条件
11
+ */
12
+ export type MongoQuery<T> = Filter<T>;
13
+
14
+ /**
15
+ * MongoDB 更新操作类型
16
+ * 用于动态构建更新操作
17
+ */
18
+ export type MongoUpdate<T> = UpdateFilter<T>;
19
+
20
+ /**
21
+ * MongoDB 聚合管道阶段类型
22
+ */
23
+ export type MongoPipelineStage = Record<string, unknown>;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * 域辅助工具函数
3
+ */
4
+
5
+ import type { Handler } from 'hydrooj';
6
+ import type { HandlerWithDomain } from '../types/hydrooj';
7
+
8
+ /**
9
+ * 从 Handler 获取当前域 ID
10
+ * @param handler Handler 实例
11
+ * @returns 域 ID,默认为 'system'
12
+ */
13
+ export function getDomainId(handler: Handler): string {
14
+ const h = handler as HandlerWithDomain;
15
+ return handler.args?.domainId || h.domain?._id || 'system';
16
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * MongoDB helpers
3
+ *
4
+ * Use the same mongodb/bson version that HydroOJ runtime is using to avoid
5
+ * BSON major-version mismatches when converting ids.
6
+ */
7
+ import { createRequire } from 'module';
8
+
9
+ // Resolve modules relative to the HydroOJ installation (the host runtime),
10
+ // so we always share the same mongodb / bson implementation.
11
+ const requireFromHydro = createRequire(require.resolve('hydrooj'));
12
+ const mongodb = requireFromHydro('mongodb') as typeof import('mongodb');
13
+
14
+ export const ObjectId = mongodb.ObjectId;
15
+ export type ObjectIdType = import('mongodb').ObjectId;
@@ -0,0 +1,8 @@
1
+ {% extends "layout/basic.html" %}
2
+
3
+ {% block content %}
4
+ <div class="container">
5
+ <!-- React 配置页面组件挂载点 -->
6
+ <div id="ai-helper-admin-config-root"></div>
7
+ </div>
8
+ {% endblock %}
@@ -0,0 +1,8 @@
1
+ {% extends "layout/basic.html" %}
2
+
3
+ {% block content %}
4
+ <div class="container">
5
+ <!-- React 统计页面组件挂载点 -->
6
+ <div id="ai-helper-analytics-root"></div>
7
+ </div>
8
+ {% endblock %}
@@ -0,0 +1,8 @@
1
+ {% extends "layout/basic.html" %}
2
+
3
+ {% block content %}
4
+ <div class="container">
5
+ <!-- AI 助手统一入口 React 组件挂载点 -->
6
+ <div id="ai-helper-dashboard-root"></div>
7
+ </div>
8
+ {% endblock %}
@@ -0,0 +1,8 @@
1
+ {% extends "layout/basic.html" %}
2
+
3
+ {% block content %}
4
+ <div class="container">
5
+ <!-- React 详情组件挂载点 -->
6
+ <div id="ai-helper-conversation-detail-root"></div>
7
+ </div>
8
+ {% endblock %}
@@ -0,0 +1,8 @@
1
+ {% extends "layout/basic.html" %}
2
+
3
+ {% block content %}
4
+ <div class="container">
5
+ <!-- React 列表组件挂载点 -->
6
+ <div id="ai-helper-conversations-root"></div>
7
+ </div>
8
+ {% endblock %}
@@ -0,0 +1,86 @@
1
+ HydroOJ AI Helper 集成测试说明
2
+ ========================
3
+
4
+ 本文件提供一套基于 `curl` 的接口级测试脚本,覆盖「管理员配置 → 学生提问 → 数据持久化 → 教师查看/导出/统计」核心链路,无需额外测试框架。
5
+
6
+ 前置条件
7
+ --------
8
+ - HydroOJ 已安装并运行,且已加载本插件并执行 `npm run build`。
9
+ - 通过环境变量或 `.env` 配置了 `ENCRYPTION_KEY`(用于保存 AI Key)。
10
+ - 至少有一个具备 `PRIV.PRIV_EDIT_SYSTEM` 的管理员账号(教师端接口当前也要求此权限)。
11
+ - 准备若干题目与测试用户(学生/教师)。账号需已登录,以便获取 Cookie。
12
+ - 可用的 AI 配置(真实或假 API Key + 可用的 base URL/mock 服务)。请勿将真实 Key 写入仓库。
13
+ - 本地具备 `curl` 和 `python3`(仅用于解析 JSON 并保存 `conversationId`)。
14
+
15
+ 获取登录 Cookie
16
+ ---------------
17
+ 1. 使用浏览器登录 HydroOJ,对应账号分别获取 `sid=...` Cookie(管理员/教师/学生)。
18
+ 2. 在浏览器开发者工具的 Application/Storage 中复制 Cookie;或在请求头里复制整段 `Cookie: sid=xxxxx`。
19
+ 3. 后续脚本通过环境变量读取这些 Cookie。
20
+
21
+ 快速开始
22
+ --------
23
+ 在仓库根目录执行:
24
+
25
+ ```bash
26
+ cd tests/integration/curl
27
+ chmod +x *.sh
28
+
29
+ # 必需环境变量
30
+ export HYDRO_BASE_URL="http://127.0.0.1"
31
+ export HYDRO_ADMIN_COOKIE="sid=admin-cookie"
32
+ # 教师端接口目前同样需要系统权限,如无单独教师账号可复用管理员 Cookie
33
+ export HYDRO_TEACHER_COOKIE="sid=teacher-cookie"
34
+ export HYDRO_STUDENT_COOKIE="sid=student-cookie"
35
+
36
+ # 可选:AI 配置占位值
37
+ export HYDRO_AI_API_BASE_URL="https://api.openai.com/v1"
38
+ export HYDRO_AI_MODEL_NAME="gpt-3.5-turbo"
39
+ export HYDRO_AI_API_KEY="sk-REPLACE-ME"
40
+ export HYDRO_AI_RATE_LIMIT=5
41
+ export HYDRO_AI_TIMEOUT=30
42
+
43
+ # 学生端默认频率限制为每分钟 5 次,跑示例时可在脚本间留出时间或临时调高
44
+ # `HYDRO_AI_RATE_LIMIT` 配置,以避免意外触发 429。
45
+
46
+ # 依次执行核心链路
47
+ ./01-admin-config-set.sh
48
+ ./02-student-chat.sh
49
+ ./03-teacher-list.sh
50
+ ./04-teacher-detail.sh
51
+ ./05-export.sh
52
+ ./06-analytics.sh
53
+ ```
54
+
55
+ - `02-student-chat.sh` 会把最新的 `conversationId` 写入 `.conversation_id`,供后续脚本复用;也可通过 `HYDRO_CONVERSATION_ID` 覆盖。
56
+ - 如需重新获取 Cookie 或更换 base URL,请重新导出环境变量后再运行脚本。
57
+
58
+ 测试脚本一览
59
+ ------------
60
+
61
+ 脚本 | 场景 | 涉及接口 | 预期结果
62
+ ---- | ---- | -------- | -------
63
+ 01-admin-config-set.sh | 管理员设置 AI 配置并测试连接 | PUT /ai-helper/admin/config;POST /ai-helper/admin/test-connection | 200,返回配置摘要;测试连接返回 `success: true/false` 消息
64
+ 02-student-chat.sh | 学生发起对话(含 questionType 与可选代码) | POST /ai-helper/chat | 200,返回 `conversationId` 与 `message.content`;写入 `.conversation_id`
65
+ 03-teacher-list.sh | 教师查看对话列表,验证筛选参数 | GET /ai-helper/conversations | 200,包含列表与分页信息,可看到刚创建的会话;错误日期格式应返回 400
66
+ 04-teacher-detail.sh | 教师查看对话详情 | GET /ai-helper/conversations/{id} | 200,返回会话元数据与消息数组
67
+ 05-export.sh | 导出 CSV | GET /ai-helper/export | 200,`Content-Type: text/csv`,生成的 CSV 文件非空
68
+ 06-analytics.sh | 查看统计数据 | GET /ai-helper/analytics | 200,返回 `dimension` 对应的统计结构;非法 dimension 返回 400
69
+
70
+ 判定通过/失败
71
+ -------------
72
+ - HTTP 状态码:脚本中默认要求 200(部分错误场景除外),否则退出并打印响应体。
73
+ - 核心字段检查:
74
+ - 配置:返回体含 `config` 字段;`test-connection` 返回 `success` 与 `message`。
75
+ - 学生对话:返回 `conversationId`、`message.content` 非空;若触发频率限制会返回 429(可作为附加验证)。
76
+ - 列表/详情:列表返回 `conversations` 数组、`total`;详情返回 `conversation` 与 `messages` 数组。
77
+ - 导出:输出文件非空且首行包含 CSV 头;若 `format` 非 `csv` 应返回 400。
78
+ - 统计:返回 `dimension` 与 `items` 数组;若缺失 `dimension` 参数(且请求 Accept JSON),应返回 400。
79
+ - 权限异常:不带 Cookie 或使用学生 Cookie 访问教师/管理员接口应返回 403/401(可手动去掉 Cookie 重试验证)。
80
+ - 时间范围参数:传入明显非法日期(示例见 03/05/06 脚本中的 `INVALID_DATE` 示例)应返回 400。
81
+
82
+ 附加说明
83
+ --------
84
+ - 真实测试时请替换 `HYDRO_AI_API_KEY` 为有效 Key 或指向可用的 mock 服务;切勿将真实 Key 保存在仓库。
85
+ - 如果已存在历史会话,可设置 `HYDRO_CONVERSATION_ID` 跳过新建对话。
86
+ - 默认频率限制(学生端)为每分钟 5 次,建议控制调用节奏或调高测试环境限额,避免出现 429。
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env bash
2
+ # 管理员配置 AI 服务并执行连接测试
3
+ set -euo pipefail
4
+
5
+ BASE_URL="${HYDRO_BASE_URL:-http://127.0.0.1}"
6
+ COOKIE="${HYDRO_ADMIN_COOKIE:?HYDRO_ADMIN_COOKIE not set. Please export HYDRO_ADMIN_COOKIE=\"sid=...\"}"
7
+
8
+ API_BASE_URL="${HYDRO_AI_API_BASE_URL:-https://api.openai.com/v1}"
9
+ MODEL_NAME="${HYDRO_AI_MODEL_NAME:-gpt-3.5-turbo}"
10
+ API_KEY="${HYDRO_AI_API_KEY:-sk-REPLACE-ME}"
11
+ RATE_LIMIT="${HYDRO_AI_RATE_LIMIT:-5}"
12
+ TIMEOUT="${HYDRO_AI_TIMEOUT:-30}"
13
+ SYSTEM_PROMPT="${HYDRO_AI_SYSTEM_PROMPT:-You are a helpful teaching assistant.}"
14
+
15
+ echo "[T026] 01-admin-config-set: set config & test connection"
16
+ echo "Using BASE_URL=${BASE_URL}"
17
+
18
+ CONFIG_PAYLOAD=$(cat <<EOF
19
+ {
20
+ "apiBaseUrl": "$API_BASE_URL",
21
+ "modelName": "$MODEL_NAME",
22
+ "apiKey": "$API_KEY",
23
+ "rateLimitPerMinute": $RATE_LIMIT,
24
+ "timeoutSeconds": $TIMEOUT,
25
+ "systemPromptTemplate": "$SYSTEM_PROMPT"
26
+ }
27
+ EOF
28
+ )
29
+
30
+ echo "--- Updating config ---"
31
+ response=$(curl -sS -w "\nHTTP_STATUS:%{http_code}" -X PUT "$BASE_URL/ai-helper/admin/config" \
32
+ -H "Cookie: $COOKIE" \
33
+ -H "Content-Type: application/json" \
34
+ -d "$CONFIG_PAYLOAD")
35
+
36
+ body=$(printf "%s" "$response" | sed '/HTTP_STATUS:/d')
37
+ status=$(printf "%s" "$response" | sed -n 's/.*HTTP_STATUS://p')
38
+ echo "$body"
39
+ if [ "$status" != "200" ]; then
40
+ echo "Unexpected status while updating config: $status" >&2
41
+ exit 1
42
+ fi
43
+
44
+ echo "--- Test connection ---"
45
+ test_resp=$(curl -sS -w "\nHTTP_STATUS:%{http_code}" -X POST "$BASE_URL/ai-helper/admin/test-connection" \
46
+ -H "Cookie: $COOKIE")
47
+
48
+ test_body=$(printf "%s" "$test_resp" | sed '/HTTP_STATUS:/d')
49
+ test_status=$(printf "%s" "$test_resp" | sed -n 's/.*HTTP_STATUS://p')
50
+ echo "$test_body"
51
+ if [ "$test_status" != "200" ]; then
52
+ echo "Unexpected status while testing connection: $test_status" >&2
53
+ exit 1
54
+ fi
55
+
56
+ echo "[Done] admin config updated and connection tested."