Files
yunke/yunkes/src/api/index.ts
T
浅念 4c3bc3ee93 feat(staffchat): 统计分析7tab(分型明细列+计数徽标+近7天窗+msgid定位,对齐云客)
- StatsPane 占位替换: 7 tab(label 带 (N) 计数)+daterange 默认近7天(清空后端兜底)
- 明细列配置驱动(KIND_COLS);link 列跳 single/group 聊天;红包/转账行带 ?msgid 定位
- 语音通话列对齐 CallList(通话人/类型/呼入呼出/接通状态/时长/录音播放 dialog/时间/操作)
- 敏感词 tab 空态卡(词库未配置,待 C1 接入,零请求)
- 员工微信列用资料卡信息(明细行无 staff 字段,数据本属该员工)
- counts 与明细解耦;双 seq 竞态守卫(兄弟组件惯例)
- api: getStaffStatsDetail kind 收紧为 StaffStatsKind 联合(后端 Literal 6 值)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-25 19:32:14 +08:00

499 lines
13 KiB
TypeScript

// 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<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
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<DeclaredTotals> {
return getJson<DeclaredTotals>('/customers/declared-total')
}
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,
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<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,
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<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,
}),
)
}
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,
}),
)
}
// ---------------------------------------------------------------------------
// 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<Page<Group>> {
return getJson<Page<Group>>(
'/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<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
/** voice-call peer kind: group = call_number ends '@chatroom'. */
scope?: 'friend' | 'group'
page?: number
page_size?: number
}
export function getCallStats(): Promise<CallStats> {
return getJson<CallStats>('/calls/stats')
}
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,
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<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,
}),
)
}
/** 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 },
)
}
// ---------------------------------------------------------------------------
// 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,
}),
)
}
// ---------------------------------------------------------------------------
// 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,
}),
)
}
// ---------------------------------------------------------------------------
// Chat sessions (账号会话列表:好友+群,今日消息数+最近联系时间)
// ---------------------------------------------------------------------------
export interface SessionQuery {
owner_account_id: string
channel?: Channel
/** 1 = 只看已删除会话(云客侧标记),缺省 = 全部。 */
is_del?: number
}
export function getSessions(query: SessionQuery): Promise<SessionList> {
return getJson<SessionList>(
'/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<MomentsList> {
return getJson<MomentsList>(
'/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<StaffStatsQuery, 'kind'>,
): Promise<StaffStatsCounts> {
return getJson<StaffStatsCounts>(
'/staff-stats',
clean({
wechat_id: query.wechat_id,
start: query.start,
end: query.end,
}),
)
}
export function getStaffStatsDetail(
query: StaffStatsQuery & { kind: StaffStatsKind },
): Promise<StaffStatsDetail> {
return getJson<StaffStatsDetail>(
'/staff-stats',
clean({
wechat_id: query.wechat_id,
kind: query.kind,
start: query.start,
end: query.end,
}),
)
}
// ---------------------------------------------------------------------------
// Sync monitor
// ---------------------------------------------------------------------------
export function getSyncStatus(): Promise<SyncStatus> {
return getJson<SyncStatus>('/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<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,
}),
)
}
export type {
Account,
CallChannel,
Channel,
Customer,
Department,
Group,
GroupMember,
Message,
MomentsList,
Page,
PhoneCall,
SessionList,
Sms,
Staff,
StaffChatStats,
StaffStatsCounts,
StaffStatsDetail,
SyncRun,
SyncState,
SyncStatus,
VoiceClip,
}