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
|
||||
|
||||
import json
|
||||
import re
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
@ -10,7 +11,16 @@ def extract_intent_from_partial(arg_str: str) -> Optional[str]:
|
||||
return None
|
||||
match = re.search(r'"intent"\s*:\s*"([^"]{0,128})', arg_str, re.IGNORECASE | re.DOTALL)
|
||||
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
|
||||
|
||||
|
||||
|
||||
@ -41,7 +41,6 @@ export async function mounted() {
|
||||
|
||||
// 立即加载初始数据(并行获取状态,优先同步运行模式)
|
||||
const initialDataPromise = this.loadInitialData();
|
||||
this.refreshProjectGitSummary?.();
|
||||
this.startProjectGitSummaryIdleRefresh?.();
|
||||
this.startConnectionHeartbeat();
|
||||
this.fetchTerminalCount();
|
||||
@ -50,6 +49,8 @@ export async function mounted() {
|
||||
if (initialDataPromise && typeof initialDataPromise.then === 'function') {
|
||||
initialDataPromise
|
||||
.then(() => {
|
||||
// 初始数据加载完成后再刷新 git 摘要,确保 workspace/project_path 已就绪
|
||||
this.refreshProjectGitSummary?.();
|
||||
// 避免在初始加载阶段被覆盖,加载完成后再兜底检查一次
|
||||
this.checkTutorialPrompt();
|
||||
})
|
||||
|
||||
@ -232,6 +232,8 @@ export const loadMethods = {
|
||||
this.fetchNetworkPermission();
|
||||
await this.fetchVersioningStatus(conversationId, { silent: true });
|
||||
this.fetchPendingToolApprovals();
|
||||
this.fetchPendingUserQuestions();
|
||||
this.refreshProjectGitSummary();
|
||||
this.fetchConversationTokenStatistics();
|
||||
this.updateCurrentContextTokens();
|
||||
traceLog('loadConversation:after-history', {
|
||||
|
||||
@ -58,6 +58,35 @@ export const dialogMethods = {
|
||||
this.userQuestionDialogVisible = true;
|
||||
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) {
|
||||
return this.decideToolApproval(approvalId, 'approved');
|
||||
},
|
||||
|
||||
@ -70,7 +70,7 @@ export const gitMethods = {
|
||||
if (this.gitChangesPanelOpen) {
|
||||
this.loadGitChangesDiff?.();
|
||||
}
|
||||
} catch (_) {
|
||||
} catch (_error) {
|
||||
this.projectGitSummary = null;
|
||||
this.gitChangesDiff = null;
|
||||
} finally {
|
||||
|
||||
@ -151,6 +151,9 @@ export const routeMethods = {
|
||||
this.initialRouteResolved = true;
|
||||
}
|
||||
await this.restoreComposerDraftState('bootstrap-route:conversation');
|
||||
if (this.currentConversationId) {
|
||||
this.fetchPendingUserQuestions?.();
|
||||
}
|
||||
},
|
||||
handlePopState(event) {
|
||||
const state = event.state || {};
|
||||
|
||||
@ -372,6 +372,7 @@ export const socketMethods = {
|
||||
this.fetchConversationTokenStatistics();
|
||||
this.updateCurrentContextTokens();
|
||||
await this.fetchVersioningStatus(this.currentConversationId, { silent: true });
|
||||
this.fetchPendingUserQuestions();
|
||||
} catch (e) {
|
||||
console.warn('获取当前对话标题失败:', e);
|
||||
this.titleReady = true;
|
||||
|
||||
@ -170,7 +170,7 @@
|
||||
>目标模式就绪</span
|
||||
>
|
||||
<button
|
||||
v-if="userQuestionMinimized && pendingUserQuestionCount > 0"
|
||||
v-if="pendingUserQuestionCount > 0"
|
||||
type="button"
|
||||
class="floating-project-status__notice floating-project-status__notice--button"
|
||||
@click.stop="$emit('restore-user-question')"
|
||||
@ -828,33 +828,43 @@ const runtimeQueuedMessagesForRender = 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;
|
||||
if (!summary?.has_git) return null;
|
||||
if (!summary?.has_git) {
|
||||
return null;
|
||||
}
|
||||
const projectName = String(summary.project_name || '').trim();
|
||||
const branch = String(summary.branch || '').trim();
|
||||
if (!projectName || !branch) return null;
|
||||
return {
|
||||
if (!projectName || !branch) {
|
||||
return null;
|
||||
}
|
||||
const result = {
|
||||
projectName,
|
||||
projectPath: String(summary.project_path || '').trim(),
|
||||
branch,
|
||||
additions: Math.max(0, Number(summary.additions || 0)),
|
||||
deletions: Math.max(0, Number(summary.deletions || 0))
|
||||
};
|
||||
return result;
|
||||
});
|
||||
|
||||
const floatingStatusVisible = computed(() => {
|
||||
if (skillSlashMenuOpen.value || fileAtOpen.value || props.quickMenuOpen) return false;
|
||||
// 消息队列(预输入/引导消息)存在时隐藏git状态栏,与 / 菜单出现时逻辑相同
|
||||
if (runtimeQueuedMessagesForRender.value.length > 0 || props.hasPendingRuntimeGuidance)
|
||||
if (skillSlashMenuOpen.value || fileAtOpen.value || props.quickMenuOpen) {
|
||||
return false;
|
||||
return (
|
||||
}
|
||||
// 消息队列(预输入/引导消息)存在时隐藏git状态栏,与 / 菜单出现时逻辑相同
|
||||
if (runtimeQueuedMessagesForRender.value.length > 0 || props.hasPendingRuntimeGuidance) {
|
||||
return false;
|
||||
}
|
||||
const visible =
|
||||
!!projectGitSummaryForRender.value ||
|
||||
!!props.goalRunning ||
|
||||
!!props.goalModeArmed ||
|
||||
(!!props.userQuestionMinimized && Number(props.pendingUserQuestionCount || 0) > 0) ||
|
||||
(Number(props.activeSubAgentCount || 0) > 0)
|
||||
);
|
||||
(Number(props.pendingUserQuestionCount || 0) > 0) ||
|
||||
(Number(props.activeSubAgentCount || 0) > 0);
|
||||
return visible;
|
||||
});
|
||||
|
||||
const showComposerAvatar = computed(() => {
|
||||
|
||||
@ -213,6 +213,7 @@ function submit() {
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
padding: 20px 22px 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.user-question-kicker-row {
|
||||
@ -240,6 +241,9 @@ function submit() {
|
||||
line-height: 1.55;
|
||||
font-weight: 650;
|
||||
letter-spacing: -0.01em;
|
||||
white-space: pre-wrap;
|
||||
max-height: min(180px, 30vh);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.user-question-nav {
|
||||
@ -297,6 +301,9 @@ function submit() {
|
||||
color: var(--claude-text-secondary);
|
||||
line-height: 1.6;
|
||||
font-size: 13px;
|
||||
white-space: pre-wrap;
|
||||
max-height: min(160px, 25vh);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.user-question-options {
|
||||
@ -335,6 +342,7 @@ function submit() {
|
||||
color: var(--claude-text-secondary);
|
||||
font-size: 12px;
|
||||
line-height: 1.42;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.user-question-textarea-wrap {
|
||||
|
||||
@ -2289,13 +2289,37 @@ const updateFloatingMenuPosition = async () => {
|
||||
const button = document.querySelector<HTMLElement>(
|
||||
'.settings-select-wrap.open .settings-select-button'
|
||||
);
|
||||
const menu = document.querySelector<HTMLElement>(
|
||||
'.settings-select-wrap.open .settings-floating-menu'
|
||||
);
|
||||
if (!button) {
|
||||
return;
|
||||
}
|
||||
const rect = button.getBoundingClientRect();
|
||||
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 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 = {
|
||||
position: 'fixed',
|
||||
top: `${Math.round(top)}px`,
|
||||
|
||||
@ -40,6 +40,10 @@ class ServerRefactorSmokeTest(unittest.TestCase):
|
||||
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": "\\u7b49\\u5f85\\u4e0b\\u8f7d\\u5b8c\\u6210"}'),
|
||||
"等待下载完成",
|
||||
)
|
||||
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"])
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user