aardio 文档

aardio 范例: AI 编码助手

import win.ui;
import fonts.fontAwesome;
/*DSG{{*/
var winform = win.form(text="aardio - AI 编码助手";right=759;bottom=607)
winform.add(
btnClear={cls="plus";text="清除";left=589;top=572;right=655;bottom=602;align="left";color=3947580;db=1;dr=1;font=LOGFONT(h=-13);iconStyle={align="left";font=LOGFONT(h=-13;name='FontAwesome');padding={left=8}};iconText='\uF014';notify=1;textPadding={left=25};z=6};
btnCopy={cls="plus";text="复制";left=203;top=573;right=267;bottom=603;align="left";color=3947580;db=1;disabled=1;dr=1;font=LOGFONT(h=-13);iconStyle={align="left";font=LOGFONT(h=-13;name='FontAwesome');padding={left=8}};iconText='\uF0C5';notify=1;textPadding={left=25};z=11};
btnSend={cls="plus";text="问 AI";left=660;top=572;right=732;bottom=602;align="left";color=3947580;db=1;dr=1;font=LOGFONT(h=-13);iconStyle={align="left";font=LOGFONT(h=-13;name='FontAwesome');padding={left=8}};iconText='\uF0AA';notify=1;textPadding={left=25};z=5};
btnSetting={cls="plus";text="设置";left=512;top=572;right=579;bottom=602;align="left";color=3947580;db=1;dr=1;font=LOGFONT(h=-13);iconStyle={align="left";font=LOGFONT(h=-13;name='FontAwesome');padding={left=8}};iconText='\uF013';notify=1;textPadding={left=25};z=7};
btnSnap={cls="plus";text="分享";left=138;top=573;right=202;bottom=603;align="left";color=3947580;db=1;disabled=1;dr=1;font=LOGFONT(h=-13);iconStyle={align="left";font=LOGFONT(h=-13;name='FontAwesome');padding={left=8}};iconText='\uF030';notify=1;textPadding={left=25};z=13};
chkFix={cls="plus";text="更正";left=276;top=572;right=336;bottom=603;align="left";db=1;dr=1;font=LOGFONT(h=-13);iconStyle={align="left";font=LOGFONT(h=-14;name='FontAwesome')};iconText='\uF0C8 ';notify=1;textPadding={left=24};z=12};
editMaxTokens={cls="edit";left=427;top=577;right=470;bottom=600;align="right";db=1;dr=1;edge=1;z=9};
editPrompt={cls="richedit";left=11;top=452;right=754;bottom=567;db=1;dl=1;dr=1;edge=1;hscroll=1;multiline=1;vscroll=1;z=2};
promptTool={cls="syslink";text="增强检索";left=20;top=577;right=87;bottom=600;center=1;db=1;dl=1;notify=1;z=4};
spinMaxTokens={cls="spin";left=471;top=578;right=491;bottom=600;db=1;dr=1;z=8};
splitter={cls="splitter";left=8;top=453;right=751;bottom=458;db=1;dl=1;dr=1;frame=1;horz=1;z=3};
static={cls="static";text="回复长度:";left=352;top=575;right=418;bottom=598;align="right";center=1;db=1;dr=1;transparent=1;z=10};
wndBrowser={cls="custom";text="自定义控件";left=8;top=5;right=751;bottom=447;ah=1;db=1;dl=1;dr=1;dt=1;z=1}
)
/*}}*/

import fsys.table;
var config = fsys.table(io.appData("aardio/ide/aiChat/~"))

//创建显示聊天消虑的 Web 浏览器窗口
import web.form.chat;
var wb = web.form.chat(winform.wndBrowser);
wb.enableKatex(config.katex);

