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

iOS安装scriptable
添加脚本
在电脑浏览器上登陆 DMIT ,获取cookie WHMCSP8D3Nbb7msEy 的值(或者是WHMCS开头的这个cookie)。
在Scriptable app中运行脚本,设置好Cookie。格式 WHMCSP8D3Nbb7msEy=xxxxxxx;
在桌面添加小组件选择这个脚本。
如果有多个设备,在小组件的Widget Parameter中输入数字序号,如 1;
脚本内容:
const ScriptName = "DMIT";
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;
async function loadProductDetailStandard(id) {
const resp = new Request(`https://www.dmit.io/clientarea.php?action=productdetails&id=${id}&json=1&pure=1&page=standard&subaction=whmcsdetail`);
resp.method = "GET";
const cookie = Keychain.get(COOKIE_NAME);
if (!cookie) {
throw new Error("未找到 Cookie");
}
resp.headers = {
"Cookie": cookie
};
const result = await resp.loadJSON();
if (result.result === "success" && result.success) {
return result.success;
}
throw new Error(result.error || "未知错误");
}
async function loadProducts() {
try {
const reqp = new Request("https://www.dmit.io/clientarea.php?action=services");
reqp.method = "GET";
const cookie = Keychain.get(COOKIE_NAME);
if (!cookie) {
throw new Error("未找到 Cookie");
}
reqp.headers = {
"Cookie": cookie
};
const htmlContent = await reqp.loadString();
return parseProductsFromHtml(htmlContent);
}
catch (error) {
console.error(error);
return [];
}
}
async function loadGraphData(id, type) {
try {
const resp = new Request(`https://www.dmit.io/clientarea.php?action=productdetails&id=${id}&json=1&pure=1&page=graph&subaction=charts`);
resp.method = "POST";
resp.body = "data%5Btimeframe%5D=hour";
const cookie = Keychain.get(COOKIE_NAME);
if (!cookie) {
throw new Error("未找到 Cookie");
}
resp.headers = {
"Cookie": cookie
};
const result = await resp.loadJSON();
if (result.result === "success" && result.success) {
return result.success[type];
}
return { datasets: [] };
}
catch (error) {
console.error(error);
return { datasets: [] };
}
}
function applySecondaryTextStyle(text) {
text.font = Font.semiboldSystemFont(10);
applySecondaryTextColor(text);
}
function applyPrimaryTextColor(text) {
text.textColor = Color.dynamic(Color.black(), Color.white());
}
function applySecondaryTextColor(text) {
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);
block.size = new Size(80, 40);
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 = "配置DMIT小组件";
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 cleanText = (value) => {
return value
.replace(/<[^>]*>/g, " ")
.replace(/ /g, " ")
.replace(/\s+/g, " ")
.trim();
};
const rowRegex = /<li[^>]*class="[^"]*\bproducts-item\b[^"]*"[^>]*products_id="(\d+)"[^>]*>[\s\S]*?<\/li>/g;
let rowMatch;
while ((rowMatch = rowRegex.exec(htmlContent)) !== null) {
const rowHtml = rowMatch[0];
const id = parseInt(rowMatch[1], 10);
const nameMatch = rowHtml.match(/<div class="products-title-text">([\s\S]*?)<\/div>/);
const name = nameMatch ? cleanText(nameMatch[1]) : "";
const domainMatch = rowHtml.match(/<div class="products-domain">([\s\S]*?)<\/div>/);
let code = "";
if (domainMatch) {
const domainText = cleanText(domainMatch[1]);
code = domainText.split(" - ")[0].trim();
}
if (!code) {
const fallbackCodeMatch = rowHtml.match(/products-name="([^"]+)"/);
code = fallbackCodeMatch ? fallbackCodeMatch[1].trim() : "";
}
if (!name || !code || Number.isNaN(id)) {
continue;
}
products.push({ id, name, code });
}
return products;
}
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 loadProductDetailStandard(product.id.toString());
const progress = productDetails.bwusage / productDetails.bwlimit;
const chartImage = drawCircleProgress(progress);
widget.addImage(chartImage);
widget.presentAccessoryCircular();
}
async function renderAccessoryInline(widget, product) {
const productDetails = await loadProductDetailStandard(product.id.toString());
const progress = productDetails.bwusage / productDetails.bwlimit;
const chartImage = drawCircleProgress(progress);
widget.addImage(chartImage);
const label = widget.addText(`${product.code}|${Math.round(progress * 100)}%`);
label.font = Font.semiboldSystemFont(12);
applyPrimaryTextColor(label);
widget.presentAccessoryInline();
}
async function renderAccessoryRectangular(widget, product) {
const productDetails = await loadProductDetailStandard(product.id.toString());
const line1Stack = widget.addStack();
const ipText = line1Stack.addText(`IP: ${productDetails.dedicatedip}`);
ipText.font = Font.semiboldSystemFont(12);
applyPrimaryTextColor(ipText);
line1Stack.addSpacer();
const updateTimeText = line1Stack.addText(new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }));
updateTimeText.font = Font.semiboldSystemFont(12);
applySecondaryTextColor(updateTimeText);
const line2Stack = widget.addStack();
const trafficChart = createProgressBarImage(productDetails.bwlimit, productDetails.bwusage, 100, 12, new Color("#1F6FD4"), new Color("#E0E0E0"));
line2Stack.addImage(trafficChart);
const line3Stack = widget.addStack();
const usedGb = (productDetails.bwusage / 1024).toFixed(2);
const totalGb = (productDetails.bwlimit / 1024).toFixed(2);
line3Stack.addSpacer();
const trafficText = line3Stack.addText(`流量: ${usedGb}GB / ${totalGb}GB`);
trafficText.font = Font.semiboldSystemFont(10);
applySecondaryTextColor(trafficText);
line3Stack.addSpacer();
widget.presentAccessoryRectangular();
}
async function renderSmall(widget, product) {
const productDetails = await loadProductDetailStandard(product.id.toString());
const line1Stack = widget.addStack();
const nameText = line1Stack.addText(product.name);
nameText.font = Font.semiboldSystemFont(12);
applyPrimaryTextColor(nameText);
line1Stack.addSpacer();
const updateTimeText = line1Stack.addText(new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }));
updateTimeText.font = Font.semiboldSystemFont(12);
applySecondaryTextColor(updateTimeText);
widget.addSpacer();
const line2Stack = widget.addStack();
const ipText = line2Stack.addText(`IP: ${productDetails.dedicatedip}`);
ipText.font = Font.semiboldSystemFont(10);
applySecondaryTextColor(ipText);
widget.addSpacer();
const line3Stack = widget.addStack();
const trafficChart = createProgressBarImage(productDetails.bwlimit, productDetails.bwusage, 120, 14, new Color("#1F6FD4"), new Color("#E0E0E0"));
line3Stack.addImage(trafficChart);
const processText = line3Stack.addText(`${(productDetails.bwusage / productDetails.bwlimit * 100).toFixed(2)}%`);
processText.font = Font.semiboldSystemFont(10);
applySecondaryTextColor(processText);
widget.addSpacer();
const line4Stack = widget.addStack();
const usedGb = (productDetails.bwusage / 1024).toFixed(2);
const totalGb = (productDetails.bwlimit / 1024).toFixed(2);
const trafficText = line4Stack.addText(`流量: ${usedGb}GB / ${totalGb}GB`);
trafficText.font = Font.semiboldSystemFont(10);
applySecondaryTextColor(trafficText);
widget.addSpacer();
const line5Stack = widget.addStack();
const netData = await loadGraphData(product.id.toString(), "net");
const netChart = createLineChartImage([netData.datasets[0].data, netData.datasets[1].data], 120, 40);
line5Stack.addImage(netChart);
widget.presentSmall();
}
async function renderMedium(widget, product) {
const productDetails = await loadProductDetailStandard(product.id.toString());
const line1Stack = widget.addStack();
const nameText = line1Stack.addText(product.name);
nameText.font = Font.semiboldSystemFont(14);
applyPrimaryTextColor(nameText);
line1Stack.addSpacer();
const updateTimeText = line1Stack.addText(new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }));
updateTimeText.font = Font.semiboldSystemFont(12);
applySecondaryTextColor(updateTimeText);
widget.addSpacer(10);
const line2Stack = widget.addStack();
const codeText = line2Stack.addText(`${product.code}`);
codeText.font = Font.semiboldSystemFont(12);
applySecondaryTextColor(codeText);
line2Stack.addSpacer();
const ipText = line2Stack.addText(`${productDetails.dedicatedip}`);
ipText.font = Font.semiboldSystemFont(12);
applySecondaryTextColor(ipText);
widget.addSpacer(10);
const line3Stack = widget.addStack();
const trafficLabel = line3Stack.addText("流量使用");
trafficLabel.font = Font.semiboldSystemFont(12);
applySecondaryTextColor(trafficLabel);
line3Stack.addSpacer();
const reamingLabel = line3Stack.addText(`剩余${((productDetails.bwlimit - productDetails.bwusage) / 1024).toFixed(2)}GB`);
reamingLabel.font = Font.semiboldSystemFont(12);
applySecondaryTextColor(reamingLabel);
widget.addSpacer(10);
const line4Stack = widget.addStack();
const trafficChart = createProgressBarImage(productDetails.bwlimit, productDetails.bwusage, 150, 10, new Color("#1F6FD4"), new Color("#E0E0E0"));
line4Stack.addImage(trafficChart);
line4Stack.addSpacer(8);
const processText = line4Stack.addText(`${(productDetails.bwusage / productDetails.bwlimit * 100).toFixed(2)}%`);
processText.font = Font.semiboldSystemFont(12);
applySecondaryTextColor(processText);
line4Stack.centerAlignContent();
widget.addSpacer(10);
const line5Stack = widget.addStack();
const cpuData = await loadGraphData(product.id.toString(), "cpu");
const cpuChart = createLineChartImage(cpuData.datasets[0].data, 150, 40, new Color("#E67E22"));
addChartBlock(line5Stack, cpuChart, "Cpu");
const memData = await loadGraphData(product.id.toString(), "mem");
const memChart = createLineChartImage(memData.datasets[0].data, 150, 40, new Color("#1662a0"));
addChartBlock(line5Stack, memChart, "Ram");
const netData = await loadGraphData(product.id.toString(), "net");
const netChart = createLineChartImage([netData.datasets[0].data, netData.datasets[1].data], 150, 40);
addChartBlock(line5Stack, netChart, "Net", false);
const line7Stack = widget.addStack();
line7Stack.addSpacer();
const nextInvoiceText = line7Stack.addText(`续费: ${productDetails.nextinvoicedate.split(" ")[0]}`);
nextInvoiceText.font = Font.semiboldSystemFont(8);
const nextInvoiceDate = new Date(productDetails.nextinvoicedate);
const now = new Date();
const timeDiff = nextInvoiceDate.getTime() - now.getTime();
if (timeDiff <= 7 * 24 * 60 * 60 * 1000) {
nextInvoiceText.textColor = Color.red();
}
else {
applySecondaryTextColor(nextInvoiceText);
}
line7Stack.addSpacer();
line7Stack.centerAlignContent();
widget.presentMedium();
}
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();
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();
bd
发错内容了🤣
dmit cookie 是多长有效期,我电脑感觉每天都要重新登
@Tokin #3
不清楚,看起来似乎是一年的有效期。
如果在小组件这边设置了,小组件的刷新频率由iOS自动调度,理论上间隔不会太长,也能保活了。
不错
bd 大佬 nb
有没有瓦工的,而且瓦工好像有api
@id #7
没搬瓦工的帐号。
如果有API,我可以看着文档,然后列出几个请求。
你把响应结果给我。
然后我花时间弄一个。
@emo-cat #4 求一个rfc的
@emo-cat #8 https://www.bandwagonhost.net/11399.html
我不知道怎么弄……你看看有什么需要提供的