fix(frontend): 修复多个前端交互问题
- 修复运行时 intent Unicode 转义显示异常(后端 extract_intent_from_partial 解码) - 修复 ask_user 弹窗问题/说明不换行 - 修复刷新页面后等待回答提示不恢复(添加 fetchPendingUserQuestions 并在初始化路径调用) - 修复回答问题弹窗长问题撑满窗口(header/context 限高滚动) - 修复角色编辑器模型选择菜单定位与滚动 - 在 AGENTS.md 中再次强调前端调试日志必须统一筛选词 测试:/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/bin/python3.9 -m unittest test.test_server_refactor_smoke; npm run build
This commit is contained in:
parent
297a6cc5ec
commit
4e1d08f106
@ -207,7 +207,9 @@
|
|||||||
|
|
||||||
### 前端调试日志
|
### 前端调试日志
|
||||||
|
|
||||||
- **统一筛选词**:所有临时 `console.log` 必须使用**同一个**筛选前缀(例如 `[route-debug]`),方便用户在浏览器控制台用该前缀一次性筛选。
|
- **统一筛选词(强制)**:所有临时 `console.log` 必须使用**同一个**筛选前缀(例如 `[route-debug]`),方便用户在浏览器控制台用该前缀一次性筛选。
|
||||||
|
- **同一次调试任务中严禁使用多个前缀**。如果涉及多个模块(如状态栏、Git 摘要、用户问题),应共用同一个前缀(如 `[status-bar-debug]`),通过日志对象里的字段区分模块,而不是发明多个前缀增加用户筛选成本。
|
||||||
|
- 选择前缀时优先与本次排查的「用户可见现象」对齐,而不是与内部模块名对齐。
|
||||||
- **控制数量与时机**:
|
- **控制数量与时机**:
|
||||||
- 禁止一次性输出上百上千条日志刷屏。
|
- 禁止一次性输出上百上千条日志刷屏。
|
||||||
- 只在关键路径(入口、分支判断、状态变化、导航动作)打印,避免在循环、高频事件、每帧渲染中输出。
|
- 只在关键路径(入口、分支判断、状态变化、导航动作)打印,避免在循环、高频事件、每帧渲染中输出。
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
import re
|
import re
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
@ -10,7 +11,16 @@ def extract_intent_from_partial(arg_str: str) -> Optional[str]:
|
|||||||
return None
|
return None
|
||||||
match = re.search(r'"intent"\s*:\s*"([^"]{0,128})', arg_str, re.IGNORECASE | re.DOTALL)
|
match = re.search(r'"intent"\s*:\s*"([^"]{0,128})', arg_str, re.IGNORECASE | re.DOTALL)
|
||||||
if match:
|
if match:
|
||||||
return match.group(1)
|
raw = match.group(1)
|
||||||
|
try:
|
||||||
|
# 某些模型会对非 ASCII 字符输出 JSON Unicode 转义(如 \u4e2d\u6587),
|
||||||
|
# 这里用 json.loads 安全解码,失败时回退原始值。
|
||||||
|
decoded = json.loads('"{}"'.format(raw))
|
||||||
|
if isinstance(decoded, str):
|
||||||
|
return decoded
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return raw
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -41,7 +41,6 @@ export async function mounted() {
|
|||||||
|
|
||||||
// 立即加载初始数据(并行获取状态,优先同步运行模式)
|
// 立即加载初始数据(并行获取状态,优先同步运行模式)
|
||||||
const initialDataPromise = this.loadInitialData();
|
const initialDataPromise = this.loadInitialData();
|
||||||
this.refreshProjectGitSummary?.();
|
|
||||||
this.startProjectGitSummaryIdleRefresh?.();
|
this.startProjectGitSummaryIdleRefresh?.();
|
||||||
this.startConnectionHeartbeat();
|
this.startConnectionHeartbeat();
|
||||||
this.fetchTerminalCount();
|
this.fetchTerminalCount();
|
||||||
@ -50,6 +49,8 @@ export async function mounted() {
|
|||||||
if (initialDataPromise && typeof initialDataPromise.then === 'function') {
|
if (initialDataPromise && typeof initialDataPromise.then === 'function') {
|
||||||
initialDataPromise
|
initialDataPromise
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
// 初始数据加载完成后再刷新 git 摘要,确保 workspace/project_path 已就绪
|
||||||
|
this.refreshProjectGitSummary?.();
|
||||||
// 避免在初始加载阶段被覆盖,加载完成后再兜底检查一次
|
// 避免在初始加载阶段被覆盖,加载完成后再兜底检查一次
|
||||||
this.checkTutorialPrompt();
|
this.checkTutorialPrompt();
|
||||||
})
|
})
|
||||||
|
|||||||
@ -232,6 +232,8 @@ export const loadMethods = {
|
|||||||
this.fetchNetworkPermission();
|
this.fetchNetworkPermission();
|
||||||
await this.fetchVersioningStatus(conversationId, { silent: true });
|
await this.fetchVersioningStatus(conversationId, { silent: true });
|
||||||
this.fetchPendingToolApprovals();
|
this.fetchPendingToolApprovals();
|
||||||
|
this.fetchPendingUserQuestions();
|
||||||
|
this.refreshProjectGitSummary();
|
||||||
this.fetchConversationTokenStatistics();
|
this.fetchConversationTokenStatistics();
|
||||||
this.updateCurrentContextTokens();
|
this.updateCurrentContextTokens();
|
||||||
traceLog('loadConversation:after-history', {
|
traceLog('loadConversation:after-history', {
|
||||||
|
|||||||
@ -58,6 +58,35 @@ export const dialogMethods = {
|
|||||||
this.userQuestionDialogVisible = true;
|
this.userQuestionDialogVisible = true;
|
||||||
this.userQuestionMinimized = false;
|
this.userQuestionMinimized = false;
|
||||||
},
|
},
|
||||||
|
async fetchPendingUserQuestions() {
|
||||||
|
if (!this.currentConversationId) {
|
||||||
|
this.pendingUserQuestions = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/user-questions/pending?conversation_id=${encodeURIComponent(this.currentConversationId)}`
|
||||||
|
);
|
||||||
|
const payload = await response.json().catch(() => ({}));
|
||||||
|
if (!response.ok || !payload?.success) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const items = Array.isArray(payload.items) ? payload.items : [];
|
||||||
|
this.pendingUserQuestions = items;
|
||||||
|
if (items.length > 0) {
|
||||||
|
this.userQuestionActiveIndex = Math.min(
|
||||||
|
Math.max(0, Number(this.userQuestionActiveIndex || 0)),
|
||||||
|
Math.max(0, items.length - 1)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.userQuestionDialogVisible = false;
|
||||||
|
this.userQuestionMinimized = false;
|
||||||
|
this.userQuestionActiveIndex = 0;
|
||||||
|
}
|
||||||
|
} catch (_error) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
},
|
||||||
approveToolApproval(approvalId) {
|
approveToolApproval(approvalId) {
|
||||||
return this.decideToolApproval(approvalId, 'approved');
|
return this.decideToolApproval(approvalId, 'approved');
|
||||||
},
|
},
|
||||||
|
|||||||
@ -70,7 +70,7 @@ export const gitMethods = {
|
|||||||
if (this.gitChangesPanelOpen) {
|
if (this.gitChangesPanelOpen) {
|
||||||
this.loadGitChangesDiff?.();
|
this.loadGitChangesDiff?.();
|
||||||
}
|
}
|
||||||
} catch (_) {
|
} catch (_error) {
|
||||||
this.projectGitSummary = null;
|
this.projectGitSummary = null;
|
||||||
this.gitChangesDiff = null;
|
this.gitChangesDiff = null;
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@ -151,6 +151,9 @@ export const routeMethods = {
|
|||||||
this.initialRouteResolved = true;
|
this.initialRouteResolved = true;
|
||||||
}
|
}
|
||||||
await this.restoreComposerDraftState('bootstrap-route:conversation');
|
await this.restoreComposerDraftState('bootstrap-route:conversation');
|
||||||
|
if (this.currentConversationId) {
|
||||||
|
this.fetchPendingUserQuestions?.();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
handlePopState(event) {
|
handlePopState(event) {
|
||||||
const state = event.state || {};
|
const state = event.state || {};
|
||||||
|
|||||||
@ -372,6 +372,7 @@ export const socketMethods = {
|
|||||||
this.fetchConversationTokenStatistics();
|
this.fetchConversationTokenStatistics();
|
||||||
this.updateCurrentContextTokens();
|
this.updateCurrentContextTokens();
|
||||||
await this.fetchVersioningStatus(this.currentConversationId, { silent: true });
|
await this.fetchVersioningStatus(this.currentConversationId, { silent: true });
|
||||||
|
this.fetchPendingUserQuestions();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('获取当前对话标题失败:', e);
|
console.warn('获取当前对话标题失败:', e);
|
||||||
this.titleReady = true;
|
this.titleReady = true;
|
||||||
|
|||||||
@ -170,7 +170,7 @@
|
|||||||
>目标模式就绪</span
|
>目标模式就绪</span
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
v-if="userQuestionMinimized && pendingUserQuestionCount > 0"
|
v-if="pendingUserQuestionCount > 0"
|
||||||
type="button"
|
type="button"
|
||||||
class="floating-project-status__notice floating-project-status__notice--button"
|
class="floating-project-status__notice floating-project-status__notice--button"
|
||||||
@click.stop="$emit('restore-user-question')"
|
@click.stop="$emit('restore-user-question')"
|
||||||
@ -828,33 +828,43 @@ const runtimeQueuedMessagesForRender = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const projectGitSummaryForRender = computed(() => {
|
const projectGitSummaryForRender = computed(() => {
|
||||||
if (personalizationStore?.form?.show_git_status_bar === false) return null;
|
if (personalizationStore?.form?.show_git_status_bar === false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const summary = props.projectGitSummary;
|
const summary = props.projectGitSummary;
|
||||||
if (!summary?.has_git) return null;
|
if (!summary?.has_git) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const projectName = String(summary.project_name || '').trim();
|
const projectName = String(summary.project_name || '').trim();
|
||||||
const branch = String(summary.branch || '').trim();
|
const branch = String(summary.branch || '').trim();
|
||||||
if (!projectName || !branch) return null;
|
if (!projectName || !branch) {
|
||||||
return {
|
return null;
|
||||||
|
}
|
||||||
|
const result = {
|
||||||
projectName,
|
projectName,
|
||||||
projectPath: String(summary.project_path || '').trim(),
|
projectPath: String(summary.project_path || '').trim(),
|
||||||
branch,
|
branch,
|
||||||
additions: Math.max(0, Number(summary.additions || 0)),
|
additions: Math.max(0, Number(summary.additions || 0)),
|
||||||
deletions: Math.max(0, Number(summary.deletions || 0))
|
deletions: Math.max(0, Number(summary.deletions || 0))
|
||||||
};
|
};
|
||||||
|
return result;
|
||||||
});
|
});
|
||||||
|
|
||||||
const floatingStatusVisible = computed(() => {
|
const floatingStatusVisible = computed(() => {
|
||||||
if (skillSlashMenuOpen.value || fileAtOpen.value || props.quickMenuOpen) return false;
|
if (skillSlashMenuOpen.value || fileAtOpen.value || props.quickMenuOpen) {
|
||||||
// 消息队列(预输入/引导消息)存在时隐藏git状态栏,与 / 菜单出现时逻辑相同
|
|
||||||
if (runtimeQueuedMessagesForRender.value.length > 0 || props.hasPendingRuntimeGuidance)
|
|
||||||
return false;
|
return false;
|
||||||
return (
|
}
|
||||||
|
// 消息队列(预输入/引导消息)存在时隐藏git状态栏,与 / 菜单出现时逻辑相同
|
||||||
|
if (runtimeQueuedMessagesForRender.value.length > 0 || props.hasPendingRuntimeGuidance) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const visible =
|
||||||
!!projectGitSummaryForRender.value ||
|
!!projectGitSummaryForRender.value ||
|
||||||
!!props.goalRunning ||
|
!!props.goalRunning ||
|
||||||
!!props.goalModeArmed ||
|
!!props.goalModeArmed ||
|
||||||
(!!props.userQuestionMinimized && Number(props.pendingUserQuestionCount || 0) > 0) ||
|
(Number(props.pendingUserQuestionCount || 0) > 0) ||
|
||||||
(Number(props.activeSubAgentCount || 0) > 0)
|
(Number(props.activeSubAgentCount || 0) > 0);
|
||||||
);
|
return visible;
|
||||||
});
|
});
|
||||||
|
|
||||||
const showComposerAvatar = computed(() => {
|
const showComposerAvatar = computed(() => {
|
||||||
|
|||||||
@ -213,6 +213,7 @@ function submit() {
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 20px 22px 0;
|
padding: 20px 22px 0;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-question-kicker-row {
|
.user-question-kicker-row {
|
||||||
@ -240,6 +241,9 @@ function submit() {
|
|||||||
line-height: 1.55;
|
line-height: 1.55;
|
||||||
font-weight: 650;
|
font-weight: 650;
|
||||||
letter-spacing: -0.01em;
|
letter-spacing: -0.01em;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
max-height: min(180px, 30vh);
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-question-nav {
|
.user-question-nav {
|
||||||
@ -297,6 +301,9 @@ function submit() {
|
|||||||
color: var(--claude-text-secondary);
|
color: var(--claude-text-secondary);
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
max-height: min(160px, 25vh);
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-question-options {
|
.user-question-options {
|
||||||
@ -335,6 +342,7 @@ function submit() {
|
|||||||
color: var(--claude-text-secondary);
|
color: var(--claude-text-secondary);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1.42;
|
line-height: 1.42;
|
||||||
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-question-textarea-wrap {
|
.user-question-textarea-wrap {
|
||||||
|
|||||||
@ -2289,13 +2289,37 @@ const updateFloatingMenuPosition = async () => {
|
|||||||
const button = document.querySelector<HTMLElement>(
|
const button = document.querySelector<HTMLElement>(
|
||||||
'.settings-select-wrap.open .settings-select-button'
|
'.settings-select-wrap.open .settings-select-button'
|
||||||
);
|
);
|
||||||
|
const menu = document.querySelector<HTMLElement>(
|
||||||
|
'.settings-select-wrap.open .settings-floating-menu'
|
||||||
|
);
|
||||||
if (!button) {
|
if (!button) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const rect = button.getBoundingClientRect();
|
const rect = button.getBoundingClientRect();
|
||||||
const menuWidth = Math.min(300, Math.max(240, window.innerWidth - 32));
|
const menuWidth = Math.min(300, Math.max(240, window.innerWidth - 32));
|
||||||
const left = Math.max(16, Math.min(rect.right - menuWidth, window.innerWidth - menuWidth - 16));
|
const left = Math.max(16, Math.min(rect.right - menuWidth, window.innerWidth - menuWidth - 16));
|
||||||
const top = Math.max(16, Math.min(rect.bottom + 10, window.innerHeight - 80));
|
|
||||||
|
const padding = 16;
|
||||||
|
const gap = 10;
|
||||||
|
const menuHeight = menu?.getBoundingClientRect().height || 0;
|
||||||
|
let top: number;
|
||||||
|
if (menuHeight > 0) {
|
||||||
|
const spaceBelow = window.innerHeight - rect.bottom - padding;
|
||||||
|
const spaceAbove = rect.top - padding;
|
||||||
|
if (spaceBelow >= menuHeight) {
|
||||||
|
top = rect.bottom + gap;
|
||||||
|
} else if (spaceAbove >= menuHeight) {
|
||||||
|
top = rect.top - menuHeight - gap;
|
||||||
|
} else if (spaceBelow >= spaceAbove) {
|
||||||
|
// 下方空间相对更大:菜单贴底部,内容通过 max-height 滚动
|
||||||
|
top = Math.max(rect.bottom + gap, window.innerHeight - menuHeight - padding);
|
||||||
|
} else {
|
||||||
|
// 上方空间相对更大:菜单贴顶部
|
||||||
|
top = padding;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
top = Math.max(padding, Math.min(rect.bottom + gap, window.innerHeight - 80));
|
||||||
|
}
|
||||||
floatingMenuStyle.value = {
|
floatingMenuStyle.value = {
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
top: `${Math.round(top)}px`,
|
top: `${Math.round(top)}px`,
|
||||||
|
|||||||
@ -40,6 +40,10 @@ class ServerRefactorSmokeTest(unittest.TestCase):
|
|||||||
import server.chat_flow_runner_helpers as helpers
|
import server.chat_flow_runner_helpers as helpers
|
||||||
|
|
||||||
self.assertEqual(helpers.extract_intent_from_partial('{"intent": "fix"}'), "fix")
|
self.assertEqual(helpers.extract_intent_from_partial('{"intent": "fix"}'), "fix")
|
||||||
|
self.assertEqual(
|
||||||
|
helpers.extract_intent_from_partial('{"intent": "\\u7b49\\u5f85\\u4e0b\\u8f7d\\u5b8c\\u6210"}'),
|
||||||
|
"等待下载完成",
|
||||||
|
)
|
||||||
self.assertEqual(helpers.resolve_monitor_path({"file_path": " a.txt "}), "a.txt")
|
self.assertEqual(helpers.resolve_monitor_path({"file_path": " a.txt "}), "a.txt")
|
||||||
self.assertEqual(helpers.resolve_monitor_memory([1, 2, 3], entry_limit=2), ["1", "2"])
|
self.assertEqual(helpers.resolve_monitor_memory([1, 2, 3], entry_limit=2), ["1", "2"])
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user