fix: intent streaming defaults and UI stability
This commit is contained in:
parent
713659a644
commit
4262922694
@ -35,6 +35,7 @@ DEFAULT_PERSONALIZATION_CONFIG: Dict[str, Any] = {
|
|||||||
"disabled_tool_categories": [],
|
"disabled_tool_categories": [],
|
||||||
"default_run_mode": None,
|
"default_run_mode": None,
|
||||||
"auto_generate_title": True,
|
"auto_generate_title": True,
|
||||||
|
"tool_intent_enabled": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@ -124,6 +125,12 @@ def sanitize_personalization_payload(
|
|||||||
else:
|
else:
|
||||||
base["thinking_interval"] = _sanitize_thinking_interval(base.get("thinking_interval"))
|
base["thinking_interval"] = _sanitize_thinking_interval(base.get("thinking_interval"))
|
||||||
|
|
||||||
|
# 工具意图提示开关
|
||||||
|
if "tool_intent_enabled" in data:
|
||||||
|
base["tool_intent_enabled"] = bool(data.get("tool_intent_enabled"))
|
||||||
|
else:
|
||||||
|
base["tool_intent_enabled"] = bool(base.get("tool_intent_enabled"))
|
||||||
|
|
||||||
if "disabled_tool_categories" in data:
|
if "disabled_tool_categories" in data:
|
||||||
base["disabled_tool_categories"] = _sanitize_tool_categories(data.get("disabled_tool_categories"), allowed_tool_categories)
|
base["disabled_tool_categories"] = _sanitize_tool_categories(data.get("disabled_tool_categories"), allowed_tool_categories)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@ -45,7 +45,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
getToolIcon,
|
getToolIcon,
|
||||||
getToolAnimationClass,
|
getToolAnimationClass,
|
||||||
getToolStatusText,
|
getToolStatusText as baseGetToolStatusText,
|
||||||
getToolDescription,
|
getToolDescription,
|
||||||
cloneToolArguments,
|
cloneToolArguments,
|
||||||
buildToolLabel,
|
buildToolLabel,
|
||||||
@ -1653,6 +1653,8 @@ const appOptions = {
|
|||||||
id: toolCall.id,
|
id: toolCall.id,
|
||||||
name: toolCall.function.name,
|
name: toolCall.function.name,
|
||||||
arguments: arguments_obj,
|
arguments: arguments_obj,
|
||||||
|
intent_full: arguments_obj.intent || '',
|
||||||
|
intent_rendered: arguments_obj.intent || '',
|
||||||
status: 'preparing',
|
status: 'preparing',
|
||||||
result: null
|
result: null
|
||||||
},
|
},
|
||||||
@ -2585,7 +2587,14 @@ const appOptions = {
|
|||||||
},
|
},
|
||||||
getToolIcon,
|
getToolIcon,
|
||||||
getToolAnimationClass,
|
getToolAnimationClass,
|
||||||
getToolStatusText,
|
getToolStatusText(tool: any) {
|
||||||
|
const personalization = usePersonalizationStore();
|
||||||
|
const intentEnabled =
|
||||||
|
personalization?.form?.tool_intent_enabled ??
|
||||||
|
personalization?.tool_intent_enabled ??
|
||||||
|
true;
|
||||||
|
return baseGetToolStatusText(tool, { intentEnabled });
|
||||||
|
},
|
||||||
getToolDescription,
|
getToolDescription,
|
||||||
cloneToolArguments,
|
cloneToolArguments,
|
||||||
buildToolLabel,
|
buildToolLabel,
|
||||||
|
|||||||
@ -22,7 +22,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="stacked-viewport" :style="{ height: `${viewportHeight}px` }">
|
<div class="stacked-viewport" :style="{ height: `${viewportHeight}px` }">
|
||||||
<div class="stacked-inner" ref="inner" :style="{ transform: `translateY(${innerOffset}px)` }">
|
<TransitionGroup
|
||||||
|
name="stacked"
|
||||||
|
tag="div"
|
||||||
|
class="stacked-inner"
|
||||||
|
ref="inner"
|
||||||
|
:style="{ transform: `translateY(${innerOffset}px)` }"
|
||||||
|
appear
|
||||||
|
>
|
||||||
<div v-for="(action, idx) in stackableActions" :key="blockKey(action, idx)" class="stacked-item">
|
<div v-for="(action, idx) in stackableActions" :key="blockKey(action, idx)" class="stacked-item">
|
||||||
<div
|
<div
|
||||||
v-if="action.type === 'thinking'"
|
v-if="action.type === 'thinking'"
|
||||||
@ -109,7 +116,7 @@
|
|||||||
<div v-if="isToolProcessing(action)" class="progress-indicator"></div>
|
<div v-if="isToolProcessing(action)" class="progress-indicator"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</TransitionGroup>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -235,7 +242,8 @@ const setShellMetrics = (expandedOverride: Record<string, boolean> = {}) => {
|
|||||||
const contentHeight = content ? Math.ceil(content.scrollHeight) : 0;
|
const contentHeight = content ? Math.ceil(content.scrollHeight) : 0;
|
||||||
nextContentHeights[key] = contentHeight;
|
nextContentHeights[key] = contentHeight;
|
||||||
const contentH = expanded ? contentHeight : 0;
|
const contentH = expanded ? contentHeight : 0;
|
||||||
const progressH = progress ? progress.getBoundingClientRect().height : 0;
|
// 进度条是绝对定位的,不应计入布局高度,否则会导致块高度在运行时膨胀几像素
|
||||||
|
const progressH = 0;
|
||||||
const borderH = idx < children.length - 1 ? parseFloat(getComputedStyle(el).borderBottomWidth) || 0 : 0;
|
const borderH = idx < children.length - 1 ? parseFloat(getComputedStyle(el).borderBottomWidth) || 0 : 0;
|
||||||
heights.push(Math.ceil(headerH + contentH + progressH + borderH));
|
heights.push(Math.ceil(headerH + contentH + progressH + borderH));
|
||||||
});
|
});
|
||||||
|
|||||||
@ -39,6 +39,75 @@ export async function initializeLegacySocket(ctx: any) {
|
|||||||
|
|
||||||
const pendingToolEvents: Array<{ event: string; data: any; handler: () => void }> = [];
|
const pendingToolEvents: Array<{ event: string; data: any; handler: () => void }> = [];
|
||||||
|
|
||||||
|
const startIntentTyping = (action: any, intentText: any) => {
|
||||||
|
if (!action || !action.tool) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof intentText !== 'string') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const text = intentText.trim();
|
||||||
|
if (!text) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (action.tool.intent_full === text) {
|
||||||
|
if (console && console.debug) {
|
||||||
|
console.debug('[tool_intent] skip (same text)', {
|
||||||
|
id: action?.id || action?.tool?.id,
|
||||||
|
name: action?.tool?.name,
|
||||||
|
text
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof console !== 'undefined' && console.debug) {
|
||||||
|
console.debug('[tool_intent] typing start', {
|
||||||
|
id: action?.id || action?.tool?.id,
|
||||||
|
name: action?.tool?.name,
|
||||||
|
text
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (action.tool.intent_full === text && action.tool.intent_rendered === text) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 清理旧定时器
|
||||||
|
if (action.tool.intent_typing_timer) {
|
||||||
|
clearInterval(action.tool.intent_typing_timer);
|
||||||
|
action.tool.intent_typing_timer = null;
|
||||||
|
}
|
||||||
|
action.tool.intent_full = text;
|
||||||
|
action.tool.intent_rendered = '';
|
||||||
|
const duration = 1000;
|
||||||
|
const step = Math.max(10, Math.floor(duration / Math.max(1, text.length)));
|
||||||
|
let index = 0;
|
||||||
|
action.tool.intent_typing_timer = window.setInterval(() => {
|
||||||
|
action.tool.intent_rendered = text.slice(0, index + 1);
|
||||||
|
if (index === 0 && console && console.debug) {
|
||||||
|
console.debug('[tool_intent] typing tick', {
|
||||||
|
id: action?.id || action?.tool?.id,
|
||||||
|
name: action?.tool?.name,
|
||||||
|
next: action.tool.intent_rendered
|
||||||
|
});
|
||||||
|
}
|
||||||
|
index += 1;
|
||||||
|
if (index >= text.length) {
|
||||||
|
clearInterval(action.tool.intent_typing_timer);
|
||||||
|
action.tool.intent_typing_timer = null;
|
||||||
|
action.tool.intent_rendered = text;
|
||||||
|
if (console && console.debug) {
|
||||||
|
console.debug('[tool_intent] typing done', {
|
||||||
|
id: action?.id || action?.tool?.id,
|
||||||
|
name: action?.tool?.name,
|
||||||
|
full: text
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (typeof ctx?.$forceUpdate === 'function') {
|
||||||
|
ctx.$forceUpdate();
|
||||||
|
}
|
||||||
|
}, step);
|
||||||
|
};
|
||||||
|
|
||||||
const hasPendingStreamingText = () =>
|
const hasPendingStreamingText = () =>
|
||||||
!!streamingState.buffer.length ||
|
!!streamingState.buffer.length ||
|
||||||
!!streamingState.pendingCompleteContent ||
|
!!streamingState.pendingCompleteContent ||
|
||||||
@ -845,6 +914,7 @@ export async function initializeLegacySocket(ctx: any) {
|
|||||||
scheduleStreamingFlush();
|
scheduleStreamingFlush();
|
||||||
}
|
}
|
||||||
ctx.monitorEndModelOutput();
|
ctx.monitorEndModelOutput();
|
||||||
|
flushPendingToolEvents();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 工具提示事件(可选)
|
// 工具提示事件(可选)
|
||||||
@ -863,11 +933,18 @@ export async function initializeLegacySocket(ctx: any) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
socketLog('工具准备中:', data.name);
|
socketLog('工具准备中:', data.name);
|
||||||
|
if (typeof console !== 'undefined' && console.debug) {
|
||||||
|
console.debug('[tool_intent] preparing', {
|
||||||
|
id: data?.id,
|
||||||
|
name: data?.name,
|
||||||
|
intent: data?.intent,
|
||||||
|
convo: data?.conversation_id
|
||||||
|
});
|
||||||
|
}
|
||||||
if (data?.conversation_id && data.conversation_id !== ctx.currentConversationId) {
|
if (data?.conversation_id && data.conversation_id !== ctx.currentConversationId) {
|
||||||
socketLog('跳过tool_preparing(对话不匹配)', data.conversation_id);
|
socketLog('跳过tool_preparing(对话不匹配)', data.conversation_id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const handler = () => {
|
|
||||||
const msg = ctx.chatEnsureAssistantMessage();
|
const msg = ctx.chatEnsureAssistantMessage();
|
||||||
if (!msg) {
|
if (!msg) {
|
||||||
return;
|
return;
|
||||||
@ -883,7 +960,9 @@ export async function initializeLegacySocket(ctx: any) {
|
|||||||
argumentLabel: '',
|
argumentLabel: '',
|
||||||
status: 'preparing',
|
status: 'preparing',
|
||||||
result: null,
|
result: null,
|
||||||
message: data.message || `准备调用 ${data.name}...`
|
message: data.message || `准备调用 ${data.name}...`,
|
||||||
|
intent_full: data.intent || '',
|
||||||
|
intent_rendered: data.intent || ''
|
||||||
},
|
},
|
||||||
timestamp: Date.now()
|
timestamp: Date.now()
|
||||||
};
|
};
|
||||||
@ -891,18 +970,39 @@ export async function initializeLegacySocket(ctx: any) {
|
|||||||
ctx.preparingTools.set(data.id, action);
|
ctx.preparingTools.set(data.id, action);
|
||||||
ctx.toolRegisterAction(action, data.id);
|
ctx.toolRegisterAction(action, data.id);
|
||||||
ctx.toolTrackAction(data.name, action);
|
ctx.toolTrackAction(data.name, action);
|
||||||
|
if (data.intent) {
|
||||||
|
startIntentTyping(action, data.intent);
|
||||||
|
}
|
||||||
ctx.$forceUpdate();
|
ctx.$forceUpdate();
|
||||||
ctx.conditionalScrollToBottom();
|
ctx.conditionalScrollToBottom();
|
||||||
if (ctx.monitorPreviewTool) {
|
if (ctx.monitorPreviewTool) {
|
||||||
ctx.monitorPreviewTool(data);
|
ctx.monitorPreviewTool(data);
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
if (hasPendingStreamingText()) {
|
// 工具意图(流式增量)事件
|
||||||
pendingToolEvents.push({ event: 'tool_preparing', data, handler });
|
ctx.socket.on('tool_intent', (data) => {
|
||||||
} else {
|
if (ctx.dropToolEvents) {
|
||||||
handler();
|
return;
|
||||||
}
|
}
|
||||||
|
if (data?.conversation_id && data.conversation_id !== ctx.currentConversationId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof console !== 'undefined' && console.debug) {
|
||||||
|
console.debug('[tool_intent] update', {
|
||||||
|
id: data?.id,
|
||||||
|
name: data?.name,
|
||||||
|
intent: data?.intent,
|
||||||
|
convo: data?.conversation_id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const target =
|
||||||
|
ctx.toolFindAction(data.id, data.preparing_id, data.execution_id) ||
|
||||||
|
ctx.toolGetLatestAction(data.name);
|
||||||
|
if (target) {
|
||||||
|
startIntentTyping(target, data.intent);
|
||||||
|
}
|
||||||
|
ctx.$forceUpdate();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 工具状态更新事件 - 实时显示详细状态
|
// 工具状态更新事件 - 实时显示详细状态
|
||||||
@ -919,6 +1019,9 @@ export async function initializeLegacySocket(ctx: any) {
|
|||||||
if (target) {
|
if (target) {
|
||||||
target.tool.statusDetail = data.detail;
|
target.tool.statusDetail = data.detail;
|
||||||
target.tool.statusType = data.status;
|
target.tool.statusType = data.status;
|
||||||
|
if (data.intent) {
|
||||||
|
startIntentTyping(target, data.intent);
|
||||||
|
}
|
||||||
ctx.$forceUpdate();
|
ctx.$forceUpdate();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -927,6 +1030,9 @@ export async function initializeLegacySocket(ctx: any) {
|
|||||||
fallbackAction.tool.statusDetail = data.detail;
|
fallbackAction.tool.statusDetail = data.detail;
|
||||||
fallbackAction.tool.statusType = data.status;
|
fallbackAction.tool.statusType = data.status;
|
||||||
ctx.toolRegisterAction(fallbackAction, data.execution_id || data.id || data.preparing_id);
|
ctx.toolRegisterAction(fallbackAction, data.execution_id || data.id || data.preparing_id);
|
||||||
|
if (data.intent) {
|
||||||
|
startIntentTyping(fallbackAction, data.intent);
|
||||||
|
}
|
||||||
ctx.$forceUpdate();
|
ctx.$forceUpdate();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -977,6 +1083,9 @@ export async function initializeLegacySocket(ctx: any) {
|
|||||||
action.tool.message = null;
|
action.tool.message = null;
|
||||||
action.tool.id = data.id;
|
action.tool.id = data.id;
|
||||||
action.tool.executionId = data.id;
|
action.tool.executionId = data.id;
|
||||||
|
if (data.arguments && data.arguments.intent) {
|
||||||
|
startIntentTyping(action, data.arguments.intent);
|
||||||
|
}
|
||||||
ctx.toolRegisterAction(action, data.id);
|
ctx.toolRegisterAction(action, data.id);
|
||||||
ctx.toolTrackAction(data.name, action);
|
ctx.toolTrackAction(data.name, action);
|
||||||
ctx.$forceUpdate();
|
ctx.$forceUpdate();
|
||||||
@ -984,11 +1093,7 @@ export async function initializeLegacySocket(ctx: any) {
|
|||||||
ctx.monitorQueueTool(data);
|
ctx.monitorQueueTool(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (hasPendingStreamingText()) {
|
|
||||||
pendingToolEvents.push({ event: 'tool_start', data, handler });
|
|
||||||
} else {
|
|
||||||
handler();
|
handler();
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 更新action(工具完成)
|
// 更新action(工具完成)
|
||||||
@ -1039,6 +1144,9 @@ export async function initializeLegacySocket(ctx: any) {
|
|||||||
if (data.message !== undefined) {
|
if (data.message !== undefined) {
|
||||||
targetAction.tool.message = data.message;
|
targetAction.tool.message = data.message;
|
||||||
}
|
}
|
||||||
|
if (data.arguments && data.arguments.intent) {
|
||||||
|
startIntentTyping(targetAction, data.arguments.intent);
|
||||||
|
}
|
||||||
if (data.awaiting_content) {
|
if (data.awaiting_content) {
|
||||||
targetAction.tool.awaiting_content = true;
|
targetAction.tool.awaiting_content = true;
|
||||||
} else if (data.status === 'completed') {
|
} else if (data.status === 'completed') {
|
||||||
@ -1051,6 +1159,9 @@ export async function initializeLegacySocket(ctx: any) {
|
|||||||
targetAction.tool.arguments = data.arguments;
|
targetAction.tool.arguments = data.arguments;
|
||||||
targetAction.tool.argumentSnapshot = ctx.cloneToolArguments(data.arguments);
|
targetAction.tool.argumentSnapshot = ctx.cloneToolArguments(data.arguments);
|
||||||
targetAction.tool.argumentLabel = ctx.buildToolLabel(targetAction.tool.argumentSnapshot);
|
targetAction.tool.argumentLabel = ctx.buildToolLabel(targetAction.tool.argumentSnapshot);
|
||||||
|
if (data.arguments.intent) {
|
||||||
|
startIntentTyping(targetAction, data.arguments.intent);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ctx.toolRegisterAction(targetAction, data.execution_id || data.id);
|
ctx.toolRegisterAction(targetAction, data.execution_id || data.id);
|
||||||
if (data.status && ["completed", "failed", "error"].includes(data.status) && !data.awaiting_content) {
|
if (data.status && ["completed", "failed", "error"].includes(data.status) && !data.awaiting_content) {
|
||||||
@ -1079,11 +1190,7 @@ export async function initializeLegacySocket(ctx: any) {
|
|||||||
ctx.monitorResolveTool(data);
|
ctx.monitorResolveTool(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (hasPendingStreamingText()) {
|
|
||||||
pendingToolEvents.push({ event: 'update_action', data, handler });
|
|
||||||
} else {
|
|
||||||
handler();
|
handler();
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ctx.socket.on('append_payload', (data) => {
|
ctx.socket.on('append_payload', (data) => {
|
||||||
|
|||||||
@ -11,21 +11,38 @@ type ScrollContext = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function scrollToBottom(ctx: ScrollContext) {
|
export function scrollToBottom(ctx: ScrollContext) {
|
||||||
setTimeout(() => {
|
|
||||||
const messagesArea = ctx.getMessagesAreaElement?.();
|
const messagesArea = ctx.getMessagesAreaElement?.();
|
||||||
if (!messagesArea) {
|
if (!messagesArea) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (typeof ctx._setScrollingFlag === 'function') {
|
|
||||||
ctx._setScrollingFlag(true);
|
const attempts = [0, 16, 60, 150, 320, 520]; // 多次尝试覆盖布局抖动/异步伸缩
|
||||||
}
|
let cancelled = false;
|
||||||
messagesArea.scrollTop = messagesArea.scrollHeight;
|
|
||||||
setTimeout(() => {
|
const perform = (idx: number) => {
|
||||||
|
if (cancelled) return;
|
||||||
|
if (ctx.userScrolling) {
|
||||||
|
cancelled = true;
|
||||||
if (typeof ctx._setScrollingFlag === 'function') {
|
if (typeof ctx._setScrollingFlag === 'function') {
|
||||||
ctx._setScrollingFlag(false);
|
ctx._setScrollingFlag(false);
|
||||||
}
|
}
|
||||||
}, 100);
|
return;
|
||||||
}, 50);
|
}
|
||||||
|
|
||||||
|
if (idx === 0 && typeof ctx._setScrollingFlag === 'function') {
|
||||||
|
ctx._setScrollingFlag(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
messagesArea.scrollTop = messagesArea.scrollHeight;
|
||||||
|
|
||||||
|
if (idx < attempts.length - 1) {
|
||||||
|
setTimeout(() => perform(idx + 1), attempts[idx + 1] - attempts[idx]);
|
||||||
|
} else if (typeof ctx._setScrollingFlag === 'function') {
|
||||||
|
setTimeout(() => ctx._setScrollingFlag && ctx._setScrollingFlag(false), 40);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
perform(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function conditionalScrollToBottom(ctx: ScrollContext) {
|
export function conditionalScrollToBottom(ctx: ScrollContext) {
|
||||||
|
|||||||
@ -5,6 +5,7 @@ type RunMode = 'fast' | 'thinking' | 'deep';
|
|||||||
interface PersonalForm {
|
interface PersonalForm {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
auto_generate_title: boolean;
|
auto_generate_title: boolean;
|
||||||
|
tool_intent_enabled: boolean;
|
||||||
self_identify: string;
|
self_identify: string;
|
||||||
user_name: string;
|
user_name: string;
|
||||||
profession: string;
|
profession: string;
|
||||||
@ -54,6 +55,7 @@ const EXPERIMENT_STORAGE_KEY = 'agents_personalization_experiments';
|
|||||||
const defaultForm = (): PersonalForm => ({
|
const defaultForm = (): PersonalForm => ({
|
||||||
enabled: false,
|
enabled: false,
|
||||||
auto_generate_title: true,
|
auto_generate_title: true,
|
||||||
|
tool_intent_enabled: true,
|
||||||
self_identify: '',
|
self_identify: '',
|
||||||
user_name: '',
|
user_name: '',
|
||||||
profession: '',
|
profession: '',
|
||||||
@ -176,6 +178,7 @@ export const usePersonalizationStore = defineStore('personalization', {
|
|||||||
this.form = {
|
this.form = {
|
||||||
enabled: !!data.enabled,
|
enabled: !!data.enabled,
|
||||||
auto_generate_title: data.auto_generate_title !== false,
|
auto_generate_title: data.auto_generate_title !== false,
|
||||||
|
tool_intent_enabled: !!data.tool_intent_enabled,
|
||||||
self_identify: data.self_identify || '',
|
self_identify: data.self_identify || '',
|
||||||
user_name: data.user_name || '',
|
user_name: data.user_name || '',
|
||||||
profession: data.profession || '',
|
profession: data.profession || '',
|
||||||
|
|||||||
@ -406,8 +406,10 @@
|
|||||||
transform: translateY(14px);
|
transform: translateY(14px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stacked-enter-active {
|
.stacked-enter-active,
|
||||||
|
.stacked-appear-active {
|
||||||
transition: all 220ms cubic-bezier(0.4, 0, 0.2, 1);
|
transition: all 220ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
transition-delay: 120ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stacked-leave-active {
|
.stacked-leave-active {
|
||||||
@ -421,6 +423,11 @@
|
|||||||
transform: translateY(-12px);
|
transform: translateY(-12px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stacked-appear-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(14px);
|
||||||
|
}
|
||||||
|
|
||||||
.stacked-move {
|
.stacked-move {
|
||||||
transition: transform 220ms cubic-bezier(0.4, 0, 0.2, 1);
|
transition: transform 220ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
}
|
}
|
||||||
@ -440,6 +447,7 @@
|
|||||||
animation: progress-bar 2s ease-in-out infinite;
|
animation: progress-bar 2s ease-in-out infinite;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.3s ease;
|
transition: opacity 0.3s ease;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.collapsible-block.processing .progress-indicator {
|
.collapsible-block.processing .progress-indicator {
|
||||||
|
|||||||
@ -148,10 +148,28 @@ function describeReadFileResult(tool: any): string {
|
|||||||
return '文件读取完成';
|
return '文件读取完成';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getToolStatusText(tool: any): string {
|
export function getToolStatusText(tool: any, opts?: { intentEnabled?: boolean }): string {
|
||||||
if (!tool) {
|
if (!tool) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
const intentEnabled = opts?.intentEnabled !== false;
|
||||||
|
const intentText = tool.intent_rendered || tool.intent_full || '';
|
||||||
|
const hasIntent = !!intentText;
|
||||||
|
|
||||||
|
// 错误优先展示
|
||||||
|
if (
|
||||||
|
tool.message &&
|
||||||
|
(tool.status === 'failed' || tool.status === 'error' || (tool.result && tool.result.error))
|
||||||
|
) {
|
||||||
|
return tool.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开启时:有 intent 就只显示 intent
|
||||||
|
if (intentEnabled && hasIntent) {
|
||||||
|
return intentText;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭 intent 或无 intent 时,显示状态文案
|
||||||
if (tool.message) {
|
if (tool.message) {
|
||||||
return tool.message;
|
return tool.message;
|
||||||
}
|
}
|
||||||
@ -186,8 +204,11 @@ export function getToolStatusText(tool: any): string {
|
|||||||
}
|
}
|
||||||
return COMPLETED_STATUS_TEXTS[tool.name] || '执行完成';
|
return COMPLETED_STATUS_TEXTS[tool.name] || '执行完成';
|
||||||
}
|
}
|
||||||
|
if (tool.status) {
|
||||||
return `${tool.name} - ${tool.status}`;
|
return `${tool.name} - ${tool.status}`;
|
||||||
}
|
}
|
||||||
|
return tool.name || '';
|
||||||
|
}
|
||||||
|
|
||||||
export function getToolDescription(tool: any): string {
|
export function getToolDescription(tool: any): string {
|
||||||
if (!tool) {
|
if (!tool) {
|
||||||
|
|||||||
@ -2984,6 +2984,7 @@ async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspac
|
|||||||
last_tool_name = ""
|
last_tool_name = ""
|
||||||
auto_fix_attempts = 0
|
auto_fix_attempts = 0
|
||||||
last_tool_call_time = 0
|
last_tool_call_time = 0
|
||||||
|
detected_tool_intent: Dict[str, str] = {}
|
||||||
|
|
||||||
# 设置最大迭代次数
|
# 设置最大迭代次数
|
||||||
max_iterations = MAX_ITERATIONS_PER_TASK
|
max_iterations = MAX_ITERATIONS_PER_TASK
|
||||||
@ -2993,6 +2994,17 @@ async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspac
|
|||||||
pending_modify = None # {"path": str, "tool_call_id": str, "buffer": str, ...}
|
pending_modify = None # {"path": str, "tool_call_id": str, "buffer": str, ...}
|
||||||
modify_probe_buffer = ""
|
modify_probe_buffer = ""
|
||||||
|
|
||||||
|
def extract_intent_from_partial(arg_str: str) -> Optional[str]:
|
||||||
|
"""从不完整的JSON字符串中粗略提取 intent 字段,容错用于流式阶段。"""
|
||||||
|
if not arg_str or "intent" not in arg_str:
|
||||||
|
return None
|
||||||
|
import re
|
||||||
|
# 匹配 "intent": "xxx" 形式,允许前面有换行或空格;宽松匹配未闭合的引号
|
||||||
|
match = re.search(r'"intent"\s*:\s*"([^"]{0,128})', arg_str, re.IGNORECASE | re.DOTALL)
|
||||||
|
if match:
|
||||||
|
return match.group(1)
|
||||||
|
return None
|
||||||
|
|
||||||
def resolve_monitor_path(args: Dict[str, Any], fallback: Optional[str] = None) -> Optional[str]:
|
def resolve_monitor_path(args: Dict[str, Any], fallback: Optional[str] = None) -> Optional[str]:
|
||||||
candidates = [
|
candidates = [
|
||||||
args.get('path'),
|
args.get('path'),
|
||||||
@ -3987,24 +3999,62 @@ async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspac
|
|||||||
for existing in tool_calls:
|
for existing in tool_calls:
|
||||||
if existing.get("index") == tc.get("index"):
|
if existing.get("index") == tc.get("index"):
|
||||||
if "function" in tc and "arguments" in tc["function"]:
|
if "function" in tc and "arguments" in tc["function"]:
|
||||||
existing["function"]["arguments"] += tc["function"]["arguments"]
|
arg_chunk = tc["function"]["arguments"]
|
||||||
|
existing_fn = existing.get("function", {})
|
||||||
|
existing_args = existing_fn.get("arguments", "")
|
||||||
|
existing_fn["arguments"] = (existing_args or "") + arg_chunk
|
||||||
|
existing["function"] = existing_fn
|
||||||
|
|
||||||
|
combined_args = existing_fn.get("arguments", "")
|
||||||
|
tool_id = existing.get("id") or tc.get("id")
|
||||||
|
tool_name = (
|
||||||
|
existing_fn.get("name")
|
||||||
|
or tc.get("function", {}).get("name", "")
|
||||||
|
)
|
||||||
|
intent_value = extract_intent_from_partial(combined_args)
|
||||||
|
if (
|
||||||
|
intent_value
|
||||||
|
and tool_id
|
||||||
|
and detected_tool_intent.get(tool_id) != intent_value
|
||||||
|
):
|
||||||
|
detected_tool_intent[tool_id] = intent_value
|
||||||
|
brief_log(f"[intent] 增量提取 {tool_name}: {intent_value}")
|
||||||
|
sender('tool_intent', {
|
||||||
|
'id': tool_id,
|
||||||
|
'name': tool_name,
|
||||||
|
'intent': intent_value,
|
||||||
|
'conversation_id': conversation_id
|
||||||
|
})
|
||||||
|
debug_log(f" 发送工具意图: {tool_name} -> {intent_value}")
|
||||||
|
await asyncio.sleep(0.01)
|
||||||
found = True
|
found = True
|
||||||
break
|
break
|
||||||
|
|
||||||
if not found and tc.get("id"):
|
if not found and tc.get("id"):
|
||||||
tool_id = tc["id"]
|
tool_id = tc["id"]
|
||||||
tool_name = tc.get("function", {}).get("name", "")
|
tool_name = tc.get("function", {}).get("name", "")
|
||||||
|
arguments_str = tc.get("function", {}).get("arguments", "") or ""
|
||||||
|
|
||||||
# 新工具检测到,立即发送准备事件
|
# 新工具检测到,立即发送准备事件
|
||||||
if tool_id not in detected_tools and tool_name:
|
if tool_id not in detected_tools and tool_name:
|
||||||
detected_tools[tool_id] = tool_name
|
detected_tools[tool_id] = tool_name
|
||||||
|
|
||||||
|
# 尝试提前提取 intent
|
||||||
|
intent_value = None
|
||||||
|
if arguments_str:
|
||||||
|
intent_value = extract_intent_from_partial(arguments_str)
|
||||||
|
if intent_value:
|
||||||
|
detected_tool_intent[tool_id] = intent_value
|
||||||
|
brief_log(f"[intent] 预提取 {tool_name}: {intent_value}")
|
||||||
|
|
||||||
# 立即发送工具准备中事件
|
# 立即发送工具准备中事件
|
||||||
|
brief_log(f"[tool] 准备调用 {tool_name} (id={tool_id}) intent={intent_value or '-'}")
|
||||||
sender('tool_preparing', {
|
sender('tool_preparing', {
|
||||||
'id': tool_id,
|
'id': tool_id,
|
||||||
'name': tool_name,
|
'name': tool_name,
|
||||||
'message': f'准备调用 {tool_name}...'
|
'message': f'准备调用 {tool_name}...',
|
||||||
, 'conversation_id': conversation_id
|
'intent': intent_value,
|
||||||
|
'conversation_id': conversation_id
|
||||||
})
|
})
|
||||||
debug_log(f" 发送工具准备事件: {tool_name}")
|
debug_log(f" 发送工具准备事件: {tool_name}")
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
@ -4015,9 +4065,22 @@ async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspac
|
|||||||
"type": "function",
|
"type": "function",
|
||||||
"function": {
|
"function": {
|
||||||
"name": tool_name,
|
"name": tool_name,
|
||||||
"arguments": tc.get("function", {}).get("arguments", "")
|
"arguments": arguments_str
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
# 尝试从增量参数中抽取 intent,并单独推送
|
||||||
|
if tool_id and arguments_str:
|
||||||
|
intent_value = extract_intent_from_partial(arguments_str)
|
||||||
|
if intent_value and detected_tool_intent.get(tool_id) != intent_value:
|
||||||
|
detected_tool_intent[tool_id] = intent_value
|
||||||
|
sender('tool_intent', {
|
||||||
|
'id': tool_id,
|
||||||
|
'name': tool_name,
|
||||||
|
'intent': intent_value,
|
||||||
|
'conversation_id': conversation_id
|
||||||
|
})
|
||||||
|
debug_log(f" 发送工具意图: {tool_name} -> {intent_value}")
|
||||||
|
await asyncio.sleep(0.01)
|
||||||
debug_log(f" 新工具: {tool_name}")
|
debug_log(f" 新工具: {tool_name}")
|
||||||
|
|
||||||
# 检查是否被停止
|
# 检查是否被停止
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user