新增: 批次工作落地(推进链/评估闭环/事件总线/并发/加固) + 技术债清理 + 文档整理

后端:
- 工作流推进链(D-03):advance_task/状态机/闸门走 df-nodes Node trait,conditions 条件引擎扩展
- 想法评估闭环:启发式评分+对抗评估,df-ideas/scoring + df-storage/idea_eval_repo + idea 前端打通
- 全局事件数据总线:df-ai/context+context_helpers+augmentation 跨模块解耦
- AI planner/plan_hint/intent:aichat B 路线并行多轮基础
- patch_file 加固(TD-03/04):读改写整体锁防 lost update,expected_hash 合约闭环
- 压缩超时兜底(F-15 卡死根治)
- F-09 多会话并发:LlmConcurrency per-conv + streamingGuard 前端守护 + verify 脚本
- 知识注入 DRY/skills/audit 扩展

清理:
- aichat 技术债(误报 allow/死导入/过时注释 30 项)
- URGENT.md 删除(11 项加急全解决/迁 todo)
- 文档整理(todo/待决策/待审查/ARCHITECTURE/INDEX + 总线/技术债审查新文档)
This commit is contained in:
2026-06-21 20:51:26 +08:00
parent 330bb7f505
commit bd6a41fe6e
111 changed files with 11932 additions and 1034 deletions

View File

