Spaces:
Sleeping
Sleeping
Upload 20 files
Browse files- App.tsx +6 -4
- constants.ts +734 -734
- package-lock.json +0 -0
- 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 |
-
|
| 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
|
|
|
|
|
|
|
| 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;
|