修复: 安全加固+DRY 收敛+文档同步+测试补齐
安全: - ScriptNode 默认黑名单兜底(rm/del/format/shutdown/mkfs/dd) - bind_directory 分段 .. 检测替代 contains 子串(对齐 tool_registry) - ai_providers 白名单移除 api_key(防 update_field 旁路写明文) DRY: - useAiEvents 抽 cleanupTerminatedConversation 统一三分支收尾 - 新增 useStoreAction 工具,4 个 store 替换 38 处 try/catch 样板 文档: - df-core → df-types 批量替换(ARCHITECTURE/PROGRESS/SQLite-CRUD) - INDEX 补齐 9 漏列文档(单对话并行多轮/跑题试验/工程系统设计等) - Agent架构说明 死链修复(../构想审查/) - AI对话引擎工具清单改为数量+按风险分组(不再用固定数字) - ARCH 状态标签 设计阶段 → Phase 2 验证 测试: - df-relay 新增 registry_test: ConnRegistry 路由 + RelayState + 16 项单测
This commit is contained in:
@@ -565,33 +565,59 @@ function handleUserMessageEvent(event: AiChatEvent): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 公共会话终止收尾(任务 #6 DRY 抽离):AiCompleted / AiError / AiHelpRequired
|
||||
* 三分支共用同一套清场逻辑——看门狗/计时器/流式态/currentText/agentRound/per-conv
|
||||
* 挂起(localStorage 快照 + pendingMaxRounds + pendingDirAuths + convStates)。
|
||||
*
|
||||
* 语义差异(调用方自行处理):
|
||||
* - AiCompleted: 调用后追加 incomplete 气泡 / token 用量 / 队列 drain
|
||||
* - AiError: 调用后追加错误气泡 + clearAllApprovalTimers + 清 queue/pendingApprovals
|
||||
* - AiHelpRequired: 调用后翻 pendingHelp 驱动求助卡
|
||||
*/
|
||||
function cleanupTerminatedConversation(convId: string, reason: 'AiCompleted' | 'AiError' | 'AiHelpRequired') {
|
||||
clearStreamWatchdog(convId || undefined)
|
||||
clearAllToolSlowTimers() // B-260616-12: 整轮/错误/求助结束清全部工具慢执行计时器与已提示集合
|
||||
flushCurrentText()
|
||||
state.currentText = ''
|
||||
setStreaming(false, { convId: convId || null, reason })
|
||||
state.agentRound = 0 // AE-2025-07: agentic 结束/中断/求助,复位轮次(隐藏进度条)
|
||||
|
||||
// 批4 双轨收口:收尾收敛 convStates(AiCompleted/AiHelpRequired 删项→idle,AiError 置 error)。
|
||||
// 后端 CONV_STATE_ENABLED=on 时 AiConvStateChanged 已先到此处幂等 no-op;
|
||||
// off 或老后端无 AiConvStateChanged 时此处保证 convStates 不陈旧。
|
||||
if (reason === 'AiError') {
|
||||
if (convId) convStates.set(convId, 'error') // 保留 error 项供停止按钮显「重试」态
|
||||
} else {
|
||||
convStates.delete(convId)
|
||||
}
|
||||
|
||||
// TD-260621-02 per-conv:仅当 pendingMaxRounds===本 conv 才清 null(避免清其他会话的挂起)。
|
||||
if (pendingMaxRounds.value && pendingMaxRounds.value === convId) {
|
||||
pendingMaxRounds.value = null
|
||||
}
|
||||
// L1 求助协议(§2.3):求助卡挂起仅清本 conv(终止后旧求助卡应消失)。
|
||||
if (pendingHelp.value && pendingHelp.value.conversationId === convId) {
|
||||
pendingHelp.value = null
|
||||
}
|
||||
// TD-260621-03 per-conv:仅清本 conv 的 path_auth 挂起(终止只清本会话弹窗,不连累并发会话)。
|
||||
// F-09 多会话并发下全清会让 B 会话的 DirAuthDialog 凭空消失(用户报"弹窗没了我没操作")。
|
||||
pendingDirAuths.value = pendingDirAuths.value.filter(p => !p.conversationId || p.conversationId === convId)
|
||||
|
||||
// F-09: 清理分离窗口生成态快照(per-conv key,清本会话快照;兼容旧单 key)
|
||||
if (convId) {
|
||||
localStorage.removeItem(`df-ai-gen-${convId}`)
|
||||
localStorage.removeItem(`df-ai-text-${convId}`)
|
||||
}
|
||||
localStorage.removeItem('df-ai-gen')
|
||||
localStorage.removeItem('df-ai-text')
|
||||
}
|
||||
|
||||
/** lifecycle 域:整轮收尾(AiCompleted)与异常中断(AiError) */
|
||||
function handleLifecycleEvent(event: AiChatEvent): boolean {
|
||||
switch (event.type) {
|
||||
case 'AiCompleted': {
|
||||
clearStreamWatchdog(event.conversation_id || undefined)
|
||||
clearAllToolSlowTimers() // B-260616-12: 整轮结束清全部工具慢执行计时器与已提示集合
|
||||
flushCurrentText()
|
||||
state.currentText = ''
|
||||
setStreaming(false, { convId: event.conversation_id || null, reason: 'AiCompleted' })
|
||||
// 批4 双轨收口:AiCompleted 收尾收敛 convStates→idle(删 Map 项,enum 单一真相源)。
|
||||
// 后端 CONV_STATE_ENABLED=on 时 AiConvStateChanged{idle} 已先到此处幂等 no-op;
|
||||
// off 或老后端无 AiConvStateChanged 时此处保证 convStates 不陈旧。
|
||||
convStates.delete(event.conversation_id || '')
|
||||
state.agentRound = 0 // AE-2025-07: agentic 结束,复位轮次(隐藏进度条)
|
||||
// TD-260621-02 per-conv:仅当 pendingMaxRounds===本 conv 才清 null(避免清其他会话的挂起)。
|
||||
if (pendingMaxRounds.value && pendingMaxRounds.value === (event.conversation_id || '')) {
|
||||
pendingMaxRounds.value = null
|
||||
}
|
||||
// L1 求助协议(§2.3):求助卡挂起仅清本 conv(新一轮发送时旧求助卡应消失)。
|
||||
if (pendingHelp.value && pendingHelp.value.conversationId === (event.conversation_id || '')) {
|
||||
pendingHelp.value = null
|
||||
}
|
||||
// TD-260621-03 per-conv:仅清本 conv 的 path_auth 挂起(其他会话的弹窗不连累清空)。
|
||||
// 批2-A 刚把 pendingMaxRounds per-conv,pendingDirAuths 漏跟;此处补齐——
|
||||
// F-09 多会话并发下全清会让 B 会话的 DirAuthDialog 凭空消失(用户报"弹窗没了我没操作")。
|
||||
const doneConv2 = event.conversation_id || ''
|
||||
pendingDirAuths.value = pendingDirAuths.value.filter(p => !p.conversationId || p.conversationId === doneConv2)
|
||||
cleanupTerminatedConversation(event.conversation_id || '', 'AiCompleted')
|
||||
// UX-2025-04 / CR-30-2 / 决策 a1: 流中途失败保文——后端 emit AiCompleted(incomplete=true),
|
||||
// 前端追加系统提示气泡(镜像后端 session.messages 的 system 提示)。
|
||||
// 注:此系统提示仅前端展示,后端已独立 push 到 session.messages 落库。
|
||||
@@ -624,15 +650,6 @@ function handleLifecycleEvent(event: AiChatEvent): boolean {
|
||||
state.convTokenTotal = { prompt: event.prompt_tokens, completion: event.completion_tokens, total: event.total_tokens }
|
||||
}
|
||||
}
|
||||
// 清理分离窗口生成态快照(F-09:per-conv key,清本会话快照;兼容旧单 key)
|
||||
const doneConv = event.conversation_id || ''
|
||||
if (doneConv) {
|
||||
localStorage.removeItem(`df-ai-gen-${doneConv}`)
|
||||
localStorage.removeItem(`df-ai-text-${doneConv}`)
|
||||
}
|
||||
localStorage.removeItem('df-ai-gen')
|
||||
localStorage.removeItem('df-ai-text')
|
||||
void loadConversations()
|
||||
notifyConversationChanged()
|
||||
// 队列续发:当前完成后自动发下一条(经事件总线桥接,避免 import useAiSend 构成循环依赖)
|
||||
emit('ai-drain-queue', {})
|
||||
@@ -640,42 +657,12 @@ function handleLifecycleEvent(event: AiChatEvent): boolean {
|
||||
}
|
||||
|
||||
case 'AiError': {
|
||||
clearStreamWatchdog(event.conversation_id || undefined)
|
||||
clearAllToolSlowTimers() // B-260616-12: 错误收尾清全部工具慢执行计时器与已提示集合
|
||||
cleanupTerminatedConversation(event.conversation_id || '', 'AiError')
|
||||
clearAllApprovalTimers() // UX-260617-10: 错误中断释放所有审批超时计时器,防回调改 state 触发已卸载/已错流程
|
||||
// UX-260619-06: 失败前先把流式累积的 currentText 回填到占位 assistant 气泡,
|
||||
// 保留"回答到一半"的部分回复(否则清 currentText 后部分内容消失)。
|
||||
flushCurrentText()
|
||||
setStreaming(false, { convId: event.conversation_id || null, reason: 'AiError' })
|
||||
// 批4 双轨收口:AiError 收尾将 convStates 显式置 error(保留 error 项,供停止按钮显"重试"态)。
|
||||
// 与 AiConvStateChanged{error} 双写幂等(后端 on 时状态事件已先到;off/老后端时此处保证 Error 态可见)。
|
||||
// error 态在新会话发送(首活跃事件到达兜底写 generating)或下次 AiCompleted 时自然收敛。
|
||||
if (event.conversation_id) convStates.set(event.conversation_id, 'error')
|
||||
state.currentText = ''
|
||||
state.agentRound = 0 // AE-2025-07: agentic 异常中断,复位轮次
|
||||
state.queue = [] // B-32:错误收尾清队列,防生成中入队的消息被静默丢失(drainQueue 仅 AiCompleted 触发)
|
||||
// UX-260617-10: 错误收尾清残留待审批项——错误发生时若有工具停在 pending_approval,
|
||||
// 残留可点击审批按钮会让用户误以为还能批(实际后端已终止),残留审批卡误导操作。
|
||||
state.pendingApprovals = []
|
||||
// TD-260621-02 per-conv:仅当 pendingMaxRounds===本 conv 才清 null(避免清其他会话的挂起)。
|
||||
if (pendingMaxRounds.value && pendingMaxRounds.value === (event.conversation_id || '')) {
|
||||
pendingMaxRounds.value = null
|
||||
}
|
||||
// L1 求助协议(§2.3):求助卡挂起仅清本 conv(错误中断后旧求助卡应消失)。
|
||||
if (pendingHelp.value && pendingHelp.value.conversationId === (event.conversation_id || '')) {
|
||||
pendingHelp.value = null
|
||||
}
|
||||
// TD-260621-03 per-conv:仅清本 conv 的 path_auth 挂起(异常中断只清本会话弹窗,不连累并发会话)。
|
||||
const errConv2 = event.conversation_id || ''
|
||||
pendingDirAuths.value = pendingDirAuths.value.filter(p => !p.conversationId || p.conversationId === errConv2)
|
||||
// F-09: 清理分离窗口生成态快照(per-conv key,清本会话快照;兼容旧单 key)
|
||||
const errConv = event.conversation_id || ''
|
||||
if (errConv) {
|
||||
localStorage.removeItem(`df-ai-gen-${errConv}`)
|
||||
localStorage.removeItem(`df-ai-text-${errConv}`)
|
||||
}
|
||||
localStorage.removeItem('df-ai-gen')
|
||||
localStorage.removeItem('df-ai-text')
|
||||
// UX-03: 错误消息携带 error_type(供错误气泡差异化显隐「去设置」按钮)。
|
||||
// AiMessage 类型未含 errorType 字段(不在本批白名单),用对象字面量 + cast 扩展;
|
||||
// 消费方(AiChat.vue canOpenSettings)经同 cast 读取,类型闭环在两端,不污染 types.ts。
|
||||
@@ -698,32 +685,9 @@ function handleLifecycleEvent(event: AiChatEvent): boolean {
|
||||
// 前端按终止态收尾(对齐 AiError 分支:清看门狗/流式态/快照/队尾文本 flushCurrentText),
|
||||
// 但不创建错误气泡(求助非错误,是 AI 主动求助)——改为翻 pendingHelp 驱动求助卡(HelpRequiredCard)
|
||||
// 显 reason + options 按钮供用户选。
|
||||
clearStreamWatchdog(event.conversation_id || undefined)
|
||||
clearAllToolSlowTimers()
|
||||
flushCurrentText()
|
||||
setStreaming(false, { convId: event.conversation_id || null, reason: 'AiHelpRequired' })
|
||||
// 批4 双轨收口:求助即终止 loop,收敛 convStates(删 Map 项回 null/不在生成)。
|
||||
// HelpRequiredCard 守卫仅判 pendingHelp.conversationId===active,不读 conv_state,删项安全。
|
||||
convStates.delete(event.conversation_id || '')
|
||||
state.currentText = ''
|
||||
state.agentRound = 0
|
||||
cleanupTerminatedConversation(event.conversation_id || '', 'AiHelpRequired')
|
||||
// 求助即终止 loop,清残留待审批项(对齐 AiError 分支语义,防残留审批卡误导)。
|
||||
state.pendingApprovals = []
|
||||
// TD-260621-02 per-conv:仅当 pendingMaxRounds===本 conv 才清 null(求助终止同理清达 max 挂起)。
|
||||
if (pendingMaxRounds.value && pendingMaxRounds.value === (event.conversation_id || '')) {
|
||||
pendingMaxRounds.value = null
|
||||
}
|
||||
// per-conv 清本 conv path_auth 挂起(求助终止只清本会话弹窗,不连累并发会话)。
|
||||
const helpConv2 = event.conversation_id || ''
|
||||
pendingDirAuths.value = pendingDirAuths.value.filter(p => !p.conversationId || p.conversationId === helpConv2)
|
||||
// F-09: 清理分离窗口生成态快照(per-conv key,清本会话快照;兼容旧单 key)
|
||||
const helpConv = event.conversation_id || ''
|
||||
if (helpConv) {
|
||||
localStorage.removeItem(`df-ai-gen-${helpConv}`)
|
||||
localStorage.removeItem(`df-ai-text-${helpConv}`)
|
||||
}
|
||||
localStorage.removeItem('df-ai-gen')
|
||||
localStorage.removeItem('df-ai-text')
|
||||
// 翻 pendingHelp 驱动求助卡:用 convId 兜底(后端必带,无时默认当前活跃会话,优于丢卡片)。
|
||||
pendingHelp.value = {
|
||||
reason: event.reason,
|
||||
|
||||
85
src/composables/useStoreAction.ts
Normal file
85
src/composables/useStoreAction.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Store action 统一错误处理工具(任务 #7 DRY 抽离)。
|
||||
*
|
||||
* 之前 4 个 store(knowledge/ideas/projects/tasks)共 38 处重复:
|
||||
* try { ... } catch (e: any) { state.error = e?.toString() ?? t('xxx') }
|
||||
*
|
||||
* 本工具:
|
||||
* - 用 `catch (e: unknown)` 替代 `any`(类型安全)
|
||||
* - 失败时写 state.error,返回 undefined(调用方判断)
|
||||
* - 成功时返回 fn 结果
|
||||
*
|
||||
* 注:带 seq 竞态守卫的场景调用方需在 fn 内部判断,不在本工具职责范围。
|
||||
*/
|
||||
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
/** 带 error 字段的 store state 形状(仅约束 error,其他字段任意)。 */
|
||||
export interface ErrorSink {
|
||||
error: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行异步 action,失败时写 state.error + 返回 undefined。
|
||||
*
|
||||
* @param state 带 error 字段的响应式 state
|
||||
* @param i18nKey 失败时的 i18n 文案 key(兜底)
|
||||
* @param fn 异步 action
|
||||
* @returns 成功返 fn 结果;失败返 undefined
|
||||
*/
|
||||
export async function runWithCatch<T>(
|
||||
state: ErrorSink,
|
||||
i18nKey: string,
|
||||
fn: () => Promise<T>,
|
||||
): Promise<T | undefined> {
|
||||
try {
|
||||
return await fn()
|
||||
} catch (e: unknown) {
|
||||
state.error = stringifyError(e) ?? i18nKey
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 把 unknown 错误转为可读字符串(无则返 undefined,由调用方兜底 i18n)。
|
||||
*
|
||||
* Tauri invoke 抛出的通常是 string 或 Error 实例。
|
||||
*/
|
||||
export function stringifyError(e: unknown): string | undefined {
|
||||
if (typeof e === 'string') return e
|
||||
if (e instanceof Error) return e.message || e.toString()
|
||||
if (e && typeof e === 'object' && 'toString' in e) {
|
||||
try {
|
||||
return String((e as { toString(): string }).toString())
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Variant:带前置守卫的错误捕获。
|
||||
*
|
||||
* 用于「旧请求的失败不污染当前视图」场景(如 seq 守卫)。
|
||||
*
|
||||
* @param guard 返回 true 时才写 state.error;false 时静默(旧响应丢弃)
|
||||
*/
|
||||
export async function runWithCatchGuarded<T>(
|
||||
state: ErrorSink,
|
||||
i18nKey: string,
|
||||
guard: () => boolean,
|
||||
fn: () => Promise<T>,
|
||||
): Promise<T | undefined> {
|
||||
try {
|
||||
return await fn()
|
||||
} catch (e: unknown) {
|
||||
if (guard()) {
|
||||
state.error = stringifyError(e) ?? i18nKey
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
// 显式标记 Ref 未使用(避免未来用上时改导入)
|
||||
export type _Unused = Ref<unknown>
|
||||
@@ -1,6 +1,7 @@
|
||||
import { reactive, computed } from 'vue'
|
||||
import { knowledgeApi } from '@/api'
|
||||
import { t } from '@/i18n/i18n-helpers'
|
||||
import { runWithCatch, runWithCatchGuarded } from '@/composables/useStoreAction'
|
||||
import type {
|
||||
KnowledgeRecord,
|
||||
KnowledgeDetailPayload,
|
||||
@@ -140,73 +141,58 @@ export function useKnowledgeStore() {
|
||||
}
|
||||
|
||||
async function create(input: CreateKnowledgeInput) {
|
||||
try {
|
||||
const record = await knowledgeApi.create(input)
|
||||
// 手动录入默认 candidate,刷新收件箱。
|
||||
// create 已成功返回 record,刷新失败不应污染成功语义 —— 直接走原始 API + 非阻塞 console.warn,
|
||||
// 不经 loadCandidates(后者会写 state.error,污染 create 成功语义)。
|
||||
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: any) {
|
||||
console.warn('create 后刷新收件箱失败(非阻塞)', e?.toString?.() ?? e)
|
||||
} catch (e: unknown) {
|
||||
console.warn('create 后刷新收件箱失败(非阻塞)', e)
|
||||
}
|
||||
return record
|
||||
} catch (e: any) {
|
||||
state.error = e?.toString() ?? t('knowledge.err.createFailed')
|
||||
return null
|
||||
}
|
||||
return r
|
||||
})
|
||||
return record ?? null
|
||||
}
|
||||
|
||||
async function updateStatus(id: string, status: string) {
|
||||
try {
|
||||
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)
|
||||
} catch (e: any) {
|
||||
state.error = e?.toString() ?? t('knowledge.err.updateStatusFailed')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function archive(id: string) {
|
||||
try {
|
||||
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)
|
||||
} catch (e: any) {
|
||||
state.error = e?.toString() ?? t('knowledge.err.archiveFailed')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ── 配置 ──
|
||||
async function loadConfig() {
|
||||
const seq = ++_configReqSeq
|
||||
try {
|
||||
await runWithCatchGuarded(state, t('knowledge.err.loadConfigFailed'), () => seq === _configReqSeq, async () => {
|
||||
const result = await knowledgeApi.getConfig()
|
||||
if (seq !== _configReqSeq) return // 旧响应丢弃,只取最新
|
||||
if (seq !== _configReqSeq) return // 旧响应丢弃
|
||||
state.config = result
|
||||
} catch (e: any) {
|
||||
if (seq !== _configReqSeq) return
|
||||
state.error = e?.toString() ?? t('knowledge.err.loadConfigFailed')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function saveConfig(config: KnowledgeConfig) {
|
||||
try {
|
||||
await runWithCatch(state, t('knowledge.err.saveConfigFailed'), async () => {
|
||||
await knowledgeApi.saveConfig(config)
|
||||
state.config = config
|
||||
} catch (e: any) {
|
||||
state.error = e?.toString() ?? t('knowledge.err.saveConfigFailed')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function extractNow() {
|
||||
try {
|
||||
await runWithCatch(state, t('knowledge.err.extractFailed'), async () => {
|
||||
await knowledgeApi.extractNow()
|
||||
await loadCandidates()
|
||||
} catch (e: any) {
|
||||
state.error = e?.toString() ?? t('knowledge.err.extractFailed')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ── 生命线:详情 / 编辑 / 事件查询(按需调用,不入全局 state) ──
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ideaApi } from '@/api'
|
||||
import type { IdeaQuery } from '@/api/types'
|
||||
import { t } from '@/i18n/i18n-helpers'
|
||||
import { runWithCatch } from '@/composables/useStoreAction'
|
||||
import { state } from './state'
|
||||
|
||||
/** 灵感 CRUD 子 store(共享全局 state) */
|
||||
@@ -13,22 +14,18 @@ export function createIdeasStore() {
|
||||
* - 不传 → 全量(created_at DESC,等价旧行为)。
|
||||
*/
|
||||
async function loadIdeas(query?: IdeaQuery) {
|
||||
try {
|
||||
await runWithCatch(state, t('ideas.err.loadFailed'), async () => {
|
||||
state.ideas = await ideaApi.list(query)
|
||||
} catch (e: any) {
|
||||
state.error = e?.toString() ?? t('ideas.err.loadFailed')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function createIdea(input: { title: string; description?: string; priority?: number; tags?: string; source?: string }) {
|
||||
try {
|
||||
const record = await ideaApi.create(input)
|
||||
state.ideas.push(record)
|
||||
return record
|
||||
} catch (e: any) {
|
||||
state.error = e?.toString() ?? t('ideas.err.createFailed')
|
||||
return null
|
||||
}
|
||||
const record = await runWithCatch(state, t('ideas.err.createFailed'), async () => {
|
||||
const r = await ideaApi.create(input)
|
||||
state.ideas.push(r)
|
||||
return r
|
||||
})
|
||||
return record ?? null
|
||||
}
|
||||
|
||||
async function updateIdea(id: string, field: string, value: string) {
|
||||
@@ -40,12 +37,10 @@ export function createIdeasStore() {
|
||||
}
|
||||
|
||||
async function deleteIdea(id: string) {
|
||||
try {
|
||||
await runWithCatch(state, t('ideas.err.deleteFailed'), async () => {
|
||||
await ideaApi.delete(id)
|
||||
state.ideas = state.ideas.filter(i => i.id !== id)
|
||||
} catch (e: any) {
|
||||
state.error = e?.toString() ?? t('ideas.err.deleteFailed')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function evaluateIdea(id: string) {
|
||||
@@ -62,12 +57,10 @@ export function createIdeasStore() {
|
||||
}
|
||||
|
||||
async function relateIdeas(subjectId: string, targetIds: string[]) {
|
||||
try {
|
||||
await runWithCatch(state, t('ideas.err.relateFailed'), async () => {
|
||||
await ideaApi.relateIdeas(subjectId, targetIds)
|
||||
await loadIdeas() // 后端已原子更新全部受影响灵感,刷新列表保持 store 一致
|
||||
} catch (e: any) {
|
||||
state.error = e?.toString() ?? t('ideas.err.relateFailed')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { projectApi } from '@/api'
|
||||
import type { ImportProjectInput, ProjectQuery } from '@/api/project'
|
||||
import { t } from '@/i18n/i18n-helpers'
|
||||
import { runWithCatch } from '@/composables/useStoreAction'
|
||||
import { state, clearError } from './state'
|
||||
|
||||
/** 项目 CRUD 子 store(共享全局 state) */
|
||||
@@ -12,36 +13,29 @@ export function createProjectsStore() {
|
||||
async function loadProjects(query?: ProjectQuery) {
|
||||
state.loading = true
|
||||
state.error = null
|
||||
try {
|
||||
await runWithCatch(state, t('projects.err.loadFailed'), async () => {
|
||||
state.projects = await projectApi.list(query)
|
||||
} catch (e: any) {
|
||||
state.error = e?.toString() ?? t('projects.err.loadFailed')
|
||||
} finally {
|
||||
state.loading = false
|
||||
}
|
||||
})
|
||||
state.loading = false
|
||||
}
|
||||
|
||||
async function createProject(name: string, description = '', ideaId?: string, path?: string, stack?: string) {
|
||||
try {
|
||||
const record = await projectApi.create({ name, description, idea_id: ideaId, path, stack })
|
||||
state.projects.push(record)
|
||||
return record
|
||||
} catch (e: any) {
|
||||
state.error = e?.toString() ?? t('projects.err.createFailed')
|
||||
return null
|
||||
}
|
||||
const record = await runWithCatch(state, t('projects.err.createFailed'), async () => {
|
||||
const r = await projectApi.create({ name, description, idea_id: ideaId, path, stack })
|
||||
state.projects.push(r)
|
||||
return r
|
||||
})
|
||||
return record ?? null
|
||||
}
|
||||
|
||||
/** 导入历史项目(选已存在目录,后端创建+绑定+探测栈+读 README 首段一步完成) */
|
||||
async function importProject(input: ImportProjectInput) {
|
||||
try {
|
||||
const record = await projectApi.importProject(input)
|
||||
const record = await runWithCatch(state, t('projects.err.importFailed'), async () => {
|
||||
const r = await projectApi.importProject(input)
|
||||
await loadProjects() // 刷新列表(后端已 insert,统一走 load 避免本地数组与后端不一致)
|
||||
return record
|
||||
} catch (e: any) {
|
||||
state.error = e?.toString() ?? t('projects.err.importFailed')
|
||||
return null
|
||||
}
|
||||
return r
|
||||
})
|
||||
return record ?? null
|
||||
}
|
||||
|
||||
async function updateProject(id: string, field: string, value: string) {
|
||||
@@ -61,20 +55,16 @@ export function createProjectsStore() {
|
||||
}
|
||||
|
||||
async function deleteProject(id: string) {
|
||||
try {
|
||||
await runWithCatch(state, t('projects.err.deleteFailed'), async () => {
|
||||
await projectApi.delete(id) // 软删 → 回收站(可恢复)
|
||||
state.projects = state.projects.filter(p => p.id !== id)
|
||||
} catch (e: any) {
|
||||
state.error = e?.toString() ?? t('projects.err.deleteFailed')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function loadDeletedProjects() {
|
||||
try {
|
||||
await runWithCatch(state, t('projects.err.loadTrashFailed'), async () => {
|
||||
state.deletedProjects = await projectApi.listDeleted()
|
||||
} catch (e: any) {
|
||||
state.error = e?.toString() ?? t('projects.err.loadTrashFailed')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function restoreProject(id: string) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { taskApi } from '@/api'
|
||||
import type { TaskQuery } from '@/api/types'
|
||||
import { t } from '@/i18n/i18n-helpers'
|
||||
import { runWithCatch } from '@/composables/useStoreAction'
|
||||
import { state } from './state'
|
||||
|
||||
/** 任务 CRUD 子 store(共享全局 state) */
|
||||
@@ -16,43 +17,35 @@ export function createTasksStore() {
|
||||
* status/keyword 下沉后端 WHERE(P1/P2),取代 Tasks.vue 旧的前端内存 filter。
|
||||
*/
|
||||
async function loadTasks(queryOrProjectId?: TaskQuery | string) {
|
||||
try {
|
||||
await runWithCatch(state, t('tasks.err.loadFailed'), async () => {
|
||||
state.tasks = await taskApi.list(queryOrProjectId)
|
||||
} catch (e: any) {
|
||||
state.error = e?.toString() ?? t('tasks.err.loadFailed')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function createTask(input: { project_id: string; title: string; description?: string; priority?: number; branch_name?: string; assignee?: string; idea_id?: string }) {
|
||||
try {
|
||||
const record = await taskApi.create(input)
|
||||
state.tasks.push(record)
|
||||
return record
|
||||
} catch (e: any) {
|
||||
state.error = e?.toString() ?? t('tasks.err.createFailed')
|
||||
return null
|
||||
}
|
||||
const record = await runWithCatch(state, t('tasks.err.createFailed'), async () => {
|
||||
const r = await taskApi.create(input)
|
||||
state.tasks.push(r)
|
||||
return r
|
||||
})
|
||||
return record ?? null
|
||||
}
|
||||
|
||||
async function updateTask(id: string, field: string, value: string) {
|
||||
try {
|
||||
await runWithCatch(state, t('tasks.err.updateFailed'), async () => {
|
||||
await taskApi.update(id, field, value)
|
||||
const idx = state.tasks.findIndex(t => t.id === id)
|
||||
if (idx >= 0) {
|
||||
(state.tasks[idx] as any)[field] = value
|
||||
}
|
||||
} catch (e: any) {
|
||||
state.error = e?.toString() ?? t('tasks.err.updateFailed')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function deleteTask(id: string) {
|
||||
try {
|
||||
await runWithCatch(state, t('tasks.err.deleteFailed'), async () => {
|
||||
await taskApi.delete(id)
|
||||
state.tasks = state.tasks.filter(t => t.id !== id)
|
||||
} catch (e: any) {
|
||||
state.error = e?.toString() ?? t('tasks.err.deleteFailed')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user