重构: 前端 composable 拆分+详情页 Markdown 渲染+流式 memo+状态同步+bug 修复

- composables/useMarkdown 抽(AiChat 复用)+ai/* 流式核心+useAiSend catch 回滚 user msg(B-21)+useAiStream 超时文案(B-03)+useAiConversations 切换 token(FR-R1)
- views/TaskDetail 新建(F-02)+描述 md+mdReady 响应式+字段同行布局(B-24/31)
- views/ProjectDetail 多选审批 UI(F-01)+描述 md+mdReady+字段同行(B-25/31)
- views/Ideas+Knowledge 描述 md+mdReady(B-25)
- views/Tasks 项目切换联动(B-29)+task 详情跳转
- components/AiChat toast 失败提示(B-20)+流式块级 memo(ARC-08)+clean UI(AR-7)+stop 兜底(AR-5)
- components/ToolCard 审批卡片可读化(AR-3)
- stores useAiStore barrel(ARC-04)+del settings mock(ARC-01)+approve options(R-PD-5)
- i18n aiTool/nav 中英对称(AR-3/9)+删 nav.decisions 死链(ARC-02)
- router /tasks/:id(F-02)+global.css selection 可见(R-260615-01)+api task detail+App 删 decisions nav
This commit is contained in:
2026-06-15 05:14:56 +08:00
parent 2de0c6ecb7
commit 80b9243528
26 changed files with 997 additions and 207 deletions

View File

@@ -5,9 +5,8 @@
//! - 写操作立即更新缓存(乐观),并按 key debounce ~300ms 合并连续快速写
//! - 组合式 `useSetting` 让组件像用 ref 一样用 v-model 绑定某个 key
//!
//! 注意:此为「KV 偏好持久化」层,与已有的 `stores/settings.ts`(mock 的 AI 提供商/
//! 连接/通用配置列表)是两件事 —— 那个文件承载静态展示数据,本文件承载运行时偏好,
//! 故单独成文,互不干扰。
//! 注意:此为「KV 偏好持久化」层,与 AI 提供商/连接配置(store 层)是两件事 ——
//! 那边承载 provider 业务数据,本文件承载运行时偏好,故单独成文,互不干扰。
import { reactive, ref, watch, type Ref } from 'vue'
import { settingsApi } from '../api/settings'

View File

@@ -3,7 +3,6 @@ export { useProjectStore } from './project'
export { useKnowledgeStore } from './knowledge'
export { useSettingsStore } from './settings'
export type { AIProvider, Connection, GeneralSettings } from './settings'
export { useAppSettingsStore } from './appSettings'
export { useAiStore } from './ai'

View File

@@ -19,6 +19,8 @@ const state = reactive({
title: string
description: string
options: string[]
/** F-260615-01: single(缺省)/multiple */
select_type?: 'single' | 'multiple'
} | null,
loading: false,
error: null as string | null,
@@ -244,16 +246,31 @@ function createStore() {
state.liveEvents = []
}
async function approveHumanApproval(decision: string, comment?: string) {
/**
* 发送审批响应。
* - 单选(select_type=single 或缺省):传 decision 单值,decisions 留空;
* - 多选(select_type=multiple):传 decisions 数组。
* F-260615-01: 新增 decisions/select_type 透传,与后端 IPC 签名对齐。
*/
async function approveHumanApproval(
decision: string,
comment?: string,
decisions?: string[],
) {
if (!state.pendingApproval) return
try {
// 调用 IPC 命令发送审批响应
// R-PD-5: 透传 options 闭环后端校验(options 非空时 decision 必须 ∈ options否则 IPC 报错)。
// options 来自已收到的 HumanApprovalRequest 事件 payload(IPC 无法访问节点 config)。
await invoke('approve_human_approval', {
execution_id: state.pendingApproval.execution_id,
node_id: state.pendingApproval.node_id,
decision,
comment
comment,
options: state.pendingApproval.options ?? [],
decisions: decisions ?? [],
selectType: state.pendingApproval.select_type ?? 'single', // Tauri camelCase→snake_case 自动转 select_type
})
// 清除待审批状态

View File

@@ -1,98 +0,0 @@
import { reactive, computed } from 'vue'
export interface AIProvider {
id: number
name: string
apiKey: string
baseUrl: string
models: string[]
isDefault: boolean
}
export interface Connection {
id: number
name: string
type: 'mysql' | 'ssh' | 'redis' | 'mongo'
typeLabel: string
icon: string
host: string
port: number
user: string
connected: boolean
}
export interface GeneralSettings {
theme: 'dark' | 'light' | 'system'
language: string
dataDir: string
autoExecute: boolean
logLevel: 'error' | 'warn' | 'info' | 'debug'
}
const state = reactive({
aiProviders: [
{
id: 1,
name: 'Anthropic Claude',
apiKey: 'sk-ant-••••••••••••••••7xKm',
baseUrl: 'https://api.anthropic.com',
models: ['claude-sonnet-4-20250514', 'claude-opus-4-20250514'],
isDefault: true,
},
{
id: 2,
name: 'Zhipu GLM',
apiKey: '••••••••••••••••a3f2',
baseUrl: 'https://open.bigmodel.cn/api/paas',
models: ['glm-4-plus', 'glm-4-flash'],
isDefault: false,
},
{
id: 3,
name: 'DeepSeek',
apiKey: '••••••••••••••••9b8c',
baseUrl: 'https://api.deepseek.com',
models: ['deepseek-chat', 'deepseek-coder'],
isDefault: false,
},
] as AIProvider[],
connections: [
{ id: 1, name: 'flux_dev', type: 'mysql' as const, typeLabel: 'MySQL', icon: '🗄️', host: '39.99.243.191', port: 3306, user: 'root', connected: true },
{ id: 2, name: 'flux_dev', type: 'ssh' as const, typeLabel: 'SSH', icon: '🔐', host: '39.99.243.191', port: 22, user: 'root', connected: true },
{ id: 3, name: 'flux_redis', type: 'redis' as const, typeLabel: 'Redis', icon: '⚡', host: '127.0.0.1', port: 6379, user: '', connected: true },
{ id: 4, name: 'suke_dev', type: 'mongo' as const, typeLabel: 'MongoDB', icon: '🍃', host: '127.0.0.1', port: 27017, user: 'admin', connected: false },
{ id: 5, name: 'rds_read', type: 'mysql' as const, typeLabel: 'MySQL', icon: '🗄️', host: 'rm-8vbrd2wil14hclop3vm.mysql.zhangbei.rds.aliyuncs.com', port: 3306, user: 'u_read', connected: true },
] as Connection[],
general: {
theme: 'dark' as const,
language: 'zh-CN',
dataDir: 'E:/wk-lab/devflow/data',
autoExecute: false,
logLevel: 'info' as const,
} as GeneralSettings,
})
export function useSettingsStore() {
const aiProviders = computed(() => state.aiProviders)
const connections = computed(() => state.connections)
const general = computed(() => state.general)
const defaultProvider = computed(() =>
state.aiProviders.find(p => p.isDefault)
)
const connectionStats = computed(() => ({
total: state.connections.length,
connected: state.connections.filter(c => c.connected).length,
}))
return {
aiProviders,
connections,
general,
defaultProvider,
connectionStats,
}
}