thread-master/apps/web/src/data/repos.ts

892 lines
30 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.

import type {
AiSettings,
AppNotification,
Brand,
BrandProduct,
DataSource,
ExternalThreadTarget,
InspirationIdea,
InspireChatMessage,
InspireElement,
InspireSession,
InspireSessionSummary,
InviteMemberBrief,
InviteTreeNode,
Job,
Member,
MentionItem,
MyInviteNetwork,
OwnPost,
OutboxBundle,
Persona,
PlacementSettings,
ResearchHit,
Role,
ScoutHomeworkRecord,
ScoutPost,
ScoutRunBrief,
ScoutScanContext,
ThreadPlay,
ThreadsAccount,
TokenPair,
TrendItem,
TrendKind,
ViralAnalysis,
} from "../domain/types";
import type { AdminUserListPage, MemberAdminView } from "../lib/tenantUsers";
import type { PlanPurchase } from "../lib/planPurchase";
import type {
PlanId,
TenantAnalyticsQuery,
TenantUsageAnalytics,
TenantUsageSummary,
UsageEvent,
UsageMemberPrefs,
UsageMonthSummary,
} from "../lib/usageMeter";
export type MemberProfilePatch = {
display_name?: string;
nickname?: string;
full_name?: string;
email?: string;
phone?: string;
bio?: string;
timezone?: string;
notify_email?: boolean;
/**
* 頭像 URLdata URL 或 https
* 傳 `null` 清除;`undefined` 不改。
*/
avatar_url?: string | null;
gender_code?: number;
birthdate?: number;
national?: string;
address?: string;
post_code?: string;
preferred_language?: string;
currency?: string;
/** skipped | completed只寫一次 */
onboarding_status?: "skipped" | "completed";
/** 改密時必填 */
current_password?: string;
new_password?: string;
};
export type AuthRepo = {
login(email: string, password: string): Promise<{ tokens: TokenPair; member: Member }>;
logout(): Promise<void>;
/**
* null 代表「確定未登入」(沒 token 或後端回 401
* 暫時性失敗斷網、後端重啟、5xx必須 throw不可回 null
* 否則呼叫端會把使用者當成登出而踢回登入頁。
*/
me(): Promise<Member | null>;
/** 更新自己的會員資料mock 寫 session */
updateProfile(patch: MemberProfilePatch): Promise<Member>;
/**
* 申請重設密碼。mock 一律回成功(不洩漏帳號是否存在);
* 若為已知帳號會給 mock_reset_path 方便本機測試。
*/
requestPasswordReset(email: string): Promise<{
ok: true;
message: string;
}>;
/**
* 用驗證碼token 設定新密碼。
* email 必填livemock 可只靠 token。
*/
resetPassword(
token: string,
newPassword: string,
email?: string,
): Promise<{ ok: true; message: string }>;
/** 寄送信箱驗證碼(需已登入) */
sendEmailVerificationCode(): Promise<{
ok: true;
message: string;
}>;
/** 輸入驗證碼完成信箱驗證 */
verifyEmail(code: string): Promise<Member>;
/**
* 上傳圖片到物件儲存MinIO/S3回傳公開 URL。
* content 為 data URLkind 預設 avatar。
*/
/** kind: avatar 永久temp 發文暫存(發成功/取消 outbox 後刪other 相容 */
uploadImage(content: string, kind?: "avatar" | "other" | "temp"): Promise<{ url: string; key?: string }>;
};
/** 管理者新增島民 */
export type AdminCreateMemberInput = {
display_name: string;
email: string;
password?: string;
roles?: Role[];
email_verified?: boolean;
bio?: string;
/** 邀請人 uid未傳 = 掛建立者null = 根 */
parent_uid?: string | null;
};
/**
* 邀請關係mock 支線)。
* 一般會員:邀請碼 + 邀請人 + 直邀;可補填邀請碼。
* 管理員:焦點鑽取 + 調整歸屬。
*/
export type InviteRepo = {
/** 目前登入者的邀請網絡 */
getMyNetwork(): Promise<MyInviteNetwork>;
/**
* 會員補填邀請碼(僅尚無邀請人時)。
* 成功回傳更新後的網絡。
*/
applyInviteCode(code: string): Promise<MyInviteNetwork>;
/** 管理員:邀請森林 */
getTree(): Promise<InviteTreeNode[]>;
/** 管理員改邀請人null = 獨立根) */
moveMember(uid: string, newParentUid: string | null): Promise<InviteMemberBrief>;
/** 管理員:可當邀請人的候選(排除 subtree */
listParentCandidates(excludeSubtreeRootUid?: string): Promise<InviteMemberBrief[]>;
};
/** 管理員:島民列表與可寫操作 */
export type AdminUsersRepo = {
/** @deprecated 請用 listUsersPage */
listUsers(): Promise<MemberAdminView[]>;
/** 分頁列表 page 從 1 起query 可搜名稱 / Email / uid */
listUsersPage(opts?: {
page?: number;
pageSize?: number;
query?: string;
}): Promise<AdminUserListPage>;
getUser(uid: string): Promise<MemberAdminView | null>;
/** 新增島民(回傳臨時密碼) */
createMember(
input: AdminCreateMemberInput,
): Promise<{ user: MemberAdminView; temporary_password: string }>;
/** 停權 / 復權 */
setSuspended(uid: string, suspended: boolean): Promise<MemberAdminView>;
/** 改驗證狀態 */
setEmailVerified(uid: string, verified: boolean): Promise<MemberAdminView>;
/** 指派角色member / admin */
setRoles(uid: string, roles: Role[]): Promise<MemberAdminView>;
/**
* 幫使用者重設密碼。未傳 newPassword 則產生臨時密碼。
* 回傳 temporary_password 僅此次顯示。
*/
resetPassword(
uid: string,
newPassword?: string,
): Promise<{ user: MemberAdminView; temporary_password: string }>;
};
export type AccountsRepo = {
list(): Promise<ThreadsAccount[]>;
/** mock 專用:假連帳 */
createMock(): Promise<ThreadsAccount>;
/** liveOAuth start回授權 URLfake provider 會指回 callback */
oauthStart(): Promise<{ authorize_url: string; state: string }>;
remove(id: string): Promise<void>;
refreshSession(id: string): Promise<ThreadsAccount>;
};
export type PlaysRepo = {
list(): Promise<ThreadPlay[]>;
listByPost(ownPostId: string): Promise<ThreadPlay[]>;
/** 依外部 Threads 連結列出方案URL 正規化後比對) */
listByExternalUrl(url: string): Promise<ThreadPlay[]>;
/** 貼連結 → mock 解析目標貼文 */
resolveExternalLink(url: string): Promise<ExternalThreadTarget>;
get(id: string): Promise<ThreadPlay | null>;
save(play: ThreadPlay): Promise<ThreadPlay>;
remove(id: string): Promise<void>;
submit(id: string): Promise<OutboxBundle>;
/** 真 AI 產劇本一步reply / root */
generateStep(opts: {
personaId?: string;
context: string;
topic?: string;
speakerLabel?: string;
isLead?: boolean;
mode?: "reply" | "root";
}): Promise<string>;
/**
* 一次產完整劇本(背景 Job
* 完成後 play 已寫入 DB前端應 reload play。
*/
generateScript(playId: string): Promise<{ jobId: string; async: boolean }>;
};
export type OutboxRepo = {
list(): Promise<OutboxBundle[]>;
get(id: string): Promise<OutboxBundle | null>;
simulateSuccess(id: string): Promise<OutboxBundle>;
simulateRootFail(id: string): Promise<OutboxBundle>;
retryStep(bundleId: string, stepId: string): Promise<OutboxBundle>;
/** 刪除整筆發送佇列項目 */
remove(id: string): Promise<void>;
};
/** 任務列表分桶:定期排程 / 執行中 / 歷史 */
export type JobListTab = "recurring" | "active" | "history";
export type JobListQuery = {
tab?: JobListTab;
page?: number;
pageSize?: number;
};
export type JobListResult = {
list: Job[];
pagination: {
page: number;
pageSize: number;
total: number;
totalPages: number;
};
};
export type ListPage<T> = {
list: T[];
pagination: {
page: number;
pageSize: number;
total: number;
totalPages: number;
};
};
export type ScoutRunListQuery = {
page?: number;
pageSize?: number;
brandId?: string | null;
mode?: import("../domain/types").ScoutMode;
};
export type ScoutResultListQuery = {
page?: number;
pageSize?: number;
};
export type JobsRepo = {
/**
* 分頁列表。傳 query 時走 tab+page無 query 時預設 active 前 50 筆(相容舊呼叫)。
* 回傳完整 JobListResult若只要陣列可用 .list。
*/
list(query?: JobListQuery): Promise<JobListResult>;
get(id: string): Promise<Job | null>;
startDemo(): Promise<Job>;
/** 手動刪除(執行中不可刪;後端終態約 2 天後也會自動清) */
remove(id: string): Promise<void>;
};
export type NotificationsRepo = {
list(): Promise<AppNotification[]>;
unreadCount(): Promise<number>;
markRead(id: string): Promise<void>;
markAllRead(): Promise<void>;
};
/** Threads OAuth 平台狀態App Secret 不回傳) */
export type ThreadsPlatformStatus = {
provider: "fake" | "meta" | string;
configured: boolean;
oauth_ready: boolean;
connect_path: string;
/** Meta Valid OAuth Redirect URIs 用 */
callback_url: string;
public_web_base: string;
app_id_masked?: string;
hint: string;
};
export type SettingsRepo = {
getDataSource(): DataSource;
setDataSource(ds: DataSource): void;
/**
* 一次拿:已存設定(含 selected model+ 模型清單。
* provider 可選:預覽另一個 provider 的 models不改存檔
*/
getAi(provider?: string): Promise<AiSettings>;
saveAi(patch: Partial<AiSettings> & { api_key?: string; research_api_key?: string }): Promise<AiSettings>;
getPlacement(): Promise<PlacementSettings>;
savePlacement(
patch: Partial<PlacementSettings> & { brave_api_key?: string; exa_api_key?: string },
): Promise<PlacementSettings>;
/** @deprecated 優先用 getAi(provider) 一併取 models + selected_model */
listModels(provider: string): Promise<string[]>;
/** 平台 Threads App 是否已接 Meta設定頁狀態卡 */
getThreadsStatus(): Promise<ThreadsPlatformStatus>;
};
export type PersonasRepo = {
list(): Promise<Persona[]>;
get(id: string): Promise<Persona | null>;
getActiveId(): Promise<string>;
setActiveId(id: string): Promise<void>;
save(persona: Persona): Promise<Persona>;
remove(id: string): Promise<void>;
analyzeFromText(id: string, rawText: string, sourceLabel?: string): Promise<Persona>;
analyzeFromAccount(id: string, username: string): Promise<Persona>;
};
export type OwnPostsRepo = {
list(accountId?: string): Promise<OwnPost[]>;
lastSyncedAt(): Promise<number | null>;
sync(accountId: string): Promise<OwnPost[]>;
/** 點開貼文後才載留言(避免同步時一次打爆 API */
loadReplies(postId: string): Promise<OwnPost>;
generateReply(opts: {
postId: string;
replyId?: string;
personaId?: string;
}): Promise<string>;
sendReply(opts: {
postId: string;
replyId?: string;
text: string;
/** 用哪個帳號送出mock 寫入 username */
accountId?: string;
/** 附圖 URLmock 記張數/可選縮圖) */
imageUrls?: string[];
}): Promise<OwnPost>;
manageReply(opts: { postId: string; replyId: string; hide: boolean }): Promise<OwnPost>;
setReplyControl(opts: { postId: string; replyControl: string }): Promise<OwnPost>;
analyzePost(postId: string): Promise<OwnPost>;
generateFromFormula(postId: string, personaId?: string): Promise<{ title: string; topic: string; root: string }>;
};
export type MentionsRepo = {
list(accountId?: string): Promise<MentionItem[]>;
/** 從 Threads Graph 同步「@我」提及(需 threads_manage_mentions */
sync(accountId: string): Promise<MentionItem[]>;
generateReply(id: string, personaId?: string): Promise<MentionItem>;
markReplied(id: string, text?: string, imageUrls?: string[]): Promise<MentionItem>;
skip(id: string): Promise<MentionItem>;
};
export type InspirationRepo = {
list(): Promise<InspirationIdea[]>;
/** 預設靈感榜Threads 熱標(提示用) */
listTrends(kind?: TrendKind | "all"): Promise<TrendItem[]>;
refreshTrends(kind?: TrendKind | "all"): Promise<TrendItem[]>;
/** 元素庫 */
listElements(): Promise<InspireElement[]>;
saveElement(el: InspireElement): Promise<InspireElement>;
removeElement(id: string): Promise<void>;
/** 目前作用中的 session */
getSession(): Promise<InspireSession>;
/**
* 相容舊 API開新對話並切過去舊 session 保留)。
* 建議 UI 用 createSession。
*/
clearSession(): Promise<InspireSession>;
/** 多對話列表 */
listSessions(): Promise<InspireSessionSummary[]>;
createSession(title?: string): Promise<InspireSession>;
activateSession(id: string): Promise<InspireSession>;
deleteSession(id: string): Promise<InspireSession>;
/**
* 聊天產文。pinnedIds 為本輪套用元素;會寫回 session。
* mode=generate 時 assistant 帶 draft.body乾淨正文
*/
chat(opts: {
message: string;
pinnedIds: string[];
mode: "chat" | "generate";
personaId?: string;
sessionId?: string;
/** chat本輪先用 Exa 查資料 */
useWeb?: boolean;
/** generate待改寫素材 */
material?: string;
}): Promise<{ session: InspireSession; messages: InspireChatMessage[] }>;
/**
* 串流聊天產文。onDelta 每收到一段正文就回呼;結束回完整 session。
* 中止時 reject `AbortError``isAbortError()` 可判斷),呼叫端不需當成失敗顯示。
*/
chatStream(
opts: {
message: string;
pinnedIds: string[];
mode: "chat" | "generate";
personaId?: string;
sessionId?: string;
useWeb?: boolean;
material?: string;
/** 使用者按停止或元件卸載時中止;不帶則跑到結束。 */
signal?: AbortSignal;
},
onDelta: (chunk: string) => void,
): Promise<{
session: InspireSession;
/** 實際送 AI 的完整 prompt與預覽同組裝 */
prompt?: string;
fingerprint?: string;
charCount?: number;
runeCount?: number;
sections?: string[];
}>;
/**
* 觀測:後端用與送出相同的 buildInspirePrompt 組裝真實 prompt不呼叫 AI、不扣額
* fingerprint 與送出後 done 事件的 prompt_fingerprint 可比對是否一致。
*/
previewPrompt(opts: {
message: string;
pinnedIds: string[];
mode: "chat" | "generate";
personaId?: string;
sessionId?: string;
material?: string;
}): Promise<{
prompt: string;
mode: string;
sections: string[];
blocks: Array<{ title: string; body: string }>;
fingerprint: string;
charCount: number;
runeCount: number;
pinned: Array<{ id: string; kind: string; title: string; body: string }>;
note?: string;
}>;
};
export type ResearchRepo = {
search(query: string): Promise<ResearchHit[]>;
};
export type MediaRepo = {
generateImage(prompt: string): Promise<{ id: string; url: string; prompt: string }>;
/** 本機選檔 → 可預覽附圖Phase B mock不真上傳 */
attachLocal(files: FileList | File[]): Promise<{ id: string; url: string; name?: string }[]>;
};
export type PersonaPreviewResult = {
topic: string;
topic_source: "news" | "manual" | "fallback" | string;
post_text: string;
reply_text: string;
notes?: string;
};
export type ComposeRepo = {
/**
* 仿寫:走背景 Job。
* - async=true → 回 jobId前端輪詢 job 取 payload.result_text
* - async=false → 同步 text測試降級
*/
mimic(
sourceText: string,
personaId?: string,
direction?: string,
structureNotes?: string,
): Promise<{ text?: string; jobId?: string; async: boolean }>;
/** 依指紋真 LLM 試產主貼 + 回文(可抓新聞話題) */
personaPreview(opts: {
personaId: string;
topic?: string;
useNews?: boolean;
}): Promise<PersonaPreviewResult>;
analyzeViral(text: string): Promise<ViralAnalysis>;
/** 單篇送出 Outbox */
publishSingle(opts: {
accountId: string;
text: string;
title?: string;
imageUrls?: string[];
/**
* 預計發送時間 unix nanoseconds UTC。
* 未傳或 ≤ now → 立即/盡快排程。
*/
schedule_start_at?: number;
/** Threads 話題標籤topic_tag可不加 # */
topicTag?: string;
/** Threads reply_control發文時設定誰可以回覆 */
replyControl?: string;
}): Promise<OutboxBundle>;
};
export type ScoutRepo = {
listBrands(): Promise<Brand[]>;
get(id: string): Promise<Brand | null>;
getActiveBrandId(): Promise<string>;
setActiveBrandId(id: string): Promise<void>;
/** 對齊舊 BrandsPage建立牌子 */
createBrand(input?: { display_name?: string; brief?: string }): Promise<Brand>;
saveBrand(brand: Brand): Promise<Brand>;
removeBrand(id: string): Promise<void>;
listProducts(brandId: string): Promise<BrandProduct[]>;
/** 全部產品(探查下拉用) */
listAllProducts(): Promise<BrandProduct[]>;
getProduct(id: string): Promise<BrandProduct | null>;
saveProduct(product: BrandProduct): Promise<BrandProduct>;
removeProduct(id: string): Promise<void>;
/**
* 反著做:貼商品連結 → 抓取/推估後回填表單草稿(尚未存檔)。
* Phase B mocklive 應走後端爬頁。
*/
importProductFromUrl(url: string): Promise<{
label: string;
product_context: string;
pain_points: string[];
match_tags: string[];
placement_url: string;
source_note: string;
}>;
/**
* 意圖 + 可選產品 → 可審知識 brief痛點周邊掃描詞
* 無產品 = theme 模式
*/
prepareBrief(opts: {
intent: string;
brandId?: string | null;
productId?: string | null;
/** demand找需求痛點provider找解法提供者activity非推廣主題互動 */
purpose?: "demand" | "provider" | "activity";
/**
* 是否做完整上網功課(摘要+分層來源)。
* 預設 false只組痛點掃描詞不擋海巡背景再 deep 補齊。
*/
deep?: boolean;
}): Promise<ScoutRunBrief>;
/** 依 brief含使用者勾選後的 scan_terms建立背景海巡任務。 */
runScanFromBrief(brief: ScoutRunBrief): Promise<{ job: Job; run: import("../domain/types").ScoutRun }>;
listRuns(query?: ScoutRunListQuery): Promise<ListPage<import("../domain/types").ScoutRun>>;
listRunPosts(
runId: string,
query?: ScoutResultListQuery,
): Promise<
ListPage<ScoutPost> & {
run: import("../domain/types").ScoutRun;
}
>;
/** @deprecated 用 prepareBrief */
getScanContext(brandId: string, productId?: string | null): Promise<ScoutScanContext>;
/** 列出命中brandId 空 = 全部(含主題模式) */
listPosts(brandId?: string | null): Promise<ScoutPost[]>;
/** @deprecated 用 runScanFromBrief */
runScan(
brandId: string,
productId?: string | null,
extraTerms?: string[],
): Promise<{ job: Job }>;
draftOutreach(postId: string, personaId?: string): Promise<ScoutPost>;
skipOutreach(postId: string): Promise<ScoutPost>;
markPublished(postId: string): Promise<ScoutPost>;
/** 升級為商機:複製成 Opportunity不改 outreach_status */
promoteToOpportunity(postId: string): Promise<{
opportunity_id: string;
status: string;
intent_band?: string;
intent_score?: number;
}>;
/**
* 模擬發送外展回覆:寫入草稿、標記 published、可帶帳號
*/
sendOutreach(opts: {
postId: string;
text: string;
accountId?: string;
personaId?: string;
}): Promise<ScoutPost>;
/** 刪除單則命中 */
removePost(postId: string): Promise<void>;
/** 刪除同一主題分組下全部命中 */
removeTheme(themeKey: string): Promise<void>;
/** 刪除獨立海巡批次及其結果 */
removeRun(runId: string): Promise<void>;
/** 已完成的功課列表(持久化) */
listHomework(): Promise<ScoutHomeworkRecord[]>;
/** 存一輪功課(同 theme_key 覆蓋) */
saveHomework(record: ScoutHomeworkRecord): Promise<ScoutHomeworkRecord>;
getHomework(themeKey: string): Promise<ScoutHomeworkRecord | null>;
removeHomework(themeKey: string): Promise<void>;
};
export type UsageRepo = {
/** 目前登入者本月摘要 + 明細 */
getSummary(monthKey?: string): Promise<UsageMonthSummary>;
listEvents(limit?: number): Promise<UsageEvent[]>;
getPlanId(): Promise<PlanId>;
/** 讀方案無限uid 需管理員權限,省略則為自己 */
getMemberPrefs(uid?: string): Promise<UsageMemberPrefs>;
/** 管理員:設某人方案/無限 */
setMemberPrefs(uid: string, patch: Partial<UsageMemberPrefs>): Promise<UsageMemberPrefs>;
/** 管理員:全體本月用量 */
getTenantSummary(monthKey?: string): Promise<TenantUsageSummary>;
/** 管理員:全體區間分析(日/月/年 + 購買 vs 消耗) */
getTenantAnalytics(query?: TenantAnalyticsQuery): Promise<TenantUsageAnalytics>;
createCheckoutSession(plan_id: PlanId, request_id: string): Promise<BillingCheckoutSession>;
getCheckoutSession(id: string): Promise<BillingCheckoutStatus>;
getBillingSubscription(): Promise<BillingSubscription>;
createBillingPortalSession(): Promise<BillingPortalSession>;
/** 自己的購買紀錄 */
listMyPurchases(limit?: number): Promise<PlanPurchase[]>;
};
export type BillingCheckoutSession = {
id: string;
session_id: string;
url: string;
expires_at: number;
};
export type BillingCheckoutStatus = {
id: string;
plan_id: PlanId;
checkout_status: string;
payment_status: string;
fulfillment_status: string;
};
export type BillingSubscription = {
plan_id: PlanId;
status: string;
current_period_end: number;
cancel_at_period_end: boolean;
};
export type BillingPortalSession = { url: string };
export type GrowthRepo = {
getOutcomeSummary(range?: "week" | "month"): Promise<import("../domain/types").OutcomeSummary>;
listOutcomes(page?: number, pageSize?: number): Promise<import("../domain/types").OutcomeEvent[]>;
reportConversion(
id: string,
amount?: number,
note?: string,
currency?: string,
): Promise<import("../domain/types").OutcomeEvent>;
getLatestCheckup(): Promise<import("../domain/types").WeeklyCheckup>;
generateCheckupNow(): Promise<import("../domain/types").WeeklyCheckup>;
listAccountHealth(): Promise<import("../domain/types").AccountHealth[]>;
listWorkspaces(): Promise<{
list: import("../domain/types").Workspace[];
current_workspace_id: string;
}>;
switchWorkspace(id: string): Promise<{
list: import("../domain/types").Workspace[];
current_workspace_id: string;
}>;
createWorkspace(name: string): Promise<import("../domain/types").Workspace>;
};
/**
* demand-radar 加值層。契約見 docs/product/demand-radar/spec.md §5.2。
* 尚未實作的能力後端回 501010`apiRequest` 會丟 ApiError呼叫端不得當成空結果。
*/
export type RadarRepo = {
getServiceProfile(): Promise<import("../domain/types").ServiceProfile>;
getRadarSchedule(): Promise<import("../domain/types").RadarSchedule>;
saveRadarSchedule(hours: number[]): Promise<import("../domain/types").RadarSchedule>;
saveServiceProfile(
patch: Omit<import("../domain/types").ServiceProfile, "exists" | "updated_at">,
): Promise<import("../domain/types").ServiceProfile>;
listWatches(
page?: number,
pageSize?: number,
status?: string,
contextMode?: import("../domain/types").RadarWatchContextMode,
brandId?: string,
productId?: string,
): Promise<{
list: import("../domain/types").RadarWatch[];
total: number;
active_count: number;
max_active: number;
profile_exists: boolean;
}>;
createWatch(input: {
terms: string[];
exclude_terms?: string[];
regions?: string[];
enabled?: boolean;
brand_id?: string;
product_id?: string;
}): Promise<import("../domain/types").RadarWatch>;
assignWatchProduct(
id: string,
brandId: string,
productId: string,
): Promise<import("../domain/types").RadarWatch>;
updateWatch(
id: string,
patch: { terms?: string[]; exclude_terms?: string[]; regions?: string[] },
): Promise<import("../domain/types").RadarWatch>;
pauseWatch(id: string): Promise<import("../domain/types").RadarWatch>;
resumeWatch(id: string): Promise<import("../domain/types").RadarWatch>;
archiveWatch(id: string): Promise<void>;
/** 永久刪除已封存的巡邏設定;歷史商機與巡邏紀錄保留。 */
deleteArchivedWatch(id: string): Promise<void>;
suggestWatchTerms(
input?: number | { limit?: number; brand_id?: string; product_id?: string },
): Promise<import("../domain/types").WatchTermSuggestion[]>;
triggerWatchSweep(id: string): Promise<{ job_id: string; sweep_id?: string }>;
getToday(filter?: {
brand_id?: string;
product_id?: string;
fit_band?: string;
}): Promise<import("../domain/types").RadarToday>;
listOpportunities(filter?: {
page?: number;
pageSize?: number;
band?: string;
status?: string;
watch_id?: string;
brand_id?: string;
product_id?: string;
fit_band?: string;
match_state?: string;
review_state?: import("../domain/types").OpportunityReviewState;
time_scope?: import("../domain/types").OpportunityTimeScope;
priority_band?: import("../domain/types").OpportunityPriorityBand;
sort?: string;
from?: number;
to?: number;
}): Promise<{ list: import("../domain/types").Opportunity[]; total: number }>;
getOpportunity(id: string): Promise<import("../domain/types").Opportunity>;
acceptOpportunity(id: string): Promise<{
opportunity_id: string;
contact_id?: string;
status: string;
}>;
dismissOpportunity(id: string, reason?: string): Promise<import("../domain/types").Opportunity>;
updateOpportunityReviewState(
id: string,
patch: {
state: import("../domain/types").OpportunityReviewState;
removal_reason?: import("../domain/types").OpportunityRemovalReason;
removal_note?: string;
duplicate_of_opportunity_id?: string;
},
): Promise<import("../domain/types").Opportunity>;
overrideOpportunity(
id: string,
patch: { band?: string; status?: string; note?: string },
): Promise<import("../domain/types").Opportunity>;
setPrimaryProduct(
id: string,
productId: string,
reason: string,
): Promise<import("../domain/types").Opportunity>;
listReplies(opportunityId: string): Promise<import("../domain/types").ReplyVariant[]>;
createReply(
opportunityId: string,
variant: import("../domain/types").ReplyVariantKind,
): Promise<import("../domain/types").ReplyVariant>;
/**
* channel: outbox | manual_copydm 僅 manual_copy。
* accountId 在 channel=outbox 時必填:要用哪個已連的 Threads 帳號真的送出。
*/
markReplyUsed(
opportunityId: string,
replyId: string,
channel: "outbox" | "manual_copy",
accountId?: string,
): Promise<{
reply: import("../domain/types").ReplyVariant;
health_advice?: string;
}>;
listSweeps(
page?: number,
pageSize?: number,
watchId?: string,
): Promise<{ list: import("../domain/types").RadarSweep[]; total: number }>;
/** 手動匯入商機P1貼 ThreadsFacebook 貼文網址+內文或 CSV 批次,走同一套五問判定 */
importOpportunities(
items: import("../domain/types").ManualImportItem[],
context?: { brand_id?: string; product_id?: string },
): Promise<{ results: import("../domain/types").ManualImportResult[] }>;
/** 立即探索:短詞 fan-out 搜尋 → 五問判定,寫入今日商機 */
exploreOpportunities(
terms: string[],
context?: { brand_id?: string; product_id?: string; preview_id?: string; credit_ceiling?: number; demand_input_version?: string; demand_map_version?: number },
): Promise<import("../domain/types").ExploreResult>;
getDemandMap(productId: string): Promise<import("../domain/types").DemandMap>;
updateDemandMap(productId: string, patch: Omit<import("../domain/types").DemandMap, "product_id" | "updated_at"> & { expected_map_version: number }): Promise<import("../domain/types").DemandMap>;
enrichDemandMap(productId: string, input: { preview_id: string; credit_ceiling: number; expected_map_version: number }): Promise<import("../domain/types").DemandMap>;
getCostPreview(input: { action: string; watch_id?: string; product_id?: string; candidate_limit?: number }): Promise<import("../domain/types").CostPreview>;
};
export type CrmRepo = {
listContacts(filter?: {
page?: number;
pageSize?: number;
query?: string;
stage?: string;
/** 留空不篩選truefalse 才篩 */
follow_up?: boolean;
band?: string;
sort?: "last_touch_at" | "intent_score";
}): Promise<{
list: import("../domain/types").Contact[];
total: number;
stage_counts: import("../domain/types").ContactStageCount[];
}>;
getContact(
id: string,
page?: number,
pageSize?: number,
): Promise<import("../domain/types").ContactDetail>;
/** 從工作名單移除;保留商機、接觸與成交歷史。 */
deleteContact(id: string): Promise<void>;
/** 允許任意跳轉,含回退、跳級與終態重開 */
updateStage(
id: string,
stage: import("../domain/types").ContactStage,
note?: string,
): Promise<import("../domain/types").Contact>;
setFollowUp(
id: string,
needsFollowUp: boolean,
days?: number,
): Promise<import("../domain/types").Contact>;
addNote(id: string, body: string): Promise<import("../domain/types").ContactTouch>;
mergeContact(id: string, sourceContactId: string): Promise<import("../domain/types").Contact>;
unmergeContact(id: string, mergedContactId: string): Promise<import("../domain/types").Contact>;
/** 成交一律由人回報;系統不自動判定 */
reportConversion(
id: string,
input: { amount?: number; currency?: string; note?: string },
): Promise<{ contact_id: string; outcome_id?: string; stage: string }>;
updateConversion(
id: string,
input: { amount?: number; currency?: string; note?: string },
): Promise<{ contact_id: string; outcome_id?: string; stage: string }>;
deleteConversion(id: string): Promise<void>;
listFollowUps(
page?: number,
pageSize?: number,
status?: string,
): Promise<{ list: import("../domain/types").FollowUp[]; total: number }>;
doneFollowUp(id: string): Promise<import("../domain/types").FollowUp>;
snoozeFollowUp(id: string, days: number): Promise<import("../domain/types").FollowUp>;
generateFollowUpMessage(id: string): Promise<string>;
getStats(from?: number, to?: number): Promise<import("../domain/types").CrmStats>;
};
export type Repos = {
dataSource: DataSource;
auth: AuthRepo;
accounts: AccountsRepo;
plays: PlaysRepo;
outbox: OutboxRepo;
jobs: JobsRepo;
notifications: NotificationsRepo;
settings: SettingsRepo;
personas: PersonasRepo;
ownPosts: OwnPostsRepo;
mentions: MentionsRepo;
inspiration: InspirationRepo;
research: ResearchRepo;
media: MediaRepo;
compose: ComposeRepo;
scout: ScoutRepo;
usage: UsageRepo;
adminUsers: AdminUsersRepo;
invite: InviteRepo;
growth: GrowthRepo;
radar: RadarRepo;
crm: CrmRepo;
};