From 9f39a6571a395215bb5b06e033e824356df96a38 Mon Sep 17 00:00:00 2001 From: binary-husky Date: Tue, 28 Jan 2025 02:52:56 +0800 Subject: [PATCH] feat: customized font & font size --- README.md | 2 +- config.py | 24 ++++++++ main.py | 7 +-- themes/common.css | 19 +++++- themes/green.css | 1 - themes/green.py | 8 +++ themes/gui_toolbar.py | 16 +++-- themes/init.js | 137 ++++++++++++++++++++++++++++++++++++++++-- themes/welcome.js | 36 ++++++----- version | 4 +- 10 files changed, 220 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index ba6dd484..4c10f885 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ > [!IMPORTANT] +> `master主分支`最新动态(2025.1.28): 增加字体自定义功能 > `frontier开发分支`最新动态(2024.12.9): 更新对话时间线功能,优化xelatex论文翻译 > `wiki文档`最新动态(2024.12.5): 更新ollama接入指南 -> `master主分支`最新动态(2024.12.19): 更新3.91版本,更新release页一键安装脚本 > > 2024.10.10: 突发停电,紧急恢复了提供[whl包](https://drive.google.com/drive/folders/14kR-3V-lIbvGxri4AHc8TpiA1fqsw7SK?usp=sharing)的文件服务器 > 2024.10.8: 版本3.90加入对llama-index的初步支持,版本3.80加入插件二级菜单功能(详见wiki) diff --git a/config.py b/config.py index 7c39b692..4740717f 100644 --- a/config.py +++ b/config.py @@ -85,6 +85,30 @@ DEFAULT_WORKER_NUM = 3 THEME = "Default" AVAIL_THEMES = ["Default", "Chuanhu-Small-and-Beautiful", "High-Contrast", "Gstaff/Xkcd", "NoCrypt/Miku"] +FONT = "Theme-Default-Font" +AVAIL_FONTS = [ + "默认值(Theme-Default-Font)", + "宋体(SimSun)", + "黑体(SimHei)", + "楷体(KaiTi)", + "仿宋(FangSong)", + "华文细黑(STHeiti Light)", + "华文楷体(STKaiti)", + "华文仿宋(STFangsong)", + "华文宋体(STSong)", + "华文中宋(STZhongsong)", + "华文新魏(STXinwei)", + "华文隶书(STLiti)", + "思源宋体(Source Han Serif CN VF@https://chinese-fonts-cdn.deno.dev/packages/syst/dist/SourceHanSerifCN/result.css)", + "月星楷(Moon Stars Kai HW@https://chinese-fonts-cdn.deno.dev/packages/moon-stars-kai/dist/MoonStarsKaiHW-Regular/result.css)", + "珠圆体(MaokenZhuyuanTi@https://chinese-fonts-cdn.deno.dev/packages/mkzyt/dist/猫啃珠圆体/result.css)", + "平方萌萌哒(PING FANG MENG MNEG DA@https://chinese-fonts-cdn.deno.dev/packages/pfmmd/dist/平方萌萌哒/result.css)", + "Helvetica", + "ui-sans-serif", + "sans-serif", + "system-ui" +] + # 默认的系统提示词(system prompt) INIT_SYS_PROMPT = "Serve me as a writing and programming assistant." diff --git a/main.py b/main.py index 4b526b1b..310fa6a3 100644 --- a/main.py +++ b/main.py @@ -49,7 +49,7 @@ def main(): # 读取配置 proxies, WEB_PORT, LLM_MODEL, CONCURRENT_COUNT, AUTHENTICATION = get_conf('proxies', 'WEB_PORT', 'LLM_MODEL', 'CONCURRENT_COUNT', 'AUTHENTICATION') CHATBOT_HEIGHT, LAYOUT, AVAIL_LLM_MODELS, AUTO_CLEAR_TXT = get_conf('CHATBOT_HEIGHT', 'LAYOUT', 'AVAIL_LLM_MODELS', 'AUTO_CLEAR_TXT') - ENABLE_AUDIO, AUTO_CLEAR_TXT, PATH_LOGGING, AVAIL_THEMES, THEME, ADD_WAIFU = get_conf('ENABLE_AUDIO', 'AUTO_CLEAR_TXT', 'PATH_LOGGING', 'AVAIL_THEMES', 'THEME', 'ADD_WAIFU') + ENABLE_AUDIO, AUTO_CLEAR_TXT, AVAIL_FONTS, AVAIL_THEMES, THEME, ADD_WAIFU = get_conf('ENABLE_AUDIO', 'AUTO_CLEAR_TXT', 'AVAIL_FONTS', 'AVAIL_THEMES', 'THEME', 'ADD_WAIFU') NUM_CUSTOM_BASIC_BTN, SSL_KEYFILE, SSL_CERTFILE = get_conf('NUM_CUSTOM_BASIC_BTN', 'SSL_KEYFILE', 'SSL_CERTFILE') DARK_MODE, INIT_SYS_PROMPT, ADD_WAIFU, TTS_TYPE = get_conf('DARK_MODE', 'INIT_SYS_PROMPT', 'ADD_WAIFU', 'TTS_TYPE') if LLM_MODEL not in AVAIL_LLM_MODELS: AVAIL_LLM_MODELS += [LLM_MODEL] @@ -178,7 +178,7 @@ def main(): # 左上角工具栏定义 from themes.gui_toolbar import define_gui_toolbar checkboxes, checkboxes_2, max_length_sl, theme_dropdown, system_prompt, file_upload_2, md_dropdown, top_p, temperature = \ - define_gui_toolbar(AVAIL_LLM_MODELS, LLM_MODEL, INIT_SYS_PROMPT, THEME, AVAIL_THEMES, ADD_WAIFU, help_menu_description, js_code_for_toggle_darkmode) + define_gui_toolbar(AVAIL_LLM_MODELS, LLM_MODEL, INIT_SYS_PROMPT, THEME, AVAIL_THEMES, AVAIL_FONTS, ADD_WAIFU, help_menu_description, js_code_for_toggle_darkmode) # 浮动菜单定义 from themes.gui_floating_menu import define_gui_floating_menu @@ -228,9 +228,6 @@ def main(): cancel_handles.append(submit_btn.click(**predict_args)) resetBtn.click(None, None, [chatbot, history, status], _js= """clear_conversation""") # 先在前端快速清除chatbot&status resetBtn2.click(None, None, [chatbot, history, status], _js="""clear_conversation""") # 先在前端快速清除chatbot&status - # reset_server_side_args = (lambda history: ([], [], "已重置"), [history], [chatbot, history, status]) - # resetBtn.click(*reset_server_side_args) # 再在后端清除history - # resetBtn2.click(*reset_server_side_args) # 再在后端清除history clearBtn.click(None, None, [txt, txt2], _js=js_code_clear) clearBtn2.click(None, None, [txt, txt2], _js=js_code_clear) if AUTO_CLEAR_TXT: diff --git a/themes/common.css b/themes/common.css index 557e800c..22b6e294 100644 --- a/themes/common.css +++ b/themes/common.css @@ -1,9 +1,16 @@ +:root { + --gpt-academic-message-font-size: 15px; +} + +.message { + font-size: var(--gpt-academic-message-font-size) !important; +} + #plugin_arg_menu { transform: translate(-50%, -50%); border: dashed; } - /* hide remove all button */ .remove-all.svelte-aqlk7e.svelte-aqlk7e.svelte-aqlk7e { visibility: hidden; @@ -207,6 +214,7 @@ .welcome-content { text-wrap: balance; height: 55px; + font-size: 13px; display: flex; align-items: center; } @@ -275,4 +283,13 @@ .tooltip.svelte-p2nen8.svelte-p2nen8 { box-shadow: 10px 10px 15px rgba(0, 0, 0, 0.5); left: 10px; +} + +#elem_fontsize, +#elem_top_p, +#elem_temperature, +#elem_max_length_sl, +#elem_prompt { + /* 左右为0;顶部为0,底部为2px */ + padding: 0 0 4px 0; } \ No newline at end of file diff --git a/themes/green.css b/themes/green.css index d920372f..2e181c51 100644 --- a/themes/green.css +++ b/themes/green.css @@ -567,7 +567,6 @@ ul:not(.options) { border-radius: var(--radius-xl) !important; border: none; padding: var(--spacing-xl) !important; - font-size: 15px !important; line-height: var(--line-md) !important; min-height: calc(var(--text-md)*var(--line-md) + 2*var(--spacing-xl)); min-width: calc(var(--text-md)*var(--line-md) + 2*var(--spacing-xl)); diff --git a/themes/green.py b/themes/green.py index ba681ae0..61485e7a 100644 --- a/themes/green.py +++ b/themes/green.py @@ -10,6 +10,14 @@ theme_dir = os.path.dirname(__file__) def adjust_theme(): try: set_theme = gr.themes.Soft( + font=[ + "Helvetica", + "Microsoft YaHei", + "ui-sans-serif", + "sans-serif", + "system-ui", + ], + font_mono=["ui-monospace", "Consolas", "monospace"], primary_hue=gr.themes.Color( c50="#EBFAF2", c100="#CFF3E1", diff --git a/themes/gui_toolbar.py b/themes/gui_toolbar.py index 7004da9f..703edcfb 100644 --- a/themes/gui_toolbar.py +++ b/themes/gui_toolbar.py @@ -1,6 +1,7 @@ import gradio as gr +from toolbox import get_conf -def define_gui_toolbar(AVAIL_LLM_MODELS, LLM_MODEL, INIT_SYS_PROMPT, THEME, AVAIL_THEMES, ADD_WAIFU, help_menu_description, js_code_for_toggle_darkmode): +def define_gui_toolbar(AVAIL_LLM_MODELS, LLM_MODEL, INIT_SYS_PROMPT, THEME, AVAIL_THEMES, AVAIL_FONTS, ADD_WAIFU, help_menu_description, js_code_for_toggle_darkmode): with gr.Floating(init_x="0%", init_y="0%", visible=True, width=None, drag="forbidden", elem_id="tooltip"): with gr.Row(): with gr.Tab("上传文件", elem_id="interact-panel"): @@ -9,12 +10,12 @@ def define_gui_toolbar(AVAIL_LLM_MODELS, LLM_MODEL, INIT_SYS_PROMPT, THEME, AVAI with gr.Tab("更换模型", elem_id="interact-panel"): md_dropdown = gr.Dropdown(AVAIL_LLM_MODELS, value=LLM_MODEL, elem_id="elem_model_sel", label="更换LLM模型/请求源").style(container=False) - top_p = gr.Slider(minimum=-0, maximum=1.0, value=1.0, step=0.01,interactive=True, label="Top-p (nucleus sampling)",) + top_p = gr.Slider(minimum=-0, maximum=1.0, value=1.0, step=0.01,interactive=True, label="Top-p (nucleus sampling)", elem_id="elem_top_p") temperature = gr.Slider(minimum=-0, maximum=2.0, value=1.0, step=0.01, interactive=True, label="Temperature", elem_id="elem_temperature") - max_length_sl = gr.Slider(minimum=256, maximum=1024*32, value=4096, step=128, interactive=True, label="Local LLM MaxLength",) + max_length_sl = gr.Slider(minimum=256, maximum=1024*32, value=4096, step=128, interactive=True, label="Local LLM MaxLength", elem_id="elem_max_length_sl") system_prompt = gr.Textbox(show_label=True, lines=2, placeholder=f"System Prompt", label="System prompt", value=INIT_SYS_PROMPT, elem_id="elem_prompt") temperature.change(None, inputs=[temperature], outputs=None, - _js="""(temperature)=>gpt_academic_gradio_saveload("save", "elem_prompt", "js_temperature_cookie", temperature)""") + _js="""(temperature)=>gpt_academic_gradio_saveload("save", "elem_temperature", "js_temperature_cookie", temperature)""") system_prompt.change(None, inputs=[system_prompt], outputs=None, _js="""(system_prompt)=>gpt_academic_gradio_saveload("save", "elem_prompt", "js_system_prompt_cookie", system_prompt)""") md_dropdown.change(None, inputs=[md_dropdown], outputs=None, @@ -22,6 +23,8 @@ def define_gui_toolbar(AVAIL_LLM_MODELS, LLM_MODEL, INIT_SYS_PROMPT, THEME, AVAI with gr.Tab("界面外观", elem_id="interact-panel"): theme_dropdown = gr.Dropdown(AVAIL_THEMES, value=THEME, label="更换UI主题").style(container=False) + fontfamily_dropdown = gr.Dropdown(AVAIL_FONTS, value=get_conf("FONT"), elem_id="elem_fontfamily", label="更换字体类型").style(container=False) + fontsize_slider = gr.Slider(minimum=5, maximum=25, value=15, step=1, interactive=True, label="字体大小(默认15)", elem_id="elem_fontsize") checkboxes = gr.CheckboxGroup(["基础功能区", "函数插件区", "浮动输入区", "输入清除键", "插件参数区"], value=["基础功能区", "函数插件区"], label="显示/隐藏功能区", elem_id='cbs').style(container=False) opt = ["自定义菜单"] value=[] @@ -31,7 +34,10 @@ def define_gui_toolbar(AVAIL_LLM_MODELS, LLM_MODEL, INIT_SYS_PROMPT, THEME, AVAI dark_mode_btn.click(None, None, None, _js=js_code_for_toggle_darkmode) open_new_tab = gr.Button("打开新对话", variant="secondary").style(size="sm") open_new_tab.click(None, None, None, _js=f"""()=>duplicate_in_new_window()""") - + fontfamily_dropdown.select(None, inputs=[fontfamily_dropdown], outputs=None, + _js="""(fontfamily)=>{gpt_academic_gradio_saveload("save", "elem_fontfamily", "js_fontfamily", fontfamily); gpt_academic_change_chatbot_font(fontfamily, null, null);}""") + fontsize_slider.change(None, inputs=[fontsize_slider], outputs=None, + _js="""(fontsize)=>{gpt_academic_gradio_saveload("save", "elem_fontsize", "js_fontsize", fontsize); gpt_academic_change_chatbot_font(null, fontsize, null);}""") with gr.Tab("帮助", elem_id="interact-panel"): gr.Markdown(help_menu_description) diff --git a/themes/init.js b/themes/init.js index d8a2e67a..5d7de63b 100644 --- a/themes/init.js +++ b/themes/init.js @@ -5,6 +5,129 @@ function remove_legacy_cookie() { } +function processFontFamily(fontfamily) { + // 检查是否包含括号 + if (fontfamily.includes('(')) { + // 分割字符串 + const parts = fontfamily.split('('); + const fontNamePart = parts[1].split(')')[0].trim(); // 获取括号内的部分 + + // 检查是否包含 @ + if (fontNamePart.includes('@')) { + const [fontName, fontUrl] = fontNamePart.split('@').map(part => part.trim()); + return { fontName, fontUrl }; + } else { + return { fontName: fontNamePart, fontUrl: null }; + } + } else { + return { fontName: fontfamily, fontUrl: null }; + } +} + +// 检查字体是否存在 +function checkFontAvailability(fontfamily) { + return new Promise((resolve) => { + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + + // 设置两个不同的字体进行比较 + const testText = 'abcdefghijklmnopqrstuvwxyz0123456789'; + context.font = `16px ${fontfamily}, sans-serif`; + const widthWithFont = context.measureText(testText).width; + + context.font = '16px sans-serif'; + const widthWithFallback = context.measureText(testText).width; + + // 如果宽度相同,说明字体不存在 + resolve(widthWithFont !== widthWithFallback); + }); +} +async function checkFontAvailabilityV2(fontfamily) { + fontName = fontfamily; + console.log('Checking font availability:', fontName); + if ('queryLocalFonts' in window) { + try { + const fonts = await window.queryLocalFonts(); + const fontExists = fonts.some(font => font.family === fontName); + console.log(`Local Font "${fontName}" exists:`, fontExists); + return fontExists; + } catch (error) { + console.error('Error querying local fonts:', error); + return false; + } + } else { + console.error('queryLocalFonts is not supported in this browser.'); + return false; + } +} +// 动态加载字体 +function loadFont(fontfamily, fontUrl) { + return new Promise((resolve, reject) => { + // 使用 Google Fonts 或其他字体来源 + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = fontUrl; + link.onload = () => { + toast_push(`字体 "${fontfamily}" 已成功加载`, 3000); + resolve(); + }; + link.onerror = (error) => { + reject(error); + }; + document.head.appendChild(link); + }); +} +function gpt_academic_change_chatbot_font(fontfamily, fontsize, fontcolor) { + const chatbot = document.querySelector('#gpt-chatbot'); + // 检查元素是否存在 + if (chatbot) { + if (fontfamily != null) { + // 更改字体 + const result = processFontFamily(fontfamily); + if (result.fontName == "Theme-Default-Font") { + chatbot.style.fontFamily = result.fontName; + return; + } + // 检查字体是否存在 + checkFontAvailability(result.fontName).then((isAvailable) => { + if (isAvailable) { + // 如果字体存在,直接应用 + chatbot.style.fontFamily = result.fontName; + } else { + if (result.fontUrl == null) { + // toast_push('无法加载字体,本地字体不存在,且URL未提供', 3000); + // 直接把失效的字体放上去,让系统自动fallback + chatbot.style.fontFamily = result.fontName; + return; + } else { + toast_push('正在下载字体', 3000); + // 如果字体不存在,尝试加载字体 + loadFont(result.fontName, result.fontUrl).then(() => { + chatbot.style.fontFamily = result.fontName; + }).catch((error) => { + console.error(`无法加载字体 "${result.fontName}":`, error); + }); + } + } + }); + + } + if (fontsize != null) { + // 修改字体大小 + document.documentElement.style.setProperty( + '--gpt-academic-message-font-size', + `${fontsize}px` + ); + } + if (fontcolor != null) { + // 更改字体颜色 + chatbot.style.color = fontcolor; + } + } else { + console.error('#gpt-chatbot is missing'); + } +} + async function GptAcademicJavaScriptInit(dark, prompt, live2d, layout, tts) { // 第一部分,布局初始化 remove_legacy_cookie(); @@ -13,7 +136,7 @@ async function GptAcademicJavaScriptInit(dark, prompt, live2d, layout, tts) { ButtonWithDropdown_init(); update_conversation_metadata(); window.addEventListener("gptac_restore_chat_from_local_storage", restore_chat_from_local_storage); - + // 加载欢迎页面 const welcomeMessage = new WelcomeMessage(); welcomeMessage.begin_render(); @@ -23,7 +146,7 @@ async function GptAcademicJavaScriptInit(dark, prompt, live2d, layout, tts) { welcomeMessage.update(); }); chatbotObserver.observe(chatbotIndicator, { attributes: true, childList: true, subtree: true }); - + if (layout === "LEFT-RIGHT") { chatbotAutoHeight(); } if (layout === "LEFT-RIGHT") { limit_scroll_position(); } @@ -46,7 +169,7 @@ async function GptAcademicJavaScriptInit(dark, prompt, live2d, layout, tts) { } // 自动朗读 - if (tts != "DISABLE"){ + if (tts != "DISABLE") { enable_tts = true; if (getCookie("js_auto_read_cookie")) { auto_read_tts = getCookie("js_auto_read_cookie") @@ -56,7 +179,11 @@ async function GptAcademicJavaScriptInit(dark, prompt, live2d, layout, tts) { } } } - + // 字体 + gpt_academic_gradio_saveload("load", "elem_fontfamily", "js_fontfamily", null, "str"); + gpt_academic_change_chatbot_font(getCookie("js_fontfamily"), null, null); + gpt_academic_gradio_saveload("load", "elem_fontsize", "js_fontsize", null, "str"); + gpt_academic_change_chatbot_font(null, getCookie("js_fontsize"), null); // SysPrompt 系统静默提示词 gpt_academic_gradio_saveload("load", "elem_prompt", "js_system_prompt_cookie", null, "str"); // Temperature 大模型温度参数 @@ -66,7 +193,7 @@ async function GptAcademicJavaScriptInit(dark, prompt, live2d, layout, tts) { const cached_model = getCookie("js_md_dropdown_cookie"); var model_sel = await get_gradio_component("elem_model_sel"); // determine whether the cached model is in the choices - if (model_sel.props.choices.includes(cached_model)){ + if (model_sel.props.choices.includes(cached_model)) { // change dropdown gpt_academic_gradio_saveload("load", "elem_model_sel", "js_md_dropdown_cookie", null, "str"); // 连锁修改chatbot的label diff --git a/themes/welcome.js b/themes/welcome.js index 3111e560..db452fd4 100644 --- a/themes/welcome.js +++ b/themes/welcome.js @@ -85,7 +85,7 @@ class WelcomeMessage { this.card_array = []; this.static_welcome_message_previous = []; this.reflesh_time_interval = 15 * 1000; - + this.major_title = "欢迎使用GPT-Academic"; const reflesh_render_status = () => { for (let index = 0; index < this.card_array.length; index++) { @@ -99,6 +99,8 @@ class WelcomeMessage { // call update when page size change, call this.update when page size change window.addEventListener('resize', this.update.bind(this)); + // add a loop to reflesh cards + this.startRefleshCards(); } begin_render() { @@ -106,9 +108,12 @@ class WelcomeMessage { } async startRefleshCards() { + // sleep certain time await new Promise(r => setTimeout(r, this.reflesh_time_interval)); - await this.reflesh_cards(); + // checkout visible status if (this.visible) { + // if visible, then reflesh cards + await this.reflesh_cards(); setTimeout(() => { this.startRefleshCards.call(this); }, 1); @@ -194,35 +199,37 @@ class WelcomeMessage { } async update() { - // console.log('update') + // update the card visibility const elem_chatbot = document.getElementById('gpt-chatbot'); const chatbot_top = elem_chatbot.getBoundingClientRect().top; const welcome_card_container = document.getElementsByClassName('welcome-card-container')[0]; + // detect if welcome card overflow let welcome_card_overflow = false; if (welcome_card_container) { const welcome_card_top = welcome_card_container.getBoundingClientRect().top; if (welcome_card_top < chatbot_top) { welcome_card_overflow = true; - // console.log("welcome_card_overflow"); } } var page_width = document.documentElement.clientWidth; const width_to_hide_welcome = 1200; if (!await this.isChatbotEmpty() || page_width < width_to_hide_welcome || welcome_card_overflow) { + // overflow ! if (this.visible) { - console.log("remove welcome"); - this.removeWelcome(); this.visible = false; // this two lines must always be together + // console.log("remove welcome"); + this.removeWelcome(); this.card_array = []; this.static_welcome_message_previous = []; } return; } if (this.visible) { + // console.log("already visible"); return; } - console.log("show welcome"); - this.showWelcome(); this.visible = true; // this two lines must always be together - this.startRefleshCards(); + // not overflow, not yet shown, then create and display welcome card + // console.log("show welcome"); + this.showWelcome(); } showCard(message) { @@ -263,7 +270,7 @@ class WelcomeMessage { } async showWelcome() { - + this.visible = true; // 首先,找到想要添加子元素的父元素 const elem_chatbot = document.getElementById('gpt-chatbot'); @@ -274,7 +281,7 @@ class WelcomeMessage { // 创建主标题 const major_title = document.createElement('div'); major_title.classList.add('welcome-title'); - major_title.textContent = "欢迎使用GPT-Academic"; + major_title.textContent = this.major_title; welcome_card_container.appendChild(major_title) // 创建卡片 @@ -297,16 +304,17 @@ class WelcomeMessage { } async removeWelcome() { + this.visible = false; // remove welcome-card-container const elem_chatbot = document.getElementById('gpt-chatbot'); const welcome_card_container = document.getElementsByClassName('welcome-card-container')[0]; - // 添加隐藏动画 + // begin hide animation welcome_card_container.classList.add('hide'); - // 等待动画结束后再移除元素 welcome_card_container.addEventListener('transitionend', () => { elem_chatbot.removeChild(welcome_card_container); }, { once: true }); - const timeout = 600; // 与CSS中transition的时间保持一致(1s) + // add a fail safe timeout + const timeout = 600; // 与 CSS 中 transition 的时间保持一致(1s) setTimeout(() => { if (welcome_card_container.parentNode) { elem_chatbot.removeChild(welcome_card_container); diff --git a/version b/version index 2edd3596..a1885152 100644 --- a/version +++ b/version @@ -1,5 +1,5 @@ { - "version": 3.91, + "version": 3.92, "show_feature": true, - "new_feature": "优化前端并修复TTS的BUG <-> 添加时间线回溯功能 <-> 支持chatgpt-4o-latest <-> 增加RAG组件 <-> 升级多合一主提交键" + "new_feature": "字体和字体大小自定义 <-> 优化前端并修复TTS的BUG <-> 添加时间线回溯功能 <-> 支持chatgpt-4o-latest <-> 增加RAG组件 <-> 升级多合一主提交键" }