- 更新全部账号ID 升级为 批量更新账号资料:弹窗勾选 账号ID/昵称/粉丝数/IP属地/性别/简介/标签 - 标签勾选时调用 LLM 重新打标(覆盖已有标签),AI_TAGGING_TOOL 未配置自动跳过 - 只写入选中字段,爬取为空的字段跳过不清空;演示账号(xhs- 前缀)不参与 - .env.app.example 为 SEED_DEMO_DATA 补充注释
142 lines
4.5 KiB
JavaScript
142 lines
4.5 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import {
|
|
runAccountFieldRefresh,
|
|
sanitizeRefreshFields,
|
|
} from "../lib/account-field-refresh-service.ts";
|
|
|
|
function fakeDb(accounts) {
|
|
const updates = [];
|
|
return {
|
|
db: {
|
|
prepare: (sql) => {
|
|
if (sql.startsWith("SELECT")) {
|
|
return {
|
|
all: async () => ({
|
|
results: accounts.map((account) => ({
|
|
id: account.id,
|
|
platform: account.platform,
|
|
platform_uid: account.platform_uid,
|
|
profile_url: account.profile_url,
|
|
})),
|
|
success: true,
|
|
meta: { changes: 0, last_row_id: 0 },
|
|
}),
|
|
};
|
|
}
|
|
assert.match(sql, /^UPDATE accounts SET [a-z_, ?= ]+ WHERE id = \?$/);
|
|
return {
|
|
bind: (...args) => ({
|
|
run: async () => {
|
|
updates.push({ sql, args });
|
|
return { success: true, meta: { changes: 1, last_row_id: 0 } };
|
|
},
|
|
}),
|
|
};
|
|
},
|
|
},
|
|
updates,
|
|
};
|
|
}
|
|
|
|
const profile = (overrides = {}) => ({
|
|
nickname: "昵称A",
|
|
redId: "id-a",
|
|
followers: 123,
|
|
ipLocation: "上海",
|
|
gender: "女",
|
|
bio: "简介A",
|
|
recentNoteTitles: [],
|
|
providerTags: [],
|
|
...overrides,
|
|
});
|
|
|
|
const noTag = async () => [];
|
|
|
|
test("writes only the selected fields as dynamic columns", async () => {
|
|
const accounts = [
|
|
{ id: "a1", platform: "小红书", platform_uid: "uid-1", profile_url: "https://www.xiaohongshu.com/user/profile/a1" },
|
|
];
|
|
const { db, updates } = fakeDb(accounts);
|
|
const status = await runAccountFieldRefresh(
|
|
db,
|
|
async () => profile(),
|
|
noTag,
|
|
["nickname", "ipLocation"],
|
|
);
|
|
|
|
assert.equal(status.status, "done");
|
|
assert.equal(status.updated, 1);
|
|
assert.equal(status.fields.join(","), "nickname,ipLocation");
|
|
assert.equal(updates.length, 1);
|
|
assert.match(updates[0].sql, /SET nickname = \?, ip_location = \?/);
|
|
assert.doesNotMatch(updates[0].sql, /public_account_id|followers|gender|bio|tags/);
|
|
assert.deepEqual(updates[0].args, ["昵称A", "上海", "a1"]);
|
|
});
|
|
|
|
test("tags field uses the tagger output and joins with commas", async () => {
|
|
const accounts = [
|
|
{ id: "a1", platform: "小红书", platform_uid: "uid-1", profile_url: "https://www.xiaohongshu.com/user/profile/a1" },
|
|
];
|
|
const { db, updates } = fakeDb(accounts);
|
|
const status = await runAccountFieldRefresh(
|
|
db,
|
|
async () => profile({ redId: "" }),
|
|
async () => ["美妆", "学生党"],
|
|
["tags"],
|
|
);
|
|
|
|
assert.equal(status.updated, 1);
|
|
assert.match(updates[0].sql, /SET tags = \?/);
|
|
assert.deepEqual(updates[0].args, ["美妆,学生党", "a1"]);
|
|
});
|
|
|
|
test("empty scrape values and empty tagger result are skipped without writing", async () => {
|
|
const accounts = [
|
|
{ id: "a1", platform: "小红书", platform_uid: "uid-1", profile_url: "https://www.xiaohongshu.com/user/profile/a1" },
|
|
];
|
|
const { db, updates } = fakeDb(accounts);
|
|
const status = await runAccountFieldRefresh(
|
|
db,
|
|
async () => profile({ redId: " ", nickname: null, followers: null, ipLocation: "", gender: "", bio: "" }),
|
|
noTag,
|
|
["publicAccountId", "nickname", "followers", "ipLocation", "gender", "bio", "tags"],
|
|
);
|
|
|
|
assert.equal(status.updated, 0);
|
|
assert.equal(status.skipped, 1);
|
|
assert.equal(updates.length, 0);
|
|
});
|
|
|
|
test("scrape failures count as failed and demo accounts are excluded", async () => {
|
|
const accounts = [
|
|
{ id: "boom", platform: "小红书", platform_uid: "uid-1", profile_url: "https://www.xiaohongshu.com/user/profile/boom" },
|
|
{ id: "demo", platform: "小红书", platform_uid: "xhs-8af3", profile_url: "https://www.xiaohongshu.com/user/profile/demo" },
|
|
{ id: "ok", platform: "抖音", platform_uid: "uid-2", profile_url: "https://www.douyin.com/user/ok" },
|
|
];
|
|
const { db, updates } = fakeDb(accounts);
|
|
const status = await runAccountFieldRefresh(
|
|
db,
|
|
async (url) => {
|
|
if (url.includes("boom")) throw new Error("mcp down");
|
|
return profile({ redId: "ok-2" });
|
|
},
|
|
noTag,
|
|
["publicAccountId"],
|
|
);
|
|
|
|
assert.equal(status.total, 2);
|
|
assert.equal(status.updated, 1);
|
|
assert.equal(status.failed, 1);
|
|
assert.deepEqual(updates[0].args.slice(-1), ["ok"]);
|
|
});
|
|
|
|
test("sanitizeRefreshFields filters unknown values and falls back to account id", () => {
|
|
assert.deepEqual(sanitizeRefreshFields(["nickname", "hacked", "tags"]), [
|
|
"nickname",
|
|
"tags",
|
|
]);
|
|
assert.deepEqual(sanitizeRefreshFields([]), ["publicAccountId"]);
|
|
assert.deepEqual(sanitizeRefreshFields("bad"), ["publicAccountId"]);
|
|
});
|