414 lines
14 KiB
JavaScript
414 lines
14 KiB
JavaScript
/*
|
||
* @name 青龙 KYAPP 变量自动上传
|
||
* @version v1.0.0
|
||
* @description 用于自动提取 device-id 和 refresh_token 并上传到青龙面板 KYAPP 变量
|
||
* @author Antigravity
|
||
*
|
||
* [rewrite_local]
|
||
* ^https:\/\/h5\.kaixinyf\.cn\/api\/window\/windows url script-request-header ky_qx_upload.js
|
||
*
|
||
* [mitm]
|
||
* hostname = h5.kaixinyf.cn
|
||
*
|
||
* ⚠️ 使用说明:
|
||
* 1. 请并在下方配置青龙面板信息 QL_URL, QL_CLIENT_ID, QL_CLIENT_SECRET
|
||
* 2. 将脚本添加到 Rewrite
|
||
* 3. 触发请求即可自动上传
|
||
*/
|
||
|
||
const $ = new Env("KYAPP自动上传");
|
||
|
||
// ---------------------- 用户配置区域 ----------------------
|
||
// 请在下方填写您的青龙面板信息
|
||
const QL_URL = "http://chickliu.store:5732"; // 例如 http://192.168.1.1:5700
|
||
const QL_CLIENT_ID = "M2HkEY-8RRWq"; // 应用ID
|
||
const QL_CLIENT_SECRET = "-LHulEYdGa2TR7_th6oX4Zxb"; // 应用密钥
|
||
// ---------------------------------------------------------
|
||
|
||
const TARGET_URL_KEY = "/api/window/windows";
|
||
|
||
(async () => {
|
||
try {
|
||
if (typeof $request === "undefined") {
|
||
$.msg($.name, "脚本运行环境错误", "请在 Quantumult X Rewrite 中使用 script-request-header");
|
||
return;
|
||
}
|
||
|
||
if ($request.url.indexOf(TARGET_URL_KEY) === -1) {
|
||
return;
|
||
}
|
||
|
||
$.log(`[${$.name}] 开始捕获...`);
|
||
|
||
// 1. 提取信息
|
||
const headers = $request.headers;
|
||
const cookieStr = headers['Cookie'] || headers['cookie'];
|
||
|
||
// 兼容不同写法
|
||
const deviceId = headers['device-id'] || headers['Device-Id'];
|
||
|
||
if (!deviceId) {
|
||
// $.msg($.name, "捕获失败", "未找到 device-id");
|
||
return;
|
||
}
|
||
|
||
if (!cookieStr) {
|
||
// $.msg($.name, "捕获失败", "未找到 Cookie");
|
||
return;
|
||
}
|
||
|
||
const refreshToken = getCookieValue(cookieStr, "refresh_token");
|
||
if (!refreshToken) {
|
||
// $.msg($.name, "捕获失败", "Cookie 中未找到 refresh_token");
|
||
return;
|
||
}
|
||
|
||
$.log(`获取到 DeviceID: ${deviceId}`);
|
||
$.log(`获取到 RefreshToken: ${refreshToken.substring(0, 10)}...`);
|
||
|
||
// 2. 登录青龙
|
||
const token = await qlLogin();
|
||
if (!token) return;
|
||
|
||
// 3. 获取现有变量
|
||
const envs = await qlGetEnvs(token);
|
||
let targetEnvId = null;
|
||
let targetEnvValue = "[]"; // 默认为空数组字符串
|
||
|
||
const existingEnv = envs.find(e => e.name === "KYAPP");
|
||
if (existingEnv) {
|
||
targetEnvId = existingEnv.id;
|
||
targetEnvValue = existingEnv.value || "[]";
|
||
}
|
||
|
||
// 4. 处理数据逻辑
|
||
let dataList = [];
|
||
try {
|
||
dataList = JSON.parse(targetEnvValue);
|
||
if (!Array.isArray(dataList)) dataList = [];
|
||
} catch (e) {
|
||
$.log("原有变量格式不是JSON,重置为空数组");
|
||
dataList = [];
|
||
}
|
||
|
||
const newData = {
|
||
"device_id": deviceId,
|
||
"token_body": refreshToken
|
||
};
|
||
|
||
const existingIndex = dataList.findIndex(item => item.device_id === deviceId);
|
||
let successMsg = "";
|
||
|
||
if (existingIndex !== -1) {
|
||
// 更新
|
||
$.log(`检测到设备 ${deviceId} 已存在,更新 token`);
|
||
dataList[existingIndex] = newData;
|
||
successMsg = `${deviceId} 更新成功`;
|
||
} else {
|
||
// 新增
|
||
$.log(`检测到新设备 ${deviceId},添加数据`);
|
||
dataList.push(newData);
|
||
successMsg = `${deviceId} 添加成功`;
|
||
}
|
||
|
||
const newEnvValue = JSON.stringify(dataList);
|
||
|
||
// 5. 上传/更新变量
|
||
if (targetEnvId) {
|
||
await qlUpdateEnv(token, targetEnvId, "KYAPP", newEnvValue, successMsg);
|
||
} else {
|
||
await qlAddEnv(token, [{ name: "KYAPP", value: newEnvValue }], successMsg);
|
||
}
|
||
|
||
} catch (e) {
|
||
$.logErr(e);
|
||
$.msg($.name, "运行时错误", e.message);
|
||
} finally {
|
||
$.done();
|
||
}
|
||
})();
|
||
|
||
// ---------------------- 辅助函数 ----------------------
|
||
|
||
function getCookieValue(cookie, name) {
|
||
const match = cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
|
||
return match ? match[2] : null;
|
||
}
|
||
|
||
// 青龙 API: 登录
|
||
async function qlLogin() {
|
||
const options = {
|
||
url: `${QL_URL}/open/auth/token?client_id=${QL_CLIENT_ID}&client_secret=${QL_CLIENT_SECRET}`,
|
||
timeout: 10000
|
||
};
|
||
|
||
return new Promise((resolve) => {
|
||
$.get(options, (err, resp, data) => {
|
||
try {
|
||
if (err) throw new Error(JSON.stringify(err));
|
||
const body = JSON.parse(data);
|
||
if (body.code === 200) {
|
||
resolve(body.data.token);
|
||
} else {
|
||
$.msg($.name, "青龙登录失败", body.message);
|
||
resolve(null);
|
||
}
|
||
} catch (e) {
|
||
$.msg($.name, "青龙登录异常", "请检查 URL 和 Client ID/Secret 设置");
|
||
$.logErr(e);
|
||
resolve(null);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// 青龙 API: 获取变量
|
||
async function qlGetEnvs(token) {
|
||
const options = {
|
||
url: `${QL_URL}/open/envs?searchValue=KYAPP`,
|
||
headers: {
|
||
"Authorization": `Bearer ${token}`
|
||
},
|
||
timeout: 10000
|
||
};
|
||
|
||
return new Promise((resolve) => {
|
||
$.get(options, (err, resp, data) => {
|
||
try {
|
||
if (err) throw new Error(JSON.stringify(err));
|
||
const body = JSON.parse(data);
|
||
if (body.code === 200) {
|
||
resolve(body.data);
|
||
} else {
|
||
resolve([]);
|
||
}
|
||
} catch (e) {
|
||
$.logErr(e);
|
||
resolve([]);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// 青龙 API: 更新变量
|
||
async function qlUpdateEnv(token, id, name, value, msg) {
|
||
const options = {
|
||
url: `${QL_URL}/open/envs`,
|
||
method: "PUT",
|
||
headers: {
|
||
"Authorization": `Bearer ${token}`,
|
||
"Content-Type": "application/json"
|
||
},
|
||
body: JSON.stringify({
|
||
id: id,
|
||
name: name,
|
||
value: value
|
||
}),
|
||
timeout: 10000
|
||
};
|
||
|
||
return new Promise((resolve) => {
|
||
$.put(options, (err, resp, data) => {
|
||
try {
|
||
if (err) throw new Error(JSON.stringify(err));
|
||
const body = JSON.parse(data);
|
||
if (body.code === 200) {
|
||
$.msg($.name, "操作成功", msg || `设备 ${name} 数据已更新`);
|
||
} else {
|
||
$.msg($.name, "更新失败", body.message);
|
||
}
|
||
} catch (e) {
|
||
$.logErr(e);
|
||
$.msg($.name, "更新异常", e.message);
|
||
}
|
||
resolve();
|
||
});
|
||
});
|
||
}
|
||
|
||
// 青龙 API: 新增变量
|
||
async function qlAddEnv(token, envsArray, msg) {
|
||
const options = {
|
||
url: `${QL_URL}/open/envs`,
|
||
method: "POST",
|
||
headers: {
|
||
"Authorization": `Bearer ${token}`,
|
||
"Content-Type": "application/json"
|
||
},
|
||
body: JSON.stringify(envsArray),
|
||
timeout: 10000
|
||
};
|
||
|
||
return new Promise((resolve) => {
|
||
$.post(options, (err, resp, data) => {
|
||
try {
|
||
if (err) throw new Error(JSON.stringify(err));
|
||
const body = JSON.parse(data);
|
||
if (body.code === 200) {
|
||
$.msg($.name, "操作成功", msg || "KYAPP 变量已创建");
|
||
} else {
|
||
$.msg($.name, "上传失败", body.message);
|
||
}
|
||
} catch (e) {
|
||
$.logErr(e);
|
||
$.msg($.name, "上传异常", e.message);
|
||
}
|
||
resolve();
|
||
});
|
||
});
|
||
}
|
||
|
||
// ---------------------- Env Class ----------------------
|
||
function Env(name, env) {
|
||
"undefined" != typeof process && JSON.stringify(process.env).indexOf("GITHUB") > -1 && process.exit(0);
|
||
return new class {
|
||
constructor(name, env) {
|
||
this.name = name;
|
||
this.notifyStr = '';
|
||
this.startTime = (new Date).getTime();
|
||
Object.assign(this, env);
|
||
this.log("", `🔔${this.name}, 开始!`);
|
||
}
|
||
isNode() {
|
||
return "undefined" != typeof module && !!module.exports
|
||
}
|
||
isQuanX() {
|
||
return "undefined" != typeof $task
|
||
}
|
||
isSurge() {
|
||
return "undefined" != typeof $httpClient && "undefined" == typeof $loon
|
||
}
|
||
isLoon() {
|
||
return "undefined" != typeof $loon
|
||
}
|
||
get(options, callback) {
|
||
if (this.isQuanX()) {
|
||
if (typeof options == "string") options = { url: options }
|
||
options["method"] = "GET"
|
||
$task.fetch(options).then(response => {
|
||
callback(null, response, response.body)
|
||
}, reason => callback(reason.error, null, null))
|
||
}
|
||
if (this.isSurge() || this.isLoon()) {
|
||
$httpClient.get(options, (error, response, body) => {
|
||
callback(error, response, body)
|
||
})
|
||
}
|
||
if (this.isNode()) {
|
||
this.got = this.got ? this.got : require("got");
|
||
const { url, ...otherOptions } = options;
|
||
this.got.get(url, otherOptions).then(response => {
|
||
const { statusCode, headers, body } = response;
|
||
callback(null, { status: statusCode, statusCode, headers, body }, body)
|
||
}, error => {
|
||
const { message, response } = error;
|
||
callback(message, response, response && response.body)
|
||
})
|
||
}
|
||
}
|
||
post(options, callback) {
|
||
if (this.isQuanX()) {
|
||
if (typeof options == "string") options = { url: options }
|
||
options["method"] = "POST"
|
||
$task.fetch(options).then(response => {
|
||
callback(null, response, response.body)
|
||
}, reason => callback(reason.error, null, null))
|
||
}
|
||
if (this.isSurge() || this.isLoon()) {
|
||
$httpClient.post(options, (error, response, body) => {
|
||
callback(error, response, body)
|
||
})
|
||
}
|
||
if (this.isNode()) {
|
||
this.got = this.got ? this.got : require("got");
|
||
const { url, ...otherOptions } = options;
|
||
this.got.post(url, otherOptions).then(response => {
|
||
const { statusCode, headers, body } = response;
|
||
callback(null, { status: statusCode, statusCode, headers, body }, body)
|
||
}, error => {
|
||
const { message, response } = error;
|
||
callback(message, response, response && response.body)
|
||
})
|
||
}
|
||
}
|
||
put(options, callback) { // Added put method for standard HTTP PUT
|
||
if (this.isQuanX()) {
|
||
if (typeof options == "string") options = { url: options }
|
||
options["method"] = "PUT"
|
||
$task.fetch(options).then(response => {
|
||
callback(null, response, response.body)
|
||
}, reason => callback(reason.error, null, null))
|
||
}
|
||
if (this.isSurge() || this.isLoon()) {
|
||
options.method = 'PUT';
|
||
$httpClient.put(options, (error, response, body) => { // Surge/Loon might not have .put shorthand, using general request usually or strict method
|
||
callback(error, response, body)
|
||
})
|
||
}
|
||
if (this.isNode()) {
|
||
this.got = this.got ? this.got : require("got");
|
||
const { url, ...otherOptions } = options;
|
||
this.got.put(url, otherOptions).then(response => {
|
||
const { statusCode, headers, body } = response;
|
||
callback(null, { status: statusCode, statusCode, headers, body }, body)
|
||
}, error => {
|
||
const { message, response } = error;
|
||
callback(message, response, response && response.body)
|
||
})
|
||
}
|
||
}
|
||
msg(title = name, subtitle = '', body = '') {
|
||
const opts = (val) => {
|
||
if (this.isLoon()) return {
|
||
openUrl: val
|
||
}
|
||
if (this.isQuanX()) return {
|
||
'open-url': val
|
||
}
|
||
if (this.isSurge()) return {
|
||
url: val
|
||
}
|
||
return {}
|
||
}
|
||
if (this.isSurge() || this.isLoon()) {
|
||
$notification.post(title, subtitle, body)
|
||
}
|
||
if (this.isQuanX()) {
|
||
$notify(title, subtitle, body)
|
||
}
|
||
this.log('==============📣系统通知📣==============')
|
||
this.log(title)
|
||
if (subtitle) this.log(subtitle)
|
||
if (body) this.log(body)
|
||
}
|
||
log(...logs) {
|
||
if (logs.length > 0) {
|
||
this.logs = [...(this.logs || []), ...logs]
|
||
}
|
||
console.log(logs.join(this.logSeparator || "\n"))
|
||
}
|
||
logErr(err, msg) {
|
||
const isSurge = this.isSurge()
|
||
const isQuanX = this.isQuanX()
|
||
const isLoon = this.isLoon()
|
||
if (!isSurge && !isQuanX && !isLoon) {
|
||
this.log('', `❗️${this.name}, 错误!`, err.stack)
|
||
} else {
|
||
this.log('', `❗️${this.name}, 错误!`, err)
|
||
}
|
||
}
|
||
wait(time) {
|
||
return new Promise(resolve => setTimeout(resolve, time))
|
||
}
|
||
done(val = {}) {
|
||
const endTime = (new Date).getTime()
|
||
const costTime = (endTime - this.startTime) / 1000
|
||
this.log('', `🔔${this.name}, 结束! 🕛 ${costTime} 秒`)
|
||
this.log()
|
||
if (this.isSurge() || this.isQuanX() || this.isLoon()) {
|
||
$done(val)
|
||
}
|
||
}
|
||
}(name, env)
|
||
}
|