编辑代码

(async () => {
    // 用户名 和 密码
    // 这个随便设置,当用户不存在时会自动注册
    const username = "";
    const password = "";

    // 存储路径
    const storagePath = "/data/storage/client_ip.json";

    /////////////////// 以下代码非必要勿动 ///////////////////

    // 检查参数
    if(!username || !password){
        alert("请填写用户名和密码");
        return;
    }
    if(!storagePath){
        alert("请设置存储路径");
        return;
    }

    // 获取数据
    let data = await getFileContent(storagePath) || "{}";
    data = JSON.parse(data);
    if(data.code && data.code!== 0) data = {};

    // 如果存储的用户名不是当前用户名,则清空旧数据,重新获取
    if(username !== data.username) data = {};

    // api是否发生变化
    let changedApi = false;
    // 是否已监控手机端关于窗口
    let isMonitoring = false;
    // 上次上传时间
    let lastUploadTime = { time: 0 };

    if(isMobile() || username !== data.username) {
        // 加载时执行
        changeClientIpHandler();

        // 监听网络连接状态
        window.addEventListener('online', changeClientIpHandler);
    }

    // 监听网络连接状态
    async function changeClientIpHandler() {
        // 获取当前局域网ip和端口和平台
        const ip = siyuan.config.localIPs.filter(ip=>isPrivateIP(ip))[0] || "";
        if(!ip) return;
        const port = location.port;

        // 写入数据
        data.ip = ip;
        data.port = port;
        await putFileContent(storagePath, JSON.stringify(data));

        // 上传数据
        uploadIpAndPort();
    }

    async function uploadIpAndPort() {
        // 获取token
        if(!data.token){
            const user = await login();
            if(user.success === false) {
                console.log('login failed', user);
                alert(user.message);
                return;
            }
            data.username = username;
            data.token = user.data.token;
            await putFileContent(storagePath, JSON.stringify(data));
        }
        //获取项目id
        if(!data.project_id){
            let project = await getProjectId("siyuan");
            if(project.success === false) {
                console.log('get project failed', project);
                alert(project.message);
                return;
            }
            if(project.data.length === 0) {
                // 创建项目
                const ret = await createProject();
                if(ret.success === false) {
                    console.log('create project failed', ret);
                    alert(ret.message);
                    return;
                }
                // 再次读取刚创建的项目
                project = await getProjectId("siyuan");
                if(project.success === false || project.data.length === 0) {
                    console.log('get project failed', project);
                    alert(project.message);
                    return;
                }
            }
            data.project_id = project.data[0]._id;
            await putFileContent(storagePath, JSON.stringify(data));
        }
        // 获取api id和api url
        if(!data.api_id || !data.api_url){
            let api = await getMocks("/ip");
            if(api.success === false) {
                console.log('get api id failed', api);
                alert(api.message);
                return;
            }
            if(api.data.mocks.length === 0) {
                api = await createApi();
                if(api.success === false) {
                    if(api.message === '请检查接口是否已经存在') {
                        console.log("api has exist");
                    } else {
                        console.log('create api failed', api);
                        alert(api.message);
                        return;
                    }
                }
                api = await getMocks("/ip");
                if(api.success === false) {
                    console.log('get api id failed', api);
                    alert(api.message);
                    return;
                }
            }
            changedApi = true;
            data.api_id = api.data.mocks[0]._id;
            data.api_url = "https://mock.presstime.cn/mock/"+data.project_id+api.data.project.url+"/ip";
            await putFileContent(storagePath, JSON.stringify(data));
        }
        // 更新api
        let result = await updateApi();
        if(!result.success){
            if(result.message.includes("401:")) {
                const user = await login();
                if(user.success === false) {
                    console.log('login failed', user);
                    alert(user.message);
                    return;
                }
                data.token = user.data.token;
                await putFileContent(storagePath, JSON.stringify(data));
                result = await updateApi();
                if(!result.success){
                    console.log('update api failed', result);
                    alert(result.message);
                    return;
                }
            } else {
                console.log('update api failed', result);
                alert(result.message);
                return;
            }
        }
        lastUploadTime.time = new Date().getTime();
        console.log('update ip success', data.ip, data.port);

        //生成html
        generateHTML();

        // 开始监控手机端关于窗口事件
        monitorMobileAboutWindow();
    }

    async function login() {
        const result = await apiRequest("u/login", {
            "name": username,
            "password": password
        });
        if(!result.success){
            if(result.message === '用户不存在'){
                const registerRet = await register();
                if(!registerRet.success){
                    return registerRet;
                }
                return await login();
            } else {
                return result;
            }
        }
        return result;
    }

    async function register() {
        const result = await apiRequest("u/register", {
            "name": username,
            "password": password
        });
        return result;
    }

    async function createProject() {
        const result = await apiRequest("project/create", {
            "id": "",
            "name": "siyuan",
            "group": "",
            "swagger_url": "",
            "description": "siyuan api(用于动态获取手机端ip,请勿删除)",
            "url": "/api",
            "members": []
        });
        return result;
    }

    async function getProjectId(keywords='') {
        const result = await apiRequest("project?page_size=30&page_index=1&keywords="+keywords+"&type=&group=&filter_by_author=0", {}, 'GET');
        return result;
    }

    async function getMocks(filterApi) {
        const result = await apiRequest("mock?project_id="+data.project_id+"&page_size=2000&page_index=1&keywords=", {}, 'GET');
        if(!result.success){
            return result;
        }
        if(filterApi) {
            result.data.mocks = result.data.mocks.filter(mock => {
                return mock.url === filterApi;
            });
        }
        return result;
    }

    async function createApi(ip, port) {
        const result = await apiRequest('mock/create', {
            "url": "/ip",
            "mode": "{\n  \"data\": {ip:\""+data.ip+"\", port:\""+data.port+"\"}\n}",
            "method": "get",
            "description": "get ip(用于动态获取手机端ip,请勿删除)",
            "project_id": data.project_id
        });
        return result;
    }

    async function updateApi() {
        const result = await apiRequest('mock/update', {
            "url": "/ip",
            "mode": "{\n  \"data\": {ip:\""+data.ip+"\", port:\""+data.port+"\"}\n}",
            "method": "get",
            "description": "get ip(用于动态获取手机端ip,请勿删除)",
            "id": data.api_id
        });
        return result;
    }

    async function apiRequest(url, payload, method) {
        method = (method || 'POST').toLocaleUpperCase();
        var myHeaders = new Headers();
        myHeaders.append("Authorization", "Bearer " + (data.token || ""));
        myHeaders.append("Pragma", "no-cache");
        myHeaders.append("Priority", "u=1, i");
        myHeaders.append("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36");
        myHeaders.append("Cookie", "easy-mock_token="+ (data.token || ""));
        myHeaders.append("Content-Type", "application/json;charset=UTF-8");
        var requestOptions = {
            method: method || 'POST',
            headers: myHeaders,
            redirect: 'follow'
        };
        if(method === 'POST') {
            var raw = JSON.stringify(payload||{});
            requestOptions["body"] = raw;
        }
        try {
            const response = await fetch("https://mock.presstime.cn/api/" + url.replace(/^\/+/, ''), requestOptions);
            // 检查响应状态码是否为2xx
            if (!response.ok) {
                const errorData = await response.text();
                console.error(`Error: ${response.status} ${response.statusText}`, errorData);
                throw new Error(`Request failed with status ${response.status}: ${errorData.message}`);
            }
            const result = await response.json();
            return result;
        } catch (error) {
            console.error('Failed to fetch:', error);
            return {code:-1, success:false, message:error.message, data:null};
        }
    }

    // 写入文件内容
    async function putFileContent(path, content) {
        const formData = new FormData();
        formData.append("path", path);
        formData.append("file", new Blob([content]));
        return fetch("/api/file/putFile", {
            method: "POST",
            body: formData,
        })
        .then((response) => {
            if (response.ok) {
                //console.log("File saved successfully");
            }
            else {
                throw new Error("Failed to save file");
            }
        })
        .catch((error) => {
            console.error(error);
        });
    }

    // 获取文件内容
    async function getFileContent(path) {
        return fetch("/api/file/getFile", {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
            },
            body: JSON.stringify({
                path,
            }),
        })
        .then((response) => {
            if (response.ok) {
                return response.text();
            }
            else {
                throw new Error("Failed to get file content");
            }
        })
        .catch((error) => {
            console.error(error);
        });
    }

    // 获取平台 返回 "windows" | "linux" | "darwin" | "docker" | "android" | "ios"
    function getPlatform() {
        if (["docker", "ios", "android"].includes(window.siyuan.config.system.container)) {
            return window.siyuan.config.system.container;
        } else {
            return window.siyuan.config.system.os;
        }
    }

    function isMobile() {
        return getPlatform() === "ios" || getPlatform() === "android";
    }

    // 判断是否局域网ip
    function isPrivateIP(ip) {
        const ipParts = ip.split('.');
        const firstOctet = parseInt(ipParts[0], 10);

        // 检查10.0.0.0/8
        if (firstOctet === 10) {
          return true;
        }

        // 检查172.16.0.0/12
        if (firstOctet === 172) {
          const secondOctet = parseInt(ipParts[1], 10);
          if (secondOctet >= 16 && secondOctet <= 31) {
            return true;
          }
        }

        // 检查192.168.0.0/16
        if (firstOctet === 192 && parseInt(ipParts[1], 10) === 168) {
          return true;
        }

        // 检查169.254.0.0/16
        if (firstOctet === 169 && parseInt(ipParts[1], 10) === 254) {
          return true;
        }

        // 如果都不匹配,则不是私有IP
        return false;
    }

    // 调用是咧
    //observeLastUploadTimeChange((newValue, oldValue) => {console.log(newValue, oldValue)}, lastUploadTime.time)
    function observeLastUploadTimeChange(callback, oldValue, delay) {
      delay = delay ||30000;
      const timer = setTimeout(() => {
        callback = null;
      }, delay);
      Object.defineProperty(lastUploadTime, 'time', {
        get() {
          return time;
        },
        set(newValue) {
          time = newValue;
          if (oldValue !== newValue && callback) {
            callback(newValue, oldValue);
            callback = null;
            if(timer) clearTimeout(timer);
          }
        }
      });
    }

    function showMessage(message, delay) {
        fetchSyncPost("/api/notification/pushMsg", {
          "msg": message,
          "timeout": delay || 7000
        });
    }

    async function fetchSyncPost (url, data) {
        const init = {
            method: "POST",
        };
        if (data) {
            if (data instanceof FormData) {
                init.body = data;
            } else {
                init.body = JSON.stringify(data);
            }
        }
        try {
            const res = await fetch(url, init);
            const res2 = await res.json();
            return res2;
        } catch(e) {
            console.log(e)
            return [];
        }
    }

    // 生成HTML
    async function generateHTML() {
        let html = `<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="apple-touch-icon" href="https://b3log.org/images/brand/siyuan-128.png">
    <link rel="icon" type="image/png" href="https://b3log.org/images/brand/siyuan-128.png">
    <link rel="shortcut icon" type="image/x-icon" href="https://b3log.org/images/brand/siyuan-128.png">
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="mobile-web-app-capable" content="yes"/>
    <meta name="apple-mobile-web-app-status-bar-style" content="black">
    <title>思源笔记手机伺服版</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            text-align: center;
            margin-top: 50px;
        }
    </style>
</head>
<body>
    <h1 id="title">正在获取ip和端口...</h1>
    <p id="status">请耐心等待...</p>

    <script>
        // 你的api URL
        const ipApiUrl = "{{ipApiUrl}}";

        // 获取IP和端口
        async function fetchIPAndPort() {
            try {
                const response = await fetch(ipApiUrl);
                if (!response.ok) {
                    throw new Error(\`获取失败: \${response.status}\`);
                }

                const data = await response.json();
                const ip = data.data.ip;
                const port = data.data.port;
                window.location.href = \`http://\${ip}:\${port}\`;
            } catch (error) {
                document.getElementById('status').innerText = \`获取失败: \${error.message}\`;
            }
        }

        // 判断是否第一次运行
        if (location.search.indexOf("first=first") !== -1 || localStorage.getItem('__sy_client_is_first') === null) {
            localStorage.setItem('__sy_client_is_first', 'true');
            document.getElementById('title').innerText = '请先添加到收藏夹';
            const appStatus = \`<div>第一次运行时会提示添加到收藏夹,再次运行或刷新后将自动跳转</div>
            <div style="margin-top:20px">你也可以使用APP方式启动,详见 <a href="https://ld246.com/article/1724975916806/comment/1725113978862?r=wilsons#comments" target="_blank">APP方式启动</a></div>
            <div style="margin-top:20px">或者你也可以将页面另存为APP应用,详见 <a href="https://ld246.com/article/1724975916806/comment/1725061553011?r=wilsons#comments" target="_blank">另存为APP应用</a></div>\`;
            document.getElementById('status').innerHTML = appStatus;
        } else {
            // 调用函数
            fetchIPAndPort();
        }
    </script>
</body>
</html>`;
        let file = await getFileContent("/data/public/siyuan.html") || "{}";
        try{
            file = JSON.parse(file);
        } catch(e) {
            //console.log('parse json error', e.message);
        }
        if((file.code && file.code === 404) || changedApi) {
            console.log('generateHTML OK');
            html = html.replace('{{ipApiUrl}}', data.api_url);
            putFileContent("/data/public/siyuan.html", html);
        }
    }

    // 开始监控手机端关于窗口
    async function monitorMobileAboutWindow() {
        if(!isMobile() || isMonitoring) return;
        const model = document.querySelector('#model');
        if(!model) return;
        isMonitoring = true;
        console.log('isMonitoring true');
        observeAboutShow(model, async () => {
            await sleep(100);
            const browserLabel = model.querySelector("#modelMain input.b3-text-field[value^='http://']")?.parentElement;
            const uploadIp = document.createElement('div');
            uploadIp.innerHTML = `
            <button class="b3-button b3-button--outline fn__block" id="uploadIP">
                <svg><use xlink:href="#iconUpload"></use></svg>上传IP
            </button>
            `;
            if(browserLabel){
                browserLabel.appendChild(uploadIp);
                const uploadIpBtn = uploadIp.querySelector("button");
                uploadIpBtn.addEventListener("click", async () => {
                    changeClientIpHandler();
                    observeLastUploadTimeChange((newValue, oldValue) => {
                        showMessage("上传成功");
                    });
                });
            }
        });
    }

    // 监控手机端关于窗口被打开
    function observeAboutShow(element, callback) {
        const mutationCallback = function(mutationsList) {
            for (const mutation of mutationsList) {
                if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                    mutation.addedNodes.forEach(node => {
                        if (node.nodeType === Node.TEXT_NODE && node.textContent.trim() === siyuan.languages.about) {
                            callback();
                        }
                    });
                }
            }
        };
        const observer = new MutationObserver(mutationCallback);
        const config = { childList: true, subtree: true};
        observer.observe(element, config);
        // 返回一个函数,用于停止观察
        return () => {
            observer.disconnect();
        };
    }

    //sleep
    function sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }
})();