419 lines
14 KiB
TypeScript
419 lines
14 KiB
TypeScript
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;
|
||
/**
|
||
* 頭像 URL(data 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;
|
||
/** 附圖 URL(mock 記張數/可選縮圖) */
|
||
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 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<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;
|
||
};
|