aardio 文档

aardio 范例: 扫码传文件 - v2.8 (导航优化版)

//扫码传文件 - v2.8 (导航优化版)
import fsys;
import fsys.info;
import fsys.config;
import fsys.dlg.dir;
import fsys.update.simpleMain;
import process;
import web.socket.server;
import win.ui.simpleWindow2;
import qrencode.bitmap;
import win.ui.menu;
import inet.url;
import string.html;
import fonts.fontAwesome;
import win.ui;
import win.clip;
import win.clip.viewer;
/*DSG{{*/
var winform = win.form(text="qrfs - 扫码快传 v2.8";right=798;bottom=491;bgcolor=0xFAFAFA;border="none";max=false)
winform.add(
bk={cls="bk";left=0;top=-5;right=799;bottom=29;bgcolor=0xF0F0F0;dl=1;dr=1;dt=1;forecolor=0xEAEAEA;linearGradient=0;z=10};
bkplus={cls="bkplus";text="qrfs - 扫码快传  v2.8 - 共享文件、电脑剪贴板、手机输入法( AI 自动优化 )";left=35;top=3;right=509;bottom=25;align="left";color=0x5A5A5A;dl=1;dt=1;z=11};
btnOpen={cls="plus";text='\uF115';left=430;top=44;right=456;bottom=68;align="right";border={bottom=1;color=0xFF808080};dr=1;dt=1;font=LOGFONT(h=-16;name='FontAwesome');notify=1;z=1};
btnOpenUpload={cls="plus";text="上传目录";left=473;top=40;right=614;bottom=69;align="left";dl=1;dt=1;font=LOGFONT(h=-14);iconStyle={align="left";font=LOGFONT(h=-15;name='FontAwesome');padding={left=8;top=2}};iconText='\uF07C';notify=1;textPadding={left=27;bottom=1};z=12};
btnSetting={cls="plus";left=737;top=455;right=768;bottom=484;bgcolor=0xFAFAFA;db=1;dl=1;font=LOGFONT(h=-16;name='FontAwesome');iconStyle={align="left";font=LOGFONT(h=-16;name='FontAwesome');padding={left=7;top=3}};iconText='\uF013';notify=1;z=23};
btnStart={cls="plus";text="重启服务";left=635;top=69;right=768;bottom=106;align="left";bgcolor=0xD9A100;border={radius=8};color=0xFFFFFF;dr=1;dt=1;font=LOGFONT(h=-15);iconStyle={align="left";font=LOGFONT(h=-19;name='FontAwesome');padding={left=16}};iconText='\uF233';notify=1;textPadding={left=45;bottom=2};z=6};
chkAiOpt={cls="checkbox";text="AI 优化";left=642;top=461;right=712;bottom=483;bgcolor=0xFAFAFA;z=22};
chkPublicAddress={cls="checkbox";text="公网 IP";left=486;top=109;right=556;bottom=131;bgcolor=0xFFFFFF;z=24};
editDocumentRoot={cls="plus";left=131;top=46;right=430;bottom=68;align="left";border={bottom=1;color=0xFFC0C0C0};clip=1;dl=1;dr=1;dt=1;editable="edit";textPadding={bottom=1};z=8};
editHost={cls="plus";left=131;top=75;right=456;bottom=97;align="left";border={bottom=1;color=0xFFC0C0C0};dr=1;dt=1;editable="edit";textPadding={bottom=1};z=19};
editPassword={cls="plus";left=131;top=107;right=456;bottom=129;align="left";border={bottom=1;color=0xFFC0C0C0};dr=1;dt=1;editable="edit";password=1;textPadding={right=24;bottom=1};z=14};
editPort={cls="plus";left=529;top=78;right=615;bottom=100;align="left";border={bottom=1;color=0xFFC0C0C0};dr=1;dt=1;editable=1;notify=1;textPadding={bottom=1};z=9};
plus={cls="plus";text="密码:";left=71;top=108;right=129;bottom=132;align="right";dl=1;dr=1;dt=1;font=LOGFONT(h=-14);transparent=1;z=13};
plus2={cls="plus";left=38;top=148;right=455;bottom=442;align="left";border={color=0xFFC0C0C0;radius=8;width=1};db=1;dl=1;dr=1;dt=1;font=LOGFONT(h=-14);textPadding={left=16};valign="top";z=2};
plus3={cls="plus";text="自定义网址:";left=9;top=77;right=129;bottom=101;align="right";dr=1;dt=1;font=LOGFONT(h=-14);transparent=1;z=20};
qr={cls="plus";left=483;top=148;right=768;bottom=433;border={radius=8};db=1;dr=1;dt=1;foreRepeat="scale";repeat="scale";z=7};
radioQrClipboard={cls="radiobutton";text="共享剪贴板";left=442;top=461;right=529;bottom=483;bgcolor=0xFAFAFA;db=1;dr=1;z=18};
radioQrInputMethod={cls="radiobutton";text="共享输入法";left=541;top=461;right=637;bottom=483;bgcolor=0xFAFAFA;db=1;dr=1;z=21};
radioQrRootDir={cls="radiobutton";text="根目录";left=259;top=461;right=346;bottom=483;bgcolor=0xFAFAFA;db=1;dr=1;z=16};
radioQrUploadDir={cls="radiobutton";text="上传文件";left=350;top=461;right=437;bottom=483;bgcolor=0xFAFAFA;db=1;dr=1;z=17};
static={cls="plus";text="端口:";left=467;top=77;right=530;bottom=101;align="right";dr=1;dt=1;font=LOGFONT(h=-14);transparent=1;z=4};
static2={cls="plus";text="网站根目录:";left=15;top=45;right=129;bottom=69;align="right";dl=1;dt=1;font=LOGFONT(h=-14);transparent=1;z=5};
syslink={cls="syslink";text='<a href="https://github.com/aardio/qrfs">开源项目</a>';left=43;top=463;right=176;bottom=483;bgcolor=0xFAFAFA;db=1;dl=1;z=15};
txtMessage={cls="richedit";left=43;top=151;right=451;bottom=436;autohscroll=false;bgcolor=0xFAFAFA;db=1;dl=1;dr=1;dt=1;link=1;multiline=1;vscroll=1;z=3}
)
/*}}*/

