新增: 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

@@ -0,0 +1,167 @@
//! 窗口分离模式 — detach/reattach/resumeInDetached/closeDetachedWindow/dock/syncToMain/startFollowMain/stopFollowMain
//!
//! 模块级私有(不进 reactive):
//! - _unlistenMove / _unlistenResize: 主窗口 move/resize 跟随的 unlistener
//!
//! 耦合:
//! - detachPanel/resumeInDetached 调 useAiConversations.switchConversation
//! - resumeInDetached 调 useAiEvents.nextMsgId 占位 ai 气泡
//! - reattachPanel/detachPanel 调 useAiPanel.persistUiState
import { state } from '../../stores/ai'
import { nextMsgId } from './useAiEvents'
import { switchConversation } from './useAiConversations'
import { persistUiState } from './useAiPanel'
// ── 主窗口事件跟随 unlistener ──
let _unlistenMove: (() => void) | null = null
let _unlistenResize: (() => void) | null = null
/** 分离 AI 面板到独立窗口(若已存在则聚焦);快照当前生成态供分离窗口接管 */
async function detachPanel() {
const { WebviewWindow } = await import('@tauri-apps/api/webviewWindow')
const existing = await WebviewWindow.getByLabel('ai-detached')
if (existing) {
await existing.setFocus()
return
}
// 快照当前生成态,供分离窗口接管(保持正在进行的对话)
if (state.streaming && state.generatingConvId) {
localStorage.setItem('df-ai-gen', state.generatingConvId)
localStorage.setItem('df-ai-text', state.currentText)
}
const convId = state.activeConversationId
const sep = convId ? `?conv=${encodeURIComponent(convId)}` : ''
const url = window.location.origin + window.location.pathname + '#/ai-detached' + sep
const win = new WebviewWindow('ai-detached', {
url,
title: 'DevFlow AI',
width: 520,
height: 700,
minWidth: 360,
minHeight: 400,
center: true,
decorations: true,
})
win.once('tauri://created', () => {
state.panelOpen = false
persistUiState()
})
win.once('tauri://error', (e) => {
console.error('[AI] 窗口创建失败:', e)
state.detached = false
})
win.once('tauri://destroyed', () => {
state.detached = false
state.docked = false
stopFollowMain()
})
state.detached = true
}
/** 关闭分离窗口并回到内嵌面板(主窗口侧调用) */
async function reattachPanel() {
stopFollowMain()
const { WebviewWindow } = await import('@tauri-apps/api/webviewWindow')
const win = await WebviewWindow.getByLabel('ai-detached')
if (win) {
await win.close()
}
state.detached = false
state.docked = false
state.panelOpen = true
persistUiState()
localStorage.removeItem('df-ai-gen')
localStorage.removeItem('df-ai-text')
}
/** 分离窗口挂载时接管主窗口当前对话(含生成中态) */
async function resumeInDetached(convId: string | null) {
const id = convId || state.activeConversationId
if (id) await switchConversation(id)
const gen = localStorage.getItem('df-ai-gen')
if (gen) {
// 接管正在生成的对话:恢复已生成文本并补占位,后续 delta 继续追加
state.currentText = localStorage.getItem('df-ai-text') || ''
state.messages.push({
id: `ai-${nextMsgId()}`,
role: 'assistant',
content: '',
timestamp: Date.now(),
})
state.streaming = true
state.generatingConvId = gen
}
}
/** 关闭分离窗口自身(分离窗口侧调用) */
async function closeDetachedWindow() {
state.docked = false
stopFollowMain()
localStorage.removeItem('df-ai-gen')
localStorage.removeItem('df-ai-text')
const { getCurrentWebviewWindow } = await import('@tauri-apps/api/webviewWindow')
await getCurrentWebviewWindow().close()
}
/** 吸附/取消吸附AI 窗口跟随主窗口右侧 */
async function dockDetached() {
// 取消吸附
if (state.docked) {
state.docked = false
stopFollowMain()
return
}
try {
await syncToMain()
state.docked = true
startFollowMain()
} catch (e) {
console.error('[AI] 吸附失败:', e)
}
}
/** 同步 AI 窗口位置到主窗口右侧(保留 4px 间隙) */
async function syncToMain() {
const { WebviewWindow, getCurrentWebviewWindow } = await import('@tauri-apps/api/webviewWindow')
const { PhysicalPosition, PhysicalSize } = await import('@tauri-apps/api/dpi')
const mainWin = await WebviewWindow.getByLabel('main')
const aiWin = getCurrentWebviewWindow()
if (!mainWin) return
const pos = await mainWin.outerPosition()
const size = await mainWin.innerSize()
await aiWin.setPosition(new PhysicalPosition(pos.x + size.width + 4, pos.y))
await aiWin.setSize(new PhysicalSize(600, size.height))
}
/** 启动主窗口 move/resize 跟随(吸附时调用) */
async function startFollowMain() {
stopFollowMain()
const { WebviewWindow } = await import('@tauri-apps/api/webviewWindow')
const mainWin = await WebviewWindow.getByLabel('main')
if (!mainWin) return
_unlistenMove = await mainWin.onMoved(() => { void syncToMain() })
_unlistenResize = await mainWin.onResized(() => { void syncToMain() })
}
/** 停止主窗口跟随 */
function stopFollowMain() {
_unlistenMove?.()
_unlistenResize?.()
_unlistenMove = null
_unlistenResize = null
}
export function useAiWindow() {
return {
detachPanel,
reattachPanel,
resumeInDetached,
closeDetachedWindow,
dockDetached,
syncToMain,
startFollowMain,
stopFollowMain,
}
}