feat: add avatar chat and knowledge workflow
This commit is contained in:
@@ -0,0 +1,302 @@
|
||||
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'
|
||||
|
||||
// API 基址:优先级 window.__APP_CONFIG__.apiBase > 环境变量 > 默认 '/api'
|
||||
// - 开发/Vite 代理:'/api'(由 vite.config 代理到后端 :8000)
|
||||
// - web-view 内(混合架构):需配置为后端公网地址,例如 'https://geo.99hui.com/api'
|
||||
// - 同域部署的构建产物:可保持 '/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): AxiosInstance => {
|
||||
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)
|
||||
}
|
||||
)
|
||||
|
||||
return request
|
||||
}
|
||||
|
||||
// 递归修复时区标识(复用现有项目逻辑)
|
||||
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
|
||||
}
|
||||
|
||||
// 获取分身列表
|
||||
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 deleteAvatar = (id: string) =>
|
||||
request.delete(`/avatar/${id}`)
|
||||
|
||||
// ==================== Token 管理 API ====================
|
||||
|
||||
// 获取 Token 余额
|
||||
export const getTokenBalance = () =>
|
||||
request.get<{ balance: number }>('/token/balance')
|
||||
|
||||
// 获取充值套餐
|
||||
export const getRechargePlans = () =>
|
||||
request.get<Array<{ id: string; name: string; amount: number; price: number }>>('/token/plans')
|
||||
|
||||
// 执行充值
|
||||
export const chargeToken = (planId: string) =>
|
||||
request.post<{ balance: number; charged: number }>('/token/charge', { planId })
|
||||
|
||||
// ==================== 授权管理 API ====================
|
||||
|
||||
export interface Authorization {
|
||||
id: string
|
||||
avatarId: string
|
||||
targetType: 'user' | 'organization' | 'application'
|
||||
targetId: string
|
||||
targetName: string
|
||||
permissions: string[]
|
||||
status: 'active' | 'inactive'
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
// 获取授权列表
|
||||
export const getAuthorizationList = (avatarId: string) =>
|
||||
request.get<Authorization[]>(`/avatar/${avatarId}/authorizations`)
|
||||
|
||||
// 更新授权
|
||||
export const updateAuthorization = (avatarId: string, data: Partial<Authorization>) =>
|
||||
request.put(`/avatar/${avatarId}/authorizations`, data)
|
||||
|
||||
// ==================== 组织管理 API ====================
|
||||
|
||||
export interface Organization {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
role: 'admin' | 'member' | 'viewer'
|
||||
memberCount: number
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
// 获取组织列表
|
||||
export const getOrganizationList = (params?: any) =>
|
||||
request.get<{ data: Organization[]; total: number }>('/organizations', { params })
|
||||
|
||||
// 创建组织
|
||||
export const createOrganization = (data: Partial<Organization>) =>
|
||||
request.post<Organization>('/organizations', data)
|
||||
|
||||
// ==================== 知识库管理 API ====================
|
||||
|
||||
export interface KnowledgeDoc {
|
||||
id: string
|
||||
avatarId: string
|
||||
filename: string
|
||||
fileType: string
|
||||
fileSize: number
|
||||
fileUrl: string
|
||||
status: string
|
||||
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)
|
||||
|
||||
// ==================== 会会用户资料 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; token: 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 })
|
||||
|
||||
// 当前登录用户
|
||||
export const getCurrentUser = () =>
|
||||
request.get<UserProfile & { huihuiUserId: string; phone: string }>('/huihui/me')
|
||||
|
||||
// 退出登录
|
||||
export const logoutUser = () =>
|
||||
request.post('/huihui/logout')
|
||||
|
||||
export default request
|
||||
Reference in New Issue
Block a user