用AI批量把小红书作品设为仅自己可见

有时候想把小红书历史作品统一改成“仅自己可见”,但平台通常没有提供批量设置入口。
这时候最省事的办法,不是手动一条条点,而是让 AI 帮你写一个页面自动化脚本,再由 AI 或你自己执行。

这个脚本能解决什么问题

它做的事情很简单:

  1. 打开小红书创作平台的作品管理页
  2. 自动遍历作品卡片
  3. 逐条打开权限设置
  4. 改成“仅自己可见”
  5. 自动滚动,直到全部处理完成

适合这类需求:

  • 批量改为“仅自己可见”
  • 批量改为“公开可见”
  • 批量改成别的可见范围
  • 其他“页面上重复点击几十次”的操作

核心思路

不要直接猜接口,也不要一上来就写死坐标点击。
更稳妥的方式是:

  • 先让 AI 打开真实页面
  • 识别每个作品卡片的按钮结构
  • 找到“权限设置”的弹窗和选项
  • 用页面原生交互去操作

这样做的好处是:页面自己的登录态、校验、风控参数都会跟着浏览器一起走,成功率通常更高。

如果你有类似需求,要改哪些地方

脚本通常只需要改这几类信息:

  • 目标页面 URL
  • 列表项选择器,比如 .note-card
  • 操作按钮选择器
  • 目标文案,比如“仅自己可见”
  • 滚动容器

这里有一个很容易踩坑的点:
有些站点滚动的不是整个网页,而是中间某个内容容器。
如果 AI 只会 window.scrollTo(...),脚本可能只能处理第一页。

怎么把这类任务交给你的 AI

你可以直接把下面这段话发给 Codex、Hermes 或其他支持浏览器操作的 AI:

1
2
3
4
5
6
7
8
9
10
11
12
13
请打开这个页面,先不要直接写接口脚本。
先检查真实 DOM 结构,确认列表项、操作按钮、弹窗、目标选项和滚动容器。
然后写一个可以批量执行的页面脚本。

目标:
把当前列表里的全部作品批量改成“仅自己可见”。

要求:
1. 优先使用页面原生交互,不要先猜接口
2. 能自动滚动加载全部数据
3. 已经是目标状态的作品要跳过
4. 输出处理总数、成功数、跳过数
5. 最后把脚本保存成一个独立的 js 文件

如果你的目标不是“仅自己可见”,只要把提示词里的目标文案换掉即可。

最后

这类需求的重点不是“小红书”本身,而是一个通用方法:

当一个网页没有批量按钮,但每条操作路径完全一致时,就很适合交给 AI 先观察页面,再写自动化脚本。

你真正需要描述清楚的只有三件事:

  • 你想批量改什么
  • 目标页面在哪里
  • 成功条件是什么

剩下的 DOM 分析、脚本编写、滚动处理、异常跳过,交给 AI 去做就行。

附录代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
// Run this in the DevTools console on:
// https://creator.xiaohongshu.com/new/note-manager
//
// It scrolls through all loaded notes, opens each note's privacy dialog,
// switches the note to "仅自己可见", and keeps going until it reaches the
// total count shown on the page.

