Kanban:补齐任务操作链路,明确能力边界 (#615)
* [verified] fix(kanban): harden WUI parity bridge - Align board slug normalization with canonical underscore/lowercase/64-char rules - Validate malformed Kanban action bodies before CLI shell-out - Narrow task log no-log handling and expose phase-1 capabilities - Extend client/server regression coverage for parity actions * fix(kanban): guard archived task detail actions --------- Co-authored-by: ekko <152005280+EKKOLearnAI@users.noreply.github.com>
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
import * as kanbanApi from '@/api/hermes/kanban'
|
||||
import type { KanbanTask, KanbanStats, KanbanAssignee, KanbanBoard, KanbanCapabilities } from '@/api/hermes/kanban'
|
||||
import type { KanbanTask, KanbanStats, KanbanAssignee, KanbanBoard, KanbanCapabilities, KanbanDiagnosticsOptions, KanbanDispatchOptions } from '@/api/hermes/kanban'
|
||||
|
||||
export const KANBAN_SELECTED_BOARD_STORAGE_KEY = 'hermes.kanban.selectedBoard'
|
||||
export const DEFAULT_KANBAN_BOARD = 'default'
|
||||
|
||||
const BOARD_SLUG_RE = /^[a-z0-9][a-z0-9-]{0,62}$/
|
||||
const BOARD_SLUG_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/
|
||||
|
||||
function safeStorageGet(key: string): string | null {
|
||||
if (typeof window === 'undefined') return null
|
||||
@@ -27,7 +27,7 @@ function safeStorageSet(key: string, value: string) {
|
||||
}
|
||||
|
||||
export function normalizeBoardSlug(board?: string | null): string {
|
||||
const trimmed = board?.trim()
|
||||
const trimmed = board?.trim().toLowerCase()
|
||||
if (!trimmed) return DEFAULT_KANBAN_BOARD
|
||||
return BOARD_SLUG_RE.test(trimmed) ? trimmed : DEFAULT_KANBAN_BOARD
|
||||
}
|
||||
@@ -72,6 +72,20 @@ export const useKanbanStore = defineStore('kanban', () => {
|
||||
return visible
|
||||
})
|
||||
|
||||
function isCapabilitySupported(key: string): boolean {
|
||||
if (!capabilities.value) return false
|
||||
const detail = capabilities.value.capabilities?.find(capability => capability.key === key)
|
||||
if (detail) return detail.status === 'supported'
|
||||
if (capabilities.value.missing?.includes(key)) return false
|
||||
return capabilities.value.supports?.[key] === true
|
||||
}
|
||||
|
||||
function assertCapability(key: string): void {
|
||||
if (!isCapabilitySupported(key)) {
|
||||
throw new Error(`Kanban capability "${key}" is not supported by the current Hermes backend`)
|
||||
}
|
||||
}
|
||||
|
||||
function boardExists(board: string): boolean {
|
||||
return activeBoards.value.some(item => item.slug === board)
|
||||
}
|
||||
@@ -257,6 +271,57 @@ export const useKanbanStore = defineStore('kanban', () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function addComment(taskId: string, body: string, author?: string) {
|
||||
assertCapability('commentsWrite')
|
||||
return kanbanApi.addComment(taskId, { body, author }, { board: selectedBoard.value })
|
||||
}
|
||||
|
||||
async function getTaskLog(taskId: string, tail?: number) {
|
||||
assertCapability('taskLog')
|
||||
return kanbanApi.getTaskLog(taskId, { board: selectedBoard.value, tail })
|
||||
}
|
||||
|
||||
async function getDiagnostics(opts: Omit<KanbanDiagnosticsOptions, 'board'> = {}) {
|
||||
assertCapability('diagnostics')
|
||||
return kanbanApi.getDiagnostics({ ...opts, board: selectedBoard.value })
|
||||
}
|
||||
|
||||
async function reclaimTask(taskId: string, reason?: string) {
|
||||
assertCapability('reclaim')
|
||||
const board = selectedBoard.value
|
||||
const result = await kanbanApi.reclaimTask(taskId, { board, reason })
|
||||
if (board === selectedBoard.value) await Promise.all([fetchTasks(true), fetchStats(), fetchBoards()])
|
||||
return result
|
||||
}
|
||||
|
||||
async function reassignTask(taskId: string, profile: string, opts: { reclaim?: boolean; reason?: string } = {}) {
|
||||
assertCapability('reassign')
|
||||
const board = selectedBoard.value
|
||||
const result = await kanbanApi.reassignTask(taskId, profile, { board, ...opts })
|
||||
if (board === selectedBoard.value) {
|
||||
const task = tasks.value.find(t => t.id === taskId)
|
||||
if (task) task.assignee = profile === 'none' ? null : profile
|
||||
await Promise.all([fetchStats(), fetchAssignees()])
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
async function specifyTask(taskId: string, author?: string) {
|
||||
assertCapability('specify')
|
||||
const board = selectedBoard.value
|
||||
const result = await kanbanApi.specifyTask(taskId, { board, author })
|
||||
if (board === selectedBoard.value) await Promise.all([fetchTasks(true), fetchStats(), fetchBoards()])
|
||||
return result
|
||||
}
|
||||
|
||||
async function dispatch(opts: Omit<KanbanDispatchOptions, 'board'> = {}) {
|
||||
assertCapability('dispatch')
|
||||
const board = selectedBoard.value
|
||||
const result = await kanbanApi.dispatch({ ...opts, board })
|
||||
if (board === selectedBoard.value) await Promise.all([fetchTasks(true), fetchStats(), fetchBoards()])
|
||||
return result
|
||||
}
|
||||
|
||||
function setFilter(key: 'status' | 'assignee', value: string | null) {
|
||||
if (key === 'status') filterStatus.value = value
|
||||
else filterAssignee.value = value
|
||||
@@ -273,6 +338,7 @@ export const useKanbanStore = defineStore('kanban', () => {
|
||||
boards,
|
||||
capabilities,
|
||||
activeBoards,
|
||||
isCapabilitySupported,
|
||||
loading,
|
||||
boardsLoading,
|
||||
boardWarning,
|
||||
@@ -291,6 +357,13 @@ export const useKanbanStore = defineStore('kanban', () => {
|
||||
blockTask,
|
||||
unblockTasks,
|
||||
assignTask,
|
||||
addComment,
|
||||
getTaskLog,
|
||||
getDiagnostics,
|
||||
reclaimTask,
|
||||
reassignTask,
|
||||
specifyTask,
|
||||
dispatch,
|
||||
setFilter,
|
||||
setSelectedBoard,
|
||||
recoverSelectedBoard,
|
||||
|
||||
Reference in New Issue
Block a user