chore: initial baseline with P0-safety .gitignore

This commit is contained in:
Simon
2026-06-14 16:49:18 +08:00
commit 63262292d7
510 changed files with 146008 additions and 0 deletions
+400
View File
@@ -0,0 +1,400 @@
// =============================================================================
// 企微IT智能服务台 — H5用户端 API 调用层
// =============================================================================
// 说明:封装会话、消息、审批、下载等与后端 /api/h5/ 交互的 API 方法
// 注意:OAuth2 相关 API 已迁移至 @/api/employee.ts
// 1. 会话相关(当前会话、发送消息、轮询消息、敲桌子招手)
// 2. 审批流程链接
// 3. 软件下载列表
// =============================================================================
import apiClient from '@/api'
// -------------------------------------------------------------------------
// 类型定义
// -------------------------------------------------------------------------
/** 用户信息(兼容旧接口 /h5/user) */
export interface UserInfo {
/** 员工 ID */
employee_id: string
/** 员工姓名 */
employee_name: string
/** 部门名称 */
department: string
/** 岗位 */
position: string
/** 职级 */
level: string
/** 是否 VIP 员工 */
is_vip: boolean
/** 头像 URL */
avatar_url: string
}
/** 会话信息 */
export interface ConversationInfo {
/** 会话 ID */
conversation_id: string
/** 员工 ID */
employee_id: string
/** 员工姓名(会话发起人) */
employee_name: string
/** 会话状态:waiting(排队中) / serving(服务中) / closed(已结单) */
status: 'waiting' | 'serving' | 'closed'
/** 坐席 ID(未接入时为空) */
agent_id: string
/** 坐席名称(未接入时为空) */
agent_name: string
/** 创建时间 */
created_at: string
/** 更新时间 */
updated_at: string
/** AI 实质性回复计数(满3次可呼叫坐席) */
ai_substantive_reply_count?: number
/** 是否可以呼叫人工坐席(AI 回复 >= 3 次) */
can_call_agent?: boolean
/** 被邀请参与会话的人员列表(邀请功能 P0-09~P0-11 */
participants?: ParticipantItem[]
}
/** 消息类型 */
export type MessageType = 'employee' | 'agent' | 'ai' | 'system'
/** 消息内容类型(text/image/file/voice/video/location 等) */
export type MsgContentType = 'text' | 'image' | 'file' | 'voice' | 'video' | 'location'
/** 单条消息 */
export interface Message {
/** 消息 ID */
message_id: string
/** 会话 ID */
conversation_id: string
/** 消息类型:employee(员工) / agent(坐席) / ai(AI) / system(系统) */
message_type: MessageType
/** 消息内容类型:text/image/file 等 */
msg_type?: MsgContentType
/** 消息内容 */
content: string
/** 发送者名称 */
sender_name: string
/** 创建时间 */
created_at: string
/** 图片/文件 URL */
media_url?: string
/** 文件名 */
file_name?: string
/** 文件大小(字节) */
file_size?: number
/** 扩展数据(额外字段,如 pic_url 等) */
extra_data?: Record<string, any>
/** 引用回复:被回复的消息 ID */
reply_to_id?: string
/** 发送状态:sending(发送中) / sent(已发送) / failed(发送失败) - 用于乐观更新UI */
status?: 'sending' | 'sent' | 'failed'
}
/** 发送消息请求参数 */
export interface SendMessageRequest {
/** 消息内容 */
content: string
/** 消息内容类型:text/image/file(默认 text */
msg_type?: MsgContentType
/** 图片/文件 URL(非文本消息必填) */
media_url?: string
/** 文件名 */
file_name?: string
/** 文件大小(字节) */
file_size?: number
}
/** 轮询消息请求参数 */
export interface PollMessagesParams {
/** 获取此 ID 之后的消息(增量轮询) */
after_message_id?: string
}
/** 招手请求参数 */
export interface ShakeRequest {
/** 员工 ID */
employee_id: string
/** 员工姓名 */
employee_name: string
}
/** 招手响应数据(与后端 h5.py shake 端点一致) */
export interface ShakeResponse {
/** 趣味话术内容 */
funny_phrase: string
/** 会话信息(用于判断是否已接入坐席) */
conversation: {
/** 会话状态:queued(排队中) / serving(服务中) / closed(已结单) */
status: string
}
}
/** 审批流程链接 */
export interface ApprovalLink {
/** 链接 ID */
id: string
/** 链接标题 */
title: string
/** 链接地址 */
url: string
/** 分类名称 */
category: string
/** 图标(可选) */
icon?: string
}
/** 软件下载项 */
export interface SoftwareDownload {
/** 软件 ID */
id: string
/** 软件名称 */
name: string
/** 版本号 */
version: string
/** 下载地址 */
download_url: string
/** 分类名称 */
category: string
/** 支持平台标签(如 "Windows", "macOS" */
platforms: string[]
/** 图标(可选) */
icon?: string
}
/** 发送消息响应(含 AI 自动回复) */
export interface SendMessageResponse {
/** 用户发送的消息 */
user_message: Message
/** AI 自动回复消息 */
ai_reply: Message
/** 是否为引导类回复(打招呼/呼叫人工),不计入实质回复 */
is_guidance: boolean
/** 当前 AI 实质性回复计数 */
ai_reply_count: number
/** 是否可以呼叫人工坐席 */
can_call_agent: boolean
}
// -------------------------------------------------------------------------
// 邀请功能类型(P0-09~P0-11
// -------------------------------------------------------------------------
/** 参与者信息(与后端 ParticipantInfo 对应) */
export interface ParticipantItem {
/** 企微员工UserID 或部门ID */
id: string
/** 姓名 或 部门名称 */
name: string
/** 部门(仅员工类型) */
department?: string
/** 类型 — employee(个人)或 department(部门) */
type?: 'employee' | 'department'
/** 头像URL(从企微通讯录获取,无头像时为空字符串) */
avatar?: string
/** 是否已加入(邀请后、点击加入前为 false) */
joined?: boolean
/** 加入时间(ISO 格式) */
joined_at?: string
}
// -------------------------------------------------------------------------
// 后端字段 → H5前端字段映射
// -------------------------------------------------------------------------
// 根因:后端 MessageResponse 使用 id / sender_type
// 但 H5 前端 Message 接口使用 message_id / message_type。
// 字段名不一致导致 MessageBubble 无法识别消息类型(全 undefined),
// Vue :key 也失效(key 全为 undefined),消息无法正常渲染。
// 修复:在 API 层统一映射,保持 H5 组件代码不变。
/**
* 将后端 MessageResponse 映射为 H5 前端 Message 格式
* - id → message_id
* - sender_type → message_type
* - 其余字段直接透传
*/
function mapMessage(raw: any): Message {
return {
message_id: raw.id || raw.message_id || '',
conversation_id: raw.conversation_id || '',
message_type: (raw.sender_type || raw.message_type || 'text') as MessageType,
msg_type: raw.msg_type,
content: raw.content || '',
sender_name: raw.sender_name || '',
created_at: raw.created_at || '',
media_url: raw.media_url,
file_name: raw.file_name,
file_size: raw.file_size,
extra_data: raw.extra_data,
reply_to_id: raw.reply_to_id,
}
}
/**
* 批量映射后端消息列表为 H5 前端 Message 格式
*/
function mapMessages(rawList: any[]): Message[] {
return (rawList || []).map(mapMessage)
}
// -------------------------------------------------------------------------
// API 方法
// -------------------------------------------------------------------------
// 注意:响应拦截器返回 response.data(即 {code, data, message} 包装对象)
// API 函数通过 await + response.data 取出业务数据(与原始工作代码一致)
// -------------------------------------------------------------------------
/**
* 获取当前用户信息(兼容旧接口)
* 返回当前登录员工的详细信息(姓名、部门、岗位、VIP 状态等)
* 注意:推荐使用 @/api/employee.ts 中的 getEmployeeInfo() 替代
* @returns 用户信息对象
*/
export async function getUser(): Promise<UserInfo> {
const response: any = await apiClient.get('/h5/user')
return response.data
}
/**
* 获取当前会话
* 返回当前员工正在进行的会话,如果无活跃会话则返回 null
* @returns 会话信息或 null
*/
export async function getCurrentConversation(): Promise<ConversationInfo | null> {
const response: any = await apiClient.get('/h5/conversations/current')
return response.data
}
/**
* 发送消息(含 AI 自动回复)
* 在当前会话中发送一条消息,后端自动生成 AI 回复
* @param data 消息内容
* @returns 包含用户消息和 AI 回复的响应
*/
export async function sendMessage(data: SendMessageRequest): Promise<SendMessageResponse> {
// 图片/文件消息后端处理可能较慢(AI + Dify),增加超时到30秒
// 修复截图发送超时BugapiClient默认10s不够
const response: any = await apiClient.post('/h5/conversations/current/messages', data, {
timeout: 30000,
})
// response = {code:0, data: {user_message:..., ai_reply:...}, message:"success"}
// response.data = 业务数据 {user_message:..., ai_reply:..., ...}
const raw = response.data
// 修复字段映射:后端返回 id/sender_typeH5前端期望 message_id/message_type
return {
user_message: mapMessage(raw.user_message),
ai_reply: raw.ai_reply ? mapMessage(raw.ai_reply) : raw.ai_reply,
is_guidance: raw.is_guidance,
ai_reply_count: raw.ai_reply_count,
can_call_agent: raw.can_call_agent,
}
}
/**
* 轮询消息
* 获取当前会话中指定消息 ID 之后的新消息(增量轮询)
* @param params 轮询参数(after_message_id 用于增量获取)
* @returns 新消息列表
*/
export async function pollMessages(params?: PollMessagesParams): Promise<Message[]> {
const response: any = await apiClient.get('/h5/conversations/current/messages/poll', { params })
// response.data = { items: [...], has_more: bool }
const data = response.data
const rawItems = data?.items || data || []
// 修复字段映射:后端返回 id/sender_typeH5前端期望 message_id/message_type
return mapMessages(rawItems)
}
/**
* 摇人 — 一键呼叫 IT 坐席
* 触发转人工流程,返回趣味话术和会话状态
* @param data 摇人请求参数(employee_id 必填,employee_name 可选)
* @returns 摇人响应(包含趣味话术和会话信息)
*/
export async function shake(data: ShakeRequest): Promise<ShakeResponse> {
const response: any = await apiClient.post('/h5/conversations/current/shake', data)
return response.data
}
/**
* 获取审批流程链接列表
* 返回所有可用的审批流程链接,按分类分组
* @returns 审批流程链接数组
*/
export async function getApprovalLinks(): Promise<ApprovalLink[]> {
const response: any = await apiClient.get('/h5/approval-links')
// response.data = { items: [...] } 或 [...]
const data = response.data
return (data?.items || data || []) as ApprovalLink[]
}
/**
* 获取软件下载列表
* 返回所有可下载的软件列表,按分类分组
* @returns 软件下载数组
*/
export async function getSoftwareDownloads(): Promise<SoftwareDownload[]> {
const response: any = await apiClient.get('/h5/software-downloads')
// response.data = { items: [...] } 或 [...]
const data = response.data
return (data?.items || data || []) as SoftwareDownload[]
}
// -------------------------------------------------------------------------
// 邀请功能 APIP0-09~P0-11
// -------------------------------------------------------------------------
// 注意:H5 专用端点使用 /h5/ 前缀,认证通过 Bearer Token 自动获取 employee_id
// 后端 _get_current_employee 依赖会从 Token 中提取 employee_id,无需前端传递
// --------------------------------------------------------------------------
/**
* 被邀请人加入会话
* 通过企微卡片链接点击后调用,后端从 Token 认证获取 employee_id
*
* @param conversationId - 会话ID
* @returns 更新后的会话信息
*/
export async function joinConversation(
conversationId: string
): Promise<any> {
const response: any = await apiClient.post(
`/h5/conversations/${conversationId}/join`
)
return response.data
}
/**
* 参与者主动退出会话
* 后端从 Token 认证获取 employee_id,无需前端传递
*
* @param conversationId - 会话ID
* @returns 更新后的会话信息
*/
export async function leaveAsParticipant(
conversationId: string
): Promise<any> {
const response: any = await apiClient.post(
`/h5/conversations/${conversationId}/leave-participant`
)
return response.data
}
/**
* 获取会话参与者列表
* 返回指定会话的所有被邀请参与者信息
*
* @param conversationId - 会话ID
* @returns 参与者列表
*/
export async function getParticipants(
conversationId: string
): Promise<ParticipantItem[]> {
const response: any = await apiClient.get(
`/h5/conversations/${conversationId}/participants`
)
const data = response.data
return data?.participants || []
}
+127
View File
@@ -0,0 +1,127 @@
// =============================================================================
// 企微IT智能服务台 — H5用户端员工API
// =============================================================================
// 说明:封装员工认证和身份信息相关的 API 方法
// 1. OAuth2 授权回调(code 换取 token + 用户信息)
// 2. 获取当前员工详细信息
// 3. 获取 OAuth2 授权 URL
// 4. Mock 登录(测试阶段,跳过 OAuth2)
// =============================================================================
import apiClient from '@/api'
// -------------------------------------------------------------------------
// 类型定义
// -------------------------------------------------------------------------
/** OAuth2 回调请求参数 */
export interface OAuthCallbackRequest {
/** 企微 OAuth2 授权码 */
code: string
/** 企微 OAuth2 state 参数(可选) */
state?: string
}
/** OAuth2 回调返回数据 */
export interface OAuthCallbackResponse {
/** 员工 ID */
employee_id: string
/** 员工姓名 */
employee_name: string
/** 访问令牌 */
token: string
/** 部门名称 */
department: string
/** 岗位 */
position: string
/** 头像 URL */
avatar: string
}
/** 员工详细信息 */
export interface EmployeeInfo {
/** 员工 ID */
employee_id: string
/** 员工姓名 */
employee_name: string
/** 部门名称 */
department: string
/** 岗位 */
position: string
/** 手机号 */
mobile: string
/** 邮箱 */
email: string
/** 头像 URL */
avatar: string
/** 是否 VIP 员工 */
is_vip: boolean
}
/** OAuth2 授权URL响应 */
export interface OAuthAuthorizeResponse {
/** 企微OAuth2授权URL */
authorize_url: string
}
// -------------------------------------------------------------------------
// API 方法
// -------------------------------------------------------------------------
// 注意:响应拦截器返回 response.data(即 {code, data, message} 包装对象)
// API 函数通过 await + response.data 取出业务数据(与原始工作代码一致)
// -------------------------------------------------------------------------
/**
* OAuth2 授权回调
* 将企微 OAuth2 授权码传给后端,换取员工身份和访问令牌
* 成功后保存 token 和基本信息到 localStorage
*
* @param data 包含 code 和可选 state 的请求参数
* @returns 员工身份信息(employee_id, employee_name, token 等)
* @throws 授权失败时抛出异常
*/
export async function oauthCallback(data: OAuthCallbackRequest): Promise<OAuthCallbackResponse> {
const response: any = await apiClient.post('/h5/oauth/callback', data)
// response = {code:0, data: {token:"...", ...}, message:"success"}(拦截器返回值)
// response.data = 业务数据 {token:"...", employee_id:"...", ...}
return response.data
}
/**
* Mock 登录(测试阶段,跳过 OAuth2)
* 直接通过员工 ID 获取真实的 Bearer Token
* 仅当后端 MOCK_LOGIN_ENABLED=true 时可用
*
* @param data 包含 employee_id 和 employee_name 的请求参数
* @returns 员工身份信息(employee_id, employee_name, token 等)
* @throws 登录失败时抛出异常
*/
export async function mockLogin(data: { employee_id: string; employee_name?: string }): Promise<OAuthCallbackResponse> {
const response: any = await apiClient.post('/h5/mock-login', data)
return response.data
}
/**
* 获取当前员工详细信息
* 返回当前登录员工的详细信息(姓名、部门、岗位、手机号、邮箱等)
* 需要携带有效的 Bearer Token
* @returns 员工详细信息对象
*/
export async function getEmployeeInfo(): Promise<EmployeeInfo> {
const response: any = await apiClient.get('/h5/me')
return response.data
}
/**
* 获取企微 OAuth2 授权 URL
* 返回完整的授权链接,前端跳转到该链接进行静默授权
* @returns 包含 authorize_url 的响应对象
*/
export async function getOAuthAuthorizeUrl(): Promise<OAuthAuthorizeResponse> {
// 传入 redirect_uri 确保后端构造正确的回调地址(而非默认的 /h5/)
const redirectUri = window.location.origin + '/itdesk/'
const response: any = await apiClient.get('/h5/oauth/authorize', {
params: { redirect_uri: redirectUri },
})
return response.data
}
+203
View File
@@ -0,0 +1,203 @@
// =============================================================================
// 企微IT智能服务台 — H5用户端 Axios 实例与拦截器
// =============================================================================
// 说明:创建 Axios 实例,配置:
// 1. 请求基础 URL
// 2. 请求拦截器(添加 Bearer Token 认证头)
// 3. 响应拦截器(统一错误处理 + 401 自动重新授权)
// =============================================================================
import axios from 'axios'
import type { AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from 'axios'
// Vant 轻提示
import { showToast } from 'vant'
// Bug #2 修复:从独立回调模块导入,替代 dynamic import('@/stores/employee')
// 打破 api/index.ts → stores/employee.ts 的循环依赖
import { triggerAuthExpired } from '@/utils/authCallback'
// --------------------------------------------------------------------------
// 创建 Axios 实例
// --------------------------------------------------------------------------
const apiClient: AxiosInstance = axios.create({
// 基础 URL:所有请求会自动加上这个前缀
baseURL: '/api',
// 请求超时时间(20秒,原10秒)
// 原因:图片/文件上传、AI消息处理等场景后端处理需要更多时间
// 修复截图发送超时Bug
timeout: 20000,
// 默认请求头
headers: {
'Content-Type': 'application/json',
},
})
// --------------------------------------------------------------------------
// 请求拦截器
// --------------------------------------------------------------------------
// 在每个请求发送前执行,用于添加 Bearer Token 认证头
apiClient.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
// 从 localStorage 获取 token,添加到 Authorization 头
// 替换旧的 X-Employee-Id 明文头,使用 Bearer Token 进行安全认证
const token = localStorage.getItem('h5_token')
if (token) {
config.headers['Authorization'] = `Bearer ${token}`
}
// 兼容过渡:如果同时存在 employee_id 且无 token,则仍然发送 X-Employee-Id
// 这确保了在 token 过期但 localStorage 中仍有旧数据的降级场景
if (!token) {
const employeeId = localStorage.getItem('employee_id')
if (employeeId) {
config.headers['X-Employee-Id'] = employeeId
}
}
return config
},
(error) => {
// 请求配置错误时直接返回
return Promise.reject(error)
}
)
// --------------------------------------------------------------------------
// 响应拦截器
// --------------------------------------------------------------------------
// 在每个响应返回后执行,用于统一处理错误
// 特殊处理 401:自动清除 token 并重新走 OAuth2 授权流程
apiClient.interceptors.response.use(
(response: AxiosResponse) => {
// 从响应中提取业务数据
const res = response.data
// 统一响应格式:{code: 0, data: {}, message: "success"}
if (res.code !== 0) {
// 特殊处理:业务码 1002 = 未授权(token 过期/无效)
// 后端 _get_current_employee 在 Redis 查不到 token 时返回此码
if (res.code === 1002) {
handleAuthExpired('biz1002')
return Promise.reject(new Error(res.message || '未授权'))
}
// 普通业务错误:显示轻提示
showToast(res.message || '请求失败')
return Promise.reject(new Error(res.message || '请求失败'))
}
// 业务成功:返回 response.data(即 {code, data, message} 包装对象)
// API 函数通过 response.data 取出业务数据(与原始工作代码一致)
return response.data
},
async (error) => {
// 网络错误或服务器错误
let message = '网络异常,请稍后重试'
if (error.response) {
switch (error.response.status) {
case 401:
// HTTP 401Token 过期或无效(FastAPI 直接返回的 HTTP 状态码)
await handleAuthExpired('http401')
break
case 403:
message = '拒绝访问'
break
case 404:
message = '请求的资源不存在'
break
case 500:
message = '服务器内部错误'
break
default:
message = `请求失败 (${error.response.status})`
}
} else if (error.code === 'ECONNABORTED') {
message = '请求超时,请稍后重试'
}
// 显示轻提示(401 时不显示通用提示,因为会自动跳转授权)
if (!error.response || error.response.status !== 401) {
showToast(message)
}
return Promise.reject(error)
}
)
// --------------------------------------------------------------------------
// 辅助:处理认证过期/未授权(复用逻辑)
// --------------------------------------------------------------------------
// 场景1: HTTP 401Axios error 拦截器)
// 场景2: 业务码 1002 "未授权"success 拦截器,后端 Redis token 过期时返回)
// 防循环机制:通过 localStorage 计数器限制 OAuth2 重定向次数
/** OAuth2 重定向计数器 key(与 employee store 保持一致) */
const OAUTH_REDIRECT_COUNT_KEY = 'oauth_redirect_count'
/** 最大允许重定向次数 */
const OAUTH_MAX_REDIRECT_COUNT = 3
// Bug #3 修复:401 去重锁,防止并发请求同时触发多次 OAuth2 重定向
// 当第一个 401 处理完成后,后续并发的 401 等待同一个 Promise 即可
let _authExpiredPromise: Promise<void> | null = null
async function handleAuthExpired(source: 'http401' | 'biz1002'): Promise<void> {
const label = source === 'http401' ? 'HTTP 401' : '业务码 1002'
// Bug #3 修复:如果已有 401 正在处理中,复用同一个 Promise,避免多次重定向
if (_authExpiredPromise) {
console.warn(`[API] ${label} 未授权 — 已有处理进行中,等待完成`)
return _authExpiredPromise
}
console.warn(`[API] ${label} 未授权,清除凭证并跳转登录`)
// 创建处理 Promise 并缓存(去重用)
_authExpiredPromise = (async () => {
try {
// 清除本地 token
localStorage.removeItem('h5_token')
localStorage.removeItem('employee_id')
localStorage.removeItem('employee_name')
// 判断是 mock 模式还是生产模式
const corpId = import.meta.env.VITE_WECOM_CORP_ID || ''
if (!corpId) {
// Mock 模式:跳转登录页
showToast('登录已过期,请重新登录')
// 避免重复跳转(当前已经在登录页时不再跳转)
if (window.location.pathname !== '/itdesk/login') {
window.location.href = '/itdesk/login'
}
} else {
// 防循环检测:超过最大重定向次数时停止跳转
const currentCount = parseInt(localStorage.getItem(OAUTH_REDIRECT_COUNT_KEY) || '0', 10)
if (currentCount >= OAUTH_MAX_REDIRECT_COUNT) {
console.error('[API] OAuth2 重定向次数超限,疑似无限循环,停止重定向')
showToast('登录状态异常,请刷新页面重试')
return
}
console.warn(`[API] OAuth2 重定向计数: ${currentCount}/${OAUTH_MAX_REDIRECT_COUNT}`)
// Bug #2 修复:通过回调注册中心触发 store 的 handleUnauthorized
// 替代原来的 dynamic import('@/stores/employee'),消除循环依赖风险
const triggered = await triggerAuthExpired()
if (!triggered) {
console.warn('[API] 认证过期处理器未注册,降级为刷新页面')
window.location.reload()
}
}
} catch (e) {
console.warn('[API] 401 处理失败,刷新页面:', e)
window.location.reload()
} finally {
// Bug #3 修复:处理完成后清除锁,允许未来的 401 重新触发
_authExpiredPromise = null
}
})()
return _authExpiredPromise
}
// 导出 Axios 实例
export default apiClient
+150
View File
@@ -0,0 +1,150 @@
// =============================================================================
// 企微IT智能服务台 — H5用户端消息 API
// =============================================================================
// 说明:封装消息相关的 API 调用
// 包括:消息撤回、删除、标记已读、图片上传、文件上传
// =============================================================================
import apiClient from './index'
import type { AxiosResponse } from 'axios'
import type { Message } from './conversation'
// --------------------------------------------------------------------------
// 类型定义
// --------------------------------------------------------------------------
/** 消息列表响应 */
export interface MessageListData {
items: Message[]
has_more: boolean
}
// --------------------------------------------------------------------------
// API 函数
// --------------------------------------------------------------------------
/**
* 撤回消息(2分钟内)
*
* @param messageId - 消息ID
* @returns 撤回结果
*/
export async function recallMessage(messageId: string): Promise<any> {
const response: AxiosResponse = await apiClient.post(
`/messages/${messageId}/recall`
)
return response.data
}
/**
* 删除消息
*
* @param messageId - 消息ID
* @returns 删除结果
*/
export async function deleteMessage(messageId: string): Promise<any> {
const response: AxiosResponse = await apiClient.delete(
`/messages/${messageId}`
)
return response.data
}
/**
* 标记会话已读
*
* @param conversationId - 会话ID
* @returns 标记结果
*/
export async function markConversationRead(conversationId: string): Promise<any> {
const response: AxiosResponse = await apiClient.post(
`/conversations/${conversationId}/mark-read`
)
return response.data
}
/**
* 轮询新消息(H5用户端)
*
* @param afterMessageId - 上次轮询的最后一消息ID
* @returns 新消息列表
*/
export async function pollMessages(afterMessageId?: string): Promise<Message[]> {
const params: Record<string, string> = {}
if (afterMessageId) {
params.after_message_id = afterMessageId
}
const response: AxiosResponse = await apiClient.get(
'/h5/conversations/current/messages/poll',
{ params }
)
const data = response.data.data
const items = data?.items || []
// 映射后端字段到前端字段
return items.map((item: any) => ({
message_id: item.id || item.message_id || '',
conversation_id: item.conversation_id || '',
message_type: item.sender_type || 'text',
msg_type: item.msg_type,
content: item.content || '',
sender_name: item.sender_name || '',
created_at: item.created_at || '',
media_url: item.media_url,
file_name: item.file_name,
file_size: item.file_size,
extra_data: item.extra_data,
reply_to_id: item.reply_to_id,
status: item.status,
}))
}
/**
* 上传图片
*
* @param file - 图片文件
* @returns 上传结果(包含 url, filename, file_size
*/
export async function uploadImage(file: File): Promise<{
url: string
filename: string
file_size: number
}> {
const formData = new FormData()
formData.append('file', file)
const response: AxiosResponse = await apiClient.post(
'/messages/image',
formData,
{
headers: {
'Content-Type': 'multipart/form-data',
},
}
)
return response.data.data
}
/**
* 上传文件
*
* @param file - 文件
* @returns 上传结果(包含 url, filename, file_size
*/
export async function uploadMessageFile(file: File): Promise<{
url: string
filename: string
file_size: number
}> {
const formData = new FormData()
formData.append('file', file)
const response: AxiosResponse = await apiClient.post(
'/messages/file',
formData,
{
headers: {
'Content-Type': 'multipart/form-data',
},
}
)
return response.data.data
}
+47
View File
@@ -0,0 +1,47 @@
// =============================================================================
// 企微IT智能服务台 — H5用户端排查模板类型定义
// =============================================================================
// 说明:与坐席端 troubleshooting.ts 共享的类型定义
// H5 端暂不直接调用排查模板 API(数据通过 WebSocket 从坐席端推送),
// 但需要 FlowchartNode 等类型来渲染交互式排查流程
// =============================================================================
/** 排查步骤路径节点 */
export interface PathStep {
/** 步骤标题 */
label: string
/** 步骤状态: done / current / pending */
status: 'done' | 'current' | 'pending'
}
/** 决策树递归节点 */
export interface FlowchartNode {
/** 节点唯一标识 */
id: string
/** 节点类型: step(步骤/操作)/ decision(判断/问答) */
type: 'step' | 'decision'
/** 节点标签文字 */
label: string
/** 节点状态: done / current / pending */
status?: 'done' | 'current' | 'pending'
/** 子节点列表(step 类型) */
children?: FlowchartNode[]
/** "是" 分支(decision 类型) */
yes_branch?: FlowchartNode
/** "否" 分支(decision 类型) */
no_branch?: FlowchartNode
}
/** 排查模板摘要(WebSocket 推送时使用) */
export interface TroubleshootingTemplateSummary {
/** 模板唯一标识 */
id: string
/** 模板名称 */
name: string
/** 分类 */
category: string
/** 排障步骤路径 */
path_steps: PathStep[]
/** 流程图定义 */
flowchart: FlowchartNode
}
+65
View File
@@ -0,0 +1,65 @@
// =============================================================================
// 企微IT智能服务台 — H5用户端文件上传 API
// =============================================================================
// 说明:封装文件/图片上传接口
// 1. 上传文件到后端 /api/uploadmultipart/form-data
// 2. 返回文件 URL、文件名、文件大小等信息
// =============================================================================
import apiClient from '@/api'
/** 上传响应数据 */
export interface UploadResponse {
/** 文件访问 URL(相对路径,如 /media/2026/06/10/xxx.png */
url: string
/** 服务器存储的文件名 */
filename: string
/** 文件大小(字节) */
file_size: number
/** 消息类型(image 或 file,根据扩展名判断) */
msg_type: 'image' | 'file'
}
/**
* 上传文件到服务器
*
* 做什么:将文件/图片上传到后端 /api/upload 端点
* 流程:
* 1. 创建 FormData,将文件添加到 file 字段
* 2. 如果传入的是 Blob(如粘贴的图片),自动生成文件名
* 3. 发送 multipart/form-data 请求
* 4. 返回上传结果(URL + 文件信息)
*
* @param file - 要上传的文件(File 或 Blob 对象)
* @param blobNamePrefix - Blob 文件名前缀(默认 'paste',截图场景传 'screenshot'
* @returns 上传响应(含文件 URL、文件名等)
*/
export async function uploadFile(file: File | Blob, blobNamePrefix: string = 'paste'): Promise<UploadResponse> {
// 构建 FormData
const formData = new FormData()
// 如果是 Blob 而非 File,需要生成文件名(File 自带 name 属性)
if (file instanceof Blob && !(file instanceof File)) {
// 粘贴的图片默认为 PNG 格式,使用传入的前缀区分来源
const fileName = `${blobNamePrefix}_${Date.now()}.png`
formData.append('file', file, fileName)
} else {
formData.append('file', file as File)
}
// 发送上传请求(60 秒超时,大文件上传可能较慢)
// 注意:必须显式删除 Content-Type,让浏览器自动生成带 boundary 的 multipart/form-data
// 原因:apiClient 实例默认设置了 'Content-Type': 'application/json'
// 如果不覆盖,Axios 会保留 application/json,后端无法解析 FormData 中的 file 字段
const response: any = await apiClient.post('/upload', formData, {
headers: {
'Content-Type': undefined,
},
timeout: 60000,
})
// 响应拦截器已确保 code === 0
// response = {code:0, data: {url:"...",...}, message:"success"}(拦截器返回值)
// response.data = 业务数据 {url:"...", filename:"...", ...}
return response.data as UploadResponse
}