// ==UserScript==
// @name Lobechat Webdav 同步功能
// @namespace https://github.com/dlzmoe/UserScript
// @version 0.0.7
// @author dlzmoe
// @description 给 lobechat 程序添加 webdav 同步的功能。
// @license Apache-2.0
// @icon https://chat.oaipro.com/favicon-32x32.ico
// @match *://chat.oaipro.com/*
// @match *://chat-preview.lobehub.com/*
// @require https://unpkg.com/vue@3.4.38/dist/vue.global.prod.js
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
// ==/UserScript==
(e=>{if(typeof GM_addStyle=="function"){GM_addStyle(e);return}const o=document.createElement("style");o.textContent=e,document.head.append(o)})(" .lobewebdav{position:fixed;left:10px;bottom:100px;z-index:100}.lobewebdav .lobewebdav-dialog{position:fixed;left:50%;top:50%;transform:translate(-50%,-50%);width:600px;height:400px;overflow-y:scroll;padding:30px;border-radius:20px;background:#fff;box-shadow:1px 5px 10px #0003;color:#333}.lobewebdav .lobewebdav-dialog .close{position:absolute;right:20px;top:30px;cursor:pointer;transition:all .2s linear}.lobewebdav .lobewebdav-dialog .close:hover{color:#666}.lobewebdav .lobewebdav-dialog h2{margin-bottom:20px}.lobewebdav .lobewebdav-dialog .item{display:flex;align-items:center;margin-top:1em}.lobewebdav .lobewebdav-dialog .item label{width:120px;text-align:right;white-space:nowrap}.lobewebdav .lobewebdav-dialog .item input{flex:1;border-radius:4px;height:28px;border:1px solid #999;transition:all .1s linear;padding:0 10px;background:#fff;color:#333}.lobewebdav .lobewebdav-dialog .item input:focus{border-color:#666}.lobewebdav .lobewebdav-dialog button{outline:none;border:none;border-radius:5px;background:#333;color:#fff;height:30px;padding:0 15px;transition:all .1s linear;cursor:pointer}.lobewebdav .lobewebdav-dialog button+button{margin-left:10px}.lobewebdav .lobewebdav-dialog button:hover{background:#666} ");
(function (vue) {
'use strict';
const name = "lobechat-webdav";
const version = "0.0.7";
const author = "dlzmoe";
const description = "Add webdav synchronization function to lobechat program.";
const type = "module";
const license = "Apache-2.0";
const scripts = {
dev: "vite --mode development",
build: "vite build",
preview: "vite preview"
};
const dependencies = {
vue: "^3.4.27",
webdav: "^5.7.1"
};
const devDependencies = {
"@vitejs/plugin-vue": "^5.0.4",
less: "^4.1.0",
"less-loader": "^8.0.0",
"style-loader": "^2.0.0",
vite: "^5.2.12",
"vite-plugin-monkey": "^4.0.0"
};
const packageJson = {
name,
version,
author,
description,
type,
license,
scripts,
dependencies,
devDependencies
};
const _export_sfc = (sfc, props) => {
const target = sfc.__vccOpts || sfc;
for (const [key, val] of props) {
target[key] = val;
}
return target;
};
const _sfc_main = {
data() {
return {
open: false,
exportData: {},
importData: {},
webdav: {
baseurl: "",
username: "",
password: ""
},
msg: ""
};
},
methods: {
// 打开弹窗
opendialog() {
this.open = !this.open;
},
// 保存密码
savewebdav() {
localStorage.setItem("lobechat-webdav", JSON.stringify(this.webdav));
this.msg = "WebDav 密码已保存!";
},
// 获取 lobechat 数据生成 json
getIndexedDB() {
const dbName = "LOBE_CHAT_DB";
const storeNames = ["messages", "sessionGroups", "sessions", "topics", "users"];
let request = indexedDB.open(dbName);
request.onsuccess = (event) => {
const db = event.target.result;
let state = {
messages: [],
sessionGroups: [],
sessions: [],
topics: [],
users: []
};
let pendingStores = 0;
storeNames.forEach((storeName) => {
if (db.objectStoreNames.contains(storeName)) {
pendingStores++;
const transaction = db.transaction([storeName], "readonly");
const objectStore = transaction.objectStore(storeName);
const allRecords = objectStore.getAll();
allRecords.onsuccess = (event2) => {
const result = event2.target.result;
state[storeName] = result;
pendingStores--;
if (pendingStores === 0) {
this.exportData = JSON.stringify({
exportType: "all",
state,
version: 7
});
}
};
allRecords.onerror = (event2) => {
console.error(`Error fetching data from ${storeName}:`, event2);
};
} else {
console.warn(`Object store ${storeName} not found in database ${dbName}`);
}
});
if (pendingStores === 0) {
console.log("No valid object stores found in the database.");
}
};
request.onerror = (event) => {
console.error("Error opening database:", event);
};
},
// 检查文件夹是否存在
checkFolderExists(folderUrl) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "PROPFIND",
url: folderUrl,
headers: {
Authorization: "Basic " + btoa(`${this.webdav.username}:${this.webdav.password}`),
Depth: "1"
// 只检查一层
},
onload: function(response) {
if (response.status === 207) {
resolve(true);
} else if (response.status === 404) {
resolve(false);
} else {
reject(new Error(`Error checking folder: ${response.statusText}`));
}
},
onerror: function(error) {
reject(error);
}
});
});
},
// 创建文件夹
createFolder(folderUrl) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "MKCOL",
url: folderUrl,
headers: {
Authorization: "Basic " + btoa(`${this.webdav.username}:${this.webdav.password}`)
},
onload: function(response) {
if (response.status === 201) {
resolve(true);
} else {
reject(new Error(`Error creating folder: ${response.statusText}`));
}
},
onerror: function(error) {
reject(error);
}
});
});
},
// 检查并创建文件夹
async checkAndCreateFolder() {
this.getIndexedDB();
const folderUrl = `${this.webdav.baseurl}lobechat-webdav-backup/`;
try {
const exists = await this.checkFolderExists(folderUrl);
if (!exists) {
await this.createFolder(folderUrl);
console.log("Folder 'lobechat-webdav-backup' created successfully.");
} else {
console.log("Folder 'lobechat-webdav-backup' already exists.");
}
const data = this.exportData;
if (!data) {
console.error("Export data is not initialized properly.");
return;
}
const uploadUrl = `${this.webdav.baseurl}lobechat-webdav-backup/data.json`;
try {
const uploadResponse = await this.uploadFile(uploadUrl, data);
this.msg = "同步到云端成功!";
} catch (error) {
console.error("Upload failed:", error);
this.msg = "同步失败!";
}
} catch (error) {
console.error(error);
}
},
uploadFile(url, fileData) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "PUT",
url,
data: fileData,
headers: {
"Content-Type": "text/plain",
Authorization: "Basic " + btoa(`${this.webdav.username}:${this.webdav.password}`)
},
onload: function(response) {
if (response.status >= 200 && response.status < 300) {
resolve(response);
} else {
reject(new Error(`Upload failed: ${response.statusText}`));
}
},
onerror: function(error) {
reject(error);
}
});
});
},
downloadFile(url) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url,
headers: {
Authorization: "Basic " + btoa(`${this.webdav.username}:${this.webdav.password}`)
},
onload: function(response) {
if (response.status >= 200 && response.status < 300) {
resolve(response.responseText);
} else {
reject(new Error(`Download failed: ${response.statusText}`));
}
},
onerror: function(error) {
reject(error);
}
});
});
},
// 上传
async uploadSampleFile() {
this.checkAndCreateFolder();
},
// 下载
async downloadSampleFile() {
const downloadUrl = `${this.webdav.baseurl}lobechat-webdav-backup/data.json`;
try {
const downloadResponse = await this.downloadFile(downloadUrl);
const importData = JSON.parse(downloadResponse);
console.log(importData);
this.msg = "下载成功,即将同步数据,请勿操作页面!";
const dbName = "LOBE_CHAT_DB";
const storeNames = ["messages", "sessionGroups", "sessions", "topics", "users"];
let request = indexedDB.open(dbName);
request.onsuccess = function(event) {
const db = event.target.result;
const state = importData.state;
console.log(importData);
storeNames.forEach((storeName) => {
if (db.objectStoreNames.contains(storeName)) {
const transaction = db.transaction([storeName], "readwrite");
const objectStore = transaction.objectStore(storeName);
const clearRequest = objectStore.clear();
clearRequest.onsuccess = function() {
console.log(`${storeName} store cleared.`);
const data = state[storeName];
if (Array.isArray(data)) {
data.forEach((item) => {
const addRequest = objectStore.add(item);
addRequest.onsuccess = function() {
console.log(`Item added to ${storeName} store.`);
};
addRequest.onerror = function(event2) {
console.error(`Error adding item to ${storeName}:`, event2);
};
});
}
};
clearRequest.onerror = function(event2) {
console.error(`Error clearing ${storeName} store:`, event2);
};
} else {
console.warn(`Object store ${storeName} not found in database ${dbName}`);
}
});
};
setTimeout(() => {
this.msg = "同步完成,请刷新页面!";
location.reload();
}, 2e3);
request.onerror = function(event) {
console.error("Error opening database:", event);
};
} catch (error) {
console.error(error);
this.msg = "下载失败,请检查是否存在备份!";
}
}
},
created() {
const lobechat_webdav = JSON.parse(localStorage.getItem("lobechat-webdav"));
if (lobechat_webdav) {
this.webdav = lobechat_webdav;
}
console.log(
`%c ${packageJson.name} %c 已开启 `,
"padding: 2px 1px; color: #fff; background: #606060;",
"padding: 2px 1px; color: #fff; background: #42c02e;"
);
}
};
const _hoisted_1 = { class: "lobewebdav" };
const _hoisted_2 = /* @__PURE__ */ vue.createElementVNode("div", {
style: { "border-radius": "8px", "height": "44px", "width": "44px" },
class: "layoutkit-flexbox css-5wokcq acss-1rzhzi1"
}, [
/* @__PURE__ */ vue.createElementVNode("span", {
class: "anticon acss-17q14cp",
role: "img"
}, [
/* @__PURE__ */ vue.createElementVNode("svg", {
xmlns: "http://www.w3.org/2000/svg",
width: "24",
height: "24",
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round",
class: "icon icon-tabler icons-tabler-outline icon-tabler-brand-webflow"
}, [
/* @__PURE__ */ vue.createElementVNode("path", {
stroke: "none",
d: "M0 0h24v24H0z",
fill: "none"
}),
/* @__PURE__ */ vue.createElementVNode("path", { d: "M17 10s-1.376 3.606 -1.5 4c-.046 -.4 -1.5 -8 -1.5 -8c-2.627 0 -3.766 1.562 -4.5 3.5c0 0 -1.843 4.593 -2 5c-.013 -.368 -.5 -4.5 -.5 -4.5c-.15 -2.371 -2.211 -3.98 -4 -3.98l2 12.98c2.745 -.013 4.72 -1.562 5.5 -3.5c0 0 1.44 -4.3 1.5 -4.5c.013 .18 1 8 1 8c2.758 0 4.694 -1.626 5.5 -3.5l3.5 -9.5c-2.732 0 -4.253 2.055 -5 4z" })
])
])
], -1);
const _hoisted_3 = [
_hoisted_2
];
const _hoisted_4 = { class: "lobewebdav-dialog" };
const _hoisted_5 = /* @__PURE__ */ vue.createElementVNode("svg", {
xmlns: "http://www.w3.org/2000/svg",
width: "24",
height: "24",
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round",
class: "icon icon-tabler icons-tabler-outline icon-tabler-x"
}, [
/* @__PURE__ */ vue.createElementVNode("path", {
stroke: "none",
d: "M0 0h24v24H0z",
fill: "none"
}),
/* @__PURE__ */ vue.createElementVNode("path", { d: "M18 6l-12 12" }),
/* @__PURE__ */ vue.createElementVNode("path", { d: "M6 6l12 12" })
], -1);
const _hoisted_6 = [
_hoisted_5
];
const _hoisted_7 = /* @__PURE__ */ vue.createElementVNode("h2", null, "同步 Lobechat 数据到 WebDav", -1);
const _hoisted_8 = { class: "item" };
const _hoisted_9 = /* @__PURE__ */ vue.createElementVNode("label", null, "WebDav 地址:", -1);
const _hoisted_10 = { class: "item" };
const _hoisted_11 = /* @__PURE__ */ vue.createElementVNode("label", null, "WebDav 用户名:", -1);
const _hoisted_12 = { class: "item" };
const _hoisted_13 = /* @__PURE__ */ vue.createElementVNode("label", null, "WebDav 密码:", -1);
const _hoisted_14 = { class: "item" };
const _hoisted_15 = { class: "item" };
const _hoisted_16 = { class: "item" };
const _hoisted_17 = { class: "msg" };
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return vue.openBlock(), vue.createElementBlock("div", _hoisted_1, [
vue.createElementVNode("a", {
"aria-label": "webdav",
onClick: _cache[0] || (_cache[0] = (...args) => $options.opendialog && $options.opendialog(...args)),
href: "javascript:void(0)"
}, _hoisted_3),
vue.withDirectives(vue.createElementVNode("div", _hoisted_4, [
vue.createElementVNode("div", {
class: "close",
onClick: _cache[1] || (_cache[1] = ($event) => this.open = false)
}, _hoisted_6),
_hoisted_7,
vue.createElementVNode("div", _hoisted_8, [
_hoisted_9,
vue.withDirectives(vue.createElementVNode("input", {
type: "text",
"onUpdate:modelValue": _cache[2] || (_cache[2] = ($event) => $data.webdav.baseurl = $event),
placeholder: "https://xxx.com/dav/"
}, null, 512), [
[vue.vModelText, $data.webdav.baseurl]
])
]),
vue.createElementVNode("div", _hoisted_10, [
_hoisted_11,
vue.withDirectives(vue.createElementVNode("input", {
type: "text",
"onUpdate:modelValue": _cache[3] || (_cache[3] = ($event) => $data.webdav.username = $event)
}, null, 512), [
[vue.vModelText, $data.webdav.username]
])
]),
vue.createElementVNode("div", _hoisted_12, [
_hoisted_13,
vue.withDirectives(vue.createElementVNode("input", {
type: "password",
"onUpdate:modelValue": _cache[4] || (_cache[4] = ($event) => $data.webdav.password = $event)
}, null, 512), [
[vue.vModelText, $data.webdav.password]
])
]),
vue.createElementVNode("div", _hoisted_14, [
vue.createElementVNode("button", {
onClick: _cache[5] || (_cache[5] = (...args) => $options.savewebdav && $options.savewebdav(...args))
}, "保存密码")
]),
vue.createElementVNode("div", _hoisted_15, [
vue.createElementVNode("button", {
onClick: _cache[6] || (_cache[6] = (...args) => $options.uploadSampleFile && $options.uploadSampleFile(...args))
}, "同步到云端"),
vue.createElementVNode("button", {
onClick: _cache[7] || (_cache[7] = (...args) => $options.downloadSampleFile && $options.downloadSampleFile(...args))
}, "下载到本地")
]),
vue.createElementVNode("div", _hoisted_16, [
vue.createElementVNode("div", _hoisted_17, vue.toDisplayString($data.msg), 1)
])
], 512), [
[vue.vShow, $data.open]
])
]);
}
const App = /* @__PURE__ */ _export_sfc(_sfc_main, [["render", _sfc_render]]);
const app = vue.createApp(App);
app.mount(
(() => {
const appDiv = document.createElement("div");
document.body.append(appDiv);
return appDiv;
})()
);
})(Vue);