haixunMaster/lib/types/research.ts

140 lines
3.3 KiB
TypeScript
Raw Permalink Normal View History

2026-06-21 12:50:31 +00:00
export type SearchIntent =
| "痛點"
| "知識"
| "經驗"
| "對比"
| "工具"
| "語錄"
| "需求"
| "求助";
export const SEARCH_INTENTS: SearchIntent[] = [
"痛點",
"知識",
"經驗",
"對比",
"工具",
"語錄",
"需求",
"求助",
];
export const SEARCH_TAG_TYPES = ["短詞", "情境", "帳號", "語錄"] as const;
/** 短詞=2-4字高命中 | 情境=5-8字 | 帳號=相似創作者 | 語錄=故事型小語 */
export type SearchTagType = "短詞" | "情境" | "帳號" | "語錄";
export interface SuggestedTag {
tag: string;
reason: string;
searchIntent?: SearchIntent;
searchType?: SearchTagType;
}
2026-06-21 16:28:26 +00:00
export type AccountConfidence = "high" | "medium" | "low";
2026-06-21 12:50:31 +00:00
export interface SimilarAccount {
username: string;
reason: string;
/** web=網路搜尋找到的真實連結threads=Threads 關鍵字搜尋熱門作者 */
source?: "web" | "threads" | "scan";
profileUrl?: string;
postUrl?: string;
2026-06-21 16:28:26 +00:00
confidence?: AccountConfidence;
lastActiveAt?: string;
2026-06-21 12:50:31 +00:00
}
export interface ResearchMap {
audienceSummary: string;
contentGoal: string;
questions: string[];
pillars: string[];
suggestedTags: SuggestedTag[];
similarAccounts?: SimilarAccount[];
exclusions: string[];
}
export type QualityTier = "GOOD" | "OK" | "EXCLUDE";
export interface QualityAssessment {
id: string;
relevanceScore: number;
placementScore?: number;
qualityTier: QualityTier;
qualityReason: string;
placementReason?: string;
}
export interface MatrixRow {
sortOrder: number;
searchTag: string;
angle: string;
hook: string;
text: string;
referenceNotes: string;
sourcePermalinks: string[];
rationale: string;
}
export function parseResearchMap(raw: string | null | undefined): ResearchMap | null {
if (!raw) return null;
try {
return JSON.parse(raw) as ResearchMap;
} catch {
return null;
}
}
export function parseSelectedTags(raw: string | null | undefined): string[] {
if (!raw) return [];
try {
const parsed = JSON.parse(raw) as unknown;
return Array.isArray(parsed) ? parsed.filter((t): t is string => typeof t === "string") : [];
} catch {
return [];
}
}
export function isAccountTag(tag: string): boolean {
return tag.startsWith("@");
}
export function normalizeUsername(tag: string): string {
return tag.replace(/^@/, "").trim();
}
export function threadsProfileUrl(username: string): string | null {
const clean = normalizeUsername(username);
if (!clean) return null;
return `https://www.threads.com/@${encodeURIComponent(clean)}`;
}
const THREADS_POST_URL_RE =
/(?:https?:)?\/\/(?:www\.)?threads\.(?:com|net)\/@([a-zA-Z0-9._]+)\/post\/([^/?#\s]+)/i;
/** 從任意 Threads 網址正規化成可開啟的貼文連結 */
export function normalizeThreadsPostUrl(url: string): string | null {
const trimmed = url.trim();
if (!trimmed) return null;
const match = trimmed.match(THREADS_POST_URL_RE);
if (!match) return null;
return `https://www.threads.com/@${match[1]}/post/${match[2]}`;
}
export const SIMILAR_ACCOUNT_SOURCE_LABELS: Record<
NonNullable<SimilarAccount["source"]>,
string
> = {
web: "網路搜尋",
threads: "Threads 搜尋",
scan: "海巡發現",
2026-06-21 16:28:26 +00:00
};
export const SIMILAR_ACCOUNT_CONFIDENCE_LABELS: Record<
NonNullable<SimilarAccount["confidence"]>,
string
> = {
high: "高",
medium: "中",
low: "低",
2026-06-21 12:50:31 +00:00
};