①ai.ts MESSAGE_CAP=200 watch(messages.length) 拦截 push 超限 splice 最早条滚动淘汰(条件 oldLen>0 && newLen-oldLen<=2 规避切换对话整体替换大跳变/首次加载误截断),现有 composables push 逻辑零改动; ②knowledge.ts _itemsReqSeq/_configReqSeq 模块级竞态序号(loadList/loadCandidates/search/loadConfig 请求捕获 seq,响应回序号不匹配丢弃旧响应含错误分支,loading 仅最新请求复位); ③create/updateStatus/archive/saveConfig/extractNow 加 try/catch 统一塞 state.error(与 load*/search 一致,CR-28 Knowledge.vue banner 消费)。 批11 agent,vue-tsc 0
214 lines
7.0 KiB
TypeScript
214 lines
7.0 KiB
TypeScript
import { reactive, computed } from 'vue'
|
|
import { knowledgeApi } from '../api'
|
|
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 模式) ──
|
|
// 快速连点(切换筛选/连续检索/重连)时,旧响应晚到会覆盖新响应。序号自增,
|
|
// 请求发起时捕获序号,响应回时比对:非最新请求的响应直接丢弃,只取最新。
|
|
// items 序号覆盖 loadList/loadCandidates/search;candidates 与 items 共用同一队列
|
|
// (loadList/search 写 items,loadCandidates 写 candidates),故 items 序号也兜底 candidates。
|
|
let _itemsReqSeq = 0
|
|
let _configReqSeq = 0
|
|
|
|
// 7 种知识类型(snake_case,与后端 KnowledgeKind 对齐)
|
|
// 仅存 key + icon;展示文案(label)走 i18n: $t('knowledge.kind.<key>')
|
|
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): string[] {
|
|
if (!tags) return []
|
|
try {
|
|
const arr = JSON.parse(tags)
|
|
return Array.isArray(arr) ? arr : []
|
|
} catch {
|
|
return []
|
|
}
|
|
}
|
|
|
|
export function useKnowledgeStore() {
|
|
// ── 加载列表(默认排除 archived) ──
|
|
async function loadList(status?: string) {
|
|
const seq = ++_itemsReqSeq
|
|
state.loading = true
|
|
state.error = null
|
|
try {
|
|
const result = await knowledgeApi.list(status ?? null)
|
|
if (seq !== _itemsReqSeq) return // 旧响应丢弃,只取最新
|
|
state.items = result
|
|
} catch (e: any) {
|
|
if (seq !== _itemsReqSeq) return // 旧请求的失败不污染当前视图
|
|
state.error = e?.toString() ?? '加载知识库失败'
|
|
} finally {
|
|
if (seq === _itemsReqSeq) state.loading = false
|
|
}
|
|
}
|
|
|
|
// ── 加载审核收件箱(候选列表) ──
|
|
async function loadCandidates() {
|
|
const seq = ++_itemsReqSeq
|
|
try {
|
|
const result = await knowledgeApi.listCandidates()
|
|
if (seq !== _itemsReqSeq) return // 旧响应丢弃,只取最新
|
|
state.candidates = result
|
|
} catch (e: any) {
|
|
if (seq !== _itemsReqSeq) return
|
|
state.error = e?.toString() ?? '加载收件箱失败'
|
|
}
|
|
}
|
|
|
|
async function search(query: string, kind?: string) {
|
|
const seq = ++_itemsReqSeq
|
|
state.loading = true
|
|
state.error = null
|
|
try {
|
|
const result = await knowledgeApi.search({ query, kind })
|
|
if (seq !== _itemsReqSeq) return // 旧响应丢弃,只取最新
|
|
state.items = result
|
|
} catch (e: any) {
|
|
if (seq !== _itemsReqSeq) return
|
|
state.error = e?.toString() ?? '检索失败'
|
|
} finally {
|
|
if (seq === _itemsReqSeq) state.loading = false
|
|
}
|
|
}
|
|
|
|
async function create(input: CreateKnowledgeInput) {
|
|
try {
|
|
const record = await knowledgeApi.create(input)
|
|
// 手动录入默认 candidate,刷新收件箱。
|
|
// create 已成功返回 record,刷新失败不应污染成功语义 —— 直接走原始 API + 非阻塞 console.warn,
|
|
// 不经 loadCandidates(后者会写 state.error,污染 create 成功语义)。
|
|
try {
|
|
state.candidates = await knowledgeApi.listCandidates()
|
|
} catch (e: any) {
|
|
console.warn('create 后刷新收件箱失败(非阻塞)', e?.toString?.() ?? e)
|
|
}
|
|
return record
|
|
} catch (e: any) {
|
|
state.error = e?.toString() ?? '创建知识失败'
|
|
return null
|
|
}
|
|
}
|
|
|
|
async function updateStatus(id: string, status: string) {
|
|
try {
|
|
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)
|
|
} catch (e: any) {
|
|
state.error = e?.toString() ?? '更新状态失败'
|
|
}
|
|
}
|
|
|
|
async function archive(id: string) {
|
|
try {
|
|
await knowledgeApi.archive(id)
|
|
state.items = state.items.filter(k => k.id !== id)
|
|
state.candidates = state.candidates.filter(k => k.id !== id)
|
|
} catch (e: any) {
|
|
state.error = e?.toString() ?? '归档失败'
|
|
}
|
|
}
|
|
|
|
// ── 配置 ──
|
|
async function loadConfig() {
|
|
const seq = ++_configReqSeq
|
|
try {
|
|
const result = await knowledgeApi.getConfig()
|
|
if (seq !== _configReqSeq) return // 旧响应丢弃,只取最新
|
|
state.config = result
|
|
} catch (e: any) {
|
|
if (seq !== _configReqSeq) return
|
|
state.error = e?.toString() ?? '加载配置失败'
|
|
}
|
|
}
|
|
|
|
async function saveConfig(config: KnowledgeConfig) {
|
|
try {
|
|
await knowledgeApi.saveConfig(config)
|
|
state.config = config
|
|
} catch (e: any) {
|
|
state.error = e?.toString() ?? '保存配置失败'
|
|
}
|
|
}
|
|
|
|
async function extractNow() {
|
|
try {
|
|
await knowledgeApi.extractNow()
|
|
await loadCandidates()
|
|
} catch (e: any) {
|
|
state.error = e?.toString() ?? '立即抽取失败'
|
|
}
|
|
}
|
|
|
|
// ── 生命线:详情 / 编辑 / 事件查询(按需调用,不入全局 state) ──
|
|
async function getDetail(id: string): Promise<KnowledgeDetailPayload> {
|
|
return knowledgeApi.getDetail(id)
|
|
}
|
|
|
|
async function update(id: string, input: UpdateKnowledgeInput): Promise<KnowledgeRecord> {
|
|
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<KnowledgeEventRecord[]> {
|
|
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,
|
|
})
|
|
}
|