feat: enhance agent orchestration, knowledge flow and UI refinements

This commit is contained in:
2026-03-29 20:31:13 +08:00
parent d85cb9cf35
commit e0fe3ca623
301 changed files with 1197804 additions and 7863 deletions

View File

@@ -130,11 +130,11 @@ describe('useChatView orchestration state', () => {
handlers.onMetadata?.({ conversation_id: 'conv-1', message_id: 'msg-1' })
handlers.onProgress?.({
stage: 'planning',
label: 'Jarvis 正在拆解步骤',
agent: 'planner',
label: 'Jarvis 正在编排日程',
agent: 'schedule_planner',
tool_name: null,
step: '正在分配任务',
steps: ['理解问题', '分配 planner'],
step: '正在生成今日安排',
steps: ['理解当前承诺', '分配 schedule_planner'],
})
handlers.onChunk?.({ content: '最终回复' })
})
@@ -150,9 +150,9 @@ describe('useChatView orchestration state', () => {
expect(view.isTyping.value).toBe(true)
expect(view.orchestrationPanelVisible.value).toBe(true)
expect(view.orchestrationStatus.value).toBe('active')
expect(view.activeAgent.value).toBe('planner')
expect(view.visitedAgents.value).toContain('planner')
expect(view.orchestrationEventFeed.value.map((item) => item.label)).toContain('正在分配任务')
expect(view.activeAgent.value).toBe('schedule_planner')
expect(view.visitedAgents.value).toContain('schedule_planner')
expect(view.orchestrationEventFeed.value.flatMap((group) => group.items.map((item) => item.label))).toContain('正在生成今日安排')
await promise
@@ -161,8 +161,37 @@ describe('useChatView orchestration state', () => {
expect(view.store.messages[1].content).toBe('最终回复')
expect(view.orchestrationPanelVisible.value).toBe(true)
expect(view.orchestrationStatus.value).toBe('complete')
expect(view.orchestrationEventFeed.value.at(-1)?.label).toBe('响应已生成')
expect(view.activeAgent.value).toBe('planner')
expect(view.orchestrationEventFeed.value.at(-1)?.items.at(-1)?.label).toBe('响应已生成')
expect(view.activeAgent.value).toBe('schedule_planner')
expect(view.store.messages).toHaveLength(2)
})
it('surfaces schedule fulfillment progress when chat creates a reminder', async () => {
mocks.chatStream.mockImplementation(async (_message, _conversationId, _fileIds, _modelName, handlers) => {
handlers.onMetadata?.({ conversation_id: 'conv-2', message_id: 'msg-2' })
handlers.onProgress?.({
stage: 'tool',
label: 'Jarvis 正在调用工具',
agent: 'executor',
tool_name: 'create_reminder',
step: '提醒创建成功: [abcd1234] 站会 @ 2026-03-28T09:00:00',
steps: [],
})
handlers.onChunk?.({ content: '已经帮你建好提醒。' })
})
const view = useChatView()
view.inputMessage.value = '明天9点提醒我开站会'
const promise = view.sendMessage()
await Promise.resolve()
expect(view.orchestrationInsight.value.statusTitle).toBe('FULFILLMENT')
expect(view.orchestrationInsight.value.jarvisNote).toContain('真的落到系统里')
expect(view.orchestrationEventFeed.value.flatMap((group) => group.items.map((item) => item.label))).toContain('提醒创建成功: [abcd1234] 站会 @ 2026-03-28T09:00:00')
await promise
expect(view.store.messages.at(-1)?.content).toBe('已经帮你建好提醒。')
})
})

View File

