feat: KOC详情弹窗 + 任务中心单入口改版 + 资源池默认合作方/采集兜底
KOC 详情弹窗(合作方/数据回收/工作台三处入口): - 新增 KocDetailModal:基本信息 / 绑定KOC账号画像 / 参与任务记录 / 履约漏斗 - 工作台最近分发、回收队列账号名、截图回收合作方名、合作方页名称、分发详情头部合作方均可点开 - 分发表行点击仍打开分发详情(stopPropagation 隔离) 任务中心单入口改版(对齐"单任务双阶段"模型): - 任务卡发布/回收面板整块可点,全卡仅保留一个「查看明细」,回收面板重复按钮移除 - 详情页自述身份:动态 label(任务详情·分发与回收一体/仅分发)+ 阶段徽章(含回收·截止/未开通回收) - 内容分发表新增阶段筛选 chips(全部/待发布/已发布待数据/待收截图,未开通回收不显示截图项),筛选落空给专用空态文案 - 数据回收页入口文案改为「选择一个任务进入回收队列」,全站不再区分发布/回收两种任务类型 资源池 KOC 默认加入合作方 + 采集兜底: - partners 新增 released_account_id(mysql/0023):显式解绑/被顶替的账号不再被默认合作方同步回填 - 任务未设置采集计划时,默认按发布时间起连续采集 7 天,等待文案显示「等待D1 · 默认7天」
This commit is contained in:
@@ -1,7 +1,13 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import test from "node:test";
|
||||
import { isCollectionScheduleDue } from "../lib/collection-service.ts";
|
||||
import {
|
||||
DEFAULT_COLLECTION_DAYS,
|
||||
effectiveCollectionDays,
|
||||
isCollectionScheduleDue,
|
||||
parseCollectionDays,
|
||||
runDueScheduledCollections,
|
||||
} from "../lib/collection-service.ts";
|
||||
import { sortWithNullsLast } from "../lib/sort-utils.ts";
|
||||
|
||||
test("runs the Beijing collection schedule from 09:00", () => {
|
||||
@@ -78,3 +84,176 @@ test("a failed collection keeps historically successful rows on success", async
|
||||
// run 行照实记录失败,保留可观测性
|
||||
assert.match(source, /UPDATE collection_runs\s+SET status = 'failed'/);
|
||||
});
|
||||
|
||||
// —— 未设置采集计划时默认连采 7 天 ——
|
||||
|
||||
test("effective collection days fall back to 7 days when unset", () => {
|
||||
assert.deepEqual(effectiveCollectionDays(null), [1, 2, 3, 4, 5, 6, 7]);
|
||||
assert.deepEqual(effectiveCollectionDays("[]"), DEFAULT_COLLECTION_DAYS);
|
||||
assert.deepEqual(effectiveCollectionDays('{"bad":1}'), DEFAULT_COLLECTION_DAYS);
|
||||
assert.deepEqual(effectiveCollectionDays("[0,9]"), DEFAULT_COLLECTION_DAYS);
|
||||
assert.deepEqual(effectiveCollectionDays("[3,1,2,2]"), [1, 2, 3]);
|
||||
assert.deepEqual(parseCollectionDays("[7,2]"), [2, 7]);
|
||||
});
|
||||
|
||||
function fakeSchedulerDb({ tasks = [], published = [] } = {}) {
|
||||
const executed = [];
|
||||
const record = (sql, values) => executed.push({ sql, values });
|
||||
const db = {
|
||||
prepare(sql) {
|
||||
return {
|
||||
bind(...values) {
|
||||
return {
|
||||
async first() {
|
||||
record(sql, values);
|
||||
return null;
|
||||
},
|
||||
async run() {
|
||||
record(sql, values);
|
||||
return { meta: { changes: 0 } };
|
||||
},
|
||||
async all() {
|
||||
record(sql, values);
|
||||
if (/FROM tasks/.test(sql)) {
|
||||
const scoped = tasks.filter(
|
||||
(task) => !values[0] || task.id === values[0],
|
||||
);
|
||||
return {
|
||||
results: scoped.filter(
|
||||
(task) =>
|
||||
(task.task_type ?? "content_publish") !==
|
||||
"screenshot_collect",
|
||||
),
|
||||
};
|
||||
}
|
||||
if (/COALESCE\(d\.publish_time, d\.claimed_at\)/.test(sql)) {
|
||||
return {
|
||||
results: published.filter((row) => row.task_id === values[0]),
|
||||
};
|
||||
}
|
||||
// 无待采集行,避免触发真实 MCP 采集
|
||||
return { results: [] };
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
},
|
||||
async batch() {
|
||||
return [];
|
||||
},
|
||||
};
|
||||
return { db, executed };
|
||||
}
|
||||
|
||||
test("collects unplanned tasks on the default 7-day schedule from publish date", async () => {
|
||||
const { db, executed } = fakeSchedulerDb({
|
||||
tasks: [
|
||||
{
|
||||
id: "task-a",
|
||||
task_type: "content_publish",
|
||||
collection_start_date: null,
|
||||
collection_days: "[]",
|
||||
},
|
||||
],
|
||||
published: [
|
||||
// UTC 2026-10-09 01:30 → 上海 2026-10-09,D1–D4 在 10-12 09:00 前到期
|
||||
{ id: "dist-a", task_id: "task-a", publish_anchor: "2026-10-09 01:30:00" },
|
||||
],
|
||||
});
|
||||
const summary = await runDueScheduledCollections(
|
||||
db,
|
||||
Date.parse("2026-10-12T01:00:00Z"),
|
||||
{},
|
||||
);
|
||||
assert.equal(summary.dueTasks, 4);
|
||||
const anchorQuery = executed.find((entry) =>
|
||||
/COALESCE\(d\.publish_time/.test(entry.sql),
|
||||
);
|
||||
assert.ok(anchorQuery, "未设置计划时应查询每篇的发布时间");
|
||||
assert.equal(anchorQuery.values[0], "task-a");
|
||||
const scoped = executed.filter((entry) =>
|
||||
/SELECT d\.id FROM distributions d/.test(entry.sql),
|
||||
);
|
||||
assert.equal(scoped.length, 4);
|
||||
assert.ok(scoped.every((entry) => entry.values.includes("dist-a")));
|
||||
});
|
||||
|
||||
test("skips the default window once 7 days have passed since publish", async () => {
|
||||
const { db, executed } = fakeSchedulerDb({
|
||||
tasks: [
|
||||
{
|
||||
id: "task-old",
|
||||
task_type: "content_publish",
|
||||
collection_start_date: null,
|
||||
collection_days: "[]",
|
||||
},
|
||||
],
|
||||
published: [
|
||||
{ id: "dist-old", task_id: "task-old", publish_anchor: "2026-09-01 00:00:00" },
|
||||
],
|
||||
});
|
||||
const summary = await runDueScheduledCollections(
|
||||
db,
|
||||
Date.parse("2026-10-12T01:00:00Z"),
|
||||
{},
|
||||
);
|
||||
assert.equal(summary.dueTasks, 0);
|
||||
assert.ok(
|
||||
!executed.some((entry) =>
|
||||
/SELECT d\.id FROM distributions d/.test(entry.sql),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test("keeps the explicit task-level schedule when a plan is saved", async () => {
|
||||
const { db, executed } = fakeSchedulerDb({
|
||||
tasks: [
|
||||
{
|
||||
id: "task-b",
|
||||
task_type: "content_publish",
|
||||
collection_start_date: "2026-10-10",
|
||||
collection_days: "[2]",
|
||||
},
|
||||
],
|
||||
});
|
||||
const summary = await runDueScheduledCollections(
|
||||
db,
|
||||
Date.parse("2026-10-12T01:00:00Z"),
|
||||
{},
|
||||
);
|
||||
assert.equal(summary.dueTasks, 1);
|
||||
const needing = executed.filter((entry) =>
|
||||
/SELECT d\.id FROM distributions d/.test(entry.sql),
|
||||
);
|
||||
assert.deepEqual(needing.map((entry) => entry.values), [
|
||||
["task-b", "2026-10-11"],
|
||||
]);
|
||||
assert.ok(
|
||||
!executed.some((entry) => /COALESCE\(d\.publish_time/.test(entry.sql)),
|
||||
);
|
||||
});
|
||||
|
||||
test("ignores screenshot collection tasks in the scheduler", async () => {
|
||||
const { db, executed } = fakeSchedulerDb({
|
||||
tasks: [
|
||||
{
|
||||
id: "task-shot",
|
||||
task_type: "screenshot_collect",
|
||||
collection_start_date: null,
|
||||
collection_days: "[]",
|
||||
},
|
||||
],
|
||||
published: [
|
||||
{ id: "dist-shot", task_id: "task-shot", publish_anchor: "2026-10-11 00:00:00" },
|
||||
],
|
||||
});
|
||||
const summary = await runDueScheduledCollections(
|
||||
db,
|
||||
Date.parse("2026-10-12T01:00:00Z"),
|
||||
{},
|
||||
);
|
||||
assert.equal(summary.dueTasks, 0);
|
||||
assert.ok(
|
||||
!executed.some((entry) => /COALESCE\(d\.publish_time/.test(entry.sql)),
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user