merge: 合入本地 Ops 扩展与 origin/main(ECR-009–016)
保留远程用户侧 ECR-009–016 与本地 Ops 目录/RBAC/CMS/危机等能力;文档标注分叉期间 ECR 编号冲突。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -31,7 +31,17 @@ async function request<T>(method: string, path: string, body?: unknown): Promise
|
||||
headers,
|
||||
body: body === undefined ? undefined : JSON.stringify(body),
|
||||
})
|
||||
const env = (await res.json()) as ApiEnvelope<T>
|
||||
const text = await res.text()
|
||||
let env: ApiEnvelope<T>
|
||||
try {
|
||||
env = JSON.parse(text) as ApiEnvelope<T>
|
||||
} catch {
|
||||
throw new Error(
|
||||
res.status === 404
|
||||
? `接口不存在或后端未更新(${path})`
|
||||
: `响应不是 JSON(HTTP ${res.status})`,
|
||||
)
|
||||
}
|
||||
if (!res.ok || env.code !== 0) {
|
||||
throw new Error(env.message || `HTTP ${res.status}`)
|
||||
}
|
||||
@@ -96,7 +106,12 @@ export type UserDetail = {
|
||||
|
||||
export type AdminRole = 'super' | 'ops'
|
||||
|
||||
export type AdminMe = { id: string; username: string; role: AdminRole }
|
||||
export type AdminMe = {
|
||||
id: string
|
||||
username: string
|
||||
role?: string
|
||||
permissions?: string[]
|
||||
}
|
||||
|
||||
export type PushJob = {
|
||||
id: string
|
||||
@@ -125,12 +140,63 @@ export const adminApi = {
|
||||
}),
|
||||
logout: () => request<{ ok: boolean }>('POST', '/auth/logout'),
|
||||
me: () => request<AdminMe>('GET', '/me'),
|
||||
roles: () =>
|
||||
request<{ items: Array<{ id: string; name: string; system: boolean }> }>('GET', '/roles'),
|
||||
role: (id: string) =>
|
||||
request<{ id: string; name: string; system: boolean; permissions: string[] }>('GET', `/roles/${id}`),
|
||||
stats: () => request<DashboardStats>('GET', '/stats'),
|
||||
users: (q = '') =>
|
||||
request<{ items: UserListItem[] }>('GET', `/users?q=${encodeURIComponent(q)}`),
|
||||
user: (id: string) => request<UserDetail>('GET', `/users/${id}`),
|
||||
banUser: (id: string) => request<{ ok: boolean }>('POST', `/users/${id}/ban`),
|
||||
unbanUser: (id: string) => request<{ ok: boolean }>('POST', `/users/${id}/unban`),
|
||||
setUserStatus: (id: string, status: string, reason: string) =>
|
||||
request<UserDetail>('POST', `/users/${id}/status`, { status, reason }),
|
||||
statusTransitions: (id: string) =>
|
||||
request<{
|
||||
items: Array<{
|
||||
id: string
|
||||
from_status: string
|
||||
to_status: string
|
||||
admin_id: string
|
||||
reason: string
|
||||
created_at: string
|
||||
}>
|
||||
}>('GET', `/users/${id}/status-transitions`),
|
||||
userInsight: (id: string) =>
|
||||
request<{
|
||||
user_id: string
|
||||
profiles_count: number
|
||||
reports_by_type: Array<{ type: string; count: number }>
|
||||
recent_reports: Array<{ id: string; type: string; created_at: string }>
|
||||
tags: Array<{ code: string; label: string }>
|
||||
behavior: {
|
||||
events: Array<{ name: string; page_path?: string; received_at: string }>
|
||||
ask_thread_count: number
|
||||
}
|
||||
}>('GET', `/users/${id}/insight`),
|
||||
userEntitlements: (id: string) =>
|
||||
request<{
|
||||
user_id: string
|
||||
membership: {
|
||||
plan?: string
|
||||
status: string
|
||||
expires_at?: string
|
||||
ask_quota_left?: number
|
||||
active: boolean
|
||||
}
|
||||
ask_paid_quota_left: number
|
||||
flags: {
|
||||
report_detail_via_membership: boolean
|
||||
deep_access_count: number
|
||||
}
|
||||
deep_accesses: Array<{
|
||||
id: string
|
||||
report_id: string
|
||||
report_type?: string
|
||||
created_at: string
|
||||
}>
|
||||
}>('GET', `/users/${id}/entitlements`),
|
||||
grant: (id: string, plan: string) =>
|
||||
request<{ ok: boolean }>('POST', `/users/${id}/membership/grant`, { plan }),
|
||||
grantAskQuota: (id: string, delta: number) =>
|
||||
@@ -145,6 +211,323 @@ export const adminApi = {
|
||||
request<PushJob>('POST', '/push-jobs', body),
|
||||
patchPushJob: (id: string, body: { title?: string; body?: string; status?: 'draft' | 'cancelled' }) =>
|
||||
request<PushJob>('PATCH', `/push-jobs/${id}`, body),
|
||||
membershipPlans: () =>
|
||||
request<{
|
||||
items: Array<{
|
||||
code: string
|
||||
title: string
|
||||
duration_days: number
|
||||
amount_cents: number
|
||||
active: boolean
|
||||
updated_at: string
|
||||
}>
|
||||
}>('GET', '/membership-plans'),
|
||||
updateMembershipPlan: (
|
||||
code: string,
|
||||
body: { title: string; duration_days: number; amount_cents: number; active: boolean },
|
||||
) =>
|
||||
request<{
|
||||
code: string
|
||||
title: string
|
||||
duration_days: number
|
||||
amount_cents: number
|
||||
active: boolean
|
||||
}>('PUT', `/membership-plans/${code}`, body),
|
||||
createRedemptionBatch: (body: { label: string; plan_code: string; quantity: number }) =>
|
||||
request<{
|
||||
batch: { id: string; label: string; plan_code: string; quantity: number }
|
||||
codes: Array<{ id: string; code: string; status: string }>
|
||||
}>('POST', '/redemption-batches', body),
|
||||
redemptionBatches: () =>
|
||||
request<{ items: Array<{ id: string; label: string; plan_code: string; quantity: number; created_at: string }> }>(
|
||||
'GET',
|
||||
'/redemption-batches',
|
||||
),
|
||||
redemptionCodes: (batchId: string) =>
|
||||
request<{ items: Array<{ id: string; code: string; status: string; plan_code: string }> }>(
|
||||
'GET',
|
||||
`/redemption-batches/${batchId}/codes`,
|
||||
),
|
||||
disableRedemptionCode: (id: string) => request<{ ok: boolean }>('POST', `/redemption-codes/${id}/disable`),
|
||||
askThreads: (userId = '') => {
|
||||
const q = userId ? `?user_id=${encodeURIComponent(userId)}` : ''
|
||||
return request<{
|
||||
items: Array<{
|
||||
id: string
|
||||
user_id: string
|
||||
profile_id: string
|
||||
scene?: string
|
||||
message_count: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}>
|
||||
}>('GET', `/ask/threads${q}`)
|
||||
},
|
||||
askThread: (id: string) =>
|
||||
request<{
|
||||
id: string
|
||||
user_id: string
|
||||
profile_id: string
|
||||
scene?: string
|
||||
message_count: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
messages: Array<{ id: string; role: string; content: string; created_at: string }>
|
||||
}>('GET', `/ask/threads/${id}`),
|
||||
filterRules: () =>
|
||||
request<{
|
||||
items: Array<{
|
||||
id: string
|
||||
code: string
|
||||
title: string
|
||||
category: string
|
||||
pattern: string
|
||||
action: string
|
||||
active: boolean
|
||||
system: boolean
|
||||
updated_at: string
|
||||
}>
|
||||
}>('GET', '/content-safety/filter-rules'),
|
||||
filterRule: (id: string) =>
|
||||
request<{
|
||||
id: string
|
||||
code: string
|
||||
title: string
|
||||
category: string
|
||||
pattern: string
|
||||
action: string
|
||||
active: boolean
|
||||
system: boolean
|
||||
updated_at: string
|
||||
}>('GET', `/content-safety/filter-rules/${id}`),
|
||||
evaluateContent: (text: string) =>
|
||||
request<{ matches: Array<{ code: string; title: string; category: string; action: string }> }>(
|
||||
'POST',
|
||||
'/content-safety/evaluate',
|
||||
{ text },
|
||||
),
|
||||
askFeedback: () =>
|
||||
request<{
|
||||
items: Array<{
|
||||
id: string
|
||||
thread_id: string
|
||||
message_id?: string
|
||||
source: string
|
||||
rating: number
|
||||
tag?: string
|
||||
note?: string
|
||||
created_at: string
|
||||
}>
|
||||
}>('GET', '/ask/feedback'),
|
||||
createAskFeedback: (threadId: string, body: { rating: number; tag?: string; note?: string; message_id?: string }) =>
|
||||
request<{
|
||||
id: string
|
||||
thread_id: string
|
||||
rating: number
|
||||
source: string
|
||||
}>('POST', `/ask/threads/${threadId}/feedback`, body),
|
||||
systemPrompts: () =>
|
||||
request<{
|
||||
items: Array<{
|
||||
id: string
|
||||
code: string
|
||||
title: string
|
||||
scene?: string
|
||||
body: string
|
||||
version: number
|
||||
active: boolean
|
||||
system: boolean
|
||||
updated_at: string
|
||||
}>
|
||||
}>('GET', '/ai/system-prompts'),
|
||||
systemPrompt: (id: string) =>
|
||||
request<{
|
||||
id: string
|
||||
code: string
|
||||
title: string
|
||||
scene?: string
|
||||
body: string
|
||||
version: number
|
||||
active: boolean
|
||||
system: boolean
|
||||
updated_at: string
|
||||
}>('GET', `/ai/system-prompts/${id}`),
|
||||
knowledgeSources: () =>
|
||||
request<{
|
||||
items: Array<{
|
||||
id: string
|
||||
code: string
|
||||
title: string
|
||||
description?: string
|
||||
source_kind: string
|
||||
version: number
|
||||
active: boolean
|
||||
system: boolean
|
||||
updated_at: string
|
||||
}>
|
||||
}>('GET', '/ai/knowledge-sources'),
|
||||
knowledgeSource: (id: string) =>
|
||||
request<{
|
||||
id: string
|
||||
code: string
|
||||
title: string
|
||||
description?: string
|
||||
source_kind: string
|
||||
version: number
|
||||
active: boolean
|
||||
system: boolean
|
||||
updated_at: string
|
||||
}>('GET', `/ai/knowledge-sources/${id}`),
|
||||
crisisPolicies: () =>
|
||||
request<{
|
||||
items: Array<{
|
||||
id: string
|
||||
code: string
|
||||
title: string
|
||||
severity: string
|
||||
pattern: string
|
||||
action: string
|
||||
helpline_text?: string
|
||||
active: boolean
|
||||
system: boolean
|
||||
updated_at: string
|
||||
}>
|
||||
}>('GET', '/crisis/policies'),
|
||||
crisisPolicy: (id: string) =>
|
||||
request<{
|
||||
id: string
|
||||
code: string
|
||||
title: string
|
||||
severity: string
|
||||
pattern: string
|
||||
action: string
|
||||
helpline_text?: string
|
||||
active: boolean
|
||||
system: boolean
|
||||
updated_at: string
|
||||
}>('GET', `/crisis/policies/${id}`),
|
||||
evaluateCrisis: (text: string) =>
|
||||
request<{
|
||||
matches: Array<{
|
||||
code: string
|
||||
title: string
|
||||
severity: string
|
||||
action: string
|
||||
helpline_text?: string
|
||||
}>
|
||||
}>('POST', '/crisis/evaluate', { text }),
|
||||
banners: () =>
|
||||
request<{
|
||||
items: Array<{
|
||||
id: string
|
||||
code: string
|
||||
title: string
|
||||
placement: string
|
||||
image_url?: string
|
||||
link_path?: string
|
||||
sort_order: number
|
||||
active: boolean
|
||||
system: boolean
|
||||
updated_at: string
|
||||
}>
|
||||
}>('GET', '/cms/banners'),
|
||||
banner: (id: string) =>
|
||||
request<{
|
||||
id: string
|
||||
code: string
|
||||
title: string
|
||||
placement: string
|
||||
image_url?: string
|
||||
link_path?: string
|
||||
sort_order: number
|
||||
active: boolean
|
||||
system: boolean
|
||||
updated_at: string
|
||||
}>('GET', `/cms/banners/${id}`),
|
||||
feedSlots: () =>
|
||||
request<{
|
||||
items: Array<{
|
||||
id: string
|
||||
code: string
|
||||
title: string
|
||||
slot_key: string
|
||||
placement: string
|
||||
active: boolean
|
||||
system: boolean
|
||||
updated_at: string
|
||||
}>
|
||||
}>('GET', '/cms/feed-slots'),
|
||||
feedSlot: (id: string) =>
|
||||
request<{
|
||||
id: string
|
||||
code: string
|
||||
title: string
|
||||
slot_key: string
|
||||
placement: string
|
||||
active: boolean
|
||||
system: boolean
|
||||
updated_at: string
|
||||
}>('GET', `/cms/feed-slots/${id}`),
|
||||
publications: () =>
|
||||
request<{ items: Array<Record<string, unknown>> }>('GET', '/cms/publications'),
|
||||
publication: (id: string) =>
|
||||
request<Record<string, unknown>>('GET', `/cms/publications/${id}`),
|
||||
knowledgeChunks: () =>
|
||||
request<{ items: Array<Record<string, unknown>> }>('GET', '/ai/knowledge-chunks'),
|
||||
knowledgeChunk: (id: string) =>
|
||||
request<Record<string, unknown>>('GET', `/ai/knowledge-chunks/${id}`),
|
||||
tools: () =>
|
||||
request<{ items: Array<Record<string, unknown>> }>('GET', '/ai/tools'),
|
||||
tool: (id: string) =>
|
||||
request<Record<string, unknown>>('GET', `/ai/tools/${id}`),
|
||||
blockPolicies: () =>
|
||||
request<{ items: Array<Record<string, unknown>> }>('GET', '/content-safety/block-policies'),
|
||||
blockPolicie: (id: string) =>
|
||||
request<Record<string, unknown>>('GET', `/content-safety/block-policies/${id}`),
|
||||
cases: () =>
|
||||
request<{ items: Array<Record<string, unknown>> }>('GET', '/content-safety/cases'),
|
||||
case: (id: string) =>
|
||||
request<Record<string, unknown>>('GET', `/content-safety/cases/${id}`),
|
||||
events: () =>
|
||||
request<{ items: Array<Record<string, unknown>> }>('GET', '/crisis/events'),
|
||||
event: (id: string) =>
|
||||
request<Record<string, unknown>>('GET', `/crisis/events/${id}`),
|
||||
interventions: () =>
|
||||
request<{ items: Array<Record<string, unknown>> }>('GET', '/crisis/interventions'),
|
||||
intervention: (id: string) =>
|
||||
request<Record<string, unknown>>('GET', `/crisis/interventions/${id}`),
|
||||
handoffs: () =>
|
||||
request<{ items: Array<Record<string, unknown>> }>('GET', '/ask/handoffs'),
|
||||
handoff: (id: string) =>
|
||||
request<Record<string, unknown>>('GET', `/ask/handoffs/${id}`),
|
||||
requests: () =>
|
||||
request<{ items: Array<Record<string, unknown>> }>('GET', '/privacy/requests'),
|
||||
privacyRequest: (id: string) =>
|
||||
request<Record<string, unknown>>('GET', `/privacy/requests/${id}`),
|
||||
starConfigs: () =>
|
||||
request<{ items: Array<Record<string, unknown>> }>('GET', '/explore/star-configs'),
|
||||
starConfig: (id: string) =>
|
||||
request<Record<string, unknown>>('GET', `/explore/star-configs/${id}`),
|
||||
rhythmConfigs: () =>
|
||||
request<{ items: Array<Record<string, unknown>> }>('GET', '/explore/rhythm-configs'),
|
||||
rhythmConfig: (id: string) =>
|
||||
request<Record<string, unknown>>('GET', `/explore/rhythm-configs/${id}`),
|
||||
imageCardDecks: () =>
|
||||
request<{ items: Array<Record<string, unknown>> }>('GET', '/explore/image-card-decks'),
|
||||
imageCardDeck: (id: string) =>
|
||||
request<Record<string, unknown>>('GET', `/explore/image-card-decks/${id}`),
|
||||
reportTemplates: () =>
|
||||
request<{ items: Array<Record<string, unknown>> }>('GET', '/growth/report-templates'),
|
||||
reportTemplate: (id: string) =>
|
||||
request<Record<string, unknown>>('GET', `/growth/report-templates/${id}`),
|
||||
funnelDefinitions: () =>
|
||||
request<{ items: Array<Record<string, unknown>> }>('GET', '/analytics/funnel-definitions'),
|
||||
funnelDefinition: (id: string) =>
|
||||
request<Record<string, unknown>>('GET', `/analytics/funnel-definitions/${id}`),
|
||||
exploreScales: () =>
|
||||
request<{ items: Array<Record<string, unknown>> }>('GET', '/explore/scales'),
|
||||
exploreScale: (id: string) =>
|
||||
request<Record<string, unknown>>('GET', `/explore/scales/${id}`),
|
||||
orders: (params?: {
|
||||
status?: string
|
||||
kind?: string
|
||||
|
||||
Reference in New Issue
Block a user