mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 03:55:09 +08:00
feat(stream): add fine-grained phase status hints for frontend UX
This commit is contained in:
parent
d5f1d19306
commit
2ac7cc4af4
@ -172,7 +172,7 @@ public class ToolExecutionExecutor {
|
|||||||
|
|
||||||
// 4. 分类: concurrencySafe
|
// 4. 分类: concurrencySafe
|
||||||
boolean safe = isConcurrencySafe(toolName);
|
boolean safe = isConcurrencySafe(toolName);
|
||||||
preparedCalls.add(new PreparedToolCall(toolCall, callback, arguments, safe, allResponses.size()));
|
preparedCalls.add(new PreparedToolCall(toolCall, callback, arguments, safe, allResponses.size(), conversationId));
|
||||||
// 占位,Phase 2 填充
|
// 占位,Phase 2 填充
|
||||||
allResponses.add(null);
|
allResponses.add(null);
|
||||||
}
|
}
|
||||||
@ -235,6 +235,14 @@ public class ToolExecutionExecutor {
|
|||||||
private void executePreparedCalls(List<PreparedToolCall> preparedCalls,
|
private void executePreparedCalls(List<PreparedToolCall> preparedCalls,
|
||||||
List<ToolResponseMessage.ToolResponse> allResponses,
|
List<ToolResponseMessage.ToolResponse> allResponses,
|
||||||
List<GraphEventPublisher.GraphEvent> events) {
|
List<GraphEventPublisher.GraphEvent> events) {
|
||||||
|
if (!preparedCalls.isEmpty() && streamTracker != null) {
|
||||||
|
String conversationId = preparedCalls.get(0).conversationId;
|
||||||
|
String phase = classifyBatchPhase(preparedCalls);
|
||||||
|
streamTracker.updatePhase(conversationId, phase);
|
||||||
|
streamTracker.broadcastObject(conversationId, "phase", GraphEventPublisher.phase(phase, Map.of(
|
||||||
|
"toolCount", preparedCalls.size()
|
||||||
|
)).data());
|
||||||
|
}
|
||||||
// 分组: 连续的 safe 工具可以并行,遇到 unsafe 工具则先等待所有 safe 完成再独占执行
|
// 分组: 连续的 safe 工具可以并行,遇到 unsafe 工具则先等待所有 safe 完成再独占执行
|
||||||
List<List<PreparedToolCall>> batches = buildExecutionBatches(preparedCalls);
|
List<List<PreparedToolCall>> batches = buildExecutionBatches(preparedCalls);
|
||||||
|
|
||||||
@ -320,6 +328,11 @@ public class ToolExecutionExecutor {
|
|||||||
List<GraphEventPublisher.GraphEvent> events) {
|
List<GraphEventPublisher.GraphEvent> events) {
|
||||||
String toolName = pc.toolCall.name();
|
String toolName = pc.toolCall.name();
|
||||||
try {
|
try {
|
||||||
|
if (streamTracker != null) {
|
||||||
|
streamTracker.updateRunningTool(pc.conversationId, toolName);
|
||||||
|
streamTracker.broadcastObject(pc.conversationId, GraphEventPublisher.EVENT_TOOL_START,
|
||||||
|
GraphEventPublisher.toolStart(toolName, pc.arguments).data());
|
||||||
|
}
|
||||||
log.info("[ToolExecutor] Executing tool: {} with args: {}",
|
log.info("[ToolExecutor] Executing tool: {} with args: {}",
|
||||||
toolName, pc.arguments != null && pc.arguments.length() > 200
|
toolName, pc.arguments != null && pc.arguments.length() > 200
|
||||||
? pc.arguments.substring(0, 200) + "..." : pc.arguments);
|
? pc.arguments.substring(0, 200) + "..." : pc.arguments);
|
||||||
@ -338,12 +351,22 @@ public class ToolExecutionExecutor {
|
|||||||
log.info("[ToolExecutor] Tool {} returned {} chars", toolName, rawLen);
|
log.info("[ToolExecutor] Tool {} returned {} chars", toolName, rawLen);
|
||||||
}
|
}
|
||||||
events.add(GraphEventPublisher.toolComplete(toolName, result, true));
|
events.add(GraphEventPublisher.toolComplete(toolName, result, true));
|
||||||
|
if (streamTracker != null) {
|
||||||
|
streamTracker.broadcastObject(pc.conversationId, GraphEventPublisher.EVENT_TOOL_COMPLETE,
|
||||||
|
GraphEventPublisher.toolComplete(toolName, result, true).data());
|
||||||
|
streamTracker.updateRunningTool(pc.conversationId, null);
|
||||||
|
}
|
||||||
return new ToolResponseMessage.ToolResponse(
|
return new ToolResponseMessage.ToolResponse(
|
||||||
pc.toolCall.id(), toolName, result != null ? result : "");
|
pc.toolCall.id(), toolName, result != null ? result : "");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("[ToolExecutor] Tool {} execution failed: {}", toolName, e.getMessage(), e);
|
log.error("[ToolExecutor] Tool {} execution failed: {}", toolName, e.getMessage(), e);
|
||||||
String normalizedError = normalizeToolExecutionError(e);
|
String normalizedError = normalizeToolExecutionError(e);
|
||||||
events.add(GraphEventPublisher.toolComplete(toolName, normalizedError, false));
|
events.add(GraphEventPublisher.toolComplete(toolName, normalizedError, false));
|
||||||
|
if (streamTracker != null) {
|
||||||
|
streamTracker.broadcastObject(pc.conversationId, GraphEventPublisher.EVENT_TOOL_COMPLETE,
|
||||||
|
GraphEventPublisher.toolComplete(toolName, normalizedError, false).data());
|
||||||
|
streamTracker.updateRunningTool(pc.conversationId, null);
|
||||||
|
}
|
||||||
return new ToolResponseMessage.ToolResponse(
|
return new ToolResponseMessage.ToolResponse(
|
||||||
pc.toolCall.id(), toolName, normalizedError);
|
pc.toolCall.id(), toolName, normalizedError);
|
||||||
}
|
}
|
||||||
@ -408,6 +431,13 @@ public class ToolExecutionExecutor {
|
|||||||
return !DEFAULT_UNSAFE_TOOLS.contains(toolName);
|
return !DEFAULT_UNSAFE_TOOLS.contains(toolName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String classifyBatchPhase(List<PreparedToolCall> preparedCalls) {
|
||||||
|
boolean memoryOnly = preparedCalls.stream().allMatch(pc ->
|
||||||
|
"read_workspace_memory_file".equals(pc.toolCall.name())
|
||||||
|
|| "list_workspace_memory_files".equals(pc.toolCall.name()));
|
||||||
|
return memoryOnly ? "reading_memory" : "executing_tool";
|
||||||
|
}
|
||||||
|
|
||||||
private String normalizeToolExecutionError(Exception e) {
|
private String normalizeToolExecutionError(Exception e) {
|
||||||
String message = e != null && e.getMessage() != null ? e.getMessage() : "未知错误";
|
String message = e != null && e.getMessage() != null ? e.getMessage() : "未知错误";
|
||||||
String lower = message.toLowerCase(Locale.ROOT);
|
String lower = message.toLowerCase(Locale.ROOT);
|
||||||
@ -444,7 +474,8 @@ public class ToolExecutionExecutor {
|
|||||||
ToolCallback callback,
|
ToolCallback callback,
|
||||||
String arguments,
|
String arguments,
|
||||||
boolean concurrencySafe,
|
boolean concurrencySafe,
|
||||||
int resultIndex
|
int resultIndex,
|
||||||
|
String conversationId
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private record ApprovalBarrier(String pendingId, String toolName) {}
|
private record ApprovalBarrier(String pendingId, String toolName) {}
|
||||||
|
|||||||
@ -206,6 +206,10 @@ public class ReasoningNode implements NodeAction {
|
|||||||
|
|
||||||
GraphEventPublisher.GraphEvent phaseEvent = GraphEventPublisher.phase("reasoning",
|
GraphEventPublisher.GraphEvent phaseEvent = GraphEventPublisher.phase("reasoning",
|
||||||
Map.of("iteration", accessor.iterationCount()));
|
Map.of("iteration", accessor.iterationCount()));
|
||||||
|
pushPhase(conversationId, "reasoning", Map.of(
|
||||||
|
"iteration", accessor.iterationCount(),
|
||||||
|
"llmCallCount", nextLlmCallCount
|
||||||
|
));
|
||||||
|
|
||||||
NodeStreamingChatHelper.StreamResult result;
|
NodeStreamingChatHelper.StreamResult result;
|
||||||
try {
|
try {
|
||||||
@ -225,6 +229,11 @@ public class ReasoningNode implements NodeAction {
|
|||||||
messages.size(), compactedMessages.size());
|
messages.size(), compactedMessages.size());
|
||||||
// compact retry 是第 2 次 LLM 调用,先递增再调用
|
// compact retry 是第 2 次 LLM 调用,先递增再调用
|
||||||
nextLlmCallCount++;
|
nextLlmCallCount++;
|
||||||
|
pushPhase(conversationId, "reasoning", Map.of(
|
||||||
|
"iteration", accessor.iterationCount(),
|
||||||
|
"llmCallCount", nextLlmCallCount,
|
||||||
|
"compacted", true
|
||||||
|
));
|
||||||
result = streamingHelper.streamCall(chatModel, retryPrompt, conversationId, "reasoning_compact_retry");
|
result = streamingHelper.streamCall(chatModel, retryPrompt, conversationId, "reasoning_compact_retry");
|
||||||
} else {
|
} else {
|
||||||
log.warn("[ReasoningNode] Compaction did not reduce messages, cannot retry");
|
log.warn("[ReasoningNode] Compaction did not reduce messages, cannot retry");
|
||||||
@ -296,6 +305,10 @@ public class ReasoningNode implements NodeAction {
|
|||||||
log.info("[ReasoningNode] LLM requested {} tool call(s): {}",
|
log.info("[ReasoningNode] LLM requested {} tool call(s): {}",
|
||||||
result.toolCalls().size(),
|
result.toolCalls().size(),
|
||||||
result.toolCalls().stream().map(AssistantMessage.ToolCall::name).toList());
|
result.toolCalls().stream().map(AssistantMessage.ToolCall::name).toList());
|
||||||
|
pushPhase(conversationId, "executing_tool", Map.of(
|
||||||
|
"iteration", accessor.iterationCount(),
|
||||||
|
"toolCount", result.toolCalls().size()
|
||||||
|
));
|
||||||
|
|
||||||
return MateClawStateAccessor.output()
|
return MateClawStateAccessor.output()
|
||||||
.needsToolCall(true)
|
.needsToolCall(true)
|
||||||
@ -315,6 +328,10 @@ public class ReasoningNode implements NodeAction {
|
|||||||
} else {
|
} else {
|
||||||
String content = result.text();
|
String content = result.text();
|
||||||
log.info("[ReasoningNode] LLM produced final answer ({} chars)", content != null ? content.length() : 0);
|
log.info("[ReasoningNode] LLM produced final answer ({} chars)", content != null ? content.length() : 0);
|
||||||
|
pushPhase(conversationId, "drafting_answer", Map.of(
|
||||||
|
"iteration", accessor.iterationCount(),
|
||||||
|
"answerChars", content != null ? content.length() : 0
|
||||||
|
));
|
||||||
|
|
||||||
return MateClawStateAccessor.output()
|
return MateClawStateAccessor.output()
|
||||||
.needsToolCall(false)
|
.needsToolCall(false)
|
||||||
@ -347,4 +364,12 @@ public class ReasoningNode implements NodeAction {
|
|||||||
throw new RuntimeException("无法反序列化 forced_tool_call: " + e.getMessage(), e);
|
throw new RuntimeException("无法反序列化 forced_tool_call: " + e.getMessage(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void pushPhase(String conversationId, String phase, Map<String, Object> extra) {
|
||||||
|
if (streamTracker == null || !StringUtils.hasText(conversationId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
streamTracker.updatePhase(conversationId, phase);
|
||||||
|
streamTracker.broadcastObject(conversationId, "phase", GraphEventPublisher.phase(phase, extra).data());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -82,6 +82,10 @@ public class SummarizingNode implements NodeAction {
|
|||||||
|
|
||||||
log.info("[SummarizingNode] Summarizing {} observations ({} total chars) for user query",
|
log.info("[SummarizingNode] Summarizing {} observations ({} total chars) for user query",
|
||||||
observations.size(), accessor.totalObservationChars());
|
observations.size(), accessor.totalObservationChars());
|
||||||
|
pushPhase(conversationId, "summarizing_observations", Map.of(
|
||||||
|
"observationCount", observations.size(),
|
||||||
|
"summaryChars", accessor.totalObservationChars()
|
||||||
|
));
|
||||||
|
|
||||||
// 构建 summarize prompt
|
// 构建 summarize prompt
|
||||||
StringBuilder observationText = new StringBuilder();
|
StringBuilder observationText = new StringBuilder();
|
||||||
@ -176,4 +180,12 @@ public class SummarizingNode implements NodeAction {
|
|||||||
"summaryChars", summaryContent.length()))))
|
"summaryChars", summaryContent.length()))))
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void pushPhase(String conversationId, String phase, Map<String, Object> extra) {
|
||||||
|
if (streamTracker == null || conversationId == null || conversationId.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
streamTracker.updatePhase(conversationId, phase);
|
||||||
|
streamTracker.broadcastObject(conversationId, "phase", GraphEventPublisher.phase(phase, extra).data());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1218,8 +1218,14 @@ public class ChatController {
|
|||||||
runtimeProviderId = String.valueOf(data.getOrDefault("runtimeProviderId", ""));
|
runtimeProviderId = String.valueOf(data.getOrDefault("runtimeProviderId", ""));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if ("phase".equals(delta.eventType())) {
|
||||||
|
String phase = String.valueOf(delta.eventData().getOrDefault("phase", ""));
|
||||||
|
if (!phase.isBlank()) {
|
||||||
|
streamTracker.updatePhase(conversationId, phase);
|
||||||
|
}
|
||||||
|
}
|
||||||
// 累积工具调用事件,用于持久化到消息历史
|
// 累积工具调用事件,用于持久化到消息历史
|
||||||
accumulateToolEvent(delta.eventType(), delta.eventData());
|
accumulateToolEvent(delta.eventType(), delta.eventData(), conversationId);
|
||||||
try {
|
try {
|
||||||
broadcastEvent(conversationId, delta.eventType(), delta.eventData());
|
broadcastEvent(conversationId, delta.eventType(), delta.eventData());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@ -1229,6 +1235,7 @@ public class ChatController {
|
|||||||
}
|
}
|
||||||
if (delta.content() != null && !delta.content().isBlank()) {
|
if (delta.content() != null && !delta.content().isBlank()) {
|
||||||
content.append(delta.content());
|
content.append(delta.content());
|
||||||
|
streamTracker.updatePhase(conversationId, "drafting_answer");
|
||||||
if (!delta.persistenceOnly()) {
|
if (!delta.persistenceOnly()) {
|
||||||
broadcastEvent(conversationId, "content_delta", Map.of("delta", delta.content()));
|
broadcastEvent(conversationId, "content_delta", Map.of("delta", delta.content()));
|
||||||
}
|
}
|
||||||
@ -1243,9 +1250,10 @@ public class ChatController {
|
|||||||
|
|
||||||
boolean isAwaitingApproval() { return awaitingApproval; }
|
boolean isAwaitingApproval() { return awaitingApproval; }
|
||||||
|
|
||||||
private void accumulateToolEvent(String eventType, Map<String, Object> data) {
|
private void accumulateToolEvent(String eventType, Map<String, Object> data, String conversationId) {
|
||||||
if ("tool_approval_requested".equals(eventType)) {
|
if ("tool_approval_requested".equals(eventType)) {
|
||||||
awaitingApproval = true;
|
awaitingApproval = true;
|
||||||
|
streamTracker.updatePhase(conversationId, "awaiting_approval");
|
||||||
} else if ("tool_call_started".equals(eventType)) {
|
} else if ("tool_call_started".equals(eventType)) {
|
||||||
Map<String, Object> tc = new LinkedHashMap<>();
|
Map<String, Object> tc = new LinkedHashMap<>();
|
||||||
tc.put("name", data.getOrDefault("toolName", ""));
|
tc.put("name", data.getOrDefault("toolName", ""));
|
||||||
|
|||||||
@ -2,8 +2,12 @@
|
|||||||
<div v-if="isLoading && !hideBar" class="stream-loading-bar">
|
<div v-if="isLoading && !hideBar" class="stream-loading-bar">
|
||||||
<div class="stream-loading-content">
|
<div class="stream-loading-content">
|
||||||
<span class="loading-icon" :class="phaseIconClass">{{ phaseIcon }}</span>
|
<span class="loading-icon" :class="phaseIconClass">{{ phaseIcon }}</span>
|
||||||
<span class="loading-text" :class="phaseTextClass">{{ statusText }}</span>
|
<div class="loading-copy">
|
||||||
<span v-if="runningToolName" class="loading-tool">{{ runningToolName }}</span>
|
<span class="loading-text" :class="phaseTextClass">{{ statusText }}</span>
|
||||||
|
<span v-if="runningToolName" class="loading-tool">{{ runningToolName }}</span>
|
||||||
|
<span v-if="statusDetail" class="loading-detail">{{ statusDetail }}</span>
|
||||||
|
<span v-if="slowHint" class="loading-slow">{{ slowHint }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="loading-right">
|
<div class="loading-right">
|
||||||
<!-- 排队指示器 -->
|
<!-- 排队指示器 -->
|
||||||
@ -25,7 +29,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch, onBeforeUnmount } from 'vue'
|
import { ref, computed, watch, onBeforeUnmount } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import type { StreamPhase } from '@/types'
|
import type { PhaseEventData, StreamPhase } from '@/types'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
@ -36,6 +40,8 @@ interface Props {
|
|||||||
promptTokens?: number
|
promptTokens?: number
|
||||||
/** 当前流阶段 */
|
/** 当前流阶段 */
|
||||||
phase?: StreamPhase
|
phase?: StreamPhase
|
||||||
|
/** 最近一次阶段事件 */
|
||||||
|
phaseInfo?: PhaseEventData | null
|
||||||
/** 当前正在执行的工具名称 */
|
/** 当前正在执行的工具名称 */
|
||||||
runningToolName?: string
|
runningToolName?: string
|
||||||
/** 是否有排队消息 */
|
/** 是否有排队消息 */
|
||||||
@ -49,6 +55,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
completionTokens: 0,
|
completionTokens: 0,
|
||||||
promptTokens: 0,
|
promptTokens: 0,
|
||||||
phase: 'thinking',
|
phase: 'thinking',
|
||||||
|
phaseInfo: null,
|
||||||
runningToolName: '',
|
runningToolName: '',
|
||||||
hasQueued: false,
|
hasQueued: false,
|
||||||
})
|
})
|
||||||
@ -57,10 +64,17 @@ const { t } = useI18n()
|
|||||||
|
|
||||||
// Phase-aware 状态文本(i18n)
|
// Phase-aware 状态文本(i18n)
|
||||||
const phaseI18nMap: Record<string, string> = {
|
const phaseI18nMap: Record<string, string> = {
|
||||||
|
preparing_context: 'chat.streamPreparingContext',
|
||||||
|
reading_memory: 'chat.streamReadingMemory',
|
||||||
|
reasoning: 'chat.streamReasoning',
|
||||||
|
drafting_answer: 'chat.streamDraftingAnswer',
|
||||||
|
summarizing_observations: 'chat.streamSummarizingObservations',
|
||||||
thinking: 'chat.streamThinking',
|
thinking: 'chat.streamThinking',
|
||||||
streaming: 'chat.streamGenerating',
|
streaming: 'chat.streamGenerating',
|
||||||
executing_tool: 'chat.streamExecutingTool',
|
executing_tool: 'chat.streamExecutingTool',
|
||||||
awaiting_approval: 'chat.streamAwaitingApproval',
|
awaiting_approval: 'chat.streamAwaitingApproval',
|
||||||
|
finalizing: 'chat.streamFinalizing',
|
||||||
|
failed: 'chat.streamFailed',
|
||||||
interrupting: 'chat.streamInterrupting',
|
interrupting: 'chat.streamInterrupting',
|
||||||
queued: 'chat.streamQueued',
|
queued: 'chat.streamQueued',
|
||||||
reconnecting: 'chat.streamReconnecting',
|
reconnecting: 'chat.streamReconnecting',
|
||||||
@ -77,10 +91,17 @@ const statusText = computed(() => {
|
|||||||
|
|
||||||
const phaseIcon = computed(() => {
|
const phaseIcon = computed(() => {
|
||||||
switch (props.phase) {
|
switch (props.phase) {
|
||||||
|
case 'preparing_context': return '◔'
|
||||||
|
case 'reading_memory': return '⌕'
|
||||||
|
case 'reasoning': return '◐'
|
||||||
|
case 'drafting_answer': return '✎'
|
||||||
|
case 'summarizing_observations': return '≋'
|
||||||
case 'thinking': return '◐'
|
case 'thinking': return '◐'
|
||||||
case 'streaming': return '▸'
|
case 'streaming': return '▸'
|
||||||
case 'executing_tool': return '⚙'
|
case 'executing_tool': return '⚙'
|
||||||
case 'awaiting_approval': return '⏸'
|
case 'awaiting_approval': return '⏸'
|
||||||
|
case 'finalizing': return '✓'
|
||||||
|
case 'failed': return '!'
|
||||||
case 'interrupting': return '⊘'
|
case 'interrupting': return '⊘'
|
||||||
case 'queued': return '◷'
|
case 'queued': return '◷'
|
||||||
case 'reconnecting': return '↻'
|
case 'reconnecting': return '↻'
|
||||||
@ -91,6 +112,7 @@ const phaseIcon = computed(() => {
|
|||||||
const phaseIconClass = computed(() => {
|
const phaseIconClass = computed(() => {
|
||||||
switch (props.phase) {
|
switch (props.phase) {
|
||||||
case 'awaiting_approval': return 'icon-paused'
|
case 'awaiting_approval': return 'icon-paused'
|
||||||
|
case 'failed': return 'icon-warning'
|
||||||
case 'interrupting': return 'icon-warning'
|
case 'interrupting': return 'icon-warning'
|
||||||
case 'queued': return 'icon-queued'
|
case 'queued': return 'icon-queued'
|
||||||
default: return 'icon-active'
|
default: return 'icon-active'
|
||||||
@ -100,12 +122,53 @@ const phaseIconClass = computed(() => {
|
|||||||
const phaseTextClass = computed(() => {
|
const phaseTextClass = computed(() => {
|
||||||
switch (props.phase) {
|
switch (props.phase) {
|
||||||
case 'awaiting_approval': return 'text-amber'
|
case 'awaiting_approval': return 'text-amber'
|
||||||
|
case 'failed': return 'text-red'
|
||||||
case 'interrupting': return 'text-red'
|
case 'interrupting': return 'text-red'
|
||||||
case 'queued': return 'text-blue'
|
case 'queued': return 'text-blue'
|
||||||
default: return ''
|
default: return ''
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const detailI18nMap: Record<string, string> = {
|
||||||
|
preparing_context: 'chat.streamPreparingContextDetail',
|
||||||
|
reading_memory: 'chat.streamReadingMemoryDetail',
|
||||||
|
reasoning: 'chat.streamReasoningDetail',
|
||||||
|
drafting_answer: 'chat.streamDraftingAnswerDetail',
|
||||||
|
summarizing_observations: 'chat.streamSummarizingObservationsDetail',
|
||||||
|
thinking: 'chat.streamThinkingDetail',
|
||||||
|
streaming: 'chat.streamGeneratingDetail',
|
||||||
|
executing_tool: 'chat.streamExecutingToolDetail',
|
||||||
|
awaiting_approval: 'chat.streamAwaitingApprovalDetail',
|
||||||
|
finalizing: 'chat.streamFinalizingDetail',
|
||||||
|
failed: 'chat.streamFailedDetail',
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusDetail = computed(() => {
|
||||||
|
if (props.phaseInfo?.phase) {
|
||||||
|
const key = detailI18nMap[props.phaseInfo.phase]
|
||||||
|
if (key) return t(key)
|
||||||
|
}
|
||||||
|
const key = detailI18nMap[props.phase]
|
||||||
|
return key ? t(key) : ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const slowHint = computed(() => {
|
||||||
|
const secs = elapsedSeconds.value
|
||||||
|
if (props.phase === 'summarizing_observations' && secs >= 15) {
|
||||||
|
return t('chat.streamSlowSummarizing')
|
||||||
|
}
|
||||||
|
if ((props.phase === 'reasoning' || props.phase === 'thinking') && secs >= 20) {
|
||||||
|
return t('chat.streamSlowReasoning')
|
||||||
|
}
|
||||||
|
if (secs >= 45) {
|
||||||
|
return t('chat.streamSlowGeneral')
|
||||||
|
}
|
||||||
|
if (secs >= 8) {
|
||||||
|
return t('chat.streamSlowShort')
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
|
||||||
// 耗时统计
|
// 耗时统计
|
||||||
const elapsedSeconds = ref(0)
|
const elapsedSeconds = ref(0)
|
||||||
const elapsedTime = ref('0s')
|
const elapsedTime = ref('0s')
|
||||||
@ -174,11 +237,18 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
.stream-loading-content {
|
.stream-loading-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
color: #f97316;
|
color: #f97316;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loading-copy {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.loading-icon {
|
.loading-icon {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@ -214,6 +284,7 @@ onBeforeUnmount(() => {
|
|||||||
.text-blue { color: #3b82f6; }
|
.text-blue { color: #3b82f6; }
|
||||||
|
|
||||||
.loading-tool {
|
.loading-tool {
|
||||||
|
align-self: flex-start;
|
||||||
font-family: ui-monospace, 'SFMono-Regular', Consolas, monospace;
|
font-family: ui-monospace, 'SFMono-Regular', Consolas, monospace;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
background: rgba(249, 115, 22, 0.1);
|
background: rgba(249, 115, 22, 0.1);
|
||||||
@ -226,6 +297,16 @@ onBeforeUnmount(() => {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loading-detail {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--mc-text-secondary, #94a3b8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-slow {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #f59e0b;
|
||||||
|
}
|
||||||
|
|
||||||
.loading-right {
|
.loading-right {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import { ref, computed } from 'vue'
|
|||||||
import { useMessages } from './useMessages'
|
import { useMessages } from './useMessages'
|
||||||
import { useStream } from './useStream'
|
import { useStream } from './useStream'
|
||||||
import { useMessageQueue } from './useMessageQueue'
|
import { useMessageQueue } from './useMessageQueue'
|
||||||
import type { Message, MessageContentPart, StreamPhase, HeartbeatData, QueuedMessage } from '@/types'
|
import type { Message, MessageContentPart, StreamPhase, HeartbeatData, QueuedMessage, PhaseEventData } from '@/types'
|
||||||
import { classifyBackendError, type ChatErrorInfo } from '@/types/chatError'
|
import { classifyBackendError, type ChatErrorInfo } from '@/types/chatError'
|
||||||
|
|
||||||
export interface UseChatOptions {
|
export interface UseChatOptions {
|
||||||
@ -46,6 +46,8 @@ export interface UseChatReturn {
|
|||||||
isGenerating: import('vue').ComputedRef<boolean>
|
isGenerating: import('vue').ComputedRef<boolean>
|
||||||
/** 当前流阶段 */
|
/** 当前流阶段 */
|
||||||
streamPhase: import('vue').Ref<StreamPhase>
|
streamPhase: import('vue').Ref<StreamPhase>
|
||||||
|
/** 最近一次阶段事件 */
|
||||||
|
phaseInfo: import('vue').Ref<PhaseEventData | null>
|
||||||
/** 当前错误 */
|
/** 当前错误 */
|
||||||
error: import('vue').Ref<Error | null>
|
error: import('vue').Ref<Error | null>
|
||||||
/** 排队的消息 */
|
/** 排队的消息 */
|
||||||
@ -105,6 +107,7 @@ export function useChat(options: UseChatOptions): UseChatReturn {
|
|||||||
/** stopGeneration 的 fallback timer,新流开始时必须清除,防止误杀新连接 */
|
/** stopGeneration 的 fallback timer,新流开始时必须清除,防止误杀新连接 */
|
||||||
let stopFallbackTimer: ReturnType<typeof setTimeout> | null = null
|
let stopFallbackTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
const streamPhase = ref<StreamPhase>('idle')
|
const streamPhase = ref<StreamPhase>('idle')
|
||||||
|
const phaseInfo = ref<PhaseEventData | null>(null)
|
||||||
const heartbeat = ref<HeartbeatData | null>(null)
|
const heartbeat = ref<HeartbeatData | null>(null)
|
||||||
/** Track which conversation the current stream belongs to */
|
/** Track which conversation the current stream belongs to */
|
||||||
let streamConversationId = ''
|
let streamConversationId = ''
|
||||||
@ -159,7 +162,7 @@ export function useChat(options: UseChatOptions): UseChatReturn {
|
|||||||
stream.on('content_delta', (data) => {
|
stream.on('content_delta', (data) => {
|
||||||
if (currentAssistantId.value) {
|
if (currentAssistantId.value) {
|
||||||
appendMessageContent(currentAssistantId.value, data.delta || '', 'text')
|
appendMessageContent(currentAssistantId.value, data.delta || '', 'text')
|
||||||
if (streamPhase.value === 'thinking') {
|
if (['thinking', 'reasoning', 'drafting_answer', 'preparing_context'].includes(streamPhase.value)) {
|
||||||
streamPhase.value = 'streaming'
|
streamPhase.value = 'streaming'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -168,7 +171,9 @@ export function useChat(options: UseChatOptions): UseChatReturn {
|
|||||||
stream.on('thinking_delta', (data) => {
|
stream.on('thinking_delta', (data) => {
|
||||||
if (currentAssistantId.value) {
|
if (currentAssistantId.value) {
|
||||||
appendMessageContent(currentAssistantId.value, data.delta || '', 'thinking')
|
appendMessageContent(currentAssistantId.value, data.delta || '', 'thinking')
|
||||||
streamPhase.value = 'thinking'
|
if (streamPhase.value !== 'summarizing_observations') {
|
||||||
|
streamPhase.value = 'thinking'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -265,6 +270,9 @@ export function useChat(options: UseChatOptions): UseChatReturn {
|
|||||||
|
|
||||||
streamPhase.value = data.status === 'awaiting_approval' ? 'awaiting_approval'
|
streamPhase.value = data.status === 'awaiting_approval' ? 'awaiting_approval'
|
||||||
: data.status === 'stopped' ? 'stopped' : 'completed'
|
: data.status === 'stopped' ? 'stopped' : 'completed'
|
||||||
|
if (data.status !== 'awaiting_approval') {
|
||||||
|
phaseInfo.value = null
|
||||||
|
}
|
||||||
|
|
||||||
// 兜底清理排队状态(如果 queued_input_started 已经处理了则这里是 no-op)
|
// 兜底清理排队状态(如果 queued_input_started 已经处理了则这里是 no-op)
|
||||||
if (!messageQueue.hasQueued.value) {
|
if (!messageQueue.hasQueued.value) {
|
||||||
@ -307,6 +315,7 @@ export function useChat(options: UseChatOptions): UseChatReturn {
|
|||||||
}
|
}
|
||||||
error.value = new Error(data.message || '请求失败')
|
error.value = new Error(data.message || '请求失败')
|
||||||
streamPhase.value = 'idle'
|
streamPhase.value = 'idle'
|
||||||
|
phaseInfo.value = null
|
||||||
// 错误时清理排队状态,避免脏残留
|
// 错误时清理排队状态,避免脏残留
|
||||||
messageQueue.clear()
|
messageQueue.clear()
|
||||||
|
|
||||||
@ -369,11 +378,16 @@ export function useChat(options: UseChatOptions): UseChatReturn {
|
|||||||
|
|
||||||
stream.on('phase', (data) => {
|
stream.on('phase', (data) => {
|
||||||
const phase = data.phase as StreamPhase
|
const phase = data.phase as StreamPhase
|
||||||
if (phase) streamPhase.value = phase
|
if (phase) {
|
||||||
|
streamPhase.value = phase
|
||||||
|
phaseInfo.value = { ...data, phase }
|
||||||
|
}
|
||||||
if (currentAssistantId.value) {
|
if (currentAssistantId.value) {
|
||||||
const msg = getMessage(currentAssistantId.value)
|
const msg = getMessage(currentAssistantId.value)
|
||||||
if (msg) {
|
if (msg) {
|
||||||
const metadata = parseMetadata((msg as any).metadata)
|
const metadata = parseMetadata((msg as any).metadata)
|
||||||
|
// 去重:相同 phase 不触发 updateMessage,避免不必要的 Vue 响应式更新
|
||||||
|
if (metadata.currentPhase === data.phase) return
|
||||||
updateMessage(currentAssistantId.value, {
|
updateMessage(currentAssistantId.value, {
|
||||||
...msg,
|
...msg,
|
||||||
metadata: { ...metadata, currentPhase: data.phase }
|
metadata: { ...metadata, currentPhase: data.phase }
|
||||||
@ -538,10 +552,17 @@ export function useChat(options: UseChatOptions): UseChatReturn {
|
|||||||
// 从 heartbeat 中更新 phase(如果前端还没有更精确的 phase)
|
// 从 heartbeat 中更新 phase(如果前端还没有更精确的 phase)
|
||||||
if (data.currentPhase && streamPhase.value !== 'interrupting') {
|
if (data.currentPhase && streamPhase.value !== 'interrupting') {
|
||||||
const phaseMap: Record<string, StreamPhase> = {
|
const phaseMap: Record<string, StreamPhase> = {
|
||||||
|
'preparing_context': 'preparing_context',
|
||||||
|
'reading_memory': 'reading_memory',
|
||||||
|
'reasoning': 'reasoning',
|
||||||
|
'drafting_answer': 'drafting_answer',
|
||||||
|
'summarizing_observations': 'summarizing_observations',
|
||||||
'thinking': 'thinking',
|
'thinking': 'thinking',
|
||||||
'streaming': 'streaming',
|
'streaming': 'streaming',
|
||||||
'executing_tool': 'executing_tool',
|
'executing_tool': 'executing_tool',
|
||||||
'awaiting_approval': 'awaiting_approval',
|
'awaiting_approval': 'awaiting_approval',
|
||||||
|
'finalizing': 'finalizing',
|
||||||
|
'failed': 'failed',
|
||||||
}
|
}
|
||||||
const mapped = phaseMap[data.currentPhase]
|
const mapped = phaseMap[data.currentPhase]
|
||||||
if (mapped) streamPhase.value = mapped
|
if (mapped) streamPhase.value = mapped
|
||||||
@ -594,6 +615,7 @@ export function useChat(options: UseChatOptions): UseChatReturn {
|
|||||||
assistantMessage.conversationId = data.conversationId || streamConversationId
|
assistantMessage.conversationId = data.conversationId || streamConversationId
|
||||||
currentAssistantId.value = assistantMessage.id as string
|
currentAssistantId.value = assistantMessage.id as string
|
||||||
streamPhase.value = 'thinking'
|
streamPhase.value = 'thinking'
|
||||||
|
phaseInfo.value = null
|
||||||
})
|
})
|
||||||
|
|
||||||
// ===== 发送消息(支持运行中继续发送) =====
|
// ===== 发送消息(支持运行中继续发送) =====
|
||||||
@ -619,6 +641,7 @@ export function useChat(options: UseChatOptions): UseChatReturn {
|
|||||||
errorFired = false
|
errorFired = false
|
||||||
streamConversationId = conversationId
|
streamConversationId = conversationId
|
||||||
streamPhase.value = 'thinking'
|
streamPhase.value = 'thinking'
|
||||||
|
phaseInfo.value = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!isApprovalCommand) {
|
if (!isApprovalCommand) {
|
||||||
@ -684,6 +707,7 @@ export function useChat(options: UseChatOptions): UseChatReturn {
|
|||||||
assistantMessage.conversationId = conversationId
|
assistantMessage.conversationId = conversationId
|
||||||
currentAssistantId.value = assistantMessage.id as string
|
currentAssistantId.value = assistantMessage.id as string
|
||||||
streamPhase.value = 'thinking'
|
streamPhase.value = 'thinking'
|
||||||
|
phaseInfo.value = null
|
||||||
await stream.connect({
|
await stream.connect({
|
||||||
agentId,
|
agentId,
|
||||||
message: content,
|
message: content,
|
||||||
@ -716,6 +740,7 @@ export function useChat(options: UseChatOptions): UseChatReturn {
|
|||||||
|
|
||||||
// 标记为停止中(让 UI 立即反馈)
|
// 标记为停止中(让 UI 立即反馈)
|
||||||
streamPhase.value = 'stopped'
|
streamPhase.value = 'stopped'
|
||||||
|
phaseInfo.value = null
|
||||||
|
|
||||||
if (streamConversationId) {
|
if (streamConversationId) {
|
||||||
try {
|
try {
|
||||||
@ -783,6 +808,7 @@ export function useChat(options: UseChatOptions): UseChatReturn {
|
|||||||
streamConversationId = conversationId
|
streamConversationId = conversationId
|
||||||
error.value = null
|
error.value = null
|
||||||
errorFired = false
|
errorFired = false
|
||||||
|
phaseInfo.value = null
|
||||||
|
|
||||||
// 创建 assistant 占位消息用于接收重连后的流数据
|
// 创建 assistant 占位消息用于接收重连后的流数据
|
||||||
const assistantMessage = createAssistantMessage('')
|
const assistantMessage = createAssistantMessage('')
|
||||||
@ -841,6 +867,7 @@ export function useChat(options: UseChatOptions): UseChatReturn {
|
|||||||
messages,
|
messages,
|
||||||
isGenerating,
|
isGenerating,
|
||||||
streamPhase,
|
streamPhase,
|
||||||
|
phaseInfo,
|
||||||
error,
|
error,
|
||||||
queuedMessage: messageQueue.queuedMessage,
|
queuedMessage: messageQueue.queuedMessage,
|
||||||
hasQueued: messageQueue.hasQueued,
|
hasQueued: messageQueue.hasQueued,
|
||||||
|
|||||||
@ -136,15 +136,37 @@ export default {
|
|||||||
queuedReplace: 'Message queued. Press Enter to replace...',
|
queuedReplace: 'Message queued. Press Enter to replace...',
|
||||||
queuedBadge: '{count} queued',
|
queuedBadge: '{count} queued',
|
||||||
// Stream status
|
// Stream status
|
||||||
|
streamPreparingContext: 'Preparing context...',
|
||||||
|
streamReadingMemory: 'Reading memory...',
|
||||||
|
streamReasoning: 'Analyzing...',
|
||||||
|
streamDraftingAnswer: 'Drafting answer...',
|
||||||
|
streamSummarizingObservations: 'Organizing information...',
|
||||||
streamThinking: 'Thinking...',
|
streamThinking: 'Thinking...',
|
||||||
streamGenerating: 'Generating...',
|
streamGenerating: 'Generating...',
|
||||||
streamExecutingTool: 'Executing tool...',
|
streamExecutingTool: 'Executing tool...',
|
||||||
streamAwaitingApproval: 'Awaiting approval',
|
streamAwaitingApproval: 'Awaiting approval',
|
||||||
|
streamFinalizing: 'Finalizing...',
|
||||||
|
streamFailed: 'Interrupted',
|
||||||
streamInterrupting: 'Interrupting...',
|
streamInterrupting: 'Interrupting...',
|
||||||
streamQueued: 'Queued, waiting...',
|
streamQueued: 'Queued, waiting...',
|
||||||
streamReconnecting: 'Reconnecting...',
|
streamReconnecting: 'Reconnecting...',
|
||||||
streamStopped: 'Stopped',
|
streamStopped: 'Stopped',
|
||||||
streamCompleted: 'Completed',
|
streamCompleted: 'Completed',
|
||||||
|
streamPreparingContextDetail: 'Collecting the current question, conversation history, and required context.',
|
||||||
|
streamReadingMemoryDetail: 'Looking up the most relevant long-term memory and recent notes for this question.',
|
||||||
|
streamReasoningDetail: 'Comparing options and organizing the answer structure based on the available information.',
|
||||||
|
streamDraftingAnswerDetail: 'The main analysis is done. Turning it into a readable answer now.',
|
||||||
|
streamSummarizingObservationsDetail: 'A larger tool result just came back. Condensing the key points before continuing.',
|
||||||
|
streamThinkingDetail: 'The model is still reasoning internally. Visible text may not appear immediately.',
|
||||||
|
streamGeneratingDetail: 'Visible output has started and more content should appear shortly.',
|
||||||
|
streamExecutingToolDetail: 'Reading files, querying data, or running the necessary operation for a more accurate answer.',
|
||||||
|
streamAwaitingApprovalDetail: 'Your confirmation is required before continuing.',
|
||||||
|
streamFinalizingDetail: 'Saving the result and closing out this response.',
|
||||||
|
streamFailedDetail: 'This run did not finish cleanly, but you can retry or keep asking follow-up questions.',
|
||||||
|
streamSlowShort: 'This step is taking a bit longer than usual, but it is still running.',
|
||||||
|
streamSlowSummarizing: 'Organizing a larger tool result. This step often takes longer.',
|
||||||
|
streamSlowReasoning: 'The model is doing heavier analysis here. This is not a frozen connection.',
|
||||||
|
streamSlowGeneral: 'Processing is still ongoing. The time is currently being spent in internal analysis.',
|
||||||
// Approval bar
|
// Approval bar
|
||||||
approvalAllow: 'Allow',
|
approvalAllow: 'Allow',
|
||||||
approvalExecute: 'to execute?',
|
approvalExecute: 'to execute?',
|
||||||
|
|||||||
@ -136,15 +136,37 @@ export default {
|
|||||||
queuedReplace: '消息已排队,按回车替换...',
|
queuedReplace: '消息已排队,按回车替换...',
|
||||||
queuedBadge: '{count} 条排队',
|
queuedBadge: '{count} 条排队',
|
||||||
// 流状态
|
// 流状态
|
||||||
|
streamPreparingContext: '准备上下文...',
|
||||||
|
streamReadingMemory: '读取记忆...',
|
||||||
|
streamReasoning: '分析问题...',
|
||||||
|
streamDraftingAnswer: '生成回答...',
|
||||||
|
streamSummarizingObservations: '整理信息...',
|
||||||
streamThinking: '思考中...',
|
streamThinking: '思考中...',
|
||||||
streamGenerating: '生成中...',
|
streamGenerating: '生成中...',
|
||||||
streamExecutingTool: '执行工具...',
|
streamExecutingTool: '执行工具...',
|
||||||
streamAwaitingApproval: '等待审批',
|
streamAwaitingApproval: '等待审批',
|
||||||
|
streamFinalizing: '正在收尾...',
|
||||||
|
streamFailed: '本轮中断',
|
||||||
streamInterrupting: '中断中...',
|
streamInterrupting: '中断中...',
|
||||||
streamQueued: '排队中...',
|
streamQueued: '排队中...',
|
||||||
streamReconnecting: '重新连接...',
|
streamReconnecting: '重新连接...',
|
||||||
streamStopped: '已停止',
|
streamStopped: '已停止',
|
||||||
streamCompleted: '已完成',
|
streamCompleted: '已完成',
|
||||||
|
streamPreparingContextDetail: '正在收集当前问题、历史对话和必要配置。',
|
||||||
|
streamReadingMemoryDetail: '正在查找与你当前问题最相关的长期记忆和今日记录。',
|
||||||
|
streamReasoningDetail: '正在基于现有信息进行判断、比较方案并组织回答结构。',
|
||||||
|
streamDraftingAnswerDetail: '已经完成主要分析,正在把结论写成可直接阅读的答案。',
|
||||||
|
streamSummarizingObservationsDetail: '刚刚拿到了较多工具结果,正在提炼重点,避免回答冗长或遗漏关键信息。',
|
||||||
|
streamThinkingDetail: '正在做内部推理,这一步未必会立即产生正文。',
|
||||||
|
streamGeneratingDetail: '已经进入可见输出阶段,会陆续显示结果。',
|
||||||
|
streamExecutingToolDetail: '正在读取文件、查询数据或执行必要操作,以便给出更准确的答案。',
|
||||||
|
streamAwaitingApprovalDetail: '接下来需要你的确认,确认后我会继续执行。',
|
||||||
|
streamFinalizingDetail: '正在保存结果并结束当前响应。',
|
||||||
|
streamFailedDetail: '这次处理没有完整完成,但你可以继续追问或重试。',
|
||||||
|
streamSlowShort: '这一步比平时慢一些,但系统仍在处理中。',
|
||||||
|
streamSlowSummarizing: '正在整理较长的工具结果。因为信息较多,这一步可能需要更久。',
|
||||||
|
streamSlowReasoning: '正在做较复杂的分析和取舍,不是卡住。',
|
||||||
|
streamSlowGeneral: '处理还在继续,当前主要耗时在内部分析,不代表连接中断。',
|
||||||
// 审批栏
|
// 审批栏
|
||||||
approvalAllow: '允许',
|
approvalAllow: '允许',
|
||||||
approvalExecute: '执行?',
|
approvalExecute: '执行?',
|
||||||
|
|||||||
@ -362,10 +362,17 @@ export const CHANNEL_FIELD_DEFS: Record<string, ChannelFieldDef[]> = {
|
|||||||
|
|
||||||
/** 流阶段(前后端统一命名) */
|
/** 流阶段(前后端统一命名) */
|
||||||
export type StreamPhase =
|
export type StreamPhase =
|
||||||
|
| 'preparing_context' // 正在准备上下文
|
||||||
|
| 'reading_memory' // 正在读取记忆/历史
|
||||||
|
| 'reasoning' // 正在推理分析
|
||||||
|
| 'drafting_answer' // 正在起草答案
|
||||||
|
| 'summarizing_observations' // 正在整理工具结果
|
||||||
| 'thinking' // 模型推理中
|
| 'thinking' // 模型推理中
|
||||||
| 'streaming' // 正在输出文本
|
| 'streaming' // 正在输出文本
|
||||||
| 'executing_tool' // 正在执行工具
|
| 'executing_tool' // 正在执行工具
|
||||||
| 'awaiting_approval' // 等待审批
|
| 'awaiting_approval' // 等待审批
|
||||||
|
| 'finalizing' // 正在收尾
|
||||||
|
| 'failed' // 已失败
|
||||||
| 'interrupting' // 正在中断
|
| 'interrupting' // 正在中断
|
||||||
| 'queued' // 有排队消息
|
| 'queued' // 有排队消息
|
||||||
| 'reconnecting' // 正在重连
|
| 'reconnecting' // 正在重连
|
||||||
@ -373,6 +380,17 @@ export type StreamPhase =
|
|||||||
| 'completed' // 已完成
|
| 'completed' // 已完成
|
||||||
| 'idle' // 空闲
|
| 'idle' // 空闲
|
||||||
|
|
||||||
|
/** 阶段事件数据 */
|
||||||
|
export interface PhaseEventData {
|
||||||
|
phase: StreamPhase | string
|
||||||
|
timestamp?: number
|
||||||
|
toolName?: string
|
||||||
|
toolCount?: number
|
||||||
|
observationCount?: number
|
||||||
|
summaryChars?: number
|
||||||
|
iteration?: number
|
||||||
|
}
|
||||||
|
|
||||||
/** 排队的用户消息 */
|
/** 排队的用户消息 */
|
||||||
export interface QueuedMessage {
|
export interface QueuedMessage {
|
||||||
/** 消息内容 */
|
/** 消息内容 */
|
||||||
|
|||||||
@ -159,6 +159,7 @@
|
|||||||
:completion-tokens="currentGeneratingTokens"
|
:completion-tokens="currentGeneratingTokens"
|
||||||
:prompt-tokens="currentPromptTokens"
|
:prompt-tokens="currentPromptTokens"
|
||||||
:phase="streamPhase"
|
:phase="streamPhase"
|
||||||
|
:phase-info="phaseInfo"
|
||||||
:running-tool-name="currentRunningToolName"
|
:running-tool-name="currentRunningToolName"
|
||||||
:has-queued="hasQueued"
|
:has-queued="hasQueued"
|
||||||
/>
|
/>
|
||||||
@ -363,6 +364,7 @@ const {
|
|||||||
messages,
|
messages,
|
||||||
isGenerating,
|
isGenerating,
|
||||||
streamPhase,
|
streamPhase,
|
||||||
|
phaseInfo,
|
||||||
queuedMessage,
|
queuedMessage,
|
||||||
hasQueued,
|
hasQueued,
|
||||||
queueSize,
|
queueSize,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user