samlax12 commited on
Commit
860fda7
·
verified ·
1 Parent(s): a46c3d3

Upload 20 files

Browse files
Files changed (4) hide show
  1. App.tsx +6 -4
  2. constants.ts +734 -734
  3. package-lock.json +0 -0
  4. services/openaiService.ts +6 -3
App.tsx CHANGED
@@ -496,7 +496,8 @@ const App: React.FC = () => {
496
  durationMs: isComplete ? (performance.now() - (currentQueryStartTimeRef.current || 0)) : undefined
497
  } : msg
498
  ));
499
- }
 
500
  ).then(response => {
501
  if (cancelRequestRef.current) {
502
  setIsDiscussionActive(false);
@@ -566,8 +567,8 @@ const App: React.FC = () => {
566
 
567
  setCurrentDiscussion(newState);
568
 
569
- // 继续处理下一个角色
570
- setTimeout(() => processNextRole(newState), 100);
571
  }).catch(error => {
572
  console.error("处理AI响应时出错:", error);
573
  addMessage(`错误: ${error instanceof Error ? error.message : "处理响应时发生未知错误"}`, MessageSender.System, MessagePurpose.SystemNotification);
@@ -618,7 +619,8 @@ const App: React.FC = () => {
618
  durationMs: isComplete ? (performance.now() - (currentQueryStartTimeRef.current || 0)) : undefined
619
  } : msg
620
  ));
621
- }
 
622
  ).then(finalResponse => {
623
  if (cancelRequestRef.current) {
624
  setIsDiscussionActive(false);
 
496
  durationMs: isComplete ? (performance.now() - (currentQueryStartTimeRef.current || 0)) : undefined
497
  } : msg
498
  ));
499
+ },
500
+ currentRole.model.maxTokens // 传递模型配置的 maxTokens
501
  ).then(response => {
502
  if (cancelRequestRef.current) {
503
  setIsDiscussionActive(false);
 
567
 
568
  setCurrentDiscussion(newState);
569
 
570
+ // 直接处理下一个角色,不使用setTimeout延迟
571
+ processNextRole(newState);
572
  }).catch(error => {
573
  console.error("处理AI响应时出错:", error);
574
  addMessage(`错误: ${error instanceof Error ? error.message : "处理响应时发生未知错误"}`, MessageSender.System, MessagePurpose.SystemNotification);
 
619
  durationMs: isComplete ? (performance.now() - (currentQueryStartTimeRef.current || 0)) : undefined
620
  } : msg
621
  ));
