logo NodeSeekbeta

闲着无聊弄了一个DMIT的iOS Scriptable的小组件

如果你需要 vmiss的小组件可以前往查看

特别提醒,文中用到的Cookie默认1年有效期,且可以执行重装系统之类的风险操作,千万不要泄漏。
特别提醒,文中用到的Cookie默认1年有效期,且可以执行重装系统之类的风险操作,千万不要泄漏。
特别提醒,文中用到的Cookie默认1年有效期,且可以执行重装系统之类的风险操作,千万不要泄漏。
特别提醒,文中用到的Cookie默认1年有效期,且可以执行重装系统之类的风险操作,千万不要泄漏。

介绍-1

介绍-2

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(/&nbsp;/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();
1234
  • bd

  • 发错内容了🤣

  • dmit cookie 是多长有效期,我电脑感觉每天都要重新登

  • @Tokin #3

    不清楚,看起来似乎是一年的有效期。
    如果在小组件这边设置了,小组件的刷新频率由iOS自动调度,理论上间隔不会太长,也能保活了。

  • 不错

  • bd 大佬 nb

  • 有没有瓦工的,而且瓦工好像有api

  • @id #7

    没搬瓦工的帐号。
    如果有API,我可以看着文档,然后列出几个请求。
    你把响应结果给我。
    然后我花时间弄一个。

1234

你好啊,陌生人!

我的朋友,看起来你是新来的,如果想参与到讨论中,点击下面的按钮!

📈用户数目📈

目前论坛共有72254位seeker

🎉欢迎新用户🎉