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

419 lines
14 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,
InspireAngle,
InspireChatMessage,
InspireElement,
InspireSession,
Job,
Member,
MentionItem,
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;
email?: string;
bio?: string;
timezone?: string;
notify_email?: boolean;
/**
* 頭像 URLdata URL 或 https
* 傳 `null` 清除;`undefined` 不改。
*/
avatar_url?: string | null;
/** 改密時必填 */
current_password?: string;
new_password?: string;
};
export type AuthRepo = {
login(email: string, password: string): Promise<{ tokens: TokenPair; member: Member }>;
logout(): Promise<void>;
me(): Promise<Member | null>;
/** 更新自己的會員資料mock 寫 session */
updateProfile(patch: MemberProfilePatch): Promise<Member>;
/**
* 申請重設密碼。mock 一律回成功(不洩漏帳號是否存在);
* 若為已知帳號會給 mock_reset_path 方便本機測試。
*/
requestPasswordReset(email: string): Promise<{
ok: true;
message: string;
/** 僅 mock可點的重設路徑例如 /reset-password?token=… */
mock_reset_path?: string;
}>;
/** 用 token 設定新密碼mock */
resetPassword(token: string, newPassword: string): Promise<{ ok: true; message: string }>;
/** 寄送信箱驗證碼(需已登入) */
sendEmailVerificationCode(): Promise<{
ok: true;
message: string;
/** 僅 mock畫面上顯示驗證碼方便測試 */
mock_code?: string;
}>;
/** 輸入驗證碼完成信箱驗證 */
verifyEmail(code: string): Promise<Member>;
};
/** 管理者新增島民 */
export type AdminCreateMemberInput = {
display_name: string;
email: string;
password?: string;
roles?: Role[];
email_verified?: boolean;
bio?: string;
};
/** 管理員:島民列表與可寫操作 */
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[]>;
createMock(): Promise<ThreadsAccount>;
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>;
};
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 JobsRepo = {
list(): Promise<Job[]>;
get(id: string): Promise<Job | null>;
startDemo(): Promise<Job>;
};
export type NotificationsRepo = {
list(): Promise<AppNotification[]>;
unreadCount(): Promise<number>;
markRead(id: string): Promise<void>;
markAllRead(): Promise<void>;
};
export type SettingsRepo = {
getDataSource(): DataSource;
setDataSource(ds: DataSource): void;
getAi(): 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>;
listModels(provider: string): Promise<string[]>;
};
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[]>;
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>;
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[]>;
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[]>;
searchTrends(query: string): Promise<TrendItem[]>;
/** 元素庫 */
listElements(): Promise<InspireElement[]>;
saveElement(el: InspireElement): Promise<InspireElement>;
removeElement(id: string): Promise<void>;
/** 當前聊天 session單一會話 */
getSession(): Promise<InspireSession>;
saveSession(session: InspireSession): Promise<InspireSession>;
clearSession(): Promise<InspireSession>;
/**
* 聊天產文。pinnedIds 為本輪套用元素;會寫回 session。
* mode=generate 時 assistant 帶 draft.body乾淨正文
*/
chat(opts: {
message: string;
pinnedIds: string[];
mode: "chat" | "generate";
}): Promise<{ session: InspireSession; messages: InspireChatMessage[] }>;
/** @deprecated 舊角度流;保留相容 */
sparkAngles(
trendId: string,
opts?: { personaId?: string | null; brandId?: string | null },
): Promise<InspireAngle[]>;
/** @deprecated */
sparkAnglesForTopic(
label: string,
opts?: { personaId?: string | null; brandId?: string | null; samples?: string[] },
): Promise<{ trend: TrendItem; angles: InspireAngle[] }>;
bookmarkTrend(trendId: string): Promise<InspirationIdea>;
saveIdea(idea: InspirationIdea): Promise<InspirationIdea>;
generate(topic: string, personaId?: string): Promise<InspirationIdea>;
listViral(): Promise<ViralSample[]>;
/** @deprecated */
sparkFromTrend(
trendId: string,
personaId?: string,
opts?: { save?: boolean },
): Promise<InspirationIdea>;
};
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 ComposeRepo = {
mimic(sourceText: string, personaId?: string): Promise<string>;
analyzeViral(text: string): Promise<ViralAnalysis>;
/** 單篇送出 Outbox */
publishSingle(opts: {
accountId: string;
text: string;
title?: string;
imageUrls?: 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;
}>;
listTopics(brandId?: string): Promise<ScoutTopic[]>;
saveTopic(topic: ScoutTopic): Promise<ScoutTopic>;
removeTopic(id: string): Promise<void>;
/**
* 意圖 + 可選產品 → 可審知識 brief痛點周邊掃描詞
* 無產品 = theme 模式
*/
prepareBrief(opts: {
intent: string;
brandId?: string | null;
productId?: string | null;
/** value痛點置入預設activity關鍵字活躍 */
purpose?: "value" | "activity";
/**
* 是否做完整上網功課(摘要+分層來源)。
* 預設 false只組痛點掃描詞不擋海巡背景再 deep 補齊。
*/
deep?: boolean;
}): Promise<ScoutRunBrief>;
/** 依 brief含使用者勾選後的 scan_terms海巡 */
runScanFromBrief(brief: ScoutRunBrief): Promise<ScoutPost[]>;
/** @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<ScoutPost[]>;
draftOutreach(postId: string, personaId?: string): Promise<ScoutPost>;
skipOutreach(postId: string): Promise<ScoutPost>;
markPublished(postId: string): Promise<ScoutPost>;
/**
* 模擬發送外展回覆:寫入草稿、標記 published、可帶帳號
*/
sendOutreach(opts: {
postId: string;
text: string;
accountId?: string;
personaId?: string;
}): Promise<ScoutPost>;
/** 刪除單則命中 */
removePost(postId: string): Promise<void>;
/** 刪除同一主題分組下全部命中 */
removeTheme(themeKey: 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 = {
/** 目前登入者(或指定 uid本月摘要 + 明細 */
getSummary(monthKey?: string, uid?: string): Promise<UsageMonthSummary>;
listEvents(limit?: number, uid?: string): Promise<UsageEvent[]>;
getPlanId(): Promise<PlanId>;
setPlanId(id: PlanId): Promise<void>;
getMemberPrefs(uid?: string): Promise<UsageMemberPrefs>;
/** 管理員:設某人方案/無限 */
setMemberPrefs(uid: string, patch: Partial<UsageMemberPrefs>): Promise<UsageMemberPrefs>;
/** 管理員:全體本月用量 */
getTenantSummary(monthKey?: string): Promise<TenantUsageSummary>;
/** 管理員:全體區間分析(日/月/年 + 購買 vs 消耗) */
getTenantAnalytics(query?: TenantAnalyticsQuery): Promise<TenantUsageAnalytics>;
/**
* 會員自己購買方案mock假付款成功後才改 plan
* 管理員直接 setMemberPrefs 不算購買。
*/
purchasePlan(plan_id: PlanId, opts?: { mock_ref?: string }): Promise<PlanPurchase>;
/** 自己的購買紀錄 */
listMyPurchases(limit?: number): Promise<PlanPurchase[]>;
};
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;
};