LINUX SB 快照站

一个小红书批量取关脚本 让我深刻体会到 deepseek 简直拉完了

原帖: linux.sb/topic/13447 · 共 6 楼 · 标题快照 2026-08-17 20:08:29

楼主
发帖 2026-08-17 19:47:41 · 快照 2026-08-17 20:08:29

使用模型:deeepseek v4 flash 、 grok 4.5

需求是:使用autox.js的语法写一个小红书批量取关的脚本,先点击已关注的按钮:select....,它会弹出取消关注就点击一下,id("0_resource_name_obfuscated").className("android.widget.TextView").text("不再关注").findOne(),然后如果当前页面没有已关注的按钮,就向下滑动


使用的是 dsh + dsh tui

image

debug 至少 8 轮以上死活写不出来,一直在向下滑动

image


grok4.5 第一轮就可以取关一个用户,对话 2 轮修复了复杂的逻辑流程后即可运行

image

1786967208132

分享一下脚本吧,用的是autox.js

https://github.com/autox-community/AutoX

/**
 * 小红书批量取关 - AutoX.js(layout 精简版,Rhino 兼容)
 *
 * 控件:
 *   TextView "已关注" → 父级 FrameLayout(clickable) 才是按钮
 *   整行 LinearLayout 可点会进主页,不要点
 *
 * 确认:
 *   A. "不再关注"
 *   B. "取消关注" 可能弹两次
 *
 * 进入关注列表后运行;音量下键停止。
 */

"auto";

auto.waitFor();
console.show();
sleep(200);

var PKG = "com.xingin.xhs";
var MAX = 500;
var GAP = 700;
var CONFIRM_MS = 2500;
var EMPTY_SCROLL = 3;

var stop = false;
var ok = 0;
var fail = 0;
var empty = 0;

try {
    events.observeKey();
    events.onKeyDown("volume_down", function () {
        stop = true;
        toast("已停止");
    });
} catch (e0) {
    console.log("音量键监听不可用: " + e0);
}

function log(m) {
    console.log("[" + new Date().toLocaleTimeString() + "] " + m);
}

/** 找「已关注」对应的可点击小 FrameLayout */
function findFollowBtns() {
    var tvs = text("已关注").packageName(PKG).find();
    var out = [];
    var h = device.height;
    var w = device.width;
    var i, tv, b, btn, p2, bb;

    for (i = 0; i < tvs.length; i++) {
        tv = tvs[i];
        b = tv.bounds();
        if (b.bottom - b.top < 40) continue;
        if (b.top < 200 || b.bottom > h - 80) continue;
        if (b.left < w * 0.6) continue;

        btn = tv.parent();
        if (!btn) continue;
        if (!btn.clickable()) {
            p2 = btn.parent();
            if (p2 && p2.clickable()) {
                btn = p2;
            }
        }
        bb = btn.bounds();
        if (bb.right - bb.left > w * 0.4) continue;
        if (!btn.clickable()) continue;
        out.push(btn);
    }

    out.sort(function (a, b) {
        return a.bounds().top - b.bounds().top;
    });
    return out;
}

function tap(node) {
    if (!node) return false;
    try {
        if (node.click()) return true;
    } catch (e1) {
        // fallthrough
    }
    var b = node.bounds();
    return click(b.centerX(), b.centerY());
}

function confirmExists() {
    return text("不再关注").exists() || text("取消关注").exists();
}

function findConfirm() {
    var node = text("不再关注").findOnce();
    if (node) return node;
    node = text("取消关注").findOnce();
    if (node) return node;
    return null;
}

/** 点确认,最多 3 次(覆盖双次取消关注) */
function tapConfirms() {
    var clicked = 0;
    var deadline = Date.now() + CONFIRM_MS;
    var node;
    var round;

    while (Date.now() < deadline && !stop) {
        node = findConfirm();
        if (node) {
            log("确认「" + (node.text() || "") + "」");
            tap(node);
            clicked++;
            sleep(450);
            break;
        }
        sleep(100);
    }
    if (!clicked) return false;

    for (round = 0; round < 2; round++) {
        node = text("不再关注").findOne(800);
        if (!node) {
            node = text("取消关注").findOne(800);
        }
        if (!node) break;
        log("再确认「" + (node.text() || "") + "」");
        tap(node);
        clicked++;
        sleep(450);
    }
    return clicked > 0;
}

function scrollList() {
    var w = device.width;
    var h = device.height;
    swipe(w / 2, h * 0.72, w / 2, h * 0.38, 400);
    sleep(900);
}

function ensureList() {
    if (confirmExists()) return true;
    if (text("已关注").packageName(PKG).exists()) return true;
    if (textContains("我的关注").exists()) return true;
    log("不在列表,back");
    back();
    sleep(600);
    return text("已关注").exists() || textContains("我的关注").exists();
}

// ---------- main ----------
log("请停在关注列表页,开始取关");
toast("批量取关中,音量下停止");
sleep(800);

if (currentPackage() !== PKG) {
    app.launchPackage(PKG);
    sleep(2000);
    log("请手动进入关注列表…");
    var waitUntil = Date.now() + 30000;
    while (Date.now() < waitUntil && !stop && !text("已关注").exists()) {
        sleep(400);
    }
}

while (!stop && ok < MAX) {
    if (confirmExists()) {
        if (tapConfirms()) {
            ok++;
            log("成功 #" + ok + "(清弹层)");
            sleep(GAP);
            continue;
        }
    }

    ensureList();

    var btns = findFollowBtns();
    if (!btns.length) {
        empty++;
        log("无按钮,下滑 " + empty + "/" + EMPTY_SCROLL);
        if (empty >= EMPTY_SCROLL) break;
        scrollList();
        continue;
    }
    empty = 0;

    var btn = btns[0];
    var bb = btn.bounds();
    log("点已关注 #" + (ok + 1) + " " + bb.left + "," + bb.top + "-" + bb.right + "," + bb.bottom);

    if (!tap(btn)) {
        fail++;
        sleep(400);
        continue;
    }
    sleep(300);

    if (tapConfirms()) {
        ok++;
        log("取关成功 #" + ok);
        sleep(GAP);
    } else {
        fail++;
        log("无确认弹层,可能进了主页");
        ensureList();
        sleep(400);
        if (fail % 3 === 0) {
            scrollList();
        }
    }
}

try {
    events.removeAllKeyDownListeners();
} catch (e2) {
    // ignore
}

log("结束:成功 " + ok + " 失败 " + fail);
toast("完成 成功" + ok + " 失败" + fail);

这下真成小难梁了 😅

最后由 RrOrange 编辑于 2026-08-17 19:48
#1
发帖 2026-08-17 19:50:29 · 快照 2026-08-17 20:13:29

dp还得努力啊😅

#2
发帖 2026-08-17 19:51:47 · 快照 2026-08-17 20:13:29

@lc999 #1 之前还用dp做了一个我开源项目的一个feat,现在想想有点后怕🤡

#3
发帖 2026-08-17 20:06:17 · 快照 2026-08-17 20:13:29

为什么不用协议发包?

#4
发帖 2026-08-17 20:07:47 · 快照 2026-08-17 20:13:29

可以用,怕风控,还是走正常用户渠道

#5
发帖 2026-08-17 21:01:21 · 快照 2026-08-17 21:08:00

不是说好像得bash加极简模式