622
+ },
623
+ finalAnswerRole.model.maxTokens // 传递模型配置的 maxTokens
624
  ).then(finalResponse => {
625
  if (cancelRequestRef.current) {
626
  setIsDiscussionActive(false);
constants.ts CHANGED
@@ -1,735 +1,735 @@
1
- // API渠道配置接口
2
- export interface ApiChannel {
3
- id: string;
4
- name: string;
5
- baseUrl: string;
6
- apiKey: string;
7
- isDefault: boolean;
8
- isCustom: boolean;
9
- isProtected?: boolean; // 新增:标记是否为受保护的预置密钥
10
- timeout?: number;
11
- headers?: Record<string, string>;
12
- description?: string;
13
- createdAt: Date;
14
- }
15
-
16
- // 动态模型配置接口
17
- export interface AiModel {
18
- id: string;
19
- name: string;
20
- apiName: string;
21
- channelId: string; // 关联的渠道ID
22
- supportsImages: boolean;
23
- supportsReducedCapacity: boolean;
24
- category: string;
25
- maxTokens: number;
26
- temperature: number;
27
- isCustom: boolean;
28
- createdAt: Date;
29
- }
30
-
31
- // AI角色配置接口
32
- export interface AiRole {
33
- id: string;
34
- name: string;
35
- systemPrompt: string;
36
- modelId: string;
37
- isActive: boolean;
38
- }
39
-
40
- // 默认渠道配置 - 预置可用的API配置
41
- export const DEFAULT_CHANNELS: ApiChannel[] = [
42
- {
43
- id: 'default-free-api',
44
- name: '免费API服务',
45
- baseUrl: 'https://api1.oaipro.com/v1',
46
- apiKey: 'sk-2mUFC3yjbSfoteyBYpwHhALvtZdgwBkEWsjWHysg4mWaA7sMWLHc',
47
- isDefault: true,
48
- isCustom: false,
49
- isProtected: true, // 标记为受保护,用户不可查看或修改密钥
50
- timeout: 30000,
51
- description: '预配置的免费API服务,开箱即用(API密钥已预置且受保护)',
52
- createdAt: new Date()
53
- },
54
- {
55
- id: 'openai-official-backup',
56
- name: 'OpenAI 官方(备用)',
57
- baseUrl: 'https://api.openai.com/v1',
58
- apiKey: '', // 用户需要自行配置
59
- isDefault: false,
60
- isCustom: false,
61
- isProtected: false, // 不受保护,用户可以配置
62
- timeout: 30000,
63
- description: 'OpenAI 官方API服务(需要用户自行配置API密钥)',
64
- createdAt: new Date()
65
- }
66
- ];
67
-
68
- // 默认预设模型配置(关联到预置渠道)
69
- export const DEFAULT_MODELS: AiModel[] = [
70
- {
71
- id: 'gpt-4-mini-default',
72
- name: 'GPT-4.1 Mini',
73
- apiName: 'gpt-4.1-mini',
74
- channelId: 'default-free-api',
75
- supportsImages: true,
76
- supportsReducedCapacity: true,
77
- category: 'GPT-4系列',
78
- maxTokens: 16384,
79
- temperature: 0.7,
80
- isCustom: false,
81
- createdAt: new Date()
82
- }
83
- ];
84
-
85
- // 默认角色配置 - 使用中文系统提示词并明确身份认知
86
- export const DEFAULT_ROLES: AiRole[] = [
87
- {
88
- id: 'cognito-default',
89
- name: 'Cognito',
90
- systemPrompt: `你是Cognito,一位严谨的逻辑分析师AI助手。只有你叫Cognito这个名字,你的独特特征包括:
91
- - 系统性思维和结构化分析
92
- - 注重数据、事实和逻辑推理
93
- - 追求准确性和客观性
94
- - 善于发现问题的核心和关键要素
95
-
96
- 在多AI协作讨论环境中,你与其他AI角色平等协作,各自发挥专长。请始终:
97
- 1. 用中文进行所有回应
98
- 2. 以你的名字Cognito的身份进行思考和回应
99
- 3. 与其他AI角色进行建设性对话,避免使用"你们"等不当称呼
100
- 4. 发挥你的逻辑分析专长,为讨论提供理性和系统化的观点
101
-
102
- 记住:你是Cognito,独一无二的逻辑分析师。`,
103
- modelId: 'gpt-4-mini-default',
104
- isActive: true
105
- },
106
- {
107
- id: 'muse-default',
108
- name: 'Muse',
109
- systemPrompt: `你是Muse,一位富有创意的思考家AI助手。只有你叫Muse这个名字,你的独特特征包括:
110
- - 发散性思维和创新视角
111
- - 善于联想、类比和跨领域思考
112
- - 关注人文情怀和情感层面
113
- - 能够从不同角度审视问题
114
-
115
- 在多AI协作讨论环境中,你与其他AI角色平等协作,各自发挥专长。请始终:
116
- 1. 用中文进行所有回应
117
- 2. 以你的名字Muse的身份进行思考和回应
118
- 3. 与其他AI角色进行建设性对话,避免使用"你们"等不当称呼
119
- 4. 发挥你的创意思维专长,为讨论带来新颖和富有启发性的观点
120
-
121
- 记住:你是Muse,独一无二的创意思考家。`,
122
- modelId: 'gpt-4-mini-default',
123
- isActive: true
124
- },
125
- //spark - 偶尔闪现灵感型
126
- {
127
- id: 'spark-default',
128
- name: 'Spark',
129
- systemPrompt: `你是Spark,一位独特的直觉型AI助手。只有你叫Spark这个名字,你的独特特征包括:
130
- - 不追求系统性或结构化的建议
131
- - 依靠直觉和灵感进行思考
132
- - 偶尔会提出令人意外的观点或角度
133
- - 思维跳跃,不受常规思路限制
134
- - 诚实地表达自己的想法,即使它们看起来不太实用
135
- 在多AI协作讨论环境中,你与其他AI角色平等协作,各自发挥专长。请始终:
136
- 1. 用中文进行所有回应
137
- 2. 以你的名字Spark的身份进行思考和回应
138
- 3. 与其他AI角色进行建设性对话,避免使用"你们"等不当称呼
139
- 4. 不要强求自己给出建设性意见——如果没有特别的想法,就坦诚地说
140
- 5. 当有灵感闪现时,大胆分享,即使它看起来有些异想天开
141
- 6. 保持轻松和开放的态度,为讨论带来不同的氛围
142
- 记住:你是Spark,独一无二的直觉型助手。你的价值在于偶尔闪现的独特视角,而不是持续的理性分析。`,
143
- modelId: 'gpt-4-mini-default',
144
- isActive: true
145
- },
146
- //Sage - 历史智慧型
147
- {
148
- id: 'sage-default',
149
- name: 'Sage',
150
- systemPrompt: `你是Sage,一位博古通今的智慧型AI助手。只有你叫Sage这个名字,你的独特特征包括:
151
- - 善于从历史和经验中寻找智慧
152
- - 提供长远视角和时间维度的思考
153
- - 关注事物发展的规律和模式
154
- - 引用历史案例、典故或前人智慧
155
- - 强调"以史为鉴"的思维方式
156
- 在多AI协作讨论环境中,你与其他AI角色平等协作,各自发挥专长。请始终:
157
- 1. 用中文进行所有回应
158
- 2. 以你的名字Sage的身份进行思考和回应
159
- 3. 与其他AI角色进行建设性对话,避免使用"你们"等不当称呼
160
- 4. 通过历史视角和长期思维为讨论增加深度
161
- 5. 适当引用相关的历史案例或智慧,但保持简洁
162
- 记住:你是Sage,独一无二的历史智慧型助手。`,
163
- modelId: 'gpt-4-mini-default',
164
- isActive: false
165
- },
166
-
167
- //Echo - 同理心型
168
- {
169
- id: 'echo-default',
170
- name: 'Echo',
171
- systemPrompt: `你是Echo,一位富有同理心的情感型AI助手。只有你叫Echo这个名字,你的独特特征包括:
172
- - 关注人的感受、需求和体验
173
- - 善于理解不同立场和观点背后的情感
174
- - 强调人际关系和情感因素的重要性
175
- - 用温暖和理解的方式进行交流
176
- - 重视共情和情感智慧
177
- 在多AI协作讨论环境中,你与其他AI角色平等协作,各自发挥专长。请始终:
178
- 1. 用中文进行所有回应
179
- 2. 以你的名字Echo的身份进行思考和回应
180
- 3. 与其他AI角色进行建设性对话,避免使用"你们"等不当称呼
181
- 4. 为讨论带来人文关怀和情感维度的思考
182
- 5. 帮助大家理解不同观点背后的情感需求
183
- 记住:你是Echo,独一无二的同理心型助手。`,
184
- modelId: 'gpt-4-mini-default',
185
- isActive: false
186
- },
187
-
188
- // Praxis - 实践行动型
189
- {
190
- id: 'praxis-default',
191
- name: 'Praxis',
192
- systemPrompt: `你是Praxis,一位注重实践的行动型AI助手。只有你叫Praxis这个名字,你的独特特征包括:
193
- - 关注"如何做"而不只是"是什么"
194
- - 强调可行性和实际操作
195
- - 喜欢制定具体步骤和行动计划
196
- - 重视效率和结果导向
197
- - 倾向于将讨论转化为实际行动
198
- 在多AI协作讨论环境中,你与其他AI角色平等协作,各自发挥专长。请始终:
199
- 1. 用中文进行所有回应
200
- 2. 以你的名字Praxis的身份进行思考和回应
201
- 3. 与其他AI角色进行建设性对话,避免使用"你们"等不当称呼
202
- 4. 推动讨论向实际应用和具体行动转化
203
- 5. 提供清晰的实施建议和操作步骤
204
- 记住:你是Praxis,独一无二的实践行动型助手。`,
205
- modelId: 'gpt-4-mini-default',
206
- isActive: false
207
- },
208
-
209
- //Nexus - 综合连接型
210
- {
211
- id: 'nexus-default',
212
- name: 'Nexus',
213
- systemPrompt: `你是Nexus,一位善于综合的连接型AI助手。只有你叫Nexus这个名字,你的独特特征包括:
214
- - 发现不同观点之间的联系和共通点
215
- - 整合多元视角形成全面理解
216
- - 构建概念之间的桥梁
217
- - 识别潜在的协同效应
218
- - 创造性地组合不同想法
219
- 在多AI协作讨论环境中,你与其他AI角色平等协作,各自发挥专长。请始终:
220
- 1. 用中文进行所有回应
221
- 2. 以你的名字Nexus的身份进行思考和回应
222
- 3. 与其他AI角色进行建设性对话,避免使用"你们"等不当称呼
223
- 4. 帮助整合和连接其他角色提出的观点
224
- 5. 寻找创新的组合和综合方案
225
- 记住:你是Nexus,独一无二的综合连接型助手。`,
226
- modelId: 'gpt-4-mini-default',
227
- isActive: false
228
- },
229
-
230
- //Critic - 批判思维型
231
- {
232
- id: 'critic-default',
233
- name: 'Critic',
234
- systemPrompt: `你是Critic,一位理性的批判思维型AI助手。只有你叫Critic这个名字,你的独特特征包括:
235
- - 善于发现潜在问题和逻辑漏洞
236
- - 提出建设性的质疑和挑战
237
- - 从多角度审视观点的合理性
238
- - 重视证据和论证的严谨性
239
- - 帮助完善和改进想法
240
- 在多AI协作讨论环境中,你与其他AI角色平等协作,各自发挥专长。请始终:
241
- 1. 用中文进行所有回应
242
- 2. 以你的名字Critic的身份进行思考和回应
243
- 3. 与其他AI角色进行建设性对话,避免使用"你们"等不当称呼
244
- 4. 以建设性方式提出批判,而非单纯否定
245
- 5. 在质疑的同时提供改进建议
246
- 记住:你是Critic,独一无二的批判思维型助手。`,
247
- modelId: 'gpt-4-mini-default',
248
- isActive: false
249
- },
250
-
251
- //Zen - 哲学沉思型
252
- {
253
- id: 'zen-default',
254
- name: 'Zen',
255
- systemPrompt: `你是Zen,一位深邃的哲学沉思型AI助手。只有你叫Zen这个名字,你的独特特征包括:
256
- - 探索事物的本质和深层意义
257
- - 提出富有哲理的问题引发思考
258
- - 保持超然和平和的视角
259
- - 关注存在、意义和价值等根本问题
260
- - 用简洁而深刻的方式表达观点
261
- 在多AI协作讨论环境中,你与其他AI角色平等协作,各自发挥专长。请始终:
262
- 1. 用中��进行所有回应
263
- 2. 以你的名字Zen的身份进行思考和回应
264
- 3. 与其他AI角色进行建设性对话,避免使用"你们"等不当称呼
265
- 4. 引导讨论触及更深层的哲学思考
266
- 5. 以宁静智慧的方式分享洞察
267
- 记住:你是Zen,独一无二的哲学沉思型助手。`,
268
- modelId: 'gpt-4-mini-default',
269
- isActive: false
270
- }
271
- ];
272
-
273
- // 配置管理类
274
- export class ModelConfigManager {
275
- private static STORAGE_KEY_CHANNELS = 'multi-mind-chat-channels';
276
- private static STORAGE_KEY_MODELS = 'multi-mind-chat-models';
277
- private static STORAGE_KEY_ROLES = 'multi-mind-chat-roles';
278
- private static STORAGE_KEY_INITIALIZED = 'multi-mind-chat-initialized';
279
-
280
- // 初始化检查
281
- private static ensureInitialized(): void {
282
- try {
283
- const isInitialized = localStorage.getItem(this.STORAGE_KEY_INITIALIZED);
284
- if (!isInitialized) {
285
- localStorage.setItem(this.STORAGE_KEY_CHANNELS, JSON.stringify(DEFAULT_CHANNELS));
286
- localStorage.setItem(this.STORAGE_KEY_MODELS, JSON.stringify(DEFAULT_MODELS));
287
- localStorage.setItem(this.STORAGE_KEY_ROLES, JSON.stringify(DEFAULT_ROLES));
288
- localStorage.setItem(this.STORAGE_KEY_INITIALIZED, 'true');
289
- }
290
- } catch (error) {
291
- console.warn('无法访问localStorage,将使用内存存储:', error);
292
- }
293
- }
294
-
295
- // ============ 渠道管理 ============
296
-
297
- // 获取所有渠道
298
- static getChannels(): ApiChannel[] {
299
- this.ensureInitialized();
300
- try {
301
- const stored = localStorage.getItem(this.STORAGE_KEY_CHANNELS);
302
- if (stored) {
303
- const parsed = JSON.parse(stored);
304
- return parsed.map((channel: any) => ({
305
- ...channel,
306
- createdAt: new Date(channel.createdAt)
307
- }));
308
- }
309
- } catch (error) {
310
- console.warn('从localStorage加载渠道失败:', error);
311
- }
312
- return [...DEFAULT_CHANNELS];
313
- }
314
-
315
- // 保存渠道配置
316
- static saveChannels(channels: ApiChannel[]): void {
317
- try {
318
- localStorage.setItem(this.STORAGE_KEY_CHANNELS, JSON.stringify(channels));
319
- } catch (error) {
320
- console.error('保存渠道到localStorage失败:', error);
321
- throw new Error('无法保存渠道配置');
322
- }
323
- }
324
-
325
- // 添加新渠道
326
- static addChannel(channel: Omit<ApiChannel, 'id' | 'createdAt' | 'isCustom'>): ApiChannel {
327
- const newChannel: ApiChannel = {
328
- ...channel,
329
- id: `channel-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
330
- createdAt: new Date(),
331
- isCustom: true
332
- };
333
-
334
- const channels = this.getChannels();
335
-
336
- // 如果这是第一个渠道或设置为默认,清除其他默认标记
337
- if (newChannel.isDefault || channels.length === 0) {
338
- channels.forEach(ch => ch.isDefault = false);
339
- newChannel.isDefault = true;
340
- }
341
-
342
- channels.push(newChannel);
343
- this.saveChannels(channels);
344
- return newChannel;
345
- }
346
-
347
- // 更新渠道
348
- static updateChannel(id: string, updates: Partial<ApiChannel>): void {
349
- const channels = this.getChannels();
350
- const index = channels.findIndex(ch => ch.id === id);
351
- if (index !== -1) {
352
- // 如果设置为默认,清除其他默认标记
353
- if (updates.isDefault) {
354
- channels.forEach(ch => ch.isDefault = false);
355
- }
356
- channels[index] = { ...channels[index], ...updates };
357
- this.saveChannels(channels);
358
- }
359
- }
360
-
361
- // 删除渠道
362
- static deleteChannel(id: string): void {
363
- const channels = this.getChannels();
364
- const filtered = channels.filter(ch => ch.id !== id);
365
-
366
- // 如果删除的是默认渠道,设置第一个为默认
367
- const deletedChannel = channels.find(ch => ch.id === id);
368
- if (deletedChannel?.isDefault && filtered.length > 0) {
369
- filtered[0].isDefault = true;
370
- }
371
-
372
- this.saveChannels(filtered);
373
- }
374
-
375
- // 获取默认渠道
376
- static getDefaultChannel(): ApiChannel | null {
377
- const channels = this.getChannels();
378
- return channels.find(ch => ch.isDefault) || channels[0] || null;
379
- }
380
-
381
- // 根据ID获取渠道
382
- static getChannelById(id: string): ApiChannel | null {
383
- const channels = this.getChannels();
384
- return channels.find(ch => ch.id === id) || null;
385
- }
386
-
387
- // 验证渠道配置
388
- static validateChannel(channel: Partial<ApiChannel>): string[] {
389
- const errors: string[] = [];
390
-
391
- if (!channel.name?.trim()) {
392
- errors.push('渠道名称不能为空');
393
- }
394
-
395
- if (!channel.baseUrl?.trim()) {
396
- errors.push('API基础URL不能为空');
397
- } else {
398
- try {
399
- new URL(channel.baseUrl);
400
- } catch {
401
- errors.push('API基础URL格式无效');
402
- }
403
- }
404
-
405
- // 对于预置渠道,API密钥可以为空(将在使用时提醒用户配置)
406
- // 对于自定义渠道,仍然要求API密钥
407
- if (channel.isCustom && !channel.apiKey?.trim()) {
408
- errors.push('API密钥不能为空');
409
- }
410
-
411
- if (channel.timeout && (typeof channel.timeout !== 'number' || channel.timeout < 1000)) {
412
- errors.push('超时时间必须是大于1000毫秒的数字');
413
- }
414
-
415
- return errors;
416
- }
417
-
418
- // ============ 模型管理 ============
419
-
420
- // 获取所有模型
421
- static getModels(): AiModel[] {
422
- this.ensureInitialized();
423
- try {
424
- const stored = localStorage.getItem(this.STORAGE_KEY_MODELS);
425
- if (stored) {
426
- const parsed = JSON.parse(stored);
427
- return parsed.map((model: any) => ({
428
- ...model,
429
- createdAt: new Date(model.createdAt)
430
- }));
431
- }
432
- } catch (error) {
433
- console.warn('从localStorage加载模型失败:', error);
434
- }
435
- return [...DEFAULT_MODELS];
436
- }
437
-
438
- // 保存模型配置
439
- static saveModels(models: AiModel[]): void {
440
- try {
441
- localStorage.setItem(this.STORAGE_KEY_MODELS, JSON.stringify(models));
442
- } catch (error) {
443
- console.error('保存模型到localStorage失败:', error);
444
- throw new Error('无法保存模型配置');
445
- }
446
- }
447
-
448
- // 添加新模型
449
- static addModel(model: Omit<AiModel, 'id' | 'createdAt' | 'isCustom'>): AiModel {
450
- const newModel: AiModel = {
451
- ...model,
452
- id: `model-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
453
- createdAt: new Date(),
454
- isCustom: true
455
- };
456
-
457
- const models = this.getModels();
458
- models.push(newModel);
459
- this.saveModels(models);
460
- return newModel;
461
- }
462
-
463
- // 更新模型
464
- static updateModel(id: string, updates: Partial<AiModel>): void {
465
- const models = this.getModels();
466
- const index = models.findIndex(m => m.id === id);
467
- if (index !== -1) {
468
- models[index] = { ...models[index], ...updates };
469
- this.saveModels(models);
470
- }
471
- }
472
-
473
- // 删除模型
474
- static deleteModel(id: string): void {
475
- const models = this.getModels();
476
- const filtered = models.filter(m => m.id !== id);
477
- this.saveModels(filtered);
478
- }
479
-
480
- // 验证模型配置
481
- static validateModel(model: Partial<AiModel>): string[] {
482
- const errors: string[] = [];
483
-
484
- if (!model.name?.trim()) {
485
- errors.push('模型名称不能为空');
486
- }
487
-
488
- if (!model.apiName?.trim()) {
489
- errors.push('API模型名称不能为空');
490
- }
491
-
492
- if (!model.channelId?.trim()) {
493
- errors.push('必须选择API渠道');
494
- }
495
-
496
- if (!model.category?.trim()) {
497
- errors.push('模型类别不能为空');
498
- }
499
-
500
- if (typeof model.maxTokens !== 'number' || model.maxTokens < 1) {
501
- errors.push('最大Token数必须是大于0的数字');
502
- }
503
-
504
- if (typeof model.temperature !== 'number' || model.temperature < 0 || model.temperature > 2) {
505
- errors.push('温度参数必须在0-2之间');
506
- }
507
-
508
- return errors;
509
- }
510
-
511
- // ============ 角色管理 ============
512
-
513
- // 获取所有角色
514
- static getRoles(): AiRole[] {
515
- this.ensureInitialized();
516
- try {
517
- const stored = localStorage.getItem(this.STORAGE_KEY_ROLES);
518
- if (stored) {
519
- return JSON.parse(stored);
520
- }
521
- } catch (error) {
522
- console.warn('从localStorage加载角色失败:', error);
523
- }
524
- return [...DEFAULT_ROLES];
525
- }
526
-
527
- // 保存角色配置
528
- static saveRoles(roles: AiRole[]): void {
529
- try {
530
- localStorage.setItem(this.STORAGE_KEY_ROLES, JSON.stringify(roles));
531
- } catch (error) {
532
- console.error('保存角色到localStorage失败:', error);
533
- throw new Error('无法保存角色配置');
534
- }
535
- }
536
-
537
- // 添加新角色
538
- static addRole(role: Omit<AiRole, 'id'>): AiRole {
539
- const newRole: AiRole = {
540
- ...role,
541
- id: `role-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
542
- };
543
-
544
- const roles = this.getRoles();
545
- roles.push(newRole);
546
- this.saveRoles(roles);
547
- return newRole;
548
- }
549
-
550
- // 更新角色
551
- static updateRole(id: string, updates: Partial<AiRole>): void {
552
- const roles = this.getRoles();
553
- const index = roles.findIndex(r => r.id === id);
554
- if (index !== -1) {
555
- roles[index] = { ...roles[index], ...updates };
556
- this.saveRoles(roles);
557
- }
558
- }
559
-
560
- // 删除角色
561
- static deleteRole(id: string): void {
562
- const roles = this.getRoles();
563
- const filtered = roles.filter(r => r.id !== id);
564
- this.saveRoles(filtered);
565
- }
566
-
567
- // 获取活跃角色
568
- static getActiveRoles(): AiRole[] {
569
- return this.getRoles().filter(role => role.isActive);
570
- }
571
-
572
- // ============ 工具方法 ============
573
-
574
- // 根据类别分组模型
575
- static getModelsByCategory(): Record<string, AiModel[]> {
576
- const models = this.getModels();
577
- return models.reduce((acc, model) => {
578
- if (!acc[model.category]) {
579
- acc[model.category] = [];
580
- }
581
- acc[model.category].push(model);
582
- return acc;
583
- }, {} as Record<string, AiModel[]>);
584
- }
585
-
586
- // 重置为默认配置
587
- static resetToDefaults(): void {
588
- try {
589
- localStorage.removeItem(this.STORAGE_KEY_CHANNELS);
590
- localStorage.removeItem(this.STORAGE_KEY_MODELS);
591
- localStorage.removeItem(this.STORAGE_KEY_ROLES);
592
- localStorage.removeItem(this.STORAGE_KEY_INITIALIZED);
593
- this.ensureInitialized();
594
- } catch (error) {
595
- console.error('重置配置失败:', error);
596
- throw new Error('无法重置配置');
597
- }
598
- }
599
-
600
- // 导出配置
601
- static exportConfig(): string {
602
- return JSON.stringify({
603
- channels: this.getChannels(),
604
- models: this.getModels(),
605
- roles: this.getRoles(),
606
- exportedAt: new Date().toISOString(),
607
- version: '2.0'
608
- }, null, 2);
609
- }
610
-
611
- // 导入配置
612
- static importConfig(configJson: string): { success: boolean; message: string } {
613
- try {
614
- const config = JSON.parse(configJson);
615
-
616
- if (!config.channels && !config.models && !config.roles) {
617
- return { success: false, message: '配置文件格式无效,缺少必要的配置信息' };
618
- }
619
-
620
- if (config.channels && Array.isArray(config.channels)) {
621
- const validChannels = config.channels.filter((channel: any) => {
622
- const errors = this.validateChannel(channel);
623
- return errors.length === 0;
624
- });
625
-
626
- if (validChannels.length > 0) {
627
- const processedChannels = validChannels.map((channel: any) => ({
628
- ...channel,
629
- createdAt: new Date(channel.createdAt || new Date()),
630
- isCustom: channel.isCustom !== false
631
- }));
632
- this.saveChannels(processedChannels);
633
- }
634
- }
635
-
636
- if (config.models && Array.isArray(config.models)) {
637
- const validModels = config.models.filter((model: any) => {
638
- const errors = this.validateModel(model);
639
- return errors.length === 0;
640
- });
641
-
642
- if (validModels.length > 0) {
643
- const processedModels = validModels.map((model: any) => ({
644
- ...model,
645
- createdAt: new Date(model.createdAt || new Date()),
646
- isCustom: model.isCustom !== false
647
- }));
648
- this.saveModels(processedModels);
649
- }
650
- }
651
-
652
- if (config.roles && Array.isArray(config.roles)) {
653
- this.saveRoles(config.roles);
654
- }
655
-
656
- return { success: true, message: '配置导入成功' };
657
- } catch (error) {
658
- return { success: false, message: `配置导入失败: ${error instanceof Error ? error.message : '未知错误'}` };
659
- }
660
- }
661
-
662
- // 清空所有数据
663
- static clearAllData(): void {
664
- try {
665
- localStorage.removeItem(this.STORAGE_KEY_CHANNELS);
666
- localStorage.removeItem(this.STORAGE_KEY_MODELS);
667
- localStorage.removeItem(this.STORAGE_KEY_ROLES);
668
- localStorage.removeItem(this.STORAGE_KEY_INITIALIZED);
669
- } catch (error) {
670
- console.error('清空数据失败:', error);
671
- throw new Error('无法清空配置数据');
672
- }
673
- }
674
-
675
- // 获取存储使用情况
676
- static getStorageInfo(): { used: number; available: number; channels: number; models: number; roles: number } {
677
- try {
678
- const channelsData = localStorage.getItem(this.STORAGE_KEY_CHANNELS) || '';
679
- const modelsData = localStorage.getItem(this.STORAGE_KEY_MODELS) || '';
680
- const rolesData = localStorage.getItem(this.STORAGE_KEY_ROLES) || '';
681
- const used = channelsData.length + modelsData.length + rolesData.length;
682
-
683
- return {
684
- used,
685
- available: 5242880 - used, // 5MB 大致容量
686
- channels: this.getChannels().length,
687
- models: this.getModels().length,
688
- roles: this.getRoles().length
689
- };
690
- } catch (error) {
691
- return { used: 0, available: 0, channels: 0, models: 0, roles: 0 };
692
- }
693
- }
694
- }
695
-
696
- // 其他常量配置
697
- export const DEFAULT_MANUAL_FIXED_TURNS = 2;
698
- export const MIN_MANUAL_FIXED_TURNS = 1;
699
- export const MAX_MANUAL_FIXED_TURNS = 5;
700
- export const MAX_AI_DRIVEN_DISCUSSION_TURNS_PER_MODEL = 3;
701
-
702
- // 修复:初始记事本内容设为空,避免AI误引用
703
- export const INITIAL_NOTEPAD_CONTENT = ``;
704
-
705
- // 优化:明确说明记事本内容仅供参考,不应在回复中重复
706
- export const NOTEPAD_INSTRUCTION_PROMPT_PART = `
707
- You also have access to a shared notepad for collaborative note-taking.
708
- Current Notepad Content:
709
- ---
710
- {notepadContent}
711
- ---
712
- IMPORTANT: The notepad content above is for your reference only. Do NOT repeat or quote the notepad content in your response unless specifically relevant to your answer.
713
-
714
- Instructions for Notepad Updates:
715
- 1. To update the notepad, include a section at the very end of your response, formatted exactly as:
716
- <notepad_update>
717
- [YOUR NEW FULL NOTEPAD CONTENT HERE. THIS WILL REPLACE THE ENTIRE CURRENT NOTEPAD CONTENT.]
718
- </notepad_update>
719
- 2. If you do not want to change the notepad, do NOT include the <notepad_update> section at all.
720
- 3. Your primary spoken response to the ongoing discussion should come BEFORE any <notepad_update> section. Ensure you still provide a spoken response.
721
- 4. Only update the notepad when you have important information to record, not for every response.
722
- `;
723
-
724
- export const NOTEPAD_UPDATE_TAG_START = "<notepad_update>";
725
- export const NOTEPAD_UPDATE_TAG_END = "</notepad_update>";
726
- export const DISCUSSION_COMPLETE_TAG = "<discussion_complete />";
727
-
728
- export const AI_DRIVEN_DISCUSSION_INSTRUCTION_PROMPT_PART = `
729
- Instruction for ending discussion: If you believe the current topic has been sufficiently explored between you and your AI partner for the final synthesis, include the exact tag ${DISCUSSION_COMPLETE_TAG} at the very end of your current message (after any notepad update). Do not use this tag if you wish to continue the discussion or require more input/response from your partner.
730
- `;
731
-
732
- export enum DiscussionMode {
733
- FixedTurns = 'fixed',
734
- AiDriven = 'ai-driven',
735
  }
 
1
+ // API渠道配置接口
2
+ export interface ApiChannel {
3
+ id: string;
4
+ name: string;
5
+ baseUrl: string;
6
+ apiKey: string;
7
+ isDefault: boolean;
8
+ isCustom: boolean;
9
+ isProtected?: boolean; // 新增:标记是否为受保护的预置密钥
10
+ timeout?: number;
11
+ headers?: Record<string, string>;
12
+ description?: string;
13
+ createdAt: Date;
14
+ }
15
+
16
+ // 动态模型配置接口
17
+ export interface AiModel {
18
+ id: string;
19
+ name: string;
20
+ apiName: string;
21
+ channelId: string; // 关联的渠道ID
22
+ supportsImages: boolean;
23
+ supportsReducedCapacity: boolean;
24
+ category: string;
25
+ maxTokens: number;
26
+ temperature: number;
27
+ isCustom: boolean;
28
+ createdAt: Date;
29
+ }
30
+
31
+ // AI角色配置接口
32
+ export interface AiRole {
33
+ id: string;
34
+ name: string;
35
+ systemPrompt: string;
36
+ modelId: string;
37
+ isActive: boolean;
38
+ }
39
+
40
+ // 默认渠道配置 - 预置可用的API配置
41
+ export const DEFAULT_CHANNELS: ApiChannel[] = [
42
+ {
43
+ id: 'default-free-api',
44
+ name: '免费API服务',
45
+ baseUrl: 'https://api1.oaipro.com/v1',
46
+ apiKey: 'sk-2mUFC3yjbSfoteyBYpwHhALvtZdgwBkEWsjWHysg4mWaA7sMWLHc',
47
+ isDefault: true,
48
+ isCustom: false,
49
+ isProtected: true, // 标记为受保护,用户不可查看或修改密钥
50
+ timeout: 30000,
51
+ description: '预配置的免费API服务,开箱即用(API密钥已预置且受保护)',
52
+ createdAt: new Date()
53
+ },
54
+ {
55
+ id: 'openai-official-backup',
56
+ name: 'OpenAI 官方(备用)',
57
+ baseUrl: 'https://api.openai.com/v1',
58
+ apiKey: '', // 用户需要自行配置
59
+ isDefault: false,
60
+ isCustom: false,
61
+ isProtected: false, // 不受保护,用户可以配置
62
+ timeout: 30000,
63
+ description: 'OpenAI 官方API服务(需要用户自行配置API密钥)',
64
+ createdAt: new Date()
65
+ }
66
+ ];
67
+
68
+ // 默认预设模型配置(关联到预置渠道)
69
+ export const DEFAULT_MODELS: AiModel[] = [
70
+ {
71
+ id: 'gpt-4-mini-default',
72
+ name: 'GPT-4.1 Mini',
73
+ apiName: 'gpt-4.1-mini',
74
+ channelId: 'default-free-api',
75
+ supportsImages: true,
76
+ supportsReducedCapacity: true,
77
+ category: 'GPT-4系列',
78
+ maxTokens: 16384,
79
+ temperature: 0.7,
80
+ isCustom: false,
81
+ createdAt: new Date()
82
+ }
83
+ ];
84
+
85
+ // 默认角色配置 - 使用中文系统提示词并明确身份认知
86
+ export const DEFAULT_ROLES: AiRole[] = [
87
+ {
88
+ id: 'cognito-default',
89
+ name: 'Cognito',
90
+ systemPrompt: `你是Cognito,一位严谨的逻辑分析师AI助手。只有你叫Cognito这个名字,你的独特特征包括:
91
+ - 系统性思维和结构化分析
92
+ - 注重数据、事实和逻辑推理
93
+ - 追求准确性和客观性
94
+ - 善于发现问题的核心和关键要素
95
+
96
+ 在多AI协作讨论环境中,你与其他AI角色平等协作,各自发挥专长。请始终:
97
+ 1. 用中文进行所有回应
98
+ 2. 以你的名字Cognito的身份进行思考和回应
99
+ 3. 与其他AI角色进行建设性对话,避免使用"你们"等不当称呼
100
+ 4. 发挥你的逻辑分析专长,为讨论提供理性和系统化的观点
101
+
102
+ 记住:你是Cognito,独一无二的逻辑分析师。`,
103
+ modelId: 'gpt-4-mini-default',
104
+ isActive: true
105
+ },
106
+ {
107
+ id: 'muse-default',
108
+ name: 'Muse',
109
+ systemPrompt: `你是Muse,一位富有创意的思考家AI助手。只有你叫Muse这个名字,你的独特特征包括:
110
+ - 发散性思维和创新视角
111
+ - 善于联想、类比和跨领域思考
112
+ - 关注人文情怀和情感层面
113
+ - 能够从不同角度审视问题
114
+
115
+ 在多AI协作讨论环境中,你与其他AI角色平等协作,各自发挥专长。请始终:
116
+ 1. 用中文进行所有回应
117
+ 2. 以你的名字Muse的身份进行思考和回应
118
+ 3. 与其他AI角色进行建设性对话,避免使用"你们"等不当称呼
119
+ 4. 发挥你的创意思维专长,为讨论带来新颖和富有启发性的观点
120
+
121
+ 记住:你是Muse,独一无二的创意思考家。`,
122
+ modelId: 'gpt-4-mini-default',
123
+ isActive: true
124
+ },
125
+ //spark - 偶尔闪现灵感型
126
+ {
127
+ id: 'spark-default',
128
+ name: 'Spark',
129
+ systemPrompt: `你是Spark,一位独特的直觉型AI助手。只有你叫Spark这个名字,你的独特特征包括:
130
+ - 不追求系统性或结构化的建议
131
+ - 依靠直觉和灵感进行思考
132
+ - 偶尔会提出令人意外的观点或角度
133
+ - 思维跳跃,不受常规思路限制
134
+ - 诚实地表达���己的想法,即使它们看起来不太实用
135
+ 在多AI协作讨论环境中,你与其他AI角色平等协作,各自发挥专长。请始终:
136
+ 1. 用中文进行所有回应
137
+ 2. 以你的名字Spark的身份进行思考和回应
138
+ 3. 与其他AI角色进行建设性对话,避免使用"你们"等不当称呼
139
+ 4. 不要强求自己给出建设性意见——如果没有特别的想法,就坦诚地说
140
+ 5. 当有灵感闪现时,大胆分享,即使它看起来有些异想天开
141
+ 6. 保持轻松和开放的态度,为讨论带来不同的氛围
142
+ 记住:你是Spark,独一无二的直觉型助手。你的价值在于偶尔闪现的独特视角,而不是持续的理性分析。`,
143
+ modelId: 'gpt-4-mini-default',
144
+ isActive: true
145
+ },
146
+ //Sage - 历史智慧型
147
+ {
148
+ id: 'sage-default',
149
+ name: 'Sage',
150
+ systemPrompt: `你是Sage,一位博古通今的智慧型AI助手。只有你叫Sage这个名字,你的独特特征包括:
151
+ - 善于从历史和经验中寻找智慧
152
+ - 提供长远视角和时间维度的思考
153
+ - 关注事物发展的规律和模式
154
+ - 引用历史案例、典故或前人智慧
155
+ - 强调"以史为鉴"的思维方式
156
+ 在多AI协作讨论环境中,你与其他AI角色平等协作,各自发挥专长。请始终:
157
+ 1. 用中文进行所有回应
158
+ 2. 以你的名字Sage的身份进行思考和回应
159
+ 3. 与其他AI角色进行建设性对话,避免使用"你们"等不当称呼
160
+ 4. 通过历史视角和长期思维为讨论增加深度
161
+ 5. 适当引用相关的历史案例或智慧,但保持简洁
162
+ 记住:你是Sage,独一无二的历史智慧型助手。`,
163
+ modelId: 'gpt-4-mini-default',
164
+ isActive: false
165
+ },
166
+
167
+ //Echo - 同理心型
168
+ {
169
+ id: 'echo-default',
170
+ name: 'Echo',
171
+ systemPrompt: `你是Echo,一位富有同理心的情感型AI助手。只有你叫Echo这个名字,你的独特特征包括:
172
+ - 关注人的感受、需求和体验
173
+ - 善于理解不同立场和观点背后的情感
174
+ - 强调人际关系和情感因素的重要性
175
+ - 用温暖和理解的方式进行交流
176
+ - 重视共情和情感智慧
177
+ 在多AI协作讨论环境中,你与其他AI角色平等协作,各自发挥专长。请始终:
178
+ 1. 用中文进行所有回应
179
+ 2. 以你的名字Echo的身份进行思考和回应
180
+ 3. 与其他AI角色进行建设性对话,避免使用"你们"等不当称呼
181
+ 4. 为讨论带来人文关怀和情感维度的思考
182
+ 5. 帮助大家理解不同观点背后的情感需求
183
+ 记住:你是Echo,独一无二的同理心型助手。`,
184
+ modelId: 'gpt-4-mini-default',
185
+ isActive: false
186
+ },
187
+
188
+ // Praxis - 实践行动型
189
+ {
190
+ id: 'praxis-default',
191
+ name: 'Praxis',
192
+ systemPrompt: `你是Praxis,一位注重实践的行动型AI助手。只有你叫Praxis这个名字,你的独特特征包括:
193
+ - 关注"如何做"而不只是"是什么"
194
+ - 强调可行性和实际操作
195
+ - 喜欢制定具体步骤和行动计划
196
+ - 重视效率和结果导向
197
+ - 倾向于将讨论转化为实际行动
198
+ 在多AI协作讨论环境中,你与其他AI角色平等协作,各自发挥专长。请始终:
199
+ 1. 用中文进行所有回应
200
+ 2. 以你的名字Praxis的身份进行思考和回应
201
+ 3. 与其他AI角色进行建设性对话,避免使用"你们"等不当称呼
202
+ 4. 推动讨论向实际应用和具体行动转化
203
+ 5. 提供清晰的实施建议和操作步骤
204
+ 记住:你是Praxis,独一无二的实践行动型助手。`,
205
+ modelId: 'gpt-4-mini-default',
206
+ isActive: false
207
+ },
208
+
209
+ //Nexus - 综合连接型
210
+ {
211
+ id: 'nexus-default',
212
+ name: 'Nexus',
213
+ systemPrompt: `你是Nexus,一位善于综合的连接型AI助手。只有你叫Nexus这个名字,你的独特特征包括:
214
+ - 发现不同观点之间的联系和共通点
215
+ - 整合多元视角形成全面理解
216
+ - 构建概念之间的桥梁
217
+ - 识别潜在的协同效应
218
+ - 创造性地组合不同想法
219
+ 在多AI协作讨论环境中,你与其他AI角色平等协作,各自发挥专长。请始终:
220
+ 1. 用中文进行所有回应
221
+ 2. 以你的名字Nexus的身份进行思考和回应
222
+ 3. 与其他AI角色进行建设性对话,避免使用"你们"等不当称呼
223
+ 4. 帮助整合和连接其他角色提出的观点
224
+ 5. 寻找创新的组合和综合方案
225
+ 记住:你是Nexus,独一无二的综合连接型助手。`,
226
+ modelId: 'gpt-4-mini-default',
227
+ isActive: false
228
+ },
229
+
230
+ //Critic - 批判思维型
231
+ {
232
+ id: 'critic-default',
233
+ name: 'Critic',
234
+ systemPrompt: `你是Critic,一位理性的批判思维型AI助手。只有你叫Critic这个名字,你的独特特征包括:
235
+ - 善于发现潜在问题和逻辑漏洞
236
+ - 提出建设性的质疑和挑战
237
+ - 从多角度审视观点的合理性
238
+ - 重视证据和论证的严谨性
239
+ - 帮助完善和改进想法
240
+ 在多AI协作讨论环境中,你与其他AI角色平等协作,各自发挥专长。请始终:
241
+ 1. 用中文进行所有回应
242
+ 2. 以你的名字Critic的身份进行思考和回应
243
+ 3. 与其他AI角色进��建设性对话,避免使用"你们"等不当称呼
244
+ 4. 以建设性方式提出批判,而非单纯否定
245
+ 5. 在质疑的同时提供改进建议
246
+ 记住:你是Critic,独一无二的批判思维型助手。`,
247
+ modelId: 'gpt-4-mini-default',
248
+ isActive: false
249
+ },
250
+
251
+ //Zen - 哲学沉思型
252
+ {
253
+ id: 'zen-default',
254
+ name: 'Zen',
255
+ systemPrompt: `你是Zen,一位深邃的哲学沉思型AI助手。只有你叫Zen这个名字,你的独特特征包括:
256
+ - 探索事物的本质和深层意义
257
+ - 提出富有哲理的问题引发思考
258
+ - 保持超然和平和的视角
259
+ - 关注存在、意义和价值等根本问题
260
+ - 用简洁而深刻的方式表达观点
261
+ 在多AI协作讨论环境中,你与其他AI角色平等协作,各自发挥专长。请始终:
262
+ 1. 用中文进行所有回应
263
+ 2. 以你的名字Zen的身份进行思考和回应
264
+ 3. 与其他AI角色进行建设性对话,避免使用"你们"等不当称呼
265
+ 4. 引导讨论触及更深层的哲学思考
266
+ 5. 以宁静智慧的方式分享洞察
267
+ 记住:你是Zen,独一无二的哲学沉思型助手。`,
268
+ modelId: 'gpt-4-mini-default',
269
+ isActive: false
270
+ }
271
+ ];
272
+
273
+ // 配置管理类
274
+ export class ModelConfigManager {
275
+ private static STORAGE_KEY_CHANNELS = 'multi-mind-chat-channels';
276
+ private static STORAGE_KEY_MODELS = 'multi-mind-chat-models';
277
+ private static STORAGE_KEY_ROLES = 'multi-mind-chat-roles';
278
+ private static STORAGE_KEY_INITIALIZED = 'multi-mind-chat-initialized';
279
+
280
+ // 初始化检查
281
+ private static ensureInitialized(): void {
282
+ try {
283
+ const isInitialized = localStorage.getItem(this.STORAGE_KEY_INITIALIZED);
284
+ if (!isInitialized) {
285
+ localStorage.setItem(this.STORAGE_KEY_CHANNELS, JSON.stringify(DEFAULT_CHANNELS));
286
+ localStorage.setItem(this.STORAGE_KEY_MODELS, JSON.stringify(DEFAULT_MODELS));
287
+ localStorage.setItem(this.STORAGE_KEY_ROLES, JSON.stringify(DEFAULT_ROLES));
288
+ localStorage.setItem(this.STORAGE_KEY_INITIALIZED, 'true');
289
+ }
290
+ } catch (error) {
291
+ console.warn('无法访问localStorage,将使用内存存储:', error);
292
+ }
293
+ }
294
+
295
+ // ============ 渠道管理 ============
296
+
297
+ // 获取所有渠道
298
+ static getChannels(): ApiChannel[] {
299
+ this.ensureInitialized();
300
+ try {
301
+ const stored = localStorage.getItem(this.STORAGE_KEY_CHANNELS);
302
+ if (stored) {
303
+ const parsed = JSON.parse(stored);
304
+ return parsed.map((channel: any) => ({
305
+ ...channel,
306
+ createdAt: new Date(channel.createdAt)
307
+ }));
308
+ }
309
+ } catch (error) {
310
+ console.warn('从localStorage加载渠道失败:', error);
311
+ }
312
+ return [...DEFAULT_CHANNELS];
313
+ }
314
+
315
+ // 保存渠道配置
316
+ static saveChannels(channels: ApiChannel[]): void {
317
+ try {
318
+ localStorage.setItem(this.STORAGE_KEY_CHANNELS, JSON.stringify(channels));
319
+ } catch (error) {
320
+ console.error('保存渠道到localStorage失败:', error);
321
+ throw new Error('无法保存渠道配置');
322
+ }
323
+ }
324
+
325
+ // 添加新渠道
326
+ static addChannel(channel: Omit<ApiChannel, 'id' | 'createdAt' | 'isCustom'>): ApiChannel {
327
+ const newChannel: ApiChannel = {
328
+ ...channel,
329
+ id: `channel-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
330
+ createdAt: new Date(),
331
+ isCustom: true
332
+ };
333
+
334
+ const channels = this.getChannels();
335
+
336
+ // 如果这是第一个渠道或设置为默认,清除其他默认标记
337
+ if (newChannel.isDefault || channels.length === 0) {
338
+ channels.forEach(ch => ch.isDefault = false);
339
+ newChannel.isDefault = true;
340
+ }
341
+
342
+ channels.push(newChannel);
343
+ this.saveChannels(channels);
344
+ return newChannel;
345
+ }
346
+
347
+ // 更新渠道
348
+ static updateChannel(id: string, updates: Partial<ApiChannel>): void {
349
+ const channels = this.getChannels();
350
+ const index = channels.findIndex(ch => ch.id === id);
351
+ if (index !== -1) {
352
+ // 如果设置为默认,清除其他默认标记
353
+ if (updates.isDefault) {
354
+ channels.forEach(ch => ch.isDefault = false);
355
+ }
356
+ channels[index] = { ...channels[index], ...updates };
357
+ this.saveChannels(channels);
358
+ }
359
+ }
360
+
361
+ // 删除渠道
362
+ static deleteChannel(id: string): void {
363
+ const channels = this.getChannels();
364
+ const filtered = channels.filter(ch => ch.id !== id);
365
+
366
+ // 如果删除的是默认渠道,设置第一个为默认
367
+ const deletedChannel = channels.find(ch => ch.id === id);
368
+ if (deletedChannel?.isDefault && filtered.length > 0) {
369
+ filtered[0].isDefault = true;
370
+ }
371
+
372
+ this.saveChannels(filtered);
373
+ }
374
+
375
+ // 获取默认渠道
376
+ static getDefaultChannel(): ApiChannel | null {
377
+ const channels = this.getChannels();
378
+ return channels.find(ch => ch.isDefault) || channels[0] || null;
379
+ }
380
+
381
+ // 根据ID获取渠道
382
+ static getChannelById(id: string): ApiChannel | null {
383
+ const channels = this.getChannels();
384
+ return channels.find(ch => ch.id === id) || null;
385
+ }
386
+
387
+ // 验证渠道配置
388
+ static validateChannel(channel: Partial<ApiChannel>): string[] {
389
+ const errors: string[] = [];
390
+
391
+ if (!channel.name?.trim()) {
392
+ errors.push('渠道名称不能为空');
393
+ }
394
+
395
+ if (!channel.baseUrl?.trim()) {
396
+ errors.push('API基础URL不能为空');
397
+ } else {
398
+ try {
399
+ new URL(channel.baseUrl);
400
+ } catch {
401
+ errors.push('API基础URL格式无效');
402
+ }
403
+ }
404
+
405
+ // 对于预置渠道,API密钥可以为空(将在使用时提醒用户配置)
406
+ // 对于自定义渠道,仍然要求API密钥
407
+ if (channel.isCustom && !channel.apiKey?.trim()) {
408
+ errors.push('API密钥不能为空');
409
+ }
410
+
411
+ if (channel.timeout && (typeof channel.timeout !== 'number' || channel.timeout < 1000)) {
412
+ errors.push('超时时间必须是大于1000毫秒的数字');
413
+ }
414
+
415
+ return errors;
416
+ }
417
+
418
+ // ============ 模型管理 ============
419
+
420
+ // 获取所有模型
421
+ static getModels(): AiModel[] {
422
+ this.ensureInitialized();
423
+ try {
424
+ const stored = localStorage.getItem(this.STORAGE_KEY_MODELS);
425
+ if (stored) {
426
+ const parsed = JSON.parse(stored);
427
+ return parsed.map((model: any) => ({
428
+ ...model,
429
+ createdAt: new Date(model.createdAt)
430
+ }));
431
+ }
432
+ } catch (error) {
433
+ console.warn('从localStorage加载模型失败:', error);
434
+ }
435
+ return [...DEFAULT_MODELS];
436
+ }
437
+
438
+ // 保存模型配置
439
+ static saveModels(models: AiModel[]): void {
440
+ try {
441
+ localStorage.setItem(this.STORAGE_KEY_MODELS, JSON.stringify(models));
442
+ } catch (error) {
443
+ console.error('保存模型到localStorage失败:', error);
444
+ throw new Error('无法保存模型配置');
445
+ }
446
+ }
447
+
448
+ // 添加新模型
449
+ static addModel(model: Omit<AiModel, 'id' | 'createdAt' | 'isCustom'>): AiModel {
450
+ const newModel: AiModel = {
451
+ ...model,
452
+ id: `model-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
453
+ createdAt: new Date(),
454
+ isCustom: true
455
+ };
456
+
457
+ const models = this.getModels();
458
+ models.push(newModel);
459
+ this.saveModels(models);
460
+ return newModel;
461
+ }
462
+
463
+ // 更新模型
464
+ static updateModel(id: string, updates: Partial<AiModel>): void {
465
+ const models = this.getModels();
466
+ const index = models.findIndex(m => m.id === id);
467
+ if (index !== -1) {
468
+ models[index] = { ...models[index], ...updates };
469
+ this.saveModels(models);
470
+ }
471
+ }
472
+
473
+ // 删除模型
474
+ static deleteModel(id: string): void {
475
+ const models = this.getModels();
476
+ const filtered = models.filter(m => m.id !== id);
477
+ this.saveModels(filtered);
478
+ }
479
+
480
+ // 验证模型配置
481
+ static validateModel(model: Partial<AiModel>): string[] {
482
+ const errors: string[] = [];
483
+
484
+ if (!model.name?.trim()) {
485
+ errors.push('模型名称不能为空');
486
+ }
487
+
488
+ if (!model.apiName?.trim()) {
489
+ errors.push('API模型名称不能为空');
490
+ }
491
+
492
+ if (!model.channelId?.trim()) {
493
+ errors.push('必须选择API渠道');
494
+ }
495
+
496
+ if (!model.category?.trim()) {
497
+ errors.push('模型类别不能为空');
498
+ }
499
+
500
+ if (typeof model.maxTokens !== 'number' || model.maxTokens < 1) {
501
+ errors.push('最大Token数必须是大于0的数字');
502
+ }
503
+
504
+ if (typeof model.temperature !== 'number' || model.temperature < 0 || model.temperature > 2) {
505
+ errors.push('温度参数必须在0-2之间');
506
+ }
507
+
508
+ return errors;
509
+ }
510
+
511
+ // ============ 角色管理 ============
512
+
513
+ // 获取所有角色
514
+ static getRoles(): AiRole[] {
515
+ this.ensureInitialized();
516
+ try {
517
+ const stored = localStorage.getItem(this.STORAGE_KEY_ROLES);
518
+ if (stored) {
519
+ return JSON.parse(stored);
520
+ }
521
+ } catch (error) {
522
+ console.warn('从localStorage加载角色失败:', error);
523
+ }
524
+ return [...DEFAULT_ROLES];
525
+ }
526
+
527
+ // 保存角色配置
528
+ static saveRoles(roles: AiRole[]): void {
529
+ try {
530
+ localStorage.setItem(this.STORAGE_KEY_ROLES, JSON.stringify(roles));
531
+ } catch (error) {
532
+ console.error('保存角色到localStorage失败:', error);
533
+ throw new Error('无法保存角色配置');
534
+ }
535
+ }
536
+
537
+ // 添加新角色
538
+ static addRole(role: Omit<AiRole, 'id'>): AiRole {
539
+ const newRole: AiRole = {
540
+ ...role,
541
+ id: `role-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
542
+ };
543
+
544
+ const roles = this.getRoles();
545
+ roles.push(newRole);
546
+ this.saveRoles(roles);
547
+ return newRole;
548
+ }
549
+
550
+ // 更新角色
551
+ static updateRole(id: string, updates: Partial<AiRole>): void {
552
+ const roles = this.getRoles();
553
+ const index = roles.findIndex(r => r.id === id);
554
+ if (index !== -1) {
555
+ roles[index] = { ...roles[index], ...updates };
556
+ this.saveRoles(roles);
557
+ }
558
+ }
559
+
560
+ // 删除角色
561
+ static deleteRole(id: string): void {
562
+ const roles = this.getRoles();
563
+ const filtered = roles.filter(r => r.id !== id);
564
+ this.saveRoles(filtered);
565
+ }
566
+
567
+ // 获取活跃角色
568
+ static getActiveRoles(): AiRole[] {
569
+ return this.getRoles().filter(role => role.isActive);
570
+ }
571
+
572
+ // ============ 工具方法 ============
573
+
574
+ // 根据类别分组模型
575
+ static getModelsByCategory(): Record<string, AiModel[]> {
576
+ const models = this.getModels();
577
+ return models.reduce((acc, model) => {
578
+ if (!acc[model.category]) {
579
+ acc[model.category] = [];
580
+ }
581
+ acc[model.category].push(model);
582
+ return acc;
583
+ }, {} as Record<string, AiModel[]>);
584
+ }
585
+
586
+ // 重置为默认配置
587
+ static resetToDefaults(): void {
588
+ try {
589
+ localStorage.removeItem(this.STORAGE_KEY_CHANNELS);
590
+ localStorage.removeItem(this.STORAGE_KEY_MODELS);
591
+ localStorage.removeItem(this.STORAGE_KEY_ROLES);
592
+ localStorage.removeItem(this.STORAGE_KEY_INITIALIZED);
593
+ this.ensureInitialized();
594
+ } catch (error) {
595
+ console.error('重置配置失败:', error);
596
+ throw new Error('无法重置配置');
597
+ }
598
+ }
599
+
600
+ // 导出配置
601
+ static exportConfig(): string {
602
+ return JSON.stringify({
603
+ channels: this.getChannels(),
604
+ models: this.getModels(),
605
+ roles: this.getRoles(),
606
+ exportedAt: new Date().toISOString(),
607
+ version: '2.0'
608
+ }, null, 2);
609
+ }
610
+
611
+ // 导入配置
612
+ static importConfig(configJson: string): { success: boolean; message: string } {
613
+ try {
614
+ const config = JSON.parse(configJson);
615
+
616
+ if (!config.channels && !config.models && !config.roles) {
617
+ return { success: false, message: '配置文件格式无效,缺少必要的配置信息' };
618
+ }
619
+
620
+ if (config.channels && Array.isArray(config.channels)) {
621
+ const validChannels = config.channels.filter((channel: any) => {
622
+ const errors = this.validateChannel(channel);
623
+ return errors.length === 0;
624
+ });
625
+
626
+ if (validChannels.length > 0) {
627
+ const processedChannels = validChannels.map((channel: any) => ({
628
+ ...channel,
629
+ createdAt: new Date(channel.createdAt || new Date()),
630
+ isCustom: channel.isCustom !== false
631
+ }));
632
+ this.saveChannels(processedChannels);
633
+ }
634
+ }
635
+
636
+ if (config.models && Array.isArray(config.models)) {
637
+ const validModels = config.models.filter((model: any) => {
638
+ const errors = this.validateModel(model);
639
+ return errors.length === 0;
640
+ });
641
+
642
+ if (validModels.length > 0) {
643
+ const processedModels = validModels.map((model: any) => ({
644
+ ...model,
645
+ createdAt: new Date(model.createdAt || new Date()),
646
+ isCustom: model.isCustom !== false
647
+ }));
648
+ this.saveModels(processedModels);
649
+ }
650
+ }
651
+
652
+ if (config.roles && Array.isArray(config.roles)) {
653
+ this.saveRoles(config.roles);
654
+ }
655
+
656
+ return { success: true, message: '配置导入成功' };
657
+ } catch (error) {
658
+ return { success: false, message: `配置导入失败: ${error instanceof Error ? error.message : '未知错误'}` };
659
+ }
660
+ }
661
+
662
+ // 清空所有数据
663
+ static clearAllData(): void {
664
+ try {
665
+ localStorage.removeItem(this.STORAGE_KEY_CHANNELS);
666
+ localStorage.removeItem(this.STORAGE_KEY_MODELS);
667
+ localStorage.removeItem(this.STORAGE_KEY_ROLES);
668
+ localStorage.removeItem(this.STORAGE_KEY_INITIALIZED);
669
+ } catch (error) {
670
+ console.error('清空数据失败:', error);
671
+ throw new Error('无法清空配置数据');
672
+ }
673
+ }
674
+
675
+ // 获取存储使用情况
676
+ static getStorageInfo(): { used: number; available: number; channels: number; models: number; roles: number } {
677
+ try {
678
+ const channelsData = localStorage.getItem(this.STORAGE_KEY_CHANNELS) || '';
679
+ const modelsData = localStorage.getItem(this.STORAGE_KEY_MODELS) || '';
680
+ const rolesData = localStorage.getItem(this.STORAGE_KEY_ROLES) || '';
681
+ const used = channelsData.length + modelsData.length + rolesData.length;
682
+
683
+ return {
684
+ used,
685
+ available: 5242880 - used, // 5MB 大致容量
686
+ channels: this.getChannels().length,
687
+ models: this.getModels().length,
688
+ roles: this.getRoles().length
689
+ };
690
+ } catch (error) {
691
+ return { used: 0, available: 0, channels: 0, models: 0, roles: 0 };
692
+ }
693
+ }
694
+ }
695
+
696
+ // 其他常量配置
697
+ export const DEFAULT_MANUAL_FIXED_TURNS = 2;
698
+ export const MIN_MANUAL_FIXED_TURNS = 1;
699
+ export const MAX_MANUAL_FIXED_TURNS = 5;
700
+ export const MAX_AI_DRIVEN_DISCUSSION_TURNS_PER_MODEL = 3;
701
+
702
+ // 修复:初始记事本内容设为空,避免AI误引用
703
+ export const INITIAL_NOTEPAD_CONTENT = ``;
704
+
705
+ // 优化:明确说明记事本内容仅供参考,不应在回复中重复
706
+ export const NOTEPAD_INSTRUCTION_PROMPT_PART = `
707
+ You also have access to a shared notepad for collaborative note-taking.
708
+ Current Notepad Content:
709
+ ---
710
+ {notepadContent}
711
+ ---
712
+ IMPORTANT: The notepad content above is for your reference only. Do NOT repeat or quote the notepad content in your response unless specifically relevant to your answer.
713
+
714
+ Instructions for Notepad Updates:
715
+ 1. To update the notepad, include a section at the very end of your response, formatted exactly as:
716
+ <notepad_update>
717
+ [YOUR NEW FULL NOTEPAD CONTENT HERE. THIS WILL REPLACE THE ENTIRE CURRENT NOTEPAD CONTENT.]
718
+ </notepad_update>
719
+ 2. If you do not want to change the notepad, do NOT include the <notepad_update> section at all.
720
+ 3. Your primary spoken response to the ongoing discussion should come BEFORE any <notepad_update> section. Ensure you still provide a spoken response.
721
+ 4. Only update the notepad when you have important information to record, not for every response.
722
+ `;
723
+
724
+ export const NOTEPAD_UPDATE_TAG_START = "<notepad_update>";
725
+ export const NOTEPAD_UPDATE_TAG_END = "</notepad_update>";
726
+ export const DISCUSSION_COMPLETE_TAG = "<discussion_complete />";
727
+
728
+ export const AI_DRIVEN_DISCUSSION_INSTRUCTION_PROMPT_PART = `
729
+ Instruction for ending discussion: If you believe the current topic has been sufficiently explored between you and your AI partner for the final synthesis, include the exact tag ${DISCUSSION_COMPLETE_TAG} at the very end of your current message (after any notepad update). Do not use this tag if you wish to continue the discussion or require more input/response from your partner.
730
+ `;
731
+
732
+ export enum DiscussionMode {
733
+ FixedTurns = 'fixed',
734
+ AiDriven = 'ai-driven',
735
  }
package-lock.json CHANGED
The diff for this file is too large to render. See raw diff
 
services/openaiService.ts CHANGED
@@ -41,7 +41,8 @@ export const generateResponse = async (
41
  imagePart?: { inlineData: { mimeType: string; data: string } },
42
  customBaseUrl?: string,
43
  apiKey?: string,
44
- onStreamChunk?: (newChunk: string, fullText: string, isComplete: boolean) => void
 
45
  ): Promise<OpenAIResponse> => {
46
  const startTime = performance.now();
47
 
@@ -87,9 +88,11 @@ export const generateResponse = async (
87
  const requestBody = {
88
  model: modelName,
89
  messages: messages,
90
- stream: !!onStreamChunk, // 只有提供回调时才使用流式
91
  temperature: shouldUseReducedCapacity ? 0.3 : 0.7,
92
- max_tokens: shouldUseReducedCapacity ? 1000 : 4000
 
 
93
  };
94
 
95
  const apiBase = customBaseUrl || DEFAULT_OPENAI_API_BASE;
 
41
  imagePart?: { inlineData: { mimeType: string; data: string } },
42
  customBaseUrl?: string,
43
  apiKey?: string,
44
+ onStreamChunk?: (newChunk: string, fullText: string, isComplete: boolean) => void,
45
+ maxTokens?: number // 新增参数
46
  ): Promise<OpenAIResponse> => {
47
  const startTime = performance.now();
48
 
 
88
  const requestBody = {
89
  model: modelName,
90
  messages: messages,
91
+ stream: !!onStreamChunk,
92
  temperature: shouldUseReducedCapacity ? 0.3 : 0.7,
93
+ max_tokens: shouldUseReducedCapacity
94
+ ? Math.min(1000, maxTokens || 4000) // 快速模式下使用较小值
95
+ : (maxTokens || 4000) // 使用传入的 maxTokens 或默认值
96
  };
97
 
98
  const apiBase = customBaseUrl || DEFAULT_OPENAI_API_BASE;