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