之前写了个在 Sub-Store 里添加 webshare 节点的脚本 ( 适用于 Sub-Store 的 WebShare 节点生成脚本 ),但终归缺少了流量信息,现在补齐,需要在参数设置里设置 apiKey。效果如图:
默认:
• 无 NODE_PREFIX:节点名类似
01
• 无 DIALER_PROXY
• 必填只需要 apiKey
•
可选:
• nodePrefix=WEB | 家宽
• dialerProxy=Relay
• nodePrefix=none 表示不要前缀
• dialerProxy=none 表示不要前置代理
async function operator(proxies, targetPlatform, context) {
proxies = Array.isArray(proxies) ? proxies : [];
var SUBS_KEY = "subs";
var A = typeof $arguments !== "undefined" && $arguments ? $arguments : {};
var $ = typeof $substore !== "undefined" ? $substore : null;
var PU = typeof ProxyUtils !== "undefined" && ProxyUtils ? ProxyUtils : {};
var B = PU.Buffer || (typeof Buffer !== "undefined" ? Buffer : null);
var R = typeof scriptResourceCache !== "undefined" ? scriptResourceCache : null;
var source = context && context.source ? context.source : null;
var BASE = "https://proxy.webshare.io";
var VERSION = 12;
var TIMEOUT = 8000;
var NODE_TTL = 12 * 3600 * 1000;
var NODE_KEEP = 7 * 24 * 3600 * 1000;
var INFO_TTL = 30 * 60 * 1000;
var INFO_KEEP = 7 * 24 * 3600 * 1000;
var PAGE_SIZE = 100;
var MAX_PAGES = 20;
function arg(name, def) {
var v = A ? A[name] : undefined;
return v === undefined || v === null ? def || "" : String(v).trim();
}
function isOff(v) {
return /^(|0|false|no|none|null|off)$/i.test(String(v == null ? "" : v).trim());
}
var API = String(arg("apiKey") || arg("apikey") || arg("token")).trim();
var NODE_PREFIX = arg("nodePrefix", "");
var DIALER_PROXY = arg("dialerProxy", "");
if (isOff(NODE_PREFIX)) NODE_PREFIX = "";
if (isOff(DIALER_PROXY)) DIALER_PROXY = "";
if (!$ || !$.http || !$.http.get) throw new Error("当前环境缺少 $substore.http.get");
if (!API) throw new Error("Sub-Store arguments 未填写:apiKey");
function log(s) {
console.log("[Webshare] " + s);
}
function warn(s) {
console.log("[Webshare] WARN: " + s);
}
function yes(v) {
return /^(1|true|yes|y|on)$/i.test(String(v == null ? "" : v).trim());
}
function hash(s) {
var h = 0;
s = String(s || "");
for (var i = 0; i < s.length; i++) {
h = ((h << 5) - h + s.charCodeAt(i)) | 0;
}
return (h >>> 0).toString(16);
}
var PREFIX = "webshare:v" + VERSION + ":" + hash(API + "\n" + NODE_PREFIX + "\n" + DIALER_PROXY);
var K_NODES = PREFIX + ":nodes";
var K_INFO = PREFIX + ":userinfo";
function redact(s) {
s = String(s == null ? "" : s);
if (API) s = s.split(API).join("[ApiKey]");
return s.replace(/Token\s+[A-Za-z0-9._-]+/gi, "Token [ApiKey]");
}
function msg(e) {
return redact(e && e.message ? e.message : e).slice(0, 500);
}
function makeError(message, props) {
var e = new Error(message);
if (props) {
for (var k in props) e[k] = props[k];
}
return e;
}
function opt() {
return typeof $options !== "undefined" && $options ? $options : {};
}
function flag(names) {
var o = opt();
var q = o._req && o._req.query ? o._req.query : {};
for (var i = 0; i < names.length; i++) {
var k = names[i];
if (yes(A[k]) || yes(o[k]) || yes(q[k])) return true;
}
return false;
}
function bodyText(body) {
if (body == null) return "";
if (typeof body === "string") return body;
if (B && B.from) {
try {
return B.from(body).toString("utf8");
} catch (e) {}
}
try {
return JSON.stringify(body);
} catch (e) {
return String(body);
}
}
function parseJson(x) {
if (!x) return null;
if (typeof x !== "string") return x;
try {
return JSON.parse(x);
} catch (e) {
return null;
}
}
function isTransient(status) {
status = Number(status);
return [0, 408, 425, 429, 500, 502, 503, 504, 522, 523, 524].indexOf(status) >= 0;
}
async function getJson(path) {
var url = /^https?:\/\//i.test(String(path)) ? String(path) : BASE + path;
var r;
try {
r = await $.http.get.call($.http, {
url: url,
timeout: TIMEOUT,
headers: {
Authorization: "Token " + API,
Accept: "application/json",
"User-Agent": "Mozilla/5.0"
}
});
} catch (e) {
throw makeError("请求失败:" + msg(e), { transient: true });
}
var status = Number((r && (r.statusCode || r.status)) || 0);
var text = bodyText(r && r.body);
var data = null;
try {
data = text ? JSON.parse(text.replace(/^\uFEFF/, "")) : null;
} catch (e) {}
if (status < 200 || status >= 300) {
throw makeError("HTTP " + status + ":" + redact(text).slice(0, 300), {
status: status,
transient: isTransient(status),
auth: status === 401 || status === 403
});
}
if (data === null) {
throw makeError("响应不是 JSON", { transient: true });
}
return data;
}
async function getPaged(path) {
var out = [];
var url = path;
for (var i = 0; url && i < MAX_PAGES; i++) {
var data = await getJson(url);
var rows = [];
if (data && Array.isArray(data.results)) rows = data.results;
else if (Array.isArray(data)) rows = data;
for (var j = 0; j < rows.length; j++) out.push(rows[j]);
url = data && data.next ? String(data.next) : "";
}
return out;
}
function cacheRead(key, freshMs, keepMs) {
if (!R) return null;
var o;
try {
o = parseJson(R.get(key));
} catch (e) {
return null;
}
if (!o || o.v !== VERSION) return null;
var at = Number(o.at || 0);
var age = Date.now() - at;
if (!at || !isFinite(age) || age < 0 || age >= keepMs) {
cacheDel(key);
return null;
}
o.age = age;
o.fresh = age < freshMs;
return o;
}
function cacheWrite(key, data, ttl) {
if (!R) return;
try {
var o = {
v: VERSION,
at: Date.now()
};
for (var k in data) o[k] = data[k];
R.set(key, JSON.stringify(o), ttl);
} catch (e) {
warn("缓存写入失败:" + msg(e));
}
}
function cacheDel(key) {
if (!R) return;
try {
if (typeof R.delete === "function") return R.delete(key);
if (typeof R.del === "function") return R.del(key);
if (typeof R.remove === "function") return R.remove(key);
R.set(key, "", 1000);
} catch (e) {}
}
function clearCache() {
cacheDel(K_NODES);
cacheDel(K_INFO);
log("已清理缓存");
}
function padN(n, len) {
var s = String(n);
while (s.length < len) s = "0" + s;
return s;
}
function num(x) {
var n = Number(x);
return isFinite(n) ? n : 0;
}
function cleanIso(s) {
s = String(s || "").trim();
if (!s) return "";
return s.replace(/(\.\d{3})\d+(Z|[+-]\d{2}:?\d{2})$/i, "$1$2");
}
function nowIso() {
return new Date().toISOString();
}
function toUnix(x) {
if (x == null || x === "") return 0;
if (typeof x === "number") {
if (!isFinite(x) || x <= 0) return 0;
return x > 1e12 ? Math.floor(x / 1000) : Math.floor(x);
}
var s = String(x).trim();
if (!s) return 0;
if (/^\d+$/.test(s)) {
var n = Number(s);
if (!isFinite(n) || n <= 0) return 0;
return n > 1e12 || s.length >= 13 ? Math.floor(n / 1000) : Math.floor(n);
}
var t = Date.parse(cleanIso(s));
return isFinite(t) ? Math.floor(t / 1000) : 0;
}
function flagEmoji(cc) {
cc = String(cc || "US").toUpperCase();
if (cc === "ZZ") return "🌐";
if (!/^[A-Z]{2}$/.test(cc)) return "";
if (typeof String.fromCodePoint !== "function") return "";
return (
String.fromCodePoint(0x1f1e6 + cc.charCodeAt(0) - 65) +
String.fromCodePoint(0x1f1e6 + cc.charCodeAt(1) - 65)
);
}
async function fetchNodes() {
var rows = await getPaged("/api/v2/proxy/list/?mode=direct&page=1&page_size=" + PAGE_SIZE);
var valid = [];
for (var i = 0; i < rows.length; i++) {
var p = rows[i];
if (
p &&
p.proxy_address &&
p.port &&
p.username &&
p.password &&
p.valid !== false &&
String(p.valid).toLowerCase() !== "false"
) {
valid.push(p);
}
}
if (!valid.length) {
throw makeError("Webshare 没有返回可用代理", { business: true });
}
var nodes = [];
for (var j = 0; j < valid.length; j++) {
var x = valid[j];
var icon = flagEmoji(x.country_code || x.country || "US");
var idx = padN(j + 1, 2);
var label = NODE_PREFIX ? NODE_PREFIX + " " + idx : idx;
var name = (icon ? icon + " " : "") + label;
var node = {
name: name,
type: "socks5",
server: String(x.proxy_address),
port: Number(x.port),
username: String(x.username),
password: String(x.password)
};
if (DIALER_PROXY) {
node["dialer-proxy"] = DIALER_PROXY;
}
nodes.push(node);
}
return nodes;
}
async function fetchInfo() {
var subscription = await getJson("/api/v2/subscription/");
var start = cleanIso(subscription && subscription.start_date);
var end = cleanIso(subscription && subscription.end_date);
var lte = nowIso();
if (!start) {
throw makeError("subscription.start_date 为空,无法生成流量统计区间", { business: true });
}
log("统计区间:" + start + " ~ " + lte);
var statsPath =
"/api/v2/stats/aggregate/" +
"?timestamp__gte=" +
encodeURIComponent(start) +
"×tamp__lte=" +
encodeURIComponent(lte);
var res = await Promise.all([
getPaged("/api/v2/subscription/plan/"),
getJson(statsPath)
]);
var plans = res[0];
var stats = res[1];
var limitGb = 0;
for (var i = 0; i < plans.length; i++) {
var p = plans[i];
if (String((p && p.status) || "").toLowerCase() === "active") {
limitGb += Math.max(0, num(p.bandwidth_limit));
}
}
if (limitGb <= 0) {
throw makeError("未找到有效 active plan 流量额度", { business: true });
}
var used = Math.max(0, Math.round(num(stats && stats.bandwidth_total)));
var total = Math.round(limitGb * Math.pow(1024, 3));
var expire = toUnix(end);
if (!expire) {
throw makeError("subscription.end_date 解析失败,无法生成到期时间", { business: true });
}
return "upload=0;download=" + used + ";total=" + total + ";expire=" + expire;
}
function currentSubName() {
if (!source || typeof source !== "object" || source._collection) return "";
if (source.name) return source.name;
for (var k in source) {
if (k.charAt(0) === "_") continue;
var sub = source[k];
if (sub && sub.name) return sub.name;
if (sub && (sub.url || sub.content)) return k;
}
return "";
}
function writeInfo(info) {
if (!$ || !$.read || !$.write || !info) return false;
try {
var name = currentSubName();
if (!name) return false;
var subs = $.read(SUBS_KEY) || [];
if (typeof subs === "string") subs = JSON.parse(subs);
if (!Array.isArray(subs)) return false;
var sub = null;
for (var i = 0; i < subs.length; i++) {
if (subs[i] && subs[i].name === name) {
sub = subs[i];
break;
}
}
if (!sub) return false;
if (sub.subUserinfo !== info) {
sub.subUserinfo = info;
$.write(subs, SUBS_KEY);
log("已写入流量信息:" + info);
}
return true;
} catch (e) {
warn("写入流量信息失败:" + msg(e));
return false;
}
}
async function ensureInfo(force) {
var old = cacheRead(K_INFO, INFO_TTL, INFO_KEEP);
if (old && old.fresh && !force) {
writeInfo(old.info);
return old.info;
}
try {
var info = await fetchInfo();
cacheWrite(K_INFO, { info: info }, INFO_KEEP);
writeInfo(info);
log("生成流量信息:" + info);
return info;
} catch (e) {
if (old && old.info && e && e.transient) {
warn("流量刷新失败,使用旧缓存:" + msg(e));
writeInfo(old.info);
return old.info;
}
warn("流量刷新失败:" + msg(e));
return "";
}
}
function append(nodes) {
return proxies.concat(nodes);
}
if (flag(["clearCache", "clearcache", "clear-cache"])) {
clearCache();
}
var force = flag(["refresh", "force", "noCache", "nocache", "no-cache"]);
var oldNodes = cacheRead(K_NODES, NODE_TTL, NODE_KEEP);
if (oldNodes && oldNodes.fresh && !force && Array.isArray(oldNodes.nodes) && oldNodes.nodes.length) {
log("命中节点缓存,节点数:" + oldNodes.nodes.length);
await ensureInfo(false);
return append(oldNodes.nodes);
}
try {
var nodes = await fetchNodes();
cacheWrite(K_NODES, { nodes: nodes }, NODE_KEEP);
await ensureInfo(force);
log("生成节点数:" + nodes.length);
return append(nodes);
} catch (e) {
if (oldNodes && Array.isArray(oldNodes.nodes) && oldNodes.nodes.length && e && e.transient) {
warn("节点刷新失败,使用旧缓存:" + msg(e));
await ensureInfo(force);
return append(oldNodes.nodes);
}
throw e;
}
}
1 个帖子 - 1 位参与者