优化: 上下文管理与提示词链路批
- T1 usage 口径: anthropic usage 两层容错+cache 三口径归一(prompt=input+read+creation) - T2 压缩水位判据: compress_should_trigger 双模式+ratio 钳位,开关关逐字节等价 - T3 跨会话检索: history_search 工具(两跳式)+message_repo 关键词检索+prompt 指引 - T4 代语义闭环: status 终局补写+飞书停靠/换代推卡+交付引导+watcher 兜底补漏 - 段序缓存重排: build_runtime_prompt 恒静段上移/工作上下文压尾(前缀缓存失效面收窄) - 智谱缓存解析: anthropic 臂补 prompt_tokens_details.cached_tokens 嵌套回退 - 工时禁令: 提示词双语加「禁止预估工时,AI Coding 工时无意义」 - 拖拽文件落盘: 非图片附件存引用行,防整段文本灌入输入框 - 事实修正: 提示词「40+ 工具」→「80+」(基线 81);tool_failure_memory 测试串行锁除竞态 - UI 拆分: TopBar/ToolResultBody 壳层化(top-bar/tool-result-body 子件目录)
This commit is contained in:
@@ -1,212 +0,0 @@
|
||||
<template>
|
||||
<!-- F-260619-03 Phase B + path_auth 审批链阶段1: 路径授权弹窗(LLM 想访问白名单外目录,
|
||||
后端挂起 loop 等用户决定)。阶段1 改遍历 pendingDirAuths(数组)——同轮多个文件工具落不同
|
||||
未授权目录时后端连发多条 AiDirAuthRequired,数组并存不互覆盖。
|
||||
条件:pendingDirAuths 非空(AiDirAuthRequired 事件置)+ 当前视图正在生成(防切走后误显)。
|
||||
每项独立四按钮:点"本次" → ai_authorize_dir(decision=once, 写 once 层单次授权,执行后清)→ 后端执行工具 + try_continue。
|
||||
点"当前会话" → ai_authorize_dir(decision=session, 写 session 层会话授权,切会话清)→ 执行 + 续 loop。
|
||||
点"始终放行" → ai_authorize_dir(decision=always, 写持久化 Settings KV)→ 执行 + 续 loop。
|
||||
点"拒绝" → ai_authorize_dir(decision=deny, 工具返 Err)→ 续 loop。
|
||||
store 单例共享(state/aiApi),pendingDirAuths 模块级 ref(useAiEvents)直接导入。
|
||||
toast 经 emit 转父(保持单一 toast 源)。 -->
|
||||
<div v-for="item in visibleDirAuths" :key="item.id" ref="dirAuthRefs" class="ai-dir-auth">
|
||||
<span class="ai-dir-auth-text">{{ $t('aiChat.dirAuthRequired') }}</span>
|
||||
<span class="ai-dir-auth-hint">
|
||||
{{ $t('aiChat.dirAuthHint', { tool: item.tool, path: item.path }) }}
|
||||
</span>
|
||||
<div class="ai-dir-auth-actions">
|
||||
<button
|
||||
class="ai-dir-auth-btn ai-dir-auth-btn--once"
|
||||
:disabled="actingIds.has(item.id)"
|
||||
@click="handleAuthorize(item.id, 'once')"
|
||||
>{{ $t('aiChat.dirAuthOnce') }}</button>
|
||||
<button
|
||||
class="ai-dir-auth-btn ai-dir-auth-btn--session"
|
||||
:disabled="actingIds.has(item.id)"
|
||||
@click="handleAuthorize(item.id, 'session')"
|
||||
>{{ $t('aiChat.dirAuthSession') }}</button>
|
||||
<button
|
||||
class="ai-dir-auth-btn ai-dir-auth-btn--always"
|
||||
:disabled="actingIds.has(item.id)"
|
||||
@click="handleAuthorize(item.id, 'always')"
|
||||
>{{ $t('aiChat.dirAuthAlways') }}</button>
|
||||
<button
|
||||
class="ai-dir-auth-btn ai-dir-auth-btn--deny"
|
||||
:disabled="actingIds.has(item.id)"
|
||||
@click="handleAuthorize(item.id, 'deny')"
|
||||
>{{ $t('aiChat.dirAuthDeny') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- P1-e-c: deny 拒绝二次确认(拒绝不可逆——工具返 Err、loop 续跑不再重试;对齐 ToolCard/ApprovalPopup) -->
|
||||
<ConfirmDialog
|
||||
:visible="confirmState.visible"
|
||||
:msg="confirmState.msg"
|
||||
:danger-label="$t('common.confirm')"
|
||||
@result="answerConfirm"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, computed, watch, nextTick, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAiStore } from '../../stores/ai'
|
||||
import { pendingDirAuths } from '../../composables/ai/useAiPendingState'
|
||||
import { aiApi } from '../../api'
|
||||
import { useConfirm } from '../../composables/useConfirm'
|
||||
import ConfirmDialog from '../../components/ConfirmDialog.vue'
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'toast', payload: { msg: string; type: 'error' | 'warning' | 'info' }): void
|
||||
}>()
|
||||
|
||||
const store = useAiStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
// P1-e-c: deny 拒绝二次确认统一弹层(useConfirm 状态机 + ConfirmDialog 组件,同 ToolCard/ApprovalPopup)
|
||||
const { confirmState, confirmDialog, answerConfirm } = useConfirm()
|
||||
|
||||
// F-260620 根治(授权挂起无声卡死):挂起态弹窗显示只依赖 isGenerating(conv),不依赖 streaming。
|
||||
// streaming 会被流式看门狗超时清(useAiStream onStreamTimeout),而 AiDirAuthRequired 挂起常发生在
|
||||
// 首轮慢响应(看门狗已超时清 streaming)之后——此时 streaming=false 但后端 generating=true、
|
||||
// generatingConvs 含 conv(useAiEvents handleEvent 对非完成/错误事件 add)。若守卫仍 && streaming,
|
||||
// 挂起弹窗永不显示 → loop 无声卡死(用户发消息无响应)。去 streaming,isGenerating(conv) 单条件覆盖。
|
||||
// 多会话并发 — isGenerating(active) 替代单值比对
|
||||
const isViewingGenerating = computed(() =>
|
||||
store.isGenerating(store.state.activeConversationId),
|
||||
)
|
||||
|
||||
// pendingDirAuths 非空 + 当前视图正在生成(防切走后误显)双重守卫。
|
||||
// 阶段1:数组化后,visibleDirAuths 是 pendingDirAuths 的派生视图(按 conv 过滤当前会话挂起项),
|
||||
// 与 showDirAuthCard 合并为单一 computed(非空即显)。
|
||||
const visibleDirAuths = computed(() => {
|
||||
if (!isViewingGenerating.value) return []
|
||||
// 仅显当前会话的挂起项(active conv);其他会话的挂起不在本视图弹窗。
|
||||
const active = store.state.activeConversationId
|
||||
return pendingDirAuths.value.filter(p => !p.conversationId || p.conversationId === active)
|
||||
})
|
||||
|
||||
// 阶段1:逐 id 追踪"处理中"态(数组多挂起并存,单 boolean 不够)。
|
||||
// reactive Set 的增删驱动按钮 disabled;Set 判定 O(1)。
|
||||
const actingIds = reactive(new Set<string>())
|
||||
|
||||
// M8:卡片根元素 ref 数组(v-for ref 收集)。挂起弹窗出现在输入区上方,常被消息流/进度条挤出视口,
|
||||
// 用户看不到按钮 → 挂起无声卡死。新增项时滚动定位到卡片让审批入口立即可见。
|
||||
const dirAuthRefs = ref<HTMLElement[]>([])
|
||||
|
||||
/** 点三选项之一:按 id 定位挂起项,调 ai_authorize_dir。后端 remove pending → 写授权/拒 →
|
||||
* execute → try_continue。不主动清 pendingDirAuths——由后端 AiApprovalResult/AiCompleted/AiError
|
||||
* 在 useAiEvents 内按 id filter / 清空。IPC 失败回滚 actingIds 让用户可重试。 */
|
||||
async function handleAuthorize(id: string, decision: 'once' | 'session' | 'always' | 'deny'): Promise<void> {
|
||||
if (actingIds.has(id)) return
|
||||
actingIds.add(id)
|
||||
// P1-e-c: deny 拒绝前二次确认(拒绝不可逆——工具返 Err、loop 续跑不再重试;与 ToolCard onDeny 同款)。
|
||||
// 放 actingIds.add 之后、IPC 之前;取消则删 actingIds 复位处理中态(保持 pending 可重新决策),不调用 IPC。
|
||||
if (decision === 'deny') {
|
||||
const ok = await confirmDialog(t('aiChat.dirAuthDenyConfirm'))
|
||||
if (!ok) {
|
||||
actingIds.delete(id)
|
||||
return
|
||||
}
|
||||
}
|
||||
try {
|
||||
await aiApi.authorizeDir(id, decision)
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e)
|
||||
emit('toast', { msg: t('aiChat.dirAuthFailed', { msg }), type: 'error' })
|
||||
actingIds.delete(id) // IPC 失败:复位该 id,允许重试
|
||||
return
|
||||
}
|
||||
// F-260620 根治(授权按钮永转圈):IPC 成功后 try_continue spawn 续跑 loop,续跑 loop 可能立刻又
|
||||
// 触发 AiDirAuthRequired(同 id 不可能,但其他 id 可能)置新 pendingDirAuths 项;该 id 的项会由
|
||||
// AiApprovalResult 按 id filter 移除。这里不依赖 pendingDirAuths 状态复位 actingIds——改为
|
||||
// watch pendingDirAuths,某 id 离开数组(已被后端消费)即删 actingIds,精准复位。
|
||||
}
|
||||
|
||||
/** pendingDirAuths 变化时,离开数组(已被后端 AiApprovalResult/AiCompleted/AiError 按 id 移除)的
|
||||
* id 从 actingIds 删,精准复位"处理中"态(数组化后多挂起,单 watch===null 模式不再适用)。 */
|
||||
watch(() => pendingDirAuths.value, (items) => {
|
||||
const liveIds = new Set(items.map(p => p.id))
|
||||
for (const id of [...actingIds]) {
|
||||
if (!liveIds.has(id)) actingIds.delete(id)
|
||||
}
|
||||
})
|
||||
|
||||
/** M8:visibleDirAuths 出现新挂起项时,scrollIntoView 让审批入口立刻可见(挂起弹窗在输入区上方,
|
||||
* 易被消息流/进度条挤出视口,用户不滚动看不到 → 挂起无声卡死)。仅新增项触发滚动,避免已可见项
|
||||
* 反复跳动打扰。 */
|
||||
watch(() => visibleDirAuths.value.map(d => d.id).join('|'), async () => {
|
||||
await nextTick()
|
||||
// 滚到最新(数组末)项,block:'center' 让按钮组居中可见
|
||||
const last = dirAuthRefs.value[dirAuthRefs.value.length - 1]
|
||||
if (last) last.scrollIntoView({ block: 'center', behavior: 'smooth' })
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ai-dir-auth {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
margin-bottom: 6px;
|
||||
padding: 6px 8px;
|
||||
background: color-mix(in srgb, var(--df-warning) 10%, transparent);
|
||||
border: 0.5px solid color-mix(in srgb, var(--df-warning) 40%, transparent);
|
||||
border-radius: var(--df-radius);
|
||||
}
|
||||
.ai-dir-auth-text {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--df-warning);
|
||||
}
|
||||
.ai-dir-auth-hint {
|
||||
font-size: 10px;
|
||||
color: var(--df-text-dim);
|
||||
opacity: 0.9;
|
||||
word-break: break-all;
|
||||
}
|
||||
.ai-dir-auth-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-top: 2px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.ai-dir-auth-btn {
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
padding: 3px 10px;
|
||||
border-radius: calc(var(--df-radius) - 2px);
|
||||
transition: filter 0.15s var(--df-ease);
|
||||
}
|
||||
.ai-dir-auth-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.ai-dir-auth-btn--once {
|
||||
background: var(--df-accent);
|
||||
color: #fff;
|
||||
}
|
||||
.ai-dir-auth-btn--once:hover:not(:disabled) { filter: brightness(1.1); }
|
||||
.ai-dir-auth-btn--session {
|
||||
background: color-mix(in srgb, var(--df-accent) 15%, transparent);
|
||||
color: var(--df-accent);
|
||||
border: 0.5px solid color-mix(in srgb, var(--df-accent) 40%, transparent);
|
||||
}
|
||||
.ai-dir-auth-btn--session:hover:not(:disabled) { filter: brightness(1.1); }
|
||||
.ai-dir-auth-btn--always {
|
||||
background: var(--df-warning);
|
||||
color: #000;
|
||||
}
|
||||
.ai-dir-auth-btn--always:hover:not(:disabled) { filter: brightness(1.1); }
|
||||
.ai-dir-auth-btn--deny {
|
||||
background: transparent;
|
||||
color: var(--df-text-dim);
|
||||
border: 0.5px solid var(--df-border);
|
||||
}
|
||||
.ai-dir-auth-btn--deny:hover:not(:disabled) {
|
||||
color: var(--df-text);
|
||||
border-color: var(--df-text-dim);
|
||||
}
|
||||
</style>
|
||||
@@ -5,7 +5,7 @@
|
||||
复用 MaxRoundsCard 模式:per-conv 守卫(pendingHelp.conversationId === active)+ 行为已记入本地 ref。
|
||||
显 reason(原因)+ context(最近错误 sample)+ options 按钮供用户选(换思路/授权路径/人工接管)。
|
||||
选 option → 仅关闭求助卡(后端 loop 已终止,用户重新发消息即"换思路/人工接管"入口,
|
||||
路径授权走 DirAuthDialog 既有链路)。机制优先 prompt 说教:代码强制熔断 + 结构化求助,
|
||||
机制优先 prompt 说教:代码强制熔断 + 结构化求助,
|
||||
非"教 AI 失败就问用户"(LLM 不可靠)。 -->
|
||||
<div v-if="helpActive" ref="helpRef" class="ai-help-required">
|
||||
<span class="ai-help-required-text">{{ helpActive.reason }}</span>
|
||||
@@ -64,7 +64,7 @@ watch(helpActive, async (p) => {
|
||||
|
||||
/** 选 option:关求助卡即可。后端 loop 已在 AiHelpRequired 时终止(guard.reset + return),
|
||||
* 用户选 option 后的"换思路/授权路径/人工接管"由用户重新发消息/操作触发(无独立 IPC)。
|
||||
* 点"授权路径"提示用户路径授权链路(DirAuthDialog 自动触发,无需此卡代发)。
|
||||
* 路径授权链路已退役(path_auth 2026-10-25),无需此卡代发。
|
||||
* 选后清 pendingHelp,卡片隐藏(对齐 MaxRoundsCard 点继续/停止后由事件清的语义,此处无续跑事件故立即清)。 */
|
||||
function handleSelectOption(opt: string): void {
|
||||
void opt // option 仅区分按钮文案,后端 loop 已终止无独立 IPC 续跑,选即清卡片
|
||||
|
||||
@@ -1782,7 +1782,7 @@ defineExpose({
|
||||
v-if="m.toolCalls && m.toolCalls.length > 0"
|
||||
ref="toolCardListRef"
|
||||
:toolCalls="m.toolCalls"
|
||||
@approve="({ id, approved, decision }) => store.approveToolCall(id, approved, decision)"
|
||||
@approve="({ id, approved }) => store.approveToolCall(id, approved)"
|
||||
@batch-approve="(decision) => store.batchApprove(decision)"
|
||||
@install-obscura="onInstallObscura"
|
||||
@pending-out-of-view="onPendingOutOfView"
|
||||
@@ -1817,7 +1817,7 @@ defineExpose({
|
||||
<div v-if="m.toolCalls && m.toolCalls.length > 0" class="ai-trace-round">
|
||||
<ToolCardList
|
||||
:toolCalls="m.toolCalls"
|
||||
@approve="({ id, approved, decision }) => store.approveToolCall(id, approved, decision)"
|
||||
@approve="({ id, approved }) => store.approveToolCall(id, approved)"
|
||||
@batch-approve="(decision) => store.batchApprove(decision)"
|
||||
@install-obscura="onInstallObscura"
|
||||
@pending-out-of-view="onPendingOutOfView"
|
||||
@@ -2124,13 +2124,11 @@ defineExpose({
|
||||
</div>
|
||||
|
||||
<!-- 工具调用卡片(渲染/折叠/审批全下沉到 ToolCardList+ToolCard 子组件,MessageList 仅转发审批) -->
|
||||
<!-- path_auth 审批链阶段3b:approve 事件透传 decision(path 类 once/always/deny),
|
||||
store.approveToolCall 据 decision 走 authorizeDir,缺省走 approve。 -->
|
||||
<ToolCardList
|
||||
v-if="item.msg.toolCalls && item.msg.toolCalls.length > 0"
|
||||
ref="toolCardListRef"
|
||||
:toolCalls="item.msg.toolCalls"
|
||||
@approve="({ id, approved, decision }) => store.approveToolCall(id, approved, decision)"
|
||||
@approve="({ id, approved }) => store.approveToolCall(id, approved)"
|
||||
@batch-approve="(decision) => store.batchApprove(decision)"
|
||||
@install-obscura="onInstallObscura"
|
||||
@pending-out-of-view="onPendingOutOfView"
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
// 无人值守边界配置卡(P1 总装简化):授权面收敛单一档——风险上限固定 Medium、
|
||||
// 目录白名单不开放(空=不额外扩权,全局 allowed_dirs 照常生效),破坏性命令恒禁止;
|
||||
// 目录白名单不开放(全局路径授权已退役为默认全放行,2026-10-25),破坏性命令恒禁止;
|
||||
// 可调项只剩预算两项(最大轮次/最长运行时长),各带「不限」勾选(0 直传,后端 0=不限)。
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
Reference in New Issue
Block a user