All checks were successful
Deploy Noval to K3s / deploy (push) Successful in 54s
- **子 Agent 支持**: - 新增 `dispatch_subagent` 工具,支持分配特定任务给子 Agent,由其独立操作。 - 前端新增 `ChatSubAgent` 组件,支持子 Agent 任务状态展示(运行中/已完成)、工具调用进度及输出预览等。 - **技能管理扩展**: - 新增 Skill 编辑器页面(`SkillEditorView.vue`),支持创建、编辑、删除技能。 - 完善技能工具权限分组选择功能,按类型灵活分配工具。 - **工具增强**: - 新增工具接口:`get_outline_detail`、`get_character_detail`、`get_chapter_detail`,用于获取详情内容。 - 优化部分工具返回数据,仅输出基础信息以减少上下文体积。 - 后端添加 `SPECIAL_TOOLS` 支持,增强 `dispatch_subagent` 的特殊处理能力。 同时优化部分前后端交互,提升代码维护性与可读性。
217 lines
6.5 KiB
TypeScript
217 lines
6.5 KiB
TypeScript
import { http } from './client'
|
||
import type { AIConfig, AIConfigSave, ChatMessageList, ToolCallEntry } from '@/types/chat'
|
||
|
||
// ── AI配置 ──
|
||
|
||
export async function getAIConfig(): Promise<AIConfig> {
|
||
const { data } = await http.get<AIConfig>('/ai/config')
|
||
return data
|
||
}
|
||
|
||
export async function saveAIConfig(payload: AIConfigSave): Promise<AIConfig> {
|
||
const { data } = await http.put<AIConfig>('/ai/config', payload)
|
||
return data
|
||
}
|
||
|
||
export async function testAIConfig(payload: AIConfigSave): Promise<{ ok: boolean; message: string }> {
|
||
const { data } = await http.post<{ ok: boolean; message: string }>('/ai/config/test', payload)
|
||
return data
|
||
}
|
||
|
||
// ── 聊天消息 ──
|
||
|
||
export async function fetchMessages(novelId?: number): Promise<ChatMessageList> {
|
||
const params: Record<string, unknown> = {}
|
||
if (novelId !== undefined) params.novel_id = novelId
|
||
const { data } = await http.get<ChatMessageList>('/chat/messages', { params })
|
||
return data
|
||
}
|
||
|
||
export async function clearMessages(novelId?: number): Promise<void> {
|
||
const params: Record<string, unknown> = {}
|
||
if (novelId !== undefined) params.novel_id = novelId
|
||
await http.delete('/chat/messages', { params })
|
||
}
|
||
|
||
export interface CompressResult {
|
||
compressed: boolean
|
||
reason?: string
|
||
error?: string
|
||
removed_count?: number
|
||
kept_count?: number
|
||
summary_length?: number
|
||
}
|
||
|
||
export async function compressContext(novelId?: number): Promise<CompressResult> {
|
||
const params: Record<string, unknown> = {}
|
||
if (novelId !== undefined) params.novel_id = novelId
|
||
const { data } = await http.post<CompressResult>('/chat/compress', null, { params })
|
||
return data
|
||
}
|
||
|
||
// ── 流式聊天 ──
|
||
|
||
export interface TokenUsage {
|
||
prompt_tokens: number
|
||
completion_tokens: number
|
||
total_tokens: number
|
||
}
|
||
|
||
export interface ToolResultInfo {
|
||
name: string
|
||
label: string
|
||
result: string
|
||
}
|
||
|
||
export interface PendingToolCallData {
|
||
id: string
|
||
name: string
|
||
label: string
|
||
arguments: Record<string, unknown>
|
||
}
|
||
|
||
export interface SubAgentStartInfo {
|
||
skill_id: number
|
||
skill_name: string
|
||
task: string
|
||
subagent_id: string
|
||
}
|
||
|
||
export interface SubAgentToolCallInfo {
|
||
name: string
|
||
label: string
|
||
arguments: Record<string, unknown>
|
||
}
|
||
|
||
export interface SubAgentToolResultInfo {
|
||
name: string
|
||
label: string
|
||
result: string
|
||
}
|
||
|
||
export interface StreamChatOptions {
|
||
messages: { role: string; content: string }[]
|
||
novelId?: number
|
||
pageContext?: string
|
||
toolsEnabled?: boolean
|
||
autoApproveTools?: boolean
|
||
skillId?: number | null
|
||
// 工具审批续传
|
||
pendingToolCalls?: PendingToolCallData[]
|
||
assistantText?: string
|
||
// 回调
|
||
onChunk: (text: string) => void
|
||
onError: (error: string) => void
|
||
onDone: (usage?: TokenUsage, pending?: boolean) => void
|
||
onToolCallsPending?: (calls: PendingToolCallData[], assistantText: string) => void
|
||
onToolCallsAuto?: (calls: PendingToolCallData[]) => void
|
||
onToolResult?: (info: ToolResultInfo) => void
|
||
// 子 Agent 回调(subagentId 用于区分并行的多个子 Agent)
|
||
onSubAgentStart?: (info: SubAgentStartInfo) => void
|
||
onSubAgentChunk?: (text: string, subagentId: string) => void
|
||
onSubAgentToolCall?: (info: SubAgentToolCallInfo, subagentId: string) => void
|
||
onSubAgentToolResult?: (info: SubAgentToolResultInfo, subagentId: string) => void
|
||
onSubAgentDone?: (result: string, subagentId: string) => void
|
||
signal?: AbortSignal
|
||
}
|
||
|
||
export async function streamChat(options: StreamChatOptions): Promise<void> {
|
||
const {
|
||
messages, novelId, pageContext, toolsEnabled, autoApproveTools, skillId,
|
||
pendingToolCalls, assistantText,
|
||
onChunk, onError, onDone, onToolCallsPending, onToolCallsAuto, onToolResult,
|
||
onSubAgentStart, onSubAgentChunk, onSubAgentToolCall, onSubAgentToolResult, onSubAgentDone,
|
||
signal,
|
||
} = options
|
||
|
||
const body = JSON.stringify({
|
||
messages,
|
||
novel_id: novelId ?? null,
|
||
page_context: pageContext ?? null,
|
||
tools_enabled: toolsEnabled ?? true,
|
||
auto_approve_tools: autoApproveTools ?? false,
|
||
skill_id: skillId ?? null,
|
||
pending_tool_calls: pendingToolCalls ?? null,
|
||
assistant_text: assistantText ?? null,
|
||
})
|
||
|
||
const resp = await fetch('/api/chat/stream', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body,
|
||
signal,
|
||
})
|
||
|
||
if (!resp.ok || !resp.body) {
|
||
onError(`请求失败: ${resp.status}`)
|
||
onDone()
|
||
return
|
||
}
|
||
|
||
const reader = resp.body.getReader()
|
||
const decoder = new TextDecoder()
|
||
let buffer = ''
|
||
|
||
try {
|
||
while (true) {
|
||
const { done, value } = await reader.read()
|
||
if (done) break
|
||
|
||
buffer += decoder.decode(value, { stream: true })
|
||
const lines = buffer.split('\n')
|
||
buffer = lines.pop() ?? ''
|
||
|
||
for (const line of lines) {
|
||
if (!line.startsWith('data: ')) continue
|
||
try {
|
||
const event = JSON.parse(line.slice(6))
|
||
if (event.done) {
|
||
onDone(event.usage ?? undefined, event.pending ?? false)
|
||
return
|
||
}
|
||
if (event.error) {
|
||
onError(event.error)
|
||
onDone()
|
||
return
|
||
}
|
||
if (event.content) {
|
||
onChunk(event.content)
|
||
}
|
||
if (event.tool_calls_pending && onToolCallsPending) {
|
||
onToolCallsPending(event.tool_calls_pending, event.assistant_text ?? '')
|
||
}
|
||
if (event.tool_calls_auto && onToolCallsAuto) {
|
||
onToolCallsAuto(event.tool_calls_auto)
|
||
}
|
||
if (event.tool_result && onToolResult) {
|
||
onToolResult(event.tool_result)
|
||
}
|
||
// 子 Agent 事件(所有事件携带 subagent_id 用于区分并行的子 Agent)
|
||
if (event.subagent_start && onSubAgentStart) {
|
||
onSubAgentStart(event.subagent_start)
|
||
}
|
||
if (event.subagent_chunk && onSubAgentChunk) {
|
||
onSubAgentChunk(event.subagent_chunk, event.subagent_id ?? '')
|
||
}
|
||
if (event.subagent_tool_call && onSubAgentToolCall) {
|
||
onSubAgentToolCall(event.subagent_tool_call, event.subagent_id ?? '')
|
||
}
|
||
if (event.subagent_tool_result && onSubAgentToolResult) {
|
||
onSubAgentToolResult(event.subagent_tool_result, event.subagent_id ?? '')
|
||
}
|
||
if (event.subagent_done !== undefined && onSubAgentDone) {
|
||
onSubAgentDone(event.subagent_done, event.subagent_id ?? '')
|
||
}
|
||
} catch {
|
||
// 忽略解析错误
|
||
}
|
||
}
|
||
}
|
||
} catch (e) {
|
||
if ((e as Error).name !== 'AbortError') {
|
||
onError((e as Error).message)
|
||
}
|
||
}
|
||
onDone()
|
||
}
|