Compare commits

..

1 Commits

Author SHA1 Message Date
pzvjlefpx 560c798f1f src1.1
2 months ago

@ -1,4 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.12" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12" project-jdk-type="Python SDK" />
</project>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>

@ -15,8 +15,12 @@
response = client.chat("Hello, world!", model="gpt-4o")
"""
from .client import LLMClient
from .models import ModelProvider, LLMModel
try:
from .client import LLMClient
from .models import ModelProvider, LLMModel
except ImportError:
from client import LLMClient
from models import ModelProvider, LLMModel
from .exceptions import LLMAPIError, ModelNotFoundError, APIKeyError
from .utils import call_llm
from .multi_perspective_engine import MultiPerspectiveEngine

@ -1,8 +1,10 @@
{
"default_provider": "OpenAI",
"default_chat_model": "gpt-4o",
"default_structured_model": "gpt-4o",
"default_provider": "SparkAI",
"default_chat_model": "4.0Ultra",
"default_structured_model": "4.0Ultra",
"request_timeout": 30,
"OPENAI_API_KEY": "your-openai-api-key-here",
"OPENAI_BASE_URL": "https://api.openai.com/v1"
"SPARKAI_APP_ID": "7eaa68c4",
"SPARKAI_API_SECRET": "ZDRlYjA5MmZjZjkwODIxYTA0MjM3ZmRi",
"SPARKAI_API_KEY": "61bbbe0cb7b562612dc3841f583594bd",
"SPARKAI_BASE_URL": "wss://spark-api.xf-yun.com/v3.5/chat"
}

@ -17,6 +17,7 @@ logger = logging.getLogger(__name__)
# 添加llm-api到路径
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'llm-api'))
from sparkai_service import sync_sparkai_chat
from fast_client import quick_generate, quick_test
from models import ModelProvider, LLMModel
@ -28,52 +29,136 @@ class AIService:
def __init__(self):
self.timeout = config.LLM_TIMEOUT
# 默认AI设置
self.providers = {
'OpenAI': 'openai_client.OpenAIClient',
'Anthropic': 'anthropic_client.AnthropicClient',
'Google': 'google_client.GoogleAIClient',
'Groq': 'groq_client.GroqClient',
'DeepSeek': 'deepseek_client.DeepSeekClient',
'SparkAI': 'sparkai_service.SparkAIClient' # 讯飞星火
}
self.default_settings = {
'provider': 'Ollama',
'model': 'qwen3:0.6b',
'api_key': '',
'base_url': 'http://localhost:11434'
}
def get_client(self, provider: str, **kwargs):
"""获取AI客户端 - 延迟导入实现"""
if provider not in self.providers:
raise ValueError(f"不支持的AI供应商: {provider}")
# 动态导入对应的客户端模块
module_class_path = self.providers[provider]
module_name, class_name = module_class_path.rsplit('.', 1)
try:
module = __import__(module_name, fromlist=[class_name])
client_class = getattr(module, class_name)
return client_class(**kwargs)
except ImportError as e:
raise ImportError(f"无法导入{provider}客户端: {str(e)}")
except AttributeError as e:
raise AttributeError(f"在模块{module_name}中找不到类{class_name}: {str(e)}")
def chat_completion(self, messages: List[Dict], user_settings: Dict) -> Dict[str, Any]:
"""统一的聊天补全接口"""
provider = user_settings.get('provider', 'OpenAI')
if provider == 'SparkAI':
# 讯飞星火需要特殊的消息格式处理和WebSocket连接
formatted_messages = []
for msg in messages:
if msg['role'] == 'system':
# 讯飞星火没有system角色将system消息转换为user消息
formatted_messages.append({
"role": "user",
"content": msg['content']
})
else:
formatted_messages.append(msg)
# 直接调用SparkAI服务绕过fast_client的HTTP处理
return self._call_sparkai_directly(formatted_messages, user_settings)
else:
# 其他供应商使用原有逻辑
return sync_chat_completion(messages, user_settings)
def _call_sparkai_directly(self, messages: List[Dict], user_settings: Dict) -> Dict[str, Any]:
"""直接调用讯飞星火WebSocket API"""
try:
from sparkai_service import sync_sparkai_chat
# 提取SparkAI专用配置
spark_settings = {
'app_id': user_settings.get('app_id') or user_settings.get('api_key', '').split('|')[
0] if '|' in user_settings.get('api_key', '') else '',
'api_key': user_settings.get('api_key', ''),
'api_secret': user_settings.get('api_secret') or user_settings.get('api_key', '').split('|')[
1] if '|' in user_settings.get('api_key', '') and len(
user_settings.get('api_key', '').split('|')) > 1 else '',
'domain': user_settings.get('model', '4.0Ultra')
}
return sync_sparkai_chat(messages, spark_settings)
except Exception as e:
logger.error(f"SparkAI直接调用失败: {e}")
# 返回结构化错误响应
return {
"choices": [{
"message": {
"role": "assistant",
"content": f"SparkAI服务暂时不可用: {str(e)}"
}
}],
"error": str(e)
}
def _get_ai_settings(self, user_settings: Optional[Dict] = None) -> Dict[str, Any]:
"""获取AI设置"""
if user_settings:
return user_settings
return self.default_settings
def _create_llm_model(self, settings: Dict) -> LLMModel:
"""创建LLM模型对象"""
try:
provider = ModelProvider(settings['provider'])
except ValueError:
provider = ModelProvider.OLLAMA
model = LLMModel(
display_name=settings['model'],
model_name=settings['model'],
provider=provider
)
# 处理base_url格式化
# 处理base_url格式化 - 特别处理SparkAI的WebSocket URL
base_url = settings.get('base_url', '')
if base_url and not base_url.startswith(('http://', 'https://')):
# 自动添加http://协议前缀
base_url = f'http://{base_url}'
logger.debug(f"自动添加协议前缀格式化后的base_url: {base_url}")
if base_url:
# 如果是SparkAI保持wss://协议不变
if settings['provider'] == 'SparkAI':
if not base_url.startswith('wss://'):
# 自动添加wss://协议前缀
base_url = f'wss://{base_url.replace("http://", "").replace("https://", "")}'
else:
# 其他供应商使用HTTP协议
if not base_url.startswith(('http://', 'https://')):
base_url = f'http://{base_url}'
logger.debug(f"格式化后的base_url: {base_url}")
# 添加API配置
if hasattr(model, 'api_key'):
model.api_key = settings.get('api_key', '')
if hasattr(model, 'base_url'):
model.base_url = base_url
else:
# 如果模型没有base_url属性动态添加
setattr(model, 'api_key', settings.get('api_key', ''))
setattr(model, 'base_url', base_url)
return model
def _extract_context_info(self, context: Dict) -> Dict[str, str]:

@ -9,14 +9,14 @@
"grade": "初中",
"brainstorm_content": "",
"outline_content": "",
"writing_content": "",
"writing_content": "However, the ethical implications surrounding AI is complex and multifaceted. The question of whom should be held accountable for the decisions made by an autonomous system remains largely unanswered. For instance, if an algorithm denies someone a loan or parole, who is to blame? The programmer, the company, or the algorithm itself? This quandary, amongst others, pose a significant threat to our established legal frameworks.",
"ai_feedback": "",
"final_score": 0,
"scores": "{\"brainstorm\": 0, \"outline\": 0, \"writing\": 0, \"highlight\": 0}",
"status": "writing",
"word_count": 0,
"word_count": 364,
"created_at": "2025-10-01T13:18:06.056827",
"updated_at": "2025-10-09T10:43:01.941600",
"updated_at": "2025-10-13T16:22:46.862208",
"completed_at": null
}
]

@ -6,10 +6,13 @@
"grade": "高中",
"subject": "英语",
"created_at": "2025-07-07T23:12:26.092924",
"ai_provider": "",
"ai_model": "",
"ai_provider": "SparkAI",
"ai_model": "Spark-4.0-Ultra",
"ai_api_key": "",
"ai_base_url": "",
"updated_at": "2025-10-09T10:43:25.409404"
"ai_base_url": "wss://spark-api.xf-yun.com/v4.0/chat",
"updated_at": "2025-10-13T16:19:53.734216",
"ai_app_id": "",
"ai_api_secret": "",
"ai_domain": "4.0Ultra"
}
]

@ -39,7 +39,10 @@ def get_user_ai_settings(user_id: int) -> dict:
'provider': 'Ollama', # 默认使用Ollama
'model': 'gemma3:latest',
'api_key': '',
'base_url': 'localhost:11434'
'base_url': 'localhost:11434',
'app_id': '', # 新增讯飞星火字段
'api_secret': '', # 新增讯飞星火字段
'domain': '4.0Ultra' # 新增讯飞星火字段
}
# 如果用户没有设置AI配置使用默认值
@ -51,7 +54,10 @@ def get_user_ai_settings(user_id: int) -> dict:
'provider': provider,
'model': model,
'api_key': user.get('ai_api_key', ''),
'base_url': base_url
'base_url': base_url,
'app_id': user.get('ai_app_id', ''), # 新增讯飞星火字段
'api_secret': user.get('ai_api_secret', ''), # 新增讯飞星火字段
'domain': user.get('ai_domain', '4.0Ultra') # 新增讯飞星火字段
}
def build_ai_context(user, project=None, extra_context=None) -> dict:
@ -174,19 +180,26 @@ def update_user_ai_settings():
ai_model = data.get('ai_model', '')
ai_api_key = data.get('ai_api_key', '')
ai_base_url = data.get('ai_base_url', '')
ai_app_id = data.get('ai_app_id', '') # 新增讯飞星火字段
ai_api_secret = data.get('ai_api_secret', '') # 新增讯飞星火字段
ai_domain = data.get('ai_domain', '4.0Ultra') # 新增讯飞星火字段
@with_user_dao
def _update_user_ai(dao):
user = dao.get_user_by_id(user_id)
if not user:
return None
# 保存更新 - 直接传递字段而不是整个user对象
updated_user = dao.update_user(user_id,
# 保存更新 - 添加讯飞星火字段
updated_user = dao.update_user(user_id,
ai_provider=ai_provider,
ai_model=ai_model,
ai_api_key=ai_api_key,
ai_base_url=ai_base_url
ai_base_url=ai_base_url,
ai_app_id=ai_app_id, # 新增
ai_api_secret=ai_api_secret, # 新增
ai_domain=ai_domain # 新增
)
return updated_user

@ -0,0 +1,152 @@
"""
讯飞星火AI服务模块
"""
import json
import base64
import hashlib
import hmac
import time
from datetime import datetime
from urllib.parse import urlparse
import ssl
from dateutil import parser
from websocket import create_connection
import websocket
import threading
from typing import Dict, Any, Optional, List
class SparkAIClient:
"""讯飞星火AI客户端"""
def __init__(self, app_id: str, api_key: str, api_secret: str, domain: str = "4.0Ultra"):
self.app_id = app_id
self.api_key = api_key
self.api_secret = api_secret
self.domain = domain
self.base_url = "wss://spark-api.xf-yun.com/v4.0/chat"
def _create_auth_url(self) -> str:
"""创建认证URL"""
from urllib.parse import urlparse, urlencode
import time
# 生成RFC1123格式的时间戳
now = time.time()
date = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(now))
# 解析URL
url_parts = urlparse(self.base_url)
host = url_parts.hostname
path = url_parts.path
# 生成签名原始字符串
signature_origin = f"host: {host}\ndate: {date}\nGET {path} HTTP/1.1"
# 使用API Secret进行HMAC-SHA256加密
signature_sha = hmac.new(
self.api_secret.encode('utf-8'),
signature_origin.encode('utf-8'),
digestmod=hashlib.sha256
).digest()
# Base64编码
signature = base64.b64encode(signature_sha).decode('utf-8')
# 构建Authorization参数
authorization_origin = f'api_key="{self.api_key}", algorithm="hmac-sha256", headers="host date request-line", signature="{signature}"'
authorization = base64.b64encode(authorization_origin.encode('utf-8')).decode('utf-8')
# 构建请求URL
params = {
"authorization": authorization,
"date": date,
"host": host
}
auth_url = f"{self.base_url}?{urlencode(params)}"
return auth_url
def chat_completion(self, messages: List[Dict], temperature: float = 0.7, max_tokens: int = 2048) -> Dict[str, Any]:
"""与讯飞星火进行对话"""
try:
# 创建认证URL
auth_url = self._create_auth_url()
# 创建WebSocket连接
ws = create_connection(auth_url)
# 构建请求数据
request_data = {
"header": {
"app_id": self.app_id
},
"parameter": {
"chat": {
"domain": self.domain,
"temperature": temperature,
"max_tokens": max_tokens
}
},
"payload": {
"message": {
"text": messages
}
}
}
# 发送请求
ws.send(json.dumps(request_data))
# 接收响应
full_response = ""
while True:
result = ws.recv()
data = json.loads(result)
# 检查错误
if data["header"]["code"] != 0:
error_msg = data["header"]["message"]
raise Exception(f"讯飞星火API错误: {error_msg}")
# 累加文本内容
if "text" in data["payload"]["choices"]:
for text in data["payload"]["choices"]["text"]:
full_response += text["content"]
# 检查是否结束
if data["header"]["status"] == 2:
break
ws.close()
return {
"choices": [{
"message": {
"role": "assistant",
"content": full_response
}
}],
"usage": {
"total_tokens": len(full_response) // 4 # 估算token数量
}
}
except Exception as e:
raise Exception(f"讯飞星火通信失败: {str(e)}")
def sync_sparkai_chat(messages: List[Dict], user_settings: Dict) -> Dict[str, Any]:
"""同步调用讯飞星火聊天接口"""
try:
client = SparkAIClient(
app_id=user_settings.get('app_id', ''),
api_key=user_settings.get('api_key', ''),
api_secret=user_settings.get('api_secret', ''),
domain=user_settings.get('domain', '4.0Ultra')
)
return client.chat_completion(messages)
except Exception as e:
raise Exception(f"讯飞星火服务调用失败: {str(e)}")

@ -1334,17 +1334,41 @@ function updateNavModelOptions() {
const baseUrlSection = document.getElementById('navBaseUrlSection');
const baseUrlInput = document.getElementById('navAIBaseUrl');
const apiKeyInput = document.getElementById('navAPIKey');
const sparkAISection = document.getElementById('navSparkAISection');
if (!provider || !modelSelect) return;
// 永远显示Base URL字段
baseUrlSection.style.display = 'block';
// 根据供应商类型显示/隐藏API Key字段
if (provider === 'Ollama' || provider === 'LMStudio') {
modelSelect.innerHTML = '<option value="">请选择模型</option>';
if (provider === 'SparkAI') {
// 讯飞星火显示特定字段隐藏API Key
apiKeySection.style.display = 'none';
if (sparkAISection) sparkAISection.style.display = 'block';
// 设置默认BaseUrl
if (!baseUrlInput.value) {
baseUrlInput.value = 'wss://spark-api.xf-yun.com/v4.0/chat';
}
// 预定义的讯飞星火模型 - 确保这里正确执行
const sparkModels = ['Spark-4.0-Ultra', 'Spark-3.5-Pro', 'Spark-2.0'];
sparkModels.forEach(model => {
const option = document.createElement('option');
option.value = model;
option.textContent = model;
modelSelect.appendChild(option);
});
console.log('SparkAI模型列表已刷新:', sparkModels);
} else if (provider === 'Ollama' || provider === 'LMStudio') {
// 本地服务隐藏API Key
apiKeySection.style.display = 'none';
if (sparkAISection) sparkAISection.style.display = 'none';
// 设置默认BaseUrl如果当前为空
if (!baseUrlInput.value) {
if (provider === 'Ollama') {
@ -1353,17 +1377,18 @@ function updateNavModelOptions() {
baseUrlInput.value = 'http://localhost:1234';
}
}
// 清空API Key
apiKeyInput.value = '';
// 获取本地模型列表
modelSelect.innerHTML = '<option value="">加载中...</option>';
UserSettings.refreshAIModels();
} else if (provider) {
// 云端服务显示API Key
apiKeySection.style.display = 'block';
if (sparkAISection) sparkAISection.style.display = 'none';
// 设置默认BaseUrl如果当前为空
if (!baseUrlInput.value) {
const defaultBaseUrls = {
@ -1371,7 +1396,8 @@ function updateNavModelOptions() {
'Anthropic': 'https://api.anthropic.com',
'Google': 'https://generativelanguage.googleapis.com/v1beta',
'Groq': 'https://api.groq.com/openai/v1',
'DeepSeek': 'https://api.deepseek.com'
'DeepSeek': 'https://api.deepseek.com',
'SparkAI': 'wss://spark-api.xf-yun.com/v4.0/chat'
};
baseUrlInput.value = defaultBaseUrls[provider] || '';
}
@ -1382,7 +1408,8 @@ function updateNavModelOptions() {
'Anthropic': ['claude-3-5-haiku-latest', 'claude-3-5-sonnet-latest'],
'Google': ['gemini-2.0-flash-exp', 'gemini-1.5-pro'],
'Groq': ['llama-3.3-70b-versatile', 'mixtral-8x7b-32768'],
'DeepSeek': ['deepseek-chat', 'deepseek-reasoner']
'DeepSeek': ['deepseek-chat', 'deepseek-reasoner'],
'SparkAI': ['Spark-4.0-Ultra', 'Spark-3.5-Pro']
};
if (AI_MODELS[provider]) {
@ -1404,6 +1431,7 @@ function updateNavModelOptions() {
// 清空API Key但保留Base URL
apiKeyInput.value = '';
}
console.log('供应商切换完成:', provider, '当前模型数量:', modelSelect.options.length);
}
function showUserProfile() {

@ -166,6 +166,7 @@
<option value="Google">Google</option>
<option value="Groq">Groq</option>
<option value="DeepSeek">DeepSeek</option>
<option value="SparkAI">讯飞星火</option> <!-- 新增讯飞星火 -->
<option value="Ollama">Ollama (本地)</option>
<option value="LMStudio">LM Studio (本地)</option>
</select>

@ -582,6 +582,7 @@
<option value="Google">Google</option>
<option value="Groq">Groq</option>
<option value="DeepSeek">DeepSeek</option>
<option value="SparkAI">讯飞星火</option> <!-- 新增讯飞星火 -->
<option value="Ollama">Ollama (本地)</option>
<option value="LMStudio">LM Studio (本地)</option>
</select>
@ -778,6 +779,7 @@ const AI_MODELS = {
'Google': ['gemini-2.0-flash-exp', 'gemini-1.5-pro'],
'Groq': ['llama-3.3-70b-versatile', 'mixtral-8x7b-32768'],
'DeepSeek': ['deepseek-chat', 'deepseek-reasoner'],
'SparkAI': ['4.0Ultra'],
'Ollama': [],
'LMStudio': []
};
@ -799,13 +801,59 @@ function loadAISettings() {
}
}
function setupSparkAIConfig() {
const provider = document.getElementById('navAIProvider').value; // 注意文档1中是 navAIProvider
const configArea = document.querySelector('.ai-settings');
if (provider === 'SparkAI') {
// 显示额外的配置字段
if (!document.getElementById('sparkExtraConfig')) {
const extraConfig = `
<div class="row mt-2" id="sparkExtraConfig">
<div class="col-6">
<label class="form-label small mb-1">App ID</label>
<input type="text" class="form-control form-control-sm" id="sparkAppId"
placeholder="输入App ID">
</div>
<div class="col-6">
<label class="form-label small mb-1">API Secret</label>
<input type="password" class="form-control form-control-sm" id="sparkApiSecret"
placeholder="输入API Secret">
</div>
</div>
`;
// 插入到配置区域
const baseUrlSection = document.getElementById('navBaseUrlSection');
if (baseUrlSection && baseUrlSection.parentNode) {
baseUrlSection.parentNode.insertAdjacentHTML('afterend', extraConfig);
}
}
} else {
// 移除讯飞星火专用配置
const extraConfig = document.getElementById('sparkExtraConfig');
if (extraConfig) {
extraConfig.remove();
}
}
}
// 供应商选择变化时更新模型选项
document.addEventListener('DOMContentLoaded', function() {
document.getElementById('aiProvider').addEventListener('change', function() {
updateModelOptions();
updateFormFields();
});
document.getElementById('aiProvider').addEventListener('change', function() {
const aiProvider = document.getElementById('aiProvider');
if (aiProvider) {
aiProvider.addEventListener('change', setupSparkAIConfig);
// 初始化时也执行一次
setupSparkAIConfig();
}
});
// BaseUrl改变时自动刷新模型列表
document.getElementById('baseUrl').addEventListener('blur', function() {
const provider = document.getElementById('aiProvider').value;
@ -821,6 +869,7 @@ document.addEventListener('DOMContentLoaded', function() {
updateModelOptions();
}
}, 1000)); // 1秒延迟避免频繁请求
});
function updateModelOptions() {
@ -846,24 +895,38 @@ function updateModelOptions() {
}
}
// 更新表单字段显示逻辑
function updateFormFields() {
const provider = document.getElementById('aiProvider').value;
const apiKeySection = document.getElementById('apiKeySection');
const baseUrlSection = document.getElementById('baseUrlSection');
if (provider === 'Ollama' || provider === 'LMStudio') {
apiKeySection.style.display = 'none';
baseUrlSection.style.display = 'block';
// 设置默认URL
if (provider === 'Ollama') {
document.getElementById('baseUrl').value = 'http://localhost:11434';
} else if (provider === 'LMStudio') {
document.getElementById('baseUrl').value = 'http://localhost:1234';
}
} else if (provider === 'SparkAI') {
// 讯飞星火需要API Key和AppID/Secret
apiKeySection.style.display = 'block';
baseUrlSection.style.display = 'block';
document.getElementById('baseUrl').value = 'wss://spark-api.xf-yun.com/v3.5/chat';
// 更新API Key标签提示
document.querySelector('label[for="apiKey"]').textContent = 'API Key (格式: appid|apisecret)';
document.querySelector('#apiKey').placeholder = '请输入appid和apisecret用|分隔';
} else if (provider) {
apiKeySection.style.display = 'block';
baseUrlSection.style.display = 'none';
// 恢复默认标签
document.querySelector('label[for="apiKey"]').textContent = 'API Key';
document.querySelector('#apiKey').placeholder = '输入您的API Key';
} else {
apiKeySection.style.display = 'none';
baseUrlSection.style.display = 'none';

Loading…
Cancel
Save