feat: KOC资源页新增一键爬取更新全部账号ID
This commit is contained in:
@@ -74,6 +74,17 @@ type Account = {
|
||||
last_seen_at: string;
|
||||
};
|
||||
|
||||
type IdRefreshStatus = {
|
||||
status: "running" | "done";
|
||||
total: number;
|
||||
processed: number;
|
||||
updated: number;
|
||||
failed: number;
|
||||
skipped: number;
|
||||
startedAt: string;
|
||||
finishedAt: string | null;
|
||||
};
|
||||
|
||||
type ResourceImportPreview = {
|
||||
summary: { total: number; create: number; update: number; error: number };
|
||||
rows: Array<{
|
||||
@@ -3200,6 +3211,10 @@ function ResourcesPage({
|
||||
const [editAccountId, setEditAccountId] = useState("");
|
||||
const [editWorking, setEditWorking] = useState(false);
|
||||
const [editError, setEditError] = useState("");
|
||||
const [idRefresh, setIdRefresh] = useState<IdRefreshStatus | null>(null);
|
||||
const [idRefreshStarting, setIdRefreshStarting] = useState(false);
|
||||
const idRefreshNotifiedRef = useRef(true);
|
||||
const [idRefreshError, setIdRefreshError] = useState("");
|
||||
const resourceAccounts = useMemo(() => {
|
||||
const deliveriesByAccount = new Map<string, Distribution[]>();
|
||||
distributions.forEach((distribution) => {
|
||||
@@ -3447,6 +3462,84 @@ function ResourcesPage({
|
||||
setEditWorking(false);
|
||||
}
|
||||
};
|
||||
const runningIdRefresh = idRefresh?.status === "running";
|
||||
const fetchIdRefreshStatus = useCallback(async () => {
|
||||
const response = await fetch("/api/action", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ action: "public_account_id_refresh_status" }),
|
||||
});
|
||||
const result = await readApiResponse<{ refresh: IdRefreshStatus | null }>(
|
||||
response,
|
||||
"查询失败",
|
||||
);
|
||||
if (!response.ok) throw new Error(result.error || "查询失败");
|
||||
return result.refresh;
|
||||
}, []);
|
||||
const applyIdRefreshStatus = useCallback(
|
||||
(status: IdRefreshStatus) => {
|
||||
if (status.status === "done" && !idRefreshNotifiedRef.current) {
|
||||
idRefreshNotifiedRef.current = true;
|
||||
const summary = `账号ID更新完成:成功 ${status.updated} 个${
|
||||
status.skipped ? `,未识别 ${status.skipped} 个` : ""
|
||||
}${status.failed ? `,失败 ${status.failed} 个` : ""}`;
|
||||
void onImported(summary);
|
||||
}
|
||||
setIdRefresh(status);
|
||||
},
|
||||
[onImported],
|
||||
);
|
||||
useEffect(() => {
|
||||
void fetchIdRefreshStatus()
|
||||
.then((status) => {
|
||||
if (status?.status === "running") {
|
||||
idRefreshNotifiedRef.current = false;
|
||||
applyIdRefreshStatus(status);
|
||||
}
|
||||
})
|
||||
.catch(() => undefined);
|
||||
}, [applyIdRefreshStatus, fetchIdRefreshStatus]);
|
||||
useEffect(() => {
|
||||
if (!runningIdRefresh) return;
|
||||
const timer = window.setInterval(() => {
|
||||
void fetchIdRefreshStatus()
|
||||
.then((status) => {
|
||||
if (status) applyIdRefreshStatus(status);
|
||||
})
|
||||
.catch(() => undefined);
|
||||
}, 3000);
|
||||
return () => window.clearInterval(timer);
|
||||
}, [applyIdRefreshStatus, fetchIdRefreshStatus, runningIdRefresh]);
|
||||
const startIdRefresh = async () => {
|
||||
if (runningIdRefresh || idRefreshStarting) return;
|
||||
if (
|
||||
!window.confirm(
|
||||
"将逐个爬取全部账号主页以更新小红书号/抖音号(其他资料不变),账号较多时需要几分钟。确定开始?",
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setIdRefreshStarting(true);
|
||||
setIdRefreshError("");
|
||||
const response = await fetch("/api/action", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ action: "refresh_public_account_ids" }),
|
||||
});
|
||||
const result = await readApiResponse<{ refresh: IdRefreshStatus }>(
|
||||
response,
|
||||
"启动失败",
|
||||
);
|
||||
if (!response.ok) throw new Error(result.error || "启动失败");
|
||||
idRefreshNotifiedRef.current = false;
|
||||
setIdRefresh(result.refresh);
|
||||
} catch (reason) {
|
||||
setIdRefreshError(reason instanceof Error ? reason.message : "启动失败");
|
||||
} finally {
|
||||
setIdRefreshStarting(false);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="stack">
|
||||
<section className="resource-intro">
|
||||
@@ -3542,6 +3635,18 @@ function ResourcesPage({
|
||||
</button>
|
||||
<div className="resource-toolbar-summary">
|
||||
<span>共 <b>{filteredAccounts.length}</b> 个结果</span>
|
||||
<button
|
||||
className="export-data-button"
|
||||
disabled={runningIdRefresh || idRefreshStarting}
|
||||
title="爬取全部账号主页,更新小红书号/抖音号,其他资料不变"
|
||||
onClick={() => void startIdRefresh()}
|
||||
>
|
||||
{idRefreshStarting
|
||||
? "正在启动…"
|
||||
: runningIdRefresh
|
||||
? `更新中 ${idRefresh?.processed ?? 0}/${idRefresh?.total ?? 0}`
|
||||
: "更新全部账号ID"}
|
||||
</button>
|
||||
<button
|
||||
className="export-data-button"
|
||||
disabled={exporting || filteredAccounts.length === 0}
|
||||
@@ -3552,6 +3657,7 @@ function ResourcesPage({
|
||||
{exporting ? "正在导出…" : "导出筛选结果"}
|
||||
</button>
|
||||
</div>
|
||||
{idRefreshError && <div className="form-error">{idRefreshError}</div>}
|
||||
</div>
|
||||
<div className="resource-grid">
|
||||
{filteredAccounts.map(({ account, sources, partnerManagedOnly }) => {
|
||||
|
||||
@@ -54,6 +54,10 @@ import {
|
||||
} from "../../../lib/wecom-group-push-service";
|
||||
import { isManagerRequest } from "../../../lib/user-auth";
|
||||
import { extractPublishUrl } from "../../../lib/publish-url";
|
||||
import {
|
||||
getPublicAccountIdRefreshStatus,
|
||||
startPublicAccountIdRefresh,
|
||||
} from "../../../lib/account-id-refresh-service";
|
||||
|
||||
type ActionBody = {
|
||||
action?: string;
|
||||
@@ -683,6 +687,14 @@ export async function POST(request: Request) {
|
||||
const taskId = String(body.taskId ?? "").trim();
|
||||
const pushes = await listGroupPushes(db, taskId || undefined);
|
||||
return Response.json({ pushes });
|
||||
} else if (body.action === "refresh_public_account_ids") {
|
||||
const status = startPublicAccountIdRefresh(
|
||||
db,
|
||||
resolveCollectionMcpConfig(env as unknown as CollectionMcpBindings),
|
||||
);
|
||||
return Response.json({ refresh: status });
|
||||
} else if (body.action === "public_account_id_refresh_status") {
|
||||
return Response.json({ refresh: getPublicAccountIdRefreshStatus() });
|
||||
} else if (body.action === "wecom_group_chats") {
|
||||
const groups = await listGroupChatRows(db);
|
||||
return Response.json({ groups });
|
||||
|
||||
Reference in New Issue
Block a user