// Typed endpoint wrappers for the M5b browse experience. // // Mirrors the FastAPI routes in yunke-server/app/api/routers/. Each wrapper // drops `undefined`/empty params so the query string only carries set filters // (the backend treats missing as "no filter"). import { getJson, requestJson } from './client' import type { Account, CallChannel, CallStats, Channel, Customer, Department, Group, GroupMember, Message, MomentsList, Page, PhoneCall, SessionList, Sms, Staff, StaffChatStats, StaffStatsCounts, StaffStatsDetail, SyncRun, SyncState, SyncStatus, VoiceClip, } from './types' /** Drop entries whose value is `undefined`, `null`, or `''`. */ function clean(params: Record): Record { const out: Record = {} for (const [k, v] of Object.entries(params)) { if (v === undefined || v === null || v === '') continue out[k] = v } return out } export interface CustomerQuery { channel?: Channel | 'all' page?: number page_size?: number owner_account_id?: string friend_id?: string user_id?: string department_id?: string add_time_since?: number last_chat_since?: number /** 文本搜索: 昵称/微信号/备注名/手机号子串匹配(通讯录搜索框)。 */ q?: string } export interface DeclaredTotals { wechat: { declared_total: number; account_count: number } whatsapp: { declared_total: number; account_count: number } } export function getCustomerDeclaredTotal(): Promise { return getJson('/customers/declared-total') } export function getCustomers(query: CustomerQuery = {}): Promise> { return getJson>( '/customers', clean({ page: query.page ?? 1, page_size: query.page_size ?? 20, channel: query.channel === 'all' ? undefined : query.channel, owner_account_id: query.owner_account_id, friend_id: query.friend_id, user_id: query.user_id, department_id: query.department_id, add_time_since: query.add_time_since, last_chat_since: query.last_chat_since, q: query.q, }), ) } export interface MessageQuery { channel?: Channel /** 群聊按 room_id 查时可省略(chat_group owner 与消息 owner 可能错位)。 */ owner_account_id?: string talker_id?: string room_id?: string /** 结构化类型过滤(如 red_transfer),对齐云客聊天筛选。 */ msg_type?: string /** 全文模糊搜索。 */ q?: string start?: number end?: number page?: number page_size?: number } export function getMessages(query: MessageQuery): Promise> { return getJson>( '/messages', clean({ channel: query.channel, owner_account_id: query.owner_account_id, talker_id: query.talker_id, room_id: query.room_id, msg_type: query.msg_type, q: query.q, start: query.start, end: query.end, page: query.page ?? 1, page_size: query.page_size ?? 50, }), ) } export function getDepartments(): Promise { return getJson('/departments') } export interface StaffQuery { department_id?: string is_on_job?: boolean page?: number page_size?: number } export function getStaff(query: StaffQuery = {}): Promise> { return getJson>( '/staff', clean({ department_id: query.department_id, is_on_job: query.is_on_job, page: query.page ?? 1, page_size: query.page_size ?? 20, }), ) } export interface AccountQuery { /** channel filter; `'all'` (default) drops the param so both channels union. */ channel?: Channel | 'all' page?: number page_size?: number } export function getAccounts(query: AccountQuery = {}): Promise> { return getJson>( '/accounts', clean({ channel: query.channel === 'all' ? undefined : query.channel, page: query.page ?? 1, page_size: query.page_size ?? 20, }), ) } // --------------------------------------------------------------------------- // Chat groups // --------------------------------------------------------------------------- export interface GroupQuery { channel?: Channel | 'all' owner_account_id?: string user_id?: string /** 精确单行直查(群聊页群名解析)。 */ room_id?: string /** 群名称模糊搜索(通讯录群 tab 搜索框)。 */ q?: string page?: number page_size?: number } export function getGroups(query: GroupQuery = {}): Promise> { return getJson>( '/groups', clean({ channel: query.channel === 'all' ? undefined : query.channel, owner_account_id: query.owner_account_id, user_id: query.user_id, room_id: query.room_id, q: query.q, page: query.page ?? 1, page_size: query.page_size ?? 20, }), ) } export function getGroupMembers(roomId: string): Promise { // room_id can contain '@' — axios encodes params, but path segments need // explicit encoding so the FastAPI `{room_id}` path param matches. return getJson( `/groups/${encodeURIComponent(roomId)}/members`, ) } // --------------------------------------------------------------------------- // Phone / voice calls // --------------------------------------------------------------------------- export interface CallQuery { /** call-type filter; `'all'` (default) drops the param. */ channel?: CallChannel | 'all' user_id?: string /** ms-epoch inclusive window compared against start_call_time. */ start?: number end?: number /** voice-call peer kind: group = call_number ends '@chatroom'. */ scope?: 'friend' | 'group' page?: number page_size?: number } export function getCallStats(): Promise { return getJson('/calls/stats') } export function getCalls(query: CallQuery = {}): Promise> { return getJson>( '/calls', clean({ channel: query.channel === 'all' ? undefined : query.channel, user_id: query.user_id, start: query.start, end: query.end, scope: query.scope, page: query.page ?? 1, page_size: query.page_size ?? 20, }), ) } // --------------------------------------------------------------------------- // Voice clips (长语音片段 — read-only mirror of 云客 /wechatweb/call/list) // --------------------------------------------------------------------------- export interface VoiceClipQuery { /** 员工 userId 精确过滤(modal 从 CallList 行打开时传)。 */ user_id?: string /** 员工姓名模糊过滤(云客 modal 的「员工姓名」筛选项)。 */ user_real_name?: string /** 本地通道标记(wechat / whatsapp),非上游筛选。 */ channel?: string /** ms-epoch inclusive 窗口,与 end_time 比较(录音结束时间)。 */ start?: number end?: number /** 企业串码。 */ company_code?: string page?: number page_size?: number } export function getVoiceClips(query: VoiceClipQuery = {}): Promise> { return getJson>( '/voice-clips', clean({ user_id: query.user_id, user_real_name: query.user_real_name, channel: query.channel, start: query.start, end: query.end, company_code: query.company_code, page: query.page ?? 1, page_size: query.page_size ?? 10, }), ) } /** PATCH /voice-clips/{id}/match 响应(镜像 backend payload)。 */ export interface VoiceClipMatchResult { voice_clip_id: number matched_friend_id: string | null } /** * 手动匹配 voice_clip ↔ customer (LOCAL 写)。`friendId=null` 取消匹配。 * 该写操作只落本地镜像库,不回写云客。 */ export function matchVoiceClip( voiceClipId: number, friendId: string | null, ): Promise { return requestJson( 'patch', `/voice-clips/${voiceClipId}/match`, { friend_id: friendId }, ) } // --------------------------------------------------------------------------- // SMS // --------------------------------------------------------------------------- export interface SmsQuery { user_id?: string /** ms-epoch inclusive window compared against create_time. */ start?: number end?: number page?: number page_size?: number } export function getSms(query: SmsQuery = {}): Promise> { return getJson>( '/sms', clean({ user_id: query.user_id, start: query.start, end: query.end, page: query.page ?? 1, page_size: query.page_size ?? 20, }), ) } // --------------------------------------------------------------------------- // Staff chat stats (微信员工 5 数字 + 客户明细,全本地 message 聚合) // --------------------------------------------------------------------------- export interface StaffChatStatsQuery { owner_account_id: string /** ms-epoch inclusive 窗口(默认近 3 天,由后端兜底)。 */ start?: number end?: number } export function getStaffChatStats(query: StaffChatStatsQuery): Promise { return getJson( '/staff-chat-stats', clean({ owner_account_id: query.owner_account_id, start: query.start, end: query.end, }), ) } // --------------------------------------------------------------------------- // Chat sessions (账号会话列表:好友+群,今日消息数+最近联系时间) // --------------------------------------------------------------------------- export interface SessionQuery { owner_account_id: string channel?: Channel /** 1 = 只看已删除会话(云客侧标记),缺省 = 全部。 */ is_del?: number } export function getSessions(query: SessionQuery): Promise { return getJson( '/sessions', clean({ owner_account_id: query.owner_account_id, channel: query.channel, is_del: query.is_del, }), ) } // --------------------------------------------------------------------------- // 员工聊天窗口 (staff-chat, 对齐云客 wechat-window): 朋友圈 + 风控统计 // --------------------------------------------------------------------------- export interface MomentsQuery { wechat_id: string page?: number page_size?: number } export function getMoments(query: MomentsQuery): Promise { return getJson( '/moments', clean({ wechat_id: query.wechat_id, page: query.page, page_size: query.page_size, }), ) } /** staff-stats 明细 kind(与后端 KindParam Literal 6 值一致;无 sensitive——词库未接入)。 */ export type StaffStatsKind = | 'new_customers' | 'new_groups' | 'red_transfer' | 'del_customers' | 'blocked_customers' | 'voice_calls' export interface StaffStatsQuery { wechat_id: string /** 传 kind 走明细模式,缺省返回 7 数字 counts。 */ kind?: StaffStatsKind start?: number end?: number } export function getStaffStatsCounts( query: Omit, ): Promise { return getJson( '/staff-stats', clean({ wechat_id: query.wechat_id, start: query.start, end: query.end, }), ) } export function getStaffStatsDetail( query: StaffStatsQuery & { kind: StaffStatsKind }, ): Promise { return getJson( '/staff-stats', clean({ wechat_id: query.wechat_id, kind: query.kind, start: query.start, end: query.end, }), ) } // --------------------------------------------------------------------------- // Sync monitor // --------------------------------------------------------------------------- export function getSyncStatus(): Promise { return getJson('/sync/status') } // --------------------------------------------------------------------------- // Full-text search (messages + customers) // --------------------------------------------------------------------------- export type SearchType = 'messages' | 'customers' | 'all' export interface SearchQuery { q: string /** which section(s) to query; defaults to `all`. */ type?: SearchType channel?: Channel | 'all' /** ms-epoch inclusive window compared against message.timestamp. */ start?: number end?: number page?: number page_size?: number } /** * Combined search payload returned by `/api/search`. Each requested section is * present; unrequested sections are omitted entirely by the backend (so the * caller can branch on `section in payload`). */ export interface SearchPayload { messages?: Page customers?: Page } export function search(query: SearchQuery): Promise { return getJson( '/search', clean({ q: query.q, type: query.type ?? 'all', channel: query.channel === 'all' ? undefined : query.channel, start: query.start, end: query.end, page: query.page ?? 1, page_size: query.page_size ?? 20, }), ) } /** 登录:用户名/密码换共享 API token(成功后由调用方 setToken 持久化)。 */ export function login(username: string, password: string): Promise<{ token: string }> { return requestJson<{ token: string }>('post', '/auth/login', { username, password, }) } export type { Account, CallChannel, Channel, Customer, Department, Group, GroupMember, Message, MomentsList, Page, PhoneCall, SessionList, Sms, Staff, StaffChatStats, StaffStatsCounts, StaffStatsDetail, SyncRun, SyncState, SyncStatus, VoiceClip, }