新增: 批次工作落地(推进链/评估闭环/事件总线/并发/加固) + 技术债清理 + 文档整理
后端: - 工作流推进链(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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user