如果你需要 DMIT的小组件可以前往查看
特别提醒,文中用到的Cookie默认1年有效期,且可以执行重装系统之类的风险操作,千万不要泄漏。
特别提醒,文中用到的Cookie默认1年有效期,且可以执行重装系统之类的风险操作,千万不要泄漏。
特别提醒,文中用到的Cookie默认1年有效期,且可以执行重装系统之类的风险操作,千万不要泄漏。
特别提醒,文中用到的Cookie默认1年有效期,且可以执行重装系统之类的风险操作,千万不要泄漏。






iOS安装scriptable
添加脚本
在电脑浏览器上登陆 VMISS ,获取cookie WHMCSlogin_auth_tk 的值。
在Scriptable app中运行脚本,设置好Cookie。
在桌面添加小组件选择这个脚本。
如果有多个设备,在小组件的Widget Parameter中输入数字序号,如 1
脚本内容:
const ScriptName = "VMISS";
const COOKIE_NAME = `${ScriptName}_WHMCSlogin_auth_tk`;
const CANVAS_SIZE = 50;
const RADIUS = 20;
const LINE_WIDTH = 4;
const productsIndex = args.widgetParameter ? parseInt(args.widgetParameter) - 1 : 0;
function parseTrafficUsedToGb(trafficUsed) {
const value = parseFloat(trafficUsed.replace(/[^0-9.]/g, ""));
if (trafficUsed.includes("MB")) {
return value / 1024;
}
if (trafficUsed.includes("TB")) {
return value * 1024;
}
return value;
}
function applySecondaryTextStyle(text) {
text.font = Font.semiboldSystemFont(10);
text.textColor = Color.dynamic(Color.gray(), Color.lightGray());
}
function addCenteredLabel(container, label) {
const labelStack = container.addStack();
labelStack.addSpacer();
const text = labelStack.addText(label);
applySecondaryTextStyle(text);
labelStack.addSpacer();
}
function addChartBlock(rowStack, chart, label, spacerAfter = true) {
const block = rowStack.addStack();
block.layoutVertically();
block.centerAlignContent();
block.addImage(chart);
block.addSpacer(2);
addCenteredLabel(block, label);
if (spacerAfter) {
rowStack.addSpacer();
}
}
function pickProductWithFallback(products, preferredIndex) {
if (products.length === 0) {
return null;
}
const startIndex = Math.min(Math.max(preferredIndex, 0), products.length - 1);
for (let i = startIndex; i >= 0; i--) {
const product = products[i];
if (product) {
return product;
}
}
for (let i = startIndex + 1; i < products.length; i++) {
const product = products[i];
if (product) {
return product;
}
}
return null;
}
function showWidgetErrorAndFinish(widget, message) {
const errorText = widget.addText(message);
errorText.textColor = Color.red();
errorText.font = Font.semiboldSystemFont(14);
Script.setWidget(widget);
widget.presentSmall();
Script.complete();
}
async function handleSettingsMenu() {
const setting = new Alert();
setting.title = "设置";
setting.message = "配置vmiss小组件";
setting.addAction("设置 Cookie");
setting.addDestructiveAction("删除 Cookie");
setting.addAction("取消");
const response = await setting.present();
if (response === 0) {
const cookieInput = new Alert();
cookieInput.title = "输入 Cookie";
cookieInput.message = "请输入您的 cookie 值:";
cookieInput.addTextField("Cookie");
cookieInput.addAction("保存");
cookieInput.addAction("取消");
const cookieResponse = await cookieInput.present();
if (cookieResponse === 0) {
const cookieValue = (cookieInput.textFieldValue(0) ?? "").trim();
if (!cookieValue) {
const invalidAlert = new Alert();
invalidAlert.title = "Cookie 输入有误";
invalidAlert.message = "Cookie 不能为空,请重新输入。";
invalidAlert.addAction("好的");
await invalidAlert.present();
}
else {
Keychain.set(COOKIE_NAME, cookieValue);
const confirmAlert = new Alert();
confirmAlert.title = "保存成功";
confirmAlert.message = "您的 cookie 已成功保存。";
confirmAlert.addAction("好的");
await confirmAlert.present();
}
}
return true;
}
if (response === 1) {
const confirmDelete = new Alert();
confirmDelete.title = "确认删除";
confirmDelete.message = "您确定要删除已保存的 Cookie 吗?";
confirmDelete.addDestructiveAction("删除");
confirmDelete.addAction("取消");
const deleteResponse = await confirmDelete.present();
if (deleteResponse === 0) {
Keychain.remove(COOKIE_NAME);
const deletedAlert = new Alert();
deletedAlert.title = "删除成功";
deletedAlert.message = "已成功删除保存的 Cookie。";
deletedAlert.addAction("好的");
await deletedAlert.present();
}
return true;
}
return false;
}
function parseProductsFromHtml(htmlContent) {
const products = [];
const rowRegex = /<tr[^>]*data-url="[^"]*id=(\d+)"[^>]*>[\s\S]*?<\/tr>/g;
let rowMatch;
while ((rowMatch = rowRegex.exec(htmlContent)) !== null) {
const rowHtml = rowMatch[0];
const id = parseInt(rowMatch[1], 10);
const codeMatch = rowHtml.match(/<b>[^<]*<\/b>\s*-\s*([A-Za-z0-9.#]+)/);
if (!codeMatch)
continue;
const code = codeMatch[1].trim();
const nameMatch = rowHtml.match(/<a[^>]*>([^-<]+?)\s*-\s*[\d.]+\s*<\/a>/);
const name = nameMatch ? nameMatch[1].trim() : "";
const ipMatch = rowHtml.match(/<a[^>]*>[^-<]+?\s*-\s*([\d.]+)\s*<\/a>/);
const ip = ipMatch ? ipMatch[1].trim() : "";
const priceMatch = rowHtml.match(/data-order="[\d.]+">(\$[\d.]+\s*[A-Z]+)/);
const price = priceMatch ? priceMatch[1].trim() : "";
const statusMatch = rowHtml.match(/<span class="status[^"]*">([^<]+)<\/span>/);
const status = statusMatch ? statusMatch[1].trim() : "";
let positionMatch = code.match(/^([A-Z]+)/);
let position = positionMatch ? positionMatch[1] : "";
if (position === "CN" && code.includes("HK")) {
position = "HK";
}
const cycleMatch = rowHtml.match(/小>(.*?)<\/span>/);
let cycle = "month";
if (cycleMatch) {
const cycleText = cycleMatch[1].toLowerCase();
if (cycleText.includes("季度") || cycleText.includes("quarter")) {
cycle = "quarter";
}
else if (cycleText.includes("半年") || cycleText.includes("半")) {
cycle = "half-year";
}
else if (cycleText.includes("年") || cycleText.includes("year")) {
cycle = "year";
}
}
products.push({
id,
name,
status,
ip,
position,
code,
price,
cycle,
});
}
return products.sort((a, b) => a.id - b.id);
}
async function loadGraphData(id, type) {
const resp = new Request(`https://app.vmiss.com/clientarea.php?action=productdetails&id=${id}&graphs=graphs&day=-2hours&_=${Date.now()}`);
resp.timeoutInterval = 25 * 1000;
const cookie = Keychain.get(COOKIE_NAME);
if (!cookie) {
throw new Error("请先设置 Cookie");
}
resp.headers = {
"Cookie": "WHMCSlogin_auth_tk=" + cookie
};
try {
const json = await resp.loadJSON();
switch (type) {
case "cpu":
if (!json || !json.cpu) {
return [];
}
return json.cpu.map(([_, value]) => value);
case "ram":
if (!json || !json.ram) {
return [];
}
return json.ram.map(([_, value]) => value);
case "net":
if (!json || !json.net) {
return [];
}
return [
json.net.in.map(([_, value]) => value),
json.net.out.map(([_, value]) => value)
];
case "disk":
if (!json || !json.disk) {
return [];
}
return [
json.disk.read.map(([_, value]) => value),
json.disk.write.map(([_, value]) => value)
];
}
}
catch (error) {
try {
const text = await resp.loadString();
if (text.includes("login")) {
throw new Error("Cookie 可能已过期,请重新设置");
}
else {
throw new Error("数据加载失败");
}
}
catch (error) {
throw new Error("数据加载失败");
}
}
}
async function loadProductDetails(id) {
const resp = new Request(`https://app.vmiss.com/clientarea.php?action=productdetails&id=${id}&getJSON`);
resp.timeoutInterval = 25 * 1000;
const cookie = Keychain.get(COOKIE_NAME);
if (!cookie) {
throw new Error("请先设置 Cookie");
}
resp.headers = {
"Cookie": "WHMCSlogin_auth_tk=" + cookie
};
try {
const info = await resp.loadJSON();
const usedGb = parseTrafficUsedToGb(info.trafficUsed);
info.trafficUsedPercent = parseFloat(((usedGb / info.trafficTotal) * 100).toFixed(2));
return info;
}
catch (error) {
try {
const text = await resp.loadString();
if (text.includes("login")) {
throw new Error("Cookie 可能已过期,请重新设置");
}
else {
throw new Error("数据加载失败");
}
}
catch (error) {
throw new Error("数据加载失败");
}
}
}
async function loadProducts(widget) {
const resp = new Request("https://app.vmiss.com/clientarea.php?action=products");
resp.timeoutInterval = 25 * 1000;
resp.headers = {
"Cookie": "WHMCSlogin_auth_tk=" + (Keychain.get(COOKIE_NAME) ?? "")
};
const productsHtml = await resp.loadString();
if (productsHtml.includes("login")) {
throw new Error("Cookie 可能已过期,请重新设置");
}
if (productsHtml.includes("login")) {
const errorText = widget.addText("Cookie 可能已过期,请重新设置");
errorText.textColor = Color.red();
errorText.font = Font.semiboldSystemFont(14);
Script.setWidget(widget);
widget.presentSmall();
Script.complete();
return [];
}
else {
return parseProductsFromHtml(productsHtml);
}
}
async function loadPositionImage(position) {
const url = `https://images.bybit.codes/flags/${position.toLowerCase()}.png`;
try {
return await new Request(url).loadImage();
}
catch (error) {
return SFSymbol.named("globe").image;
}
}
function gradientColorByRatio(ratio) {
const t = Math.max(0, Math.min(1, ratio));
const start = { r: 46, g: 204, b: 113 };
const end = { r: 231, g: 76, b: 60 };
const r = Math.round(start.r + (end.r - start.r) * t);
const g = Math.round(start.g + (end.g - start.g) * t);
const b = Math.round(start.b + (end.b - start.b) * t);
return new Color(`#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`);
}
function drawCircleProgress(progress) {
const clampedProgress = Math.max(0, Math.min(1, progress));
const context = new DrawContext();
context.size = new Size(CANVAS_SIZE, CANVAS_SIZE);
context.opaque = false;
context.respectScreenScale = true;
const center = new Point(CANVAS_SIZE / 2, CANVAS_SIZE / 2);
context.setStrokeColor(new Color("#222"));
context.setLineWidth(LINE_WIDTH);
context.strokeEllipse(new Rect(center.x - RADIUS, center.y - RADIUS, RADIUS * 2, RADIUS * 2));
const totalDegrees = 360 * clampedProgress;
const stepDegrees = 3.6;
for (let i = 0; i < totalDegrees; i += stepDegrees) {
const ratio = totalDegrees === 0 ? 0 : i / totalDegrees;
context.setFillColor(gradientColorByRatio(ratio));
const angle = (i - 90) * (Math.PI / 180);
const x = center.x + RADIUS * Math.cos(angle);
const y = center.y + RADIUS * Math.sin(angle);
context.fillEllipse(new Rect(x - LINE_WIDTH / 2, y - LINE_WIDTH / 2, LINE_WIDTH, LINE_WIDTH));
}
const percentText = `${Math.round(clampedProgress * 100)}%`;
context.setTextColor(new Color("#FFFFFF"));
context.setFont(Font.semiboldSystemFont(10));
context.setTextAlignedCenter();
context.drawTextInRect(percentText, new Rect(0, 18, CANVAS_SIZE, 14));
return context.getImage();
}
function createProgressBarImage(total, current, width, height, fillColor, backgroundColor) {
const context = new DrawContext();
context.size = new Size(width, height);
context.opaque = false;
context.respectScreenScale = true;
const bgPath = new Path();
bgPath.addRoundedRect(new Rect(0, 0, width, height), height / 2, height / 2);
context.setFillColor(backgroundColor);
context.addPath(bgPath);
context.fillPath();
const progressWidth = Math.floor(width * Math.min(current / total, 1));
const fillPath = new Path();
fillPath.addRoundedRect(new Rect(0, 0, progressWidth, height), height / 2, height / 2);
context.setFillColor(fillColor);
context.addPath(fillPath);
context.fillPath();
return context.getImage();
}
function createLineChartImage(data, width, height, lineColor = new Color("#1F6FD4"), secondaryLineColor = new Color("#16A085")) {
const context = new DrawContext();
context.size = new Size(width, height);
context.opaque = false;
context.respectScreenScale = true;
const seriesList = Array.isArray(data[0])
? data.filter((series) => series.length > 0)
: [data];
if (seriesList.length === 0) {
return context.getImage();
}
const padding = 4;
const chartWidth = Math.max(1, width - padding * 2);
const chartHeight = Math.max(1, height - padding * 2);
const mergedData = seriesList.flat();
const minValue = Math.min(...mergedData);
const maxValue = Math.max(...mergedData);
const range = Math.max(1e-6, maxValue - minValue);
const getPoints = (series) => {
return series.map((value, index) => {
const x = series.length === 1
? padding + chartWidth / 2
: padding + (index / (series.length - 1)) * chartWidth;
const normalized = (value - minValue) / range;
const y = padding + chartHeight * (1 - normalized);
return new Point(x, y);
});
};
const drawSeries = (points, color, fillAlpha) => {
if (points.length === 0) {
return;
}
const areaPath = new Path();
areaPath.move(new Point(points[0].x, height - padding));
for (const point of points) {
areaPath.addLine(point);
}
areaPath.addLine(new Point(points[points.length - 1].x, height - padding));
areaPath.closeSubpath();
context.setFillColor(new Color(color.hex, fillAlpha));
context.addPath(areaPath);
context.fillPath();
if (points.length === 1) {
context.setFillColor(color);
context.fillEllipse(new Rect(points[0].x - 1.5, points[0].y - 1.5, 3, 3));
return;
}
const linePath = new Path();
linePath.move(points[0]);
for (let i = 1; i < points.length; i++) {
const mid = new Point((points[i - 1].x + points[i].x) / 2, (points[i - 1].y + points[i].y) / 2);
linePath.addQuadCurve(mid, points[i - 1]);
}
linePath.addLine(points[points.length - 1]);
context.setStrokeColor(color);
context.setLineWidth(1.6);
context.addPath(linePath);
context.strokePath();
};
drawSeries(getPoints(seriesList[0]), lineColor, 0.12);
if (seriesList.length > 1) {
drawSeries(getPoints(seriesList[1]), secondaryLineColor, 0.08);
}
return context.getImage();
}
async function renderAccessoryCircular(widget, product) {
const productDetails = await loadProductDetails(product.id);
const progressImage = drawCircleProgress(productDetails.trafficUsedPercent / 100);
widget.addImage(progressImage);
Script.setWidget(widget);
widget.presentAccessoryCircular();
}
async function renderAccessoryInline(widget, product) {
const productDetails = await loadProductDetails(product.id);
const usageText = widget.addText(`${product.ip}|${productDetails.trafficUsedPercent.toFixed(2)}%`);
usageText.font = Font.semiboldSystemFont(10);
Script.setWidget(widget);
widget.presentAccessoryInline();
}
async function renderAccessoryRectangular(widget, product) {
const productDetails = await loadProductDetails(product.id);
const headerStack = widget.addStack();
const positionImage = await loadPositionImage(product.position);
const infoStack = headerStack.addStack();
headerStack.centerAlignContent();
const imgView = infoStack.addImage(positionImage);
imgView.imageSize = new Size(16, 16);
imgView.cornerRadius = 8;
infoStack.addSpacer(2);
infoStack.centerAlignContent();
const ipText = infoStack.addText(product.ip);
ipText.font = Font.semiboldSystemFont(10);
ipText.textColor = Color.gray();
headerStack.addSpacer();
const timeText = headerStack.addText(new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }));
timeText.font = Font.semiboldMonospacedSystemFont(12);
timeText.textColor = Color.gray();
headerStack.centerAlignContent();
widget.addSpacer(6);
const usedTrafficValue = parseFloat(productDetails.trafficUsed.replace(/[^0-9.]/g, ""));
const progressImage = createProgressBarImage(productDetails.trafficTotal, usedTrafficValue, 120, 12, new Color("#1F6FD4"), new Color("#E0E0E0"));
widget.addImage(progressImage);
widget.addSpacer(6);
const line3Stack = widget.addStack();
line3Stack.centerAlignContent();
line3Stack.addSpacer();
const detailText = line3Stack.addText(`${productDetails.trafficUsed} / ${productDetails.trafficTotal} GB`);
detailText.font = Font.semiboldSystemFont(10);
detailText.textColor = Color.gray();
line3Stack.addSpacer();
Script.setWidget(widget);
widget.presentAccessoryRectangular();
}
async function renderSmall(widget, product) {
const productDetails = await loadProductDetails(product.id);
const usedGbSmall = parseTrafficUsedToGb(productDetails.trafficUsed);
const line1Stack = widget.addStack();
const leftStack = line1Stack.addStack();
const statusCircle = SFSymbol.named(productDetails.status === "on" ? "circle.fill" : "circle").image;
const statusView = leftStack.addImage(statusCircle);
statusView.tintColor = productDetails.status === "on" ? Color.green() : Color.red();
statusView.imageSize = new Size(8, 8);
line1Stack.addSpacer(4);
const nameText = leftStack.addText(product.name.length > 6 ? product.name.slice(0, 6) + "…" : product.name);
nameText.font = Font.semiboldSystemFont(12);
nameText.textColor = Color.dynamic(Color.black(), Color.white());
line1Stack.addSpacer();
const timeText2 = line1Stack.addText(new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }));
timeText2.font = Font.semiboldMonospacedSystemFont(12);
timeText2.textColor = Color.dynamic(Color.gray(), Color.lightGray());
line1Stack.centerAlignContent();
widget.addSpacer();
const line2Stack = widget.addStack();
const posImage = await loadPositionImage(product.position);
const posImgView = line2Stack.addImage(posImage);
posImgView.imageSize = new Size(10, 10);
line2Stack.addSpacer(4);
const ipText2 = line2Stack.addText(product.ip);
applySecondaryTextStyle(ipText2);
widget.addSpacer();
const progressBarImage = createProgressBarImage(productDetails.trafficTotal, usedGbSmall, 120, 12, new Color("#1F6FD4"), new Color("#E0E0E0"));
const line4Stack = widget.addStack();
line4Stack.addImage(progressBarImage);
const processText = line4Stack.addText(`${productDetails.trafficUsedPercent.toFixed(2)}%`);
applySecondaryTextStyle(processText);
line4Stack.centerAlignContent();
widget.addSpacer();
const line3Stack = widget.addStack();
const reamingText = line3Stack.addText(`剩余${(productDetails.trafficTotal - usedGbSmall).toFixed(2)} GB`);
applySecondaryTextStyle(reamingText);
line3Stack.addSpacer();
widget.addSpacer();
const lineChartData = await loadGraphData(product.id, "net");
const lineChartImage = createLineChartImage(lineChartData, 120, 40);
widget.addImage(lineChartImage);
Script.setWidget(widget);
widget.presentSmall();
}
async function renderMedium(widget, product) {
const productDetails = await loadProductDetails(product.id);
const usedGbMedium = parseTrafficUsedToGb(productDetails.trafficUsed);
const line1Stack = widget.addStack();
const leftStack2 = line1Stack.addStack();
const statusCircle2 = SFSymbol.named(productDetails.status === "on" ? "circle.fill" : "circle").image;
const statusView2 = leftStack2.addImage(statusCircle2);
statusView2.tintColor = productDetails.status === "on" ? Color.green() : Color.red();
statusView2.imageSize = new Size(8, 8);
line1Stack.addSpacer(4);
const nameOSStack = leftStack2.addStack();
const nameText2 = nameOSStack.addText(product.name.length > 10 ? product.name.slice(0, 10) + "…" : product.name);
nameText2.font = Font.semiboldSystemFont(12);
nameText2.textColor = Color.dynamic(Color.black(), Color.white());
nameOSStack.addSpacer(8);
const osText = nameOSStack.addText(productDetails.operate_system);
applySecondaryTextStyle(osText);
line1Stack.addSpacer();
const timeText3 = line1Stack.addText(new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }));
timeText3.font = Font.semiboldMonospacedSystemFont(12);
timeText3.textColor = Color.dynamic(Color.gray(), Color.lightGray());
line1Stack.centerAlignContent();
widget.addSpacer();
const line2Stack2 = widget.addStack();
const posImage2 = await loadPositionImage(product.position);
const posImgView2 = line2Stack2.addImage(posImage2);
posImgView2.imageSize = new Size(10, 10);
const codeText = line2Stack2.addText(product.code);
applySecondaryTextStyle(codeText);
line2Stack2.addSpacer();
const ipText3 = line2Stack2.addText(product.ip);
applySecondaryTextStyle(ipText3);
widget.addSpacer();
const line3Stack2 = widget.addStack();
const trafficText = line3Stack2.addText("流量使用");
applySecondaryTextStyle(trafficText);
line3Stack2.addSpacer();
const remainingTraffic = (productDetails.trafficTotal - usedGbMedium).toFixed(2);
const remainingText = line3Stack2.addText(`剩余${remainingTraffic} GB`);
applySecondaryTextStyle(remainingText);
line3Stack2.centerAlignContent();
widget.addSpacer(4);
const progressBarImage2 = createProgressBarImage(productDetails.trafficTotal, usedGbMedium, 200, 14, new Color("#1F6FD4"), new Color("#E0E0E0"));
const line3Stack3 = widget.addStack();
line3Stack3.addImage(progressBarImage2);
line3Stack3.centerAlignContent();
widget.addSpacer(4);
const line4Stack2 = widget.addStack();
line4Stack2.centerAlignContent();
const cpuData = await loadGraphData(product.id, "cpu");
const cpuChart = createLineChartImage(cpuData, 150, 75);
addChartBlock(line4Stack2, cpuChart, "Cpu");
const ramData = await loadGraphData(product.id, "ram");
const ramChart = createLineChartImage(ramData, 150, 75, new Color("#8E44AD"));
addChartBlock(line4Stack2, ramChart, "Ram");
const netData = await loadGraphData(product.id, "net");
const netChart = createLineChartImage(netData, 150, 75, new Color("#2980B9"), new Color("#16A085"));
addChartBlock(line4Stack2, netChart, "Net", false);
line2Stack2.layoutHorizontally();
widget.presentMedium();
Script.setWidget(widget);
}
async function main() {
if (config.runsInApp) {
const shouldContinue = await handleSettingsMenu();
if (!shouldContinue) {
Script.complete();
return;
}
}
else {
const widget = new ListWidget();
const cookie = Keychain.get(COOKIE_NAME);
if (!cookie) {
showWidgetErrorAndFinish(widget, "请先设置 Cookie");
return;
}
const products = await loadProducts(widget);
if (products.length === 0) {
showWidgetErrorAndFinish(widget, "未找到产品数据");
return;
}
const product = pickProductWithFallback(products, productsIndex);
if (!product) {
showWidgetErrorAndFinish(widget, "未找到可用产品");
return;
}
switch (config.widgetFamily) {
case "accessoryCircular":
await renderAccessoryCircular(widget, product);
break;
case "accessoryInline":
await renderAccessoryInline(widget, product);
break;
case "accessoryRectangular":
await renderAccessoryRectangular(widget, product);
break;
case "small":
await renderSmall(widget, product);
break;
case "medium":
await renderMedium(widget, product);
break;
case "large":
case "extraLarge":
default:
Script.setWidget(widget);
break;
}
}
Script.complete();
}
void main();
感谢佬
这个可以啊,我看能不能抓下别的
来个dmit的,这个用的人多
@苏叶 #3
我晚点儿看看,我自己没dmit的机。
之前买过一台到期了就没续了。
@emo-cat #4 可以,期待大佬dmit的
牛逼了~
这个真牛逼
这个是真厉害了。。。
@VMISS #8
尽快出API吧,这样能更规范的方便用户获取数据,不然目前这种用cookie的方式很不安全,且数据获取的稳定性也有问题。
@苏叶 #5
scriptable widget for DMIT