@@ -32,10 +32,18 @@ interface ThinkingState {
interface OrchestrationEventItem {
id: string
time: string
label: string
kind: 'info' | 'tool' | 'success' | 'error'
}
interface OrchestrationEventGroup {
id: string
startedAt: string
status: 'active' | 'success' | 'error'
items: OrchestrationEventItem[]
}
interface OrchestrationInsight {
statusTitle: string
systemSummary: string
@@ -50,8 +58,27 @@ interface TelemetryMetricState {
interface SystemTelemetryState {
cpu: TelemetryMetricState
gpu: TelemetryMetricState
memory: TelemetryMetricState
disk: TelemetryMetricState
network: {
upload: TelemetryMetricState
download: TelemetryMetricState
}
}
interface SystemMetaState {
systemName: string
systemVersion: string
hostname: string
timestamp: string
uptimeSeconds: number
diskUsedGb: number
diskTotalGb: number
gpuName: string | null
gpuMemoryTotalMb: number | null
gpuMemoryUsedMb: number | null
gpuUtilPercent: number | null
}
interface SessionTelemetryState {
@@ -63,6 +90,10 @@ interface SessionTelemetryState {
type OrchestrationStatus = 'idle' | 'active' | 'complete' | 'error'
const ORCHESTRATION_EVENT_STORAGE_KEY = 'jarvis.chat.orchestration.events'
const ORCHESTRATION_EVENT_GROUP_LIMIT = 40
const ORCHESTRATION_EVENT_ITEM_LIMIT = 24
export function useChatView() {
const store = useConversationStore()
const auth = useAuthStore()
@@ -89,11 +120,29 @@ export function useChatView() {
})
const activeAgent = ref<string | null>(null)
const visitedAgents = ref<string[]>([])
const orchestrationEventFeed = ref<OrchestrationEventItem[]>([])
const orchestrationEventFeed = ref<OrchestrationEventGroup[]>([])
const systemTelemetry = ref<SystemTelemetryState>({
cpu: { current: null, series: [], online: false },
gpu: { current: null, series: [], online: false },
memory: { current: null, series: [], online: false },
disk: { current: null, series: [], online: false },
network: {
upload: { current: null, series: [], online: false },
download: { current: null, series: [], online: false },
},
})
const systemMeta = ref<SystemMetaState>({
systemName: '--',
systemVersion: '--',
hostname: '--',
timestamp: '',
uptimeSeconds: 0,
diskUsedGb: 0,
diskTotalGb: 0,
gpuName: null,
gpuMemoryTotalMb: null,
gpuMemoryUsedMb: null,
gpuUtilPercent: null,
})
const sessionTelemetry = ref<SessionTelemetryState>({
activitySeries: [],
@@ -105,6 +154,68 @@ export function useChatView() {
let systemTelemetryTimer: ReturnType<typeof setInterval> | null = null
let sessionTelemetryTimer: ReturnType<typeof setInterval> | null = null
function loadPersistedOrchestrationEvents() {
if (typeof window === 'undefined') return
try {
const raw = window.localStorage.getItem(ORCHESTRATION_EVENT_STORAGE_KEY)
if (!raw) return
const parsed = JSON.parse(raw)
if (!Array.isArray(parsed)) return
orchestrationEventFeed.value = parsed
.filter((group): group is OrchestrationEventGroup => (
group
&& typeof group.id === 'string'
&& typeof group.startedAt === 'string'
&& ['active', 'success', 'error'].includes(group.status)
&& Array.isArray(group.items)
))
.map((group) => ({
...group,
items: group.items.filter((item): item is OrchestrationEventItem => (
item
&& typeof item.id === 'string'
&& typeof item.time === 'string'
&& typeof item.label === 'string'
&& ['info', 'tool', 'success', 'error'].includes(item.kind)
)).slice(-ORCHESTRATION_EVENT_ITEM_LIMIT),
}))
.slice(-ORCHESTRATION_EVENT_GROUP_LIMIT)
} catch {
window.localStorage.removeItem(ORCHESTRATION_EVENT_STORAGE_KEY)
}
}
function persistOrchestrationEvents() {
if (typeof window === 'undefined') return
window.localStorage.setItem(
ORCHESTRATION_EVENT_STORAGE_KEY,
JSON.stringify(orchestrationEventFeed.value.slice(-ORCHESTRATION_EVENT_GROUP_LIMIT)),
)
}
function currentEventTime() {
return new Date().toLocaleTimeString('zh-CN', {
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
})
}
function startOrchestrationEventGroup() {
const nextGroup: OrchestrationEventGroup = {
id: `group-${Date.now()}`,
startedAt: currentEventTime(),
status: 'active',
items: [],
}
orchestrationEventFeed.value = [
...orchestrationEventFeed.value,
nextGroup,
].slice(-ORCHESTRATION_EVENT_GROUP_LIMIT)
persistOrchestrationEvents()
}
function resetOrchestrationState() {
orchestrationPanelVisible.value = true
orchestrationStatus.value = 'idle'
@@ -115,7 +226,6 @@ export function useChatView() {
}
activeAgent.value = null
visitedAgents.value = []
orchestrationEventFeed.value = []
sessionTelemetry.value = {
activitySeries: [],
eventsCount: 0,
@@ -139,7 +249,7 @@ export function useChatView() {
}
}
function updateSystemTelemetry(metric: keyof SystemTelemetryState, value: number | null, online: boolean) {
function updateSystemTelemetry(metric: 'cpu' | 'gpu' | 'memory' | 'disk', value: number | null, online: boolean) {
const current = systemTelemetry.value[metric]
systemTelemetry.value = {
...systemTelemetry.value,
@@ -151,17 +261,51 @@ export function useChatView() {
}
}
function updateNetworkTelemetry(direction: 'upload' | 'download', value: number | null, online: boolean) {
const current = systemTelemetry.value.network[direction]
systemTelemetry.value = {
...systemTelemetry.value,
network: {
...systemTelemetry.value.network,
[direction]: {
current: value,
online,
series: value === null ? current.series : appendTelemetryPoint(current.series, value),
},
},
}
}
async function loadSystemStatus() {
try {
const response = await systemApi.getStatus()
systemMeta.value = {
systemName: response.data.system_name,
systemVersion: response.data.system_version,
hostname: response.data.hostname,
timestamp: response.data.timestamp,
uptimeSeconds: response.data.uptime_seconds,
diskUsedGb: response.data.disk_used_gb,
diskTotalGb: response.data.disk_total_gb,
gpuName: response.data.gpu_name,
gpuMemoryTotalMb: response.data.gpu_memory_total_mb,
gpuMemoryUsedMb: response.data.gpu_memory_used_mb,
gpuUtilPercent: response.data.gpu_util_percent,
}
updateSystemTelemetry('cpu', response.data.cpu_percent, true)
updateSystemTelemetry('gpu', response.data.gpu_util_percent, response.data.gpu_util_percent !== null)
updateSystemTelemetry('memory', response.data.memory_percent, true)
updateSystemTelemetry('disk', response.data.disk_percent, true)
updateNetworkTelemetry('upload', response.data.network_upload_bps, true)
updateNetworkTelemetry('download', response.data.network_download_bps, true)
} catch (error) {
console.error('加载系统状态失败:', error)
updateSystemTelemetry('cpu', systemTelemetry.value.cpu.current, false)
updateSystemTelemetry('gpu', systemTelemetry.value.gpu.current, false)
updateSystemTelemetry('memory', systemTelemetry.value.memory.current, false)
updateSystemTelemetry('disk', systemTelemetry.value.disk.current, false)
updateNetworkTelemetry('upload', systemTelemetry.value.network.upload.current, false)
updateNetworkTelemetry('download', systemTelemetry.value.network.download.current, false)
}
}
@@ -187,15 +331,27 @@ export function useChatView() {
function pushOrchestrationEvent(label: string, kind: OrchestrationEventItem['kind']) {
const normalized = label.trim()
if (!normalized) return
if (orchestrationEventFeed.value.at(-1)?.label === normalized) return
orchestrationEventFeed.value = [
...orchestrationEventFeed.value,
{
id: `${Date.now()}-${orchestrationEventFeed.value.length}`,
label: normalized,
kind,
},
].slice(-5)
if (orchestrationEventFeed.value.length === 0) {
startOrchestrationEventGroup()
}
const currentGroup = orchestrationEventFeed.value.at(-1)
if (!currentGroup) return
if (currentGroup.items.at(-1)?.label === normalized) return
const nextItem: OrchestrationEventItem = {
id: `${Date.now()}-${currentGroup.items.length}`,
time: currentEventTime(),
label: normalized,
kind,
}
orchestrationEventFeed.value = orchestrationEventFeed.value.map((group, index) => (
index === orchestrationEventFeed.value.length - 1
? {
...group,
items: [...group.items, nextItem].slice(-ORCHESTRATION_EVENT_ITEM_LIMIT),
}
: group
))
persistOrchestrationEvents()
}
function buildOrchestrationInsight(payload: ChatProgressEvent): OrchestrationInsight {
@@ -212,7 +368,7 @@ export function useChatView() {
if (payload.stage === 'planning') {
return {
statusTitle: 'ROUTING',
systemSummary: payload.agent === 'planner' ? '已路由至 planner正在拆解任务' : '正在规划执行链路',
systemSummary: payload.agent === 'schedule_planner' ? '已路由至 schedule_planner正在编排日程' : '正在规划执行链路',
jarvisNote: payload.steps?.length
? '问题有几层关系,按顺序拆开会体面很多。'
: '这一步需要一点秩序感。',
@@ -220,12 +376,17 @@ export function useChatView() {
}
if (payload.stage === 'tool') {
const isScheduleAction = payload.tool_name
? ['create_reminder', 'create_goal', 'create_todo', 'create_schedule_task', 'create_task'].includes(payload.tool_name)
: false
return {
statusTitle: 'EXECUTION',
statusTitle: isScheduleAction ? 'FULFILLMENT' : 'EXECUTION',
systemSummary: payload.tool_name ? `正在调用工具 · ${payload.tool_name}` : '正在执行操作',
jarvisNote: payload.tool_name
? '工具链已接通。希望它今天愿意配合。'
: '执行阶段开始了,接下来看看链路表现。',
jarvisNote: isScheduleAction
? '这次不是只给建议,记录会真的落到系统里。'
: payload.tool_name
? '工具链已接通。希望它今天愿意配合。'
: '执行阶段开始了,接下来看看链路表现。',
}
}
@@ -274,6 +435,12 @@ export function useChatView() {
jarvisNote: '很好,问题已经收束。',
}
pushOrchestrationEvent(finalLabel, status === 'error' ? 'error' : 'success')
orchestrationEventFeed.value = orchestrationEventFeed.value.map((group, index) => (
index === orchestrationEventFeed.value.length - 1
? { ...group, status: status === 'error' ? 'error' : 'success' }
: group
))
persistOrchestrationEvents()
}
async function sendMessage() {
@@ -287,6 +454,7 @@ export function useChatView() {
const tempMessageId = `temp-${Date.now()}`
const previousConversationId = store.currentConversationId
inputMessage.value = ''
startOrchestrationEventGroup()
store.addMessage({
id: tempMessageId,
@@ -392,9 +560,7 @@ export function useChatView() {
try {
const response = await settingsApi.get()
const chatModelsList = (response.data.llm_config?.chat || []).filter((model) => model.enabled)
const vlmModels = (response.data.llm_config?.vlm || []).filter((model) => model.enabled)
// 合并 chat 和 vlm 模型
chatModels.value = [...chatModelsList, ...vlmModels]
chatModels.value = chatModelsList
if (!selectedModelName.value || !chatModels.value.some((model) => model.name === selectedModelName.value)) {
selectedModelName.value = chatModels.value[0]?.name || ''
}
@@ -518,6 +684,7 @@ export function useChatView() {
}
void loadSystemStatus()
loadPersistedOrchestrationEvents()
startSystemTelemetryPolling()
startSessionTelemetryDecay()
@@ -556,6 +723,7 @@ export function useChatView() {
activeAgent,
visitedAgents,
orchestrationEventFeed,
systemMeta,
systemTelemetry,
sessionTelemetry,
sendMessage,