- {taskDistributions.map((item) => {
+ {sortedTaskDistributions.map((item) => {
const metrics = latestPublicMetrics(item);
const noteUrl = xhsPublishUrl(item.publish_url);
const hasMetrics = metrics.likes !== null;
@@ -2512,7 +2740,7 @@ function CollectionScheduleCard({
自动采集计划
选择开始日期与采集日
-
勾选第1天到第7天,所选日期均在北京时间10:00自动采集。
+
勾选第1天到第7天,所选日期均在北京时间09:00自动采集。
已选择 {days.length} 天
diff --git a/app/api/action/route.ts b/app/api/action/route.ts
index 416c78a..31170ce 100644
--- a/app/api/action/route.ts
+++ b/app/api/action/route.ts
@@ -30,6 +30,11 @@ import {
createDistributionTask,
createScreenshotTask,
} from "../../../lib/task-service";
+import {
+ DistributionReleaseError,
+ releaseUnfinishedDistribution,
+} from "../../../lib/distribution-release-service";
+import { isManagerRequest } from "../../../lib/user-auth";
type ActionBody = {
action?: string;
@@ -135,6 +140,12 @@ export async function POST(request: Request) {
.bind(available.results.length, partnerId),
);
await db.batch(statements);
+ } else if (body.action === "release_distribution") {
+ if (!(await isManagerRequest(request))) return adminForbidden();
+ await releaseUnfinishedDistribution(
+ db,
+ String(body.distributionId ?? "").trim(),
+ );
} else if (body.action === "save_collection_schedule") {
const taskId = String(body.taskId ?? "").trim();
const startDate = String(body.startDate ?? "").trim();
@@ -198,7 +209,7 @@ export async function POST(request: Request) {
AND publish_url != ''`,
)
.bind(
- `已安排${days.length}个采集日,每日10:00执行`,
+ `已安排${days.length}个采集日,每日09:00执行`,
taskId,
),
]);
@@ -362,7 +373,13 @@ export async function POST(request: Request) {
} catch (error) {
return Response.json(
{ error: error instanceof Error ? error.message : "操作失败" },
- { status: error instanceof FeishuSourceError ? error.status : 500 },
+ {
+ status:
+ error instanceof FeishuSourceError ||
+ error instanceof DistributionReleaseError
+ ? error.status
+ : 500,
+ },
);
}
}
diff --git a/app/globals.css b/app/globals.css
index df9154b..1a9d597 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -2264,6 +2264,41 @@ a {
white-space: nowrap;
}
+.recovery-sort-button {
+ display: inline-flex;
+ min-width: 100%;
+ align-items: center;
+ justify-content: flex-start;
+ gap: 4px;
+ padding: 8px 0;
+ border: 0;
+ color: inherit;
+ background: transparent;
+ font: inherit;
+ cursor: pointer;
+}
+
+.recovery-sort-button b {
+ color: #aab2af;
+ font-size: 11px;
+ line-height: 1;
+}
+
+.recovery-sort-button:hover,
+.recovery-sort-button.active {
+ color: #168565;
+}
+
+.recovery-sort-button.active b {
+ color: #168565;
+}
+
+.recovery-sort-button:focus-visible {
+ border-radius: 4px;
+ outline: 2px solid rgba(22, 133, 101, 0.3);
+ outline-offset: 2px;
+}
+
.recovery-content {
max-width: 360px;
}
@@ -3572,6 +3607,81 @@ label small {
color: #b04b40;
}
+.eyebrow.warning {
+ color: #b06d24;
+}
+
+.release-distribution-button {
+ min-height: 30px;
+ padding: 0 11px;
+ border: 1px solid #efd9bd;
+ border-radius: 8px;
+ color: #95601f;
+ background: #fffaf2;
+ font-size: 9px;
+ font-weight: 650;
+ white-space: nowrap;
+}
+
+.release-distribution-button:hover {
+ border-color: #dfbd91;
+ background: #fff5e5;
+}
+
+.release-distribution-summary {
+ display: grid;
+ gap: 6px;
+ padding: 13px 14px;
+ border: 1px solid var(--line);
+ border-radius: 10px;
+ background: #f8faf8;
+}
+
+.release-distribution-summary strong {
+ color: var(--ink);
+ font-size: 11px;
+ line-height: 1.5;
+}
+
+.release-distribution-summary span {
+ color: #7d8985;
+ font-size: 9px;
+}
+
+.release-distribution-warning {
+ margin: 0;
+ padding: 13px 14px;
+ border: 1px solid #f1dfc8;
+ border-radius: 10px;
+ color: #805e35;
+ background: #fffaf3;
+ font-size: 10px;
+ line-height: 1.65;
+}
+
+.release-confirm-button {
+ display: inline-flex;
+ min-height: 38px;
+ align-items: center;
+ justify-content: center;
+ padding: 0 15px;
+ border: 1px solid #a56a26;
+ border-radius: 9px;
+ color: white;
+ background: #b9792f;
+ font-size: 11px;
+ font-weight: 650;
+}
+
+.release-confirm-button:hover {
+ background: #9d6425;
+}
+
+.release-confirm-button:disabled {
+ cursor: wait;
+ opacity: 0.62;
+}
+
.user-delete-warning {
margin: 0;
padding: 13px 14px;
diff --git a/docs/KOC LOOP 私有化部署指南.md b/docs/KOC LOOP 私有化部署指南.md
index cf1146d..33360a6 100644
--- a/docs/KOC LOOP 私有化部署指南.md
+++ b/docs/KOC LOOP 私有化部署指南.md
@@ -7,7 +7,7 @@
| 组件 | 容器 | 作用 | 持久化 |
| --- | --- | --- | --- |
| Nginx | `nginx` | 公网入口、反向代理、托管 KOC 领取页 | 配置随镜像 |
-| KOC 服务 | `app` | Next.js 后台、API、MCP、每天 10:00 自动采集 | 上传目录挂载卷 |
+| KOC 服务 | `app` | Next.js 后台、API、MCP、每天 09:00 自动采集 | 上传目录挂载卷 |
| MySQL 8 | `mysql` | 任务、笔记、领取、回填、账号、采集和用户数据 | MySQL 数据卷 |
访问路径:
@@ -53,7 +53,7 @@ cp .env.self-hosted.example .env.self-hosted
| `KOC_MCP_API_KEY` | Agent 调用 KOC LOOP MCP 的独立 Bearer 密钥 |
| `FEISHU_APP_ID` / `FEISHU_APP_SECRET` | 读取飞书内容表和配图 |
| `AI_TOOL_CENTER_MCP_URL` / `AI_TOOL_CENTER_MCP_KEY` | 小红书公开数据采集服务 |
-| `ENABLE_SCHEDULER` | 是否启用每天 10:00 自动采集,生产保持 `true` |
+| `ENABLE_SCHEDULER` | 是否启用每天 09:00 自动采集,生产保持 `true` |
密钥必须由密码管理器生成,禁止提交到 Git、聊天、部署日志或 URL。三个业务密钥 `ADMIN_INTERNAL_TOKEN`、`KOC_MCP_API_KEY`、`AI_TOOL_CENTER_MCP_KEY` 不得复用。
@@ -189,7 +189,7 @@ docker compose --env-file .env.self-hosted \
| `/api/health` 返回 503 | 查看 MySQL 容器健康状态与应用数据库变量 |
| 登录页可开但登录失败 | 确认迁移完成、超级管理员变量仅用于初始化 |
| 配图或截图 404 | 检查 `upload_data` 卷和 `UPLOAD_DIR=/data/koc/uploads` |
-| 10:00 未自动采集 | 检查 `ENABLE_SCHEDULER=true`、服务器日志和采集 MCP 网络 |
+| 09:00 未自动采集 | 检查 `ENABLE_SCHEDULER=true`、服务器日志和采集 MCP 网络 |
| MCP 401 | 检查请求头是否为 `Authorization: Bearer ` |
| 飞书读取失败 | 检查应用权限、文档授权和服务器到飞书 OpenAPI 的网络 |
diff --git a/docs/KOC LOOP 部署指南.md b/docs/KOC LOOP 部署指南.md
index 57d29cf..383b6b3 100644
--- a/docs/KOC LOOP 部署指南.md
+++ b/docs/KOC LOOP 部署指南.md
@@ -237,10 +237,10 @@ npm run db:generate
运营后台 Worker 配置了 Cloudflare Cron:
```text
-0 2 * * *
+0 1 * * *
```
-Cloudflare Cron 使用 UTC,`02:00 UTC` 对应北京时间每天 `10:00`。定时任务会:
+Cloudflare Cron 使用 UTC,`01:00 UTC` 对应北京时间每天 `09:00`。定时任务会:
1. 确认数据库结构;
2. 执行当天已创建的笔记数据采集任务;
diff --git a/lib/collection-service.ts b/lib/collection-service.ts
index 71a8d73..f0022e1 100644
--- a/lib/collection-service.ts
+++ b/lib/collection-service.ts
@@ -60,13 +60,23 @@ function shanghaiHourFromTimestamp(timestamp: number) {
);
}
+export function isCollectionScheduleDue(
+ scheduledDate: string,
+ timestamp: number,
+) {
+ const currentDate = shanghaiDateFromTimestamp(timestamp);
+ const currentHour = shanghaiHourFromTimestamp(timestamp);
+ return (
+ scheduledDate < currentDate ||
+ (scheduledDate === currentDate && currentHour >= 9)
+ );
+}
+
function dueSchedules(
startDate: string,
days: number[],
timestamp: number,
) {
- const currentDate = shanghaiDateFromTimestamp(timestamp);
- const currentHour = shanghaiHourFromTimestamp(timestamp);
return days
.map((scheduleDay) => ({
scheduleDay,
@@ -78,8 +88,7 @@ function dueSchedules(
): value is { scheduleDay: number; scheduledDate: string } =>
Boolean(
value.scheduledDate &&
- (value.scheduledDate < currentDate ||
- (value.scheduledDate === currentDate && currentHour >= 10)),
+ isCollectionScheduleDue(value.scheduledDate, timestamp),
),
);
}
@@ -131,8 +140,8 @@ export async function createCollectionRunTasks(
distribution.id,
scheduledDate,
scheduleDay,
- `${scheduledDate}T10:00:00+08:00`,
- `等待第${scheduleDay}天 10:00自动采集`,
+ `${scheduledDate}T09:00:00+08:00`,
+ `等待第${scheduleDay}天 09:00自动采集`,
),
);
}
@@ -162,7 +171,7 @@ export async function collectDistributionMetrics(
if (!current) throw new Error("分发记录不存在");
if (!current.publish_url) throw new Error("笔记尚未回填发布链接");
- const scheduledAt = `${scheduledDate}T10:00:00+08:00`;
+ const scheduledAt = `${scheduledDate}T09:00:00+08:00`;
const runId = uid("run");
await db
.prepare(
@@ -231,7 +240,7 @@ export async function collectDistributionMetrics(
const dayWeight = scheduleDay ?? 1;
const successDescription =
source === "automatic"
- ? `成功 · 第${dayWeight}天 10:00自动采集`
+ ? `成功 · 第${dayWeight}天 09:00自动采集`
: source === "catchup"
? `成功 · 第${dayWeight}天自动追采`
: "成功 · 手动采集";
diff --git a/lib/database.ts b/lib/database.ts
index fd849d3..71fc47b 100644
--- a/lib/database.ts
+++ b/lib/database.ts
@@ -155,6 +155,23 @@ export class DatabaseClient {
connection.release();
}
}
+
+ async transaction(operation: (database: DatabaseClient) => Promise) {
+ const pool = getPool();
+ const connection = await pool.getConnection();
+ try {
+ await connection.beginTransaction();
+ const transaction = new DatabaseClient(connection);
+ const result = await operation(transaction);
+ await connection.commit();
+ return result;
+ } catch (error) {
+ await connection.rollback();
+ throw error;
+ } finally {
+ connection.release();
+ }
+ }
}
declare global {
diff --git a/lib/distribution-release-service.ts b/lib/distribution-release-service.ts
new file mode 100644
index 0000000..f4e3c42
--- /dev/null
+++ b/lib/distribution-release-service.ts
@@ -0,0 +1,153 @@
+import type { DatabaseClient } from "./database";
+
+type ReleasableDistribution = {
+ id: string;
+ task_id: string;
+ task_type: string;
+ content_id: string;
+ content_title: string;
+ partner_id: string;
+ partner_name: string;
+ claim_id: string | null;
+ delegation_bundle_id: string | null;
+ publish_url: string | null;
+ result_submitted_at: string | null;
+};
+
+export class DistributionReleaseError extends Error {
+ readonly status: number;
+
+ constructor(message: string, status: number) {
+ super(message);
+ this.name = "DistributionReleaseError";
+ this.status = status;
+ }
+}
+
+export function distributionReleaseBlockReason(input: {
+ publishUrl?: string | null;
+ resultSubmittedAt?: string | null;
+ taskType?: string | null;
+}) {
+ if (input.publishUrl) return "已回填发布链接的笔记不能释放";
+ if (input.resultSubmittedAt) return "已提交结果截图的任务不能释放";
+ return null;
+}
+
+export async function releaseUnfinishedDistribution(
+ database: DatabaseClient,
+ distributionId: string,
+) {
+ if (!distributionId) {
+ throw new DistributionReleaseError("请选择需要释放的领取记录", 400);
+ }
+
+ return database.transaction(async (db) => {
+ const distribution = await db
+ .prepare(
+ `SELECT
+ d.id,
+ d.task_id,
+ t.task_type,
+ d.content_id,
+ c.title AS content_title,
+ d.partner_id,
+ p.name AS partner_name,
+ d.claim_id,
+ d.delegation_bundle_id,
+ d.publish_url,
+ d.result_submitted_at
+ FROM distributions d
+ JOIN tasks t ON t.id = d.task_id
+ JOIN contents c ON c.id = d.content_id
+ JOIN partners p ON p.id = d.partner_id
+ WHERE d.id = ?
+ FOR UPDATE`,
+ )
+ .bind(distributionId)
+ .first();
+
+ if (!distribution) {
+ throw new DistributionReleaseError("领取记录不存在或已被释放", 404);
+ }
+ const blocked = distributionReleaseBlockReason({
+ publishUrl: distribution.publish_url,
+ resultSubmittedAt: distribution.result_submitted_at,
+ taskType: distribution.task_type,
+ });
+ if (blocked) throw new DistributionReleaseError(blocked, 409);
+
+ await db
+ .prepare("DELETE FROM collection_runs WHERE distribution_id = ?")
+ .bind(distribution.id)
+ .run();
+ await db
+ .prepare("DELETE FROM distributions WHERE id = ?")
+ .bind(distribution.id)
+ .run();
+ await db
+ .prepare("UPDATE contents SET status = 'available' WHERE id = ?")
+ .bind(distribution.content_id)
+ .run();
+ await db
+ .prepare(
+ `UPDATE tasks
+ SET claimed_quantity = GREATEST(claimed_quantity - 1, 0)
+ WHERE id = ?`,
+ )
+ .bind(distribution.task_id)
+ .run();
+ await db
+ .prepare(
+ `UPDATE partners
+ SET claimed_total = GREATEST(claimed_total - 1, 0)
+ WHERE id = ?`,
+ )
+ .bind(distribution.partner_id)
+ .run();
+
+ if (distribution.delegation_bundle_id) {
+ await db
+ .prepare(
+ `UPDATE delegation_bundles
+ SET quantity = GREATEST(quantity - 1, 0),
+ status = CASE WHEN quantity <= 1 THEN 'revoked' ELSE status END,
+ revoked_at = CASE WHEN quantity <= 1 THEN CURRENT_TIMESTAMP ELSE revoked_at END,
+ updated_at = CURRENT_TIMESTAMP
+ WHERE id = ?`,
+ )
+ .bind(distribution.delegation_bundle_id)
+ .run();
+ }
+
+ if (distribution.claim_id) {
+ await db
+ .prepare(
+ `UPDATE claims
+ SET quantity = GREATEST(quantity - 1, 0)
+ WHERE id = ?`,
+ )
+ .bind(distribution.claim_id)
+ .run();
+ await db
+ .prepare(
+ `DELETE FROM claims
+ WHERE id = ?
+ AND quantity <= 0
+ AND NOT EXISTS (
+ SELECT 1 FROM distributions WHERE claim_id = ?
+ )`,
+ )
+ .bind(distribution.claim_id, distribution.claim_id)
+ .run();
+ }
+
+ return {
+ distributionId: distribution.id,
+ taskId: distribution.task_id,
+ contentId: distribution.content_id,
+ contentTitle: distribution.content_title,
+ partnerName: distribution.partner_name,
+ };
+ });
+}
diff --git a/lib/mcp-operations.ts b/lib/mcp-operations.ts
index 885013c..609b9c8 100644
--- a/lib/mcp-operations.ts
+++ b/lib/mcp-operations.ts
@@ -284,7 +284,7 @@ export async function setCollectionPlan(
collection_status_description = CASE WHEN latest_likes IS NULL THEN ? ELSE collection_status_description END,
updated_at = CURRENT_TIMESTAMP
WHERE task_id = ? AND COALESCE(publish_url, '') != ''`,
- ).bind(`已安排${normalizedDays.length}个采集日,每日10:00执行`, taskId),
+ ).bind(`已安排${normalizedDays.length}个采集日,每日09:00执行`, taskId),
]);
const created = await createCollectionRunTasks(db, taskId, startDate, normalizedDays);
const catchup = await runDueScheduledCollections(
diff --git a/lib/mcp-tools.ts b/lib/mcp-tools.ts
index 587c212..297749b 100644
--- a/lib/mcp-tools.ts
+++ b/lib/mcp-tools.ts
@@ -127,7 +127,7 @@ export function registerMcpOperationTools(server: McpServer, options: Options) {
"collection_plan_set",
{
title: "设置自动采集计划",
- description: "为任务设置开始日期及第1至第7天的自动采集日,系统在北京时间10:00执行。",
+ description: "为任务设置开始日期及第1至第7天的自动采集日,系统在北京时间09:00执行。",
inputSchema: z.object({
task_id: z.string().min(1),
start_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("北京时间开始日期 YYYY-MM-DD"),
diff --git a/lib/scheduler.ts b/lib/scheduler.ts
index 1ee8eae..6db9812 100644
--- a/lib/scheduler.ts
+++ b/lib/scheduler.ts
@@ -33,11 +33,11 @@ export function startScheduler() {
if (!isEnabled(getRuntimeEnv().ENABLE_SCHEDULER, true)) return;
if (globalThis.__kocLoopScheduler) return;
globalThis.__kocLoopScheduler = cron.schedule(
- "0 10 * * *",
+ "0 9 * * *",
() => void runDailyJob().catch((error) => {
console.error("[KOC LOOP] daily scheduler failed", error);
}),
{ timezone: "Asia/Shanghai", noOverlap: true },
);
- console.info("[KOC LOOP] scheduler enabled at 10:00 Asia/Shanghai");
+ console.info("[KOC LOOP] scheduler enabled at 09:00 Asia/Shanghai");
}
diff --git a/lib/sort-utils.ts b/lib/sort-utils.ts
new file mode 100644
index 0000000..388b3a5
--- /dev/null
+++ b/lib/sort-utils.ts
@@ -0,0 +1,26 @@
+export type SortDirection = "asc" | "desc";
+
+export function sortWithNullsLast(
+ items: T[],
+ valueFor: (item: T) => number | null,
+ direction: SortDirection,
+) {
+ return items
+ .map((item, index) => ({ item, index }))
+ .sort((left, right) => {
+ const leftValue = valueFor(left.item);
+ const rightValue = valueFor(right.item);
+ if (leftValue === null && rightValue === null) {
+ return left.index - right.index;
+ }
+ if (leftValue === null) return 1;
+ if (rightValue === null) return -1;
+ const compared = leftValue - rightValue;
+ return compared === 0
+ ? left.index - right.index
+ : direction === "asc"
+ ? compared
+ : -compared;
+ })
+ .map(({ item }) => item);
+}
diff --git a/tests/collection-schedule.test.mjs b/tests/collection-schedule.test.mjs
new file mode 100644
index 0000000..d24ef01
--- /dev/null
+++ b/tests/collection-schedule.test.mjs
@@ -0,0 +1,58 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+import { isCollectionScheduleDue } from "../lib/collection-service.ts";
+import { sortWithNullsLast } from "../lib/sort-utils.ts";
+
+test("runs the Beijing collection schedule from 09:00", () => {
+ const scheduledDate = "2026-08-12";
+ assert.equal(
+ isCollectionScheduleDue(
+ scheduledDate,
+ Date.parse("2026-08-12T00:59:59Z"),
+ ),
+ false,
+ );
+ assert.equal(
+ isCollectionScheduleDue(
+ scheduledDate,
+ Date.parse("2026-08-12T01:00:00Z"),
+ ),
+ true,
+ );
+ assert.equal(
+ isCollectionScheduleDue(
+ scheduledDate,
+ Date.parse("2026-08-13T00:00:00Z"),
+ ),
+ true,
+ );
+ assert.equal(
+ isCollectionScheduleDue(
+ scheduledDate,
+ Date.parse("2026-08-11T16:00:00Z"),
+ ),
+ false,
+ );
+});
+
+test("sorts both directions while keeping missing values last", () => {
+ const values = [
+ { id: "missing-a", value: null },
+ { id: "middle", value: 20 },
+ { id: "high", value: 50 },
+ { id: "missing-b", value: null },
+ { id: "low", value: 10 },
+ ];
+ assert.deepEqual(
+ sortWithNullsLast(values, (item) => item.value, "asc").map(
+ (item) => item.id,
+ ),
+ ["low", "middle", "high", "missing-a", "missing-b"],
+ );
+ assert.deepEqual(
+ sortWithNullsLast(values, (item) => item.value, "desc").map(
+ (item) => item.id,
+ ),
+ ["high", "middle", "low", "missing-a", "missing-b"],
+ );
+});
diff --git a/tests/distribution-release.test.mjs b/tests/distribution-release.test.mjs
new file mode 100644
index 0000000..44559af
--- /dev/null
+++ b/tests/distribution-release.test.mjs
@@ -0,0 +1,58 @@
+import assert from "node:assert/strict";
+import { readFile } from "node:fs/promises";
+import test from "node:test";
+import {
+ DistributionReleaseError,
+ distributionReleaseBlockReason,
+} from "../lib/distribution-release-service.ts";
+
+test("allows only unfinished assignments to return to the claim pool", () => {
+ assert.equal(
+ distributionReleaseBlockReason({
+ publishUrl: null,
+ resultSubmittedAt: null,
+ taskType: "content_publish",
+ }),
+ null,
+ );
+ assert.equal(
+ distributionReleaseBlockReason({
+ publishUrl: "https://www.xiaohongshu.com/discovery/item/example",
+ resultSubmittedAt: null,
+ taskType: "content_publish",
+ }),
+ "已回填发布链接的笔记不能释放",
+ );
+ assert.equal(
+ distributionReleaseBlockReason({
+ publishUrl: null,
+ resultSubmittedAt: "2026-08-12 09:00:00",
+ taskType: "screenshot_collect",
+ }),
+ "已提交结果截图的任务不能释放",
+ );
+ assert.equal(new DistributionReleaseError("blocked", 409).status, 409);
+});
+
+test("releases a claim atomically and restores all counters", async () => {
+ const [service, database, actionRoute, adminApp] = await Promise.all([
+ readFile(new URL("../lib/distribution-release-service.ts", import.meta.url), "utf8"),
+ readFile(new URL("../lib/database.ts", import.meta.url), "utf8"),
+ readFile(new URL("../app/api/action/route.ts", import.meta.url), "utf8"),
+ readFile(new URL("../app/admin-app.tsx", import.meta.url), "utf8"),
+ ]);
+
+ assert.match(database, /async transaction/);
+ assert.match(service, /FOR UPDATE/);
+ assert.match(service, /DELETE FROM distributions WHERE id = \?/);
+ assert.match(service, /UPDATE contents SET status = 'available'/);
+ assert.match(service, /claimed_quantity = GREATEST\(claimed_quantity - 1, 0\)/);
+ assert.match(service, /claimed_total = GREATEST\(claimed_total - 1, 0\)/);
+ assert.match(service, /UPDATE claims/);
+ assert.match(service, /UPDATE delegation_bundles/);
+ assert.match(actionRoute, /body\.action === "release_distribution"/);
+ assert.match(actionRoute, /isManagerRequest/);
+ assert.match(adminApp, /确认释放这篇笔记/);
+ assert.match(adminApp, /已释放,可重新领取/);
+ assert.match(adminApp, /role="alertdialog"/);
+});
diff --git a/tests/rendered-html.test.mjs b/tests/rendered-html.test.mjs
index a60351b..434accd 100644
--- a/tests/rendered-html.test.mjs
+++ b/tests/rendered-html.test.mjs
@@ -75,6 +75,8 @@ test("organizes distribution and recovery by task and imports the verified Feish
assert.match(adminApp, /选择一个数据回收任务/);
assert.match(adminApp, /读取表格/);
assert.match(adminApp, /当前仅展示/);
+ assert.match(adminApp, /release_distribution/);
+ assert.match(adminApp, /确认释放这篇笔记/);
assert.equal(snapshot.sheetId, "954953");
assert.equal(snapshot.rows.length, 61);
assert.equal(
@@ -229,6 +231,11 @@ test("supports task collection schedules and latest public metrics", async () =>
}
assert.match(adminApp, /选择开始日期与采集日/);
assert.match(adminApp, /第1天到第7天/);
+ assert.match(adminApp, /SortableRecoveryHeader/);
+ assert.match(adminApp, /sortRecoveryDistributions/);
+ assert.match(adminApp, /aria-sort=/);
+ assert.match(adminApp, /direction === "asc" \? "↑" : "↓"/);
+ assert.match(adminApp, /sortWithNullsLast/);
assert.match(adminApp, /内容 \/ 发布账号/);
assert.match(adminApp, /recovery-title-link/);
assert.match(adminApp, /打开小红书笔记/);
@@ -252,10 +259,11 @@ test("supports task collection schedules and latest public metrics", async () =>
assert.match(collectionService, /runDueScheduledCollections/);
assert.match(collectionService, /retryFailedCollections/);
assert.match(collectionService, /exposure IS NOT NULL AND views IS NOT NULL/);
- assert.match(collectionService, /等待第\$\{scheduleDay\}天 10:00自动采集/);
+ assert.match(collectionService, /等待第\$\{scheduleDay\}天 09:00自动采集/);
+ assert.match(collectionService, /isCollectionScheduleDue/);
assert.doesNotMatch(collectionService, /latestDueSchedule/);
assert.match(collectionService, /自动追采/);
- assert.match(scheduler, /0 10 \* \* \*/);
+ assert.match(scheduler, /0 9 \* \* \*/);
assert.match(scheduler, /backfillAccountProfiles/);
assert.match(scheduler, /Asia\/Shanghai/);
assert.match(bootstrapRoute, /backfillAccountProfiles/);