140 lines
3.3 KiB
TypeScript
140 lines
3.3 KiB
TypeScript
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: "低",
|
||
}; |