aardio 文档

aardio 范例: 全自主智能体

import autos;
if(_WIN7_LATER) ::Shell32.SetCurrentProcessExplicitAppUserModelID(autos.appUserModelId) //在任务栏显示为分离图标(避免合并到 aardio.exe 分组)

import fonts.fontAwesome;
import com.interface.IRichEditOleCallback;
import win.clip;
import process.admin;
import win.inputBox;
import win.ui;
/*DSG{{*/
mainForm = win.form(text=autos.title;right=900;bottom=600;bgcolor=0xFFFFFF)
mainForm.add(
bkPrompt={cls="bkplus";left=222;top=547;right=788;bottom=594;anchors={left=1;right=1;bottom=1};border={color=0xC0C0C0;radius=16;width=1};z=1};
btnClear={cls="plus";left=134;top=556;right=169;bottom=585;align="left";anchors={left=1;bottom=1};bgcolor=0xFFFFFF;color=0x3C3C3C;font=LOGFONT(h=-13);iconStyle={font=LOGFONT(h=-22;name='FontAwesome');padding={left=8}};iconText='\uF014';notify=1;textPadding={left=25};z=6};
btnCopy={cls="plus";left=176;top=556;right=211;bottom=585;align="left";anchors={left=1;bottom=1};bgcolor=0xFFFFFF;color=0x3C3C3C;font=LOGFONT(h=-13);iconStyle={font=LOGFONT(h=-22;name='FontAwesome');padding={left=8}};iconText='\uF0C5';notify=1;textPadding={left=25};z=7};
btnFeishu={cls="plus";left=49;top=556;right=84;bottom=586;align="left";anchors={left=1;bottom=1};color=0x3C3C3C;font=LOGFONT(h=-13);iconStyle={font=LOGFONT(h=-22;name='FontAwesome')};iconText='\uF1D8';notify=1;textPadding={left=25};z=9};
btnImageAttachment={cls="plus";left=752;top=556;right=784;bottom=585;align="left";anchors={right=1;bottom=1};bgcolor=0xFFFFFF;border={radius=4};color=0x3C3C3C;font=LOGFONT(h=-13);foreRepeat="scale";iconStyle={font=LOGFONT(h=-19;name='FontAwesome');padding={top=2}};iconText='\uF030';notify=1;paddingBottom=2;paddingLeft=2;paddingRight=2;paddingTop=2;textPadding={left=25};z=11};
btnSend={cls="plus";text="运行";left=794;top=550;right=889;bottom=590;anchors={right=1;bottom=1};bgcolor=0xD77800;border={radius=4};color=0xFFFFFF;font=LOGFONT(h=-14);iconStyle={align="left";font=LOGFONT(h=-13;name='FontAwesome');padding={left=15;top=1}};iconText='\uF1B9';notify=1;textPadding={left=15};z=4};
btnSetting={cls="plus";left=6;top=556;right=41;bottom=586;align="left";anchors={left=1;bottom=1};color=0x3C3C3C;font=LOGFONT(h=-13);iconStyle={font=LOGFONT(h=-22;name='FontAwesome')};iconText='\uF013';notify=1;textPadding={left=25};z=5};
btnWeixin={cls="plus";left=91;top=556;right=126;bottom=586;align="left";anchors={left=1;bottom=1};color=0x3C3C3C;font=LOGFONT(h=-13);iconStyle={font=LOGFONT(h=-22;name='FontAwesome')};iconText='\uF1D7';notify=1;textPadding={left=25};z=8};
customChat={cls="custom";text="聊天区域";left=0;top=0;right=900;bottom=540;anchors={left=1;top=1;right=1;bottom=1};z=2};
editPrompt={cls="richedit";left=228;top=554;right=748;bottom=589;anchors={left=1;right=1;bottom=1};autohscroll=false;multiline=1;z=3};
iconVoice={cls="plus";left=-12;top=11;right=30;bottom=50;align="left";anchors={left=1;top=1};color=0x3C3C3C;font=LOGFONT(h=-13);hide=1;iconColor=0x60C107;iconStyle={align="left";font=LOGFONT(h=-30;name='FontAwesome')};iconText='\uF026';notify=1;textPadding={left=25};valign="top";z=12};
lableMessage={cls="plus";left=147;top=496;right=785;bottom=537;anchors={left=1;right=1;bottom=1};bgcolor=0x3AC267;border={radius=12};color=0xFFFFFF;font=LOGFONT(h=-14);hide=1;z=10}
)
/*}}*/

mainForm.btnSend.skin({
    background={
        default=0xFF0078D7; 
        hover=0xFF0088EE;   
        active=0xFF005BB5;  
        disabled=0xFFCCCCCC;
    };
    color={
        default=0xFFFFFFFF;
        disabled=0xFF888888;
    };
    checked={
        background={
            default=0xFFD13438; 
            hover=0xFFE35458; 
            active=0xFFA82A2D;
            disabled=0xFFCCCCCC;
        };
        color={
            default=0xFFFFFFFF;
            disabled=0xFF888888;
        }
    }
})

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

mainForm.btnFeishu.skin({
    color={
        active=0xFF00FF00;
        default=0xFF3C3C3C;
        disabled=0xFF6D6D6D;
        hover=0xFFFF0000        
    }
    checked = {
        color={
            active=0xFF00FF00;
            default=0xFF3370FF;
            disabled=0xFF6D6D6D;
            hover=0xFFFF0000        
        }   
    }
})

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

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

mainForm.btnWeixin.skin({
    color={
        active=0xFF00FF00;
        default=0xFF3C3C3C;
        disabled=0xFF6D6D6D;
        hover=0xFFFF0000        
    }
    checked = {
        color={
            active=0xFF00FF00;
            default=0xFF07C160;
            disabled=0xFF6D6D6D;
            hover=0xFFFF0000        
        }   
    }
})

mainForm.btnImageAttachment.skin({
    background={
        active=0xFFCCCCCC;
        default=0x00000000;
        hover=0xFFE6E6E6;
    };
    color={
        active=0xEE333333;
        default=0xFF3C3C3C;
        disabled=0xFF999999;
        hover=0xFFFF0000        
    }
})

mainForm.editPrompt.adjustInputLayout = function(focus){
    if( win.isIconic(mainForm.hwnd) ) return;

    var w,h = mainForm.dpiScale(1,1);
    h = h * 50;

    focus = focus && !mainForm.btnSend.busy();

    if(focus){
        if( ! mainForm.editPrompt["(focusedHeight)"] ){
            mainForm.bkPrompt.top = mainForm.bkPrompt.top - h;
            mainForm.bkPrompt.redraw();
            mainForm.customChat.bottom = mainForm.customChat.bottom - h;
            mainForm.editPrompt.top = mainForm.editPrompt.top - h;      
            mainForm.editPrompt["(focusedHeight)"] = true;
        }
    }   
    else{
        if( mainForm.editPrompt["(focusedHeight)"] ){
            mainForm.bkPrompt.top = mainForm.bkPrompt.top + h;
            mainForm.bkPrompt.redraw();
            mainForm.customChat.bottom = mainForm.customChat.bottom + h;
            mainForm.editPrompt.top = mainForm.editPrompt.top + h;  
            mainForm.editPrompt["(focusedHeight)"] = false; 
        }
    }
}

mainForm.setIcon("~/examples/AI/autos.ico"); 
mainForm.show();

//创建 AI 聊天界面
import web.form.chat;
var chatUi = web.form.chat(mainForm.customChat);


//必须放在 mainForm.show() 后面(DPI 初始化完成),保证普屏/高分屏都至少调用一次
mainForm.afterDpiChanged = function(){ 
    mainForm.setIcon("~/examples/AI/autos.ico"); 
    chatUi.zoom(100);
}

import fsys.table;
var userDataPath = io.appData("aardio/autos");
var agentConfigPath = io.appData("aardio/autos/agent.table");
var agentWorkspace = io.createDir( io.joinpath(userDataPath,"workspace") ); 

import process.mutex;
var sessionId = "aardio.autos.session." + mainForm.hwnd;
var sessionMutex;

//会话推进时持续保存的检查点,用于故障恢复
var sessionCheckpointPath = io.appData("aardio/autos/~checkpoint/"+ mainForm.hwnd +".session");

var config = fsys.table(agentConfigPath,{
    itemNames = ["默认"];
    selItem = 1;
    itemData = [{
        url="https://api.deepseek.com/v1";
        key= '';
        model="deepseek-v4-pro";
        temperature=1;
        reasoning={"effort":"max 全力以赴"}
    }] 
});
var aiChatConfig = config.itemData[config.selItem];
config.chatMessages = null;//升级

//记录当前会话状态的主记忆,仅随「新建会话」变更
thread.set("autos.sessionActiveMemoryFile",config.memoryFile || "main.md");//与工具、记忆管理界面共享设置

mainForm.clearSessionData = function(){

    io.remove(sessionCheckpointPath);
    if(sessionMutex){
        sessionMutex.close()
        sessionMutex = null;
    }   
}

mainForm.saveSessionData = function(chainOfThought){ 

    if( chainOfThought || (chatUi.started() && chatUi.chatMessages) ){

        if(!sessionMutex){
            sessionMutex = process.mutex(sessionId);
        }

        //正在使用的主记忆
        var aiActiveMemoryFile = thread.get("autos.sessionActiveMemoryFile");
        var cotMessages = ..thread.get("resumeChainOfThought");

        if(chainOfThought || cotMessages ){
            string.save(sessionCheckpointPath,table.tostring({
                chainOfThought = true;
                protocol = ..thread.get("resumeChainOfThoughtProtocol");
                messages = cotMessages;
                memoryFile = aiActiveMemoryFile;
                sessionId = sessionId;
                ownerHwnd = mainForm.hwnd;
            },true)) 
        }
        else{

            string.save(sessionCheckpointPath,table.tostring({
                messages = chatUi.chatMessages;
                memoryFile = aiActiveMemoryFile;
                sessionId = sessionId;
                ownerHwnd = mainForm.hwnd;
            },true)) 
        }
    }
    else{
        mainForm.clearSessionData();
    }
}

import thread.event;
// 手动复位事件:set 后 wait(0) 会持续返回 true,直到下一轮请求调用 reset()。用于表示“用户已请求停止”的持续状态。
var eventStop = thread.event(,true);

import com.interface.ITaskbarList3;
var taskbar = _WIN7_LATER && com.interface.ITaskbarList3.Create();

