import JSON; import win.ui; namespace web.rest.aiChat; class settingForm { ctor(parent,configFile) { if(type(configFile)!="table") error("请指定配置文件",2); ..table.mix(configFile,{ title = "AI 配置"; itemNames = ["默认"]; itemData = [{ url = configFile.url || configFile.defaultUrl || "https://ai.aardio.com/api/v1"; key = configFile.key; model = configFile.model; temperature = configFile.temperature; }]; }); /*DSG{{*/ this = ..win.form(text=configFile.title;right=686;bottom=590;border="dialog frame";exmode="none";max=false;min=false;mode="popup";parent=parent) this.add( btnAdd={cls="plus";left=18;top=487;right=52;bottom=517;align="left";color=0x3C3C3C;db=1;dl=1;font=LOGFONT(h=-13);iconStyle={align="left";font=LOGFONT(h=-13;name='FontAwesome');padding={left=8}};iconText='\uF067';notify=1;textPadding={left=25};z=15}; btnEdit={cls="plus";left=93;top=487;right=127;bottom=517;align="left";color=0x3C3C3C;db=1;dl=1;font=LOGFONT(h=-13);iconStyle={align="left";font=LOGFONT(h=-13;name='FontAwesome');padding={left=8}};iconText='\uF044';notify=1;textPadding={left=25};z=13}; btnRemove={cls="plus";left=56;top=487;right=90;bottom=517;align="left";color=0x3C3C3C;db=1;dl=1;font=LOGFONT(h=-13);iconStyle={align="left";font=LOGFONT(h=-13;name='FontAwesome');padding={left=8}};iconText='\uF1F8';notify=1;textPadding={left=25};z=14}; btnSave={cls="button";text="更新配置";left=467;top=534;right=637;bottom=579;color=0xD77800;db=1;dr=1;font=LOGFONT(h=-14);z=28}; chkCustomFlag1={cls="checkbox";text="自定义";left=135;top=560;right=442;bottom=580;db=1;dl=1;dr=1;hide=1;z=27}; chkKatex={cls="checkbox";text=" 解析数学公式";left=18;top=560;right=129;bottom=580;db=1;dl=1;z=26}; cmbProtocol={cls="combobox";left=567;top=93;right=662;bottom=115;clip=1;clipch=1;dl=1;dr=1;dt=1;edge=1;items={"自动","openai","anthropic","google","vertex"};mode="dropdownlist";vscroll=1;z=20}; editApiKey={cls="edit";left=281;top=60;right=662;bottom=82;dl=1;dr=1;dt=1;edge=1;multiline=1;z=18}; editApiUrl={cls="combobox";left=281;top=30;right=662;bottom=52;dl=1;dr=1;dt=1;edge=1;hscroll=1;items={};mode="dropdown";z=17}; editExtraParameters={cls="edit";left=281;top=175;right=662;bottom=197;dl=1;dr=1;dt=1;edge=1;multiline=1;z=23}; editModel={cls="combobox";left=281;top=93;right=522;bottom=115;clip=1;dl=1;dr=1;dt=1;edge=1;items={};mode="dropdown";vscroll=1;z=19}; editProxy={cls="edit";left=281;top=146;right=662;bottom=168;clip=1;clipch=1;dl=1;dr=1;dt=1;edge=1;z=22}; editSystemPrompt={cls="edit";left=281;top=204;right=662;bottom=517;clip=1;db=1;dl=1;dr=1;dt=1;edge=1;hscroll=1;multiline=1;vscroll=1;z=24}; groupbox={cls="groupbox";text="选择当前配置:";left=10;top=5;right=676;bottom=523;db=1;dl=1;dr=1;dt=1;edge=1;transparent=1;z=1}; lbMsgLimit={cls="static";left=379;top=533;right=407;bottom=553;db=1;dr=1;transparent=1;z=7}; lbTemperature={cls="static";left=533;top=126;right=559;bottom=146;dr=1;dt=1;z=8}; lstConfig={cls="listbox";left=18;top=30;right=187;bottom=488;db=1;dl=1;dt=1;edge=1;hscroll=1;items={};vscroll=1;z=16}; static={cls="static";text="API key:";left=196;top=62;right=279;bottom=83;align="right";dl=1;dt=1;transparent=1;z=2}; static10={cls="static";text="协议:";left=524;top=94;right=566;bottom=115;align="right";clip=1;dl=1;dt=1;z=12}; static2={cls="static";text="模型 ID:";left=196;top=94;right=279;bottom=115;align="right";dl=1;dt=1;transparent=1;z=3}; static4={cls="static";text="接口地址:";left=196;top=33;right=279;bottom=54;align="right";dl=1;dt=1;transparent=1;z=4}; static5={cls="static";text="temperature:";left=196;top=121;right=279;bottom=142;align="right";dl=1;dt=1;transparent=1;z=5}; static6={cls="static";text="上下文轮数:";left=0;top=533;right=90;bottom=565;align="right";db=1;dl=1;transparent=1;z=6}; static7={cls="static";text="代理服务器:";left=196;top=148;right=279;bottom=169;align="right";dl=1;dt=1;transparent=1;z=9}; static8={cls="static";text="系统提示词:";left=196;top=205;right=279;bottom=226;align="right";dl=1;dt=1;transparent=1;z=10}; static9={cls="static";text="自定义参数:";left=196;top=178;right=279;bottom=199;align="right";dl=1;dt=1;transparent=1;z=11}; tbMsgLimit={cls="plus";left=94;top=534;right=368;bottom=549;bgcolor=0xFFD9AB23;border={radius=-1};color=0x005CFF;db=1;dl=1;dr=1;foreRight=15;forecolor=0xFF1C77FF;max=100;min=0;paddingBottom=5;paddingTop=5;z=25}; tbTemperature={cls="plus";left=281;top=124;right=522;bottom=139;bgcolor=0xFFD9AB23;border={radius=-1};clip=1;color=0x005CFF;dl=1;dr=1;dt=1;foreRight=15;forecolor=0xFF1C77FF;max=100;min=0;paddingBottom=5;paddingTop=5;z=21} ) /*}}*/ this.tbMsgLimit.setTrackbarRange(1,100); this.tbTemperature.setTrackbarRange(1,100); this.editExtraParameters.setCueBannerText("JSON 格式附加参数") this.editModel.setCueBannerText("请在输入 key 以后点右侧下拉按钮选择模型 ID") this.editProxy.setCueBannerText("socks=127.0.0.1:1081") this.editSystemPrompt.limit = -1; this.editApiKey.onFocusGot = function(){ owner.passwordChar = null; owner.height = this.dpiScale(110); this.hyperlink1.origHide = this.hyperlink1.hide; this.hyperlink1.hide = true; } this.editApiKey.onFocusLost = function(){ owner.passwordChar = "*"; owner.text = ..string.crlf(owner.text,"") owner.height = this.dpiScale(27); this.hyperlink1.hide = this.hyperlink1.origHide; } this.editApiKey.passwordChar = "*"; this.editExtraParameters.onFocusGot = function(){ owner.height = this.dpiScale(100); if(owner.text=""){ owner.text='{\r\n\r\n}' this.setTimeout( function(){ this.editExtraParameters.caretPos = 3; },100) } } this.editExtraParameters.onFocusLost = function(){ owner.height = this.dpiScale(27); if(..string.match(owner.text,"^\s*{\s*}\s*$")) owner.text=""; } this.editApiUrl.items = configFile.urls || { "https://ai.aardio.com/api/v1", "https://api.deepseek.com/v1", "https://openrouter.ai/api/v1", "https://api-inference.modelscope.cn/v1", "https://ark.cn-beijing.volces.com/api/v3/bots", "https://ark.cn-beijing.volces.com/api/v3", "https://api.anthropic.com/v1", "https://api.openai.com/v1", "https://generativelanguage.googleapis.com/v1beta/openai", "https://aiplatform.googleapis.com/v1beta1/projects/{project_id}/locations/global/endpoints/openapi/", "https://api.x.ai/v1", "https://api.siliconflow.cn/v1", "https://api.lkeap.cloud.tencent.com/v1", "https://dashscope.aliyuncs.com/api/v1", "http://localhost:11434/v1" } this.editApiUrl.onFocusLost = function(){ var url = ..string.trim(this.editApiUrl.text,`"' `); if(#url && !..string.match(url,"^<@@http@>[sS]?\:")){ this.editApiUrl.text = (..string.startsWith(url,"localhost")?"http://":"https://")+url; } } import ..web.rest.aiChat; var cacheModelList = {}; this.editModel.onDropDown = function(){ var url = ..string.trim(this.editApiUrl.text,`"' `); var apiKey = ..string.trim(this.editApiKey.text,`"' `); if(#url && #apiKey){ if(!cacheModelList[url]){ var aiClient = ..web.rest.aiChat({ url = url; key = apiKey; proxy = ..string.trim(this.editProxy.text); model = ""; }); aiClient._http.setTimeouts(500,500,500); var models = aiClient.listModels(); if(models[[1]][["id"]]){ cacheModelList[url] = ..table.map(models,lambda(v) v["id"] ) } else { cacheModelList[url] = null; } } if(#cacheModelList[url]){ this.editModel.autoComplete(cacheModelList[url],1 ); return; } } this.editModel.items = {}; } this.editApiUrl.onListChange = function(){ this.editModel.text = "" } this.tbMsgLimit.setTrackbarRange(3,100); this.tbTemperature.setTrackbarRange(0,100); this.tbTemperature.onPosChanged = function( pos,triggeredByUser ){ var pos = this.tbTemperature.progressPos; this.lbTemperature.text = ..math.round(pos / 100,1); } this.tbMsgLimit.onPosChanged = function( pos,triggeredByUser ){ this.lbMsgLimit.text = this.tbMsgLimit.progressPos;; } import ..win.ui.listEdit; var listEdit = ..win.ui.listEdit(this.lstConfig); listEdit.editBox.setCueBannerText("请输入配置名",true); this.btnEdit.oncommand = function(id,event){ listEdit.beginEdit(); } this.btnAdd.oncommand = function(id,event){ listEdit.beginEdit(0); } this.lstConfig.onSelChange = function(){ var selIndex = this.lstConfig.selIndex; var prevSelIndex = configFile.selItem; if(prevSelIndex && prevSelIndex != selIndex){ var extraParameters = this.editExtraParameters.text; if(#extraParameters && !..string.match(extraParameters,"^\s*{\s*}\s*$")){ extraParameters = ..JSON.tryParse(extraParameters); } else{ extraParameters = null; } //保存上一个配置 var configItem = { url = this.editApiUrl.text; key = ..string.trim(this.editApiKey.text,`"' `); model = ..string.match(this.editModel.text,"\s*(\S+)"); temperature = ..math.round(this.tbTemperature.progressPos / 100,1); msgLimit = this.tbMsgLimit.progressPos; proxy = ..string.trim(this.editProxy.text); systemPrompt = this.editSystemPrompt.text; extraParameters = extraParameters; f1 = this.chkCustomFlag1.checked; } if(this.cmbProtocol.selIndex>1){ configItem.protocol = this.cmbProtocol.selText; } if( configItem.f1 && (!configItem.f1MultiSelect) ){ for(i,cfg in configFile.itemData){ cfg.f1 = null; } } configFile.itemData[prevSelIndex] = configItem; } //加载下一个配置 var selIndex = this.lstConfig.selIndex; var configItem = configFile.itemData[selIndex] || {}; this.chkCustomFlag1.checked = configItem.f1; import ..string.escape2; configItem.key = ..string.escape2.unescape(configItem.key) this.editApiKey.text = ..string.escape2.escape(configItem.key) this.editApiUrl.text = configItem.url; this.editProxy.text = configItem.proxy; this.editSystemPrompt.text = configItem.systemPrompt || configFile.defaultSystePrompt || ""; if(type(configItem.extraParameters)=="table") this.editExtraParameters.text = ..JSON.stringify(configItem.extraParameters); else this.editExtraParameters.text = ""; if(configItem.temperature===null) configItem.temperature = 0.1; this.tbTemperature.progressPos = configItem.temperature * 100; this.lbTemperature.text = configItem.temperature; this.tbMsgLimit.progressPos = configItem.msgLimit || 15; this.lbMsgLimit.text = configItem.msgLimit || 15; if(configItem.protocol){ this.cmbProtocol.selText = configItem.protocol } else{ this.cmbProtocol.selIndex = 1; } this.editModel.text = configItem.model; configFile.itemData[selIndex] = configItem; configFile.itemNames = this.lstConfig.items; configFile.itemData = ..table.slice(configFile.itemData,1,#configFile.itemNames); configFile.selItem = selIndex; configFile.save(); if(this.onSelChange){ this.onSelChange(selIndex,configItem) } } //保存并更新配置 import ..inet.url; this.btnSave.oncommand = function(id,event){ var extraParameters = ..string.trim(this.editExtraParameters.text); if(#extraParameters && !..string.match(extraParameters,"^\s*{\s*}\s*$") ){ var err; extraParameters,err = ..JSON.tryParse(extraParameters); if(!extraParameters || type(extraParameters)!="table"){ return this.msgboxErr( "JSON 格式错误 " + (err||""),configFile.title); } } var configItem = { url = ..string.trim(this.editApiUrl.text); key = ..string.trim(this.editApiKey.text,`"' `); model = ..string.match(this.editModel.text,"\s*(\S+)"); temperature = ..math.round(this.tbTemperature.progressPos / 100,1); msgLimit = this.tbMsgLimit.progressPos; proxy = ..string.trim(this.editProxy.text); systemPrompt = this.editSystemPrompt.text; extraParameters = extraParameters; f1 = this.chkCustomFlag1.checked; } if(this.cmbProtocol.selIndex>1){ configItem.protocol = this.cmbProtocol.selText; } if( configItem.f1 && (!configItem.f1MultiSelect) ){ for(i,cfg in configFile.itemData){ cfg.f1 = null; } } if(!#configItem.url){ this.editApiUrl.editBox.showWarningTip(configFile.title,"请输入接口网址!") return; } if( ..string.match(configItem.proxy,"^[\w\-_\.]+$") && !(#configItem.model) ){ this.editProxy.showWarningTip(configFile.title,"是否将模型 ID 输入到代理服务器了呢?!") return; } if(!(#configItem.model)){ this.editModel.editBox.showWarningTip(configFile.title,"请输入模型 ID !") return; } import ..string.escape2; configItem.key = ..string.escape2.unescape(configItem.key) if(!#configItem.proxy){ configItem.proxy = null; } var selIndex = this.lstConfig.selIndex; configFile.selItem = selIndex; configFile.itemData[selIndex] = configItem; configFile.katex = this.chkKatex.checked; configFile.save(); this.endModal(true); ..thread.delay(100) } this.btnRemove.oncommand = function(id,event){ if(this.lstConfig.count==1){ return this.msgboxErr("只有一个配置方案时不允许删除!"); } var selIndex = this.lstConfig.selIndex; ..table.remove(configFile.itemData,selIndex); ..table.remove(configFile.itemNames,selIndex); this.lstConfig.delete(selIndex) selIndex = selIndex<=this.lstConfig.count ? selIndex : selIndex -1; configFile.selItem = null; this.lstConfig.selIndex = selIndex; this.lstConfig.onSelChange() } listEdit.onEditChanged = function(newText,selIndex){ configFile.itemNames = this.lstConfig.items; this.lstConfig.selIndex = selIndex; this.lstConfig.onSelChange(); configFile.save(); } this.lstConfig.items = configFile.itemNames; this.lstConfig.selIndex = configFile.selItem || 1; this.lstConfig.onSelChange(); if(configFile.allowKatex){ this.chkKatex.checked = configFile.katex; } else{ this.chkKatex.checked = false; this.chkKatex.disabled = true; } if(configFile.f1Title){ this.chkCustomFlag1.text = configFile.f1Title; this.chkCustomFlag1.hide = false; } this.hyperlink1 = this.editApiKey.addCtrl( cls="plus"; marginRight=0;marginBottom=2; bgcolor=0xFFFFFF; width=32; text = configFile.hyperlink1Title || "VIP"; hide = !configFile.hyperlink1Title; ) this.hyperlink1.oncommand = function(id,event){ if(this.onHyperlink){ this.onHyperlink( this.editApiUrl.text, this.editApiKey.text ) } } this.hyperlink1.skin({ color={ active=0xFF00FF00; default=0xFF000080; disabled=0xFF999999; hover=0xFFFF0000 } }) this.btnAdd.skin({ color={ active=0xFF00FF00; default=0xFF3C3C3C; disabled=0xFF6D6D6D; hover=0xFFFF0000 } }) this.btnRemove.skin({ color={ active=0xFF00FF00; default=0xFF3C3C3C; disabled=0xFF6D6D6D; hover=0xFFFF0000 } }) this.btnEdit.skin({ color={ active=0xFF00FF00; default=0xFF3C3C3C; disabled=0xFF6D6D6D; hover=0xFFFF0000 } }) this.tbMsgLimit.skin({ background={ default=0xFF23ABD9 }; foreground={ default=0xFFFF771C; hover=0xFFFF6600 }; color={ default=0xFFFF5C00; hover=0xFFFF6600 } }) this.tbTemperature.skin({ background={ default=0xFF23ABD9 }; foreground={ default=0xFFFF771C; hover=0xFFFF6600 }; color={ default=0xFFFF5C00; hover=0xFFFF6600 } }) } } if( !owner ){ } /**details(说明) web.rest.aiChat.settingForm 对象提供两个事件 1. onSelChange 事件 ~~~aardio onSelChange = function(selIndex,configItem){ } ~~~ 在配置界面切换助手时触发。 - 参数 selIndex 为当前选项索引。 - 参数 configItem 为当前配置项。 2. onSelChange 事件 ~~~aardio onHyperlink = function(apiUrl,apiKey){ } ~~~ 点击超链接时触发。 - 参数 apiUrl 为当前接口 URL。 - 参数 apiKey 为当前接口 API key 。 可通过配置窗体的 hyperlink1 属性访问超链接控件。 hyperlink1 默认为隐藏状态,可通过 hyperlink1 控件的 hide , disabled 属性控制其显示状态。 可在 onSelChange 事件中动态设置 hyperlink1 控件是否显示。 hyperlink1 的 text 属性默认为空值,可根据需要修改。 end details**/ /**intellisense() web.rest.aiChat.settingForm = 用于创建设置 AI 接口配置的窗口。\n此窗口用到了 FontAwesome 图标 `\uF067`,`\uF044`,`\uF1F8`,\n调用者必须提前导入 fonts.fontAwesome 或精简的 fontAwesome 字体库。 web.rest.aiChat.settingForm(.(parentForm,configFile) = 窗口类。\n- 参数 @parentForm 可指定父窗体对象,也可指定空值(null )。\n- 参数 @configFile 必须指定配置文件对象(fsys.table 对象或 fsys.config 对象的成员)。\n\n参数 @configFile 可选预先指定以下字段:\n- title: 自定义窗口标题\n- f1Title: 自定义复选框 1 的标题,自动绑定定配置值 configFile.f1(flag1 的缩写)。\n如果不指定 f1Title 则不显示 自定义复选框 1 。\n- f1MultiSelect: 如果为 true 则允许多个助手启用 f1,否则为单选切换模式,默认为 false。\n- allowKatex: 是否允许启用数学公式(Win10 以下总是禁用)。\n- hyperlink1Title: 指定超链接标题,指定则默认显示 hyperlink1 控件。\n- itemNames:字符串数组,指定 AI 助手名称列表。\n- itemData:AI 接口配置数组,必须与 temNames 的元素一一对应。\n- urls: 字符串数组,接口 URL 显示的默认接口 URL 列表。\n!winform. end intellisense**/