haixunMaster/lib/jobs/progress.ts

134 lines
5.0 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
}