搭建自己的AI API对话机器人UI程序完全指南(有完整代码,在Python3.13环境下即拿即用)
目录
本文详细介绍如何将API独立站上的模型嵌入UI程序,打造自己的聊天机器人。API独立站点我就能打开。
第一章 项目概述与核心特性
1.1 项目背景与意义
在人工智能大模型时代,越来越多的开发者和企业需要集成先进的AI模型到自己的应用中。然而,直接调用各个模型的官方API存在诸多痛点:需要分别注册多个平台账户、维护不同的API密钥、处理各异的接口规范、承担高昂的计费成本。为了解决这些问题,API聚合平台应运而生。本项目基于API镜像站这一高效的OpenAI接口聚合管理平台,融合了丰富的免费和付费模型资源,为用户提供了一套完整的、开箱即用的AI对话机器人解决方案。这套解决方案采用Python与Tkinter框架开发,打造了一个功能强大、界面友好的桌面应用程序,使得普通用户无需复杂的技术背景,就能轻松体验各种最先进的AI模型。
1.2 核心功能特性
这个AI对话机器人应用集成了众多实用功能,使其成为一个专业级的AI交互工具。首先,应用支持实时流式响应,用户输入的问题能够以流式形式返回AI的回答,提供了更加自然流畅的交互体验。其次,应用内置了完整的Markdown格式支持,可以正确渲染标题、加粗、斜体、代码块等各种排版元素,让AI的回答更加易读。第三,应用特别设计了代码提取功能,当AI在回答中包含代码段时,系统会自动识别并提取这些代码块到独立的选项卡中,方便用户查看和复制。此外,应用还提供了丰富的参数调整选项,包括温度、Top P等采样参数,允许用户根据不同场景灵活调整模型的生成行为。应用还支持完整的对话导出功能,可以将整个对话历史保存为文本文件以供日后查阅,同时支持从JSON文件导入设置,方便用户备份和恢复自己的配置。
第二章 环境与依赖准备
2.1 系统需求与Python环境
为了成功运行这个AI对话机器人应用,用户需要准备一个合适的开发环境。应用要求Python 3.7或以上版本,因为代码中使用了f-string等现代Python特性。在操作系统方面,由于应用使用了Tkinter作为GUI框架,理论上可以运行在Windows、macOS和Linux等多个平台上,但在实际使用中,Windows和macOS用户体验通常最佳。建议用户在安装Python时,确保勾选了"Add Python to PATH"选项,这样可以在命令行中直接调用Python。
2.2 必需的Python库安装
项目的核心依赖包括requests库用于发送HTTP请求、json库用于处理JSON数据、threading库用于多线程操作。这些库中,requests是唯一需要额外安装的第三方库,而json和threading已经包含在Python标准库中。用户可以通过以下命令安装所有必需的库:
pip install requests 对于需要使用最新版本或特定版本的requests库的用户,可以使用以下命令:
pip install --upgrade requests 如果用户所在的网络环境访问PyPI速度较慢,可以考虑使用国内镜像源,例如阿里云镜像或清华大学镜像。一个完整的带有镜像源的安装命令示例为:
pip install -i https://mirrors.aliyun.com/pypi/simple/ requests 2.3 API服务账户注册与配置
在开始使用本应用之前,用户必须在api.aigc.bar平台上注册账户并获取API密钥。注册过程非常直观:用户需要访问这个注册链接,按照页面提示填写基本信息如邮箱地址和密码,完成邮箱验证后即可注册成功。

注册完成后,用户进入令牌管理页面,根据实际需求添加令牌,将看到自己生成的API密钥。这个密钥是访问所有AI模型的凭证,用户需要妥善保管并在应用启动时输入。为了安全起见,不建议在代码中硬编码API密钥,而应该通过环境变量或配置文件的方式存储。本应用设计了一个自动保存机制,用户首次输入API密钥后,点击"保存"按钮,系统会将密钥保存到本地文件中,下次启动应用时会自动加载,无需重复输入。
如使用免费的,则使用这个:

