(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 = {};
let changedApi = false;
let isMonitoring = false;
let lastUploadTime = { time: 0 };
if(isMobile() || username !== data.username) {
changeClientIpHandler();
window.addEventListener('online', changeClientIpHandler);
}
async function changeClientIpHandler() {
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() {
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));
}
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));
}
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));
}
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);
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);
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) {
}
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);
});
}
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";
}
function isPrivateIP(ip) {
const ipParts = ip.split('.');
const firstOctet = parseInt(ipParts[0], 10);
if (firstOctet === 10) {
return true;
}
if (firstOctet === 172) {
const secondOctet = parseInt(ipParts[1], 10);
if (secondOctet >= 16 && secondOctet <= 31) {
return true;
}
}
if (firstOctet === 192 && parseInt(ipParts[1], 10) === 168) {
return true;
}
if (firstOctet === 169 && parseInt(ipParts[1], 10) === 254) {
return true;
}
return false;
}
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 [];
}
}
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) {
}
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();
};
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
})();