Files
NovalRedot/frontend/src/api/chat.ts
ROOG 0525c8217e
All checks were successful
Deploy Noval to K3s / deploy (push) Successful in 54s
新增子 Agent 与技能系统管理:
- **子 Agent 支持**:
  - 新增 `dispatch_subagent` 工具,支持分配特定任务给子 Agent,由其独立操作。
  - 前端新增 `ChatSubAgent` 组件,支持子 Agent 任务状态展示(运行中/已完成)、工具调用进度及输出预览等。

- **技能管理扩展**:
  - 新增 Skill 编辑器页面(`SkillEditorView.vue`),支持创建、编辑、删除技能。
  - 完善技能工具权限分组选择功能,按类型灵活分配工具。

- **工具增强**:
  - 新增工具接口:`get_outline_detail`、`get_character_detail`、`get_chapter_detail`,用于获取详情内容。
  - 优化部分工具返回数据,仅输出基础信息以减少上下文体积。

- 后端添加 `SPECIAL_TOOLS` 支持,增强 `dispatch_subagent` 的特殊处理能力。

同时优化部分前后端交互,提升代码维护性与可读性。
2026-03-23 02:23:22 +08:00

217 lines
6.5 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 { 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()
}