diff --git a/crates/df-ai/src/context.rs b/crates/df-ai/src/context.rs index a201e71..6971fd9 100644 --- a/crates/df-ai/src/context.rs +++ b/crates/df-ai/src/context.rs @@ -278,8 +278,11 @@ impl ContextManager { /// 就地替换某条 tool_result 的内容(兼容审批 replace_tool_result) /// 返回 true 如果找到并替换了 + /// + /// 反向遍历:tool_result 由 append 进入历史,被替换的通常是最近的审批占位, + /// 从尾部查找命中即停,避免对长历史做正向 O(n) 累积扫描。 pub fn replace_tool_result_content(&mut self, tool_call_id: &str, new_content: &str) -> bool { - let pos = self.messages.iter().position(|t| { + let pos = self.messages.iter().rposition(|t| { matches!(t.message.role, MessageRole::Tool) && t.message.tool_call_id.as_deref() == Some(tool_call_id) }); diff --git a/crates/df-storage/src/crud.rs b/crates/df-storage/src/crud.rs index 0748262..aab1fcd 100644 --- a/crates/df-storage/src/crud.rs +++ b/crates/df-storage/src/crud.rs @@ -1174,13 +1174,29 @@ impl KnowledgeRepo { /// /// 返回 (记录, 相似度分数)。数据量 <50k 时暴力遍历 <50ms,够用; /// 更大规模再升 sqlite-vec HNSW(结果不变,只提速)。 + /// + /// 列限定: 显式列出所需列(与 search/list_by_status/top_used 一致),避免 SELECT * + /// 拉到未知新增列;embedding 单独取(不入 KnowledgeRecord)。 + /// + /// TODO(性能,低优先): 调用方(hybrid_search→merge_hybrid_results→build_knowledge_context) + /// 实际只消费 id/kind/title/content/reuse_count;reasoning(AI 生成大文本)、tags、 + /// source_project/source_ref 等元字段未被使用却仍随每行读出。真正省 IO 需返回精简结构 + /// (如 KnowledgeVectorHit { id, kind, title, content, reuse_count })替换返回类型, + /// 但这会改变 search_vector 签名与 merge_hybrid_results 调用契约——当前保守不动, + /// 待向量检索量级或 reasoning 文本体积成为瓶颈再单独立项。SELECT 列化本身不省字段, + /// 仅消除 SELECT * 的隐式依赖与未知列风险。 pub async fn search_vector(&self, query_vec: &[f32], limit: usize) -> Result> { let conn = self.conn.clone(); let query_vec = query_vec.to_vec(); tokio::task::spawn_blocking(move || { let guard = conn.blocking_lock(); + // 显式列: 14 个 KnowledgeRecord 字段 + embedding(余弦计算用) + const COLS: &str = "id,kind,title,content,tags,status,confidence,reuse_count,verified,\ + source_project,source_ref,reasoning,created_at,updated_at,embedding"; let mut stmt = guard - .prepare("SELECT * FROM knowledges WHERE status = 'published' AND embedding IS NOT NULL") + .prepare(&format!( + "SELECT {COLS} FROM knowledges WHERE status = 'published' AND embedding IS NOT NULL" + )) .map_err(|e| Error::Storage(e.to_string()))?; let rows = stmt .query_map([], |row| { diff --git a/src/composables/useConfirm.ts b/src/composables/useConfirm.ts new file mode 100644 index 0000000..71ab34a --- /dev/null +++ b/src/composables/useConfirm.ts @@ -0,0 +1,50 @@ +//! 统一确认弹层状态机 — 抽自 Projects/ProjectDetail/Ideas/Settings 四处重复 +//! +//! 背景:原生 window.confirm 在 Tauri webview 带 localhost:port 来源信息无法去除, +//! 改为自建弹层。四视图各自重复一份 {visible,msg,resolve} + Promise 机制,此处收敛。 +//! +//! 分工:本 composable 只管「状态 + Promise」,不渲染 UI —— +//! - Projects/ProjectDetail/Ideas 复用 ../components/ConfirmDialog.vue +//! - Settings 用内联模板(沿用其既有 toast/confirm 同框布局) +//! 各视图绑定 confirmState 的 visible/msg 并把按钮点击回 answerConfirm 即可。 +//! +//! i18n:composable 在 setup 内调用,文案由调用方用各自 useI18n().t 翻译后传入 msg, +//! 这里无需 i18n 依赖。dangerLabel 由 ConfirmDialog 组件侧兜底为 common.delete。 + +import { ref } from 'vue' + +export interface ConfirmState { + visible: boolean + msg: string + resolve: null | ((v: boolean) => void) +} + +/** 弹出确认对话框,返回 Promise;resolve(true) 表示用户确认,resolve(false) 表示取消 */ +export type ConfirmFn = (msg: string) => Promise + +/** 关闭弹层并回传结果 */ +export type AnswerFn = (ok: boolean) => void + +export function useConfirm() { + const confirmState = ref({ + visible: false, + msg: '', + resolve: null, + }) + + function confirmDialog(msg: string): Promise { + return new Promise(resolve => { + confirmState.value.msg = msg + confirmState.value.visible = true + confirmState.value.resolve = resolve + }) + } + + function answerConfirm(ok: boolean) { + confirmState.value.visible = false + confirmState.value.resolve?.(ok) + confirmState.value.resolve = null + } + + return { confirmState, confirmDialog, answerConfirm } +} diff --git a/src/views/Ideas.vue b/src/views/Ideas.vue index 76cf4ec..284f2df 100644 --- a/src/views/Ideas.vue +++ b/src/views/Ideas.vue @@ -207,26 +207,15 @@ import { Message } from '@arco-design/web-vue' import { useProjectStore } from '../stores/project' import { formatDate } from '../utils/time' import ConfirmDialog from '../components/ConfirmDialog.vue' +import { useConfirm } from '../composables/useConfirm' import type { IdeaRecord } from '../api/types' const { t } = useI18n() const router = useRouter() const store = useProjectStore() -// ── 确认弹层(替代原生 window.confirm,后者在 Tauri webview 带 localhost 来源信息无法去除) ── -const confirmState = ref({ visible: false, msg: '', resolve: null as null | ((v: boolean) => void) }) -function confirmDialog(msg: string): Promise { - return new Promise(resolve => { - confirmState.value.msg = msg - confirmState.value.visible = true - confirmState.value.resolve = resolve - }) -} -function answerConfirm(ok: boolean) { - confirmState.value.visible = false - confirmState.value.resolve?.(ok) - confirmState.value.resolve = null -} +// 确认弹层状态机抽至 composables/useConfirm(原 4 视图重复:Projects/ProjectDetail/Ideas/Settings) +const { confirmState, confirmDialog, answerConfirm } = useConfirm() type FilterKey = 'all' | 'hot' | 'pending' | 'promoted' diff --git a/src/views/ProjectDetail.vue b/src/views/ProjectDetail.vue index 0aba95b..7819f4d 100644 --- a/src/views/ProjectDetail.vue +++ b/src/views/ProjectDetail.vue @@ -213,6 +213,7 @@ import { formatDate } from '../utils/time' import { parseStack } from '../utils/project' import { projectStatusLabel, projectStageInfo, taskStatusLabel, taskStatusClass } from '../constants/project' import ConfirmDialog from '../components/ConfirmDialog.vue' +import { useConfirm } from '../composables/useConfirm' const route = useRoute() const router = useRouter() @@ -222,20 +223,8 @@ const logListRef = ref(null) const showApprovalDialog = ref(false) let unlisten: (() => void) | null = null -// ── 确认弹层(替代原生 window.confirm,后者在 Tauri webview 带 localhost 来源信息无法去除) ── -const confirmState = ref({ visible: false, msg: '', resolve: null as null | ((v: boolean) => void) }) -function confirmDialog(msg: string): Promise { - return new Promise(resolve => { - confirmState.value.msg = msg - confirmState.value.visible = true - confirmState.value.resolve = resolve - }) -} -function answerConfirm(ok: boolean) { - confirmState.value.visible = false - confirmState.value.resolve?.(ok) - confirmState.value.resolve = null -} +// 确认弹层状态机抽至 composables/useConfirm(原 4 视图重复:Projects/ProjectDetail/Ideas/Settings) +const { confirmState, confirmDialog, answerConfirm } = useConfirm() // ── 阶段定义(labelKey 对应 i18n projectDetail.stage*) ── const stages = [ diff --git a/src/views/Projects.vue b/src/views/Projects.vue index 7217348..89ac9c0 100644 --- a/src/views/Projects.vue +++ b/src/views/Projects.vue @@ -124,6 +124,7 @@ import { formatDate } from '../utils/time' import { parseStack } from '../utils/project' import { projectStatusLabel as statusLabel, projectBadgeClass as stageClass } from '../constants/project' import ConfirmDialog from '../components/ConfirmDialog.vue' +import { useConfirm } from '../composables/useConfirm' import type { ProjectRecord } from '../api/types' const router = useRouter() @@ -133,20 +134,8 @@ const { t } = useI18n() // 状态映射统一走 ../constants/project(与 ProjectDetail/Tasks/Dashboard 一致) // formatDate 由 ../utils/time 提供(统一毫秒字符串解析,根治 Invalid Date) -// ── 确认弹层(替代原生 window.confirm,后者在 Tauri webview 带 localhost 来源信息无法去除) ── -const confirmState = ref({ visible: false, msg: '', resolve: null as null | ((v: boolean) => void) }) -function confirmDialog(msg: string): Promise { - return new Promise(resolve => { - confirmState.value.msg = msg - confirmState.value.visible = true - confirmState.value.resolve = resolve - }) -} -function answerConfirm(ok: boolean) { - confirmState.value.visible = false - confirmState.value.resolve?.(ok) - confirmState.value.resolve = null -} +// 确认弹层状态机抽至 composables/useConfirm(原 4 视图重复:Projects/ProjectDetail/Ideas/Settings) +const { confirmState, confirmDialog, answerConfirm } = useConfirm() // ── 新建项目 ── const showCreateModal = ref(false) diff --git a/src/views/Settings.vue b/src/views/Settings.vue index d8a2ebf..271767a 100644 --- a/src/views/Settings.vue +++ b/src/views/Settings.vue @@ -373,6 +373,7 @@ import { reactive, ref, computed, watch, onMounted, onUnmounted } from 'vue' import { useI18n } from 'vue-i18n' import { aiApi, knowledgeApi } from '../api' import { useAppSettingsStore } from '../stores/appSettings' +import { useConfirm } from '../composables/useConfirm' import type { AiProviderConfig } from '../api/types' import i18n from '../i18n' @@ -399,24 +400,9 @@ function showToast(msg: string, type: 'error' | 'warning' | 'info' = 'info') { _toastTimer = setTimeout(() => { toast.visible = false }, 3000) } -// 自建确认弹层(替代 window.confirm —— 后者带 webview 来源域名/端口,无法去除) -const confirmState = reactive({ - visible: false, - msg: '', - resolve: null as null | ((v: boolean) => void), -}) -function confirmDialog(msg: string): Promise { - return new Promise(resolve => { - confirmState.msg = msg - confirmState.visible = true - confirmState.resolve = resolve - }) -} -function answerConfirm(ok: boolean) { - confirmState.visible = false - confirmState.resolve?.(ok) - confirmState.resolve = null -} +// 确认弹层状态机抽至 composables/useConfirm(原 4 视图重复:Projects/ProjectDetail/Ideas/Settings) +// 模板 confirmState.visible/msg 在