From 7b0e1601b7418e691e77a06f4de4a6326723a73b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E5=BF=B5?= Date: Thu, 10 Sep 2026 09:57:44 +0800 Subject: [PATCH] =?UTF-8?q?feat(management):=20=E5=89=8D=E7=AB=AFAPI=20cli?= =?UTF-8?q?ent=E2=80=94=E2=80=94=E7=AE=A1=E7=90=86=E7=AB=AF=E7=82=B9typed?= =?UTF-8?q?=E5=A5=91=E7=BA=A6(=E6=89=B9=E6=AC=A1B=E5=86=BB=E7=BB=93?= =?UTF-8?q?=E9=9D=A2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 对批次B as-built逐键核对后相对任务稿的修正(以真机响应为准): - DashboardResp 补 caliber_notes(总述口径说明) - DrilldownResp 补 page/page_size/scope(各分支均返回) - FactDrillRow 补 chat_hint(owner+talker 直达聊天记录) - LifecycleDrillRow.nickname 收窄为非空(后端 friend_id 兜底) - EmployeeRow.metrics 注明 user_id 键为字符串透传字段 Co-Authored-By: Claude Code --- yunkes/src/api/management.ts | 72 ++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 yunkes/src/api/management.ts diff --git a/yunkes/src/api/management.ts b/yunkes/src/api/management.ts new file mode 100644 index 0000000..3a0656a --- /dev/null +++ b/yunkes/src/api/management.ts @@ -0,0 +1,72 @@ +// 经营管理板块 API(批次C)。契约以批次B终评冻结面为准: +// None≠0(无样本分位/率=null);scope.channel 回显原值;drilldown 白名单六值。 +// 已对照批次B as-built(management.py/api_assembly.py)逐键核对: +// - dashboard 另有 caliber_notes(总述口径说明,Record) +// - drilldown 各分支均返回 page/page_size/scope(与 items/total 同层) +// - first_reply_* 行含 chat_hint(owner+talker 直达聊天记录) +// - lifecycle 行 nickname 恒非空(后端 friend_nickname→remark→friend_id 兜底) +import { getJson } from './client' +import type { Channel } from './types' + +export type MgmtChannel = 'all' | Channel +export type CompareMode = 'previous' | 'personal_baseline' | 'team_median' +export type DrilldownCode = + | 'first_reply_p90' | 'first_reply_p50' | 'unreplied' + | 'cooling_customers' | 'dormant_customers' | 'call_followup' + +export interface MgmtScope { channel: MgmtChannel; start: number; end: number; compare?: CompareMode; view?: string; user_id?: string; metric_code?: string } +export interface Kpis { + active_customers: number; active_employees: number + msg_total: number; cust_threads: number; threads_answered: number + call_count: number; call_seconds: number; answered_call: number + lifecycle: { new: number; cooling: number; dormant: number; reactivated: number } +} +export interface CommTrendPoint { day: number; msg_total: number; active_customers: number; call_cnt: number } +export interface SvcTrendPoint { day: number; fr_p50_ms: number | null; fr_p90_ms: number | null; reply_2h_rate: number | null } +export interface DimSummary { [dim: string]: { name: string; weight: number; scored_count: number; pending_count: number; insufficient_count: number; avg_score: number | null } } +export interface AnomalyCard { + rule_code: string; severity: 'critical' | 'warning'; employee_user_id: string + title: string; current_value: number; baseline_value: number | null; unit: string + sample_size: number; affected_count: number; reason: string + drilldown: { type: string; query: { metric_code?: string; user_id?: string } } + suggested_action: string +} +export interface DashboardResp { + scope: MgmtScope; data_as_of: number | null; metric_version: string; config_source: 'db' | 'default' + kpis: Kpis + communication_trend: CommTrendPoint[] + service_trend: SvcTrendPoint[] + lifecycle: Kpis['lifecycle'] + dimension_summary: DimSummary + anomalies: AnomalyCard[] + action_summary: { unreplied_2h: number; churn_real_30d: number; sensitive_pending: number } + caliber_notes: Record +} +export interface EmployeeRow { + user_id: string; name: string | null; is_on_job: boolean | null + // metrics=PeriodMetrics._asdict():数值字段外的 user_id 键是字符串(透传 + // 字段,UI 取身份用顶层 row.user_id,动态取数只碰数值键) + metrics: Record + comparisons: Record + score: { total_score: number | null; grade: string | null; score_status: string; trial: boolean; dimensions: Record } + anomaly_count: number; anomaly_weight: number +} +export interface EmployeesResp { total: number; page: number; page_size: number; rows: EmployeeRow[]; scope: MgmtScope; data_as_of: number | null; metric_version: string; config_source: 'db' | 'default' } +export interface FactDrillRow { day: number; channel: string; owner_account_id: string; peer_id: string; thread_end_ts: number; reply_ts: number; fr_ms: number; chat_hint: { owner_account_id: string; talker_id: string } } +export interface LifecycleDrillRow { friend_id: string; channel: string; nickname: string; last_day: number; state: string } +export interface CallDrillRow { call_id: string; channel: string; call_number: string | null; start_call_time: number; call_duration: number } +export interface DrilldownResp { total: number; page: number; page_size: number; scope: MgmtScope; items: Record[]; note?: string; truncated?: boolean; summary?: Record } +export interface MetricDefsResp { + definitions: Record + score_config: { version: string; name: string; trial: boolean; normalization: string; sample_guards: Record; grade_bounds: Record; dimensions: Record } + caliber_notes: string + config_source: 'db' | 'default' +} + +export const getDashboard = (channel: MgmtChannel, start: number, end: number, compare: CompareMode = 'previous') => + getJson('/management/dashboard', { channel, start, end, compare }) +export const getEmployees = (params: Record) => + getJson('/management/employees', params) +export const getEmployeeDrilldown = (userId: string, metricCode: DrilldownCode, start: number, end: number, channel: MgmtChannel, page = 1, pageSize = 20) => + getJson(`/management/employees/${userId}/drilldown`, { metric_code: metricCode, start, end, channel, page, page_size: pageSize }) +export const getMetricDefs = () => getJson('/management/metric-definitions')