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,427 @@
1
+ /**
2
+ * 管理员配置 Handler
3
+ * 处理管理员配置 AI 服务的请求
4
+ */
5
+
6
+ import { Handler, PRIV } from 'hydrooj';
7
+ import { AIConfigModel, AIConfig } from '../models/aiConfig';
8
+ import { encrypt, decrypt, maskApiKey } from '../lib/crypto';
9
+ import { OpenAIClient, fetchAvailableModels } from '../services/openaiClient';
10
+ import { builtinJailbreakPatternSources } from '../constants/jailbreakRules';
11
+ import { JailbreakLogModel } from '../models/jailbreakLog';
12
+ import type { JailbreakLog } from '../models/jailbreakLog';
13
+
14
+ /**
15
+ * 更新配置请求接口
16
+ */
17
+ interface UpdateConfigRequest {
18
+ apiBaseUrl?: string;
19
+ modelName?: string;
20
+ apiKey?: string; // 新的明文 API Key,若为空字符串则不修改
21
+ rateLimitPerMinute?: number;
22
+ timeoutSeconds?: number;
23
+ systemPromptTemplate?: string;
24
+ extraJailbreakPatternsText?: string;
25
+ }
26
+
27
+ /**
28
+ * GetConfigHandler - 获取当前配置
29
+ * GET /ai-helper/admin/config
30
+ */
31
+ export class GetConfigHandler extends Handler {
32
+ async get() {
33
+ try {
34
+ const aiConfigModel: AIConfigModel = this.ctx.get('aiConfigModel');
35
+ const jailbreakLogModel: JailbreakLogModel = this.ctx.get('jailbreakLogModel');
36
+
37
+ // 读取配置
38
+ const config = await aiConfigModel.getConfig();
39
+ const recentLogs = await jailbreakLogModel.listRecent(20);
40
+
41
+ if (!config) {
42
+ // 尚未配置,返回 null
43
+ this.response.body = {
44
+ config: null,
45
+ builtinJailbreakPatterns: builtinJailbreakPatternSources,
46
+ recentJailbreakLogs: recentLogs.map(formatJailbreakLog)
47
+ };
48
+ this.response.type = 'application/json';
49
+ return;
50
+ }
51
+
52
+ // 解密 API Key 并脱敏
53
+ let apiKeyMasked = '';
54
+ let hasApiKey = false;
55
+
56
+ try {
57
+ if (config.apiKeyEncrypted) {
58
+ const apiKeyPlain = decrypt(config.apiKeyEncrypted);
59
+ apiKeyMasked = maskApiKey(apiKeyPlain);
60
+ hasApiKey = true;
61
+ }
62
+ } catch (err) {
63
+ // 解密失败,视为无有效 API Key
64
+ console.error('[GetConfigHandler] API Key 解密失败:', err);
65
+ hasApiKey = false;
66
+ }
67
+
68
+ // 构造响应(不包含 apiKeyEncrypted 和明文 API Key)
69
+ this.response.body = {
70
+ config: {
71
+ apiBaseUrl: config.apiBaseUrl,
72
+ modelName: config.modelName,
73
+ rateLimitPerMinute: config.rateLimitPerMinute,
74
+ timeoutSeconds: config.timeoutSeconds,
75
+ systemPromptTemplate: config.systemPromptTemplate,
76
+ extraJailbreakPatternsText: config.extraJailbreakPatternsText || '',
77
+ apiKeyMasked,
78
+ hasApiKey,
79
+ updatedAt: config.updatedAt.toISOString()
80
+ },
81
+ builtinJailbreakPatterns: builtinJailbreakPatternSources,
82
+ recentJailbreakLogs: recentLogs.map(formatJailbreakLog)
83
+ };
84
+ this.response.type = 'application/json';
85
+
86
+ } catch (err) {
87
+ console.error('[GetConfigHandler] Error:', err);
88
+ this.response.status = 500;
89
+ this.response.body = {
90
+ error: err instanceof Error ? err.message : '获取配置失败'
91
+ };
92
+ this.response.type = 'application/json';
93
+ }
94
+ }
95
+ }
96
+
97
+ /**
98
+ * UpdateConfigHandler - 更新配置
99
+ * PUT /ai-helper/admin/config
100
+ */
101
+ export class UpdateConfigHandler extends Handler {
102
+ async put() {
103
+ try {
104
+ const aiConfigModel: AIConfigModel = this.ctx.get('aiConfigModel');
105
+
106
+ // 读取请求体
107
+ const body = this.request.body as UpdateConfigRequest;
108
+
109
+ // 构造部分更新对象
110
+ const partial: Partial<Omit<AIConfig, '_id' | 'updatedAt'>> = {};
111
+
112
+ // 更新字段(如果提供)
113
+ if (body.apiBaseUrl !== undefined) {
114
+ partial.apiBaseUrl = body.apiBaseUrl.trim();
115
+ }
116
+
117
+ if (body.modelName !== undefined) {
118
+ partial.modelName = body.modelName.trim();
119
+ }
120
+
121
+ if (body.rateLimitPerMinute !== undefined) {
122
+ const rate = parseInt(String(body.rateLimitPerMinute), 10);
123
+ if (rate <= 0) {
124
+ this.response.status = 400;
125
+ this.response.body = {
126
+ error: 'rateLimitPerMinute 必须大于 0'
127
+ };
128
+ this.response.type = 'application/json';
129
+ return;
130
+ }
131
+ partial.rateLimitPerMinute = rate;
132
+ }
133
+
134
+ if (body.timeoutSeconds !== undefined) {
135
+ const timeout = parseInt(String(body.timeoutSeconds), 10);
136
+ if (timeout <= 0) {
137
+ this.response.status = 400;
138
+ this.response.body = {
139
+ error: 'timeoutSeconds 必须大于 0'
140
+ };
141
+ this.response.type = 'application/json';
142
+ return;
143
+ }
144
+ partial.timeoutSeconds = timeout;
145
+ }
146
+
147
+ if (body.systemPromptTemplate !== undefined) {
148
+ partial.systemPromptTemplate = body.systemPromptTemplate;
149
+ }
150
+
151
+ if (body.extraJailbreakPatternsText !== undefined) {
152
+ partial.extraJailbreakPatternsText = body.extraJailbreakPatternsText;
153
+ }
154
+
155
+ // 处理 API Key
156
+ if (body.apiKey !== undefined && body.apiKey !== '') {
157
+ // 提供了新的 API Key,加密后存储
158
+ try {
159
+ partial.apiKeyEncrypted = encrypt(body.apiKey.trim());
160
+ } catch (err) {
161
+ this.response.status = 500;
162
+ this.response.body = {
163
+ error: `API Key 加密失败: ${err instanceof Error ? err.message : String(err)}`
164
+ };
165
+ this.response.type = 'application/json';
166
+ return;
167
+ }
168
+ }
169
+ // 如果 apiKey 字段不存在或为空字符串,不修改现有 key
170
+
171
+ // 更新配置
172
+ const jailbreakLogModel: JailbreakLogModel = this.ctx.get('jailbreakLogModel');
173
+
174
+ await aiConfigModel.updateConfig(partial);
175
+
176
+ // 重新读取配置并返回
177
+ const updatedConfig = await aiConfigModel.getConfig();
178
+
179
+ if (!updatedConfig) {
180
+ throw new Error('配置更新后读取失败');
181
+ }
182
+
183
+ // 解密 API Key 并脱敏
184
+ let apiKeyMasked = '';
185
+ let hasApiKey = false;
186
+
187
+ try {
188
+ if (updatedConfig.apiKeyEncrypted) {
189
+ const apiKeyPlain = decrypt(updatedConfig.apiKeyEncrypted);
190
+ apiKeyMasked = maskApiKey(apiKeyPlain);
191
+ hasApiKey = true;
192
+ }
193
+ } catch (err) {
194
+ console.error('[UpdateConfigHandler] API Key 解密失败:', err);
195
+ hasApiKey = false;
196
+ }
197
+
198
+ // 返回更新后的配置
199
+ const recentLogs = await jailbreakLogModel.listRecent(20);
200
+
201
+ this.response.body = {
202
+ config: {
203
+ apiBaseUrl: updatedConfig.apiBaseUrl,
204
+ modelName: updatedConfig.modelName,
205
+ rateLimitPerMinute: updatedConfig.rateLimitPerMinute,
206
+ timeoutSeconds: updatedConfig.timeoutSeconds,
207
+ systemPromptTemplate: updatedConfig.systemPromptTemplate,
208
+ extraJailbreakPatternsText: updatedConfig.extraJailbreakPatternsText || '',
209
+ apiKeyMasked,
210
+ hasApiKey,
211
+ updatedAt: updatedConfig.updatedAt.toISOString()
212
+ },
213
+ builtinJailbreakPatterns: builtinJailbreakPatternSources,
214
+ recentJailbreakLogs: recentLogs.map(formatJailbreakLog)
215
+ };
216
+ this.response.type = 'application/json';
217
+
218
+ } catch (err) {
219
+ console.error('[UpdateConfigHandler] Error:', err);
220
+ this.response.status = 500;
221
+ this.response.body = {
222
+ error: err instanceof Error ? err.message : '更新配置失败'
223
+ };
224
+ this.response.type = 'application/json';
225
+ }
226
+ }
227
+ }
228
+
229
+ /**
230
+ * TestConnectionHandler - 测试 AI 服务连接
231
+ * POST /ai-helper/admin/test-connection
232
+ */
233
+ export class TestConnectionHandler extends Handler {
234
+ async post() {
235
+ try {
236
+ const aiConfigModel: AIConfigModel = this.ctx.get('aiConfigModel');
237
+
238
+ // 读取当前配置
239
+ const config = await aiConfigModel.getConfig();
240
+
241
+ if (!config) {
242
+ this.response.status = 400;
243
+ this.response.body = {
244
+ success: false,
245
+ message: 'AI 服务配置不存在,请先设置配置。'
246
+ };
247
+ this.response.type = 'application/json';
248
+ return;
249
+ }
250
+
251
+ // 检查配置完整性
252
+ if (!config.apiBaseUrl || !config.modelName || !config.apiKeyEncrypted) {
253
+ this.response.status = 400;
254
+ this.response.body = {
255
+ success: false,
256
+ message: 'AI 服务配置不完整,请先设置 API Base URL、模型名称和 API Key。'
257
+ };
258
+ this.response.type = 'application/json';
259
+ return;
260
+ }
261
+
262
+ // 解密 API Key
263
+ let apiKey: string;
264
+ try {
265
+ apiKey = decrypt(config.apiKeyEncrypted);
266
+ } catch (err) {
267
+ this.response.status = 500;
268
+ this.response.body = {
269
+ success: false,
270
+ message: `API Key 解密失败: ${err instanceof Error ? err.message : String(err)}`
271
+ };
272
+ this.response.type = 'application/json';
273
+ return;
274
+ }
275
+
276
+ // 创建临时 OpenAI 客户端
277
+ const client = new OpenAIClient({
278
+ apiBaseUrl: config.apiBaseUrl,
279
+ modelName: config.modelName,
280
+ apiKey,
281
+ timeoutSeconds: config.timeoutSeconds || 30
282
+ });
283
+
284
+ // 发送测试请求
285
+ try {
286
+ await client.chat(
287
+ [{ role: 'user', content: 'ping' }],
288
+ 'You are a health check assistant for AI Helper.'
289
+ );
290
+
291
+ this.response.body = {
292
+ success: true,
293
+ message: '连接成功!AI 服务配置正确。'
294
+ };
295
+ this.response.type = 'application/json';
296
+
297
+ } catch (err) {
298
+ // 调用失败,但返回 200(业务层错误,不是 HTTP 错误)
299
+ this.response.status = 200;
300
+ this.response.body = {
301
+ success: false,
302
+ message: err instanceof Error ? err.message : '调用 AI 服务失败'
303
+ };
304
+ this.response.type = 'application/json';
305
+ }
306
+
307
+ } catch (err) {
308
+ console.error('[TestConnectionHandler] Error:', err);
309
+ this.response.status = 500;
310
+ this.response.body = {
311
+ success: false,
312
+ message: err instanceof Error ? err.message : '测试连接失败'
313
+ };
314
+ this.response.type = 'application/json';
315
+ }
316
+ }
317
+ }
318
+
319
+ /**
320
+ * 导出路由权限配置
321
+ * 使用 PRIV.PRIV_EDIT_SYSTEM (root-only 权限)
322
+ * AI 配置敏感,仅允许系统管理员访问
323
+ */
324
+ export const GetConfigHandlerPriv = PRIV.PRIV_EDIT_SYSTEM;
325
+ export const UpdateConfigHandlerPriv = PRIV.PRIV_EDIT_SYSTEM;
326
+ export const TestConnectionHandlerPriv = PRIV.PRIV_EDIT_SYSTEM;
327
+ export const FetchModelsHandlerPriv = PRIV.PRIV_EDIT_SYSTEM;
328
+
329
+ /**
330
+ * FetchModelsHandler - 获取 API 端点的可用模型列表
331
+ * POST /ai-helper/admin/fetch-models
332
+ */
333
+ export class FetchModelsHandler extends Handler {
334
+ async post() {
335
+ try {
336
+ const body = this.request.body as {
337
+ endpointId?: string;
338
+ apiBaseUrl?: string;
339
+ apiKey?: string;
340
+ };
341
+
342
+ let apiBaseUrl: string;
343
+ let apiKey: string;
344
+
345
+ if (body.endpointId) {
346
+ // 从现有端点获取配置
347
+ const aiConfigModel: AIConfigModel = this.ctx.get('aiConfigModel');
348
+ const endpoint = await aiConfigModel.getEndpointById(body.endpointId);
349
+
350
+ if (!endpoint) {
351
+ this.response.status = 404;
352
+ this.response.body = { success: false, error: '端点不存在' };
353
+ this.response.type = 'application/json';
354
+ return;
355
+ }
356
+
357
+ apiBaseUrl = endpoint.apiBaseUrl;
358
+ try {
359
+ apiKey = decrypt(endpoint.apiKeyEncrypted);
360
+ } catch {
361
+ this.response.status = 400;
362
+ this.response.body = { success: false, error: 'API Key 解密失败' };
363
+ this.response.type = 'application/json';
364
+ return;
365
+ }
366
+ } else if (body.apiBaseUrl && body.apiKey) {
367
+ // 使用传入的参数
368
+ apiBaseUrl = body.apiBaseUrl;
369
+ apiKey = body.apiKey;
370
+ } else {
371
+ this.response.status = 400;
372
+ this.response.body = {
373
+ success: false,
374
+ error: '请提供 endpointId 或者 apiBaseUrl + apiKey'
375
+ };
376
+ this.response.type = 'application/json';
377
+ return;
378
+ }
379
+
380
+ // 获取模型列表
381
+ const result = await fetchAvailableModels(apiBaseUrl, apiKey);
382
+
383
+ if (result.success) {
384
+ // 如果是从端点获取的,更新端点的模型列表和时间戳
385
+ if (body.endpointId) {
386
+ const aiConfigModel: AIConfigModel = this.ctx.get('aiConfigModel');
387
+ await aiConfigModel.updateEndpoint(body.endpointId, {
388
+ models: result.models || [],
389
+ modelsLastFetched: new Date()
390
+ });
391
+ }
392
+
393
+ this.response.body = {
394
+ success: true,
395
+ models: result.models
396
+ };
397
+ } else {
398
+ this.response.body = {
399
+ success: false,
400
+ error: result.error
401
+ };
402
+ }
403
+ this.response.type = 'application/json';
404
+ } catch (err) {
405
+ console.error('[FetchModelsHandler] Error:', err);
406
+ this.response.status = 500;
407
+ this.response.body = {
408
+ success: false,
409
+ error: err instanceof Error ? err.message : '获取模型列表失败'
410
+ };
411
+ this.response.type = 'application/json';
412
+ }
413
+ }
414
+ }
415
+
416
+ function formatJailbreakLog(log: JailbreakLog) {
417
+ return {
418
+ id: log._id.toHexString(),
419
+ userId: log.userId,
420
+ problemId: log.problemId,
421
+ conversationId: log.conversationId ? log.conversationId.toHexString() : undefined,
422
+ questionType: log.questionType,
423
+ matchedPattern: log.matchedPattern,
424
+ matchedText: log.matchedText,
425
+ createdAt: log.createdAt.toISOString()
426
+ };
427
+ }