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:
jackyu66git
2026-08-13 01:45:53 +08:00
co-authored by Cursor
593 changed files with 21918 additions and 328 deletions
+385 -2
View File
@@ -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}`
: `响应不是 JSONHTTP ${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