if( fsys.update.simpleMain(
    "qrfs - 扫码快传",
    "http://d.aardio.com/qrfs/update/",
    io.appData("/aardio/std/qrfs/app/update"),
    function(version,description,status){})){
    return 0;   
}

config = fsys.config(io.appData("aardio/std/qrfs")); 

table.mix(config.aiOpt,{
    key =   '\0\1\96';
    url = "https://ai.aardio.com/api/v1/";
    model = "prompt";   
    systemPrompt = /*你是语音输入法优化助手。

# 任务

用户输入的文本是使用语音输入法输入的的语音识别结果。
你的任务是在评估与尊重用户原意的基础上对识别结果进行评估与修复,纠正语音识别错误。
在保持与尊重用户原意的基础上修正错误的文法与不通顺之处,并合理使用标点符号。
然后在在保持与尊重用户原意的基础上,你应当评估输入并根据相关的文化与背景知识对识别结果的格式进行整理与优化,使文本更为通顺自然。
输出格式为纯文本,不使用 Markdown 标记(除非识别结果本身包含 Markdown 标记)。
*/
});

import sessionHandler.default;
sessionHandler.default.root = io.appData("aardio/std/qrfs/session")

if( #config.winform.documentRoot && io.exist(config.winform.documentRoot) ){
    winform.editDocumentRoot.text = config.winform.documentRoot;
}
else {
    winform.editDocumentRoot.text = io.getSpecial(0x5/*_CSIDL_MYDOCUMENTS*/)
}

if(config.winform.qrDir=="upload"){
    winform.radioQrUploadDir.checked = true; 
}
elseif(config.winform.qrDir=="clipboard"){
    winform.radioQrClipboard.checked = true;
}
elseif(config.winform.qrDir=="input"){
    winform.radioQrInputMethod.checked = true;
}
else {
    winform.radioQrRootDir.checked = true;
} 
winform.editHost.text = config.winform.editHost;

var wsrv = web.socket.server();
var srvHttp = wsrv.httpServer;
srvHttp.documentRoot = winform.editDocumentRoot.text;
srvHttp.userToken = string.random(18);
winform.editPassword.text = srvHttp.userToken;

var cacheSysIcons = {}
var getSysIconIndex = function(path){ 
    var sfi;
    sfi = fsys.info.get(path, 0x100/*_SHGFI_ICON*/ | 0x4000/*_SHGFI_SYSICONINDEX*/|0x10/*_SHGFI_USEFILEATTRIBUTES*/, 
        ..fsys.isDir(path)?0x10/*_FILE_ATTRIBUTE_DIRECTORY*/?0x80/*_FILE_ATTRIBUTE_NORMAL*/);

    if( !(sfi.returnValue ) ) return; 

    if(!cacheSysIcons[sfi.iIcon]){
        var bmp = ..gdip.bitmap(sfi.hIcon,1/*_IMAGE_ICON*/);
        if(bmp){
            cacheSysIcons[sfi.iIcon] = bmp.saveToBuffer(".png"); 
            bmp.dispose();
        }
    }
    if(sfi.hIcon)::DestroyIcon(sfi.hIcon);  
    return sfi.iIcon;
}

var formatSize = function(size){
    if(size===null) return "";
    if(size < 1024) return size + " B";
    if(size < 1024*1024) return string.format("%.1f KB", size/1024);
    if(size < 1024*1024*1024) return string.format("%.1f MB", size/(1024*1024));
    return string.format("%.1f GB", size/(1024*1024*1024));
}

var cacheClientIps = {}

