mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 20:08:18 +08:00
feat(agent,ui): recovery affordance card for non-transient LLM errors
This commit is contained in:
parent
48510c9751
commit
f18626304e
@ -45,6 +45,22 @@ public final class GraphEventPublisher {
|
|||||||
*/
|
*/
|
||||||
public static final String EVENT_FINISH_REASON = "finish_reason";
|
public static final String EVENT_FINISH_REASON = "finish_reason";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User-facing recovery affordances offered after a turn ends in a
|
||||||
|
* non-transient error. Carries the error type + message + a
|
||||||
|
* data-driven list of actions ({@code retry}, {@code regenerate},
|
||||||
|
* {@code report}) so the frontend can render the right buttons
|
||||||
|
* without hard-coding which categories deserve which actions.
|
||||||
|
*
|
||||||
|
* <p>Sibling to {@link #EVENT_FINISH_REASON} (which only carries the
|
||||||
|
* machine-readable reason). The two are kept separate so legacy
|
||||||
|
* consumers of {@code finish_reason} don't have to learn a new
|
||||||
|
* payload shape — and so a future graph branch (e.g. evidence-
|
||||||
|
* insufficient → "rerun with the listed files attached") can emit
|
||||||
|
* feedback affordances without abusing the finish_reason channel.
|
||||||
|
*/
|
||||||
|
public static final String EVENT_FEEDBACK = "feedback_event";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Multimodal sidecar routing decision for the current turn. Emitted once
|
* Multimodal sidecar routing decision for the current turn. Emitted once
|
||||||
* per turn before the graph starts streaming; the channel-side accumulator
|
* per turn before the graph starts streaming; the channel-side accumulator
|
||||||
@ -226,6 +242,30 @@ public final class GraphEventPublisher {
|
|||||||
), ts);
|
), ts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit a recovery-affordance event for the frontend. {@code errorType}
|
||||||
|
* mirrors the {@code NodeStreamingChatHelper.ErrorType} value (e.g.
|
||||||
|
* {@code AUTH_ERROR}, {@code BILLING}, {@code MODEL_NOT_FOUND}, or
|
||||||
|
* the generic {@code UNKNOWN}); {@code errorMessage} is the
|
||||||
|
* user-friendly text already displayed in the bubble; {@code actions}
|
||||||
|
* is the ordered list of buttons to render. Default offering is the
|
||||||
|
* standard {@code retry / regenerate / report} triad — call sites
|
||||||
|
* can narrow this if a category has limitations (e.g. AUTH_ERROR
|
||||||
|
* shouldn't offer "retry" until the key is fixed).
|
||||||
|
*/
|
||||||
|
public static GraphEvent feedback(String errorType, String errorMessage,
|
||||||
|
java.util.List<String> actions) {
|
||||||
|
long ts = System.currentTimeMillis();
|
||||||
|
return new GraphEvent(EVENT_FEEDBACK, Map.of(
|
||||||
|
"errorType", errorType != null ? errorType : "",
|
||||||
|
"errorMessage", errorMessage != null ? errorMessage : "",
|
||||||
|
"actions", actions != null && !actions.isEmpty()
|
||||||
|
? actions
|
||||||
|
: java.util.List.of("retry", "regenerate", "report"),
|
||||||
|
"timestamp", ts
|
||||||
|
), ts);
|
||||||
|
}
|
||||||
|
|
||||||
// ===== 提取方法 =====
|
// ===== 提取方法 =====
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -170,6 +170,24 @@ public class FinalAnswerNode implements NodeAction {
|
|||||||
validation.unsupportedReferences());
|
validation.unsupportedReferences());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build the event list. Always carries the finish_reason event so
|
||||||
|
// downstream consumers (memory gate, channel accumulator, message
|
||||||
|
// metadata persistence) see a machine-readable status. When the
|
||||||
|
// turn ended in a non-transient error, also attach a
|
||||||
|
// feedback_event so the frontend can render retry/regenerate/
|
||||||
|
// report affordances next to the red "[错误] ..." bubble — without
|
||||||
|
// this, fatal errors leave the user staring at error text with no
|
||||||
|
// way to recover short of retyping the whole prompt.
|
||||||
|
List<GraphEventPublisher.GraphEvent> events =
|
||||||
|
new java.util.ArrayList<>(2);
|
||||||
|
events.add(GraphEventPublisher.finishReason(finishReason.getValue()));
|
||||||
|
if (finishReason == FinishReason.ERROR_FALLBACK) {
|
||||||
|
events.add(GraphEventPublisher.feedback(
|
||||||
|
"ERROR_FALLBACK",
|
||||||
|
finalAnswer,
|
||||||
|
List.of("retry", "regenerate", "report")));
|
||||||
|
}
|
||||||
|
|
||||||
// 不重置 CONTENT_STREAMED/THINKING_STREAMED,保留上游节点的标志
|
// 不重置 CONTENT_STREAMED/THINKING_STREAMED,保留上游节点的标志
|
||||||
var builder = MateClawStateAccessor.output()
|
var builder = MateClawStateAccessor.output()
|
||||||
.finalAnswer(finalAnswer)
|
.finalAnswer(finalAnswer)
|
||||||
@ -183,7 +201,7 @@ public class FinalAnswerNode implements NodeAction {
|
|||||||
// signal. APPEND-strategy on PENDING_EVENTS means this
|
// signal. APPEND-strategy on PENDING_EVENTS means this
|
||||||
// composes safely with any earlier events upstream nodes
|
// composes safely with any earlier events upstream nodes
|
||||||
// attached.
|
// attached.
|
||||||
.events(List.of(GraphEventPublisher.finishReason(finishReason.getValue())));
|
.events(events);
|
||||||
|
|
||||||
if (!finalThinking.isEmpty()) {
|
if (!finalThinking.isEmpty()) {
|
||||||
builder.finalThinking(finalThinking);
|
builder.finalThinking(finalThinking);
|
||||||
|
|||||||
@ -1646,6 +1646,14 @@ public class ChatController {
|
|||||||
* having to guess from text. Empty string until the event arrives.
|
* having to guess from text. Empty string until the event arrives.
|
||||||
*/
|
*/
|
||||||
private String finishReason = "";
|
private String finishReason = "";
|
||||||
|
/**
|
||||||
|
* Recovery affordance payload from {@link
|
||||||
|
* vip.mate.agent.GraphEventPublisher#feedback}. Persisted into
|
||||||
|
* {@code metadata.feedbackEvent} so a page reload still surfaces
|
||||||
|
* the retry/regenerate/report card on the failed assistant
|
||||||
|
* bubble. Null when the turn ended cleanly.
|
||||||
|
*/
|
||||||
|
private Map<String, Object> feedbackEvent = null;
|
||||||
private Long planId = null;
|
private Long planId = null;
|
||||||
private List<String> planSteps = List.of();
|
private List<String> planSteps = List.of();
|
||||||
private Integer currentPlanStep = null;
|
private Integer currentPlanStep = null;
|
||||||
@ -1691,6 +1699,15 @@ public class ChatController {
|
|||||||
finishReason = String.valueOf(reason);
|
finishReason = String.valueOf(reason);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (vip.mate.agent.GraphEventPublisher.EVENT_FEEDBACK
|
||||||
|
.equals(delta.eventType())) {
|
||||||
|
// Snapshot the affordance payload so it persists into
|
||||||
|
// message metadata. The same event is also rebroadcast
|
||||||
|
// live (via the broadcastEvent fall-through below) so
|
||||||
|
// an already-mounted UI sees it instantly without
|
||||||
|
// waiting for the message-save round trip.
|
||||||
|
feedbackEvent = delta.eventData();
|
||||||
|
}
|
||||||
if (vip.mate.agent.GraphEventPublisher.EVENT_ROUTING_DECISION.equals(delta.eventType())) {
|
if (vip.mate.agent.GraphEventPublisher.EVENT_ROUTING_DECISION.equals(delta.eventType())) {
|
||||||
// Captured at turn start; persisted under metadata.routing so the
|
// Captured at turn start; persisted under metadata.routing so the
|
||||||
// chat UI can render which sidecar (if any) was invoked. Internal
|
// chat UI can render which sidecar (if any) was invoked. Internal
|
||||||
@ -1982,6 +1999,15 @@ public class ChatController {
|
|||||||
// brittle text matching on the assistant content.
|
// brittle text matching on the assistant content.
|
||||||
metadata.put("finishReason", finishReason);
|
metadata.put("finishReason", finishReason);
|
||||||
}
|
}
|
||||||
|
if (feedbackEvent != null && !feedbackEvent.isEmpty()) {
|
||||||
|
// Persist the recovery-affordance payload so the
|
||||||
|
// retry/regenerate/report card survives page reload.
|
||||||
|
// Stored as-is (errorType, errorMessage, actions,
|
||||||
|
// timestamp) — frontend MessageBubble reads
|
||||||
|
// metadata.feedbackEvent and renders one button per
|
||||||
|
// entry in `actions`.
|
||||||
|
metadata.put("feedbackEvent", feedbackEvent);
|
||||||
|
}
|
||||||
if (routingDecision != null && !routingDecision.isEmpty()) {
|
if (routingDecision != null && !routingDecision.isEmpty()) {
|
||||||
metadata.put("routing", routingDecision);
|
metadata.put("routing", routingDecision);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -221,6 +221,37 @@
|
|||||||
<p class="evidence-card__description">{{ $t('chat.evidenceDescription') }}</p>
|
<p class="evidence-card__description">{{ $t('chat.evidenceDescription') }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
feedback_event card: recovery affordances for turns that ended
|
||||||
|
in a non-transient error. Backend's NodeStreamingChatHelper
|
||||||
|
handles transient TLS / IO retries silently; this card only
|
||||||
|
appears for the residue (auth, billing, model-not-found, raw
|
||||||
|
parse failures, etc.) that no amount of retry can fix without
|
||||||
|
user input. Buttons are data-driven from the event's `actions`
|
||||||
|
array so the backend can narrow the offering per error type
|
||||||
|
without a frontend release.
|
||||||
|
-->
|
||||||
|
<div v-if="feedbackInfo" class="feedback-card">
|
||||||
|
<div class="feedback-card__header">
|
||||||
|
<el-icon class="feedback-card__icon"><WarningFilled /></el-icon>
|
||||||
|
<span class="feedback-card__title">{{ $t('chat.feedback.title') }}</span>
|
||||||
|
</div>
|
||||||
|
<p class="feedback-card__description">{{ $t('chat.feedback.description') }}</p>
|
||||||
|
<div class="feedback-card__actions">
|
||||||
|
<button
|
||||||
|
v-for="action in feedbackInfo.actions"
|
||||||
|
:key="action"
|
||||||
|
class="feedback-card__btn"
|
||||||
|
:class="`feedback-card__btn--${action}`"
|
||||||
|
type="button"
|
||||||
|
@click="handleFeedbackAction(action)"
|
||||||
|
>
|
||||||
|
<el-icon v-if="action === 'retry' || action === 'regenerate'"><RefreshRight /></el-icon>
|
||||||
|
{{ feedbackActionLabel(action) }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 附件列表 -->
|
<!-- 附件列表 -->
|
||||||
<div v-if="attachments?.length" class="message-attachments">
|
<div v-if="attachments?.length" class="message-attachments">
|
||||||
<div
|
<div
|
||||||
@ -359,6 +390,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch, onBeforeUnmount } from 'vue'
|
import { computed, ref, watch, onBeforeUnmount } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
import {
|
import {
|
||||||
ArrowDown,
|
ArrowDown,
|
||||||
CloseBold,
|
CloseBold,
|
||||||
@ -927,6 +959,66 @@ const isEvidenceInsufficient = computed<boolean>(() => {
|
|||||||
return parsedMetadata.value?.finishReason === 'evidence_insufficient'
|
return parsedMetadata.value?.finishReason === 'evidence_insufficient'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recovery-affordance payload from the graph's feedback_event. Populated
|
||||||
|
* for assistant turns that ended in a non-transient error (after the
|
||||||
|
* helper's TLS / IO retry loop has already given up). Shape mirrors
|
||||||
|
* GraphEventPublisher.feedback: { errorType, errorMessage, actions }.
|
||||||
|
*
|
||||||
|
* <p>Surfaces a card with buttons for each action: "retry" and
|
||||||
|
* "regenerate" both replay the last user message; "report" copies the
|
||||||
|
* error details for a bug report. The card sits right under the red
|
||||||
|
* "[错误] …" content so users see the recovery options inline rather
|
||||||
|
* than having to retype the whole prompt.
|
||||||
|
*/
|
||||||
|
interface FeedbackInfo {
|
||||||
|
errorType: string
|
||||||
|
errorMessage: string
|
||||||
|
actions: string[]
|
||||||
|
timestamp?: number
|
||||||
|
}
|
||||||
|
const feedbackInfo = computed<FeedbackInfo | undefined>(() => {
|
||||||
|
if (props.message.role !== 'assistant') return undefined
|
||||||
|
const raw = parsedMetadata.value?.feedbackEvent as FeedbackInfo | undefined
|
||||||
|
if (!raw || !Array.isArray(raw.actions) || raw.actions.length === 0) return undefined
|
||||||
|
return raw
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleFeedbackAction(action: string) {
|
||||||
|
if (action === 'retry' || action === 'regenerate') {
|
||||||
|
emit('regenerate')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (action === 'report') {
|
||||||
|
// Copy error details for a bug report. Lower-friction than a modal
|
||||||
|
// and works offline; users paste the result into wherever they file
|
||||||
|
// issues. Uses the clipboard helper with execCommand fallback for
|
||||||
|
// non-HTTPS contexts (e.g. internal IPs without TLS).
|
||||||
|
const lines = [
|
||||||
|
`Error type: ${feedbackInfo.value?.errorType || 'UNKNOWN'}`,
|
||||||
|
`Message: ${feedbackInfo.value?.errorMessage || ''}`,
|
||||||
|
`Conversation: ${(props.message as any).conversationId || ''}`,
|
||||||
|
`Message id: ${(props.message as any).id || ''}`,
|
||||||
|
`Timestamp: ${new Date(feedbackInfo.value?.timestamp || Date.now()).toISOString()}`,
|
||||||
|
].join('\n')
|
||||||
|
copyToClipboard(lines).then(() => {
|
||||||
|
ElMessage.success(t('chat.feedback.reportCopied'))
|
||||||
|
}).catch(() => {
|
||||||
|
console.error('[feedback_event] copy failed:\n' + lines)
|
||||||
|
ElMessage.error(t('chat.feedback.reportFailed'))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function feedbackActionLabel(action: string): string {
|
||||||
|
// Action labels go through i18n so the same data-driven button list
|
||||||
|
// renders correctly in zh-CN / en-US. Falls back to the raw action
|
||||||
|
// key if a future backend introduces a label we haven't translated.
|
||||||
|
const key = `chat.feedback.${action}`
|
||||||
|
const localized = t(key)
|
||||||
|
return localized === key ? action : localized
|
||||||
|
}
|
||||||
|
|
||||||
const browserActionsMeta = computed<BrowserAction[]>(() => {
|
const browserActionsMeta = computed<BrowserAction[]>(() => {
|
||||||
return parsedMetadata.value?.browserActions || []
|
return parsedMetadata.value?.browserActions || []
|
||||||
})
|
})
|
||||||
@ -1741,6 +1833,84 @@ watch(isGenerating, (generating) => {
|
|||||||
opacity: 0.85;
|
opacity: 0.85;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==================== feedback_event recovery card (ERROR_FALLBACK) ==================== */
|
||||||
|
.feedback-card {
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: color-mix(in srgb, var(--mc-danger, #dc2626) 8%, var(--mc-bg-elevated));
|
||||||
|
border: 1px solid color-mix(in srgb, var(--mc-danger, #dc2626) 30%, transparent);
|
||||||
|
font-size: 13px;
|
||||||
|
max-width: 480px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-card__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-card__icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--mc-danger, #dc2626);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-card__title {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--mc-danger, #dc2626);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-card__description {
|
||||||
|
margin: 4px 0 8px;
|
||||||
|
color: var(--mc-text-primary);
|
||||||
|
font-size: 13px;
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-card__actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 6px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-card__btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid color-mix(in srgb, var(--mc-danger, #dc2626) 35%, transparent);
|
||||||
|
background: color-mix(in srgb, var(--mc-danger, #dc2626) 10%, var(--mc-bg-elevated));
|
||||||
|
color: var(--mc-danger, #dc2626);
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-card__btn:hover {
|
||||||
|
background: color-mix(in srgb, var(--mc-danger, #dc2626) 18%, var(--mc-bg-elevated));
|
||||||
|
border-color: color-mix(in srgb, var(--mc-danger, #dc2626) 55%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Report button is secondary action — muted neutral palette so the
|
||||||
|
primary "retry" stays visually emphasized. */
|
||||||
|
.feedback-card__btn--report {
|
||||||
|
border-color: var(--mc-border);
|
||||||
|
background: var(--mc-bg-elevated);
|
||||||
|
color: var(--mc-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-card__btn--report:hover {
|
||||||
|
background: var(--mc-bg-sunken);
|
||||||
|
border-color: var(--mc-border-strong, var(--mc-border));
|
||||||
|
color: var(--mc-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
/* ==================== 附件 ==================== */
|
/* ==================== 附件 ==================== */
|
||||||
.message-attachments {
|
.message-attachments {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@ -729,6 +729,33 @@ export function useChat(options: UseChatOptions): UseChatReturn {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Live recovery affordance event. Emitted by the graph when a turn
|
||||||
|
// ends in a non-transient error (ERROR_FALLBACK). Carries
|
||||||
|
// { errorType, errorMessage, actions } — actions is the ordered list
|
||||||
|
// of buttons the failed-bubble card should render. Persisting onto
|
||||||
|
// metadata.feedbackEvent matches the post-reload code path in
|
||||||
|
// MessageBubble (which reads the same metadata key) so the card
|
||||||
|
// appears immediately during the live stream AND survives a refresh.
|
||||||
|
stream.on('feedback_event', (data) => {
|
||||||
|
if (isStaleEvent(data)) return
|
||||||
|
if (!currentAssistantId.value) return
|
||||||
|
const msg = getMessage(currentAssistantId.value)
|
||||||
|
if (!msg) return
|
||||||
|
const metadata = parseMetadata((msg as any).metadata)
|
||||||
|
updateMessage(currentAssistantId.value, {
|
||||||
|
...msg,
|
||||||
|
metadata: {
|
||||||
|
...metadata,
|
||||||
|
feedbackEvent: {
|
||||||
|
errorType: data.errorType,
|
||||||
|
errorMessage: data.errorMessage,
|
||||||
|
actions: data.actions,
|
||||||
|
timestamp: data.timestamp || Date.now(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as any)
|
||||||
|
})
|
||||||
|
|
||||||
stream.on('phase', (data) => {
|
stream.on('phase', (data) => {
|
||||||
if (isStaleEvent(data)) return
|
if (isStaleEvent(data)) return
|
||||||
const phase = data.phase as StreamPhase
|
const phase = data.phase as StreamPhase
|
||||||
|
|||||||
@ -58,6 +58,8 @@ export type SSEEventType =
|
|||||||
| 'content_truncated'
|
| 'content_truncated'
|
||||||
| 'tool_result_chunk'
|
| 'tool_result_chunk'
|
||||||
| 'delegation_batch'
|
| 'delegation_batch'
|
||||||
|
// Recovery affordance for non-transient errors (ERROR_FALLBACK turns)
|
||||||
|
| 'feedback_event'
|
||||||
|
|
||||||
export interface SSEEvent {
|
export interface SSEEvent {
|
||||||
type: SSEEventType
|
type: SSEEventType
|
||||||
|
|||||||
@ -328,6 +328,19 @@ export default {
|
|||||||
// EVIDENCE_INSUFFICIENT info card (finishReason=evidence_insufficient)
|
// EVIDENCE_INSUFFICIENT info card (finishReason=evidence_insufficient)
|
||||||
evidenceTitle: 'Run completed — some source references could not be verified',
|
evidenceTitle: 'Run completed — some source references could not be verified',
|
||||||
evidenceDescription: 'The full answer is preserved above. The classes/files listed in the trailing "[evidence insufficient] …" line were not actually opened during this run; the model may have inferred them from naming. Ask a follow-up to have each one read before relying on those references.',
|
evidenceDescription: 'The full answer is preserved above. The classes/files listed in the trailing "[evidence insufficient] …" line were not actually opened during this run; the model may have inferred them from naming. Ask a follow-up to have each one read before relying on those references.',
|
||||||
|
// feedback_event card — emitted when a turn ends in a non-transient error.
|
||||||
|
// Buttons are data-driven from the event's `actions` array; labels live
|
||||||
|
// under chat.feedback.{action} so a future backend can ship a new action
|
||||||
|
// (e.g. "switch_model") with a single new translation key.
|
||||||
|
feedback: {
|
||||||
|
title: 'This turn failed',
|
||||||
|
description: 'The model call did not finish normally; the error reason is shown above. Click Retry to replay the same prompt, or Report to copy the error details for a bug report.',
|
||||||
|
retry: 'Retry',
|
||||||
|
regenerate: 'Regenerate',
|
||||||
|
report: 'Report',
|
||||||
|
reportCopied: 'Error details copied to clipboard',
|
||||||
|
reportFailed: 'Copy failed — check browser permissions',
|
||||||
|
},
|
||||||
// Approval bar
|
// Approval bar
|
||||||
approvalAllow: 'Allow',
|
approvalAllow: 'Allow',
|
||||||
approvalExecute: 'to execute?',
|
approvalExecute: 'to execute?',
|
||||||
|
|||||||
@ -328,6 +328,19 @@ export default {
|
|||||||
// EVIDENCE_INSUFFICIENT 提示卡(finishReason=evidence_insufficient)
|
// EVIDENCE_INSUFFICIENT 提示卡(finishReason=evidence_insufficient)
|
||||||
evidenceTitle: '任务已完成,但部分源码引用未被验证',
|
evidenceTitle: '任务已完成,但部分源码引用未被验证',
|
||||||
evidenceDescription: '回答全文已保留并展示。底部「[证据不足] …」列出的类/文件并未在本次工具结果里被实际读取,模型可能基于命名推断。如需确认这些引用,请追问让模型逐一读取后再下结论。',
|
evidenceDescription: '回答全文已保留并展示。底部「[证据不足] …」列出的类/文件并未在本次工具结果里被实际读取,模型可能基于命名推断。如需确认这些引用,请追问让模型逐一读取后再下结论。',
|
||||||
|
// feedback_event card — emitted when a turn ends in a non-transient error.
|
||||||
|
// Buttons are data-driven from the event's `actions` array; labels live
|
||||||
|
// under chat.feedback.{action} so a future backend can ship a new action
|
||||||
|
// (e.g. "switch_model") with a single new translation key.
|
||||||
|
feedback: {
|
||||||
|
title: '本次回答失败',
|
||||||
|
description: '模型调用未能正常完成,已显示错误原因。可点击「重试」让模型用同一条提问再试一次,或「上报问题」复制错误详情用于反馈。',
|
||||||
|
retry: '重试',
|
||||||
|
regenerate: '重新生成',
|
||||||
|
report: '上报问题',
|
||||||
|
reportCopied: '错误详情已复制到剪贴板',
|
||||||
|
reportFailed: '复制失败,请检查浏览器权限',
|
||||||
|
},
|
||||||
// 审批栏
|
// 审批栏
|
||||||
approvalAllow: '允许',
|
approvalAllow: '允许',
|
||||||
approvalExecute: '执行?',
|
approvalExecute: '执行?',
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user