@@ -119,7 +119,7 @@ import { ref, computed, nextTick, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useAiStore } from '../../stores/ai'
import { useProjectStore } from '../../stores/project'
import type { AiMessage, ContentPart, SkillInfo, ProjectRecord, TaskRecord, IdeaRecord } from '../../api/types'
import type { AiMessage, ContentPart, SkillInfo, ProjectRecord, TaskRecord, IdeaRecord, MentionSpan } from '../../api/types'
const props = defineProps<{
/** UX-09: 编辑末条 user 消息态;父持有,handleSend 据此走 editMessage 而非 sendMessage */
@@ -252,17 +252,13 @@ const filteredSkills = computed(() => {
})
watch(inputText, (v) => {
if (pendingSkill.value) {
// 已选技能 chip 态不开任何联想浮层
skillOpen.value = false
mentionOpen.value = false
mentionStart.value = -1
return
}
// Input Augmentation: skill chip + @ 标记共存(两者正交 user intent)——删原 pendingSkill
// 非空时强制关 mention 的分支。skill chip 是独立 UI 态(已选技能),@ 是输入态(光标触发),
// 用户可同时持有已选技能并在文本里 @ 引用实体。
// / 技能联想(仅当输入以 / 开头且光标在首字符区)
skillOpen.value = v.startsWith('/')
if (skillOpen.value) skillIndex.value = 0
// UX-10: @ 实体引用(技能联想与 @ 联想互斥,二者同时仅一开)
// UX-10: @ 实体引用(技能联想与 @ 联想互斥,二者同时仅一开;视觉打架防护)
if (skillOpen.value) {
mentionOpen.value = false
mentionStart.value = -1
@@ -307,6 +303,8 @@ function detectMentionTrigger() {
function selectSkill(s: SkillInfo) {
pendingSkill.value = s
inputText.value = ''
// selectSkill 清空 inputText,pendingMentionSpans 指向的区间随之失效,一并清空防孤儿 span
pendingMentionSpans.value = []
skillOpen.value = false
mentionOpen.value = false
mentionStart.value = -1
@@ -338,6 +336,10 @@ const mentionOpen = ref(false)
const mentionIndex = ref(0)
// @ 触发符在 inputText 中的起始下标(用于选中后精确替换 @query 段)
const mentionStart = ref(-1)
// Input Augmentation: pending mention 区间元数据(selectMention 记录,handleSend 透传后清空)。
// 每个 span 描述用户气泡内一段 [类型: 名] 文本在 inputText 中的 {start,length} + kind/refId/label,
// 后端据 mentionSpans resolve 投影成 Augmentation 注入;MessageList 据此精确切区间渲染 chip。
const pendingMentionSpans = ref<MentionSpan[]>([])
const mentionGroupLabel = computed(() => ({
project: t('aiChat.mentionGroupProject'),
@@ -424,6 +426,16 @@ function selectMention(item: MentionItem) {
? `[${t('aiChat.mentionGroupTask')}: ${item.name}]`
: `[${t('aiChat.mentionGroupIdea')}: ${item.name}]`
inputText.value = before + label + ' ' + after
// Input Augmentation: 记 pending mention 区间(start=before.length,label 区间长度=label.length)。
// before 已包含此前所有插入的 [类型: 名] 标记,故 before.length 恰为本 label 在最终文本中的起点。
// refId 用实体 id(后端 resolve 用),label 即 chip 展示文本(== 后段校验用 == 文本)。
pendingMentionSpans.value.push({
start: before.length,
length: label.length,
kind: item.type,
refId: item.id,
label,
})
mentionOpen.value = false
mentionStart.value = -1
mentionIndex.value = 0
@@ -548,6 +560,15 @@ async function handleSend() {
// 后端 ai_chat_send IPC 当前不接 parts(Phase 2c 接入);本轮 parts 仅写本地 push 的
// user 消息渲染多模态图,后端请求仍走 content 文本路径。
const snapshotImgs = pendingImages.value.slice()
// Input Augmentation: 快照 pending mention 区间(同 parts 快照,失败时回填)。
// 关键:span.start 是相对原始 inputText(可能含前导空格)的偏移;text = inputText.trim()
// 已剥前导空白。若不修正偏移,trim 掉的前导空白会让所有 chip start 错位 → 渲染降级纯文本。
// 此处按剥掉的前导空白长度整体左移 start(尾部 trim 不影响已记录的 chip start;chip 不在尾部空白)。
const rawLeadingWs = inputText.value.length - inputText.value.replace(/^\s+/, '').length
const snapshotSpans = pendingMentionSpans.value.slice().map(sp => ({
...sp,
start: Math.max(0, sp.start - rawLeadingWs),
}))
const parts: ContentPart[] | undefined = snapshotImgs.length > 0
? [
// 文本片(非空时作首片,便于后端 2c 接入后 content 与 parts 文本一致;
@@ -573,6 +594,8 @@ async function handleSend() {
inputText.value = ''
pendingSkill.value = null
pendingImages.value = []
// Input Augmentation: 清空 mention 区间(与 inputText 清空同处,保证下条消息从空白态起步)
pendingMentionSpans.value = []
skillOpen.value = false
mentionOpen.value = false
mentionStart.value = -1
@@ -580,7 +603,9 @@ async function handleSend() {
// 同步清空输入框后立即发送sendMessage 内部同步 push 用户消息,
// 清空与 push 合并到同一渲染周期 flush消除"输入框已空但消息未显示"的间隙
try {
await store.sendMessage(text, skill?.name, false, parts)
// Input Augmentation: 透传 mentionSpans(L0 直接 doSend 注入 + 本地 user 消息挂 mentionSpans 渲染 chip;
// L1 入队续发当前不挂 spans,见 useAiSend.sendMessage 设计取舍注释)
await store.sendMessage(text, skill?.name, false, parts, snapshotSpans.length > 0 ? snapshotSpans : undefined)
} catch (e) {
console.error('[AI] 发送失败:', e)
inputText.value = text
@@ -589,6 +614,10 @@ async function handleSend() {
if (parts) {
pendingImages.value = snapshotImgs
}
// 发送失败:回填 mention 区间供重试(快照原样还原,区间仍与回填文本对齐)
if (snapshotSpans.length) {
pendingMentionSpans.value = snapshotSpans
}
// 用户可见提示(原仅 console.error,用户无感);Tauri IPC 错误是字符串非 Error 对象
const msg = e instanceof Error ? e.message : String(e)
emit('error', { msg: t('aiChat.toastSendFail', { msg }), type: 'error' })
@@ -625,6 +654,7 @@ function clearInput(): void {
inputText.value = ''
pendingSkill.value = null
pendingImages.value = []
pendingMentionSpans.value = []
skillOpen.value = false
mentionOpen.value = false
mentionStart.value = -1