import type { AiSettings, AppNotification, Brand, BrandProduct, DataSource, ExternalThreadTarget, InspirationIdea, InspireAngle, InspireChatMessage, InspireElement, InspireSession, InspireSessionSummary, InviteMemberBrief, InviteTreeNode, Job, Member, MentionItem, MyInviteNetwork, OwnPost, OutboxBundle, Persona, PlacementSettings, ResearchHit, Role, ScoutHomeworkRecord, ScoutPost, ScoutRunBrief, ScoutScanContext, ScoutTopic, ThreadPlay, ThreadsAccount, TokenPair, TrendItem, TrendKind, ViralAnalysis, ViralSample, } 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; /** * 頭像 URL(data 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; /** 改密時必填 */ current_password?: string; new_password?: string; }; export type AuthRepo = { login(email: string, password: string): Promise<{ tokens: TokenPair; member: Member }>; logout(): Promise; me(): Promise; /** 更新自己的會員資料(mock 寫 session) */ updateProfile(patch: MemberProfilePatch): Promise; /** * 申請重設密碼。mock 一律回成功(不洩漏帳號是否存在); * 若為已知帳號會給 mock_reset_path 方便本機測試。 */ requestPasswordReset(email: string): Promise<{ ok: true; message: string; }>; /** * 用驗證碼/token 設定新密碼。 * email 必填(live);mock 可只靠 token。 */ resetPassword( token: string, newPassword: string, email?: string, ): Promise<{ ok: true; message: string }>; /** 寄送信箱驗證碼(需已登入) */ sendEmailVerificationCode(): Promise<{ ok: true; message: string; }>; /** 輸入驗證碼完成信箱驗證 */ verifyEmail(code: string): Promise; /** * 上傳圖片到物件儲存(MinIO/S3),回傳公開 URL。 * content 為 data URL;kind 預設 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; /** * 會員補填邀請碼(僅尚無邀請人時)。 * 成功回傳更新後的網絡。 */ applyInviteCode(code: string): Promise; /** 管理員:邀請森林 */ getTree(): Promise; /** 管理員:改邀請人(null = 獨立根) */ moveMember(uid: string, newParentUid: string | null): Promise; /** 管理員:可當邀請人的候選(排除 subtree) */ listParentCandidates(excludeSubtreeRootUid?: string): Promise; }; /** 管理員:島民列表與可寫操作 */ export type AdminUsersRepo = { /** @deprecated 請用 listUsersPage */ listUsers(): Promise; /** 分頁列表 page 從 1 起;query 可搜名稱 / Email / uid */ listUsersPage(opts?: { page?: number; pageSize?: number; query?: string; }): Promise; getUser(uid: string): Promise; /** 新增島民(回傳臨時密碼) */ createMember( input: AdminCreateMemberInput, ): Promise<{ user: MemberAdminView; temporary_password: string }>; /** 停權 / 復權 */ setSuspended(uid: string, suspended: boolean): Promise; /** 改驗證狀態 */ setEmailVerified(uid: string, verified: boolean): Promise; /** 指派角色(member / admin) */ setRoles(uid: string, roles: Role[]): Promise; /** * 幫使用者重設密碼。未傳 newPassword 則產生臨時密碼。 * 回傳 temporary_password 僅此次顯示。 */ resetPassword( uid: string, newPassword?: string, ): Promise<{ user: MemberAdminView; temporary_password: string }>; }; export type AccountsRepo = { list(): Promise; /** mock 專用:假連帳 */ createMock(): Promise; /** live:OAuth start(回授權 URL;fake provider 會指回 callback) */ oauthStart(): Promise<{ authorize_url: string; state: string }>; remove(id: string): Promise; refreshSession(id: string): Promise; }; export type PlaysRepo = { list(): Promise; listByPost(ownPostId: string): Promise; /** 依外部 Threads 連結列出方案(URL 正規化後比對) */ listByExternalUrl(url: string): Promise; /** 貼連結 → mock 解析目標貼文 */ resolveExternalLink(url: string): Promise; get(id: string): Promise; save(play: ThreadPlay): Promise; remove(id: string): Promise; submit(id: string): Promise; /** 真 AI 產劇本一步(reply / root) */ generateStep(opts: { personaId?: string; context: string; topic?: string; speakerLabel?: string; isLead?: boolean; mode?: "reply" | "root"; }): Promise; /** * 一次產完整劇本(背景 Job)。 * 完成後 play 已寫入 DB,前端應 reload play。 */ generateScript(playId: string): Promise<{ jobId: string; async: boolean }>; }; export type OutboxRepo = { list(): Promise; get(id: string): Promise; simulateSuccess(id: string): Promise; simulateRootFail(id: string): Promise; retryStep(bundleId: string, stepId: string): Promise; /** 刪除整筆發送佇列項目 */ remove(id: string): Promise; }; /** 任務列表分桶:定期排程 / 執行中 / 歷史 */ 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 JobsRepo = { /** * 分頁列表。傳 query 時走 tab+page;無 query 時預設 active 前 50 筆(相容舊呼叫)。 * 回傳完整 JobListResult;若只要陣列可用 .list。 */ list(query?: JobListQuery): Promise; get(id: string): Promise; startDemo(): Promise; /** 手動刪除(執行中不可刪;後端終態約 2 天後也會自動清) */ remove(id: string): Promise; }; export type NotificationsRepo = { list(): Promise; unreadCount(): Promise; markRead(id: string): Promise; markAllRead(): Promise; }; /** 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; saveAi(patch: Partial & { api_key?: string; research_api_key?: string }): Promise; getPlacement(): Promise; savePlacement( patch: Partial & { brave_api_key?: string; exa_api_key?: string }, ): Promise; /** @deprecated 優先用 getAi(provider) 一併取 models + selected_model */ listModels(provider: string): Promise; /** 平台 Threads App 是否已接 Meta(設定頁狀態卡) */ getThreadsStatus(): Promise; }; export type PersonasRepo = { list(): Promise; get(id: string): Promise; getActiveId(): Promise; setActiveId(id: string): Promise; save(persona: Persona): Promise; remove(id: string): Promise; analyzeFromText(id: string, rawText: string, sourceLabel?: string): Promise; analyzeFromAccount(id: string, username: string): Promise; }; export type OwnPostsRepo = { list(accountId?: string): Promise; lastSyncedAt(): Promise; sync(accountId: string): Promise; /** 點開貼文後才載留言(避免同步時一次打爆 API) */ loadReplies(postId: string): Promise; generateReply(opts: { postId: string; replyId?: string; personaId?: string; }): Promise; sendReply(opts: { postId: string; replyId?: string; text: string; /** 用哪個帳號送出(mock 寫入 username) */ accountId?: string; /** 附圖 URL(mock 記張數/可選縮圖) */ imageUrls?: string[]; }): Promise; analyzePost(postId: string): Promise; generateFromFormula(postId: string, personaId?: string): Promise<{ title: string; topic: string; root: string }>; }; export type MentionsRepo = { list(accountId?: string): Promise; /** 從 Threads Graph 同步「@我」提及(需 threads_manage_mentions) */ sync(accountId: string): Promise; generateReply(id: string, personaId?: string): Promise; markReplied(id: string, text?: string, imageUrls?: string[]): Promise; skip(id: string): Promise; }; export type InspirationRepo = { list(): Promise; /** 預設靈感榜:Threads 熱標(提示用) */ listTrends(kind?: TrendKind | "all"): Promise; refreshTrends(kind?: TrendKind | "all"): Promise; searchTrends(query: string): Promise; /** 元素庫 */ listElements(): Promise; saveElement(el: InspireElement): Promise; removeElement(id: string): Promise; /** 目前作用中的 session */ getSession(): Promise; saveSession(session: InspireSession): Promise; /** * 相容舊 API:開新對話並切過去(舊 session 保留)。 * 建議 UI 用 createSession。 */ clearSession(): Promise; /** 多對話列表 */ listSessions(): Promise; createSession(title?: string): Promise; activateSession(id: string): Promise; deleteSession(id: string): Promise; /** * 聊天/產文。pinnedIds 為本輪套用元素;會寫回 session。 * mode=generate 時 assistant 帶 draft.body(乾淨正文)。 */ chat(opts: { message: string; pinnedIds: string[]; mode: "chat" | "generate"; personaId?: string; sessionId?: string; /** generate:待改寫素材 */ material?: string; }): Promise<{ session: InspireSession; messages: InspireChatMessage[] }>; /** * 串流聊天/產文。onDelta 每收到一段正文就回呼;結束回完整 session。 */ chatStream( opts: { message: string; pinnedIds: string[]; mode: "chat" | "generate"; personaId?: string; sessionId?: string; material?: string; }, 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; }>; /** @deprecated 舊角度流;保留相容 */ sparkAngles( trendId: string, opts?: { personaId?: string | null; brandId?: string | null }, ): Promise; /** @deprecated */ sparkAnglesForTopic( label: string, opts?: { personaId?: string | null; brandId?: string | null; samples?: string[] }, ): Promise<{ trend: TrendItem; angles: InspireAngle[] }>; bookmarkTrend(trendId: string): Promise; saveIdea(idea: InspirationIdea): Promise; generate(topic: string, personaId?: string): Promise; listViral(): Promise; /** @deprecated */ sparkFromTrend( trendId: string, personaId?: string, opts?: { save?: boolean }, ): Promise; }; export type ResearchRepo = { search(query: string): Promise; }; 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, structureNotes?: string, ): Promise<{ text?: string; jobId?: string; async: boolean }>; /** 依指紋真 LLM 試產主貼 + 回文(可抓新聞話題) */ personaPreview(opts: { personaId: string; topic?: string; useNews?: boolean; }): Promise; analyzeViral(text: string): Promise; /** 單篇送出 Outbox */ publishSingle(opts: { accountId: string; text: string; title?: string; imageUrls?: string[]; /** * 預計發送時間 unix nanoseconds UTC。 * 未傳或 ≤ now → 立即/盡快排程。 */ schedule_start_at?: number; /** Threads 話題標籤(topic_tag,可不加 #) */ topicTag?: string; }): Promise; }; export type ScoutRepo = { listBrands(): Promise; get(id: string): Promise; getActiveBrandId(): Promise; setActiveBrandId(id: string): Promise; /** 對齊舊 BrandsPage:建立牌子 */ createBrand(input?: { display_name?: string; brief?: string }): Promise; saveBrand(brand: Brand): Promise; removeBrand(id: string): Promise; listProducts(brandId: string): Promise; /** 全部產品(探查下拉用) */ listAllProducts(): Promise; getProduct(id: string): Promise; saveProduct(product: BrandProduct): Promise; removeProduct(id: string): Promise; /** * 反著做:貼商品連結 → 抓取/推估後回填表單草稿(尚未存檔)。 * Phase B mock;live 應走後端爬頁。 */ importProductFromUrl(url: string): Promise<{ label: string; product_context: string; pain_points: string[]; match_tags: string[]; placement_url: string; source_note: string; }>; listTopics(brandId?: string): Promise; saveTopic(topic: ScoutTopic): Promise; removeTopic(id: string): Promise; /** * 意圖 + 可選產品 → 可審知識 brief(痛點/周邊/掃描詞) * 無產品 = theme 模式 */ prepareBrief(opts: { intent: string; brandId?: string | null; productId?: string | null; /** value=痛點/置入(預設);activity=關鍵字活躍 */ purpose?: "value" | "activity"; /** * 是否做完整上網功課(摘要+分層來源)。 * 預設 false:只組痛點/掃描詞,不擋海巡;背景再 deep 補齊。 */ deep?: boolean; }): Promise; /** 依 brief(含使用者勾選後的 scan_terms)建立背景海巡任務。 */ runScanFromBrief(brief: ScoutRunBrief): Promise<{ job: Job }>; /** @deprecated 用 prepareBrief */ getScanContext(brandId: string, productId?: string | null): Promise; /** 列出命中;brandId 空 = 全部(含主題模式) */ listPosts(brandId?: string | null): Promise; /** @deprecated 用 runScanFromBrief */ runScan( brandId: string, productId?: string | null, extraTerms?: string[], ): Promise<{ job: Job }>; draftOutreach(postId: string, personaId?: string): Promise; skipOutreach(postId: string): Promise; markPublished(postId: string): Promise; /** * 模擬發送外展回覆:寫入草稿、標記 published、可帶帳號 */ sendOutreach(opts: { postId: string; text: string; accountId?: string; personaId?: string; }): Promise; /** 刪除單則命中 */ removePost(postId: string): Promise; /** 刪除同一主題分組下全部命中 */ removeTheme(themeKey: string): Promise; /** 已完成的功課列表(持久化) */ listHomework(): Promise; /** 存一輪功課(同 theme_key 覆蓋) */ saveHomework(record: ScoutHomeworkRecord): Promise; getHomework(themeKey: string): Promise; removeHomework(themeKey: string): Promise; }; export type UsageRepo = { /** 目前登入者(或指定 uid)本月摘要 + 明細 */ getSummary(monthKey?: string, uid?: string): Promise; listEvents(limit?: number, uid?: string): Promise; getPlanId(): Promise; setPlanId(id: PlanId): Promise; getMemberPrefs(uid?: string): Promise; /** 管理員:設某人方案/無限 */ setMemberPrefs(uid: string, patch: Partial): Promise; /** 管理員:全體本月用量 */ getTenantSummary(monthKey?: string): Promise; /** 管理員:全體區間分析(日/月/年 + 購買 vs 消耗) */ getTenantAnalytics(query?: TenantAnalyticsQuery): Promise; /** * 會員自己購買方案(mock:假付款成功後才改 plan)。 * 管理員直接 setMemberPrefs 不算購買。 */ purchasePlan(plan_id: PlanId, opts?: { mock_ref?: string }): Promise; /** 自己的購買紀錄 */ listMyPurchases(limit?: number): Promise; }; 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; };