Files
huihuiSquare/digital-avatar-app/src/api/index.ts
T

490 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import axios, { AxiosRequestConfig } from 'axios'
interface ApiClient {
get<T>(url: string, config?: AxiosRequestConfig): Promise<T>
post<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T>
put<T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T>
delete<T = unknown>(url: string, config?: AxiosRequestConfig): Promise<T>
}
// API 基址:优先级 window.__APP_CONFIG__.apiBase > 环境变量 > 默认 '/api'
// - 开发/Vite 代理:'/api'(由 vite.config 代理到后端 :8000)
// - digital.99hui.com 同域部署:保持 '/api'
// - 仅跨域部署时配置后端公网地址
function resolveBaseURL(): string {
const cfg = (window as any).__APP_CONFIG__
if (cfg && cfg.apiBase) return cfg.apiBase as string
const env = import.meta.env.VITE_API_BASE as string | undefined
if (env) return env
return '/api'
}
// 统一认证 token(由 uniapp 壳通过 URL 注入,见 utils/uniapp-bridge.ts)
let _authToken: string | null = null
export function setAuthToken(token: string | null) {
_authToken = token
}
export function getAuthToken(): string | null {
return _authToken
}
// 创建 axios 实例(复用现有项目模式)
const createRequest = (config?: AxiosRequestConfig): ApiClient => {
const request = axios.create({
baseURL: resolveBaseURL(),
timeout: 30000,
...config
})
// 请求拦截器:注入认证头
request.interceptors.request.use((cfg) => {
if (_authToken) {
cfg.headers = cfg.headers || {}
;(cfg.headers as any).Authorization = `Bearer ${_authToken}`
}
return cfg
})
// 响应拦截器
request.interceptors.response.use(
(res) => {
const data = fixDatetimeTZ(res.data)
if (data && data.code && data.code !== 200) {
console.error('API Error:', data.message)
return Promise.reject(new Error(data.message))
}
// 解包标准响应 { code, message, data }
if (data && data.code === 200) {
return data.data !== undefined ? data.data : data
}
return data
},
(err) => {
const msg = err.response?.data?.detail || err.response?.data?.message || err.message || '网络错误'
console.error('Network Error:', msg)
return Promise.reject(err)
}
)
// The response interceptor unwraps the API envelope before callers receive it.
return request as unknown as ApiClient
}
// 递归修复时区标识(复用现有项目逻辑)
function fixDatetimeTZ(obj: any): any {
if (typeof obj === 'string') {
return obj.replace(/T(\d{2}:\d{2}:\d{2})\+00:00/g, 'T$1\+08:00')
}
if (Array.isArray(obj)) return obj.map(fixDatetimeTZ)
if (obj && typeof obj === 'object') {
const result: any = {}
for (const k in obj) result[k] = fixDatetimeTZ(obj[k])
return result
}
return obj
}
const request = createRequest()
// ==================== 分身管理 API ====================
export interface Avatar {
id: string
name: string
displayName: string
description: string
photoUrl?: string
status: 'active' | 'inactive' | 'training'
tokenBalance: number
createdAt: string
updatedAt: string
config?: Record<string, any>
}
// 获取分身列表
export const getAvatarList = (params?: { page?: number; limit?: number }) =>
request.get<{ data: Avatar[]; total: number }>('/avatar', { params })
// 获取分身详情
export const getAvatarDetail = (id: string) =>
request.get<Avatar>(`/avatar/${id}`)
// 创建分身
export const createAvatar = (data: Partial<Avatar>) =>
request.post<Avatar>('/avatar', data)
// 更新分身
export const updateAvatar = (id: string, data: Partial<Avatar>) =>
request.put<Avatar>(`/avatar/${id}`, data)
export const uploadAvatarPhoto = (id: string, file: File) => {
const form = new FormData()
form.append('file', file)
return request.post<{ photoUrl: string }>(`/avatar/${id}/photo`, form, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
// 删除分身
export const deleteAvatar = (id: string) =>
request.delete(`/avatar/${id}`)
// ==================== Token 管理 API ====================
export interface TokenBalance {
balance: number
totalGranted: number
totalConsumed: number
}
export interface TokenUsageSummary {
avatarId: string
source: string
promptTokens: number
completionTokens: number
totalTokens: number
requestCount: number
}
export interface TokenPaymentOrder {
id: string
orderNo: string
planId: string
paymentMethod: 'wechat' | 'alipay'
payType: 'WECHAT' | 'ALIPAY'
payWay: 'APP' | 'LITE' | 'JSAPI'
pointsAmount: number
price: number
status: 'pending' | 'paid' | 'failed'
providerStatus: string
payMessage: string
failureReason: string
balance: number
}
// 获取 Token 余额
export const getTokenBalance = () =>
request.get<TokenBalance>('/token/balance')
// 获取充值套餐
export const getRechargePlans = () =>
request.get<Array<{ id: string; name: string; amount: number; price: number }>>('/token/plans')
// 创建会会支付订单;支付回调成功后积分到账。
export const chargeToken = (
planId: string,
paymentMethod: 'wechat' | 'alipay',
payScene: 'APP' | 'LITE' | 'JSAPI'
) => request.post<TokenPaymentOrder>('/token/charge', { planId, paymentMethod, payScene })
export const getTokenPaymentStatus = (orderId: string) =>
request.get<TokenPaymentOrder>(`/token/payment/${orderId}`)
// 按分身和使用场景汇总 Token 消耗
export const getTokenUsage = () =>
request.get<TokenUsageSummary[]>('/token/usage')
// ==================== 授权管理 API ====================
export type AvatarPermission = 'friend' | 'chat' | 'publish' | 'browse' | 'interact' | 'takeover'
export interface AvatarPermissionSettings {
avatarId: string
permissions: AvatarPermission[]
takeoverReplyDelaySeconds: number
disabledAvatarIds?: string[]
}
export const getAvatarPermissionSettings = (avatarId: string) =>
request.get<AvatarPermissionSettings>(`/avatar/${avatarId}/permission-settings`)
export const updateAvatarPermissionSettings = (
avatarId: string,
permissions: AvatarPermission[],
takeoverReplyDelaySeconds: number
) => request.put<AvatarPermissionSettings>(`/avatar/${avatarId}/permission-settings`, {
permissions,
takeoverReplyDelaySeconds,
})
export interface TakeoverStatus {
enabled: boolean
status: 'disabled' | 'connecting' | 'ready' | 'needs_login' | 'error'
message: string
pendingCount: number
takeoverReplyDelaySeconds: number
lastPolledAt: string | null
}
export const getTakeoverStatus = (avatarId: string) =>
request.get<TakeoverStatus>(`/avatar/${avatarId}/takeover/status`)
export interface Authorization {
id: string
avatarId: string
targetType: 'user' | 'organization' | 'application'
targetId: string
targetName: string
permissions: string[]
status: 'active' | 'inactive'
takeoverEnabled: boolean
takeoverMode: 'immediate' | 'delayed'
takeoverDelaySeconds: number
createdAt: string
}
export type AuthorizationInput = Pick<
Authorization,
'targetType' | 'targetId' | 'targetName' | 'permissions'
>
// 获取授权列表
export const getAuthorizationList = (avatarId: string) =>
request.get<Authorization[]>(`/avatar/${avatarId}/authorizations`)
// 添加授权
export const createAuthorization = (avatarId: string, data: AuthorizationInput) =>
request.post<Authorization>(`/avatar/${avatarId}/authorizations`, data)
// 更新授权
export const updateAuthorization = (avatarId: string, data: Partial<Authorization> & { id: string }) =>
request.put<Authorization>(`/avatar/${avatarId}/authorizations`, data)
// 删除授权
export const deleteAuthorization = (avatarId: string, authorizationId: string) =>
request.delete<{ id: string }>(`/avatar/${avatarId}/authorizations/${authorizationId}`)
// 更新单聊接管配置
export const updateTakeoverConfig = (avatarId: string, data: {
authorizationId: string
takeoverEnabled: boolean
takeoverMode?: 'immediate' | 'delayed'
takeoverDelaySeconds?: number
}) =>
request.put<Authorization>(`/avatar/${avatarId}/authorizations/takeover`, data)
// ==================== 组织管理 API ====================
export interface Organization {
id: string
name: string
description: string
emoji: string
type: 'team' | 'company' | 'community'
role: 'admin' | 'member' | 'viewer'
memberCount: number
createdAt: string
}
export interface CreateOrganizationInput {
name: string
desc?: string
emoji?: string
type?: 'team' | 'company' | 'community'
}
// 获取组织列表
export const getOrganizationList = (params?: any) =>
request.get<{ data: Organization[]; total: number }>('/organizations', { params })
// 创建组织
export const createOrganization = (data: CreateOrganizationInput) =>
request.post<Organization>('/organizations', data)
// ==================== 知识库管理 API ====================
export interface KnowledgeDoc {
id: string
avatarId: string
filename: string
fileType: string
fileSize: number
fileUrl: string
status: string
filePresent?: boolean
vectorized?: boolean
embeddingModel?: string
chunkCount?: number
createdAt: string
}
export interface QAPair {
id: string
avatarId: string
question: string
answer: string
enabled?: boolean
createdAt: string
updatedAt: string
}
export interface SearchResult {
docId: string
filename: string
fileType: string
snippet: string
score: number
}
// 文档列表
export const getKnowledgeDocs = (avatarId: string) =>
request.get<KnowledgeDoc[]>(`/avatar/${avatarId}/knowledge/docs`)
// 上传文档(支持 md/txt/pdf/doc/docx/xlsx)
export const uploadKnowledgeDoc = (avatarId: string, file: File) => {
const form = new FormData()
form.append('file', file)
return request.post<KnowledgeDoc>(`/avatar/${avatarId}/knowledge/docs`, form, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
// 删除文档
export const deleteKnowledgeDoc = (avatarId: string, docId: string) =>
request.delete(`/avatar/${avatarId}/knowledge/docs/${docId}`)
// 标准问答对列表
export const getQAPairs = (avatarId: string) =>
request.get<QAPair[]>(`/avatar/${avatarId}/knowledge/qa`)
// 创建问答对
export const createQAPair = (avatarId: string, data: { question: string; answer: string; enabled?: boolean }) =>
request.post<QAPair>(`/avatar/${avatarId}/knowledge/qa`, data)
// 更新问答对
export const updateQAPair = (avatarId: string, id: string, data: { question: string; answer: string; enabled?: boolean }) =>
request.put<QAPair>(`/avatar/${avatarId}/knowledge/qa/${id}`, data)
// 切换问答对启用状态
export const setQaEnabled = (avatarId: string, id: string, enabled: boolean) =>
request.put<QAPair>(`/avatar/${avatarId}/knowledge/qa/${id}/enabled`, { enabled })
// 删除问答对
export const deleteQAPair = (avatarId: string, id: string) =>
request.delete(`/avatar/${avatarId}/knowledge/qa/${id}`)
// 文档向量检索
export const searchKnowledge = (avatarId: string, q: string, topK = 5) =>
request.get<SearchResult[]>(`/avatar/${avatarId}/knowledge/search`, {
params: { q, top_k: topK }
})
// ==================== 数字分身聊天 API ====================
export interface ChatMessage {
role: 'user' | 'assistant'
content: string
}
export interface ChatResponse {
answer: string
source: 'qa' | 'knowledge' | 'qwen'
references?: Array<{ docId?: string; filename?: string; fileType?: string; snippet?: string; score?: number }>
}
export const sendAvatarChat = (avatarId: string, payload: { message: string; history?: ChatMessage[] }) =>
request.post<ChatResponse>(`/avatar/${avatarId}/chat`, payload)
export interface PublicAvatar {
id: string
name: string
displayName: string
description?: string
photoUrl?: string
emoji?: string
status: 'active' | 'inactive' | 'training'
}
export const createAvatarShareLink = (avatarId: string) =>
request.post<{ shareToken: string }>(`/avatar/${avatarId}/share`)
export const getPublicAvatar = (shareToken: string) =>
request.get<PublicAvatar>(`/public/avatar/${shareToken}`)
export const sendPublicAvatarChat = (shareToken: string, payload: { message: string; history?: ChatMessage[] }) =>
request.post<ChatResponse>(`/public/avatar/${shareToken}/chat`, payload)
type ChatStreamHandlers = {
onMeta: (meta: Pick<ChatResponse, 'source' | 'references'>) => void
onDelta: (content: string) => void
}
const streamChat = async (path: string, payload: { message: string; history?: ChatMessage[] }, handlers: ChatStreamHandlers) => {
const headers: Record<string, string> = { 'Content-Type': 'application/json', Accept: 'text/event-stream' }
if (_authToken) headers.Authorization = `Bearer ${_authToken}`
const response = await fetch(`${resolveBaseURL()}${path}`, { method: 'POST', headers, body: JSON.stringify(payload) })
if (!response.ok || !response.body) throw new Error(`对话请求失败(${response.status})`)
const reader = response.body.getReader()
const decoder = new TextDecoder()
let buffer = ''
while (true) {
const { done, value } = await reader.read()
buffer += decoder.decode(value || new Uint8Array(), { stream: !done })
const events = buffer.split('\n\n')
buffer = events.pop() || ''
for (const eventBlock of events) {
const event = eventBlock.match(/^event:\s*(.+)$/m)?.[1] || 'message'
const data = eventBlock.match(/^data:\s*(.+)$/m)?.[1]
if (!data) continue
const parsed = JSON.parse(data)
if (event === 'meta') handlers.onMeta(parsed)
if (event === 'delta') handlers.onDelta(parsed.content || '')
if (event === 'error') throw new Error(parsed.message || '对话暂时不可用')
}
if (done) break
}
}
export const streamAvatarChat = (avatarId: string, payload: { message: string; history?: ChatMessage[] }, handlers: ChatStreamHandlers) =>
streamChat(`/avatar/${avatarId}/chat/stream`, payload, handlers)
export const streamPublicAvatarChat = (shareToken: string, payload: { message: string; history?: ChatMessage[] }, handlers: ChatStreamHandlers) =>
streamChat(`/public/avatar/${shareToken}/chat/stream`, payload, handlers)
// ==================== 会会用户资料 API ====================
export interface UserProfile {
userId: string
nickname: string
avatarUrl: string
}
// 获取会会系统中的用户头像与昵称
export const getUserProfile = () =>
request.get<UserProfile>('/user/profile')
// ==================== 会会短信验证码登录 API ====================
export interface SmsLoginResult {
token: string
user: UserProfile & { huihuiUserId: string; phone: string; createdAt?: string; lastLoginAt?: string }
huihui: { userId: string; nickname: string; avatarUrl: string }
}
// 发送短信验证码(演示模式会额外返回 devCode / dev 标记)
export const sendSmsCode = (phone: string) =>
request.post<{ sent: boolean; devCode?: string; dev?: boolean }>('/huihui/sms/send', { phone })
// 短信验证码登录
export const loginBySms = (phone: string, code: string) =>
request.post<SmsLoginResult>('/huihui/sms/login', { phone, code })
// 账号密码登录(会会 loginType=password)
export const loginByPassword = (account: string, password: string) =>
request.post<SmsLoginResult>('/huihui/pwd/login', { account, password })
// Validate a production Huihui access token and exchange it for an app session.
export const loginByHuihuiToken = (token: string) =>
request.post<SmsLoginResult>('/huihui/token/login', { token })
// 当前登录用户
export const getCurrentUser = () =>
request.get<UserProfile & { huihuiUserId: string; phone: string }>('/huihui/me')
// 退出登录
export const logoutUser = () =>
request.post('/huihui/logout')
export default request