如果使用高级点的模型,也可充值后使用付费分组。
第三章 应用架构与核心代码解析
3.1 整体架构设计与类结构
这个应用的设计采用了面向对象编程范式,由两个主要类组成:MarkdownFormatter和ChatBotUI。MarkdownFormatter类专门负责处理Markdown格式的文本转换和渲染,它包含了两个重要的静态方法,一个是extract_code_blocks方法,用于从AI返回的文本中识别并提取代码块,另一个是format_text方法,用于将Markdown格式的文本正确地插入到Tkinter的Text widget中,并应用相应的标签样式。ChatBotUI类是整个应用的主类,负责创建用户界面、处理用户交互、管理对话历史、与API通信等核心业务逻辑。这种分离的设计使得代码结构清晰,各个模块职责明确,易于维护和扩展。
3.2 Markdown处理引擎
Markdown处理引擎是这个应用的一个独特之处,它能够正确地识别和渲染各种Markdown格式元素。extract_code_blocks方法使用了正则表达式来识别代码块,具体使用的模式是r'```(?:python|py)?\s*\n(.*?)\n```',这个模式能够匹配三个反引号包围的代码块,并且支持可选的python语言标识符。该方法的核心逻辑是遍历所有匹配到的代码块,逐个提取其内容,同时删除原文本中的代码块标记,返回处理后的文本和代码块列表。这样做的好处是,AI返回的代码会被自动分离出来,可以在独立的选项卡中以高亮的代码编辑器样式显示,提升了代码的可读性。
format_text方法的工作原理更加复杂,它逐行处理文本,首先检查每一行是否是标题(以#开头),如果是标题则根据#的数量确定标题级别(1到3级),应用相应的标签样式。对于非标题行,方法会逐个字符地处理,在每个位置使用正则表达式同时检查多种格式标记:文本(加粗)、文本(斜体)、文本(代码)和链接文本(链接)。当发现格式标记时,取出最先出现的那个,应用相应的tag标签,然后继续处理剩余的文本。这种逐个处理的方式虽然计算量较大,但保证了格式的正确性,避免了格式标记之间的冲突。
3.3 UI界面构建与布局设计
应用的用户界面采用了左右分栏的经典布局设计。左侧是一个统一的参数配置面板,使用了LabelFrame容器来组织各个参数控制元素。这个面板包含了API密钥管理、模型选择、系统提示词、生成参数调整等各个功能区域。API密钥部分采用了一个Frame容器,在其中水平排列了一个Entry输入框、一个"保存"按钮和一个"注册"按钮,使得用户可以方便地输入密钥或快速跳转到注册页面。系统提示词使用了ScrolledText组件,这样即使提示词内容过长也能正常显示。参数调整部分(最大输出长度、温度、Top P)采用了Spinbox和Scale等专门的组件,提供了图形化的参数调整体验。
右侧是主要的聊天和代码展示区域,采用了Notebook(选项卡)组件来实现多选项卡的界面。默认情况下,应用启动时会创建一个"聊天"选项卡,显示聊天内容。当AI返回的内容中包含代码块时,应用会动态地添加新的代码选项卡,每个代码块对应一个独立的选项卡。这样的设计使得用户可以轻松地在聊天内容和代码之间切换查看,而无需在冗长的对话历史中来回翻滚。

3.4 核心通信机制
应用与API的通信采用了一个精心设计的流程。当用户点击"发送"按钮或按下Ctrl+Enter快捷键后,应用首先会验证API密钥是否已输入,然后检查用户是否正在等待上一条消息的回复。通过验证后,用户的消息会被添加到对话历史中,UI中会显示用户消息,输入框会被清空。接着,应用会设置一个标志位is_loading为True,禁用发送按钮,更新状态标签为"正在处理...",并在后台开启一个新的工作线程来处理API请求,避免阻塞主UI线程。
在工作线程中,应用构造了一个遵循OpenAI API格式的请求payload,包含了用户选择的模型、完整的消息历史(包括系统提示词)、各种采样参数。该payload被发送到api.aigc.bar的v1/chat/completions端点。应用使用了stream=True参数来启用流式响应,这意味着API会立即开始返回结果,而不是等待完整的响应生成。应用通过response.iter_lines()方法逐行迭代处理返回的数据,每一行都是一个符合Server-Sent Events格式的JSON对象。对于包含reasoning_content字段的数据块,应用将其识别为模型的思考过程(某些模型如DeepSeek推理模型会返回此字段),并以特殊的"thinking"样式显示。对于包含content字段的数据块,应用将其作为最终回答,以"assistant"样式显示。
第四章 免费模型与基础使用
4.1 可用的免费模型列表
api.aigc.bar平台为用户提供了多个免费使用的AI模型,这些模型涵盖了多个领域和能力水平。目前应用代码中预配置的免费模型包括以下几个:首先是GPT系列模型中的gpt-4.1-nano和gpt-5-nano这两个轻量级版本,这些模型虽然参数量较小,但在基础的文本生成和问题回答方面表现可靠,适合那些对响应速度有要求或输入量较小的场景。其次是DeepSeek系列的deepseek-r1-0528和deepseek-v3两个模型,其中r1模型是推理专向模型,特别擅长数学、编程等需要逻辑推理的任务,会返回详细的思考过程;deepseek-v3是多用途模型,在知识问答、文本生成等方面表现均衡。此外还有deepseek-v3.1,这是v3的一个改进版本。最后是gemini-2.5-flash-lite,这是Google最新推出的轻量化Gemini模型,在多模态理解和快速响应方面表现优异。这些免费模型的可用额度通常有限制,新用户一般会获得初始的免费试用额度,具体的额度和有效期需要用户在平台上查看。

4.2 基础使用流程与最佳实践
使用本应用的基础流程非常简洁。首先,用户启动应用后,在左侧参数面板的"API密钥"输入框中粘贴自己的API密钥,然后点击"保存"按钮,系统会将密钥保存到本地。其次,用户在"选择模型"下拉框中选择想要使用的模型,比如选择gpt-4.1-nano进行快速体验。如果需要自定义AI的行为风格,可以在"系统提示词"的大文本框中修改系统提示,比如可以将默认的中文助手改为特定行业的专家角色。然后用户在右侧的输入框中输入想要问的问题,点击"发送"按钮或按下Ctrl+Enter快捷键,应用就会向API发送请求。等待几秒钟后(具体时间取决于模型复杂度和网络延迟),AI的回答就会以流式的形式出现在上方的聊天展示区,用户可以实时看到回答的逐字生成过程。如果AI的回答中包含代码块,应用会自动创建新的代码选项卡,用户可以点击选项卡查看代码,右键选择"复制代码"将代码复制到剪贴板。
在实际使用中,为了获得更好的效果,用户应该注意以下几个最佳实践:首先,在提问时要尽可能清晰具体,提供充足的上下文信息,这样AI能够更准确地理解需求。其次,对于需要长篇幅回答或深度思考的问题,应该选择较高的max_tokens值(比如8192或更高),同时可以适当提高温度参数以获得更创意的回答。第三,对于对答案准确性有严格要求的场景(比如代码生成、数学计算),应该选择推理能力强的模型(如deepseek-r1)或设置较低的温度值(0.3-0.5)以获得更一致的回答。第四,如果需要维持一个连贯的多轮对话,应该避免频繁地清空对话历史,这样AI可以基于之前的上下文进行更一致的回答。
第五章 付费模型配置与进阶使用
5.1 付费模型的种类与定价体系
虽然API平台提供了多个免费模型供初级用户体验,但对于需要更强大能力的用户,平台还提供了多个付费模型选项。付费模型通常分为几个层级:基础层模型包括像gpt-5-mini这样的快速、经济的模型,适合日常文本生成和简单问答任务,价格最低廉;中端层模型包括gpt-4.1、claude-code等,这些模型具有更强的理解能力和生成质量,适合需要更高准确度的任务;高端层模型包括gpt-5.1、claude-4.5-opus、claude-4.5-sonnet等最新发布的模型,具有最强的能力,适合复杂的推理、创意写作等高端任务;专用模型则包括gemini-pro(Google的旗舰模型)、llama-2等开源模型的托管版本,这些模型在特定领域可能有独特的优势。平台通常采用按照输入token数和输出token数分别计费的模式,不同模型的价格差异较大,用户需要根据自己的预算和需求选择合适的模型。

5.2 修改代码以使用付费模型
要在应用中使用付费模型,用户需要修改ChatBotUI类中的models列表。原始的模型列表定义在__init__方法中,看起来如下:
self.models = [ "gpt-4.1-nano", "gpt-5-nano", "deepseek-r1-0528", "deepseek-v3", "deepseek-v3.1", "gemini-2.5-flash-lite" ] 要添加付费模型,用户只需要在这个列表中添加新的模型字符串。例如,如果用户想添加OpenAI的GPT-4 Turbo模型和Claude 3.5 Sonnet模型,修改后的列表应该如下所示:
self.models = [ "gpt-4.1-nano", "gpt-5-nano", "deepseek-r1-0528", "deepseek-v3", "deepseek-v3.1", "gemini-2.5-flash-lite", "gpt-4-turbo", "gpt-4-turbo-preview", "claude-3.5-sonnet", "claude-3-opus", "gemini-pro" ] 添加完模型后,当用户启动应用时,在"选择模型"下拉框中就会看到新增的模型选项。用户可以选择任何一个付费模型,然后像使用免费模型一样发送消息,应用会自动调用该模型的API。需要注意的是,一旦选择了付费模型,每一次API调用都会根据平台的定价产生费用。为了避免意外的大额消费,建议用户在实验新模型时先使用较小的max_tokens值,并且在平台上设置每日或每月的消费上限。
5.3 不同分组下的模型选择指南
在api.aigc.bar平台上,各个模型通常按照功能分组进行组织。第一个分组是通用文本生成模型组,这一组包括所有的GPT系列模型和通用的Claude模型,这些模型在各种任务上都有不错的表现,适合那些没有特殊需求的一般用户。用户如果不确定选择哪个模型,可以考虑从这个分组中选择。第二个分组是推理和编程专向模型组,这一组主要包括DeepSeek的r1和v3系列,以及OpenAI的GPT-4系列,这些模型在代码生成、数学推理、逻辑问题解决等方面表现特别突出,适合开发人员和科研工作者。如果用户的任务涉及编写复杂代码或解决深层次的逻辑问题,应该优先选择这个分组中的模型。第三个分组是多模态模型组,包括Google的Gemini系列和其他支持图像输入的模型,虽然本应用的当前版本主要处理文本,但这些模型的文本生成能力也很强,在需要更全面理解的场景下表现更好。
对于不同场景,用户有以下建议的选择策略:如果进行日常问答和文本生成,推荐使用gpt-4.1-nano或gpt-5-nano这样的免费轻量级模型,既能保证质量又能节省成本。如果需要编写代码或解决数学问题,推荐使用deepseek-r1-0528或deepseek-v3,因为这两个模型在代码质量和推理准确性上都很突出,而且deepseek-r1-0528会返回详细的思考过程,帮助用户理解AI的推理逻辑。如果追求最好的输出质量且预算充足,可以选择付费的gpt-5.2或claude-4.5-sonnet,这两个模型在创意写作、深度分析等复杂任务上能力最强。如果进行快速原型设计或对延迟有严格要求,可以选择gemini-2.5-flash-lite或其他标注为"flash"的轻量版本,这些模型的响应速度最快。
第六章 高级功能与参数优化
6.1 采样参数的作用与调整方法
应用中提供的温度(Temperature)和Top P两个采样参数是控制模型生成行为的关键因素。温度参数的取值范围是0到2,它控制了模型生成结果的随机性程度。当温度设置为0时,模型会采用最贪心的策略,总是选择概率最高的下一个token,生成的结果最确定、最可预测,适合对答案准确性要求很高的场景如代码生成或数据提取。当温度设置为1时,这是一个平衡点,模型会以正常的概率分布进行采样。当温度设置为更高的值(如1.5或2)时,模型会倾向于选择概率较低但更多样化的token,生成的结果会更富创意、更多样化,适合创意写作或头脑风暴场景。Top P参数的取值范围是0到1,它采用核采样的策略,表示只从累计概率达到P的最可能的token中进行采样。例如,当Top P设置为0.9时,模型只会在那些累计概率达到90%的token中进行选择,这样可以避免选择极低概率的离奇token,同时保留一定的多样性。
在实际使用中,一个常用的参数组合是温度0.7、Top P 0.9,这个组合在多数场景下提供了很好的平衡。当用户发现模型的回答显得过于保守或重复时,可以尝试提高温度值或降低Top P值。相反,当模型的回答显得过于随意或与问题偏离时,应该降低温度或提高Top P值。
6.2 系统提示词的设计与优化
系统提示词(System Prompt)是指导AI行为的关键指令。应用中默认的系统提示词是"你是一个专业、友善且富有创意的AI助手。你会用中文回答用户的问题。"这个提示词设置了基本的角色定义和语言偏好。但用户可以根据实际需求定制系统提示词,来引导AI产生特定风格或领域的回答。例如,如果用户想让AI扮演一个C++编程专家的角色,可以将系统提示词改为"你是一位资深的C++工程师,拥有20年的软件开发经验。当用户提出关于C++的问题时,你应该从实战经验出发,提供深入、专业的解答,并给出最佳实践建议。"又或者,如果用户想让AI充当英文翻译助手,可以设置"你是一位资深的翻译工作者,精通英文和中文。你的任务是准确、优雅地将用户提供的文本翻译成指定的语言,保留原文的含义和风格。"这样的系统提示词。设计好的系统提示词应该包括以下几个要素:清晰的角色定义(你是什么角色)、明确的任务目标(你要做什么)、输出格式要求(如果需要)、以及任何特殊的限制或偏好。
6.3 对话历史管理与导出
应用实现了完整的对话历史管理功能。在后台,应用使用一个列表来存储所有的对话消息,每条消息包括"role"(消息发送者的角色,可以是"user"或"assistant")和"content"(消息的实际内容)两个字段。这个历史列表在内存中维护,用户与AI的每一条往来都会被记录。当用户清空对话时,这个列表会被重置。应用还提供了"导出对话"功能,用户点击这个按钮后,应用会打开一个文件保存对话框,用户可以选择保存位置和文件名。导出的文件是纯文本格式,包含了对话时间戳、使用的模型名称以及完整的对话内容,这样用户可以保存重要的对话记录供日后参考,或者用于分享讨论或学习。
应用还支持"导入设置"功能,这允许用户从一个JSON文件中加载之前保存的配置。用户可以手动编辑这样的JSON文件来快速设置多个配置方案。一个典型的设置JSON文件看起来如下:
{ "api_key": "sk-xxxxxxxxxxxx", "model": "deepseek-v3", "system_prompt": "你是一位资深的Python编程专家", "max_tokens": 8192, "temperature": 0.7, "top_p": 0.9 } 用户可以为不同的使用场景创建多个这样的配置文件,然后根据需要导入不同的配置,这样可以大大提高工作效率。
第七章 故障排查与优化建议
7.1 常见问题与解决方案
在使用过程中,用户可能会遇到各种问题。首先是连接错误,如果应用显示"连接失败",通常意味着无法连接到API站的服务器。这可能是由于网络问题、DNS解析失败或API服务器暂时不可用导致的。解决方法是检查网络连接是否正常(可以尝试用浏览器打开这个API独立站确认),确认API地址是否正确,如果还是无法连接可以尝试更换网络或使用VPN。其次是超时错误,应用的默认超时时间是120秒,如果在这个时间内没有收到完整的响应,就会显示超时错误。这通常发生在max_tokens设置过高、网络延迟大或API服务器响应慢的情况下。解决方法是降低max_tokens值,或者选择一个更轻量化的模型,或者等待一段时间后重试。第三个常见问题是API错误,应用会显示具体的错误码和错误信息。最常见的是401错误(未授权),这表示API密钥无效或已过期,需要检查API密钥是否正确输入。还有429错误(请求过于频繁),这表示API请求频率超过了平台的限制,需要降低请求频率。
7.2 性能优化与最佳实践
为了获得最佳的使用体验,用户应该注意以下几个优化点。首先,合理设置max_tokens参数,不要盲目设置为最大值。对于一般的问答任务,2048或4096的max_tokens通常就足够了,只有在需要生成长篇幅内容时才需要设置更高的值。较低的max_tokens不仅会提高响应速度,还会降低API调用成本。其次,在切换模型时要考虑速度和质量的权衡。轻量级模型(nano版本、flash版本)响应速度快但生成质量可能较低,适合快速原型和实时交互。复杂模型(turbo版本、opus版本)生成质量高但响应较慢,适合对质量有要求的任务。第三,利用系统提示词来约束模型的行为,一个好的系统提示词可以大大提升回答的相关性和准确性,减少冗余内容,从而提高token利用效率和成本效益。第四,定期检查api.aigc.bar平台上的消费统计和余额,设置合理的日均或月均消费上限,避免因为过度使用导致意外的费用。
第八章 总结与展望
本文详细介绍了如何基于api.aigc.bar平台和Python Tkinter框架,搭建一个功能完整、易于使用的AI对话机器人应用。从环境准备、基础使用、到高级功能,我们覆盖了使用者可能需要了解的各个方面。通过这个应用,用户可以轻松地体验各种免费和付费的先进AI模型,包括OpenAI的GPT系列、DeepSeek的推理模型、Google的Gemini模型等。应用内置的Markdown渲染、代码提取、流式响应等特性,使得与AI的交互更加高效和愉快。
展望未来,这个应用还有很多潜在的扩展方向。例如,可以添加多文件上传和处理能力,让用户可以直接在应用中分析文档、图片等多种文件格式。可以集成本地模型支持,让用户选择在本地运行轻量级模型而不仅仅依赖云端API。可以添加插件系统,让第三方开发者为应用开发各种功能扩展。可以实现更智能的对话管理,比如自动摘要、主题提取等功能。随着AI技术的不断进步,API站这样的聚合平台会集成越来越多的新模型,而这个应用框架已经为这样的扩展做好了准备。希望通过本文,开发者和用户能够充分利用这个强大的工具,探索AI的无限可能性。
窗体程序完整代码实现如下:
import tkinter as tk from tkinter import ttk, scrolledtext, messagebox, filedialog import requests import json import threading from datetime import datetime import os import re import webbrowser class MarkdownFormatter: """处理Markdown格式化""" @staticmethod def extract_code_blocks(text): """提取代码块,返回 (非代码文本, 代码块列表)""" code_blocks = [] # 更灵活的正则表达式,支持多种格式 pattern = r'```(?:python|py)?\s*\n(.*?)\n```' matches = list(re.finditer(pattern, text, flags=re.DOTALL)) # 提取所有代码块 for match in matches: code_content = match.group(1).strip() if code_content: code_blocks.append(code_content) # 替换代码块为空字符串,保留其他文本 text_without_code = re.sub(pattern, '', text, flags=re.DOTALL) return text_without_code.strip(), code_blocks @staticmethod def format_text(widget, text,): """将Markdown格式的文本插入到Text widget""" widget.config(state=tk.NORMAL) # 分行处理 lines = text.split('\n') for line_idx, line in enumerate(lines): # 处理标题 # ## ### heading_match = re.match(r'^(#{1,6})\s+(.+)$', line) if heading_match: level = min(len(heading_match.group(1)), 3) content = heading_match.group(2) widget.insert(tk.END, content, f"heading{level}") if line_idx < len(lines) - 1: widget.insert(tk.END, '\n') continue # 处理该行中的Markdown格式 pos = 0 while pos < len(line): # 查找所有格式 bold_match = re.search(r'\*\*(.+?)\*\*', line[pos:]) italic_match = re.search(r'(?<!\*)\*([^*]+?)\*(?!\*)', line[pos:]) code_match = re.search(r'`([^`]+?)`', line[pos:]) link_match = re.search(r'\[([^\]]+)\]\(([^\)]+)\)', line[pos:]) matches = [] if bold_match: matches.append(('bold', bold_match.start(), bold_match.end(), bold_match.group(1), '**')) if italic_match: matches.append(('italic', italic_match.start(), italic_match.end(), italic_match.group(1), '*')) if code_match: matches.append(('code', code_match.start(), code_match.end(), code_match.group(1), '`')) if link_match: matches.append(('link', link_match.start(), link_match.end(), link_match.group(1), 'link')) if not matches: # 没有格式,直接插入剩余文本 widget.insert(tk.END, line[pos:], base_tag) break # 找到最早出现的匹配 matches.sort(key=lambda x: x[1]) match_type, start, end, content, marker = matches[0] # 插入匹配前的文本 if start > 0: widget.insert(tk.END, line[pos:pos + start], base_tag) # 插入格式化的文本 if match_type == 'link': widget.insert(tk.END, content, 'link') else: widget.insert(tk.END, content, match_type) pos += end # 添加换行符(除了最后一行) if line_idx < len(lines) - 1: widget.insert(tk.END, '\n') widget.config(state=tk.DISABLED) class ChatBotUI: def __init__(self, root): self.root = root self.root.title("AI聊天机器人") self.root.geometry("1600x800") self.api_key = "" self.api_url = "https://api.aigc.bar/v1/chat/completions" self.register_url = "https://api.aigc.bar/register?aff=UP4F" self.tutorial_url = "https://blog.ZEEKLOG.net/nmdbbzcl/article/details/156234830" self.api_key_file = os.path.join(os.getcwd(), "api_key.txt") self.models = [ "gpt-4.1-nano", "gpt-5-nano", "deepseek-r1-0528", "deepseek-v3", "deepseek-v3.1", "gemini-2.5-flash-lite" ] self.conversation_history = [] self.is_loading = False self.code_tab_count = 0 self.setup_fonts() self.load_api_key() self.create_ui() def setup_fonts(self): """设置中文字体""" try: self.chinese_font = ("微软雅黑", 10) self.title_font = ("微软雅黑", 11, "bold") self.large_font = ("微软雅黑", 12, "bold") self.code_font = ("Courier New", 10) except: try: self.chinese_font = ("WenQuanYi Micro Hei", 10) self.title_font = ("WenQuanYi Micro Hei", 11, "bold") self.large_font = ("WenQuanYi Micro Hei", 12, "bold") self.code_font = ("Courier New", 10) except: self.chinese_font = ("Helvetica", 10) self.title_font = ("Helvetica", 11, "bold") self.large_font = ("Helvetica", 12, "bold") self.code_font = ("Courier New", 10) def load_api_key(self): """从文件加载API密钥""" try: if os.path.exists(self.api_key_file): with open(self.api_key_file, 'r', encoding='utf-8') as f: api_key = f.read().strip() if api_key: self.api_key = api_key except Exception as e: print(f"加载API密钥失败: {e}") def save_api_key(self, api_key): """保存API密钥到文件""" try: if api_key: with open(self.api_key_file, 'w', encoding='utf-8') as f: f.write(api_key) return True except Exception as e: print(f"保存API密钥失败: {e}") messagebox.showerror("错误", f"保存API密钥失败:{str(e)}") return False return False def create_ui(self): """创建用户界面""" main_frame = ttk.Frame(self.root) main_frame.pack(fill=tk.BOTH, expand=True, padx=8, pady=8) # 左侧配置面板 left_frame = ttk.LabelFrame(main_frame, text="参数配置", padding=12) left_frame.pack(side=tk.LEFT, fill=tk.BOTH, padx=5, pady=5) left_frame.config(width=300) # API密钥部分 ttk.Label(left_frame, text="API密钥:", font=self.title_font).pack(anchor=tk.W, pady=(5, 2)) api_key_frame = ttk.Frame(left_frame) api_key_frame.pack(anchor=tk.W, pady=(0, 5), fill=tk.X) self.api_key_var = tk.StringVar(value=self.api_key) self.api_key_entry = ttk.Entry(api_key_frame, textvariable=self.api_key_var, width=20, font=self.chinese_font) self.api_key_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 2)) ttk.Button(api_key_frame, text="保存", command=self.update_api_key, width=6).pack(side=tk.LEFT, padx=2) ttk.Button(api_key_frame, text="注册", command=self.open_register_page, width=6).pack(side=tk.LEFT, padx=2) ttk.Separator(left_frame, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=10) ttk.Label(left_frame, text="选择模型:", font=self.title_font).pack(anchor=tk.W, pady=(5, 2)) self.model_var = tk.StringVar(value=self.models[0]) model_combo = ttk.Combobox(left_frame, textvariable=self.model_var, values=self.models, state="readonly", width=25, font=self.chinese_font) model_combo.pack(anchor=tk.W, pady=(0, 10), fill=tk.X) ttk.Label(left_frame, text="系统提示词 (System Prompt):", font=self.title_font).pack(anchor=tk.W, pady=(5, 2)) self.system_prompt = scrolledtext.ScrolledText(left_frame, height=7, width=30, font=self.chinese_font, wrap=tk.WORD) self.system_prompt.pack(pady=(0, 10), fill=tk.BOTH, expand=True) self.system_prompt.insert(tk.END, "你是一个专业、友善且富有创意的AI助手。\n你会用中文回答用户的问题。") ttk.Label(left_frame, text="最大输出长度 (tokens):", font=self.title_font).pack(anchor=tk.W, pady=(5, 2)) max_tokens_frame = ttk.Frame(left_frame) max_tokens_frame.pack(anchor=tk.W, pady=(0, 10), fill=tk.X) self.max_tokens_var = tk.StringVar(value="16384") ttk.Spinbox(max_tokens_frame, from_=100, to=16384, textvariable=self.max_tokens_var, width=10, font=self.chinese_font).pack(side=tk.LEFT) ttk.Label(max_tokens_frame, text="(100-16384)", font=self.chinese_font).pack(side=tk.LEFT, padx=5) ttk.Label(left_frame, text="温度 (Temperature):", font=self.title_font).pack(anchor=tk.W, pady=(5, 2)) temp_frame = ttk.Frame(left_frame) temp_frame.pack(anchor=tk.W, pady=(0, 10), fill=tk.X) self.temperature_var = tk.StringVar(value="0.7") temp_scale = ttk.Scale(temp_frame, from_=0, to=2, orient=tk.HORIZONTAL, variable=self.temperature_var, command=self.update_temp_label) temp_scale.pack(side=tk.LEFT, fill=tk.X, expand=True) self.temp_label = ttk.Label(temp_frame, text="0.7", width=5, font=self.chinese_font) self.temp_label.pack(side=tk.LEFT, padx=5) ttk.Label(left_frame, text="Top P:", font=self.title_font).pack(anchor=tk.W, pady=(5, 2)) topp_frame = ttk.Frame(left_frame) topp_frame.pack(anchor=tk.W, pady=(0, 15), fill=tk.X) self.top_p_var = tk.StringVar(value="0.9") topp_scale = ttk.Scale(topp_frame, from_=0, to=1, orient=tk.HORIZONTAL, variable=self.top_p_var, command=self.update_topp_label) topp_scale.pack(side=tk.LEFT, fill=tk.X, expand=True) self.topp_label = ttk.Label(topp_frame, text="0.9", width=5, font=self.chinese_font) self.topp_label.pack(side=tk.LEFT, padx=5) button_frame = ttk.Frame(left_frame) button_frame.pack(anchor=tk.W, pady=(10, 0), fill=tk.X) ttk.Button(button_frame, text="清空对话", command=self.clear_conversation).pack(fill=tk.X, pady=2) ttk.Button(button_frame, text="导出对话", command=self.export_chat).pack(fill=tk.X, pady=2) ttk.Button(button_frame, text="导入设置", command=self.import_settings).pack(fill=tk.X, pady=2) ttk.Button(button_frame, text="使用教程", command=self.open_tutorial_page).pack(fill=tk.X, pady=2) ttk.Separator(left_frame, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=10) ttk.Label(left_frame, text="状态:", font=self.title_font).pack(anchor=tk.W, pady=(5, 2)) self.status_label = ttk.Label(left_frame, text="就绪", font=self.chinese_font, foreground="green") self.status_label.pack(anchor=tk.W) # 右侧主面板(包含聊天和代码选项卡) right_frame = ttk.LabelFrame(main_frame, text="聊天与代码", padding=10) right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=5, pady=5) # 创建选项卡容器 self.main_notebook = ttk.Notebook(right_frame) self.main_notebook.pack(fill=tk.BOTH, expand=True, pady=(0, 10)) # 聊天选项卡 chat_tab = ttk.Frame(self.main_notebook) self.main_notebook.add(chat_tab, text="聊天") self.chat_display = scrolledtext.ScrolledText(chat_tab, height=25, width=70, font=self.chinese_font, wrap=tk.WORD, state=tk.DISABLED, bg="#f5f5f5") self.chat_display.pack(fill=tk.BOTH, expand=True) # 标签配置 self.chat_display.tag_config("user", foreground="#0066cc", font=(self.chinese_font[0], self.chinese_font[1], "bold")) self.chat_display.tag_config("assistant", foreground="#009900", font=(self.chinese_font[0], self.chinese_font[1], "bold")) self.chat_display.tag_config("thinking", foreground="#FF9500", font=(self.chinese_font[0], self.chinese_font[1], "bold")) self.chat_display.tag_config("error", foreground="#cc0000", font=(self.chinese_font[0], self.chinese_font[1], "bold")) self.chat_display.tag_config("timestamp", foreground="#999999", font=(self.chinese_font[0], 9)) # Markdown标签 self.chat_display.tag_config("bold", font=(self.chinese_font[0], self.chinese_font[1], "bold")) self.chat_display.tag_config("italic", font=(self.chinese_font[0], self.chinese_font[1], "italic")) self.chat_display.tag_config("code", foreground="#d63384", background="#f8f9fa", font=("Courier", 9)) self.chat_display.tag_config("heading1", font=(self.chinese_font[0], 14, "bold"), foreground="#000080") self.chat_display.tag_config("heading2", font=(self.chinese_font[0], 12, "bold"), foreground="#000080") self.chat_display.tag_config("heading3", font=(self.chinese_font[0], 11, "bold"), foreground="#000080") self.chat_display.tag_config("link", foreground="#0066cc", underline=True) # 输入区域 ttk.Label(right_frame, text="输入消息 (Ctrl+Enter 快速发送):", font=self.title_font).pack(anchor=tk.W, pady=(5, 2)) self.input_text = tk.Text(right_frame, height=5, width=70, font=self.chinese_font, wrap=tk.WORD) self.input_text.pack(fill=tk.BOTH, padx=0, pady=(0, 8)) self.input_text.bind("<Control-Return>", lambda e: self.send_message()) button_frame_right = ttk.Frame(right_frame) button_frame_right.pack(fill=tk.X, padx=0, pady=0) self.send_button = ttk.Button(button_frame_right, text="发送", command=self.send_message) self.send_button.pack(side=tk.LEFT, padx=3) ttk.Button(button_frame_right, text="清空输入", command=self.clear_input).pack(side=tk.LEFT, padx=3) ttk.Button(button_frame_right, text="复制最后回复", command=self.copy_last_response).pack(side=tk.LEFT, padx=3) def add_code_tab(self, code_content): """添加代码选项卡""" self.code_tab_count += 1 tab_name = f"代码 {self.code_tab_count}" try: code_frame = ttk.Frame(self.main_notebook) self.main_notebook.add(code_frame, text=tab_name) # 创建代码显示区域 code_display = scrolledtext.ScrolledText(code_frame, font=self.code_font, wrap=tk.NONE, bg="#2b2b2b", fg="#f8f8f2", insertbackground="white") code_display.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) # 插入代码内容 code_display.insert(tk.END, code_content) code_display.config(state=tk.DISABLED) # 添加右键菜单复制功能 def copy_code(): self.root.clipboard_clear() self.root.clipboard_append(code_content) self.status_label.config(text="代码已复制到剪贴板", foreground="blue") popup_menu = tk.Menu(code_display, tearoff=0) popup_menu.add_command(label="复制代码", command=copy_code) def show_popup(e): try: popup_menu.tk_popup(e.x_root, e.y_root) finally: popup_menu.grab_release() code_display.bind("<Button-3>", show_popup) except Exception as e: print(f"添加代码选项卡出错: {e}") def open_register_page(self): """打开注册页面""" webbrowser.open(self.register_url) def open_tutorial_page(self): """打开使用教程页面""" webbrowser.open(self.tutorial_url) def update_api_key(self): """更新并保存API密钥""" new_api_key = self.api_key_var.get().strip() if not new_api_key: messagebox.showwarning("提示", "请输入API密钥") return if self.save_api_key(new_api_key): self.api_key = new_api_key messagebox.showinfo("成功", "API密钥已保存") self.status_label.config(text="API密钥已保存", foreground="blue") def update_temp_label(self, value): """更新温度标签""" self.temp_label.config(text=f"{float(value):.2f}") def update_topp_label(self, value): """更新Top P标签""" self.topp_label.config(text=f"{float(value):.2f}") def send_message(self): """发送消息""" # 获取当前API密钥(从输入框获取) current_api_key = self.api_key_var.get().strip() if not current_api_key: messagebox.showerror("错误", "请先填写API密钥或点击注册按钮获取密钥") return # 更新实例变量 self.api_key = current_api_key if self.is_loading: messagebox.showwarning("提示", "正在等待上一条消息的回复,请稍候...") return user_message = self.input_text.get("1.0", tk.END).strip() if not user_message: messagebox.showwarning("提示", "请输入消息") return self.display_message("你", user_message, "user") self.clear_input() self.is_loading = True self.send_button.config(state=tk.DISABLED) self.status_label.config(text="正在处理...", foreground="orange") self.root.update() threading.Thread(target=self.get_response, args=(user_message,), daemon=True).start() def get_response(self, user_message): """获取AI响应(在工作线程中执行)""" try: self.conversation_history.append({ "role": "user", "content": user_message }) headers = { "Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json" } messages = [] system_prompt = self.system_prompt.get("1.0", tk.END).strip() if system_prompt: messages.append({ "role": "system", "content": system_prompt }) if len(self.conversation_history) > 1: messages.extend(self.conversation_history[-2:]) else: messages.extend(self.conversation_history) data = { "model": self.model_var.get(), "messages": messages, "max_tokens": int(self.max_tokens_var.get()), "temperature": float(self.temperature_var.get()), "top_p": float(self.top_p_var.get()), "stream": True } response = requests.post(self.api_url, headers=headers, json=data, timeout=120, stream=True) if response.status_code != 200: error_msg = f"API错误 {response.status_code}\n" try: error_json = response.json() error_msg += json.dumps(error_json, ensure_ascii=False, indent=2) except: error_msg += response.text self.display_message("错误", error_msg, "error") self.status_label.config(text="错误", foreground="red") return # 处理流式响应 timestamp = datetime.now().strftime("%H:%M:%S") thinking_shown = False content_started = False update_count = 0 self.chat_display.config(state=tk.NORMAL) self.chat_display.insert(tk.END, f"[{timestamp}] ", "timestamp") for line in response.iter_lines(): if not line: continue line = line.decode('utf-8') if isinstance(line, bytes) else line line = line.strip() if line == "[DONE]" or line == "data: [DONE]": break if line.startswith("data: "): data_str = line[6:] elif line.startswith("{"): data_str = line else: continue try: chunk = json.loads(data_str) if "error" in chunk: error_msg = chunk.get("error", {}).get("message", "未知错误") self.chat_display.insert(tk.END, f"流式错误: {error_msg}\n", "error") break choices = chunk.get("choices", []) if not choices or len(choices) == 0: continue delta = choices[0].get("delta", {}) # 处理reasoning_content (思考过程) reasoning = delta.get("reasoning_content") if reasoning: if not thinking_shown: self.chat_display.insert(tk.END, "思考过程:\n", "thinking") thinking_shown = True reasoning_content += reasoning self.chat_display.insert(tk.END, reasoning) # 处理content (最终内容) content = delta.get("content") if content: if not content_started: if thinking_shown: self.chat_display.insert(tk.END, "\n\n最终回复:\n", "assistant") else: self.chat_display.insert(tk.END, "AI:\n", "assistant") content_started = True assistant_message += content self.chat_display.insert(tk.END, content) # 每5个数据块更新一次UI update_count += 1 if update_count % 5 == 0: self.chat_display.see(tk.END) self.root.update() except json.JSONDecodeError: continue # 完成流式接收 self.chat_display.insert(tk.END, "\n\n") self.chat_display.see(tk.END) self.chat_display.config(state=tk.DISABLED) self.root.update() # 处理回复和代码块 if assistant_message: # 提取代码块 text_without_code, code_blocks = MarkdownFormatter.extract_code_blocks(assistant_message) # 重新格式化聊天窗口(移除代码块) self.chat_display.config(state=tk.NORMAL) try: if thinking_shown: search_text = "最终回复:\n" else: search_text = "AI:\n" pos = self.chat_display.search(search_text, "1.0", nocase=True) if pos: pos_line, pos_col = pos.split('.') pos_line = str(int(pos_line) + 1) start_pos = f"{pos_line}.0" self.chat_display.delete(start_pos, tk.END) if text_without_code: MarkdownFormatter.format_text(self.chat_display, text_without_code) self.chat_display.insert(tk.END, "\n\n") except Exception as e: print(f"格式化错误: {e}") self.chat_display.config(state=tk.DISABLED) # 添加代码选项卡 if code_blocks: for code_block in code_blocks: self.add_code_tab(code_block) # 保存对话历史 self.conversation_history.append({ "role": "assistant", "content": assistant_message }) self.status_label.config(text="就绪", foreground="green") else: self.display_message("错误", "未收到有效的AI响应", "error") self.status_label.config(text="无响应", foreground="red") except requests.exceptions.Timeout: self.display_message("错误", "请求超时(120秒)。请检查网络连接或尝试更小的输出长度。", "error") self.status_label.config(text="超时错误", foreground="red") except requests.exceptions.ConnectionError as e: self.display_message("错误", f"连接失败:{str(e)}\n请检查API地址和网络连接。", "error") self.status_label.config(text="连接错误", foreground="red") except Exception as e: self.display_message("错误", f"发生错误:{str(e)}", "error") self.status_label.config(text="未知错误", foreground="red") finally: self.is_loading = False self.send_button.config(state=tk.NORMAL) if self.status_label.cget("text") == "正在处理...": self.status_label.config(text="就绪", foreground="green") def display_message(self, sender, message,): """显示消息""" self.chat_display.config(state=tk.NORMAL) timestamp = datetime.now().strftime("%H:%M:%S") self.chat_display.insert(tk.END, f"[{timestamp}] ", "timestamp") self.chat_display.insert(tk.END, f"{sender}:\n", tag) if tag in ["user", "error"]: self.chat_display.insert(tk.END, f"{message}\n\n") else: MarkdownFormatter.format_text(self.chat_display, message) self.chat_display.insert(tk.END, "\n\n") self.chat_display.config(state=tk.DISABLED) self.chat_display.see(tk.END) def clear_conversation(self): """清空对话历史""" if messagebox.askyesno("确认", "确定要清空所有对话历史吗?"): self.conversation_history = [] self.chat_display.config(state=tk.NORMAL) self.chat_display.delete("1.0", tk.END) self.chat_display.config(state=tk.DISABLED) self.status_label.config(text="对话已清空", foreground="blue") # 清除所有代码选项卡(保留聊天选项卡) while self.main_notebook.index("end") > 1: self.main_notebook.forget(1) self.code_tab_count = 0 def clear_input(self): """清空输入框""" self.input_text.delete("1.0", tk.END) def copy_last_response(self): """复制最后一条AI回复""" if self.conversation_history: for i in range(len(self.conversation_history) - 1, -1, -1): if self.conversation_history[i]["role"] == "assistant": message = self.conversation_history[i]["content"] self.root.clipboard_clear() self.root.clipboard_append(message) self.status_label.config(text="已复制到剪贴板", foreground="blue") return messagebox.showinfo("提示", "没有可复制的AI回复") def export_chat(self): """导出对话为文本文件""" if not self.conversation_history: messagebox.showwarning("提示", "没有对话可导出") return file_path = filedialog.asksaveasfilename(defaultextension=".txt", filetypes=[("文本文件", "*.txt"), ("所有文件", "*.*")]) if file_path: try: with open(file_path, 'w', encoding='utf-8') as f: f.write(f"对话导出 - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") f.write(f"模型: {self.model_var.get()}\n") f.write("=" * 60 + "\n\n") for msg in self.conversation_history: sender = "用户" if msg["role"] == "user" else "AI" f.write(f"{sender}:\n{msg['content']}\n\n") messagebox.showinfo("成功", f"对话已导出到:{file_path}") except Exception as e: messagebox.showerror("错误", f"导出失败:{str(e)}") def import_settings(self): """从JSON文件导入设置""" file_path = filedialog.askopenfilename(filetypes=[("JSON文件", "*.json"), ("所有文件", "*.*")]) if file_path: try: with open(file_path, 'r', encoding='utf-8') as f: settings = json.load(f) if "api_key" in settings: self.api_key_var.set(settings["api_key"]) if "system_prompt" in settings: self.system_prompt.delete("1.0", tk.END) self.system_prompt.insert(tk.END, settings["system_prompt"]) if "max_tokens" in settings: self.max_tokens_var.set(str(settings["max_tokens"])) if "temperature" in settings: self.temperature_var.set(str(settings["temperature"])) if "top_p" in settings: self.top_p_var.set(str(settings["top_p"])) if "model" in settings and settings["model"] in self.models: self.model_var.set(settings["model"]) messagebox.showinfo("成功", "设置导入完成") except Exception as e: messagebox.showerror("错误", f"导入失败:{str(e)}") if __name__ == "__main__": root = tk.Tk() app = ChatBotUI(root) root.mainloop()