2026-08-01 16:27:22 +08:00
|
|
|
// 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").
|
2026-08-11 10:43:34 +08:00
|
|
|
import { getJson, requestJson } from './client'
|
2026-08-01 16:27:22 +08:00
|
|
|
import type {
|
2026-08-01 17:47:50 +08:00
|
|
|
Account,
|
2026-08-01 16:38:47 +08:00
|
|
|
CallChannel,
|
2026-08-20 11:08:13 +08:00
|
|
|
CallStats,
|
2026-08-01 16:27:22 +08:00
|
|
|
Channel,
|
|
|
|
|
Customer,
|
|
|
|
|
Department,
|
2026-08-01 16:38:47 +08:00
|
|
|
Group,
|
|
|
|
|
GroupMember,
|
2026-08-01 16:27:22 +08:00
|
|
|
Message,
|
|
|
|
|
Page,
|
2026-08-01 16:38:47 +08:00
|
|
|
PhoneCall,
|
2026-08-08 15:13:30 +08:00
|
|
|
SessionList,
|
2026-08-01 16:38:47 +08:00
|
|
|
Sms,
|
2026-08-01 16:27:22 +08:00
|
|
|
Staff,
|
2026-08-07 18:42:19 +08:00
|
|
|
StaffChatStats,
|
2026-08-01 16:38:47 +08:00
|
|
|
SyncRun,
|
|
|
|
|
SyncState,
|
|
|
|
|
SyncStatus,
|
2026-08-10 18:37:29 +08:00
|
|
|
VoiceClip,
|
2026-08-01 16:27:22 +08:00
|
|
|
} from './types'
|
|
|
|
|
|
|
|
|
|
/** Drop entries whose value is `undefined`, `null`, or `''`. */
|
|
|
|
|
function clean(params: Record<string, unknown>): Record<string, unknown> {
|
|
|
|
|
const out: Record<string, unknown> = {}
|
|
|
|
|
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
|
2026-08-15 10:54:21 +08:00
|
|
|
friend_id?: string
|
2026-08-05 18:50:14 +08:00
|
|
|
user_id?: string
|
2026-08-01 16:27:22 +08:00
|
|
|
department_id?: string
|
2026-08-05 18:50:14 +08:00
|
|
|
add_time_since?: number
|
|
|
|
|
last_chat_since?: number
|
2026-08-01 16:27:22 +08:00
|
|
|
}
|
|
|
|
|
|
2026-08-20 14:04:06 +08:00
|
|
|
export interface DeclaredTotals {
|
|
|
|
|
wechat: { declared_total: number; account_count: number }
|
|
|
|
|
whatsapp: { declared_total: number; account_count: number }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getCustomerDeclaredTotal(): Promise<DeclaredTotals> {
|
|
|
|
|
return getJson<DeclaredTotals>('/customers/declared-total')
|
2026-08-20 13:44:39 +08:00
|
|
|
}
|
|
|
|
|
|
2026-08-01 16:27:22 +08:00
|
|
|
export function getCustomers(query: CustomerQuery = {}): Promise<Page<Customer>> {
|
|
|
|
|
return getJson<Page<Customer>>(
|
|
|
|
|
'/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,
|
2026-08-15 10:54:21 +08:00
|
|
|
friend_id: query.friend_id,
|
2026-08-05 18:50:14 +08:00
|
|
|
user_id: query.user_id,
|
2026-08-01 16:27:22 +08:00
|
|
|
department_id: query.department_id,
|
2026-08-05 18:50:14 +08:00
|
|
|
add_time_since: query.add_time_since,
|
|
|
|
|
last_chat_since: query.last_chat_since,
|
2026-08-01 16:27:22 +08:00
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface MessageQuery {
|
|
|
|
|
channel?: Channel
|
2026-08-22 16:50:56 +08:00
|
|
|
/** 群聊按 room_id 查时可省略(chat_group owner 与消息 owner 可能错位)。 */
|
|
|
|
|
owner_account_id?: string
|
2026-08-01 16:27:22 +08:00
|
|
|
talker_id?: string
|
|
|
|
|
room_id?: string
|
|
|
|
|
start?: number
|
|
|
|
|
end?: number
|
|
|
|
|
page?: number
|
|
|
|
|
page_size?: number
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getMessages(query: MessageQuery): Promise<Page<Message>> {
|
|
|
|
|
return getJson<Page<Message>>(
|
|
|
|
|
'/messages',
|
|
|
|
|
clean({
|
|
|
|
|
channel: query.channel,
|
|
|
|
|
owner_account_id: query.owner_account_id,
|
|
|
|
|
talker_id: query.talker_id,
|
|
|
|
|
room_id: query.room_id,
|
|
|
|
|
start: query.start,
|
|
|
|
|
end: query.end,
|
|
|
|
|
page: query.page ?? 1,
|
|
|
|
|
page_size: query.page_size ?? 50,
|
|
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getDepartments(): Promise<Department[]> {
|
|
|
|
|
return getJson<Department[]>('/departments')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface StaffQuery {
|
|
|
|
|
department_id?: string
|
|
|
|
|
is_on_job?: boolean
|
|
|
|
|
page?: number
|
|
|
|
|
page_size?: number
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getStaff(query: StaffQuery = {}): Promise<Page<Staff>> {
|
|
|
|
|
return getJson<Page<Staff>>(
|
|
|
|
|
'/staff',
|
|
|
|
|
clean({
|
|
|
|
|
department_id: query.department_id,
|
|
|
|
|
is_on_job: query.is_on_job,
|
|
|
|
|
page: query.page ?? 1,
|
|
|
|
|
page_size: query.page_size ?? 20,
|
|
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-01 17:47:50 +08:00
|
|
|
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<Page<Account>> {
|
|
|
|
|
return getJson<Page<Account>>(
|
|
|
|
|
'/accounts',
|
|
|
|
|
clean({
|
|
|
|
|
channel: query.channel === 'all' ? undefined : query.channel,
|
|
|
|
|
page: query.page ?? 1,
|
|
|
|
|
page_size: query.page_size ?? 20,
|
|
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-01 16:38:47 +08:00
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Chat groups
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
export interface GroupQuery {
|
|
|
|
|
channel?: Channel | 'all'
|
|
|
|
|
owner_account_id?: string
|
2026-08-05 19:06:27 +08:00
|
|
|
user_id?: string
|
2026-08-20 10:58:39 +08:00
|
|
|
/** 精确单行直查(群聊页群名解析)。 */
|
|
|
|
|
room_id?: string
|
2026-08-01 16:38:47 +08:00
|
|
|
page?: number
|
|
|
|
|
page_size?: number
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getGroups(query: GroupQuery = {}): Promise<Page<Group>> {
|
|
|
|
|
return getJson<Page<Group>>(
|
|
|
|
|
'/groups',
|
|
|
|
|
clean({
|
|
|
|
|
channel: query.channel === 'all' ? undefined : query.channel,
|
|
|
|
|
owner_account_id: query.owner_account_id,
|
2026-08-05 19:06:27 +08:00
|
|
|
user_id: query.user_id,
|
2026-08-20 10:58:39 +08:00
|
|
|
room_id: query.room_id,
|
2026-08-01 16:38:47 +08:00
|
|
|
page: query.page ?? 1,
|
|
|
|
|
page_size: query.page_size ?? 20,
|
|
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getGroupMembers(roomId: string): Promise<GroupMember[]> {
|
|
|
|
|
// room_id can contain '@' — axios encodes params, but path segments need
|
|
|
|
|
// explicit encoding so the FastAPI `{room_id}` path param matches.
|
|
|
|
|
return getJson<GroupMember[]>(
|
|
|
|
|
`/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
|
2026-08-19 16:48:22 +08:00
|
|
|
/** voice-call peer kind: group = call_number ends '@chatroom'. */
|
|
|
|
|
scope?: 'friend' | 'group'
|
2026-08-01 16:38:47 +08:00
|
|
|
page?: number
|
|
|
|
|
page_size?: number
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-20 11:08:13 +08:00
|
|
|
export function getCallStats(): Promise<CallStats> {
|
|
|
|
|
return getJson<CallStats>('/calls/stats')
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-01 16:38:47 +08:00
|
|
|
export function getCalls(query: CallQuery = {}): Promise<Page<PhoneCall>> {
|
|
|
|
|
return getJson<Page<PhoneCall>>(
|
|
|
|
|
'/calls',
|
|
|
|
|
clean({
|
|
|
|
|
channel: query.channel === 'all' ? undefined : query.channel,
|
|
|
|
|
user_id: query.user_id,
|
|
|
|
|
start: query.start,
|
|
|
|
|
end: query.end,
|
2026-08-19 16:48:22 +08:00
|
|
|
scope: query.scope,
|
2026-08-01 16:38:47 +08:00
|
|
|
page: query.page ?? 1,
|
|
|
|
|
page_size: query.page_size ?? 20,
|
|
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-10 18:37:29 +08:00
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// 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<Page<VoiceClip>> {
|
|
|
|
|
return getJson<Page<VoiceClip>>(
|
|
|
|
|
'/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,
|
|
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-11 10:43:34 +08:00
|
|
|
/** 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<VoiceClipMatchResult> {
|
|
|
|
|
return requestJson<VoiceClipMatchResult>(
|
|
|
|
|
'patch',
|
|
|
|
|
`/voice-clips/${voiceClipId}/match`,
|
|
|
|
|
{ friend_id: friendId },
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-01 16:38:47 +08:00
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// 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<Page<Sms>> {
|
|
|
|
|
return getJson<Page<Sms>>(
|
|
|
|
|
'/sms',
|
|
|
|
|
clean({
|
|
|
|
|
user_id: query.user_id,
|
|
|
|
|
start: query.start,
|
|
|
|
|
end: query.end,
|
|
|
|
|
page: query.page ?? 1,
|
|
|
|
|
page_size: query.page_size ?? 20,
|
|
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-07 18:42:19 +08:00
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// 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<StaffChatStats> {
|
|
|
|
|
return getJson<StaffChatStats>(
|
|
|
|
|
'/staff-chat-stats',
|
|
|
|
|
clean({
|
|
|
|
|
owner_account_id: query.owner_account_id,
|
|
|
|
|
start: query.start,
|
|
|
|
|
end: query.end,
|
|
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-08 15:13:30 +08:00
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Chat sessions (账号会话列表:好友+群,今日消息数+最近联系时间)
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
export interface SessionQuery {
|
|
|
|
|
owner_account_id: string
|
|
|
|
|
channel?: Channel
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getSessions(query: SessionQuery): Promise<SessionList> {
|
|
|
|
|
return getJson<SessionList>(
|
|
|
|
|
'/sessions',
|
|
|
|
|
clean({ owner_account_id: query.owner_account_id, channel: query.channel }),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-01 16:38:47 +08:00
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Sync monitor
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
export function getSyncStatus(): Promise<SyncStatus> {
|
|
|
|
|
return getJson<SyncStatus>('/sync/status')
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-05 11:46:21 +08:00
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// 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<Message>
|
|
|
|
|
customers?: Page<Customer>
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function search(query: SearchQuery): Promise<SearchPayload> {
|
|
|
|
|
return getJson<SearchPayload>(
|
|
|
|
|
'/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,
|
|
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-01 16:38:47 +08:00
|
|
|
export type {
|
2026-08-01 17:47:50 +08:00
|
|
|
Account,
|
2026-08-01 16:38:47 +08:00
|
|
|
CallChannel,
|
|
|
|
|
Channel,
|
|
|
|
|
Customer,
|
|
|
|
|
Department,
|
|
|
|
|
Group,
|
|
|
|
|
GroupMember,
|
|
|
|
|
Message,
|
|
|
|
|
Page,
|
|
|
|
|
PhoneCall,
|
2026-08-08 15:13:30 +08:00
|
|
|
SessionList,
|
2026-08-01 16:38:47 +08:00
|
|
|
Sms,
|
|
|
|
|
Staff,
|
2026-08-07 18:42:19 +08:00
|
|
|
StaffChatStats,
|
2026-08-01 16:38:47 +08:00
|
|
|
SyncRun,
|
|
|
|
|
SyncState,
|
|
|
|
|
SyncStatus,
|
2026-08-10 18:37:29 +08:00
|
|
|
VoiceClip,
|
2026-08-01 16:38:47 +08:00
|
|
|
}
|