修复: 审批不超时+重启恢复 + 其他小改
- APPROVAL_TIMEOUT_MS=Infinity(已决策:审批不做超时) - V33 迁移: ai_conversations 加 pending_approvals 列 - save_conversation 持久化 pending_approvals - ai_conversation_switch 从 DB 恢复 pending_approvals - switchConversation 加 try/catch(对话不存在→建新) - @项目 关联添加取消按钮(×) - TopBar 底部无标题时图标右对齐 - TaskDetail.vue 删除重复 case
This commit is contained in:
@@ -134,6 +134,7 @@
|
||||
</svg>
|
||||
<span class="ai-enrichment-badge">{{ enrichmentBadgeText }}</span>
|
||||
</button>
|
||||
<button class="ai-enrichment-dismiss" @click="dismissEnrichment" title="取消关联">×</button>
|
||||
<div v-if="enrichmentExpanded" class="ai-enrichment-panel">
|
||||
<div class="ai-enrichment-label">{{ $t('aiChat.enrichmentLabel') }}</div>
|
||||
<pre class="ai-enrichment-body"><code>{{ enrichmentDetailText }}</code></pre>
|
||||
@@ -485,6 +486,28 @@ const enrichmentDetailText = computed(() => {
|
||||
return lines.join('\n')
|
||||
})
|
||||
|
||||
/** 取消 @项目 关联:清除 mentionSpan + 移除输入框中的 [@项目: xxx] 标记 */
|
||||
function dismissEnrichment() {
|
||||
const projectSpans = pendingMentionSpans.value.filter(sp => sp.kind === 'project')
|
||||
if (projectSpans.length === 0) return
|
||||
// 从 inputText 中移除所有 [@项目: xxx] 标记
|
||||
let text = inputText.value
|
||||
for (const span of projectSpans) {
|
||||
const label = text.slice(span.start, span.start + span.length)
|
||||
// 替换标记 + 前后可能的空格为单个空格
|
||||
const pattern = new RegExp(`\\s*${escapeRegex(label)}\s*`)
|
||||
text = text.replace(pattern, ' ').trim()
|
||||
}
|
||||
inputText.value = text
|
||||
// 清除 project 类型的 mentionSpan
|
||||
pendingMentionSpans.value = pendingMentionSpans.value.filter(sp => sp.kind !== 'project')
|
||||
enrichmentExpanded.value = false
|
||||
}
|
||||
|
||||
function escapeRegex(s: string): string {
|
||||
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
}
|
||||
|
||||
const mentionGroupLabel = computed(() => ({
|
||||
project: t('aiChat.mentionGroupProject'),
|
||||
task: t('aiChat.mentionGroupTask'),
|
||||
@@ -1078,6 +1101,8 @@ defineExpose({
|
||||
|
||||
/* ── ⑥.4 Phase 4: @项目展开摘要(输入区 enrichment 预览) ── */
|
||||
.ai-enrichment-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 6px;
|
||||
border: 0.5px solid color-mix(in srgb, var(--df-accent) 30%, transparent);
|
||||
border-radius: var(--df-radius);
|
||||
@@ -1111,6 +1136,20 @@ defineExpose({
|
||||
.ai-enrichment-toggle--expanded .ai-enrichment-chevron {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
.ai-enrichment-dismiss {
|
||||
flex-shrink: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--df-text-dim);
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
padding: 0 4px;
|
||||
line-height: 1;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
.ai-enrichment-dismiss:hover {
|
||||
color: var(--df-danger);
|
||||
}
|
||||
.ai-enrichment-badge {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
@@ -538,26 +538,26 @@ onBeforeUnmount(() => {
|
||||
.ai-bottom-tools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
padding: 0 14px 4px;
|
||||
}
|
||||
.ai-bottom-title {
|
||||
font-size: 11px;
|
||||
color: var(--df-text-secondary);
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
margin-right: 8px;
|
||||
margin-right: auto;
|
||||
|
||||
}
|
||||
.ai-bottom-tools-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
/* ═══ Provider Bar (header 中间区) ═══ */
|
||||
.ai-provider-bar {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 11px;
|
||||
color: var(--df-text-secondary);
|
||||
|
||||
@@ -71,7 +71,7 @@ export function findToolCall(id: string): AiToolCallInfo | undefined {
|
||||
// ── 审批超时计时器(原 useAiSend,下沉打破 events↔send 循环依赖) ──
|
||||
|
||||
/// 审批等待超时阈值(ms) — 用户 5 分钟内不处理则自动拒绝
|
||||
const APPROVAL_TIMEOUT_MS = 5 * 60 * 1000
|
||||
const APPROVAL_TIMEOUT_MS = Infinity // 已决策:不做超时,用户不审批就一直等
|
||||
|
||||
/** toolCallId → 审批超时计时器 */
|
||||
const _approvalTimers = new Map<string, ReturnType<typeof setTimeout>>()
|
||||
|
||||
@@ -10,9 +10,9 @@ import { state } from '@/stores/ai'
|
||||
import { notifyConversationChanged } from './useAiEvents'
|
||||
import { persistUiState } from './useAiPanel'
|
||||
import { setStreaming } from './streamingGuard'
|
||||
import { nextMsgId, getConvState, clearConvStreamState } from './aiShared'
|
||||
import { nextMsgId, getConvState, clearConvStreamState, startApprovalTimer } from './aiShared'
|
||||
import { t } from '@/i18n/i18n-helpers'
|
||||
import type { AiMessage, AiToolCallInfo } from '@/api/types'
|
||||
import type { AiConversationDetail, AiMessage, AiToolCallInfo } from '@/api/types'
|
||||
|
||||
const appSettings = useAppSettingsStore()
|
||||
|
||||
@@ -92,7 +92,22 @@ let _latestSwitchId = 0
|
||||
export async function switchConversation(id: string) {
|
||||
const mySwitchId = ++_latestSwitchId
|
||||
// 允许生成中切换:后台继续生成,事件按 conversation_id 路由不污染当前视图
|
||||
const detail = await aiApi.switchConversation(id)
|
||||
let detail: AiConversationDetail
|
||||
try {
|
||||
detail = await aiApi.switchConversation(id)
|
||||
} catch {
|
||||
// 对话不存在(已删除/未落库的虚 ID)→ 创建新对话替代
|
||||
console.warn('[AI] switchConversation 失败,创建新对话:', id)
|
||||
const created = await aiApi.createConversation()
|
||||
if (mySwitchId !== _latestSwitchId) return
|
||||
void appSettings.set('df-ai-active-conv', created.id)
|
||||
// 用新对话 id 重走后续逻辑
|
||||
detail = { id: created.id, title: null, messages: '[]' }
|
||||
state.messages = []
|
||||
notifyConversationChanged()
|
||||
void loadConversations()
|
||||
return
|
||||
}
|
||||
// 过期响应丢弃(用户已切到别的对话,防 A 后返回覆盖 B)
|
||||
if (mySwitchId !== _latestSwitchId) return
|
||||
state.activeConversationId = id
|
||||
@@ -226,6 +241,9 @@ export async function switchConversation(id: string) {
|
||||
dir: tc.dir,
|
||||
reason: tc.reason,
|
||||
})
|
||||
// 重新启动审批计时器(APPROVAL_TIMEOUT_MS=Infinity 已决策,不会超时自动拒绝,
|
||||
// 启动仅用于计时器注册一致性,保持与 AiApprovalRequired 事件处理对称)
|
||||
startApprovalTimer(tc.id, tc.name, kind)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -396,8 +396,7 @@ function handleWorkflowEvent(payload: WorkflowEventPayload) {
|
||||
break
|
||||
}
|
||||
case 'workflow_failed':
|
||||
case 'node_failed':
|
||||
case 'workflow_failed': {
|
||||
case 'node_failed': {
|
||||
if (evt?.node_id) {
|
||||
const node = String(evt.node_id)
|
||||
wfNodeStatuses.value = { ...wfNodeStatuses.value, [node]: 'failed' }
|
||||
|
||||
Reference in New Issue
Block a user