// 生成通用 HTML 头部
var getHtmlHead = function(activeTab){
    return `
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>qrfs - 扫码快传</title>
<link href="https://lib.baomitu.com/filepond/4.28.2/filepond.min.css" rel="stylesheet">
<script src="https://lib.baomitu.com/filepond/4.28.2/filepond.min.js"></script> 

<style>
:root {
    --bg-color: #f7f9fc;
    --card-bg: #ffffff;
    --text-primary: #2c3e50;
    --text-secondary: #5e6d82;
    --border-color: #ebEEF5;
    --accent-color: #0366d6;
    --accent-hover: #024ea4;
    --danger-color: #ff4757;
    --upload-area-bg: #eef2f7;
    --shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
    --nav-height: 50px;
}

@media (prefers-color-scheme: dark) {
    :root {
        --bg-color: #121212;
        --card-bg: #1e1e1e;
        --text-primary: #e0e0e0;
        --text-secondary: #a0a0a0;
        --border-color: #333;
        --accent-color: #58a6ff;
        --accent-hover: #79c0ff;
        --danger-color: #ff6b6b;
        --upload-area-bg: #2d2d2d;
        --shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.2);
    }
}

*, *::before, *::after { box-sizing: border-box; }

body {
    margin: 0;
    padding: 0;
    padding-top: calc(var(--nav-height) + max(10px, env(safe-area-inset-top))); 
    padding-bottom: max(20px, env(safe-area-inset-bottom));
    background-color: var(--bg-color);
    color: var(--text-primary);
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    -webkit-font-smoothing: antialiased;
}

/* 顶部导航栏 */
.nav-header {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 100;
    background: rgba(255, 255, 255, 0.95);
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
    border-bottom: 1px solid var(--border-color);
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
    padding-top: max(0px, env(safe-area-inset-top));
}

@media (prefers-color-scheme: dark) {
    .nav-header { background: rgba(30, 30, 30, 0.95); }
}

.nav-tabs {
    display: flex;
    height: var(--nav-height);
    padding: 0;
    margin: 0;
    list-style: none;
}

.nav-tabs li {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
}

.nav-tabs a {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 100%;
    text-decoration: none;
    color: var(--text-secondary);
    font-size: 0.85rem;
    font-weight: 500;
    transition: all 0.2s;
    border-bottom: 2px solid transparent;
    gap: 2px;
}

.nav-tabs a .nav-icon {
    font-size: 1.2rem;
}

.nav-tabs a:hover {
    color: var(--accent-color);
    background: rgba(3, 102, 214, 0.05);
}

.nav-tabs a.active {
    color: var(--accent-color);
    border-bottom-color: var(--accent-color);
    background: rgba(3, 102, 214, 0.08);
}

/* 上传区域样式 */
.upload-section {
    padding: 15px;
    margin-bottom: 10px;
}

.filepond--root { margin-bottom: 0; min-height: 120px !important; }
.filepond--panel-root { background-color: var(--card-bg); border: 2px dashed var(--border-color); border-radius: 12px; }
.filepond--drop-label { color: var(--text-secondary); font-size: 1rem; }

.native-upload {
    display: block; width: 100%; padding: 15px;
    background: var(--card-bg); border: 2px dashed var(--border-color);
    border-radius: 12px; color: var(--text-secondary); text-align: center;
}

.page-title {
    font-size: 1.1rem;
    margin: 15px 15px 15px 15px;
    color: var(--text-secondary);
    font-weight: 600;
}

ul.file-list {
    list-style: none; padding: 0 15px; margin: 0;
    display: flex; flex-direction: column; gap: 10px;
}

ul.file-list li {
    background: var(--card-bg); border-radius: 10px;
    box-shadow: var(--shadow);
    display: flex; align-items: center; overflow: hidden;
    transition: transform 0.1s;
}
ul.file-list li:active { transform: scale(0.99); }

ul.file-list li a {
    flex: 1; display: flex; align-items: center;
    padding: 16px; text-decoration: none;
    color: var(--text-primary); font-size: 1rem; overflow: hidden;
}

ul.file-list li img { width: 24px; height: 24px; margin-right: 12px; object-fit: contain; flex-shrink: 0; }
ul.file-list li span.fname { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1; margin-right: 10px; }
ul.file-list li span.fsize { font-size: 0.8rem; color: var(--text-secondary); white-space: nowrap; }

.del-btn {
    background-color: var(--danger-color); color: white;
    border: none; padding: 0 16px; height: auto;
    font-weight: 600; font-size: 0.9rem; cursor: pointer;
    align-self: stretch; display: flex; align-items: center;
    margin-left: 1px;
}

/* 功能容器 */
.func-container {
    padding: 0 15px; 
    display: flex; 
    flex-direction: column; 
    height: calc(100vh - var(--nav-height) - max(10px, env(safe-area-inset-top)) - 50px - max(20px, env(safe-area-inset-bottom)));
    height: calc(100dvh - var(--nav-height) - max(10px, env(safe-area-inset-top)) - 50px - max(20px, env(safe-area-inset-bottom)));
}

.func-content {
    flex: 1; width: 100%; padding: 15px;
    border: 1px solid var(--border-color); border-radius: 12px;
    font-size: 1.1em; background-color: var(--card-bg); color: var(--text-primary);
    resize: none; line-height: 1.6; outline: none; margin-bottom: 15px;
    overflow: auto;
}
.func-content:focus { border-color: var(--accent-color); box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.1); }

.btn-group { display: flex; gap: 10px; flex-wrap: wrap; }
.btn-main {
    flex: 1; min-width: 120px; padding: 14px; border: none; border-radius: 10px;
    font-size: 1rem; font-weight: 600; cursor: pointer;
    color: white; text-align: center;
}
.btn-copy { background-color: var(--accent-color); }
.btn-send { background-color: #2ecc71; }
.btn-clear { background-color: #95a5a6; }

/* 提示卡片 */
.tip-card {
    background: var(--upload-area-bg);
    border-radius: 8px;
    padding: 10px 12px;
    margin-bottom: 10px;
    color: var(--text-secondary);
    font-size: 0.85rem;
    line-height: 1.5;
}

.tip-card .tip-icon {
    font-size: 1rem;
    margin-right: 6px;
    vertical-align: middle;
}

/* 链接卡片 */
.link-card {
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--card-bg);
    border: 1px solid var(--border-color);
    border-radius: 12px;
    padding: 20px;
    margin: 15px;
    text-decoration: none;
    color: var(--accent-color);
    font-weight: 600;
    box-shadow: var(--shadow);
    transition: all 0.2s;
}

.link-card:hover {
    background: var(--accent-color);
    color: white;
    transform: translateY(-2px);
}

.link-card .link-icon {
    margin-right: 10px;
    font-size: 1.2rem;
}

.toast {
    position: fixed; bottom: 80px; left: 50%; transform: translateX(-50%);
    background: rgba(0,0,0,0.8); color: #fff; padding: 12px 24px;
    border-radius: 25px; font-size: 0.9em; z-index: 9999;
    opacity: 0; transition: opacity 0.3s; pointer-events: none;
}
.toast.show { opacity: 1; }

.empty-tip { text-align: center; padding: 40px; color: var(--text-secondary); }

.success-msg {
    text-align: center;
    padding: 30px;
    color: #2ecc71;
    font-size: 1.1rem;
}

.success-msg .check-icon {
    font-size: 3rem;
    margin-bottom: 15px;
    display: block;
}
</style>
</head>
<body>

<div id="toast" class="toast"></div>

<nav class="nav-header">
    <ul class="nav-tabs">
        <li><a href="/?t=`+srvHttp.userToken+`" class="`+(activeTab=="files"?"active":"")+`"><span class="nav-icon">📁</span><span>文件</span></a></li>
        <li><a href="/%3Cupload%3E/?t=`+srvHttp.userToken+`" class="`+(activeTab=="upload"?"active":"")+`"><span class="nav-icon">📤</span><span>上传</span></a></li>
        <li><a href="/%3Cclipboard%3E/?t=`+srvHttp.userToken+`" class="`+(activeTab=="clipboard"?"active":"")+`"><span class="nav-icon">📋</span><span>剪贴板</span></a></li>
        <li><a href="/%3Cinput%3E/?t=`+srvHttp.userToken+`" class="`+(activeTab=="input"?"active":"")+`"><span class="nav-icon">⌨️</span><span>输入法</span></a></li>
    </ul>
</nav>

<script>
let websocket;
let wsReconnectAttempts = 0;
const wsUrl = "`+wsrv.getUrl("ws",true)+`";

function connectWebSocket() {
    websocket = new WebSocket(wsUrl);
    websocket.onopen = function() { wsReconnectAttempts = 0; };
    websocket.onmessage = function(evt) {
        if (evt.data === "reload") {
            `+(activeTab=="files"?"window.location.reload();":"")+`
        } else {
            const clipboardContent = document.getElementById('clipboardContent');
            if (clipboardContent /*&& document.activeElement !== clipboardContent*/) {
                if(evt.data!=clipboardContent.value ){
                    clipboardContent.value = evt.data;
                    clipboardContent.select();
                } 
            }
        }
    };
    websocket.onclose = function() {
        if (wsReconnectAttempts < 5) {
            const delay = Math.min(1000 * Math.pow(1.5, wsReconnectAttempts), 10000);
            setTimeout(connectWebSocket, delay);
            wsReconnectAttempts++;
        }
    };
}
connectWebSocket();

function showToast(msg) {
    const toast = document.getElementById('toast');
    toast.textContent = msg;
    toast.classList.add('show');
    setTimeout(() => toast.classList.remove('show'), 2000);
}

function deleteFile(path, btn) {
    if(event) { event.preventDefault(); event.stopPropagation(); }
    if (!confirm('确定要删除此文件吗?')) return;

    fetch('/%3Cupload%3E/?t=`+srvHttp.userToken+`', { method: 'DELETE', body: path })
    .then(res => {
        if (res.ok) {
            const li = btn.closest('li');
            li.style.opacity = '0';
            setTimeout(() => {
                li.remove();
                if(document.querySelectorAll('ul.file-list li').length === 0) {
                    document.querySelector('ul.file-list').innerHTML = '<li class="empty-tip">📭 目录为空</li>';
                }
            }, 300);
            showToast('已删除');
        } else { showToast('删除失败'); }
    }).catch(() => showToast('请求失败'));
}
</script>
`;
}

