优化: drainQueue并发互斥+deep watch指纹+前端打磨
This commit is contained in:
@@ -309,21 +309,25 @@ function scrollToFirstPending(): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除 deep:true,改在 watch source 中计算指纹(消息数 + toolCalls 的 id/status),
|
||||||
|
* 避免 deep 每帧全量深遍历长会话数组引起掉帧。流式 delta 只改 content 不影响指纹,
|
||||||
|
* 不触发无意义的收起重算。length 变化(新消息/截断)和 toolCall 状态翻转(新卡/完成/拒绝)
|
||||||
|
* 均能正确触发——与 body 的快照收缩逻辑等价,无需 deep 兜底。
|
||||||
|
*/
|
||||||
let lastMsgSnapshot = ''
|
let lastMsgSnapshot = ''
|
||||||
watch(
|
watch(
|
||||||
() => store.state.messages,
|
() => JSON.stringify({
|
||||||
(msgs) => {
|
n: store.state.messages.length,
|
||||||
const snap = JSON.stringify({
|
tc: store.state.messages.map(m => (m.toolCalls || []).map(t => ({ id: t.id, s: t.status }))),
|
||||||
n: msgs.length,
|
}),
|
||||||
tc: msgs.map(m => (m.toolCalls || []).map(t => ({ id: t.id, s: t.status }))),
|
(snap) => {
|
||||||
})
|
|
||||||
if (snap === lastMsgSnapshot) return
|
if (snap === lastMsgSnapshot) return
|
||||||
const isFirst = lastMsgSnapshot === ''
|
const isFirst = lastMsgSnapshot === ''
|
||||||
lastMsgSnapshot = snap
|
lastMsgSnapshot = snap
|
||||||
if (isFirst) return // 首次加载/切换对话不触发收起
|
if (isFirst) return
|
||||||
collapseAllToolLists(buildActiveToolIds(msgs))
|
collapseAllToolLists(buildActiveToolIds(store.state.messages))
|
||||||
},
|
},
|
||||||
{ deep: true },
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// UX-260616-03: AiAgentRound 新轮次到达时收起上一轮工具卡(round 递增)。
|
// UX-260616-03: AiAgentRound 新轮次到达时收起上一轮工具卡(round 递增)。
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
<!-- 从 TaskDetail.vue 抽出(L131-144 模板 + L214-262 脚本 + L643-670 样式),零行为变更。 -->
|
<!-- 从 TaskDetail.vue 抽出(L131-144 模板 + L214-262 脚本 + L643-670 样式),零行为变更。 -->
|
||||||
<!-- 自包含:JSON 解析 / Markdown 渲染(复用 useMarkdown 模块单例)/ review verdict 计算 / i18n 全部内聚。 -->
|
<!-- 自包含:JSON 解析 / Markdown 渲染(复用 useMarkdown 模块单例)/ review verdict 计算 / i18n 全部内聚。 -->
|
||||||
<div v-if="parsedOutput" class="info-item info-block output-block">
|
<div v-if="parsedOutput" class="info-item info-block output-block">
|
||||||
<span class="label">{{ $t('taskDetail.output') }}</span>
|
|
||||||
<div class="value output-body">
|
<div class="value output-body">
|
||||||
<!-- AI 自审结论卡(若有 review 子字段) -->
|
<!-- AI 自审结论卡(若有 review 子字段) -->
|
||||||
<div v-if="parsedOutput.review" class="review-card" :class="reviewVerdictClass">
|
<div v-if="parsedOutput.review" class="review-card" :class="reviewVerdictClass">
|
||||||
|
|||||||
@@ -267,14 +267,21 @@ async function editMessage(newMessage: string) {
|
|||||||
* 卡死),也无任何用户反馈。现显式 catch:复位 streaming + 推错误气泡(后台会话不推,避免污染
|
* 卡死),也无任何用户反馈。现显式 catch:复位 streaming + 推错误气泡(后台会话不推,避免污染
|
||||||
* 当前视图)+ 回填失败消息到该会话队首并停 drain(不静默丢用户输入,保留剩余队列供手动重试/
|
* 当前视图)+ 回填失败消息到该会话队首并停 drain(不静默丢用户输入,保留剩余队列供手动重试/
|
||||||
* 编辑/取消)。成功路径不受影响——doSend 成功后端会再 emit AiCompleted 续 drain 链路。
|
* 编辑/取消)。成功路径不受影响——doSend 成功后端会再 emit AiCompleted 续 drain 链路。
|
||||||
|
*
|
||||||
|
* 并发互斥 — 每个会话维护 _draining 标志,防止 AiCompleted 并发触发两次 drainQueue
|
||||||
|
* 导致 splice 两条消息同时发送,顺序错乱。已在 drain 中则跳过,等待下一次 AiCompleted 续发。
|
||||||
*/
|
*/
|
||||||
|
let _drainingConvs = new Set<string>()
|
||||||
export function drainQueue(convId?: string | null) {
|
export function drainQueue(convId?: string | null) {
|
||||||
if (state.queue.length === 0) return
|
if (state.queue.length === 0) return
|
||||||
// 找队首属于目标会话的消息;若穿越(非目标会话的项堵在队首)则跳过整条队列
|
// 找队首属于目标会话的消息;若穿越(非目标会话的项堵在队首)则跳过整条队列
|
||||||
const targetId = convId ?? state.activeConversationId
|
const targetId = convId ?? state.activeConversationId
|
||||||
if (!targetId) return
|
if (!targetId) return
|
||||||
|
// 并发互斥:该会话已在 drain 中则跳过,由下一轮 AiCompleted 续发
|
||||||
|
if (_drainingConvs.has(targetId)) return
|
||||||
const idx = state.queue.findIndex(q => q.conversationId === targetId)
|
const idx = state.queue.findIndex(q => q.conversationId === targetId)
|
||||||
if (idx === -1) return
|
if (idx === -1) return
|
||||||
|
_drainingConvs.add(targetId)
|
||||||
const next = state.queue.splice(idx, 1)[0]!
|
const next = state.queue.splice(idx, 1)[0]!
|
||||||
void (async () => {
|
void (async () => {
|
||||||
try {
|
try {
|
||||||
@@ -301,6 +308,8 @@ export function drainQueue(convId?: string | null) {
|
|||||||
} as AiMessage)
|
} as AiMessage)
|
||||||
}
|
}
|
||||||
console.error('[AI] drainQueue 续发失败:', e)
|
console.error('[AI] drainQueue 续发失败:', e)
|
||||||
|
} finally {
|
||||||
|
_drainingConvs.delete(targetId)
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,8 +41,8 @@
|
|||||||
原 Ideas.vue / IdeaDetail.vue <style scoped> 两处逐字一致(灵感专属 5 态色系,
|
原 Ideas.vue / IdeaDetail.vue <style scoped> 两处逐字一致(灵感专属 5 态色系,
|
||||||
与 Tasks/AuditLog 的 status-tag 语义不同,独立提取避免混淆)。 */
|
与 Tasks/AuditLog 的 status-tag 语义不同,独立提取避免混淆)。 */
|
||||||
.status-tag {
|
.status-tag {
|
||||||
font-size: 11px; font-weight: 500; padding: 2px 8px;
|
font-size: 11px; font-weight: 500; padding: 4px 10px;
|
||||||
border-radius: var(--df-radius-xs);
|
border-radius: var(--df-radius-sm);
|
||||||
flex-shrink: 0; white-space: nowrap;
|
flex-shrink: 0; white-space: nowrap;
|
||||||
}
|
}
|
||||||
.status-draft { background: rgba(90,99,128,0.2); color: var(--df-text-dim); }
|
.status-draft { background: rgba(90,99,128,0.2); color: var(--df-text-dim); }
|
||||||
|
|||||||
@@ -1508,8 +1508,7 @@ onUnmounted(() => {
|
|||||||
color: var(--df-accent);
|
color: var(--df-accent);
|
||||||
border-color: var(--df-accent);
|
border-color: var(--df-accent);
|
||||||
}
|
}
|
||||||
/* 已暂缓状态 badge */
|
/* 状态色类已全局化至 components.css,scoped 不重复定义 */
|
||||||
.status-deferred { background: rgba(255,193,7,0.15); color: #f0a030; }
|
|
||||||
|
|
||||||
/* A2[PD-P1-3]:工作流日志「全局事件」标注(次要小字标签) */
|
/* A2[PD-P1-3]:工作流日志「全局事件」标注(次要小字标签) */
|
||||||
.global-event-tag {
|
.global-event-tag {
|
||||||
@@ -1561,11 +1560,9 @@ onUnmounted(() => {
|
|||||||
.task-card:last-child { border-bottom: none; }
|
.task-card:last-child { border-bottom: none; }
|
||||||
.task-top { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
|
.task-top { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
|
||||||
.task-title { font-size: 14px; font-weight: 500; color: var(--df-text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
.task-title { font-size: 14px; font-weight: 500; color: var(--df-text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
.task-status { font-size: 11px; padding: 2px 8px; border-radius: var(--df-radius-xs); }
|
.task-status { font-size: 11px; padding: 4px 10px; border-radius: var(--df-radius-sm); }
|
||||||
.status-done { background: rgba(100,255,218,0.15); color: var(--df-success); }
|
/* 状态色类已全局化至 components.css;此处仅保留本视图特有的进度覆盖 */
|
||||||
.status-progress { background: var(--df-accent-soft); color: var(--df-accent); }
|
.status-progress { background: var(--df-accent-soft); color: var(--df-accent); }
|
||||||
.status-review { background: rgba(255,217,61,0.15); color: var(--df-warning); }
|
|
||||||
.status-todo { background: rgba(90,99,128,0.2); color: var(--df-text-dim); }
|
|
||||||
|
|
||||||
.task-branch {
|
.task-branch {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -865,22 +865,8 @@ onBeforeUnmount(() => {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== 状态/优先级标签 ===== */
|
/* 状态/优先级标签 — 尺寸/圆角由全局 components.css .status-tag 统一,scoped 不重复定义 */
|
||||||
.status-tag {
|
/* 状态色类已全局化至 components.css(scoped 删除,消除漂移) */
|
||||||
font-size: 11px; font-weight: 500;
|
|
||||||
padding: 4px 10px;
|
|
||||||
border-radius: var(--df-radius-sm);
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.status-todo { background: rgba(90,99,128,0.2); color: var(--df-text-dim); }
|
|
||||||
.status-progress { background: rgba(100,181,246,0.2); color: var(--df-info); }
|
|
||||||
.status-review { background: rgba(255,217,61,0.2); color: var(--df-warning); }
|
|
||||||
.status-done { background: rgba(100,255,218,0.15); color: var(--df-success); }
|
|
||||||
.status-abandoned { background: rgba(255,107,107,0.2); color: var(--df-danger); }
|
|
||||||
/* D-260616-01:7 态新增 — testing(橙警告,近 review 区分)/blocked(红 danger,与 cancelled 区分用实心边框)。配色与 Tasks.vue 一致保持跨视图统一 */
|
|
||||||
.status-testing { background: rgba(255,152,0,0.2); color: #ff9800; }
|
|
||||||
.status-blocked { background: rgba(255,107,107,0.12); color: var(--df-danger); border: 0.5px solid var(--df-danger); }
|
|
||||||
.status-deferred { background: rgba(255,193,7,0.15); color: #f0a030; }
|
|
||||||
|
|
||||||
.priority-badge {
|
.priority-badge {
|
||||||
font-size: 10px; font-weight: 500;
|
font-size: 10px; font-weight: 500;
|
||||||
|
|||||||
+7
-3
@@ -825,7 +825,8 @@ function openCreateModal(parent?: TaskRecord) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function confirmCreate() {
|
async function confirmCreate() {
|
||||||
if (!newTaskTitle.value.trim() || !newTaskProjectId.value) return
|
if (!newTaskProjectId.value) { alert('请选择项目'); return }
|
||||||
|
if (!newTaskTitle.value.trim()) return
|
||||||
submitting.value = true
|
submitting.value = true
|
||||||
try {
|
try {
|
||||||
const r = await store.createTask({
|
const r = await store.createTask({
|
||||||
@@ -866,6 +867,10 @@ watch(sortBy, () => {
|
|||||||
|
|
||||||
// Ctrl+N 新建 / Ctrl+F 搜索(桌面快捷键)
|
// Ctrl+N 新建 / Ctrl+F 搜索(桌面快捷键)
|
||||||
function onKeydown(e: KeyboardEvent) {
|
function onKeydown(e: KeyboardEvent) {
|
||||||
|
if (e.key === 'Escape' && showCreateModal.value) {
|
||||||
|
showCreateModal.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
if ((e.ctrlKey || e.metaKey) && e.key === 'n') {
|
if ((e.ctrlKey || e.metaKey) && e.key === 'n') {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
openCreateModal()
|
openCreateModal()
|
||||||
@@ -1314,12 +1319,11 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
/* 迷你进度条(细条渐变填充,宽度=完成子任务百分比) */
|
/* 迷你进度条(细条渐变填充,宽度=完成子任务百分比) */
|
||||||
.mini-progress {
|
.mini-progress {
|
||||||
width: 72px;
|
flex: 1;
|
||||||
height: 4px;
|
height: 4px;
|
||||||
background: var(--df-border);
|
background: var(--df-border);
|
||||||
border-radius: var(--df-radius-xs);
|
border-radius: var(--df-radius-xs);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
}
|
||||||
.mini-progress-fill {
|
.mini-progress-fill {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
Reference in New Issue
Block a user