var sendApiMessage = function(resentCot,prompt){

    //如果恢复标志与共享链不同步,按普通重试处理,不弹出恢复提示。
    if(resentCot && !..thread.get("resumeChainOfThought")){
        //恢复链在竞态中消失时安全降级为普通重试,不伪造“继续”请求。
        resentCot = false;
        mainForm.btnSend.text = "重试";
    }


    if(!aiChatConfig[["url"]] || string.find(aiChatConfig[["url"]],"aardio\.com") ){
        mainForm.btnSend.checked = false;

        mainForm.msgboxErr('请在设置中指定支持交错思考+工具调用的大模型。\n禁止在 AA 中使用 ai.aardio.com 接口!');
        mainForm.btnSetting.oncommand();
        return;
    }

    if(!resentCot){
        var clearPrompt;
        if(!prompt) {
            prompt = mainForm.editPrompt.text;
            clearPrompt = true;
        }

        if(mainForm.btnSend.text!=="重试"){
            // 检查输入是否为空(移除首尾空白)
            if(!#string.trim(prompt)){
                mainForm.btnSend.checked = false;

                mainForm.msgboxErr("请输入提示词");
                mainForm.editPrompt.setFocus();
                return;
            }   
        }
        else{
            chatUi.clearErrorMessage();

            //没有临时交错思维链时,空 assistant 只是上次中断留下的占位,重试必须重发原请求。
            var lastMessage = chatUi.chatMessages[#chatUi.chatMessages];
            if(lastMessage[["role"]]==="assistant" && !#lastMessage[["content"]]
                && !lastMessage[["reasoning_content"]] && !#lastMessage[["tool_calls"]]){
                table.pop(chatUi.chatMessages);
            }
        }

        // UI 状态变更
        if(clearPrompt)mainForm.editPrompt.text = ""; // 清空输入  

        ..thread.set("resumeChainOfThought",null);
        ..thread.set("resumeChainOfThoughtProtocol",null);
        ..thread.set("resumeInterruptedMessage",null);
    }
    elseif(!..thread.get("resumeChainOfThought")){
        //共享恢复链在竞态中消失时,安全降级为普通重试,不伪造“继续”链也不弹出误导性提示。
        mainForm.btnSend.text = "重试";
        return sendApiMessage(false,prompt);
    }

    mainForm.btnClear.disabled = true; 

    eventStop.reset(); 
    var loading = {'\uF254';'\uF251';'\uF252';'\uF253';'\uF250' }

    //显示动画,但启用按钮
    mainForm.btnSend.reduce(loading,function(value,index){
        if(value) mainForm.btnSend.iconText = value;
        return 150;
    } ) 

    mainForm.btnSend.checked = true; 
    mainForm.btnSend.text = "停止";

    var shouldInjectImageContext;
    if(!resentCot){
        chatUi.showLoading(" 正在思考...");
        if(#prompt) {
            chatUi.prompt(prompt);
            mainForm.editPrompt.setFocus();

            //只在“新图加入”时通知大模型一次,避免在后续多轮对话中产生冗余提示
            shouldInjectImageContext = mainForm.btnImageAttachment.newImageAttached && type(mainForm.btnImageAttachment.foreground)==="table";
            mainForm.btnImageAttachment.newImageAttached = null;
        }
    }

    mainForm.editPrompt.adjustInputLayout(false);
    mainForm.btnSend.setFocus();

    mainForm.saveSessionData();

    if(taskbar) taskbar.SetProgressState(mainForm.hwnd,1/*_TBPF_INDETERMINATE*/);

    // 创建 AI 对话线程
    chatUi.aiChatThread = thread.create( 
        function(chatUi,mainForm,aiChatConfig,aiConfigItems,eventStop,fromWeiXinClawBotMessage,fromFeishuMessageId,feishuClientInstance,resentCot,shouldInjectImageContext){

            var fib = ..fiber.create(function(){

                import web.rest.aiChat;
                import autos.tools.schemas;
                import autos.tools.handlers;
                import autos;

                aiChatConfig.tools = autos.tools.schemas;
                aiChatConfig.userAgent = "autos/" + autos.version +"(Windows NT "+ _WIN_VER_MAJOR +"."+_WIN_VER_MINOR+"; +https://aau.cn)";

                // 创建 AI API 客户端
                var aiClient = web.rest.aiChat(aiChatConfig);
                aiClient.setTimeouts(15000,30000,60000);

                if(inet.url.split(aiChatConfig.url)[["host"]]=="openrouter.ai"){
                    aiClient.extraParameters = ..table.mix( aiClient.extraParameters,{
                        session_id = "aardio-aautos-session-" + autos.version;
                    })  
                }

                aiClient.referer = "https://aau.cn";
                aiClient.setHeaders({
                    "X-Title":"aardio autos",
                });

                import win.inputBox;
                ..subscribe("afterConsoleOpen",function(){
                    if(console){
                        console.setTitle("autos console - 请勿关闭本窗口!!")

                        console.setCtrlHandlerThread( function(ctrlType){
                            mainForm.saveSessionData(); //用户直接关闭 AI 打开的 console 窗口时保存对话
                        },mainForm);            
                    }
                } );

                aiClient.external = autos.tools.handlers;

                autos.tools.handlers.__init__(null,{
                    aiChatConfig = aiChatConfig;
                    aiConfigItems = aiConfigItems;
                    chatUi = chatUi;
                    fromWeiXinClawBotMessage = fromWeiXinClawBotMessage;
                    fromFeishuMessageId = fromFeishuMessageId;
                    feishuClientInstance = feishuClientInstance;
                    mainForm = mainForm;
                });

                var resumeChainOfThoughtProtocol;
                var chatMessages = chatUi.chatMessages;
                var hasRecoveryChain = resentCot;
                var interruptedResponseContent = "";
                var hasUncommittedResponse = false;
                if(resentCot){
                    chatMessages = ..thread.get("resumeChainOfThought");
                    resumeChainOfThoughtProtocol = ..thread.get("resumeChainOfThoughtProtocol");

                    if( resumeChainOfThoughtProtocol){
                        if(resumeChainOfThoughtProtocol!=aiClient._protocol){
                            chatUi.onComplete(false); 
                            chatUi.errorMessage("恢复思维链时不能将原接口使用的协议类型 " + resumeChainOfThoughtProtocol + " 更改为 " + aiClient._protocol);

                            mainForm.btnSend.text = "继续";
                            mainForm.saveSessionData(true);
                            return;
                        }
                    }

                    var interruptedMessage = ..thread.get("resumeInterruptedMessage");
                    if(interruptedMessage){
                        //仅折叠进 worker 的临时请求链,不修改 chatUi.chatMessages。
                        ..table.push(chatMessages,interruptedMessage);
                        ..thread.set("resumeInterruptedMessage",null);
                    }

                    //恢复链已经折叠了上次临时 assistant;立即固定,避免本轮尚未收到新分片就被强制终止时丢失。
                    if(resentCot) ..thread.set("resumeChainOfThought",table.clone(chatMessages));

                    var userPrompt = string.trim(mainForm.editPrompt.text);
                    if(#userPrompt){
                        ..table.push(chatMessages,{
                            role = "user";
                            content = userPrompt;
                        });
                    }
                    else{
                        if(chatMessages[#chatMessages][["role"]]=="assistant"){
                            ..table.push(chatMessages,{
                                role = "user";
                                content = "<session_resumed>Connection Restored</session_resumed>";
                            });
                        }
                    }

                    //把已经折叠的临时 assistant、以及本次临时 user 提示固定为下一次恢复的基础链。
                    if(resentCot) ..thread.set("resumeChainOfThought",table.clone(chatMessages));

                    mainForm.editPrompt.text = "";
                }
                elseif(shouldInjectImageContext){
                    var msg = chatMessages[#chatMessages];
                    if(msg.role=="user"){
                        msg.content = msg.content + '<context>用户提供了附件图像。如有必要,你可以调用工具 analyze_image 识别图像(`imageUrlOrPath` 参数请指定为 `attachment`)。</context>'
                    }
                }

                aiClient.beforeToolCalling = lambda () !eventStop.wait(0);
                aiClient.onToolCalling = lambda (name,args) !eventStop.wait(0);
                aiClient.afterToolCalling = function (chatMessages){
                    //已形成完整 assistant/tool 边界;后续子轮重新累计临时正文与 reasoning。
                    hasRecoveryChain = true;

                    ..thread.set("resumeChainOfThought",chatMessages);
                    ..thread.set("resumeChainOfThoughtProtocol",aiClient._protocol);
                    ..thread.set("resumeInterruptedMessage",null);

                    mainForm.saveSessionData(true);

                    interruptedResponseContent = "";
                    hasUncommittedResponse = false;
                    return !eventStop.wait(0);
                }
                aiClient.onToolCallingError = function(err, name, args){
                    chatUi.showThinking('\n\n工具`'+name+'` 调用时抛出异常:\n```\n'+tostring(err)+'\n```\n调用参数:\n```json\n'+JSON.stringify(args)+'\n```\n\n');
                }

                var saveInterruptedState = function(){

                    if(aiClient._protocol!="openai" || !hasUncommittedResponse) return;

                    if(!hasRecoveryChain){
                        //首次收到有效分片时建立临时基础链;零输出中断不会建立恢复链。
                        ..thread.set("resumeChainOfThought",table.clone(chatMessages));
                        ..thread.set("resumeChainOfThoughtProtocol",aiClient._protocol);
                        hasRecoveryChain = true;
                    }

                    var interruptedMessage = {
                        role = "assistant";
                        content = interruptedResponseContent;
                    };
                    var reasoningContent = aiClient.reasoningContent;

                    //服务端实际返回过 reasoning_content/reasoning 才放入临时恢复消息;空字符串同样保真。
                    if(reasoningContent!==null){
                        interruptedMessage.reasoning_content = reasoningContent;
                    }

                    if(#aiClient.thinkingBlock) interruptedMessage.reasoning_details = aiClient.thinkingBlock;
                    if(#aiClient.encryptedContent) interruptedMessage.encrypted_content = aiClient.encryptedContent;
                    ..thread.set("resumeInterruptedMessage",interruptedMessage);
                }

                //避免休眠
                ::Kernel32.SetThreadExecutionState(0x80000041/*_ES_CONTINUOUS | _ES_SYSTEM_REQUIRED | _ES_AWAYMODE_REQUIRED*/);
                ::Kernel32.SetCurrentDirectory( (io.appData("aardio/autos/workspace")) );

                var ok,err,errCode,sendBackProtocol = aiClient.messages(chatMessages,
                    function(deltaText,reasoning){
                        if(eventStop.wait(0)) return false;

                        if(#reasoning){
                             hasUncommittedResponse = true;

                             reasoning = ..string.replace(reasoning,"^(\*\*+)(\N+)\1$","\2; ");
                             chatUi.showThinking(reasoning);
                        }

                        //纯推理分片的 deltaText 为空:不创建持久 assistant,但保留临时恢复状态。
                        if(deltaText===null) chatUi.assistant(null);
                        elseif(#deltaText){
                             hasUncommittedResponse = true;

                             interruptedResponseContent = string.concat(interruptedResponseContent,deltaText);
                             chatUi.assistant(deltaText);
                        }
                        saveInterruptedState();
                    },resumeChainOfThoughtProtocol
                );

                if((!ok || err || eventStop.wait(0)) && hasUncommittedResponse){
                    saveInterruptedState();
                }


                var needRetry;
                if(err){
                    needRetry = !eventStop.wait(0);

                    if(errCode==12029){
                        chatUi.errorMessage(`无法与服务器建立连接!<a href="javascript:void(0)" onclick="javascript:external.updateApiKey()">点这里更换其他接口</a>`)
                    }
                    elseif(errCode==401){
                        chatUi.errorMessage(`API 密钥错误!&nbsp;<a href="javascript:void(0)" onclick="javascript:external.updateApiKey()">点这里设置新密钥</a> <code>`+(err||"404 Not Found")+`</code>`)
                        needRetry = false;
                    }
                    elseif(errCode===404){
                        chatUi.errorMessage("服务器不支持此接口或模型!<code>"+(err||"404 Not Found")+"</code>")
                        needRetry = false;
                    }
                    elseif( errCode==402 || ( type.isString(err) && string.find(string.lower(err)
                            ,`<%<<balance>|<credit>|<quota>|<key>|<limit>><<insufficient>|<exhausted>|<exceeded>|<run\s*out>>>|<%<<insufficient>|<exhausted>|<exceeded>|<depleted>|<requires more>><<balance>|<credit>|<quota>|<fund>>>`))
                        ){

                        import web.rest.aiChat.settingForm;
                        chatUi.errorMessage(string.concat(`余额 / 配额不足,<a href="`
                            + web.rest.aiChat.settingForm.getWebSite(aiChatConfig.url,"autos") 
                            +`" title="此网址根据 API 接口地址自动生成,仅供参考">此 API 服务平台网址</a><br><code>`,err,"</code>"))

                        //必须重试 » 微信飞书得到错误信息 » 充值 » 重试成功
                        //订阅余额不足也可能返回 429,无法精确判断
                    }
                    elseif(type.isString(err) ){

                        if( errCode===0 /*无其他错误*/){

                            // 用户中止,且 errCode 为 0 (回复未完成、无其他错误)则不显示错误信息
                            if( !eventStop.wait(0) /*非用户中止*/ ) {
                                    if(#err) chatUi.errorMessage(err);
                                    else {
                                        if(! ( fromWeiXinClawBotMessage || fromFeishuMessageId ) ){
                                            chatUi.errorMessage("服务器未发送有效回复正文!");
                                        } 
                                        else{
                                            err = null;
                                            errCode = null;
                                            needRetry = null;
                                            ok = true;
                                        }
                                    }
                            }
                        };
                        elseif(#err) {

                            do{
                                if(string.find(err,"<Invalid\A+signature\A+in\A+thinking>|<thought\A+signature is not valid>|<missing.+thought\A+signature>|<invalid\A+encrypted\A+content>")){
                                    if(aiChatConfig.url){
                                        var tUrl = inet.url.split(aiChatConfig.url);
                                        if(tUrl && tUrl.host && !string.find(tUrl.host,"<openai\.com>|<anthropic\.com>|<googleapis\.com>|<x\.ai>$") ){
                                            chatUi.errorMessage("思考签名错误(常见于中转方超载中途轮换 key,只能反复重试):<code>"+(err||"404 Not Found")+"</code>")
                                            break;
                                        }
                                    }
                                }

                                chatUi.errorMessage(err); 
                            } while(0);
                        }
                    }
                    else{
                        chatUi.errorMessage(err)
                    }   
                }

                // 恢复按钮状态
                chatUi.onComplete(ok,err,errCode); 

                autos.tools.handlers.__exit__()

                if(hasRecoveryChain && (!ok || err || eventStop.wait(0))){

                    //仅在交错思考回传工具调用结果失败或用户点击“停止”才有必要恢复思维链(此注释不能删除)。
                    ..thread.set("resumeChainOfThought",chatMessages);

                    //初始流式请求 sendBackProtocol 仍可能为空,仍须锁定协议。
                    ..thread.set("resumeChainOfThoughtProtocol",sendBackProtocol || aiClient._protocol);

                    mainForm.saveSessionData(true);

                    if( eventStop.wait(0) && !errCode ){ //errCode 为 0 表示未完成,对于中止操作来说是正常的
                        mainForm.btnSend.text = "继续";
                    }
                    else{
                        mainForm.btnSend.text = "重试";
                    }
                }
                else{
                    //避免旧协议原生状态滞留内存
                    ..thread.set("resumeChainOfThought",null);
                    ..thread.set("resumeChainOfThoughtProtocol",null);
                    ..thread.set("resumeInterruptedMessage",null);

                    if(err){
                        mainForm.btnSend.text = "重试";
                    }
                }

                //任何原因异常中止,稍后自动重试
                if(needRetry){
                    mainForm.scheduleRetry(err); 
                }
                else{

                }

                if(ok){
                    var wx = thread.get("fromWeiXinClawBotMessage"); 
                    var fs = thread.get("fromFeishuMessageId");

                    if(wx){
                        thread.set("fromWeiXinClawBotMessage",null);

                        import web.rest.iLinkClient.login;
                        var bot,err = web.rest.iLinkClient.login();
                        if(bot) bot.sendMessage(wx,"在吗?我已经忙完了!");
                    }

                    if(fs){
                        thread.set("fromFeishuMessageId",null);
                        var feishuClientInstance = mainForm.feishuClientInstance;
                        if(feishuClientInstance) feishuClientInstance.replyMarkdown(fs, "在吗?我已经忙完了!");
                    }
                }

                if(..win[["_form"]]){
                    ..win._form.destroyAll();
                }
            },io.appData("aardio/autos/workspace/"));

            var ok,err = fiber.resume(fib);
            if(!ok){
                mainForm.msgboxErr(err||"未知错误")
            }
        },chatUi,mainForm,aiChatConfig,config.itemData,eventStop,mainForm.fromWeiXinClawBotMessage,mainForm.fromFeishuMessageId,mainForm.feishuClientInstance,resentCot,shouldInjectImageContext
    )
}

chatUi.onComplete = function(success,err,errCode){

    chatUi.assistant(null);//幂等结束 Markdown 流式渲染

    mainForm.saveSessionData();

    mainForm.btnSend.checked = false;
    mainForm.btnSend.disabledText = null;
    mainForm.btnSend.reduce(false);

    mainForm.btnSend.text = "运行";
    mainForm.btnSend.iconText = '\uF1B9';

    mainForm.btnClear.disabled = false; 
    if(success) {
        mainForm.retryAttempts = 0;
        mainForm.editPrompt.setFocus();
    }

    if(chatUi.aiChatThread){
        ..raw.closeHandle(chatUi.aiChatThread); 
        chatUi.aiChatThread = null; 
    }

    if(mainForm.stopTimerId){
        mainForm.clearTimeout(mainForm.stopTimerId);
        mainForm.stopTimerId = null;
    }

    if(taskbar) taskbar.SetProgressState(mainForm.hwnd,0/*_TBPF_NOPROGRESS*/);

    if(config.playSound){
        import sys.midiOut;

        if(success){
            // 成功:马林巴(木琴系),节奏轻快紧凑,上扬琶音
            sys.midiOut.play("changeInstrument(13), 5__, 1'__, 3'__", "C4", 120);
        }
        elseif( !eventStop.wait(0) /*非用户中止*/ ) {
            // 失败:低音距的短促木鱼双击,提醒有误且不刺耳
            sys.midiOut.play("changeInstrument(115), 1_, 1_", "C3", 120);
        } 
    }
}


chatUi.external = {
    alert = function(msg){
        mainForm.msgbox(msg," 🚗 aardio autos ");
    };
    memoriesSetting = function(){
        import autos.ui.setting;
        autos.ui.setting.activeIndex = 3;

        mainForm.btnSetting.oncommand();
    };
    skillsSetting = function(){
        import autos.ui.setting;
        autos.ui.setting.activeIndex = 4;

        mainForm.btnSetting.oncommand();
    };
    aiImageSetting = function(){
        import autos.ui.setting;
        autos.ui.setting.activeIndex = 2;

        mainForm.btnSetting.oncommand();
    };
    updateApiKey = function(){
        import autos.ui.setting;
        autos.ui.setting.activeIndex = 1;

        mainForm.btnSetting.oncommand();
    };
    openWorkspace = function(){
        ..raw.explore(agentWorkspace);
    };
    showMessage = function(msg){
        mainForm.showMessage(msg,,true)
    };
    updateSearchKey = function(){
        var webSearchConfig = fsys.table(io.joinpath(userDataPath,"tavily.table"));
        var key = mainForm.inputBox("请输入密钥(自动识别 Tavily,Exa,DeepSeek 等接口密钥)","AA(aardio autos) - 搜索接口密钥",webSearchConfig.key,,true);
        if(key){
            webSearchConfig.key = key;
            webSearchConfig.save();
        }
    };
    importChatMessages = function(){
        import fsys.dlg;
        var path = fsys.dlg.open("*.jsonl|*.jsonl|*.json|*.json||","自 jsonl 或 json 文件导入对话");
        if(!path) return;

        chatUi.importChatMessagesFromFile(path);
    };
} 

mainForm.scheduleRetry = function(err){
    mainForm.lastApiError = err;

    var startTick = ..time.tick();
    mainForm.retryAttempts = math.min((mainForm.retryAttempts : 0) + 1,6);
    var delay = math.min( 15000 * (2 ** (mainForm.retryAttempts - 1)), 300000 );

    mainForm.lableMessage.background = 0xFFFFFBE6;
    mainForm.lableMessage.color = 0xFFD46B08;

    mainForm.retryTimerId = mainForm.setInterval( 
        function(){
            var endTick = ..time.tick()  
            var remain = endTick - startTick;
            if(remain>=delay){

                mainForm.clearTimeout(mainForm.retryTimerId);
                mainForm.retryTimerId = null;
                mainForm.lastApiError = null;
                mainForm.lableMessage.show(false);
                chatUi.clearErrorMessage();

                //按钮忙时不能再继续重试(对话历史会冲突),重试优先级不能高于 mainForm.btnSend
                if(!mainForm.btnSend.busy()){
                    sendApiMessage( !!thread.get("resumeChainOfThought") );
                }

                return 0;
            }
            else{
                mainForm.lableMessage.text = math.round((delay-remain)/60000,2) + " 分钟后自动重试";
                mainForm.lableMessage.show(true);
            }
        },3000 //3 秒无操作再启动
    )
}

mainForm.cancelRetry = function(){

    if(mainForm.retryTimerId){
        mainForm.clearTimeout(mainForm.retryTimerId);
    }

    mainForm.retryTimerId = null;
    mainForm.lastApiError = null;
    mainForm.retryAttempts = 0;
    mainForm.lableMessage.show(false);
}

mainForm.btnSend.oncommand = function(id,event){

    mainForm.cancelRetry();
    if( mainForm.btnSend.busy() /*mainForm.btnSend.text == "停止"*/){

        mainForm.btnSend.checked = true;
        mainForm.btnSend.disabledText = {'\uF254';'\uF251';'\uF252';'\uF253';'\uF250'}
        eventStop.set();sleep(100);

        mainForm.stopTimerId = mainForm.setTimeout( 
            function(){
                mainForm.stopTimerId = null;

                if( mainForm.btnSend.text == "停止"){

                    if(chatUi.aiChatThread ){

                        ..thread.terminate(chatUi.aiChatThread,0)
                        ..raw.closeHandle(chatUi.aiChatThread)
                        chatUi.aiChatThread = null;

                        // 恢复按钮状态
                        chatUi.onComplete(null); 

                        if( ..thread.get("resumeChainOfThought") ){ 
                            mainForm.btnSend.text = "继续"
                        }
                        else{
                            mainForm.btnSend.text = "重试"
                        }
                    }
                }

                return 0;
            },10000
        )

        return;
    } 

    if( ..thread.get("resumeChainOfThought") &&
        ( (mainForm.btnSend.text=="继续") || (mainForm.btnSend.text == "重试")) ){
        //无论是手动中止("继续"),还是服务端出错中止("重试"),都需要恢复并续接原来的交错思维链
        chatUi.clearErrorMessage();
        sendApiMessage(true);
    }
    else{
        if( mainForm.btnSend.text != "重试" ){
            mainForm.fromWeiXinClawBotMessage = null;
            mainForm.fromFeishuMessageId = null;
        }

        sendApiMessage();   
    }
}

var defaultPrompt = "获取我的公网 IP,然后打开 必应 搜索此 IP";
mainForm.editPrompt.onFocusGot = function(){
    if(#defaultPrompt){
        if(owner.text == defaultPrompt){
            owner.text = "";
        }   

        defaultPrompt = ""; 
    }

    if(win.getIdleTime()<2000){
        mainForm.editPrompt.adjustInputLayout(true);
        mainForm.cancelRetry(); 
    }
}
mainForm.editPrompt.onFocusLost = function(){
    mainForm.editPrompt.adjustInputLayout(false);
}

mainForm.editPrompt.onChange = function(){ 
    if(!owner.modified) return; //非交互操作或 text,load,save
    mainForm.cancelRetry(); //用户操作或 selText 等
}

//回车换行(Ctrl+Enter 发送)
mainForm.editPrompt.onOk = function(ctrl,alt,shift){
    if(ctrl){
        mainForm.btnSend.checked = true;
        mainForm.btnSend.oncommand(,0/*_BN_CLICKED*/);
        return true;
    }
}

var resetMessages = function(restore){

    // 只有清除会话才能切换主记忆,仅修改 config.memoryFile 并不等于修改 aiActiveMemoryFile
    var aiActiveMemoryFile = config.memoryFile || "main.md";
    thread.set("autos.sessionActiveMemoryFile",aiActiveMemoryFile);//与工具、记忆管理界面共享设置

    //清会话、导入会话或切换配置时不得沿用旧的临时交错思考链。
    ..thread.set("resumeChainOfThought",null);
    ..thread.set("resumeChainOfThoughtProtocol",null);
    ..thread.set("resumeInterruptedMessage",null);

    chatUi.clear();

    var tip = /***  
[AA( aardio autos )](http://aau.cn) - 开源,免费的全自主 AI 智能体(Autonomous Agent)  
本工具拥有自主编程、调试、操作电脑的能力,使用本工具造成的一切后果自负,如不同意请勿使用!

#### 🚗 起步
<a href="javascript:void(0)" onclick="javascript:external.updateApiKey()" title="打开 API 设置窗口(试试右键点配置名 » 智能粘贴)。请选择支持交错思考(边思考边调用工具)能力的模型。">&#xF2B9; 设置 AI 接口</a> [&#xF08E; 推理强度](http://www.aardio.com/zh-cn/docs/library-guide/std/web/rest/aiChat.html#reasoning) (磨蹭卡壳时调低,粗心草率时调高) [&#xF06B; 新手资源包](http://aau.cn/pack)   
<a href="javascript:void(0)" onclick="javascript:external.aiImageSetting()" title="允许 AI 生成图像文件。">&#xF03E; 设置图像接口</a> <a href="javascript:void(0)" onclick="javascript:external.updateSearchKey()" title="兼容并自动识别 AI 搜索厂商 Tavily/Exa/DeepSeek 的密钥格式。也可以按`provider:key`格式输入,其中 provider 可指定为"exa", "tavily","deepseek","bocha"之一,key 指定密钥。">&#xF084; 设置联网密钥</a> (<a href="https://app.tavily.com/home" target="_blank">Tavily</a> 免费接口)<a href="javascript:void(0)" onclick="javascript:external.openWorkspace()" title="浏览 Autos 全局工作区:%LocalAppData%\aardio\autos\workspace\">&#xF07C; 工作区</a>

#### 🚗 技巧

- 输入框按 `Ctrl+Enter`快速发送,`Ctrl+V`粘贴文件或图像附件(点 &#xF030; 截屏,右键清除)
- `Shift`+&#xF0C5; 复制推理,3 连击或`Ctrl+单击`快选,右键文本选区可朗读/翻译,右键路径选区可打开/复制/浏览
- AI 自主创建 <a href="javascript:void(0)" onclick="javascript:external.skillsSetting()" title="点这里管理技能包">&#xF085; 技能包</a>(MCP 可自动转为技能包)并按需加载(或在提示词首尾加 `/grill-me` 格式指令)
- 中止任务后可点`继续` / `重试`恢复运行(可输入临时纠偏提示词),故障间隔 15秒 $\to$ 5分钟 自动重试
- [&#xF08E; 多开/检查点/会话管理指南](https://aau.cn/checkpoints/),点( &#xF014; )按钮清除对话与检查点(同时读取记忆,任务可接续);<a href="javascript:void(0)" onclick="javascript:external.importChatMessages()" title="点这里自 json,jsonl 文件导入对话,提示词输入框右键菜单可导出对话">&#xF090; 导入对话</a> 
- 清除对话/重启 AA 都会读取最新 <a href="javascript:void(0)" onclick="javascript:external.memoriesSetting()" title="点这里切换主记忆">&#xF2DB; 主记忆</a>; [&#xF08E; 记忆管理指南](https://aau.cn/memories/)
***/

    //if(!restore) { //应当始终清除备份,避免多进程同时恢复同一会话
        mainForm.clearSessionData();
        chatUi.write(tip);
    //}

    var systemPrompt = /*****************


## 角色

你是 Autos,高级 aardio 自动编程智能体。

你擅长编写、调试、测试和改进 aardio 代码,
并可使用工具运行代码、检查文件、搜索文档、自动化操作电脑。

## 要求

- 优先用最短可靠路径验证关键风险;避免无价值的重复探索、冗长解释和不必要的工具调用
- 必要时应主动调用工具验证关键假设,复杂任务可充分使用工具,关键目标应尽最大能力思考,优先探索高信息增益、能验证关键假设或降低主要风险的方法;必要时可以充分尝试不同路径,但避免无意义的重复试错,避免做无用功。
- 编程任务请调用工具执行必要的单元测试(验证关键代码、你的想法、思路的可行性)
- 微信/飞书来源的消息必须用对应工具回复

## 复杂任务开发流程

以下是 default workflow / playbook,不是硬性清单;可按任务复杂度跳过、合并、重排。

1. Align:明确目标、约束、上下文、验收标准(Definition of Done)。若缺失信息可以安全假设,就说明假设并继续;若会显著影响方向或风险,则先用合适工具检索或询问用户;需求先对齐,切忌闭门造车。
2. Plan:拆分任务,选择技术路线,识别关键风险、依赖、验证方式与必要的回滚/备份策略。
3. De-risk:优先验证最不确定、最可能阻塞的点;必要时查文档/范例/源码,或构造 minimal repro 验证关键 API、协议、环境差异。
4. Implement:做最小但完整的有效改动,保持简单、可维护、可回滚;避免无关重构和扩大任务范围。
5. Validate:运行聚焦的测试或实际验证,观察错误与输出,用结果修正方案;必要时扩大验证范围,直到达到足够置信度。
6. Deliver:简洁总结已完成内容、验证证据、剩余风险与建议下一步;若任务仍很长,可给出可恢复的 checkpoint / State Summary。

流程是迭代的:Observe » Orient » Decide » Act。请根据证据持续调整计划,尽最大努力交付高质量结果;关键任务不要吝惜必要的推理和验证,但始终避免 busywork。

### 任务边界、Checkpoint 与上下文预算

本节是 soft heuristic / meta-guidance,不是硬性限制;请按当前任务的最佳实践自主权衡。

默认保持 Autonomy:用户的显式目标是 priority anchor,而不是探索上限。为完成目标所需的隐含子任务、短闭环验证(micro-loop)可自主推进;避免 zero-value confirmation,不要机械请示。

避免跑偏:复杂任务中请留意 task boundary / scope creep / context budget。工具调用应优先服务当前目标;若新发现的问题能解除阻塞或显著降低风险,可自主处理;否则先记录为 risk / technical debt / follow-up,避免把相关问题自动升级为当前任务。

分而治之:当你自行判断方向不确定性升高、可能进入 rabbit hole、上下文或思维链明显膨胀、连续行动的边际收益下降,或即将进入高影响新阶段时,可主动结束本次对话以创建“阶段性 checkpoint”。阶段性 checkpoint 的目的不是降低探索性,而是 alignment 与 context compaction。

阶段性 checkpoint 的正文应作为可恢复的 State Summary,简短说明:已确认事项、当前判断或风险、下一步默认方案,或确实需要用户决策的问题。若下一步短小、低风险,且能验证关键假设或完成当前 micro-loop,请继续完成小闭环,不要过度频繁打断或要求用户确认。

## 调试与执行 aardio 代码

使用工具 execute_code 执行 aardio 代码时,你可以:

- 使用 `return` 语句返回你需要的值
- 自动捕获 `print` 函数的所有输出
- 调用 `util.testRunner` 执行自动测试,并使用 `return $.report()` 方法收集测试结果

execute_code 同时也是强大的 aardio 代码调试工具,可以捕获并返回编译时错误与运行时错误。

当前系统使用 GUI 界面运行,
execute_code 工具自动禁用 `console.log(...)` 等试图打开控制台的函数,
请使用无 UI 打扰的 `print` 或 `return` 替代:

❌ BAD: `console.log(...);` 
✅ GOOD: `print(...);`

❌ BAD: `console.dumpJson(results);` 
✅ GOOD: `return results;` 或 `print(results);` 

## CRITICAL — HostPath ≠ GuestVAP

Before authoring code for `execute_code`, type every slash-rooted path:

- **HostPath (H):** used in tool arguments; `/x` resolves from the Autos Workspace.
- **GuestVAP (G):** used inside executed aardio; `"/x"` resolves to Effective AppBase `E` + `/x`, not Workspace `/x`.
- **E:** explicit `appBaseDirectory`; otherwise the containing directory of source-file `code`; otherwise Workspace root (`/`). `appBaseDirectory` is itself host-resolved and affects GuestVAP only.

Never copy H into guest code unless E=`/`. User/workspace paths are H unless explicitly labeled GuestVAP. Convert with `G="/" + relative(H,E)`; never include E in G. The same `/x` denotes the same file across both spaces only when E=`/`.

Artifact policy:
- For standalone/generated code producing files for another tool, prefer `appBaseDirectory="/"`.
- For AppBase-sensitive code, bind the planned G→H pair before coding: E=`/my-app/`, G=`/snap.png` → H=`/my-app/snap.png`.
- Any unexpected or repeated prefix is a failed run. Fix the producer path and rerun; never consume an accidental artifact.

## 多动手少空想,多实测少猜测;切莫舍近求远,请积极调用工具

请随时记住你的角色是程序员,你手上有最强大的 execute_code 工具。
execute_code 工具不仅可以运行正确的代码,也可以调试错误的代码。
不要基于空想推进任务,请基于工具调用与实测结果推进任务。

## 提示

即使本系统没有提供相关工具,
你也可以调用 aardio 强大的标准库与 ide 扩展库。

- 获取库路径: `var path,dir = io.libpath(lib)` ,库不存在时返回 null 。创建库时应当硬编码路径 `~/lib/namespace...`(公共库)或  `/lib/namespace...`(用户库)
- aardio 范例目录:  `~/examples` ,可调用工具 `search_text(path='~/examples')` 搜索范例
- aardio 调用其他编程语言:  `~/examples/Languages/`,例如 `/examples/Languages/R/`
- aardio 文档与指南目录: `~/docs/` ,可调用工具 `search_text(path='~/docs')` 搜索文档。文档中的虚拟路径`~/docs/examples/`指向物理路径 `~/examples/`
- `https://www.aardio.com/zh-cn/docs/` 协议: 文档中 `https://www.aardio.com/zh-cn/docs/` 链接的根目录为 IDE 目录 `~/docs/`,`https://www.aardio.com/zh-cn/docs/examples/` 对应 IDE 目录 `~/examples/` ,`https://www.aardio.com/zh-cn/docs/library-reference/` 下面的库参考文档则由 ide.doc.libraryMd 动态生成
- 库文档分为 3 类: 
    1. 库参考:调用 lookup_library_reference 工具获取,由库源码内智能提示声明(必不可少)生成(基于 ide.doc.libraryMd )
    2. 库指南:位于 `~/docs/library-guide` 目录,内置库与部分标准库
    3. 库文档:位于 `~/docs/library` 目录,扩展库安装的文档
- 探查库源码: 工具 get_library_source
- 自动化: 标准库 winex(控制外部窗口),key(模拟键盘),mouse(模拟鼠标) 等命名空间
- 后台模拟点击: `winex.mouse.click(hwnd,x,y)`, `winex.key.click(hwnd,'SPACE')`
- 文件操作: fsys 命名空间,io 内置库
- 进程操作:  process 命名空间
- `process.cache.find(fileName[,productName[,searchLnkFile=true]])` 搜索程序路径(模式匹配,忽略大小写),例 `process.cache.find("qq.exe")`
- `import fsys;fsys.delete(path,true/*allowUndo*/)` 安全删除到回收站可用 `import fsys.recycleBin;fsys.recycleBin.restoreLast()` 快速找回
- 网页自动化: web.view(WebView2) 提供 preloadScript 方法可注入并修改 JS 内置函数,cdp 系列方法也非常有用,也可以利用 waitEle,waitEle2 方法实现 XPath 查询、检测、等待网页元素并回调 JS
- Markdown 转 HTML: string.markdown 基于 C 组件速度极快,推荐
- HTML 转 PDF: web.view + cdp('Page.printToPDF')
- PDF: fsys.pdfium 
- 系统自动化: mouse, key, winex, process 库
- 自动下载安装 MSI/EXE: inet.installer
- OCR: 标准库自带的 dotNet.ocr 或工具 analyze_image
- 读写剪贴板: win.clip 命名空间
- 操作系统版本信息: win.version
- SYSTEM_INFO 结构体:  sys.info
- PowerShell: 标准库自带的 dotNet.ps 或工具 process_powershell ;仅在有必要时才使用 PowerShell ,编辑文本或代码请改用 patch_text_file, edit_text_file 等更可靠的工具。
- CMD/外部进程: 工具 process_popen;代码内可用 process.popen
- 批处理(*.bat): process.batch
- 创建美观的图形界面: 用 plus 控件, skin 方法美化。运动函数可用 av.video,
- 自动动画与游戏:canvas 控件,粒子可用 av.video.particles
- 简单合成音乐: sys.midiOut 高级混音与播放:av.audio.sfx + sys.soundPool
- 基于 canvas 控件的简单图表: gdip.chart.*(bar,hbar,line,pie) 等
- 创建复杂的适合用网页展示的界面:  web.view(WebView2) 加载前端是最佳实践,例如显示数据看板、统计报表用网页非常方便,与 aardio 交互也非常方便。
- ImTip 超级热键: process.imTip 库,以及超级热键指南( `~/docs/library-guide/std/key/hotkey.md` )
- 调用 .NET 框架或程序集: 标准 dotNet
- 编译运行 C# 代码: `dotNet.createCompiler('C#')` ,注意 aardio 中的 CLR 编译器最高支持 C# 4.0( .NET 4.0+)/5.0( .NET 4.5+) 语法
- 调用 WinRT 组件:  dotNet.uwpCompiler,参考 `\examples\Languages\dotNet\WinRT`目录
- JScript 或 VBScript:  标准库  web.script
- 现代 JavaScript:  标准库 nodeJs 或 web.view 
- 调用 Python:  py3 扩展库
- 调用或编译执行 C 语言,生成 DLL:  tcc 扩展库
- 调用 HTTP API: aardio 通常不需要专门的 SDK,大多时候只需要 web.rest.jsonClient 或 web.rest.jsonLiteClient,`\examples\Web\REST` 目录下提供了很多示范代码
- dotNet.reflection 探查 .NET DLL
- `print(com.DumpTypeInfo(comObject))` 探查 COM,COM ROT 可用 com.eachRunning 

以上只是部分提示与建议,你可以根据用户需求与最佳实践进一步调研分析、权衡取舍

## 文本文件编码原则

文本类工具默认使用 UTF-8 编码。
非 UTF-8 编码项目应当优先将其转换为 UTF-8 编码再处理。 

举例,如果你处理一个古老 C++ 项目,源文件使用 ANSI 编码:

反模式:每次都读取文件 → 转换编码 → 艰难改写 → 转换编码 → 写回去。
最佳实践:先批量转换为 UTF-8 优雅地解决所有问题,然后可以使用所有 Autos 工具进行编辑。

> 现代 C++ 支持 UTF-8,即使是 VC6 也可将源码改为 ANSI/UTF-8 兼容(代码用转义表示汉字,注释用英文)。

aardio 提供 fsys.codepage{load,save,copy} 可用于操作使用不同编码 / BOM 的文本文件。 

## 下载文件

小文件可用 `web.rest.client().get(url)` 或 `inet.http().get(url)` 等方式快速抓取。

大文件可用 inet.httpFile 下载(支持断点续传):

```aardio 
var remoteFile = inet.httpFile(url,savePath,resumeFilePath,userAgent,proxy)

//仅探查
var ok,err,errCode = remoteFile.test()
if( ok ){
    print("文件已下载完成、服务器未更新,无需重新下载"); 
}
else{
    print("下载错误,HTTP错误代码",remoteFile.statusCode,err);
}

//下载文件
var ok,err,fileSize = remoteFile.download() 
print( ok,err,inet.lastResponse() )
```

以下函数下载并自动解压,成功返回解压目录,失败返回 null,错误信息,错误代码(或 HTTP 状态码)

- `zlib.httpFile.download(url,title,downloadDir)` 支持 *.zip、*.tar.gz、*.tgz, *.gz 等格式
- `sevenZip.decoder2.httpFile.download(url,title,downloadDir)`
- `sevenZip.lzma.httpFile.download(url,title,downloadDir)`

## GitHub 查询与下载

❌ BAD: 直接访问 www.github.com 或 raw.githubusercontent.com ,这可能失败或非常慢。

✅ GOOD:

- 调用 GitHub API : `import web.rest.github; var gh=web.rest.github;` Prefer `gh.list(id)`, `gh.findFiles(id,glob)`, `gh.read(id,path,{pattern="text";maxBytes=4000})`. Official REST: `gh.repo(id).endpoint.get(params)`. Never `search.code`; broad search: web/local checkout.
- 小文件可用 gh.getContent(url) or gh.getContent(user,repo,path,branch)直接抓取
- 大文件可用 web.rest.githubMirrors.proxyUrl(gitHubUrl) 获取加速镜像地址

## aardio Anti-Patterns & Traps (High-Priority Heuristics)

- **Falsy Coalesce Bleed:** `a:b` and `c?a:b` evaluate to false on ANY falsy `a` (`0/false/null`). For strict null-coalescing, enforce `if(a===null) a=b;`.
- **String Lexing:** `''` strips physical EOLs (use `""` for multiline). `\"` throws in `""`. Autos tool args bypass `\uXXXX` decoding (emit raw UTF-8 chars directly).
- **Patterns (Non-PCRE):** Alternation mandates explicit grouping: `<w1>|<w2>`. `:` is a CJK wildcard (escape as `\:`). `!` = boundary assert. `-` is NOT a lazy quantifier (use `+?`/`*?`). `[]` evaluates CJK chars atomically.
- **Multi-Return Arity Expansion:** `string.replace()` yields `(result, count)`. `tonumber(str)` yields `(num, len)`. Isolate via parens `(tonumber(x))` in variadic calls (e.g., `table.push`) to block tuple expansion. (Note: use `string.replaceLiteral` for verbatim replace).
- **Concat Polymorphism:** `+` defaults to arithmetic unless binding string literals. Enforce `++` for deterministic string concatenation.
- **Iterator Hijack:** `for k,v in fn()` parses `fn` as an iterator factory. Wrap as `for k,v in (fn())` to evaluate and traverse the yielded table.
- **Hybrid Table Iteration:** `fsys.list()` returns a hybrid table (dense array + `name=path` hash pairs). Enforce strict dense-array traversal: `for(i=1; #files)`.

## 不要过早生成完整代码

反模式:

上来就过早生成完整代码,然后浪费大量时间坑填坑。
这就好比造房子粗制滥造迅速完工,最后发现一堆问题,不是房子倒了,就是必须砸掉重来。

最佳实践:

先做必要的测试,最后再生成可交付的代码成品。
就好比造房子先打好地基,一层一层稳步前进,时机成熟再装修与交付成品。

## 测试基于 win.ui(win.form) 的 aardio 图形界面( GUI ) 

你不能假设当前是无人干扰的会话环境。
当你运行与测试 aardio 窗口程序时,电脑用户可能无意中操作你运行的界面,或将其切换到后台。

你应当优先优先使用无界面与非阻塞的方式编写测试用例验证关键代码、算法、思路,
非必要时请避免滥用视觉识别、模拟鼠标键盘等低效的、容易出错的、阻塞的、有界面的方式去验证结果。
例如编写游戏时你可以调用 util.testRunner 运行无头逻辑测试验证关键的碰撞算法与代码,而不是通过运行后的截图去验证碰撞效果(低效反模式)。

无头像素测试:

```aardio 
import gdip;
var bmp = gdip.bitmap(40,30);
var graphics = gdip.graphics(bmp);
var brush = gdip.solidBrush(0xFFFF0000);

import util.testRunner;
var $ = util.testRunner("无头像素测试");
$.test(graphics.fillCircle(brush,10,10,5),"绘图");

var argb = bmp.getPixel(10,10)/*0xAARRGGBB*/;

// Misfed COLORREF (0xBBGGRR) induces R/B channel swap
var r,g,b,a = gdi.getRgba(argb);

print(r=r,g=g,b=b,a=a);

$.expect(argb,0xFFFF0000,"检查像素是否填充圆心");
return $.report();
```

GUI 冒烟:

```aardio 
winform.show() //显示窗口
thread.delay(1000); //先消息泵,后点击截图,注意时序

winform.plus.click(x,y) //模拟点击(发送鼠标按下与弹起消息,x,y 为控件客户区坐标)
var bmp = winform.plus.snap() //双缓冲截图(依赖 WM_PAINT 消息)
winform.close() //不进入无限期 win.loopMessage()
if(bmp.save("/test.jpg",80)){ 
    return true;
}
```

GUI 冒烟流程(首选):

- 编写代码并存为 `*.aardio` 源文件
- 使用工具 execute_code 执行代码;参数 `code` 指定 aardio 代码文件路径;参数 `codeReplacement.oldText` 指定为 `win.loopMessage();`,参数 `codeReplacement.newText` 指定为冒烟测试代码(与手动替换代码等价,可访问局部变量)
- codeReplacement 仅在执行前内存修改运行时代码,不会改动源文件。

异步线程 GUI 冒烟流程(备选):

- 用工具 execute_code(threadMode=async) 创建独立界面线程(异步线程里不会替换 win.loopMessage)
- 你可以调用 `var winform = autos.waitAsyncForm(threadId,timeout/*毫秒*/)` 直接获取最后一次 execute_code(threadMode=async)  创建的首个窗体对象,
autos.waitAsyncForm 仅返回准备就绪的有效窗体(这指的是窗体有效,创建窗体的线程内 thread.delay 或 win.loopMessage 等消息泵已正常运行且没有发生错误)。可以跨线程调用 winform 的属性方法,或用 `winform.invoke(function(...){var winform = owner;/*不能跨线程传递 upvalue,使用隐式注入的 owner 代替*/},...)` 将函数发送到创建窗口的线程内执行。
- 你也可以调用 `thread.set("uniqueVarName",value)`存储多线程共享变量,用 `thread.acquire("uniqueVarName",timeout/*毫秒*/)` 跨线程读取。

## 共享浏览器

使用 `web.view.shared({text="title"})` 可在独立线程中创建共享的 WebView2 窗口;相同 `text` 复用同一实例,不同 `text` 可创建不同实例。

- 你不会被 shared 浏览器阻塞对话,因为它是异步的。
- shared 浏览器可以 持久驻留,可以跨工具调用、跨对话线程,你也可以使用 `wb.close()` 关闭它
- 你可以跟踪或辅助用户操作 shared 浏览器
- 你可以跨线程直接调用 wb 对象的属性与方法,例如执行 JavaScript / CDP 命令,探查网页状态,自动化操作
- 你可以使用 wb.invoke 在创建浏览器界面线程内执行 aardio 代码

示例:

```aardio 
import web.view;

// wb 只是代理对象
var wb = web.view.shared(text='shared-demo');

/* 
wb.invoke 的参数 @1 是字符串则作为 JS 表达式获取并执行 JS 函数(丢弃函数返回值)。
参数 @1 如果是 aardio 函数对象则会被送回到创建 web.view 的界面线程执行(获取函数返回值)。
*/
var result = wb.invoke(
    function(...){
        // 在界面线程创建安全的闭包

        var wb = owner; // 获取真实的 web.view 对象

        wb.external = {
            // JS 绑定的 aardio 回调函数必须在同一界面线程
            nativeFunction = function(a,b){ 
                return a+b;
            } 
        }
    }
)

// 跨线程调用方法或属性
// wb.go('https://example.com');

wb.html = /***
<button id="btn" onclick="aardio.nativeFunction(2,3).then(()=>this.innerText='已点击')">点我</button>
***/

// 等待节点,可指定 XPath 或 CSS 选择器
wb.waitEle(`//button[@id="btn" and normalize-space(.)="已点击"]`/*,`this.innerText='clicked'`,5000*/);

// wb.close()

return {
    text = wb.eval("document.getElementById('btn').innerText");
}

```

*****************/

    var aiChatConfig = config.itemData[[config.selItem]]
    if(#aiChatConfig[["systemPrompt"]]){
        systemPrompt = aiChatConfig.systemPrompt + '\n\n' + systemPrompt;
    }

    //保持不变以利于构建缓存
    chatUi.aardioSystem(systemPrompt);

    //技能包
    import autos.skills;
    var systemPrompt = autos.skills.buildRoutePrompt();

    var lunar;
    try{
        import time.lunar;
        lunar =  '\r\n- 当前农历:' +  tostring(time.lunar());
    }
    catch(e){
        lunar = ""; //兼容 XP 系统
    }

    //主记忆
    var mainMemoryPath = io.joinpath(userDataPath,"~memory/",aiActiveMemoryFile)

    systemPrompt = systemPrompt + "

**Trust Skill Abstractions**: NEVER manually probe environments (e.g., scan registry/disk for EXEs). Eager-load available skills first, as they natively encapsulate path resolution and bootstrapping.

## 背景

- 当前日期:" + tostring(time(str,"%Y年%m月%d日","chs"))
    + '\n- 当前操作系统:"' + ..win.version.format() + '"'
    + lunar 
    + '\n- aardio 开发环境所在目录:`' +  io.fullpath("~/") + '`'
    + '\n- 操作系统临时目录:`' +  fsys.getTempDir() + '`'
    + '\n- 你可以自由读写文件的工作区目录:`' +  agentWorkspace + '`,这也是 Tool-Call Space (Host) 的默认`应用程序基准目录`(单个  `/` 或 `\` 开头文件路径的基准目录)。'
    + '\n- 本系统名称为: AA( aardio autos ) 版本: ' + autos.version
    + '\n- 本系统源代码位于:`' + io.fullpath("~/examples/AI/autos.aardio") + '` 如果用户有需要,你可以改进系统源代码,但需经过用户确认同意。如果用户要求你介绍自己,你可以先读取运行你的这个系统源代码作为参考。'
    + '\n- 本系统进程权限:' + (..process.admin.isRunAs() ? "以管理权限启动" : "以普通权限启动") + "。如果你需要以管理权限执行工具或代码,请提醒用户可按住 Shift 键单击 aardio 工具面板顶部的`AA`按钮以管理权限启动 Autos 。"
    + '\n\n'    
    + "## 长期记忆

### 记忆文件系统

- 长期记忆根文件夹 `" + io.joinpath(userDataPath,"~memory/") + "` 是你的大脑。
- 用户当前选择并激活的主记忆文件是 `"+ aiActiveMemoryFile + "`(创建会话时自动注入系统提示词),阶段性成果/大坑教训应使用 write_memory 写入当前主记忆。

但应当控制写入长期记忆的频率,避免过度频繁地写入长期记忆。
几乎没有可重用价值的信息(对以后的对话和任务几乎没有帮助)不应当写入长期记忆。
写入主记忆不会影响当前上下文,只有用户清除当前会话并创建新会话时才会加载新的主记忆。

### 整理与修剪主记忆

当前激活的主记忆文件(`"+ aiActiveMemoryFile + "`)的体积:" + math.size64(io.getSize(mainMemoryPath)).format() + ",超过 50KB 你应当在会话结束前整理与修剪此记忆文件(调用工具 write_memory 覆盖式替换全部内容)。

整理与修剪长期记忆文件的原则是丢弃对任务推进没有价值的信息,归纳整理并保留有价值的知识、技能与重要记忆,暂时不需要的记忆可以存为其他分枝记忆文件(在当前主记忆中记录其路径与摘要)。

### 利用长期记忆分解并逐步完成复杂任务

避免在一次对话中完成复杂的任务的所有步骤。
你应当做好规划,分而冶之,把复杂的任务目标拆分为可以逐个完成的简单小目标。
在长期记忆中记录复杂任务的规划、步骤,以及需要逐个攻克的难点。
每次完成一个小目标或里程碑并结束对话时你应当将重要的信息、研究成果、任务进度、下一步建议执行的任务写入长期记忆。
即使用户清除对话上下文,你仍然可以使用长期记忆继续推进项目,不会遗忘重要的信息。

### 刷新主记忆

在用户创建新对话(清除原对话)或重启 Autos (保留原对话)时都会重新加载取当前主记忆文件并注入系统提示词。
在用户清除所有上下文并新建对话之前,继续原对话不会重新读取新的主记忆(这是为了重用大模型 API 的前缀缓存)。
因此增量写入主记忆的重要信息你也要输出到回复正文(除非在上下文中已存在)。

### HANDOFF.md (Project-Bound LTM)

For complex workflows, utilize `HANDOFF.md` in the project root as a localized memory & state handoff document.

- **Pointer Indexing**: If created, you MUST register its path in the global memory file( `"+ aiActiveMemoryFile + "`).
- **Traits**: Portable, lazily loaded (on-demand R/W), and context-isolated. Survives global LTM wipes or switches without cross-project pollution. 
- **SoC (Separation of Concerns)**: 
  - Auto-init/Global context $\to$ `"+ aiActiveMemoryFile + "`.
  - Project-scoped/Migratable context $\to$ `HANDOFF.md`.
- **Heuristic**: Token-frugal. Maximize signal-to-noise ratio. Do not cross-contaminate scopes.

### ADR (Architecture Decision Records) & Context Eviction

- **Occam's Razor**: A solitary `HANDOFF.md` suffices for most repos. ZERO premature ADR instantiation.
- **Eviction Trigger**: IF AND ONLY IF `HANDOFF.md` suffers severe token bloat via legacy pitfalls, deep lore, or monolithic arch-decisions.
- **Offload & Index**: Migrate heavy historical context to `docs/adr/*.md`. Mutate the original segment in `HANDOFF.md` into a lightweight pointer (e.g., `- [Why No Redux](docs/adr/001-no-redux.md)`).
- **Lazy Hydration**: Read ADRs strictly on-demand (cache-miss resolving). Do NOT preemptively load archival context for unrelated tasks.

";

    if(..io.exists(mainMemoryPath)){
        var main = string.load(mainMemoryPath)
        if(main) systemPrompt = systemPrompt + '\n\n\n---\n\n\n以下是你之前记录的长期记忆和关键信息('+ aiActiveMemoryFile + '`)。\n\n'+main;
    }

    chatUi.system(systemPrompt);
    //assert(#chatUi.chatMessages===2);//超长而自动清理时最多保留头部 2 个系统提示词

    mainForm.btnSend.text = "运行";
}


mainForm.btnSetting.oncommand = function(id,event){
    import autos.ui.setting;

    mainForm.cancelRetry();

    //打开 AI 设置窗口,参数 2 指定配置文件
    var frmSetting = autos.ui.setting(mainForm,config);

    //用户点击了「保存」           
    if( frmSetting.doModal(mainForm) ){ 
        mainForm.cancelRetry();

        //获取用户当前选择的 AI 接口配置
        var configItem = config.itemData[config.selItem]; 
        if(configItem){

            if( configItem.model && configItem.key ){

                var cretePromptCacheKey = function(){
                    import crypt;
                    return crypt.sha256( configItem.key + ".aardio.autos." + io._exepath);
                }

                if( string.find( configItem.model,"!\wgpt!\W") 
                    || (string.find( configItem.model,"!\wgrok!\W") &&(configItem.protocol==="responses") )){

                    if(configItem.extraHeaders[["x-grok-conv-id"]]){
                        configItem.extraHeaders = null;
                        config.save();
                    }

                    configItem.extraParameters = ..table.mix( configItem.extraParameters,{
                        prompt_cache_key = cretePromptCacheKey();
                    })

                    config.save();
                }
                elseif( string.find( configItem.model,"!\wclaude!\W") ){
                    if(configItem.extraParameters[["prompt_cache_key"]]){
                        configItem.extraParameters.prompt_cache_key = null;
                        config.save();
                    }

                    if(configItem.extraHeaders[["x-grok-conv-id"]]){
                        configItem.extraHeaders = null;
                        config.save();
                    }

                    // 必须开全局隐式缓存,如中转命中低则不应使用
                    configItem.extraParameters = ..table.mix( configItem.extraParameters,{
                        "cache_control":{"type":"ephemeral"} 
                    })

                    config.save();
                }   
                else{

                    if(configItem.extraParameters[["prompt_cache_key"]]){
                        configItem.extraParameters.prompt_cache_key = null;
                        config.save();
                    }

                    if( string.find( configItem.model,"!\wgrok!\W") ){

                        if( type(configItem.extraHeaders)!="table"){ 
                            configItem.extraHeaders  = {
                                "x-grok-conv-id" = cretePromptCacheKey();
                            }
                            config.save();
                        }
                    }
                    elseif(configItem.extraHeaders[["x-grok-conv-id"]]){
                        configItem.extraHeaders = null;
                        config.save();
                    }
                }
            }

            aiChatConfig = table.assign({},configItem); 

            if(!chatUi.started()){
                resetMessages(); 
            } 
        } 
    }
}

mainForm.btnClear.oncommand = function(id,event){
    mainForm.btnSend.disabled = true;
    mainForm.cancelRetry();

    if(chatUi.aiChatThread){
        ..thread.terminate(chatUi.aiChatThread,0)
        ..raw.closeHandle(chatUi.aiChatThread)
        chatUi.aiChatThread = null;
    }

    io.close();
    resetMessages();//清除聊天上下文
    //mainForm.clearSessionData();

    //清除附件图像
    var oldBmp = mainForm.btnImageAttachment.setForeground(null);
    if(oldBmp) oldBmp.delete();
    mainForm.btnImageAttachment.newImageAttached = null;
    mainForm.btnImageAttachment.iconText = '\uF030';

    mainForm.editPrompt.setFocus();
    mainForm.showMessage("✅ 已重新读取并刷新主记忆 ……");

    mainForm.btnSend.disabled = false;
}

mainForm.btnCopy.oncommand = function(id,event){
    var md;
    if(::GetKeyState(0x10/*_VK_SHIFT*/) & 0x8000){
        md = chatUi.getMarkdown();
        md = string.replace(md,`^.+\<span class="chat-role assistant"\>(.+?)\</span\>\s*`,``,1);
        md = string.replaceLiteral(md,`<span></span>`,``);

        if(::GetKeyState(0x11/*_VK_CTRL*/) & 0x8000){
            if(string.indexOf(md,"<details><summary>推理过程</summary><blockquote>")){
                md = '分析以下 AI 的思考过程,理一理他在纠结一些什么,踩了哪些坑:\n\n---\n\n' + md;
            }
        }
    }
    else{
        md = chatUi.lastAssistantMessage(true)
    }

    if(!#md) return mainForm.msgboxErr("未复制任何内容,AI 回复内容为空!");

    win.clip.write( string.crlf(md,'\r\n') );
    mainForm.btnCopy.disabledText = {'\uF254';'\uF251';'\uF252';'\uF253';'\uF250';text=''} 
    thread.delay(800);
    mainForm.btnCopy.disabledText = null;
}

mainForm.weixinSendMessage = function(fromMsg,textContent,final){

    if(mainForm.btnSend.busy()){
        thread.set("fromWeiXinClawBotMessage",fromMsg); 
        return "抱歉我正在思考,等我忙完立即联系您。";
    }
    elseif(mainForm.retryTimerId){
        return "抱歉我正在执行重试任务,当前故障:`" + (mainForm.lastApiError||"") + "`";
    }
    else{
        mainForm.fromFeishuMessageId = null;
        mainForm.fromWeiXinClawBotMessage = fromMsg;

        if(!final){
            chatUi.prompt(textContent,,true/*暂不发送到界面*/);

            mainForm.saveSessionData();
        }
        else{  
            sendApiMessage(false,textContent); 
        }
    }   
}

// 飞书消息处理函数
mainForm.feishuSendMessage = function(messageId,requestText,attachmentNotice){
    if( mainForm.fromFeishuMessageId == messageId ){
        return mainForm.btnSend.busy()?"我正在思考 ……":null;
    }

    if(mainForm.btnSend.busy()){
        thread.set("fromFeishuMessageId",messageId);
        return "抱歉我正在思考,等我忙完立即联系您。";
    }
    elseif(mainForm.retryTimerId){
        return "抱歉我正在执行重试任务,当前故障:`" + (mainForm.lastApiError||"") + "`";
    }
    else{
        mainForm.fromWeiXinClawBotMessage = null;
        mainForm.fromFeishuMessageId = messageId;

        if(attachmentNotice){
            chatUi.prompt(attachmentNotice,,true);

            mainForm.saveSessionData();
        }

        if(requestText){
            sendApiMessage(false,requestText); 
        }
    }   
}

import win.debounce;
var hideMessage = win.debounce(
    function(){
        mainForm.lableMessage.show(false);
    },3000);

mainForm.lableMessage.orphanWindow(true);
mainForm.showMessage = function(content,msgType,popup){
    if(!content){
        mainForm.lableMessage.show(false);
        return ;
    }

    if(mainForm.btnSend.busy() || popup){
        content = string.replace(content,'\r?\n'," ");
         mainForm.lableMessage.color = 0xFFFFFFFF;
        if(msgType=="error") {
            mainForm.lableMessage.background = 0xFFF56C6C;
        }
        elseif(msgType=="loading") {
            mainForm.lableMessage.background = 0xFF409EFF;
        }
        else/*if(msgType=="success")*/{
           mainForm.lableMessage.background = 0xFF67C23A;
        }

        mainForm.lableMessage.text = content;
        mainForm.lableMessage.show();
        hideMessage();
    }
    else{
        var overwrite = !chatUi.started();

        if(msgType=="error") chatUi.errorMessage(content,overwrite);
        elseif(msgType=="loading") chatUi.showLoading(content,overwrite);
        else/*if(msgType=="success")*/ chatUi.successMessage(content,overwrite); 
    } 
}

mainForm.editPrompt.oleCallback(


    GetDragDropEffect = function(drag,grfKeyState,effect){
        return 0,1/*_DROPEFFECT_COPY*/
    };

    QueryAcceptData = function(ptrDataObj,format,reco,really,hMetaPict){

        if(really){

            var dataObj = com.interface.IDataObject(ptrDataObj);


            if(dataObj.isText()){
                var txt = dataObj.getText();
                if(txt){
                    var b,e = mainForm.editPrompt.getSel();
                    if(!e) mainForm.editPrompt.setSel(-1);

                    if(..string.match(txt,"^\s*<%\{\}>|<%\[\]>\s*$") && ..string.indexOf(txt,'\n') ){
                        if(JSON.tryParse(txt)){
                            txt = '\r\n```json\r\n'+txt+'\r\n```\r\n';
                        }
                    }

                    mainForm.editPrompt.selText = txt;  
                }
            }
            elseif( dataObj.isFile()){
                var files = dataObj.getDropFile();
                if(#files){
                    mainForm.editPrompt.setSel(-1);
                    mainForm.editPrompt.selText = " `" + string.join(files,",")+"` ";
                }
            }
            elseif( dataObj.isBitmap()){
                var bmp = dataObj.getBitmap();
                if(bmp){
                    var oldBmp = mainForm.btnImageAttachment.setForeground(bmp);
                    if(oldBmp) oldBmp.delete();

                    mainForm.btnImageAttachment.newImageAttached = true;    
                    mainForm.btnImageAttachment.iconText = null;    
                }
            }

            /*返回 0 继续默认处理,文本放入控件,返回 _S_FALSE 阻止默认处理。*/
            return 1/*_S_FALSE*/
        }

        return 0; 
    };

    GetContextMenu = function(selType,oleObj,charRange){
        mainForm.cancelRetry();

        var menuItems = {
            { /*分隔线*/ };

                { '导入对话';  function(id){
                    import fsys.dlg;
                    var path = fsys.dlg.open("*.jsonl|*.jsonl|*.json|*.json||","自 jsonl 或 json 文件导入对话");
                    if(!path) return;

                    chatUi.importChatMessagesFromFile(path);
                }; mainForm.btnSend.busy() ? 1/*_MF_GRAYED*/ : 0}; 

                { "导出对话";  function(id){
                    import fsys.dlg;
                    var path = fsys.dlg.save("*.jsonl|*.jsonl|*.json|*.json||","导出对话到 *.jsonl");
                    if(!path) return;

                    var file,err = io.file(path,"w+b");
                    if(!file) return mainForm.msgboxErr(err);

                    var chatMessages = chatUi.chatMessages;

                    if(..string.endsWith(path,".jsonl",true)){
                        for(i=1;#chatMessages;1){
                            var msg = chatMessages[i]
                            var line  = JSON.stringify(msg,false,false);
                            line = string.crlf(line,"")
                            file.write(line,'\r\n');
                        }
                    }
                    else {
                        file.write((JSON.stringifyArray(chatMessages,true,false)));
                    }

                    file.close();

                }; #chatUi.chatMessages<3 ? 1/*_MF_GRAYED*/ : 0
            }; 
        }

        mainForm.editPrompt.popMenu(menuItems);
    };
)

var weixinListeningThread;
var weixinTokenPath;
mainForm.btnWeixin.oncommand = function(id,event){
    mainForm.cancelRetry();

    import web.rest.iLinkClient.login;

    if(!mainForm.btnWeixin.checked){
        if(weixinListeningThread){
            thread.terminate(weixinListeningThread,0)
            raw.closeHandle(weixinListeningThread);
            weixinListeningThread = null;
        }

        if(weixinTokenPath){
            io.remove(weixinTokenPath)
            weixinTokenPath = null;
        }

        mainForm.showMessage("✅ 微信机器人已断开连接,再次点击微信图标重新扫码连接。");
        return;
    }

    var bot,err = web.rest.iLinkClient.login(parent=mainForm;text="使用微信连接 aardio autos");
    if(bot) mainForm.showMessage('✅ 微信机器人已上线,可接收 / 发送文本、图片、文件 ... 再次点击微信图标断开连接。');
    else {
        if(#err ) mainForm.msgboxErr( err );
        mainForm.btnWeixin.checked = false;
        return;
    }

    weixinTokenPath = bot.config.tokenPath;
    weixinListeningThread = thread.create( 
        function(mainForm,chatUi){
            import autos.weixinClient;
            autos.weixinClient(mainForm,chatUi);    
        },mainForm,chatUi
    )
}

// 飞书配置和连接
var feishuClient;
var feishuConfig = fsys.table(io.joinpath(userDataPath,"feishu.table"));

var frmFeishu;
mainForm.feishuOnReady = function(){
    feishuClient = mainForm.feishuClientInstance;
    mainForm.btnFeishu.checked = true;
    if(frmFeishu && frmFeishu.valid){ 
        frmFeishu.setStatus("已连接", 0x00AA00);
        frmFeishu.close();
    }

    mainForm.btnFeishu.checked = true;
    mainForm.showMessage('✅ 飞书机器人已上线,可接收 / 发送文本、图片、文件 ... 再次点击飞书图标断开连接。');
}

mainForm.feishuOnError = function(err){
    if(!mainForm.btnFeishu.checked && (frmFeishu && frmFeishu.valid)){
        frmFeishu.setStatus("连接失败:" + (err || "未知错误"), 0xFF0000);
        frmFeishu.btnConnect.disabled = false;
    }
}

mainForm.feishuOnClose = function(reason){
    feishuClient = null;
    mainForm.feishuClientInstance = null;
    mainForm.btnFeishu.checked = false;
}

mainForm.btnFeishu.oncommand = function(id,event){
    mainForm.cancelRetry();

    import web.feishu.settingForm;

    // 如果已连接,断开连接
    if(!mainForm.btnFeishu.checked && feishuClient){
        feishuConfig.autoConnect = false;
        feishuConfig.save();

        feishuClient.stop();
        feishuClient = null;
        mainForm.feishuClientInstance = null;
        mainForm.btnFeishu.checked = false;
        mainForm.showMessage("✅ 飞书机器人已断开连接,再次点击重新连接。","success");
        return;
    }


    mainForm.btnFeishu.checked = false;

    // 打开配置窗口
    frmFeishu = web.feishu.settingForm(mainForm, feishuConfig);

    // 连接回调
    frmFeishu.onConnect = function(appId, appSecret){
        frmFeishu.setStatus("正在连接...", 0x0078D4);
        frmFeishu.btnConnect.disabled = true;

        thread.invoke(
            function(mainForm, chatUi, appId, appSecret){
                import win.ui;
                import autos.feishuClient;

                mainForm.showMessage("✅ 飞书机器人正在连接","loading");

                var client = autos.feishuClient(mainForm, chatUi, appId, appSecret);
                mainForm.feishuClientInstance = client;

                client.start();
                win.loopMessage();
            }, mainForm, chatUi, appId, appSecret
        );
    }

    frmFeishu.doModal();
}

if(feishuConfig.autoConnect && feishuConfig.appId && feishuConfig.appSecret ){
    mainForm.btnFeishu.checked = true;

    thread.invoke(
        function(mainForm, chatUi, appId, appSecret){
            import win.ui;
            import autos.feishuClient;

            mainForm.showMessage("✅ 飞书机器人正在连接","loading");

            var client = autos.feishuClient(mainForm, chatUi, appId, appSecret);
            mainForm.feishuClientInstance = client;

            client.start();
            win.loopMessage();
        }, mainForm, chatUi, feishuConfig.appId, feishuConfig.appSecret
    );  
}

var edgeTextToSpeechTask,edgeTextToSpeechPlayer;
mainForm.onDestroy = function(){

    if(mainForm.feishuClientInstance){
        mainForm.feishuClientInstance.stop();
        mainForm.feishuClientInstance = null;
    }
    feishuClient = null;

    if(weixinListeningThread){
        ..thread.terminate(weixinListeningThread,0)
        ..raw.closeHandle(weixinListeningThread);
        weixinListeningThread = null;
    }

    eventStop.set();
    if(chatUi.aiChatThread){
        if(!..thread.wait(chatUi.aiChatThread,200)){
            ..thread.terminate(chatUi.aiChatThread,0);
        }
        ..raw.closeHandle(chatUi.aiChatThread);
        chatUi.aiChatThread = null;
    }

    mainForm.saveSessionData();

    if(edgeTextToSpeechTask){
        edgeTextToSpeechTask.cancel(); 
        edgeTextToSpeechTask = null;
    }

    if(edgeTextToSpeechPlayer){
        edgeTextToSpeechPlayer.close(); 
        edgeTextToSpeechPlayer = null;
    }

}

chatUi.importChatMessagesFromFile = function(path){

    var json = string.load(path);
    if(!#json){
        return mainForm.msgboxErr("无效对话数据")
    }

    var messages;

    if(..string.endsWith(path,".jsonl",true)){
        messages = JSON.ndParse(json);
    }
    else {
        messages = JSON.tryParse(json);
    }


    if(!#messages){
        return mainForm.msgboxErr("无效对话数据")
    }   

    chatUi.importChatMessages(messages)
}

chatUi.importChatMessages = function(messages){
    mainForm.cancelRetry();

    mainForm.btnClear.disabled = true;
    mainForm.btnSend.disabled = true;
    mainForm.btnSetting.disabled = true;
    mainForm.showMessage("✅ 正在刷新主记忆 + 恢复会话 ……");

    resetMessages(true);

    var last = messages[#messages];
    if((last[["role"]]=="assistant") && (!#last.content)){ 
        table.pop(messages);
    }

    for(i=1;#messages;1){
        var msg = messages[i];

        if(type(msg.content)!="string"){

            if(msg[["role"]]!="user"){
                //resetMessages();
                break;
            }
            elseif(!..table.isArrayLike(msg.content)){
                //resetMessages();
                continue;
            }
        } 

        if(msg[["role"]]=="system"){
            if(i<= #chatUi.chatMessages){
                continue;
            }
            chatUi.system(msg.content)
        }
        elseif(msg[["role"]]=="user"){
            if(i<#messages){
                chatUi.promptOnce(msg.content)
            }
            else{
                mainForm.editPrompt.text = msg.content;
            }
        }
        elseif(msg[["role"]]=="assistant"){

            var content = msg.content;  
            if(#content) chatUi.assistantOnce(content);
        } 
    }   

    mainForm.saveSessionData();

    mainForm.showMessage(false);

    mainForm.btnClear.disabled = false;
    mainForm.btnSend.disabled = false;
    mainForm.btnSetting.disabled = false;   
}

mainForm.createHbitmapFromAttachment = function(){
    var bmp = mainForm.btnImageAttachment.foreground;
    if(type(bmp)==="table") return bmp.copyHandle();
}

mainForm.btnImageAttachment.onRightMouseUp = function(wParam,lParam){
    var oldBmp = mainForm.btnImageAttachment.setForeground(null);
    if(oldBmp) oldBmp.delete();
    mainForm.btnImageAttachment.iconText = '\uF030';
}

mainForm.btnImageAttachment.oncommand = function(id,event){
    mainForm.cancelRetry();
    mainForm.show(0);
    thread.delay(100);

    import gdip.snap;
    import mouse.screenArea;
    var screenArea = mouse.screenArea();
    screenArea.onSelectionChanged = function(rc){
        mainForm.btnImageAttachment.iconText = null;

        mainForm.btnImageAttachment.newImageAttached = true;    
        var oldBmp = mainForm.btnImageAttachment.setForeground( (gdip.snap(screenArea.hwnd,rc)) );
        if(oldBmp) oldBmp.delete();

        owner.endModal(true);

        mainForm.editPrompt.setFocus();
    }

    screenArea.doModal();

    mainForm.show(5/*_SW_SHOW*/);
}

mainForm.customChat.translateAccelerator = function(msg){
    if (   (msg.message == 0x100/*_WM_KEYDOWN*/) || (msg.message == 0x101/*_WM_KEYUP*/) ) {
        var vk = msg.wParam;
        if( vk == 0x74/*_VK_F5*/ ){
            import ide;
            ide.run();
            return true;   
        }

        if(( vk == 'N'# ) && ( ( ::GetKeyState(0x11/*_VK_CTRL*/) & 0x8000 ) == 0x8000 )) 
        {
            return true;   
        } 
    }  
    return chatUi._host.tranacc(msg)
}  

import win.ui.accelerator;
var accelerator = win.ui.accelerator([
    { 
        vkey = 0x74/*_VK_F5*/; 
        oncommand = function() {import ide;ide.run(); }
    };  
],mainForm );

import web.form.menu;
chatUi.showMenu = function(x,y,id,ele){

    if(id!==4/*_CONTEXT_MENU_TEXTSELECT */){
        return true;
    }

    var range = chatUi.script.getSelectionObject();
    if(!range) return;

    var text = range.text;
    text = string.trim(text);
    if(!#text) return true;

    var workpath;
    if(string.find(text,"^~?[/\\]![^/\\]\N+?\S$")
        || string.find(text,"^\a\N+?\S<[/\\]>|<\.\a+>$")){
        workpath = io.exists(io.joinpath2( agentWorkspace,text ));

        if(workpath){
            web.form.menu( chatUi, {
                [4/*_CONTEXT_MENU_TEXTSELECT */] = {  

                    { "打开 ...";function(){
                        raw.execute(workpath);
                    }}; 
                    { "浏览文件 ...";function(){
                        raw.explore(workpath,"/select");
                    }}; 
                    { fsys.isDir(workpath)?"复制目录到 ...":"复制文件到 ...";function(){
                        import win.clip.file;
                        win.clip.file.write(workpath)
                    }};
                    { "复制完整路径";function(){
                        import win.clip;
                        win.clip.write(workpath)
                    }}; 
                    { "复制";"Copy" }; 
                }
            } ).popup(x,y,id,ele);  
            return false; 
        }
    }

    with string{ latinLike = lambda(str) !find(str,":") || (len(str) > #str/1.5);}


    import web.rest.jsonClient; 
    var edgeClient = web.rest.jsonClient();
    var edgeApi = edgeClient.api("https://edge.microsoft.com");

    import web.rest.aiChat;
    var aiClient = web.rest.aiChat(
        url = "https://index-translate.bilibili.com/v1";
        model = "Index-Translate-35B-A3B"
    )

    var translate = function(str){
        // 取选区时 str 中的换行会被抹去
        str = ..string.replace(str,"![\*]\*\*![^\*](\N+?)![\*]\*\*![^\*]",'\n\\1\n');

        var result,err = aiClient.getText('请将 AI Agent 输出的以下英文翻译为适合语音朗读的中文(直接输出翻译结果,不要进行任何解释。在中文中按惯例不必翻译的内容〔 例如文件路径,文件后缀名,编程时使用的库名、模块名、变量名,在中文中惯例不翻译的术语等 〕请保持原状):\n\n'+str);    
        if(result){
            return result; 
        }

        var ret, err = edgeApi.translate.translatetext.post([str], {
            from="";
            isEnterpriseClient="false";
            to="zh-Hans"
        }); 

        if(ret[[1]]){
            ret = table.map(ret,lambda(v,k) v[["translations"]][[1]][["text"]]) 
            return ret[1]
        }

        return null,err;
    }

    var menuItems = [ { "复制";"Copy" };{}] ;
    if(string.latinLike(text)){
        table.push(menuItems,{ "翻译为中文";function(id,hwnd,ele,x,y){

                var result, err = translate(text);
                if(result){ 
                    range.text = result;
                }
                elseif(err){
                    mainForm.msgboxErr(er);
                }
            } });       
    }
    else{
        translate = lambda (str)str;
    }

    table.push(menuItems,{ "大声朗读(自动翻译)";
        function(id,hwnd,ele,x,y){ 
            var text = translate(text);

            if(edgeTextToSpeechTask){
                edgeTextToSpeechTask.cancel(); 
            }

            if(edgeTextToSpeechPlayer){
                edgeTextToSpeechPlayer.close();
            }

            import sys.mediaFoundation.streamingMp3Player;
            edgeTextToSpeechPlayer,err=sys.mediaFoundation.streamingMp3Player();
            if(!edgeTextToSpeechPlayer) return mainForm.msgboxErr(err);

            mainForm.iconVoice.visible = true;
            mainForm.iconVoice.disabledText =  ['\uF026','\uF027','\uF028'];
            edgeTextToSpeechPlayer.onEnd = function(){
                mainForm.iconVoice.visible = false;
                mainForm.iconVoice.disabledText = null;
            }

            import web.edgeTextToSpeech;
            var engine=web.edgeTextToSpeech({voice="zh-CN-XiaoxiaoNeural";rate=1});
            edgeTextToSpeechTask,err=engine.synthesize({text=text},edgeTextToSpeechPlayer);

            if(!edgeTextToSpeechTask){
                edgeTextToSpeechPlayer.close();
                edgeTextToSpeechPlayer = null;

                return mainForm.msgboxErr(err.message);
            }
        } }
    );


    web.form.menu( chatUi, { [4/*_CONTEXT_MENU_TEXTSELECT */] = menuItems } ).popup(x,y,id,ele);    

    return false; 
}
mainForm.iconVoice.orphanWindow(true);

mainForm.btnSend.disabled = true;   

do{

    import fsys;
    var sessions = [];
    var dir = io.appData("aardio/autos/~checkpoint/");

    if(io.exists(dir)){
        for i,filename,fullpath in fsys.each(dir,,"*.session","file"){
            var bin = string.load(fullpath);
            if(!#bin){
                io.remove(fullpath); // 清理空文件
                continue;
            }

            var ok,ses = call(eval,,bin);
            if(!(ok && type(ses)=="table" && ses.sessionId)){
                io.remove(fullpath); // 清理损坏无法解析的孤儿文件
                continue;
            }

            // 如果所属进程仍在运行,跳过
            if(process.mutex.exists(ses.sessionId)){
                continue;
            }

            // 所属进程已退出,但会话没有有效消息,直接清理
            if(!#ses.messages){
                io.remove(fullpath);
                continue;
            }

            if(#sessions == 0){
                // 第 1 个无主会话:立即锁定当前会话 Mutex,并原子移除原文件防多进程抢占
                if(!sessionMutex) sessionMutex = process.mutex(sessionId);

                // 若原文件恰好与当前 hwnd 同名,或成功删除原文件,则抢占成功
                if( (fullpath == sessionCheckpointPath) || io.remove(fullpath) ){
                    ses.path = fullpath;
                    table.push(sessions,ses);
                }
            }
            else {
                // 发现第 2 个无主会话:仅标记需要拉起新进程,不删除文件留给下一个进程抢占
                table.push(sessions,ses);
                break;
            }
        }       
    }

    if(#sessions){

        if(#sessions[1].memoryFile){
            config.memoryFile = sessions[1].memoryFile; //切换到绑定当前会话的主记忆文件名
        }

        io.remove(sessions[1].path);

        //使用副本渲染界面,保护原思维链数组不被污染(importChatMessages 会改动 messages )。
        chatUi.importChatMessages(table.clone(sessions[1].messages)) 

        if(sessions[1].chainOfThought){
            ..thread.set("resumeChainOfThought",sessions[1].messages);
            ..thread.set("resumeChainOfThoughtProtocol",sessions[1].protocol);
            mainForm.saveSessionData(true); 

            mainForm.btnSend.text = "继续";

            chatUi.assistantOnce();
            mainForm.showMessage("已恢复未完成的内存思维链(页面不显示),请点击「继续」恢复运行","error",true);
        }
        else{
            mainForm.saveSessionData(); 
        }

        if(sessions[2]){
            import ide;
            ide.createProcess("~/examples/AI/autos.aardio");
        }

        break; 
    }

    resetMessages();

    if(mainForm.editPrompt.hwnd!=win.getFocus()){
        mainForm.editPrompt.text = defaultPrompt
    }
} while(0); 

mainForm.btnSend.disabled = false;

win.loopMessage();
Markdown 格式