添加 js/ky.js

This commit is contained in:
chickliu 2025-12-26 21:16:08 +08:00
parent 08c38b61fa
commit 15c8c5f354

410
js/ky.js Normal file
View File

@ -0,0 +1,410 @@
/*
* @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);
if (existingIndex !== -1) {
// 更新
$.log(`检测到设备 ${deviceId} 已存在,更新 token`);
dataList[existingIndex] = newData;
} else {
// 新增
$.log(`检测到新设备 ${deviceId},添加数据`);
dataList.push(newData);
}
const newEnvValue = JSON.stringify(dataList);
// 5. 上传/更新变量
if (targetEnvId) {
await qlUpdateEnv(token, targetEnvId, "KYAPP", newEnvValue);
} else {
await qlAddEnv(token, [{ name: "KYAPP", value: newEnvValue }]);
}
} 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) {
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, "更新成功", `设备 ${name} 数据已更新`);
} else {
$.msg($.name, "更新失败", body.message);
}
} catch (e) {
$.logErr(e);
$.msg($.name, "更新异常", e.message);
}
resolve();
});
});
}
// 青龙 API: 新增变量
async function qlAddEnv(token, envsArray) {
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, "上传成功", "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)
}