mateclaw/mateclaw-ui/src/api/index.ts

989 lines
40 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 from 'axios'
import { handleAuthFailure, updateTokenFromHeader } from '@/utils/auth'
// Axios 实例
export const http = axios.create({
baseURL: '/api/v1',
timeout: 30000,
})
// 请求拦截器:注入 Token + Workspace ID + Accept-Language
http.interceptors.request.use((config) => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
const workspaceId = localStorage.getItem('mc-workspace-id')
if (workspaceId) {
config.headers['X-Workspace-Id'] = workspaceId
}
// Forward the user's UI locale so locale-sensitive endpoints (e.g.
// template apply) can pick the right display strings. Native browsers
// already send Accept-Language, but the user's chosen UI language may
// differ from the OS default — explicitly setting it keeps the two
// in sync.
const locale = localStorage.getItem('mateclaw_locale')
if (locale) {
config.headers['Accept-Language'] = locale
}
return config
})
// 响应拦截器:适配后端 R<T> { code, msg, data } 格式
http.interceptors.response.use(
(res) => {
// 滑动窗口续期:后端在 Token 接近过期时通过响应头下发新 Token
updateTokenFromHeader(res.headers)
const data = res.data
// 后端统一响应格式 R<T>: { code: number, msg: string, data: T }
if (data && typeof data === 'object' && 'code' in data) {
if (data.code === 200) return data
// 401 = authentication failure → log out
// 403 = authorization failure (e.g. workspace permission denied) → keep session, surface error to caller
if (data.code === 401) {
handleAuthFailure()
return Promise.reject(new Error(data.msg || 'Unauthorized'))
}
return Promise.reject(new Error(data.msg || 'Request failed'))
}
return data
},
(err) => {
if (err.response?.status === 401) {
handleAuthFailure()
}
// Wrap as Error so consumers can read e.message uniformly, but preserve
// err.response so callers needing the structured payload (e.g. workflow
// compile errors → response.data.data.errors[]) can still reach it.
// The previous shape rejected with a bare string, which silently
// stripped the body and made 422-class errors look like "no response"
// on the publish/compile flows.
const wrapped = new Error(err.response?.data?.msg || err.message || 'Request failed')
;(wrapped as Error & { response?: unknown }).response = err.response
return Promise.reject(wrapped)
}
)
// ==================== 受保护文件访问 ====================
/**
* 使用 JWT 认证 fetch 受保护文件,返回 Blob。
* 用于 <img> 和 <a download> 无法自动携带 Authorization header 的场景。
* 使用原生 fetch不走 axios避免 baseURL 拼接和 R<T> 拦截器干扰。
*/
export async function fetchAuthenticatedBlob(fileUrl: string): Promise<Blob> {
const token = localStorage.getItem('token')
const headers: Record<string, string> = {}
if (token) headers.Authorization = `Bearer ${token}`
const response = await fetch(fileUrl, { headers })
if (!response.ok) throw new Error(`Fetch failed: ${response.status}`)
return response.blob()
}
// ==================== Auth ====================
export const authApi = {
login: (data: { username: string; password: string }) =>
http.post('/auth/login', data),
listUsers: () => http.get('/auth/users'),
createUser: (data: any) => http.post('/auth/users', data),
changePassword: (id: number, oldPassword: string, newPassword: string) =>
http.put(`/auth/users/${id}/password`, null, { params: { oldPassword, newPassword } }),
}
// ==================== Agent ====================
export const agentApi = {
list: () => http.get('/agents'),
get: (id: string | number) => http.get(`/agents/${id}`),
create: (data: any) => http.post('/agents', data),
update: (id: string | number, data: any) => http.put(`/agents/${id}`, data),
delete: (id: string | number) => http.delete(`/agents/${id}`),
chat: (id: string | number, data: any) => http.post(`/agents/${id}/chat`, data),
execute: (id: string | number, data: any) => http.post(`/agents/${id}/execute`, data),
getState: (id: string | number) => http.get(`/agents/${id}/state`),
/** Lightweight capability snapshot used by the chat console attachment hint. */
getCapabilities: (id: string | number) => http.get(`/agents/${id}/capabilities`),
}
// ==================== Templates ====================
export const templateApi = {
list: () => http.get('/templates'),
apply: (id: string) => http.post(`/templates/${id}/apply`),
}
// ==================== Chat ====================
export const chatApi = {
uploadFile: async (conversationId: string, file: File) => {
const formData = new FormData()
formData.append('file', file)
formData.append('conversationId', conversationId)
return http.post('/chat/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
})
},
stream: (data: any, signal?: AbortSignal) => {
const headers: Record<string, string> = {
Accept: 'text/event-stream',
'Content-Type': 'application/json',
'Cache-Control': 'no-cache',
}
const token = localStorage.getItem('token')
if (token) {
headers.Authorization = `Bearer ${token}`
}
return fetch('/api/v1/chat/stream', {
method: 'POST',
headers,
body: JSON.stringify(data),
signal,
})
},
stop: (conversationId: string) =>
http.post<{ stopped: boolean }>(`/chat/${conversationId}/stop`),
getPendingApprovals: (conversationId: string) =>
http.get(`/chat/${conversationId}/pending-approvals`),
}
// ==================== Conversation ====================
export const conversationApi = {
list: () => http.get('/conversations'),
listMessages: (conversationId: string, params?: { beforeId?: number; limit?: number }) =>
http.get(`/conversations/${conversationId}/messages`, { params }),
getStatus: (conversationId: string) =>
http.get(`/conversations/${conversationId}/status`),
delete: (conversationId: string) =>
http.delete(`/conversations/${conversationId}`),
clearMessages: (conversationId: string) =>
http.delete(`/conversations/${conversationId}/messages`),
rename: (conversationId: string, title: string) =>
http.put(`/conversations/${conversationId}/title`, { title }),
}
// ==================== Skill ====================
export const skillApi = {
/** Paginated skill listing with search, source, status, and sort filters. */
page: (params: {
page?: number
size?: number
keyword?: string
skillType?: string
source?: string
sort?: string
runtime?: string
agentId?: string | number
enabled?: boolean
/** 'PASSED' / 'FAILED' — filters by security_scan_status. */
scanStatus?: string
} = {}) => http.get('/skills', { params }),
/** Tab count aggregate — returns { all, builtin, mcp, dynamic } */
counts: () => http.get('/skills/counts'),
/** RFC-042 §2.3.4 — manually rescan a single skill's security */
rescan: (id: string | number) => http.post(`/skills/${id}/rescan`),
listEnabled: () => http.get('/skills/enabled'),
get: (id: string | number) => http.get(`/skills/${id}`),
create: (data: any) => http.post('/skills', data),
update: (id: string | number, data: any) => http.put(`/skills/${id}`, data),
delete: (id: string | number) => http.delete(`/skills/${id}`),
toggle: (id: string | number, enabled: boolean) =>
http.put(`/skills/${id}/toggle?enabled=${enabled}`),
getActiveSkills: () => http.get('/skills/runtime/active'),
getRuntimeStatus: () => http.get('/skills/runtime/status'),
refreshRuntime: () => http.post('/skills/runtime/refresh'),
exportWorkspace: (id: string | number) => http.post(`/skills/${id}/export-workspace`),
getWorkspaceInfo: (id: string | number) => http.get(`/skills/${id}/workspace`),
// RFC-090 §7 + §11.4 — pre-flight requirements + LESSONS.md + reverse lookup
requirements: (id: string | number) => http.get(`/skills/${id}/requirements`),
getLessons: (id: string | number) => http.get(`/skills/${id}/lessons`),
clearLessons: (id: string | number) => http.post(`/skills/${id}/lessons/clear`),
employees: (id: string | number) => http.get(`/skills/${id}/employees`),
}
// ==================== Activity Feed (RFC-090 §4.5) ====================
export const activityApi = {
feed: (params: { source?: string; page?: number; size?: number; workspaceId?: number } = {}) =>
http.get('/activity/feed', { params }),
}
// ==================== Backstage (admin runtime view) ====================
export interface BackstageRunCard {
conversationId: string
agentId: number | null
agentName: string | null
agentIcon: string | null
username: string | null
currentPhase: string | null
runningToolName: string | null
waitingReason: string | null
done: boolean
stopRequested: boolean
firstTokenReceived: boolean
subscriberCount: number
queueLen: number
ageMs: number
msSinceLastEvent: number
/** null when healthy; otherwise: 'idle_silent' | 'tool_silent' | 'hard_cap' */
stuckReason: string | null
orphan: boolean
subagentCount: number
}
export interface BackstageSubagentCard {
subagentId: string
parentConversationId: string | null
childConversationId: string | null
agentId: number | null
agentName: string | null
agentIcon: string | null
goal: string | null
status: string | null
currentPhase: string | null
lastTool: string | null
toolCount: number
ageMs: number
}
export interface BackstageSummary {
running: number
stuck: number
orphan: number
queued: number
subagentsActive: number
}
export interface BackstageSnapshot {
summary: BackstageSummary
runs: BackstageRunCard[]
subagents: BackstageSubagentCard[]
timestamp: number
}
export const backstageApi = {
snapshot: () => http.get<{ data: BackstageSnapshot }>('/admin/agent-runtime/snapshot'),
stop: (conversationId: string) =>
http.post(`/admin/agent-runtime/runs/${encodeURIComponent(conversationId)}/stop`),
recycle: (conversationId: string) =>
http.post(`/admin/agent-runtime/runs/${encodeURIComponent(conversationId)}/recycle`),
interruptSubagent: (subagentId: string) =>
http.post(`/admin/agent-runtime/subagents/${encodeURIComponent(subagentId)}/interrupt`),
sweep: () => http.post('/admin/agent-runtime/sweep'),
}
// ==================== ACP Endpoints (RFC-090 Phase 7) ====================
export const acpApi = {
list: () => http.get('/acp/endpoints'),
get: (id: number | string) => http.get(`/acp/endpoints/${id}`),
create: (data: any) => http.post('/acp/endpoints', data),
update: (id: number | string, data: any) => http.put(`/acp/endpoints/${id}`, data),
delete: (id: number | string) => http.delete(`/acp/endpoints/${id}`),
toggle: (id: number | string, enabled: boolean) =>
http.put(`/acp/endpoints/${id}/toggle?enabled=${enabled}`),
test: (id: number | string) => http.post(`/acp/endpoints/${id}/test`),
}
// ==================== Skill Templates (RFC-091) ====================
export const skillTemplateApi = {
list: () => http.get('/skill-templates'),
get: (id: string) => http.get(`/skill-templates/${id}`),
instantiate: (id: string, values: Record<string, unknown>) =>
http.post(`/skill-templates/${id}/instantiate`, values),
}
// ==================== Skill Install ====================
export const skillInstallApi = {
searchHub: (q: string, limit = 20) =>
http.get('/skills/install/hub/search', { params: { q, limit } }),
startInstall: (data: { bundleUrl: string; version?: string; enable?: boolean; targetName?: string; overwrite?: boolean }) =>
http.post('/skills/install/start', data),
getStatus: (taskId: string) =>
http.get(`/skills/install/status/${taskId}`),
cancelInstall: (taskId: string) =>
http.post(`/skills/install/cancel/${taskId}`),
uninstall: (skillName: string) =>
http.delete(`/skills/install/${skillName}`),
uploadZip: (file: File, options?: { enable?: boolean; overwrite?: boolean; targetName?: string }) => {
const formData = new FormData()
formData.append('file', file)
return http.post('/skills/install/upload', formData, {
params: options,
headers: { 'Content-Type': 'multipart/form-data' },
})
},
}
// ==================== Datasource ====================
export const datasourceApi = {
list: () => http.get('/datasources'),
get: (id: string | number) => http.get(`/datasources/${id}`),
create: (data: any) => http.post('/datasources', data),
update: (id: string | number, data: any) => http.put(`/datasources/${id}`, data),
delete: (id: string | number) => http.delete(`/datasources/${id}`),
toggle: (id: string | number, enabled: boolean) =>
http.put(`/datasources/${id}/toggle?enabled=${enabled}`),
test: (id: string | number) => http.post(`/datasources/${id}/test`),
}
// ==================== Tool ====================
export const toolApi = {
list: () => http.get('/tools'),
listEnabled: () => http.get('/tools/enabled'),
/**
* Unified picker source for the agent edit tool tab — returns built-in
* tools plus every MCP-discovered tool grouped by server. The `name`
* field is what gets saved into mate_agent_tool.tool_name.
*/
listAvailable: () => http.get('/tools/available'),
get: (id: string | number) => http.get(`/tools/${id}`),
create: (data: any) => http.post('/tools', data),
update: (id: string | number, data: any) => http.put(`/tools/${id}`, data),
delete: (id: string | number) => http.delete(`/tools/${id}`),
toggle: (id: string | number, enabled: boolean) =>
http.put(`/tools/${id}/toggle?enabled=${enabled}`),
}
// ==================== Channel ====================
export const channelApi = {
list: () => http.get('/channels'),
get: (id: string | number) => http.get(`/channels/${id}`),
create: (data: any) => http.post('/channels', data),
update: (id: string | number, data: any) => http.put(`/channels/${id}`, data),
delete: (id: string | number) => http.delete(`/channels/${id}`),
toggle: (id: string | number, enabled: boolean) =>
http.put(`/channels/${id}/toggle?enabled=${enabled}`),
status: () => http.get('/channels/status'),
/** Real-time per-channel health (true transport state, not DB enabled flag). */
health: (id: string | number) => http.get(`/channels/${id}/health`),
/** Batch health for all channels in current workspace. */
healthAll: () => http.get('/channels/health'),
/**
* Wizard Step 2 — validate a draft config without persisting.
* Returns a VerificationResult: { ok, skipped, durationMs, headline,
* identity, invalidField, hint }.
*/
preflight: (channelType: string, configJson: string) =>
http.post('/channels/preflight', { channelType, configJson }),
// 微信 iLink Bot QR 码登录
weixinQrcode: () => http.get('/channels/webhook/weixin/qrcode'),
weixinQrcodeStatus: (qrcode: string) =>
http.get(`/channels/webhook/weixin/qrcode/status?qrcode=${encodeURIComponent(qrcode)}`),
// Feishu one-click app registration (oapi-sdk 2.6+ scene/registration)
feishuRegisterBegin: (domain: string) =>
http.post(`/channels/webhook/feishu/register/begin?domain=${encodeURIComponent(domain)}`),
feishuRegisterStatus: (sessionId: string) =>
http.get(`/channels/webhook/feishu/register/status?session=${encodeURIComponent(sessionId)}`),
// DingTalk one-click app registration (OAuth Device Flow)
dingtalkRegisterBegin: () =>
http.post('/channels/webhook/dingtalk/register/begin'),
dingtalkRegisterStatus: (sessionId: string) =>
http.get(`/channels/webhook/dingtalk/register/status?session=${encodeURIComponent(sessionId)}`),
}
// ==================== MCP Server ====================
export const mcpApi = {
list: () => http.get('/mcp/servers'),
get: (id: string | number) => http.get(`/mcp/servers/${id}`),
create: (data: any) => http.post('/mcp/servers', data),
update: (id: string | number, data: any) => http.put(`/mcp/servers/${id}`, data),
delete: (id: string | number) => http.delete(`/mcp/servers/${id}`),
toggle: (id: string | number, enabled: boolean) =>
http.put(`/mcp/servers/${id}/toggle?enabled=${enabled}`),
test: (id: string | number) => http.post(`/mcp/servers/${id}/test`),
refresh: () => http.post('/mcp/servers/refresh'),
}
// ==================== Plan ====================
export const planApi = {
listByAgent: (agentId: string) => http.get(`/plans?agentId=${agentId}`),
get: (id: string | number) => http.get(`/plans/${id}`),
}
// ==================== Model ====================
export const modelApi = {
listProviders: () => http.get('/models'),
listEnabled: () => http.get('/models/enabled'),
get: (id: string | number) => http.get(`/models/${id}`),
getDefault: () => http.get('/models/default'),
create: (data: any) => http.post('/models', data),
update: (id: string | number, data: any) => http.put(`/models/${id}`, data),
delete: (id: string | number) => http.delete(`/models/${id}`),
setDefault: (id: string | number) => http.post(`/models/${id}/default`),
updateProviderConfig: (providerId: string, data: any) =>
http.put(`/models/${providerId}/config`, data),
createCustomProvider: (data: any) => http.post('/models/custom-providers', data),
// Issue #39: fall back to a query-param endpoint when the providerId can't
// safely sit in a path segment (slash / space / etc.) — those rows would
// otherwise be undeletable because Spring's {providerId} doesn't span "/".
deleteCustomProvider: (providerId: string) => {
const safe = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/.test(providerId)
return safe
? http.delete(`/models/custom-providers/${providerId}`)
: http.delete('/models/custom-providers', { params: { providerId } })
},
addProviderModel: (providerId: string, data: any) =>
http.post(`/models/${providerId}/models`, data),
removeProviderModel: (providerId: string, modelId: string) =>
http.delete(`/models/${providerId}/models/${encodeURIComponent(modelId)}`),
getActive: () => http.get('/models/active'),
setActive: (data: { providerId: string; model: string }) =>
http.put('/models/active', data),
// 模型发现与连接测试
discoverModels: (providerId: string) =>
http.post(`/models/${providerId}/discover`),
applyDiscoveredModels: (providerId: string, modelIds: string[]) =>
http.post(`/models/${providerId}/discover/apply`, { modelIds }),
testConnection: (providerId: string) =>
http.post(`/models/${providerId}/test-connection`),
testModel: (providerId: string, modelId: string) =>
http.post(`/models/${providerId}/models/${encodeURIComponent(modelId)}/test`),
// ==================== RFC-074: enabled / catalog ====================
/** Full provider catalog including enabled=false rows; powers the Add Provider drawer. */
catalog: () => http.get('/models/catalog'),
/** Opt a provider into the dropdown; backend triggers re-probe via ModelConfigChangedEvent. */
enableProvider: (providerId: string) => http.post(`/models/${providerId}/enable`),
/** Hide a provider; if it owned the current default model, backend auto-promotes a replacement. */
disableProvider: (providerId: string) => http.post(`/models/${providerId}/disable`),
// ==================== Embedding Model (RFC Embedding UI) ====================
listByType: (modelType: 'chat' | 'embedding', modality?: 'vision' | 'video' | 'audio') =>
http.get('/models/by-type', { params: { modelType, modality } }),
testEmbedding: (modelId: string | number) =>
http.post(`/models/embedding/${modelId}/test`),
getDefaultEmbedding: () => http.get('/models/embedding/default'),
setDefaultEmbedding: (modelId: string | number | '') =>
http.post('/models/embedding/default', { modelId }),
}
// ==================== Provider Pool (RFC-009 Phase 4) ====================
export interface ProviderPoolEntry {
providerId: string
providerName: string
inPool: boolean
removalSource: string | null
removalMessage: string | null
removedAtMs: number | null
inCooldown: boolean
cooldownRemainingMs: number
consecutiveFailures: number
}
export interface ReprobeResult {
providerId: string
success: boolean
latencyMs: number
errorMessage: string | null
inPool: boolean
}
export const providerPoolApi = {
snapshot: () => http.get<ProviderPoolEntry[]>('/llm/provider-pool'),
reprobe: (providerId: string) =>
http.post<ReprobeResult>(`/llm/provider-pool/${encodeURIComponent(providerId)}/reprobe`),
}
// ==================== OAuth ====================
export const oauthApi = {
authorize: () => http.get('/oauth/openai/authorize'),
status: () => http.get('/oauth/openai/status'),
refresh: () => http.post('/oauth/openai/refresh'),
revoke: () => http.delete('/oauth/openai/revoke'),
callbackPaste: (callbackUrl: string) =>
http.post('/oauth/openai/callback-paste', { callbackUrl }),
// Device Authorization Grant — used when MateClaw runs on a remote host so the
// browser cannot reach localhost:1455 for the PKCE callback.
deviceStart: () => http.post('/oauth/openai/device/start'),
devicePoll: (deviceAuthId: string) =>
http.post('/oauth/openai/device/poll', { deviceAuthId }),
deviceCancel: (deviceAuthId: string) =>
http.post('/oauth/openai/device/cancel', { deviceAuthId }),
}
// RFC-062: Claude Code OAuth piggybacks on the user's local Claude Code
// install — no in-app authorize/revoke flow yet (PR-4). Until then the UI
// can only check status + force a re-detect from disk.
export const claudeCodeOAuthApi = {
status: () => http.get('/oauth/anthropic/status'),
reload: () => http.post('/oauth/anthropic/reload'),
}
// ==================== Setup ====================
export const setupApi = {
onboardingStatus: () => http.get('/setup/onboarding-status'),
}
// ==================== Settings ====================
export const settingsApi = {
get: () => http.get('/settings'),
update: (data: any) => http.put('/settings', data),
getLanguage: () => http.get('/settings/language'),
updateLanguage: (language: string) => http.put('/settings/language', { language }),
}
// ==================== Workspace ====================
const encodeFilePath = (filename: string) =>
filename.split('/').map(encodeURIComponent).join('/')
export const agentContextApi = {
listFiles: (agentId: string | number) =>
http.get(`/agents/${agentId}/workspace/files`),
getFile: (agentId: string | number, filename: string) =>
http.get(`/agents/${agentId}/workspace/files/${encodeFilePath(filename)}`),
saveFile: (agentId: string | number, filename: string, content: string) =>
http.put(`/agents/${agentId}/workspace/files/${encodeFilePath(filename)}`, { content }),
deleteFile: (agentId: string | number, filename: string) =>
http.delete(`/agents/${agentId}/workspace/files/${encodeFilePath(filename)}`),
getPromptFiles: (agentId: string | number) =>
http.get(`/agents/${agentId}/workspace/prompt-files`),
setPromptFiles: (agentId: string | number, files: string[]) =>
http.put(`/agents/${agentId}/workspace/prompt-files`, { files }),
}
// ==================== Security ====================
export const securityApi = {
getGuardConfig: () => http.get('/security/guard/config'),
updateGuardConfig: (data: any) => http.put('/security/guard/config', data),
getFileGuardConfig: () => http.get('/security/guard/config/file-guard'),
updateFileGuardConfig: (data: any) => http.put('/security/guard/config/file-guard', data),
listRules: (params?: any) => http.get('/security/guard/rules', { params }),
listBuiltinRules: (params?: any) => http.get('/security/guard/rules/builtin', { params }),
createRule: (data: any) => http.post('/security/guard/rules', data),
updateRule: (ruleId: string, data: any) => http.put(`/security/guard/rules/${ruleId}`, data),
toggleRule: (ruleId: string, enabled: boolean) =>
http.put(`/security/guard/rules/${ruleId}/toggle?enabled=${enabled}`),
deleteRule: (ruleId: string) => http.delete(`/security/guard/rules/${ruleId}`),
listAuditLogs: (params?: any) => http.get('/security/audit/logs', { params }),
getAuditStats: () => http.get('/security/audit/stats'),
listApprovals: (params?: any) => http.get('/security/approvals', { params }),
}
// ==================== Token Usage ====================
export const tokenUsageApi = {
getSummary: (params?: { startDate?: string; endDate?: string; modelName?: string; providerId?: string }) =>
http.get('/token-usage', { params }),
}
// ==================== CronJob ====================
export const cronJobApi = {
list: () => http.get('/cron-jobs'),
get: (id: string | number) => http.get(`/cron-jobs/${id}`),
create: (data: any) => http.post('/cron-jobs', data),
update: (id: string | number, data: any) => http.put(`/cron-jobs/${id}`, data),
delete: (id: string | number) => http.delete(`/cron-jobs/${id}`),
toggle: (id: string | number, enabled: boolean) =>
http.put(`/cron-jobs/${id}/toggle`, null, { params: { enabled } }),
runNow: (id: string | number) => http.post(`/cron-jobs/${id}/run`),
activeRuns: (conversationId: string) =>
http.get('/cron-jobs/active-runs', { params: { conversationId } }),
}
// ==================== Wiki Knowledge Base ====================
export const wikiApi = {
// Knowledge Base
listKBs: () => http.get('/wiki/knowledge-bases'),
getKB: (id: number) => http.get(`/wiki/knowledge-bases/${id}`),
listKBsByAgent: (agentId: number) => http.get(`/wiki/knowledge-bases/agent/${agentId}`),
createKB: (data: { name: string; description?: string; agentId?: number }) =>
http.post('/wiki/knowledge-bases', data),
updateKB: (id: number, data: { name?: string; description?: string; agentId?: number; embeddingModelId?: string | number | null }) =>
http.put(`/wiki/knowledge-bases/${id}`, data),
deleteKB: (id: number) => http.delete(`/wiki/knowledge-bases/${id}`),
getConfig: (id: number) => http.get(`/wiki/knowledge-bases/${id}/config`),
updateConfig: (id: number, content: string) =>
http.put(`/wiki/knowledge-bases/${id}/config`, { content }),
// Directory Scan
setSourceDirectory: (id: number, path: string) =>
http.put(`/wiki/knowledge-bases/${id}/source-directory`, { path }),
scanDirectory: (id: number) => http.post(`/wiki/knowledge-bases/${id}/scan`),
// Raw Materials
listRaw: (kbId: number) => http.get(`/wiki/knowledge-bases/${kbId}/raw`),
addRawText: (kbId: number, data: { title: string; content: string }) =>
http.post(`/wiki/knowledge-bases/${kbId}/raw/text`, data),
uploadRaw: (kbId: number, formData: FormData, onProgress?: (pct: number) => void) =>
http.post(`/wiki/knowledge-bases/${kbId}/raw/upload`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
onUploadProgress: onProgress
? (e) => { if (e.total) onProgress(Math.round((e.loaded / e.total) * 100)) }
: undefined,
}),
deleteRaw: (kbId: number, rawId: number) =>
http.delete(`/wiki/knowledge-bases/${kbId}/raw/${rawId}`),
reprocessRaw: (kbId: number, rawId: number) =>
http.post(`/wiki/knowledge-bases/${kbId}/raw/${rawId}/reprocess`),
cancelRaw: (kbId: number, rawId: number) =>
http.post(`/wiki/knowledge-bases/${kbId}/raw/${rawId}/cancel`),
downloadRaw: (kbId: number, rawId: number) =>
http.get<Blob>(`/wiki/knowledge-bases/${kbId}/raw/${rawId}/download`, {
responseType: 'blob',
}),
// Wiki Pages
listPages: (kbId: number, rawId?: number) =>
http.get(`/wiki/knowledge-bases/${kbId}/pages`, rawId != null ? { params: { rawId } } : undefined),
getPage: (kbId: number, slug: string) =>
http.get(`/wiki/knowledge-bases/${kbId}/pages/${encodeURIComponent(slug)}`),
updatePage: (kbId: number, slug: string, content: string) =>
http.put(`/wiki/knowledge-bases/${kbId}/pages/${encodeURIComponent(slug)}`, { content }),
deletePage: (kbId: number, slug: string) =>
http.delete(`/wiki/knowledge-bases/${kbId}/pages/${encodeURIComponent(slug)}`),
batchDeletePages: (kbId: number, slugs: string[]) =>
http.delete(`/wiki/knowledge-bases/${kbId}/pages/batch`, { data: slugs }),
getBacklinks: (kbId: number, slug: string) =>
http.get(`/wiki/knowledge-bases/${kbId}/pages/${encodeURIComponent(slug)}/backlinks`),
// RFC-051 PR-7: archived pages
listArchivedPages: (kbId: number) =>
http.get(`/wiki/knowledge-bases/${kbId}/pages/archived`),
archivePage: (kbId: number, slug: string) =>
http.post(`/wiki/knowledge-bases/${kbId}/pages/${encodeURIComponent(slug)}/archive`),
unarchivePage: (kbId: number, slug: string) =>
http.post(`/wiki/knowledge-bases/${kbId}/pages/${encodeURIComponent(slug)}/unarchive`),
// Processing
processKB: (kbId: number) => http.post(`/wiki/knowledge-bases/${kbId}/process`),
getProcessingStatus: (kbId: number) => http.get(`/wiki/knowledge-bases/${kbId}/processing-status`),
// RFC-029: Relations
getRelatedPages: (kbId: number, slug: string, topK = 5) =>
http.get(`/wiki/kb/${kbId}/pages/${encodeURIComponent(slug)}/related`, { params: { topK } }),
explainRelation: (kbId: number, slugA: string, slugB: string) =>
http.get(`/wiki/kb/${kbId}/pages/${encodeURIComponent(slugA)}/relation/${encodeURIComponent(slugB)}`),
getPageCitations: (kbId: number, pageId: number) =>
http.get(`/wiki/kb/${kbId}/pages/${pageId}/citations`),
// RFC-030: Jobs
getWikiJobs: (kbId: number, rawId: number) =>
http.get(`/wiki/kb/${kbId}/jobs`, { params: { rawId } }),
getKBStats: (kbId: number) =>
http.get(`/wiki/kb/${kbId}/stats`),
// RFC-031: Enrichment & Repair
enrichPage: (kbId: number, slug: string) =>
http.post(`/wiki/kb/${kbId}/pages/${encodeURIComponent(slug)}/enrich`),
repairPage: (kbId: number, slug: string) =>
http.post(`/wiki/kb/${kbId}/pages/${encodeURIComponent(slug)}/repair`),
// RFC-032: Search preview
searchPreview: (kbId: number, data: { query: string; mode?: string; topK?: number }) =>
http.post(`/wiki/kb/${kbId}/search-preview`, data),
}
// ==================== Workspace (Team) ====================
export const workspaceTeamApi = {
list: () => http.get('/workspaces'),
get: (id: string | number) => http.get(`/workspaces/${id}`),
create: (data: any) => http.post('/workspaces', data),
update: (id: string | number, data: any) => http.put(`/workspaces/${id}`, data),
delete: (id: string | number) => http.delete(`/workspaces/${id}`),
listMembers: (id: string | number) => http.get(`/workspaces/${id}/members`),
addMember: (id: string | number, data: { username: string; password?: string; nickname?: string; role?: string }) =>
http.post(`/workspaces/${id}/members`, data),
updateMemberRole: (id: string | number, memberId: string | number, role: string) =>
http.put(`/workspaces/${id}/members/${memberId}`, { role }),
removeMember: (id: string | number, memberId: string | number) =>
http.delete(`/workspaces/${id}/members/${memberId}`),
}
// ==================== Agent Binding ====================
export const agentBindingApi = {
listSkills: (agentId: string | number) => http.get(`/agents/${agentId}/skills`),
setSkills: (agentId: string | number, skillIds: number[]) => http.put(`/agents/${agentId}/skills`, skillIds),
bindSkill: (agentId: string | number, skillId: number) => http.post(`/agents/${agentId}/skills/${skillId}`),
unbindSkill: (agentId: string | number, skillId: number) => http.delete(`/agents/${agentId}/skills/${skillId}`),
listTools: (agentId: string | number) => http.get(`/agents/${agentId}/tools`),
setTools: (agentId: string | number, toolNames: string[]) => http.put(`/agents/${agentId}/tools`, toolNames),
// RFC-009 PR-3: per-agent provider preference order. Empty list = use global chain order.
listProviderPreferences: (agentId: string | number) =>
http.get(`/agents/${agentId}/provider-preferences`),
setProviderPreferences: (agentId: string | number, providerIds: string[]) =>
http.put(`/agents/${agentId}/provider-preferences`, providerIds),
}
// ==================== Dashboard ====================
export const dashboardApi = {
overview: () => http.get('/dashboard/overview'),
trend: (days = 30) => http.get('/dashboard/trend', { params: { days } }),
agentRanking: (days = 7, topN = 10) => http.get('/dashboard/agent-ranking', { params: { days, topN } }),
cronJobRuns: (cronJobId: string | number, limit = 20) => http.get(`/dashboard/cron-runs/${cronJobId}`, { params: { limit } }),
recentRuns: (limit = 20) => http.get('/dashboard/cron-runs', { params: { limit } }),
}
// ==================== Plugins ====================
export const pluginApi = {
list: () => http.get('/plugins'),
get: (name: string) => http.get(`/plugins/${name}`),
disable: (name: string) => http.post(`/plugins/${name}/disable`),
enable: (name: string) => http.post(`/plugins/${name}/enable`),
updateConfig: (name: string, config: Record<string, any>) => http.put(`/plugins/${name}/config`, config),
}
// ==================== Audit Events ====================
export const auditApi = {
listEvents: (params: {
action?: string
resourceType?: string
startTime?: string
endTime?: string
page?: number
size?: number
}) => http.get('/audit/events', { params }),
}
// ==================== Feature Flags ====================
export interface FeatureFlag {
id: number
flagKey: string
enabled: boolean
description?: string
whitelistKbIds?: string
whitelistUserIds?: string
rolloutPercent?: number
createTime?: string
updateTime?: string
}
export interface FeatureFlagUpdate {
enabled?: boolean
description?: string
whitelistKbIds?: string
whitelistUserIds?: string
rolloutPercent?: number
}
export const featureFlagApi = {
list: () => http.get<FeatureFlag[]>('/feature-flags'),
update: (flagKey: string, data: FeatureFlagUpdate) =>
http.put(`/feature-flags/${flagKey}`, data),
}
export interface WikiHotCache {
id: number
kbId: number
content: string | null
contentHash: string | null
lastUpdated: string | null
updateReason: string | null
rebuildCount: number
lastRebuildStartedAt: string | null
lastRebuildDurationMs: number | null
lastRebuildError: string | null
createTime: string
updateTime: string
}
export const hotCacheApi = {
get: (kbId: number) => http.get<WikiHotCache | null>(`/wiki/hot-cache/${kbId}`),
regenerate: (kbId: number) => http.post(`/wiki/hot-cache/${kbId}/regenerate`),
reset: (kbId: number) => http.delete(`/wiki/hot-cache/${kbId}`),
}
// ==================== Workflow ====================
export interface WorkflowSummary {
id: number
workspaceId: number
name: string
description?: string
enabled: boolean
draftJson?: string
draftUpdatedAt?: string
latestRevisionId?: number
createTime: string
updateTime: string
}
export interface WorkflowCompileError {
code: string
path: string
message: string
}
export interface WorkflowCompileFailure {
errorCount: number
errors: WorkflowCompileError[]
}
export interface WorkflowRun {
id: number
workflowId: number
revisionId: number
workspaceId: number
state: string
triggeredBy?: string
initialInputRef?: string
finalOutputRef?: string
errorMessage?: string
startedAt?: string
completedAt?: string
}
export interface WorkflowRunStep {
id: number
runId: number
stepIndex: number
iterationIndex?: number
stepName?: string
state: string
outputRef?: string
outputSummary?: string
outputContentType?: string
errorMessage?: string
durationMs?: number
startedAt?: string
completedAt?: string
}
/** Pause record carried inline on a paused run so the UI can resume
* without a second roundtrip. Mirrors mate_workflow_run_pause. */
export interface WorkflowRunPause {
id: number
runId: number
stepId?: number
pauseKind: string
pauseToken: string
externalApprovalId?: number
pausedAt: string
resumeDeadline?: string
resumePayloadRef?: string
resumedAt?: string
resumeOutcome?: string
}
export interface PausedRunSummary {
run: WorkflowRun
pause: WorkflowRunPause | null
}
export interface RunDetail {
run: WorkflowRun
steps: WorkflowRunStep[]
/** Most recent unresolved pause record; null when the run is not paused. */
activePause: WorkflowRunPause | null
}
export type ResumeOutcome = 'approved' | 'rejected' | 'timeout' | 'cancelled'
export interface ResumeResponse {
kind: string
runId?: number | null
errorMessage?: string | null
}
export const workflowApi = {
list: (workspaceId: number) =>
http.get<WorkflowSummary[]>('/workflows', { params: { workspaceId } }),
get: (id: number) => http.get<WorkflowSummary>(`/workflows/${id}`),
create: (data: Partial<WorkflowSummary>) =>
http.post<WorkflowSummary>('/workflows', data),
update: (id: number, data: Partial<WorkflowSummary>) =>
http.put<WorkflowSummary>(`/workflows/${id}`, data),
delete: (id: number) => http.delete(`/workflows/${id}`),
saveDraft: (id: number, draftJson: string, userId?: number) =>
http.put(`/workflows/${id}/draft`, { draftJson }, { params: userId ? { userId } : {} }),
compile: (id: number) => http.post(`/workflows/${id}/compile`),
publish: (id: number, note?: string, userId?: number) =>
http.post(`/workflows/${id}/publish`,
note ? { note } : {},
{ params: userId ? { userId } : {} }),
runs: (id: number, limit = 50) =>
http.get<WorkflowRun[]>(`/workflows/${id}/runs`, { params: { limit } }),
runDetail: (runId: number) =>
http.get<RunDetail>(`/workflows/runs/${runId}`),
/** List paused runs across the workspace so operators can resume them. */
listPausedRuns: (limit = 50) =>
http.get<PausedRunSummary[]>('/workflows/runs/paused', { params: { limit } }),
/** Resume a paused workflow run with one of the four documented outcomes. */
resumeRun: (runId: number, pauseToken: string, outcome: ResumeOutcome, payload?: string) =>
http.post<ResumeResponse>(`/workflows/runs/${runId}/resume`, {
pauseToken,
outcome,
payload,
}),
/** Generate a workflow draft from natural language. Returns the parsed
* shape; the caller is responsible for creating a workflow row +
* saving the draft if the user accepts it. */
generateDraft: (description: string) =>
http.post<GeneratedDraft>('/workflows/draft/generate', { description }),
/** Canonical workflow templates the generator can apply directly. */
listDraftTemplates: () =>
http.get<WorkflowDraftTemplate[]>('/workflows/draft/templates'),
/** Compile arbitrary draft JSON without persisting. Used by the template
* picker / generator-result preview to give the operator the same compile
* signal the existing /workflows/{id}/compile endpoint provides for saved
* workflows. Resolves on success; rejects with an axios error whose
* response.data.data carries the WorkflowCompileFailure on a 422. */
previewCompileDraft: (draftJson: string) =>
http.post('/workflows/draft/preview-compile', { draftJson }),
}
export interface GeneratedDraft {
name: string
description: string
draftJson: string
triggerDrafts: Array<Record<string, unknown>>
warnings: string[]
missingFields: string[]
confidence?: number | null
compileOk: boolean
compileErrors: WorkflowCompileError[]
}
export interface WorkflowDraftTemplate {
id: string
label: string
description: string
matchHints: string[]
draftJson: string
triggerDraftsJson: string
}
// ==================== Trigger ====================
export interface TriggerSummary {
id: number
workspaceId: number
name?: string
patternType: string
patternJson: string
targetType: string
targetId: number
payloadTemplate?: string
rateLimitPerMin: number
dedupWindowSecs: number
botSelfFilter: boolean
enabled: boolean
fireCount: number
maxFires: number
lastFiredAt?: string
/** Stamp of the last dispatch attempt regardless of outcome (FIRED /
* SKIPPED / FAILED). Distinguishes "never attempted" from
* "attempted but the pre-flight skipped". */
lastDispatchedAt?: string
/** Most recent dispatch outcome message; null when the last attempt
* fired cleanly. */
lastError?: string
patternVersion: number
createTime: string
updateTime: string
}
export const triggerApi = {
list: (workspaceId: number) =>
http.get<TriggerSummary[]>('/triggers', { params: { workspaceId } }),
get: (id: number) => http.get<TriggerSummary>(`/triggers/${id}`),
create: (data: Partial<TriggerSummary>) =>
http.post<TriggerSummary>('/triggers', data),
update: (id: number, data: Partial<TriggerSummary>) =>
http.put<TriggerSummary>(`/triggers/${id}`, data),
delete: (id: number) => http.delete(`/triggers/${id}`),
ingestEvent: (envelope: {
workspaceId: number
patternType: string
eventId?: string
senderId?: string
data?: Record<string, unknown>
}) => http.post('/triggers/events', envelope),
}