import { reactive, computed } from 'vue' import { knowledgeApi } from '@/api' import { t } from '@/i18n/i18n-helpers' import { runWithCatch, runWithCatchGuarded } from '@/composables/useStoreAction' import { parseJsonArray } from '@/utils/json' import type { KnowledgeRecord, KnowledgeDetailPayload, KnowledgeEventRecord, CreateKnowledgeInput, UpdateKnowledgeInput, KnowledgeConfig, } from '@/api/types' // ── 全局响应式状态(单例) ── const state = reactive({ items: [] as KnowledgeRecord[], candidates: [] as KnowledgeRecord[], config: null as KnowledgeConfig | null, loading: false, error: null as string | null, }) // ── 异步竞态保护(请求序号,类似 ai.ts messages 的 switch token 模式) ── // 快速连点(切换筛选/连续检索/重连)时,旧响应晚到会覆盖新响应。序号自增, // 请求发起时捕获序号,响应回时比对:非最新请求的响应直接丢弃,只取最新。 // 拆两套序号:loadList/search 都写 state.items 共用 _itemsSeq;loadCandidates 写 // state.candidates 用独立 _candidatesSeq。各自防竞态,互不丢弃。 // (P1 修复:原先三者共用 _itemsReqSeq,onMounted 并发 loadList+loadCandidates 会 // 让后发起的 loadCandidates ++ 覆盖 loadList 的 seq,导致 loadList 响应被误判为 // 旧响应丢弃,首次进入列表必空。) let _itemsSeq = 0 let _candidatesSeq = 0 let _configReqSeq = 0 // 7 种知识类型(snake_case,与后端 KnowledgeKind 对齐) // 仅存 key + icon;展示文案(label)走 i18n: $t('knowledge.kind.') export const KNOWLEDGE_KINDS = [ { key: 'review_rule', icon: '🔍' }, { key: 'prompt_template', icon: '💬' }, { key: 'pitfall', icon: '⚠️' }, { key: 'architecture_pattern', icon: '🏛️' }, { key: 'diagnosis', icon: '🩺' }, { key: 'deployment_note', icon: '🚀' }, { key: 'workflow_optimization', icon: '⚡' }, ] as const /** 解析 tags JSON 字符串为 string[] */ export function parseTags(tags: string | null | undefined): string[] { return parseJsonArray(tags) } // ── 知识 label 文案辅助(列表 Knowledge.vue 与详情 KnowledgeDetail.vue 共用) ── // 原两文件各内联一份重复实现(SW-260618-20 详情迁移时复制),提取至此单一来源。 // 全部依赖 i18n,故用模块级 t(同文件 parseTags 之上的全局 t)。 /** 知识类型文案(无 icon):${icon} ${text} 组合的文本部分,亦供下拉选项 */ export function kindText(key: string): string { return t(`knowledge.kind.${key}`) } /** 知识类型带 icon 文案(列表/详情卡片);未知 key 回退原值 */ export function kindLabel(key: string): string { const k = KNOWLEDGE_KINDS.find(c => c.key === key) return k ? `${k.icon} ${kindText(key)}` : key } /** 知识类型 icon;未知回退通用图标 */ export function kindIcon(key: string): string { return KNOWLEDGE_KINDS.find(c => c.key === key)?.icon ?? '📄' } /** 知识状态文案;$t(key) 命中失败(无该 status key)回退原值,避免裸露 key */ export function knowledgeStatusLabel(status: string): string { const key = `knowledge.status.${status}` const msg = t(key) return msg === key ? status : msg } /** 知识置信度文案;$t(key) 命中失败回退原值 */ export function knowledgeConfidenceLabel(c: string): string { const key = `knowledge.confidence.${c}` const msg = t(key) return msg === key ? c : msg } export function useKnowledgeStore() { // ── 加载列表(library:默认仅 published,F-260616-02 决策 a) ── // 后端 knowledge_list 默认收窄为 published,无需前端传 status。 // 显式 status 仍透传(如 status='archived' 查归档)。 async function loadList(status?: string) { const seq = ++_itemsSeq state.loading = true state.error = null try { const result = await knowledgeApi.list(status ?? null) if (seq !== _itemsSeq) return // 旧响应丢弃,只取最新 state.items = result } catch (e: any) { if (seq !== _itemsSeq) return // 旧请求的失败不污染当前视图 state.error = e?.toString() ?? t('knowledge.err.loadFailed') } finally { if (seq === _itemsSeq) state.loading = false } } // ── 加载待处理收件箱(candidate + pending_review,F-260616-02 决策 a) ── // 后端 knowledge_list_candidates 已收两种待处理状态,前端调用不变。 async function loadCandidates() { const seq = ++_candidatesSeq try { const result = await knowledgeApi.listCandidates() if (seq !== _candidatesSeq) return // 旧响应丢弃,只取最新 state.candidates = result } catch (e: any) { if (seq !== _candidatesSeq) return state.error = e?.toString() ?? t('knowledge.err.loadInboxFailed') } } async function search(query: string, kind?: string) { const seq = ++_itemsSeq state.loading = true state.error = null try { const result = await knowledgeApi.search({ query, kind }) if (seq !== _itemsSeq) return // 旧响应丢弃,只取最新 state.items = result } catch (e: any) { if (seq !== _itemsSeq) return state.error = e?.toString() ?? t('knowledge.err.searchFailed') } finally { if (seq === _itemsSeq) state.loading = false } } async function create(input: CreateKnowledgeInput) { const record = await runWithCatch(state, t('knowledge.err.createFailed'), async () => { const r = await knowledgeApi.create(input) // 手动录入默认 candidate,刷新收件箱。刷新失败不污染 create 成功语义(独立 console.warn)。 try { state.candidates = await knowledgeApi.listCandidates() } catch (e: unknown) { console.warn('create 后刷新收件箱失败(非阻塞)', e) } return r }) return record ?? null } async function updateStatus(id: string, status: string) { await runWithCatch(state, t('knowledge.err.updateStatusFailed'), async () => { await knowledgeApi.updateStatus(id, status) // 从 items 和 candidates 中同步移除 state.items = state.items.filter(k => k.id !== id) state.candidates = state.candidates.filter(k => k.id !== id) }) } async function archive(id: string) { await runWithCatch(state, t('knowledge.err.archiveFailed'), async () => { await knowledgeApi.archive(id) state.items = state.items.filter(k => k.id !== id) state.candidates = state.candidates.filter(k => k.id !== id) }) } // ── 配置 ── async function loadConfig() { const seq = ++_configReqSeq await runWithCatchGuarded(state, t('knowledge.err.loadConfigFailed'), () => seq === _configReqSeq, async () => { const result = await knowledgeApi.getConfig() if (seq !== _configReqSeq) return // 旧响应丢弃 state.config = result }) } async function saveConfig(config: KnowledgeConfig) { await runWithCatch(state, t('knowledge.err.saveConfigFailed'), async () => { await knowledgeApi.saveConfig(config) state.config = config }) } async function extractNow() { await runWithCatch(state, t('knowledge.err.extractFailed'), async () => { await knowledgeApi.extractNow() await loadCandidates() }) } // ── 生命线:详情 / 编辑 / 事件查询(按需调用,不入全局 state) ── async function getDetail(id: string): Promise { return knowledgeApi.getDetail(id) } async function update(id: string, input: UpdateKnowledgeInput): Promise { const updated = await knowledgeApi.update(id, input) // 同步刷新内存中的列表/收件箱条目 const patch = (arr: KnowledgeRecord[]) => { const idx = arr.findIndex(k => k.id === id) if (idx >= 0) arr[idx] = { ...arr[idx], ...updated } } patch(state.items) patch(state.candidates) return updated } async function getEvents(knowledgeId: string, eventType?: string, limit?: number): Promise { return knowledgeApi.events(knowledgeId, eventType, limit) } // 候选数量(供侧栏 badge 用) const candidateCount = computed(() => state.candidates.length) // 清除错误条(供 Knowledge.vue error-banner 关闭按钮调用) function clearError() { state.error = null } return reactive({ // 状态用 getter 实时读 state(防快照不跟随,同 project store 模式) get items() { return state.items }, get candidates() { return state.candidates }, get config() { return state.config }, get loading() { return state.loading }, get error() { return state.error }, candidateCount, // actions loadList, loadCandidates, search, create, updateStatus, archive, loadConfig, saveConfig, extractNow, getDetail, update, getEvents, clearError, }) }