新增: F-260619-01 任务可关联灵感(tasks.idea_id 1对1 单向)

- df-storage V20 迁移:tasks 加 idea_id TEXT REFERENCES ideas(id)(填预留空位,
  幂等 column_exists 探测)+ V9_SQL 同步 + TaskRecord.idea_id + task_repo SQL/SELECT + 白名单
- AI 工具 create_task 加 idea_id 参数;update_task 经白名单自动支持
- IPC commands/task.rs CreateTaskInput 加 idea_id
- 前端 types TaskRecord/CreateTaskInput idea_id + TaskDetail.vue 关联灵感展示
  (router-link 友好 title 非裸 id)+ i18n
- 补 idea_id 字段:df-mcp tools / df-nodes 测试 helper
- 单向 1对1,Option 可空,复用 projects.idea_id 模式,老任务 None 兼容

自验: df-storage 47 passed(含 V20 2)+ workspace EXIT 0 + vue-tsc EXIT 0
This commit is contained in:
2026-06-19 21:03:18 +08:00
parent e5a8fec1fe
commit 4a87c552cc
15 changed files with 158 additions and 17 deletions

View File

@@ -117,6 +117,8 @@ export interface TaskRecord {
* 旧任务无产出时为 undefined。前端 parsedOutput try/catch 兜底解析。
*/
output_json?: string
/** 关联灵感 ID(F-260619-01,1对1 单向,任务→灵感)。未关联时为 undefined。 */
idea_id?: string
created_at: string
updated_at: string
}
@@ -128,6 +130,8 @@ export interface CreateTaskInput {
priority?: number
branch_name?: string
assignee?: string
/** 关联灵感 ID(1对1 单向,可空)。空字符串视为不关联(与后端一致)。 */
idea_id?: string
}
// ============================================================

View File

@@ -14,6 +14,7 @@ export default {
status: 'Status',
priority: 'Priority',
project: 'Project',
relatedIdea: 'Related Idea',
branch: 'Branch',
assignee: 'Assignee',
baseBranch: 'Base Branch',

View File

@@ -14,6 +14,7 @@ export default {
status: '状态',
priority: '优先级',
project: '关联项目',
relatedIdea: '关联灵感',
branch: '分支',
assignee: '负责人',
baseBranch: '基础分支',

View File

@@ -12,7 +12,7 @@ export function createTasksStore() {
}
}
async function createTask(input: { project_id: string; title: string; description?: string; priority?: number; branch_name?: string; assignee?: string }) {
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)

View File

@@ -104,6 +104,16 @@
<span v-else></span>
</span>
</div>
<!-- F-260619-01:关联灵感(1对1 单向,idea_id 解析为友好 title,非裸 id) -->
<div class="info-item">
<span class="label">{{ $t('taskDetail.relatedIdea') }}</span>
<span class="value">
<router-link v-if="task.idea_id" :to="`/ideas/${task.idea_id}`" class="project-link">
{{ ideaTitle }}
</router-link>
<span v-else></span>
</span>
</div>
<div class="info-item">
<span class="label">{{ $t('taskDetail.branch') }}</span>
<span class="value">
@@ -146,7 +156,7 @@ import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute } from 'vue-router'
import { listen } from '@tauri-apps/api/event'
import { taskApi, projectApi } from '@/api'
import { taskApi, projectApi, ideaApi } from '@/api'
import { workflowApi } from '@/api'
import { formatDate } from '@/utils/time'
import { useRendered } from '@/composables/useMarkdown'
@@ -156,7 +166,7 @@ import {
priorityLabel,
priorityClass,
} from '../constants/project'
import type { TaskRecord, ProjectRecord, DfDataChangedPayload, WorkflowEventPayload } from '@/api/types'
import type { TaskRecord, ProjectRecord, IdeaRecord, DfDataChangedPayload, WorkflowEventPayload } from '@/api/types'
import TaskOutputCard from '@/components/task/TaskOutputCard.vue'
const { t } = useI18n()
@@ -166,6 +176,8 @@ const loading = ref(true)
const errorMsg = ref('')
const task = ref<TaskRecord | null>(null)
const projects = ref<ProjectRecord[]>([])
// F-260619-01:灵感列表用于解析 task.idea_id → 灵感 title(独立入口,同 projects 模式)
const ideas = ref<IdeaRecord[]>([])
const advancing = ref(false)
// ============================================================
@@ -204,6 +216,14 @@ const projectName = computed(() => {
return p?.name ?? task.value?.project_id ?? '—'
})
// F-260619-01:解析 task.idea_id → 灵感 title(找不到时回退 id,保证非空可点击)
const ideaTitle = computed(() => {
const ideaId = task.value?.idea_id
if (!ideaId) return '—'
const idea = ideas.value.find(i => i.id === ideaId)
return idea?.title ?? ideaId
})
// ============================================================
// F-05 推进按钮 — 状态机合法下一态映射
// ------------------------------------------------------------
@@ -379,13 +399,16 @@ async function load() {
loading.value = true
errorMsg.value = ''
try {
const [t, ps] = await Promise.all([
const [t, ps, ideasList] = await Promise.all([
taskApi.get(taskId.value),
// 项目列表用于解析 project_id → 项目名(独立入口,不依赖全局 store)
projectApi.list(),
// F-260619-01:灵感列表用于解析 idea_id → 灵感 title
ideaApi.list(),
])
task.value = t
projects.value = ps
ideas.value = ideasList
} catch (e: any) {
task.value = null
errorMsg.value = t('taskDetail.loadFailed', { msg: e?.toString() ?? t('common.unknownError') })
@@ -417,7 +440,7 @@ onMounted(async () => {
try {
_unlistenDataChanged = await listen<DfDataChangedPayload>('df-data-changed', (event) => {
const { entity } = event.payload
if (entity === 'task' || entity === 'project') {
if (entity === 'task' || entity === 'project' || entity === 'idea') {
load()
}
})