haixunMaster/lib/jobs/progress.ts

134 lines
5.0 KiB
TypeScript
Raw Permalink Normal View History

2026-06-21 12:50:31 +00:00
import type { JobProgressDetail, JobTaskProgress } from "./types";
/** 海巡/分析任務的階段說明(給 UI 顯示用) */
export const SCAN_PIPELINE_STEPS = [
{ phase: "web", title: "網路搜尋", hint: "Brave Search 補充 Threads 貼文連結" },
{ phase: "tasks", title: "關鍵字搜尋", hint: "Threads API 或瀏覽器爬蟲(含真人延遲,可能較慢)" },
{ phase: "save", title: "寫入資料", hint: "把找到的貼文存進資料庫" },
{ phase: "replies", title: "抓取留言", hint: "瀏覽器讀取熱門貼文留言Dev 模式,可略過)" },
{ phase: "quality", title: "整理結果", hint: "依互動數排序,供你自行挑選" },
] as const;
export const ANALYZE_PIPELINE_STEPS = [
{ phase: "ai", title: "AI 研究地圖", hint: "產出受眾問題、內容支柱、排除項" },
{ phase: "accounts", title: "相似帳號", hint: "網路搜尋同領域創作者(僅爆款模仿)" },
] as const;
export const STYLE_8D_PIPELINE_STEPS = [
{ phase: "session", title: "確認連線", hint: "確認 Threads Extension 同步狀態" },
{ phase: "samples", title: "抓取樣本", hint: "讀取對標帳號近期貼文與公開互動" },
{ phase: "style", title: "AI 8D", hint: "分析語氣、結構、互動、主題、節奏、視覺、轉換與風險" },
{ phase: "store", title: "儲存策略", hint: "寫入目前帳號,供產文與回覆自動使用" },
] as const;
export const AI_TASK_PIPELINE_STEPS = [
{ phase: "ai-work", title: "AI 處理", hint: "模型正在生成或分析;完成時間依模型與內容量而異" },
] as const;
export function parseJobProgressDetail(
raw: string | null | undefined
): JobProgressDetail | null {
if (!raw) return null;
try {
return JSON.parse(raw) as JobProgressDetail;
} catch {
return null;
}
}
export function initTaskProgress(
tasks: Array<{ id: string; label: string }>
): JobProgressDetail["tasks"] {
return tasks.map((t) => ({
id: t.id,
label: t.label,
status: "pending" as const,
}));
}
export function setTaskStatus(
detail: JobProgressDetail,
taskId: string,
patch: Partial<NonNullable<JobProgressDetail["tasks"]>[number]>
) {
if (!detail.tasks) return;
const task = detail.tasks.find((t) => t.id === taskId);
if (!task) return;
Object.assign(task, patch);
}
export function formatTaskMetric(task: { id: string; found?: number }): string {
if (task.found == null) return "";
if (task.id === "replies") return ` · ${task.found} 則留言`;
if (task.id === "quality") return ` · ${task.found}`;
return ` · ${task.found}`;
}
const PIPELINE_TASK_IDS = new Set(["web", "save", "replies", "quality", "ai", "accounts", "session", "samples", "style", "store", "ai-work"]);
export function isPipelineTask(task: JobTaskProgress): boolean {
return PIPELINE_TASK_IDS.has(task.id);
}
/** 比「進行中」更具體的狀態說明 */
export function describeTaskStatus(
task: JobTaskProgress,
phase?: JobProgressDetail["phase"]
): string {
switch (task.status) {
case "pending":
return "等待";
case "done":
return "完成";
case "failed":
return "失敗";
case "cancelled":
return "已取消";
case "running":
if (task.id === "web") return "Brave 網搜中…";
if (task.id === "quality") return "整理結果中…";
if (task.id === "replies") return "瀏覽器抓留言中…";
if (task.id === "save") return "寫入中…";
if (task.id === "ai") return "AI 產出研究地圖中…";
if (task.id === "accounts") return "網路搜尋帳號中…";
if (task.id === "session") return "確認 Threads 連線中…";
if (task.id === "samples") return "抓取近期貼文中…";
if (task.id === "style") return "AI 8D 分析中…";
if (task.id === "store") return "儲存帳號策略中…";
if (task.id === "ai-work") return "AI 處理中…";
if (phase === "tasks") {
if (task.step) return task.step;
return "瀏覽器爬蟲中…";
}
return "進行中…";
default:
return "進行中…";
}
}
export function formatTaskElapsed(startedAt?: number): string {
if (!startedAt) return "";
const sec = Math.max(0, Math.floor((Date.now() - startedAt) / 1000));
if (sec < 60) return ` · ${sec}s`;
const min = Math.floor(sec / 60);
const rem = sec % 60;
return ` · ${min}m${rem}s`;
}
export function getActivePhaseHint(
phase: JobProgressDetail["phase"] | undefined,
jobType?: "scan" | "analyze-topic" | "style-8d" | "ai-task"
): string | null {
if (!phase) return null;
if (jobType === "analyze-topic") {
return ANALYZE_PIPELINE_STEPS.find((s) => s.phase === phase)?.hint ?? null;
}
if (jobType === "style-8d") {
return STYLE_8D_PIPELINE_STEPS.find((s) => s.phase === phase)?.hint ?? null;
}
if (jobType === "ai-task") {
return AI_TASK_PIPELINE_STEPS.find((s) => s.phase === phase)?.hint ?? null;
}
return SCAN_PIPELINE_STEPS.find((s) => s.phase === phase)?.hint ?? null;
}