haixunMaster/lib/types/research.ts

140 lines
3.3 KiB
TypeScript
Raw 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.

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;
}
export type AccountConfidence = "high" | "medium" | "low";
export interface SimilarAccount {
username: string;
reason: string;
/** web=網路搜尋找到的真實連結threads=Threads 關鍵字搜尋熱門作者 */
source?: "web" | "threads" | "scan";
profileUrl?: string;
postUrl?: string;
confidence?: AccountConfidence;
lastActiveAt?: string;
}
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: "海巡發現",
};
export const SIMILAR_ACCOUNT_CONFIDENCE_LABELS: Record<
NonNullable<SimilarAccount["confidence"]>,
string
> = {
high: "高",
medium: "中",
low: "低",
};