新增: Phase2 阶段收尾(Sprint 1-20)

重构:删 5 零引用 crate(df-evolve/plugin/stages/task/traceability)+ 清死模块、ai.rs 拆 11 子 module、ai.ts 拆 6 composable、i18n 拆目录
功能:知识库全栈(df-project/scan + CRUD + 时间线 + 前端)、Settings 拆分、appSettings KV 迁移、模型池、LLM 并发 Semaphore
修复:审批持久化根治、ConditionEngine 默认拒绝、NodeRegistry unimplemented 清除、promote 补偿删除、工具结果截断 50KB、路径校验防 symlink 逃逸
文档:B-03 人工审批设计、决策记录三分档、规格契约自检、经验记录、todo 看板、PROGRESS 更新

详见 PROGRESS.md。src-tauri/儿童每日打卡应用/ 与本项目无关,已排除。
This commit is contained in:
2026-06-14 14:08:20 +08:00
parent 98393b4908
commit cf017f81e2
167 changed files with 19549 additions and 6886 deletions

View File

@@ -51,6 +51,7 @@
>
<span class="nav-link-icon" v-html="item.svg"></span>
<span class="nav-link-text">{{ $t(item.label) }}</span>
<span v-if="item.path === '/knowledge' && candidateCount > 0" class="nav-link-badge">{{ candidateCount }}</span>
</router-link>
</div>
</nav>
@@ -79,7 +80,8 @@
</aside>
<!-- Main (页面内容) -->
<main class="df-main" v-show="!aiStore.state.maximized">
<!-- 仅在 AI 面板可见且最大化时隐藏主内容面板关闭时无论 maximized 状态如何都显示 -->
<main class="df-main" v-show="!(aiStore.state.maximized && aiStore.state.panelOpen)">
<router-view v-slot="{ Component }">
<transition name="page" mode="out-in">
<component :is="Component" />
@@ -95,17 +97,39 @@
</div>
</transition>
</div>
<!-- 全局错误 toast(根级,主窗口与分离窗口共用)-->
<transition name="toast">
<div v-if="errorMsg" class="df-toast"> {{ errorMsg }}</div>
</transition>
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
import { useRoute } from 'vue-router'
import { useAiStore } from './stores/ai'
import { useKnowledgeStore } from './stores/knowledge'
import { useProjectStore } from './stores/project'
import { useAppSettingsStore } from './stores/appSettings'
import i18n from './i18n'
import AiChat from './components/AiChat.vue'
const route = useRoute()
const isDetached = computed(() => route.path === '/ai-detached')
const aiStore = useAiStore()
const appSettings = useAppSettingsStore()
// ── 全局错误 toast(消费 projectStore.error,所有 action 失败统一反馈)──
const projectStore = useProjectStore()
const errorMsg = ref('')
let errorTimer: ReturnType<typeof setTimeout> | null = null
watch(() => projectStore.error, (err) => {
if (!err) return
errorMsg.value = err
projectStore.clearError() // 重置,允许连续同值错误再次触发
if (errorTimer) clearTimeout(errorTimer)
errorTimer = setTimeout(() => { errorMsg.value = '' }, 3500)
})
// 点击菜单自动退出最大化 → 切为分栏模式
watch(() => route.path, (path) => {
@@ -115,7 +139,8 @@ watch(() => route.path, (path) => {
})
// ── AI 面板拖拽调宽 ──
const panelWidth = ref(parseInt(localStorage.getItem('df-ai-width') || '500', 10))
// useSetting 监听 appSettings 缓存:loadAll 异步填充后会自动刷新 ref
const panelWidth = appSettings.useSetting<number>('df-ai-width', 500)
const resizing = ref(false)
// maximized=全宽(flex:1填充) | sidebar=固定宽
@@ -147,7 +172,7 @@ function startResize(e: MouseEvent) {
resizing.value = false
document.body.style.cursor = ''
document.body.style.userSelect = ''
localStorage.setItem('df-ai-width', String(panelWidth.value))
void appSettings.set('df-ai-width', panelWidth.value)
document.removeEventListener('mousemove', onMouseMove)
document.removeEventListener('mouseup', onMouseUp)
}
@@ -158,7 +183,7 @@ function startResize(e: MouseEvent) {
// 主题初始化
function initTheme() {
const saved = localStorage.getItem('df-theme') || 'dark'
const saved = appSettings.get<string>('df-theme', 'dark')
if (saved === 'light') {
document.documentElement.setAttribute('data-theme', 'light')
} else if (saved === 'system') {
@@ -175,11 +200,58 @@ function handleKeydown(e: KeyboardEvent) {
}
}
onMounted(() => {
onMounted(async () => {
// 1. 先把 SQLite 全表 KV 填进 appSettings 缓存(后续 get/useSetting 才能读到真实值)
await appSettings.loadAll()
// 2. 一次性迁移:把旧 localStorage 的 11 个 key 导入 SQLite(仅首次跑,之后 localStorage 清净)
await migrateLegacyLocalStorage()
// 3. 缓存已就绪,初始化主题/locale
initTheme()
const savedLang = appSettings.get<string | null>('df-language', null)
if (savedLang) {
i18n.global.locale.value = savedLang as 'zh-CN' | 'en'
}
window.addEventListener('keydown', handleKeydown)
// 加载知识库候选数(供侧栏 knowledge badge 显示)
knowledgeStore.loadCandidates()
})
/// 一次性迁移:遍历遗留的 localStorage key,若有值则导入 appSettings(SQLite),
/// 然后删除 localStorage 中的旧 key。导入时按各 key 原存格式解析成 JS 值再交给 set
/// (set 内部会 JSON.stringify)。之后 localStorage 只剩 df-ai-gen / df-ai-text 流式快照。
const LEGACY_KEYS_RAW = new Set(['df-theme', 'df-language', 'df-ai-language']) // 裸字符串
const LEGACY_KEYS_JSON = new Set([ // 已 JSON.stringify(裸值或对象)
'df-ai-width', 'df-ai-ui', 'df-show-token-usage',
'df-llm-global-concurrency', 'df-llm-per-conv-concurrency',
'df-ai-active-conv', 'df-connections',
])
function parseLegacy(key: string, raw: string): unknown {
if (LEGACY_KEYS_RAW.has(key)) return raw // 裸字符串,如 'dark' / 'zh-CN'
// JSON 类:有的存的是 JSON.stringify('zh-CN') → '"zh-CN"'(虽不在本集合,保险起见 parse)
try {
return JSON.parse(raw)
} catch {
return raw
}
}
async function migrateLegacyLocalStorage(): Promise<void> {
const allKeys = [...LEGACY_KEYS_RAW, ...LEGACY_KEYS_JSON]
for (const key of allKeys) {
const raw = localStorage.getItem(key)
if (raw === null || raw === '') continue
try {
await appSettings.set(key, parseLegacy(key, raw))
} catch (e) {
console.error(`[migrate] 导入 ${key} 失败:`, e)
}
localStorage.removeItem(key)
}
}
// 知识库候选数量(侧栏 badge)— 解包 computed 取值保持响应式
const knowledgeStore = useKnowledgeStore()
const candidateCount = computed(() => knowledgeStore.candidateCount)
onUnmounted(() => {
window.removeEventListener('keydown', handleKeydown)
})
@@ -437,4 +509,15 @@ const secondaryNav = [
.page-leave-active { transition: opacity 0.12s var(--df-ease), transform 0.12s var(--df-ease); }
.page-enter-from { opacity: 0; transform: translateY(6px); }
.page-leave-to { opacity: 0; transform: translateY(-4px); }
/* ═══ 全局错误 toast ═══ */
.df-toast {
position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%);
background: var(--df-danger); color: #fff;
padding: 10px 18px; border-radius: var(--df-radius-sm);
font-size: 13px; z-index: 9999; max-width: 90vw;
box-shadow: 0 4px 16px rgba(0,0,0,0.3);
}
.toast-enter-active, .toast-leave-active { transition: all 0.25s var(--df-ease); }
.toast-enter-from, .toast-leave-to { opacity: 0; transform: translate(-50%, 10px); }
</style>