import sys
import json
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QTextEdit, QLineEdit, QPushButton, QLabel, QSplitter, QListWidget,
QStatusBar, QMessageBox
)
from PyQt5.QtGui import QFont, QPalette, QColor, QTextCursor
from PyQt5.QtCore import Qt, QThread, pyqtSignal
import requests
import base64
import hmac
import hashlib
import urllib.parse
from datetime import datetime, timezone
# 火山引擎 API 配置
API_URL = "https://open.volcengineapi.com/api/v3/chat/completions?Action=Chat&Version=2023-08-01"
ACCESS_KEY = "AKLTMjA2YWNlYmIwNDAyNGMxOThkMjBkNTQxNjEwMmFhNzA"
SECRET_KEY = "TmpJeU9XTTVaVEUwTlROak5HWmhaamcyTldZNVpqVTFaR1kxWXpZek5HVQ=="
class VolcEngineWorker(QThread):
"""后台线程处理火山引擎 API 调用"""
response_received = pyqtSignal(str, bool) # 信号:回复内容, 是否错误
status_update = pyqtSignal(str) # 状态更新信号
def __init__(self, prompt, parent=None):
super().__init__(parent)
self.prompt = prompt
def run(self):
"""线程主函数"""
try:
self.status_update.emit("正在生成回复...")
response = self.call_volcengine_api(self.prompt)
self.response_received.emit(response, False)
except Exception as e:
self.response_received.emit(f"错误: {str(e)}", True)
def generate_signature(self, secret_key, method, path, query_params, date):
"""生成火山引擎 API 签名"""
sorted_keys = sorted(query_params.keys())
canonical_query = "&".join(
f"{urllib.parse.quote(k, safe='')}={urllib.parse.quote([0], safe='')}"
for k in sorted_keys
)
signature_origin = (
f"{method} {path} HTTP/1.1\n"
f"Host: open.volcengineapi.com\n"
f"Date: {date}\n"
f"{canonical_query}"
)
decoded_secret = base64.b64decode(secret_key)
signature = hmac.new(
decoded_secret,
signature_origin.encode('utf-8'),
hashlib.sha256
).digest()
return base64.b64encode(signature).decode()
def call_volcengine_api(self, prompt):
"""调用火山引擎聊天 API"""
messages = [
{"role": "system", "content": "你是有帮助的助手"},
{"role": "user", "content": prompt}
]
request_data = {
"messages": messages,
"parameters": {
"model": "skylark-lite-public",
"temperature": 0.5,
"max_tokens": 1024
}
}
date = datetime.now(timezone.utc).strftime("%a, %d %b %Y %H:%M:%S GMT")
parsed_url = urllib.parse.urlparse(API_URL)
query_params = urllib.parse.parse_qs(parsed_url.query)
signature = self.generate_signature(
SECRET_KEY,
"POST",
parsed_url.path,
query_params,
date
)
auth_data = {
"access_key": ACCESS_KEY,
"algorithm": "HMAC-SHA256",
"headers": "host date",
"signature": signature
}
authorization = base64.b64encode(json.dumps(auth_data).encode()).decode()
headers = {
"Authorization": authorization,
"Date": date,
"Host": "open.volcengineapi.com",
"Content-Type": "application/json"
}
response = requests.post(
API_URL,
headers=headers,
json=request_data,
timeout=30
)
if response.status_code == 200:
data = response.json()
return data.get("choices", [{}])[0].get("message", {}).get("content", "")
else:
return f"API请求失败: {response.status_code} - {response.text}"
class ChatApplication(QMainWindow):
"""火山引擎聊天应用主界面"""
def __init__(self):
super().__init__()
self.init_ui()
self.setWindowTitle("火山引擎聊天助手")
self.resize(800, 600)
# 聊天历史
self.chat_history = []
def init_ui(self):
"""初始化用户界面"""
# 主窗口部件
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QVBoxLayout(central_widget)
# 分割布局
splitter = QSplitter(Qt.Horizontal)
main_layout.addWidget(splitter)
# 左侧面板 - 聊天历史
left_panel = QWidget()
left_layout = QVBoxLayout(left_panel)
left_layout.setContentsMargins(0, 0, 0, 0)
history_label = QLabel("聊天历史")
history_label.setFont(QFont("Arial", 10, QFont.Bold))
history_label.setStyleSheet("padding: 5px; background: #f0f0f0;")
left_layout.addWidget(history_label)
self.history_list = QListWidget()
self.history_list.setStyleSheet("""
QListWidget {
background-color: #f8f8f8;
border: none;
}
QListWidget::item {
padding: 8px;
border-bottom: 1px solid #e0e0e0;
}
QListWidget::item:selected {
background-color: #e0f0ff;
}
""")
self.history_list.itemClicked.connect(self.load_chat_history)
left_layout.addWidget(self.history_list)
splitter.addWidget(left_panel)
# 右侧面板 - 聊天区域
right_panel = QWidget()
right_layout = QVBoxLayout(right_panel)
right_layout.setContentsMargins(0, 0, 0, 0)
# 聊天显示区域
self.chat_display = QTextEdit()
self.chat_display.setReadOnly(True)
self.chat_display.setStyleSheet("""
QTextEdit {
background-color: #ffffff;
border: none;
padding: 10px;
font-size: 14px;
}
""")
self.chat_display.setFont(QFont("Arial", 12))
right_layout.addWidget(self.chat_display)
# 输入区域
input_layout = QHBoxLayout()
self.input_field = QLineEdit()
self.input_field.setPlaceholderText("输入消息...")
self.input_field.setStyleSheet("""
QLineEdit {
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
}
""")
self.input_field.returnPressed.connect(self.send_message)
input_layout.addWidget(self.input_field)
self.send_button = QPushButton("发送")
self.send_button.setStyleSheet("""
QPushButton {
background-color: #4a90e2;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
font-weight: bold;
}
QPushButton:hover {
background-color: #3a7bc8;
}
QPushButton:disabled {
background-color: #cccccc;
}
""")
self.send_button.clicked.connect(self.send_message)
input_layout.addWidget(self.send_button)
right_layout.addLayout(input_layout)
splitter.addWidget(right_panel)
# 设置分割比例
splitter.setSizes([200, 600])
# 状态栏
self.status_bar = QStatusBar()
self.setStatusBar(self.status_bar)
self.status_bar.showMessage("就绪")
# 添加初始消息
self.add_message("系统", "欢迎使用火山引擎聊天助手!请输入您的问题开始对话。", False)
def add_message(self, sender, message, is_user=True):
"""添加消息到聊天显示区域"""
# 格式化消息
if is_user:
html = f"""
<div style="margin: 10px 0; text-align: right;">
<div style="font-weight: bold; color: #2c3e50;">{sender}</div>
<div style="background-color: #e3f2fd; border-radius: 10px; padding: 10px;
display: inline-block; max-width: 80%; text-align: left;
border: 1px solid #bbdefb;">
{message}
</div>
</div>
"""
else:
html = f"""
<div style="margin: 10px 0;">
<div style="font-weight: bold; color: #2c3e50;">{sender}</div>
<div style="background-color: #f5f5f5; border-radius: 10px; padding: 10px;
display: inline-block; max-width: 80%; border: 1px solid #e0e0e0;">
{message}
</div>
</div>
"""
# 添加消息到聊天显示区域
self.chat_display.append(html)
self.chat_display.moveCursor(QTextCursor.End)
# 添加到聊天历史
self.chat_display.append({"sender": sender, "message": message, "is_user": is_user})
def send_message(self):
"""发送用户消息"""
message = self.input_field.text().strip()
if not message:
return
# 添加用户消息
self.add_message("您", message, True)
# 清空输入框
self.input_field.clear()
# 禁用发送按钮
self.send_button.setEnabled(False)
self.input_field.setEnabled(False)
# 创建并启动工作线程
self.worker = VolcEngineWorker(message)
self.worker.response_received.connect(self.handle_api_response)
self.worker.status_update.connect(self.status_bar.showMessage)
self.worker.start()
def handle_api_response(self, response, is_error):
"""处理 API 响应"""
# 启用发送按钮
self.send_button.setEnabled(True)
self.input_field.setEnabled(True)
if is_error:
self.add_message("系统", response, False)
QMessageBox.critical(self, "错误", f"发生错误: {response}")
else:
self.add_message("火山引擎", response, False)
# 添加到历史列表
self.history_list.addItem(f"对话 {len(self.chat_history) // 2 + 1}")
self.status_bar.showMessage("就绪")
def load_chat_history(self, item):
"""加载选中的聊天历史"""
index = self.history_list.row(item)
# 计算在历史记录中的位置
start_index = index * 2 # 每个对话包含2条消息
# 清空当前聊天显示
self.chat_display.clear()
# 添加初始欢迎消息
self.chat_display.append("""
<div style="margin: 10px 0;">
<div style="font-weight: bold; color: #2c3e50;">系统</div>
<div style="background-color: #f5f5f5; border-radius: 10px; padding: 10px;
display: inline-block; max-width: 80%; border: 1px solid #e0e0e0;">
欢迎使用火山引擎聊天助手!以下是您选择的对话历史。
</div>
</div>
""")
# 添加选中的历史消息
for i in range(start_index, start_index + 2):
if i < len(self.chat_history):
msg = self.chat_history[i]
self.add_message(msg["sender"], msg["message"], msg["is_user"])
if __name__ == "__main__":
app = QApplication(sys.argv)
# 设置应用样式
app.setStyle("Fusion")
palette = QPalette()
palette.setColor(QPalette.Window, QColor(240, 240, 240))
palette.setColor(QPalette.WindowText, QColor(0, 0, 0))
app.setPalette(palette)
window = ChatApplication()
window.show()
sys.exit(app.exec_())
append(self, text: Optional[str]): argument 1 has unexpected type 'dict'