//清除上下文
var resetMessages = function(){

    wb.clear(); 

    //输入系统提示词
    wb.aardioSystem( config.systemPrompt );

    wb.aiSystemPropmptSupperHotkeys = null;
    wb.aiSystemPropmptStringPatterns = null;
    wb.aiSystemPropmptWebView = null;
    wb.aiSystemPropmptPython = null; 
    wb.aiSystemPropmptWinform = null;
    wb.aiSystemPropmptNet = null;
    wb.aiSystemPropmptPlus = null;
    wb.aiSystemPropmptFile = null;

    import ide;
    var projPath = ide.getProjectPath();
    if(!#projPath) return;

    import fsys.file;
    var file = fsys.file(projPath,"r");
    if(!file) return;

    if(file.size() < 20000){
        file.close();
        return;
    }

    var xml = file.readAll();

    var project = '\r\n\r\n'+"
## aardio 源文件与工程文件

aardio 代码文件的后缀名为 `.aardio`,可包含 UTF-8 编码的源代码,也可以包含编译后的二进制代码。

aardio 工程文件的后缀名为 `.aproj`,其内容是 XML 格式的工程配置,也使用  UTF-8 编码 。  
"

    project = project + '\r\n\r\n用户当前在 aardio 开发环境中打开的工程文件路径是: "'+projPath+'"\r\n'

    project = project + '\r\n\r\n用户当前打开的工程文件内容如下:'

    project = project + '\r\n\r\n```xml\r\n' + xml +  '\r\n```\r\n\r\n'

    var desc = /*****

工程文件各 XML 节点的作用与含义:

- project 元素指定工程配置,并作为工程根目录包含其他 folder 或 file 元素。

project 元素的属性 ui 指定图形界面。 

    * 如果 ui 为 "win" 则为图形界面发布后运行默认不显示控制台。
    * 如果 ui 为"console" 则为控制台程序发布后运行时默认显示控制台窗口。

project 元素的属性 dstrip 指定是否移除调试符号。 

    * `dstrip="true"` 则发布后移除调试信息,生成的文件更小但错误信息会缺少调试信息(例如文件名行号)。

- folder 元素为工程中的虚拟目录

如果 folder  的属性 embed 为 "true" 则该目录发布后嵌入 EXE 资源文件,aardio 中很多函数和库都自动支持这种嵌入资源而不需要额外修改代码。例如对于 `string.load("/res/test.txt")`,无论参数指定的文件是不是 EXE 资源文件函数的返回值都是一样的,这是 aardio 的一个主要特性。

如果 folder 元素的属性 local 为 "true" 则表示这是一个本地目录(通常也是 Web  前端工程的发布目录),发布为 EXE 时将添加该目录下的所有文件。这种目录在工程中不显示子级文件或目录,右键菜单的『同步本地目录』也是无效的。 

如果 folder 元素的属性 ignored 为 "true" 是指这个目录在发布时被忽略(ignored)。这种目录通常用来指向包含 Web 前端工程源码的目录,工程本身其实并不需要这些多余的目录,生成 EXE 时也会忽略这种目录。

- file 元素则表示添加到工程中的文件

在工程根目录下只能有一个应用程序启动文件, 文件路径必须是 `main.aardio` 或以  `.main.aardio` 结束。除了启动文件,工程根目录只能包含 folder 元素。

*****/

project = project + desc + '\r\n\r\n';

    var codePath = ide.getActiveDocPath();
    if(#codePath && ..string.endWith(codePath,".aardio",true) ){
        project = project + '\r\n\r\n用户当前正在编辑的文件为: "'+codePath+'"\r\n'
    } 

    wb.system(project);
}

resetMessages();

winform.btnClear.oncommand = function(id,event){
    resetMessages();//清除聊天上下文
}

winform.splitter.origTop = winform.splitter.top;

//响应按键事件,输入用户提示词
winform.btnSend.oncommand = function(id,event){
    var prompt = winform.editPrompt.text;
    if(!#prompt){
        wb.errorMessage("请先输入问题。")
        winform.editPrompt.setFocus();
        return;
    }

    //按钮显示等待动画
    winform.btnSend.disabledText = {'\uF254';'\uF251';'\uF252';'\uF253';'\uF250'}
    winform.btnClear.disabled = true; 
    winform.btnSnap.disabled = true;
    winform.chkFix.disabled = true;

    wb.limit = config.msgLimit;

    var assistantMsg = wb.lastAssistantMessage();
    if(assistantMsg && winform.chkFix.checked){
        //Few-shot Learning
        assistantMsg.content = ide.aifix.markdown(assistantMsg.content,true);
    }

    var knowledge = ""
    prompt = string.replace(prompt,"(https?\://<www\.>?aardio\.com/zh\-cn/doc/\S+)\.<html>|<md>",
        function(url){

            url = url + ".md";
            wb.showLoading("正在读取:"+url)

            var md = inet.http.get(url);
            if(md){
                md = '\r\n\r\n用户输入的参考网址:' + url 
                    +  '\r\n\r\n下面是自该网址获取的 Markdown 格式文档或范例:'
                    +  '\r\n\r\n' + md +'\r\n\r\n------------------------\r\n\r\n'

                knowledge = knowledge + md;

            }
        });

    if(#knowledge){
        wb.system(knowledge)
    } 

    if( !wb.aiSystemPropmptSupperHotkeys && ..string.find(prompt,"<超级热键>|<@@imtip@>|<key\.hotkey>|<@@superHotkey@>")){

        if(!wb.findSystem("超级热键使用指南")){
            wb.system(..string.load("~/doc/library-guide/std/key/hotkey.md"))
            wb.aiSystemPropmptSupperHotkeys = true; 
        }
    }
    elseif( !wb.aiSystemPropmptWebView && ..string.find(prompt,"!\w<web.view>|<@@webview@>|<@@webview2@>|<@@react@>!\W")){

        if(!wb.findSystem("web.view 快速入门指南")){
            wb.system(..string.load("~/doc/library-guide/std/web/view/_.md"))
            wb.aiSystemPropmptWebView = true; 
        }
    } 
    elseif( !wb.aiSystemPropmptPython && ..string.find(prompt,"!\w<@@python@>|<py3>|<py2>!\W")){

        if(!wb.findSystem("aardio 调用 Python 入门指南")){
            var pyDoc = ..string.load("~/doc/library-guide/ext/python/_.md");
            pyDoc = ..string.concat( pyDoc,'\r\n\r\n---\r\n\r\n',..string.load("~/doc/library-guide/ext/python/conversion.md"));

            wb.system(..string.load(pyDoc))
            wb.aiSystemPropmptPython = true; 
        }
    } 
    elseif( !wb.aiSystemPropmptNet && ..string.find(prompt,"![\w\.]<\.<@@net@>>|<dotNet>|<System\.>|<[cC]#>![^#\.\a]")){

        if(!wb.findSystem("aardio 调用 .NET 入门指南")){
            var pyDoc = ..string.load("~/doc/library-guide/std/dotNet/_.md");
            pyDoc = ..string.concat( pyDoc,'\r\n\r\n---\r\n\r\n',..string.load("~/doc/library-guide/std/dotNet/type-conversion.md"));

            wb.system(..string.load(pyDoc))
            wb.aiSystemPropmptNet = true; 
        }
    } 
    elseif( !wb.aiSystemPropmptStringPatterns && ..string.find(prompt,"<模式匹配>|<string\.find>|<string\.match>|<string\.gmatch>|<string\.replace>|<string\.replaceUnmatched>")){

        if(!wb.findSystem("模式匹配与正则表达式的区别")){
            wb.system(..string.load("~/doc/library-guide/builtin/string/patterns.md"))
            wb.aiSystemPropmptStringPatterns = true; 
        }
    } 
    elseif( !wb.aiSystemPropmptPlus && ..string.find(prompt,"![\w\.]<plus\s*控件>")){

        if(!wb.findSystem("plus 控件使用指南")){
            wb.system(..string.load("~/doc/library-guide/std/win/ui/ctrl/plus.md"))
            wb.aiSystemPropmptPlus = true;
        }
    } 
    elseif( !wb.aiSystemPropmptWinform && ..string.find(prompt,"<win\.ui>|<winform>|<win\.form>|<按钮>|<窗口>|<文本框>|<编辑框>")){

        if(!wb.findSystem("如何创建窗口并添加控件")){
            wb.system(..string.load("~/doc/library-guide/std/win/ui/create-winform.md"))
            wb.aiSystemPropmptWinform = true; 
        }
    } 
    elseif( !wb.aiSystemPropmptFile && ..string.find(prompt,"<io\.file>|<读文件>|<写文件>")){

        if(!wb.findSystem("io 库文件操作")){
            wb.system(..string.load("~/doc/ibrary-guide/builtin/io/file.md"))
            wb.aiSystemPropmptFile = true; 
        }
    }

    //输入 AI 提示词
    wb.prompt( prompt );
    winform.editPrompt.text = "";

    config.maxTokens = winform.spinMaxTokens.pos;

    winform.splitter.splitAt(winform.splitter.origTop);

    //创建多线程向服务端发送请求
    thread.invoke( 
        function(wb,config){

            for(k,v in config){ 
                if(v=="")config[k] = null;
            } 

            /*
            下面的 key 24 小时后失效,
            DeepSeek 的 key 充 10 元估计能用上一年,所以请自己申请一个好吗?!
            */
            import string.escape2;
            var testKey = string.escape2('\0\48\67\91\29\5\83\2\3\4\5\0\83\8\4\85\5\4\0\83\83\9\5\9\4\1\85\3\86\6\82\5\4\83\86\2\0');

            config = table.mix(config,{

                key = testKey;
                url = "https://api.deepseek.com/v1";
                model = "deepseek-chat";
                temperature = 0.1;
            });

            if(config.maxTokens>1024 && (config.key === testKey ) ){
                wb.errorMessage(`回复长度超过 1024 时,必须更改为您自己的 API 密钥 。<a href="https://platform.deepseek.com">点这里获取密钥</a>,&nbsp;<a href="javascript:void(0)" onclick="javascript:external.updateApiKey()">点这里设置新密钥</a>。`);
                return;
            }

            //导入调用 HTTP 接口的 REST 客户端
            import web.rest.aiChat;
            var client = web.rest.aiChat(config);

            var ok,err = client.messages(wb.chatMessage,function(deltaText){
                wb.assistant(deltaText);
            } );

            if(err){
                //获取错误对象(解析 JSON 格式的错误信息)
                var errObject = client.lastResponseError()
                if(errObject[["error"]][["type"]] == "authentication_error" ){
                    wb.errorMessage(`API 密钥错误!<a href="https://platform.deepseek.com">点这里获取密钥</a>,&nbsp;<a href="javascript:void(0)" onclick="javascript:external.updateApiKey()">点这里设置新密钥</a>`)
                }
                else {
                    wb.errorMessage(err)
                }
            }  

        },wb,config//将参数传入线程
    )

    winform.btnCopy.disabled = false;
}

//在 AI 回复结束后回调此函数
wb.onWriteEnd = function(){
    winform.btnSend.disabledText = null;//关闭等待动画
    winform.btnClear.disabled = false;
    winform.btnCopy.disabled = false;
    winform.btnSnap.disabled = false;
    winform.chkFix.disabled = false;
    winform.editPrompt.setFocus();
}

//在 AI 回复结束以前回调此函数,自动修正 aardio 代码块中的常见幻觉错误
import ide.aifix;
wb.beforerWriteEnd = function(markdown){
    if(winform.chkFix.checked) return ide.aifix.markdown(markdown,true);
    return markdown;
}

//导出 aardio 函数到网页 JavaScript 中。
wb.external = {
    updateApiKey = function(){
        winform.btnSetting.oncommand();
    } 
}

import key;
import win.clip;
winform.btnCopy.oncommand = function(id,event){
    var md = wb.lastMarkdown();
    if(!#md) return winform.msgboxErr("消息为空。");

    if(key.getState("CTRL")){

        var found;
        for indent,_,code in string.gmatch(md,"!\N([ \t]*)(```+)<aardio>(.+?)!\N\s*\2![^`\S]") { 

            if(#indent){ 
                text = string.replace(text,"\n+"+indent,'\n');
            }   

            if(winform.chkFix.checked) code = ide.aifix(code,true);
            win.clip.write( code );
            found = true;
        }

        if(!found){
            return winform.msgboxErr("没有找到代码块。");   
        } 
    }
    else{
        if(winform.chkFix.checked) md = ide.aifix.markdown(md,true);
        win.clip.write( md )
    }

    winform.btnCopy.disabledText = {'\uF254';'\uF251';'\uF252';'\uF253';'\uF250';text=''} 
    thread.delay(800);
    winform.btnCopy.disabledText = null;
}

//设置接口地址与 API 令牌的窗口
winform.btnSetting.oncommand = function(id,event){
    var frmSetting = win.form(text="aardio - 设置 AI 聊天助手";right=685;bottom=589;border="dialog frame";exmode="none";max=false;min=false;mode="popup")
    frmSetting.add(
        btnAdd={cls="plus";left=18;top=473;right=52;bottom=503;align="left";color=3947580;font=LOGFONT(h=-13);iconStyle={align="left";font=LOGFONT(h=-13;name='FontAwesome');padding={left=8}};iconText='\uF067';notify=1;textPadding={left=25};z=11};
        btnEdit={cls="plus";left=93;top=473;right=127;bottom=503;align="left";color=3947580;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=473;right=90;bottom=503;align="left";color=3947580;font=LOGFONT(h=-13);iconStyle={align="left";font=LOGFONT(h=-13;name='FontAwesome');padding={left=8}};iconText='\uF1F8';notify=1;textPadding={left=25};z=12};
        btnSave={cls="button";text="更新配置";left=489;top=531;right=659;bottom=576;z=5};
        chkKatex={cls="checkbox";text="解析数学公式";left=55;top=526;right=159;bottom=546;z=16};
        editApiKey={cls="edit";left=284;top=104;right=662;bottom=131;edge=1;password=1;z=2};
        editApiUrl={cls="combobox";left=284;top=31;right=662;bottom=57;edge=1;items={};mode="dropdown";z=19};
        editModel={cls="edit";left=284;top=68;right=662;bottom=95;edge=1;z=6};
        editProxy={cls="edit";left=283;top=180;right=661;bottom=207;edge=1;z=20};
        editSystemPrompt={cls="edit";left=283;top=220;right=664;bottom=484;edge=1;hscroll=1;multiline=1;vscroll=1;z=22};
        groupbox={cls="groupbox";text="选择当前配置:";left=9;top=6;right=675;bottom=513;edge=1;z=1};
        lbMsgLimit={cls="static";left=423;top=546;right=451;bottom=566;transparent=1;z=17};
        lbTemperature={cls="static";left=633;top=140;right=659;bottom=160;transparent=1;z=18};
        lstConfig={cls="listbox";left=18;top=34;right=187;bottom=464;edge=1;hscroll=1;items={};vscroll=1;z=10};
        static={cls="static";text="模型 ID:";left=196;top=72;right=279;bottom=93;align="right";transparent=1;z=3};
        static2={cls="static";text="API key:";left=196;top=107;right=279;bottom=128;align="right";transparent=1;z=4};
        static3={cls="static";text="不会联网读取系统提示词内的超链接,建议直接添加文档内容";left=282;top=489;right=649;bottom=508;color=5921370;transparent=1;z=24};
        static4={cls="static";text="接口地址:";left=196;top=38;right=279;bottom=59;align="right";transparent=1;z=7};
        static5={cls="static";text="temperature:";left=196;top=141;right=279;bottom=162;align="right";transparent=1;z=9};
        static6={cls="static";text="上下文轮数:";left=33;top=560;right=128;bottom=592;align="right";transparent=1;z=15};
        static7={cls="static";text="代理服务器:";left=196;top=184;right=279;bottom=205;align="right";transparent=1;z=21};
        static8={cls="static";text="系统提示词:";left=196;top=219;right=279;bottom=240;align="right";transparent=1;z=23};
        tbMsgLimit={cls="trackbar";left=132;top=552;right=415;bottom=582;max=100;min=0;z=14};
        tbTemperature={cls="trackbar";left=283;top=137;right=633;bottom=167;max=100;min=0;z=8}
    )

    frmSetting.editModel.setCueBannerText("模型名前加 @ 使用 Anthropic 接口,否则使用 OpenAI 接口")
    frmSetting.editProxy.setCueBannerText("socks=127.0.0.1:1081");
    frmSetting.editSystemPrompt.limit = -1;

    frmSetting.editApiUrl.items = {
        "https://api.deepseek.com/v1",
        "https://api.anthropic.com/v1",
        "https://generativelanguage.googleapis.com/v1beta",
        "https://dashscope.aliyuncs.com/compatible-mode/v1",
        "https://ark.cn-beijing.volces.com/api/v3/bots",
        "https://api.x.ai/v1",
        "https://api.openai.com/v1"
    }

    frmSetting.editApiUrl.onListChange = function(){ 
        var url = frmSetting.editApiUrl.selText;
        if(url=="https://api.deepseek.com/v1"){
            frmSetting.editModel.text = "deepseek-chat"
        }
        elseif(url=="https://api.anthropic.com/v1"){
            frmSetting.editModel.text = "@claude-3-5-sonnet-latest"
        }
        elseif(url=="https://generativelanguage.googleapis.com/v1beta"){
            frmSetting.editModel.text = "gemini-exp-1206"
        }
        elseif(url=="https://api.x.ai/v1"){
            frmSetting.editModel.text = "grok-2-1212"
        }
        elseif(url=="https://api.openai.com/v1"){
            frmSetting.editModel.text = "chatgpt-4o-latest"
        }
        elseif(url=="https://dashscope.aliyuncs.com/compatible-mode/v1"){
            frmSetting.editModel.text = "qwen-coder-plus-latest"
        }
        elseif(url=="https://ark.cn-beijing.volces.com/api/v3/bots"){
            frmSetting.editModel.text = "bot-"
        }
    }

    frmSetting.tbMsgLimit.setRange(3,100);
    frmSetting.tbTemperature.setRange(0,10);
    frmSetting.tbTemperature.oncommand = function(id,event,pos){

        var pos = frmSetting.tbTemperature.pos; 
        frmSetting.tbTemperature.tooltip = pos / 10; 
        frmSetting.lbTemperature.text = pos / 10; 
    }

    frmSetting.tbMsgLimit.oncommand = function(id,event,pos){

        frmSetting.lbMsgLimit.text = frmSetting.tbMsgLimit.pos;;  
    }

    import win.ui.listEdit;
    var listEdit = win.ui.listEdit(frmSetting.lstConfig);
    listEdit.editBox.setCueBannerText("请输入配置名",true);

    if(!#config.itemNames) {
        config.itemNames = {"默认"}
        config.itemData = {{
            url = config.url || "https://api.deepseek.com/v1";
            key = config.key;
            model = #config.model ? config.model : "deepseek-chat";
            temperature = config.temperature;       
        }};
    } 

    frmSetting.lstConfig.onSelChange = function(){
        var selIndex = frmSetting.lstConfig.selIndex; 

        if(config.selItem && config.selItem != selIndex){
            //保存上一个配置
            var configItem = {
                url = frmSetting.editApiUrl.text;
                key = frmSetting.editApiKey.text;
                model = frmSetting.editModel.text;
                temperature = frmSetting.tbTemperature.pos / 10;
                msgLimit = frmSetting.tbMsgLimit.pos;
                proxy = string.trim(frmSetting.editProxy.text);
                systemPrompt = frmSetting.editSystemPrompt.text;
            }  

            config.itemData[config.selItem] = configItem;       
        } 

        //加载下一个配置
        var selIndex = frmSetting.lstConfig.selIndex; 
        var configItem = config.itemData[selIndex] || {};

        frmSetting.editApiUrl.text = configItem.url;
        frmSetting.editApiKey.text = configItem.key;
        frmSetting.editProxy.text = configItem.proxy;
        frmSetting.editSystemPrompt.text = configItem.systemPrompt;

        if(configItem.temperature===null) configItem.temperature = 0.1; 
        frmSetting.tbTemperature.pos = configItem.temperature * 10;
        frmSetting.tbTemperature.tooltip = configItem.temperature; 
        frmSetting.lbTemperature.text = configItem.temperature; 

        frmSetting.tbMsgLimit.pos = configItem.msgLimit || 15;
        frmSetting.lbMsgLimit.text = configItem.msgLimit || 15; 

        frmSetting.editModel.text = configItem.model; 

        config.proxy = null;
        table.assign(config,configItem);

        config.itemData[selIndex] = configItem;
        config.itemNames = frmSetting.lstConfig.items; 
        config.itemData = table.slice(config.itemData,1,#config.itemNames);
        config.selItem = selIndex; 

        config.save();  
    }

    frmSetting.lstConfig.items = config.itemNames;
    frmSetting.lstConfig.selIndex = config.selItem || 1;
    frmSetting.lstConfig.onSelChange();
    frmSetting.chkKatex.checked = config.katex;

    //保存并更新配置
    import inet.url;
    frmSetting.btnSave.oncommand = function(id,event){

        var configItem = {
            url = frmSetting.editApiUrl.text;
            key = frmSetting.editApiKey.text;
            model = frmSetting.editModel.text;
            temperature = frmSetting.tbTemperature.pos / 10;
            msgLimit = frmSetting.tbMsgLimit.pos;
            proxy = string.trim(frmSetting.editProxy.text);
            systemPrompt = frmSetting.editSystemPrompt.text;
        }

        if(!#configItem.proxy){
            configItem.proxy = null;
        }

        var tUrl = inet.url.split(configItem.url); 
        if(tUrl[["host"]]=="api.anthropic.com"){
            if(configItem.model[1]!='@'#){
                configItem.model = '@' + configItem.model;
            }
        }
        elseif(tUrl[["host"]]=="generativelanguage.googleapis.com"){
            configItem.url = "https://generativelanguage.googleapis.com/v1beta"
        }

        var selIndex = frmSetting.lstConfig.selIndex;
        config.selItem = selIndex; 
        config.itemData[selIndex] = configItem;

        config.proxy = null;
        table.assign(config,configItem);  

        config.katex = frmSetting.chkKatex.checked;
        wb.enableKatex(config.katex);


        config.save();

        frmSetting.endModal();

        if(!wb.started()){
            resetMessages();
        }
        else {

            var md = wb.getMarkdown();
            if(winform.chkFix.checked){
                md = ide.aifix.markdown(md,true)
            }

            wb.write(md);
        }


        thread.delay(100)
        winform.editPrompt.setFocus();
    }

    frmSetting.btnEdit.oncommand = function(id,event){
        listEdit.beginEdit();
    }

    frmSetting.btnAdd.oncommand = function(id,event){
        listEdit.beginEdit(0);  
    }

    frmSetting.btnRemove.oncommand = function(id,event){
        if(frmSetting.lstConfig.count==1){
            return frmSetting.msgboxErr("只有一个配置方案时不允许删除!");
        }

        var selIndex = frmSetting.lstConfig.selIndex;

        ..table.remove(config.itemData,selIndex);
        ..table.remove(config.itemNames,selIndex);
        frmSetting.lstConfig.delete(selIndex)

        selIndex = selIndex<=frmSetting.lstConfig.count ? selIndex : selIndex -1;

        config.selItem = null;
        frmSetting.lstConfig.selIndex = selIndex;
        frmSetting.lstConfig.onSelChange() 
    }   

    listEdit.onEditChanged = function(newText,selIndex){
        config.itemNames = frmSetting.lstConfig.items; 

        frmSetting.lstConfig.selIndex = selIndex;
        frmSetting.lstConfig.onSelChange();

        config.save();  
    }

    frmSetting.btnAdd.skin({
        color={
            active=0xFF00FF00;
            default=0xFF3C3C3C;
            disabled=0xFF6D6D6D;
            hover=0xFFFF0000        
        }
    })

    frmSetting.btnRemove.skin({
        color={
            active=0xFF00FF00;
            default=0xFF3C3C3C;
            disabled=0xFF6D6D6D;
            hover=0xFFFF0000        
        }
    })

    frmSetting.btnEdit.skin({
        color={
            active=0xFF00FF00;
            default=0xFF3C3C3C;
            disabled=0xFF6D6D6D;
            hover=0xFFFF0000        
        }
    })

    if(wb.documentMode<11){
        frmSetting.chkKatex.checked = false;
        frmSetting.chkKatex.disabled = true;
    }

    frmSetting.doModal(winform);
}

winform.chkFix.checked = true;
winform.chkFix.oncommand = function(id,event){
    var md = wb.getMarkdown();
    if(owner.checked){
        md = ide.aifix.markdown(md,true)
    }

    wb.write(md);
}

var tip = /*
- 在代码编辑器中按 `F1` 键可调用`编码助手`帮您续写或补全代码。
    * 建议在输入光标前用`行注释`说明需求,再按 `F1` 键。
    * 如果按 F1 前已选中代码则会打开文档或 AI 搜索,AI 不会替换选区代码。 
    * 默认密钥仅提供有限功能,建议<a href="https://platform.deepseek.com">点这里获取新密钥</a>,&nbsp;<a href="javascript:void(0)" onclick="javascript:external.updateApiKey()">点这里设置新密钥</a>以取消限制。
- 聊天界面支持联网自动读取 aardio 文档。
- 按住 `Ctrl`键点下面的 `复制` 按钮可复制 AI 最后一次输出的代码块。
- 按住 `Ctrl`键点下面的 `分享` 可截长屏到剪贴板。 
- 请注意查看下面提示词输入框的右键菜单。
- [VIP 专属技术支持服务](https://aardio.com/vip),限时 5 折!
*/

wb.write(tip)

//默认设置输入框焦点
winform.editPrompt.setFocus();

winform.splitter.ltMin = 200;
winform.splitter.rbMin = 150;

var scrollbarHeight = ::User32.GetSystemMetrics(3/*_SM_CYHSCROLL*/)
winform.editPrompt.onOk = function(ctrl,alt,shift){ 
    if(ctrl){
        winform.btnSend.oncommand();
        return true; 
    } 

    var pt = ::POINT()
    ::User32.GetCaretPos(pt) 

    var lineCount = winform.editPrompt.lineCount;
    var lineHeight = math.ceil(pt.x / lineCount + winform.dpiScale(5)); 

    if(pt.y+(lineHeight+scrollbarHeight)*3>winform.editPrompt.height){  

        winform.wndBrowser.setRedraw(false)
        winform.splitter.splitMove(-lineHeight) 
        winform.wndBrowser.setRedraw(true) 
    }
}

//打开 AI 搜索
winform.promptTool.link = "https://www.aardio.com/zh-cn/doc/?q=/zh-cn/ai/prompt/"
winform.promptTool.onHyperlinkClick = function(nmSysLink,url,id,index){
    var q = winform.editPrompt.text;
    if(#q){
        import inet.url;
        raw.execute("https://www.aardio.com/zh-cn/ai/prompt/?q="+inet.url.encode(q));
    }
    else {
        raw.execute(url);
    } 
}

//拆分界面
winform.splitter.split(winform.wndBrowser,winform.editPrompt);

winform.editPrompt.enablePopMenu(function(){
    return { 

        { '问 AI(发送)\tCtrl+Enter';  function(id){
            winform.btnSend.oncommand();
        }; 0};  

        { /*分隔线*/ };
        { (wb.aiSystemPropmptStringPatterns?"已自动":"")+"插入 模式匹配语法文档";  function(id){
            winform.editPrompt.selText = " [aardio 模式匹配语法](https://www.aardio.com/zh-cn/doc/library-guide/builtin/string/patterns.md) "
        }; wb.aiSystemPropmptStringPatterns?1/*_MF_GRAYED*/: 0};
        { (wb.aiSystemPropmptWebView?"已自动":"")+"插入 web.view 指南(网页相关)";  function(id){
            winform.editPrompt.selText = " [web.view 使用指南](https://www.aardio.com/zh-cn/doc/library-guide/std/web/view/_.md) "
        }; wb.aiSystemPropmptWebView?1/*_MF_GRAYED*/: 0};
        { "插入 web.rest 指南(HTTP 相关)";  function(id){
            winform.editPrompt.selText = " [web.rest 使用指南](https://www.aardio.com/zh-cn/doc/library-guide/std/web/rest/client.md) "
        }; 0};
        { "插入 多线程入门";  function(id){
            winform.editPrompt.selText = " [多线程开发入门](https://www.aardio.com/zh-cn/doc/guide/language/thread.md) "
        }; 0};  
        { "插入 高级选项卡指南(多窗口)";  function(id){
            winform.editPrompt.selText = " [高级选项卡指南](https://www.aardio.com/zh-cn/doc/library-guide/std/win/ui/tabs/_.md) "
        }; 0};  
        { (wb.aiSystemPropmptPlus?"已自动":"")+"插入 plus 控件指南(界面美化)";  function(id){
            winform.editPrompt.selText = " [plus 控件使用指南](https://www.aardio.com/zh-cn/doc/library-guide/std/win/ui/ctrl/plus.md) "
        }; wb.aiSystemPropmptPlus?1/*_MF_GRAYED*/: 0}; 
        { "插入 自定义控件指南";  function(id){
            winform.editPrompt.selText = " [自定义控件使用指南](https://www.aardio.com/zh-cn/doc/library-guide/std/win/ui/ctrl/custom.md) "
        }; 0};  
        { (wb.aiSystemPropmptPython?"已自动":"")+"插入 调用 Python 文档";  function(id){
            winform.editPrompt.selText = " [aardio 调用 Python 入门指南](https://www.aardio.com/zh-cn/doc/library-guide/ext/python/_.md) "
        }; wb.aiSystemPropmptPython?1/*_MF_GRAYED*/: 0};
        { (wb.aiSystemPropmptNet?"已自动":"")+"插入 调用 NET 文档";  function(id){
            winform.editPrompt.selText = " [aardio 调用 .NET 入门指南](https://www.aardio.com/zh-cn/doc/library-guide/std/dotNet/_.md) "
        }; wb.aiSystemPropmptNet?1/*_MF_GRAYED*/: 0};
        { (wb.aiSystemPropmptSupperHotkeys?"已自动":"")+"插入 超级热键文档";  function(id){
            winform.editPrompt.selText = " [超级热键使用指南](https://www.aardio.com/zh-cn/doc/library-guide/std/key/hotkey.md) "
        }; wb.aiSystemPropmptSupperHotkeys?1/*_MF_GRAYED*/: 0};  
        { "插入原生接口文档(调用 DLL)";  function(id){
            winform.editPrompt.selText = " [原生类型](https://www.aardio.com/zh-cn/doc/library-guide/builtin/raw/datatype.md)  [结构体](https://www.aardio.com/zh-cn/doc/library-guide/builtin/raw/struct.md)  [声明原生 API](https://www.aardio.com/zh-cn/doc/library-guide/builtin/raw/api.md)  [原生回调函数](https://www.aardio.com/zh-cn/doc/library-guide/builtin/raw/callback.md) "
        }; 0}; 
        { "检索相关 aardio 文档";  function(id){
            var q = winform.editPrompt.selText;
            if(#q){
                import inet.url;
                raw.execute("https://www.aardio.com/zh-cn/ai/prompt/?q="+inet.url.encode(q));
            }
        }; !owner.canCopy() ? 1/*_MF_GRAYED*/ : 0}; 

        { "AI 搜索";  function(id){
            var q = winform.editPrompt.selText;
            if(#q){
                import inet.url;

                var url = inet.url.appendExtraInfo("http://api.aardio.com/search",{
                    q = '```aardio \n' + q + '\n```';
                })
                raw.execute(url);
            }
        }; !owner.canCopy() ? 1/*_MF_GRAYED*/ : 0};     
    }
})

global.onError = function( err,over ){ 
    if(!over){
        import debug;
        var stack = debug.traceback(,"调用栈",3);
        err = string.concat(err,stack);
    }

    if( _STUDIO_INVOKED ) {
        import win;
        win.msgboxErr(err);
    }
}

winform.spinMaxTokens.buddy = winform.editMaxTokens;
winform.spinMaxTokens.setRange(1,1024*8);
winform.spinMaxTokens.pos = config.maxTokens || 1024;
winform.spinMaxTokens.inc = 1024;

winform.beforeDestroy = function(){
    config.maxTokens = winform.spinMaxTokens.pos;
}


winform.btnSnap.oncommand = function(id,event){
    import fsys.dlg;
    import web.form.snap; 

    if(key.getState("CTRL")){
        winform.btnSnap.disabled = true;

        web.form.snap(wb,function(bmp){
                var hbmp = bmp.copyHandle();
                win.clip.writeBitmap(hbmp,true);
                return true;
        } );  
    }
    else{
        var path = fsys.dlg.save("*.jpg|*.jpg","AI 聊天助手 - 保存对话截图",,winform);
        if(!path) return;

        winform.btnSnap.disabled = true;

        web.form.snap(wb,path); 
        winform.editPrompt.setFocus();
    }

    wb.doScript(`document.documentElement.scrollTop = document.documentElement.scrollHeight + 50;`);

    thread.delay(1000);
    winform.btnSnap.disabled = false;
}

//按钮外观样式
winform.btnClear.skin({
    color={
        active=0xFF00FF00;
        default=0xFF3C3C3C;
        disabled=0xFF999999;
        hover=0xFFFF0000        
    }
})

//按钮外观样式
winform.btnSend.skin({
    color={
        active=0xFF00FF00;
        default=0xFF3C3C3C;
        disabled=0xFF999999;
        hover=0xFFFF0000        
    }
})

//按钮外观样式
winform.btnSetting.skin({
    color={
        active=0xFF00FF00;
        default=0xFF3C3C3C;
        disabled=0xFF999999;
        hover=0xFFFF0000        
    }
})

winform.btnCopy.skin({
    color={
        active=0xFF00FF00;
        default=0xFF3C3C3C;
        disabled=0xFF999999;
        hover=0xFFFF0000        
    }
})

winform.btnSnap.skin({
    color={
        active=0xFF00FF00;
        default=0xFF3C3C3C;
        disabled=0xFF999999;
        hover=0xFFFF0000        
    }
})

winform.chkFix.skin({
    color={
        active=0xFF00FF00;
        default=0xFF000000;
        disabled=0xFF999999;
        hover=0xFFFF0000        
    };
    checked={
        iconText='\uF14A';  
        color={
            active=0xFF00FF00;
            default=0xFF000000;
            disabled=0xFF999999;
            hover=0xFFFF0000        
        };  
    }
})

winform.show();

win.loopMessage();
Markdown 格式