(async () => {
const CONFIG = {
targetPrivacyText: "仅自己可见",
totalCountPattern: /全部\s*(\d+)/,
maxIdleRounds: 5,
modalTimeoutMs: 20000,
optionTimeoutMs: 10000,
scrollWaitMs: 1800,
postConfirmWaitMs: 500,
};

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

const waitFor = async (fn, timeoutMs, errorMessage) => {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
const value = fn();
if (value) return value;
await sleep(100);
}
throw new Error(errorMessage);
};

const getTotalCount = () => {
const match = document.body.innerText.match(CONFIG.totalCountPattern);
return match ? Number(match[1]) : null;
};

const getCards = () => [...document.querySelectorAll(".note-card")];
const getScrollContainer = () =>
document.querySelector(".content") || document.scrollingElement;

const getNoteId = (card) => {
const raw = card.getAttribute("data-impression");
if (!raw) return null;
try {
return JSON.parse(raw)?.noteTarget?.value?.noteId ?? null;
} catch {
return null;
}
};

const isAlreadyPrivate = (card) =>
card.querySelector(".permission_msg")?.textContent?.trim() === CONFIG.targetPrivacyText;

const getPrivacyTrigger = () =>
[...document.querySelectorAll(".d-select *")]
.find((el) => {
const text = el.textContent?.trim();
return (
[
"公开可见",
"仅自己可见",
"仅互关好友可见",
"部分人可见",
"部分人不可见",
].includes(text) && el.closest(".d-select")
);
})
?.closest(".d-select");

const getPrivacyOption = () =>
[...document.querySelectorAll(".d-options-wrapper .d-grid-item")]
.find((el) => el.textContent?.trim() === CONFIG.targetPrivacyText);

const getConfirmButton = () =>
[...document.querySelectorAll("button")]
.find((button) => button.textContent?.trim() === "确定");

const setNotePrivate = async (card) => {
const noteId = getNoteId(card);
if (!noteId) throw new Error("Missing note id");

if (isAlreadyPrivate(card)) {
return { noteId, changed: false };
}

const privacyBtn = card.querySelector(".note-card__actions .note-card__action-btn");
if (!privacyBtn) throw new Error(`Missing privacy button: ${noteId}`);

privacyBtn.click();

await waitFor(
() => getConfirmButton(),
CONFIG.modalTimeoutMs,
`Privacy dialog did not open: ${noteId}`
);

const trigger = await waitFor(
() => getPrivacyTrigger(),
CONFIG.optionTimeoutMs,
`Privacy selector missing: ${noteId}`
);
trigger.click();

const option = await waitFor(
() => getPrivacyOption(),
CONFIG.optionTimeoutMs,
`Privacy option missing: ${noteId}`
);
option.click();

const confirm = await waitFor(
() => getConfirmButton(),
CONFIG.optionTimeoutMs,
`Confirm button missing: ${noteId}`
);
confirm.click();

await waitFor(
() => !getConfirmButton(),
CONFIG.modalTimeoutMs,
`Privacy dialog did not close: ${noteId}`
);

await sleep(CONFIG.postConfirmWaitMs);
return { noteId, changed: true };
};

const total = getTotalCount();
if (!total) {
throw new Error("Could not read note total from the page");
}

const seen = new Set();
let changedCount = 0;
let skippedCount = 0;
let idleRounds = 0;

console.log(`[xhs-private] target total: ${total}`);

while (seen.size < total && idleRounds < CONFIG.maxIdleRounds) {
const cards = getCards();
let roundProgress = 0;

for (const card of cards) {
const noteId = getNoteId(card);
if (!noteId || seen.has(noteId)) continue;

const result = await setNotePrivate(card);
seen.add(noteId);
roundProgress += 1;

if (result.changed) {
changedCount += 1;
console.log(`[xhs-private] changed ${changedCount}: ${noteId}`);
} else {
skippedCount += 1;
console.log(`[xhs-private] already private ${skippedCount}: ${noteId}`);
}
}

const beforeCount = cards.length;
const scrollContainer = getScrollContainer();
if (scrollContainer) {
scrollContainer.scrollTop = scrollContainer.scrollHeight;
}
await sleep(CONFIG.scrollWaitMs);
const afterCount = getCards().length;

if (roundProgress === 0 && afterCount === beforeCount) {
idleRounds += 1;
} else {
idleRounds = 0;
}

console.log(
`[xhs-private] seen=${seen.size}/${total}, loaded=${afterCount}, changed=${changedCount}, skipped=${skippedCount}, idleRounds=${idleRounds}`
);
}

const success = seen.size >= total;
const summary = {
success,
total,
seen: seen.size,
changed: changedCount,
skipped: skippedCount,
idleRounds,
};

console.log("[xhs-private] summary", summary);
return summary;
})().catch((error) => {
console.error("[xhs-private] failed", error);
throw error;
});

作者

大冬瓜头

发布于

2026-06-06

更新于

2026-06-06

许可协议