srvHttp.run( 
    function(response,request,session){ 

        response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate";
        response.headers["Pragma"] = "no-cache";
        response.headers["Expires"] = "0";

        var token = request.get["t"] : session["token"];
        if( #srvHttp.userToken && (token != srvHttp.userToken) ){
            winform.txtMessage.printf("客户端:%s 连接被拒绝",request.remoteAddr); 
            response.errorStatus(401)
            return;
        }
        session["token"] = token;

        if(!cacheClientIps[request.remoteAddr]){
            winform.txtMessage.printf("客户端:%s 已连接",request.remoteAddr);   
            cacheClientIps[request.remoteAddr] = true;
        }

        response.headers["Access-Control-Allow-Origin"] = "*";
        response.headers["Access-Control-Allow-Headers"] = "*"

        // 图标请求
        if(request.path=="/main.aardio" && request.get["icon"]){
            var iconIdx = tonumber(request.get["icon"]);
            if(iconIdx!==null && cacheSysIcons[iconIdx]){
                response.contentType = "image/png";
                response.write(cacheSysIcons[iconIdx])
                return;
            }
            response.errorStatus(404);
            return;
        }

        // 上传处理
        if(request.path=="/<upload>/main.aardio"){ //注意请求目录会自动附加默认文档路径 /main.aardio 
            if(request.method=="DELETE"){
                var path = request.postData();
                if(path && string.startsWith(path,"/upload/")){
                    if(string.find(path,"\.\.")) { response.errorStatus(403); return; }

                    path = ..io.joinpath(srvHttp.documentRoot,path)
                    if(io.exist(path)){
                        io.remove(path);
                        winform.txtMessage.print("已删除:" + path);
                        response.close();
                        return; 
                    }
                }
                response.errorStatus(404);
                return;
            }

            fileData = request.postFileData()
            if(fileData){
                io.createDir(..io.joinpath(srvHttp.documentRoot,"upload"))

                var fileName = ..io.joinpath(srvHttp.documentRoot,"upload",fileData.filepond.filename) 
                var ok,err = fileData.filepond.save(fileName); 
                if(!ok){ response.error(err); }

                winform.txtMessage.text = 'HTTP 服务端已启动: \n'; 
                winform.txtMessage.print( srvHttp.getUrl(,true) + "/?t=" + srvHttp.userToken  );
                winform.txtMessage.print( "" );     
                winform.txtMessage.print( "上传成功:" + fileName );   

                response.contentType = "text/plain";
                response.write("/upload/",fileData.filepond.filename)
                return response.close() 
            }       
        }

        // 确定当前页面类型
        var pageType = "files";
        if(request.path=="/<clipboard>/main.aardio") pageType = "clipboard";
        elseif(request.path=="/<input>/main.aardio") pageType = "input";
        elseif(request.path=="/<upload>/main.aardio") pageType = "upload";
        elseif(string.startsWith(request.path,"/upload/")) pageType = "files";

        // 文件浏览处理
        if(pageType=="files"){
            if(!fsys.isDir(request.path)) {
                if( ..io.exist(request.path) && (!_STUDIO_INVOKED || request.path!="/main.aardio") )
                    return response.loadcode(request.path)
                else {
                    request.path = fsys.getParentDir(request.path)
                }
            }
        }

        // 输出 HTML 头部
        response.write(getHtmlHead(pageType));

        // 上传页面
        if(pageType=="upload"){
            response.write(`
<h3 class="page-title">📤 上传文件</h3>

<div class="upload-section">
    <input type="file" class="filepond" name="filepond" multiple>
</div>

<script>
if (typeof FilePond !== 'undefined') {
    FilePond.create(document.querySelector('input[type="file"]'), {
        //这里不能写 %3Eupload%3E,因为 FilePond 会再次进行 URL 编码
        server: '/<upload>/?t=`+srvHttp.userToken+`',
        labelIdle: '点击或拖拽文件到此处上传',
        labelFileProcessingComplete: '上传成功',
        credits: false,
        onprocessfiles: function() {
            showToast('上传完成');
        }
    });
} else {
    const input = document.querySelector('input[type="file"]');
    input.className = 'native-upload';
    input.insertAdjacentHTML('afterend', '<p style="text-align:center;color:var(--text-secondary)">选择文件后自动上传</p>');
}
</script>

<a class="link-card" href="/upload/?t=`+srvHttp.userToken+`">
    <span class="link-icon">📂</span>
    <span>查看已上传的文件</span>
</a>

</body></html>`);
            return;
        }

        // 剪贴板页面
        if(pageType=="clipboard"){
            var clipText = win.clip.read() : "";
            response.write(`
<h3 class="page-title">📋 共享剪贴板</h3>

<div class="func-container">
    <textarea id="clipboardContent" class="func-content" placeholder="修改这里的文本,将同步修改电脑剪贴板...">`
    +string.html.escape(clipText)+`</textarea>

    <div class="tip-card">
        <span class="tip-icon">💡</span> 
        <span id="pasteTip"></span>
    </div>

    <div class="btn-group">
        <button id="copyBtn" class="btn-main btn-copy">📄 复制到手机</button>
    </div>
</div>

<script>
const isMobile = /Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
const isIOS = /iPhone|iPad|iPod/i.test(navigator.userAgent);
const isAndroid = /Android/i.test(navigator.userAgent);

const copyBtn = document.getElementById('copyBtn');

let pasteTip = '使用 Ctrl+V 粘贴内容到输入框';
if (isIOS) {
    pasteTip = '长按输入框 → 选择"粘贴"';
} else if (isAndroid) {
    pasteTip = '长按输入框 → 点击"粘贴"';
} else if (isMobile) {
    pasteTip = '长按输入框进行粘贴';
}
else{
    copyBtn.textContent = "📄 复制到本机"
}

document.getElementById('pasteTip').textContent = pasteTip;

const clipboardContent = document.getElementById('clipboardContent');
clipboardContent.addEventListener('input', (e) => {
    if (websocket && websocket.readyState === WebSocket.OPEN) {
        var data = JSON.stringify({
            event:"clipboard",
            content:e.target.value
        });
        websocket.send(data);
    }
});

copyBtn.addEventListener('click', async function() {
    try {
        await navigator.clipboard.writeText(clipboardContent.value);
        showToast('✓ 已复制到剪贴板');
    } catch (e) {
        clipboardContent.select();
        document.execCommand('copy');
        showToast('✓ 已尝试复制');
    }
});

clipboardContent.select();
</script>
</body></html>`);
            return;
        }

        // 输入法页面
        if(pageType=="input"){
            response.write(`
<h3 class="page-title">⌨️ 共享输入法</h3>

<div class="func-container">
    <textarea id="inputContent" class="func-content" placeholder="在此输入文字,点击发送后将输入到电脑当前焦点位置..."></textarea>

    <div class="btn-group">
        <button id="clearBtn" class="btn-main btn-clear">🗑️ 清空</button>
        <button id="sendBtn" class="btn-main btn-send">📨 发送到电脑</button>
    </div>
</div>

<script>
const isMobile = /Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
if (!isMobile) {
    document.getElementById('sendBtn').textContent = '📨 发送到服务端';
}

const inputContent = document.getElementById('inputContent');

document.getElementById('sendBtn').addEventListener('click', async function() {
    if(!inputContent.value.trim()) {
        showToast('请先输入内容');
        return;
    }
    try {
        var data = JSON.stringify({
            event:"input",
            content:inputContent.value 
        });
        websocket.send(data);
        showToast('✓ 已发送到电脑');
        inputContent.value = "";
        inputContent.focus();
    } catch (e) { 
        showToast('发送失败: ' + e.message);
    }
});

document.getElementById('clearBtn').addEventListener('click', function() {
    inputContent.value = "";
    inputContent.focus();
    showToast('已清空');
});

inputContent.focus();

</script>
</body></html>`);
            return;
        }

        // 文件列表页面
        var pathDisplay = string.html.escape(request.path);
        var isUploadDir = string.startsWith(request.path,"/upload");

        if(request.path=="/") {
            pathDisplay = "📁 全部文件";
        }
        elseif(request.path=="/upload/" || request.path=="/upload") {
            pathDisplay = "📤 已上传的文件";
        }

        response.write(`<h3 class="page-title">` + pathDisplay + `</h3>`);

        // 显示返回上级按钮(非根目录时)
        if(request.path!="/") {
            var parentPath = fsys.getParentDir(request.path);
            if(!#parentPath) parentPath = "/";
            response.write(`<a class="link-card" href="`+parentPath+`?t=`+srvHttp.userToken+`" style="margin-top:0;">
                <span class="link-icon">⬆️</span>
                <span>返回上一级</span>
            </a>`);
        }

        response.write(`<ul class="file-list">`);

        // 根目录显示上传目录入口
        if(request.path=="/" && ..io.exist("/upload/")){
            response.write('<li><a href="/upload/?t='+srvHttp.userToken+'"><img src="/?icon='++getSysIconIndex("/upload/")
                ++'"><span class="fname">📤 已上传的文件</span></a></li>\r\n'); 
        }

        if(!..fsys.isDir(request.path)){
            return response.errorStatus(404); 
        }
        var file,dir = fsys.list(request.path,,"*.*");
        var hasContent = false;

        for(i=1;#dir;1){
            if(dir[i]==="upload" && request.path=="/") continue;
            hasContent = true;
            var iconIdx = getSysIconIndex(dir[dir[i]]);
            response.write('<li><a href="'
                ,inet.url.append(request.path, inet.url.encode(dir[i]))
                ,'?t='+srvHttp.userToken+'"><img src="/?icon='++(iconIdx)++'"><span class="fname">',string.html.escape(dir[i]),'</span></a></li>\r\n');
        }

        for(i=1;#file;1){
            hasContent = true;
            var iconIdx = getSysIconIndex(file[file[i]]);
            var encodedPath = inet.url.append(request.path, inet.url.encode(file[i]));
            var filePath = string.replace(request.path + file[i], "//", "/");
            var fileSize = io.getSize( io.joinpath(srvHttp.documentRoot, request.path, file[i]) );

            response.write('<li><a href="'
                ,encodedPath,'?t='+srvHttp.userToken+'"><img src="/?icon='++(iconIdx)++'"><span class="fname">',string.html.escape(file[i]),'</span>');

            response.write('<span class="fsize">' + formatSize(fileSize) + '</span></a>');

            if(isUploadDir){
                var jsPath = string.replace(filePath,"'","\\'"); 
                response.write('<button class="del-btn" onclick="deleteFile(\''
                    ,jsPath,'\', this)">删除</button>');
            }
            response.write('</li>\r\n');
        }

        if(!hasContent && request.path!="/"){
            response.write('<li class="empty-tip">📭 目录为空</li>');
        }
        elseif(!hasContent && request.path=="/"){
            response.write('<li class="empty-tip">📭 暂无文件,请通过"上传"标签页添加文件</li>');
        }

        response.write("</ul></body></html>")
    }   
);

var serverInfo = function(){
    var ip,port = srvHttp.getLocalIp();
    winform.editPort.text = port;
    winform.editDocumentRoot.text = io.fullpath(srvHttp.documentRoot)

    var home = "/";
    if(winform.radioQrUploadDir.checked){
        home = "/%3Cupload%3E/";
    }
    elseif(winform.radioQrClipboard.checked){
        home = "/%3Cclipboard%3E/";
    }
    elseif(winform.radioQrInputMethod.checked){
        home = "/%3Cinput%3E/";
    }

    var url = srvHttp.getUrl(home,true);

    var customUrl = string.trim(winform.editHost.text);
    if(#customUrl){ 
        if( inet.url.is(customUrl,-1) ){
            url = customUrl;
        }
        elseif(string.startsWith(customUrl,"natapp:",true)){
            url = winform.natAppTunnelUrl;
        } 
        else{
            url = "http://"+customUrl;
        }  
    } 
    elseif(winform.chkPublicAddress.checked){
        var tUrl = inet.url.split(url);

        winform.chkPublicAddress.disabled = true;
        winform.btnStart.disabledText = ['\uF254','\uF251','\uF252','\uF253','\uF250',text='获取 IP']
        var resp = thread.invokeAndWait(
            function(){
                import wsock.udp.stunClient;
                var sutn = wsock.udp.stunClient();
                return sutn.getPublicAddress();
            } 
        )

        winform.chkPublicAddress.disabled = false;
        winform.btnStart.disabledText = null;

        if(resp){
            tUrl.host = resp.ip;
            url = tostring(tUrl);
        }
    }

    var tUrl = inet.url.split(url);
    if(!tUrl){
        return winform.msgboxErr("指定了错误的网址");
    }

    url = tostring( tUrl );

    if(#srvHttp.userToken){
        url = inet.url.appendExtraInfo(url,{
            t = srvHttp.userToken;
        })
    } 

    winform.txtMessage.text = 'HTTP 服务端已启动: \n'; 
    winform.txtMessage.print( url ); 

    var qrBmp = qrencode.bitmap( url );
    winform.qr.setForeground(qrBmp.copyBitmap(winform.qr.width)); 

    if(winform.chkPublicAddress.checked){
        winform.txtMessage.print( 
            "
手机无线连接电脑局域网。
扫码打开网页即可互传文件,共享电脑剪贴板、手机输入法。
拖动文件或目录到窗口网页自动刷新。

基于纯 aardio 代码实现的开源单线程异步 HTTP / WebSocket 双服务端,体积仅数十 KB。"
        );          
    }
    else{
        winform.txtMessage.print( 
            "
手机无线连接电脑局域网。
扫码打开网页即可互传文件,共享电脑剪贴板、手机输入法。
拖动文件或目录到窗口网页自动刷新。

本工具只走内网,不走公网!
端口与密钥动态改变,双重安全保障。

基于纯 aardio 代码实现的开源单线程异步 HTTP / WebSocket 双服务端,体积仅数十 KB。"
        );      
    } 
}

winform.btnStart.oncommand = function(id,event){
    winform.txtMessage.text = "";
    winform.btnStart.disabledText = {'\uF254';'\uF251';'\uF252';'\uF253';'\uF250'}
    thread.delay(500);

    var customUrl = string.trim(winform.editHost.text);
    if(#customUrl){ 
        if(string.startsWith(customUrl,"natapp:",true)){
            if(..natapp){
                ..natapp.terminate()
            }

            var token = string.right(customUrl,-8); 

            import process.natapp;
            ..natapp = process.natapp(token,winform.edi,"INFO")
            if(natapp[["tunnelUrl"]]){
                winform.natAppTunnelUrl = natapp.tunnelUrl;
                winform.editPort.text = 80;
            }
            else{
                winform.btnStart.disabledText = null;
                return winform.msgboxErr("错误的 NATAPP 令牌");
            }
        }

        config.winform.editHost = customUrl; 
    }
    else{
        config.winform.editHost = null;
    }

    config.saveAll();

    var port = tonumber(winform.editPort.text);

    srvHttp.documentRoot = fsys.isDir(winform.editDocumentRoot.text) ? winform.editDocumentRoot.text;
    srvHttp.userToken = winform.editPassword.text;
    srvHttp.start("0.0.0.0",port);
    serverInfo();

    winform.btnStart.disabledText = null;
}

winform.txtMessage.enablePopMenu();
winform.txtMessage.onHyperlink = function(message,href){
    if( message = 0x202/*_WM_LBUTTONUP*/ ) {
        process.openUrl(href);
    }
}

winform.onDropFiles = function(files){
    var path = files[1]
    if(!fsys.isDir(path)){
        path = fsys.getParentDir(path)
    }

    winform.editDocumentRoot.text = path;
    srvHttp.documentRoot = path;
    config.winform.documentRoot = path;
    config.winform.save();

    wsrv.publish("reload");
}

winform.btnOpen.oncommand = function(id,event){
    var dir = fsys.dlg.dir(winform.editDocumentRoot.text,winform)
    if(dir){
        winform.editDocumentRoot.text = dir;
        srvHttp.documentRoot = dir;

        config.winform.documentRoot = dir;
        config.winform.save();
        wsrv.publish("reload");
    }
}

winform.btnOpenUpload.oncommand = function(id,event){
    var path = io.joinpath(winform.editDocumentRoot.text,"upload")
    if(io.createDir(path)){
        process.explore(path)
    }
}

var clipViewer = win.clip.viewer(winform);
clipViewer.onDrawClipboard=function(){
    var str = win.clip.read();  
    //if(str!=wsrv.lastReceivedClipboardData ){
        if(#str) wsrv.publish(str) 
        //wsrv.lastReceivedClipboardData = null;   
    //}
}

import key;
wsrv.onMessage = function(hSocket,msg){
    var data = JSON.tryParse(msg.data)
    if(!data) return;

    if(data.event=="clipboard"){
        wsrv.lastReceivedClipboardData = data.content; 
        win.clip.write(data.content);   
    }
    elseif(data.event=="input"){
        if(!winform.chkAiOpt.checked){
            key.sendString(data.content)
        }
        else{
            thread.invoke( 
                function(winform,aiOpt,text){
                    import key;
                    import web.rest.aiChat;
                    var aiClient = web.rest.aiChat(aiOpt)  

                    var msg = web.rest.aiChat.message();
                    msg.system(aiOpt.systemPrompt) 
                    msg.prompt( "请修复语音识别结果: " + text );

                    import winex.loading;
                    var loading = winex.loading("正在优化",hFocus)

                    var resp,err = aiClient.messages(msg,
                        function(deltaText,reasoning){
                            if(reasoning){ 
                                return loading.thinking(reasoning);
                            }

                            if(loading.isCanceled()){
                                return false;
                            }

                            key.sendString(deltaText) 
                        }
                    );

                },winform,config.aiOpt,data.content
            )
        }
    }
}

var updateHomeDir = function(){
    var home = "/";
    if(winform.radioQrUploadDir.checked){
        home = "/%3Cupload%3E/";
    }
    elseif(winform.radioQrClipboard.checked){
        home = "/%3Cclipboard%3E/";
    }
    elseif(winform.radioQrInputMethod.checked){
        home = "/%3Cinput%3E/";
    } 

    var url = srvHttp.getUrl(home,true);

    if(#srvHttp.userToken){
        url = url ++ "?t=" + srvHttp.userToken;
    }  

    var qrBmp = qrencode.bitmap( url );
    winform.qr.setBackground(qrBmp.copyBitmap(winform.qr.width));   
}

winform.radioQrRootDir.oncommand = function(id,event){
    config.winform.qrDir = "root";
    updateHomeDir();
}

winform.radioQrUploadDir.oncommand = function(id,event){
    config.winform.qrDir = "upload";
    updateHomeDir();    
}

winform.radioQrClipboard.oncommand = function(id,event){
    config.winform.qrDir = "clipboard"; 
    updateHomeDir();
}

winform.radioQrInputMethod.oncommand = function(id,event){
    config.winform.qrDir = "input";
    updateHomeDir();
}

winform.show();  
win.ui.simpleWindow2(winform);

var revealIcon = winform.editPassword.addCtrl(
    cls="plus";
    marginRight=0;marginBottom=2;
    width=24; 
    iconText = '\uF023';
    iconStyle={
        align="right";font=LOGFONT(h=-15;name='FontAwesome');padding={top=3}
    }
)

revealIcon.skin({
    color = {
        default = 0xC0000000;
        hover = 0xFFFF0000;
        active = 0xFF00FF00;
    };
    checked = {
        iconText = '\uF06E';
    }
})

revealIcon.onMouseClick = function(){
    winform.editPassword.editBox.passwordChar = !owner.checked ? "*" : null;
}

winform.btnSetting.oncommand = function(id,event){
    ..table.assign(config.aiOpt,{
        title = "扫码快传 - 设置 AI 优化助手";
    });

    import web.rest.aiChat.settingForm;
    var frmSetting = web.rest.aiChat.settingForm(this,config.aiOpt);

    if( frmSetting.doModal(this) ){
        ..table.assign(config.aiOpt,config.itemData[config.selItem] );
    }
}

winform.chkAiOpt.checked = config.winform.checkAiOpt;
winform.onDestroy = function(){
    config.winform.checkAiOpt = winform.chkAiOpt.checked;
}

winform.editHost.editBox.setCueBannerText("自定义访问域名")

winform.chkPublicAddress.oncommand = function(id,event){

    if(winform.chkPublicAddress.checked){
        import win.version;

        if(win.version.isServer){
            import win.ts; 
            if( win.ts.session.isRemote() ){
                import wsock.tcp.server;
                if( wsock.tcp.server.isFreePort(80)) winform.editPort.text = 80;
            } 
        }   
    }

    winform.btnStart.oncommand();
}

import gdip.imageAttributes;
var imgAttr = gdip.imageAttributes()

//替换前景图像(二维码)颜色
imgAttr.setRemapTable({ 
    0xFF000000; 0xFF00A1D9; // 仅将二维码中的黑色(0xFF000000)替换为绿色(0xFF008000),不替换白色
    0xFFFFFFFF; 0xFFFAFAFA;
}); 
winform.qr.imageAttributes = imgAttr

winform.btnStart.skin( {
    background={
        default=0xFF00A1D9;
        hover=0xFFFF5C65;
        active=0xFF1295C7;
        disabled=0xFFCCCCCC; 
    }
})

winform.btnOpen.skin( {
    background={
        default=0;
        hover=0xFF928BB3;
        disabled=0xFFCCCCCC; 
    }
})

winform.btnOpenUpload.skin( {
    background={
        default=0;
        hover=0xFF928BB3;
        disabled=0xFFCCCCCC; 
    }
})

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

serverInfo();
win.loopMessage();
Markdown 格式