fix(tasks): align HeicodeTaskCard fields with live mcp-server shape
End-to-end smoke against mcp-server (test account 55@55.com) revealed the actual task.card shape: { goal: string, scope: string[], generated_artifacts: string[], manager_actions: Array<{label, deeplink}> } My earlier TS types and TaskCardView assumed {objective, first_version_scope, auto_generated, pending_context} — keys that don't exist in the real response. Tasks created via the intent flow would have rendered with empty bullet lists. Changes: - lib/heicode-mcp.ts: rewrite HeicodeTaskCard to the live shape, add HeicodeManagerAction type - features/tasks/task-card-view.tsx: - read card.goal / card.scope / card.generated_artifacts - new readManagerActions() helper renders mcp-server's {label, deeplink} buttons in place of the hardcoded action row, with normalizeDeeplink() mapping /manager/resources → /sk-sources etc. to Manager-side routes - dropped pending_context (no such field); follow-up "Pending context" footnote is now a plain explanatory line per docs §10 Smoke verified end-to-end: login (POST /api/auth/login) → 200 + JWT intent (POST /api/user/tasks/intent) → 200 + configuring task list (GET /api/user/tasks) → items shape matches type detail (GET /api/user/tasks/{id}) → follow-ups parse correctly answer × 2 (POST .../answer) → state machine flips to running, card materialises with the 4 actual fields above audit (GET /api/agnet/audit-logs) → {items, total, next_cursor} agnet (GET /api/agnet/deployments) → {items, total} balance (GET /api/user/heicode/balance) → HEICODE_USER_NOT_FOUND (test account, expected; my code catches this and returns null) UTF-8 body through the proxy works fine (earlier "parse body" error was a Windows shell quoting issue, not a proxy bug). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+91
-46
@@ -39,6 +39,7 @@ import {
|
||||
answerHeicodeTask,
|
||||
getHeicodeTask,
|
||||
type HeicodeFollowup,
|
||||
type HeicodeManagerAction,
|
||||
type HeicodeTask,
|
||||
type HeicodeTaskStatus,
|
||||
} from '@/lib/heicode-mcp'
|
||||
@@ -103,6 +104,41 @@ function readScopeArray(card: HeicodeTask['card'], key: string): string[] {
|
||||
return []
|
||||
}
|
||||
|
||||
function readManagerActions(card: HeicodeTask['card']): HeicodeManagerAction[] {
|
||||
if (!card) return []
|
||||
const v = (card as Record<string, unknown>).manager_actions
|
||||
if (!Array.isArray(v)) return []
|
||||
return v
|
||||
.filter((x): x is HeicodeManagerAction =>
|
||||
typeof x === 'object' &&
|
||||
x != null &&
|
||||
typeof (x as Record<string, unknown>).label === 'string' &&
|
||||
typeof (x as Record<string, unknown>).deeplink === 'string'
|
||||
)
|
||||
.slice(0, 6)
|
||||
}
|
||||
|
||||
/** Resolve helper icon for a manager action by deeplink keyword. */
|
||||
function iconForAction(deeplink: string): typeof GitBranch {
|
||||
const k = deeplink.toLowerCase()
|
||||
if (k.includes('resource') || k.includes('preparation') || k.includes('sk-source')) return GitBranch
|
||||
if (k.includes('audit')) return ShieldCheck
|
||||
if (k.includes('wallet') || k.includes('budget')) return Wallet
|
||||
if (k.includes('event') || k.includes('activity')) return ScrollText
|
||||
return Rocket
|
||||
}
|
||||
|
||||
/** Manager-side route normalization. mcp-server returns
|
||||
* `/manager/resources?from=task` but Manager's actual route is `/sk-sources`. */
|
||||
function normalizeDeeplink(deeplink: string): string {
|
||||
return deeplink
|
||||
.replace(/^\/manager\/resources/i, '/sk-sources')
|
||||
.replace(/^\/manager\/team/i, '/sk-sources')
|
||||
.replace(/^\/manager\/audit/i, '/audit')
|
||||
.replace(/^\/manager\/wallet/i, '/wallet')
|
||||
.replace(/^\/manager\//i, '/')
|
||||
}
|
||||
|
||||
/** Collect open follow-ups from the most recent Heicode thread entry. */
|
||||
function collectOpenFollowups(task: HeicodeTask): HeicodeFollowup[] {
|
||||
for (let i = task.thread.length - 1; i >= 0; i--) {
|
||||
@@ -169,18 +205,21 @@ export function TaskCardView() {
|
||||
}
|
||||
|
||||
const openFollowups = collectOpenFollowups(task)
|
||||
// Card shape per live mcp-server smoke test (§6.4 returns):
|
||||
// { goal: string, scope: string[], generated_artifacts: string[],
|
||||
// manager_actions: Array<{label, deeplink}> }
|
||||
const objective =
|
||||
(task.card && (task.card as Record<string, unknown>).objective as string | undefined) ||
|
||||
task.intent ||
|
||||
(task.card && (task.card as Record<string, unknown>).goal as string | undefined) ||
|
||||
task.name ||
|
||||
task.intent ||
|
||||
t('No objective')
|
||||
const firstVersionScope =
|
||||
readScopeArray(task.card, 'first_version_scope').length > 0
|
||||
? readScopeArray(task.card, 'first_version_scope')
|
||||
readScopeArray(task.card, 'scope').length > 0
|
||||
? readScopeArray(task.card, 'scope')
|
||||
: [task.intent || objective]
|
||||
const autoGenerated =
|
||||
readScopeArray(task.card, 'auto_generated').length > 0
|
||||
? readScopeArray(task.card, 'auto_generated')
|
||||
readScopeArray(task.card, 'generated_artifacts').length > 0
|
||||
? readScopeArray(task.card, 'generated_artifacts')
|
||||
: [
|
||||
t('Product brief'),
|
||||
t('Prototype description'),
|
||||
@@ -188,14 +227,10 @@ export function TaskCardView() {
|
||||
t('Check list'),
|
||||
t('Deployment steps'),
|
||||
]
|
||||
const pendingContext =
|
||||
readScopeArray(task.card, 'pending_context').length > 0
|
||||
? readScopeArray(task.card, 'pending_context')
|
||||
: [
|
||||
t(
|
||||
'Code, docs, cloud resources and high-risk approval rules. Connect them in the preparation checklist.'
|
||||
),
|
||||
]
|
||||
const managerActions = readManagerActions(task.card)
|
||||
const pendingContextLine = t(
|
||||
'Code, docs, cloud resources and high-risk approval rules. Connect them in the preparation checklist.'
|
||||
)
|
||||
|
||||
return (
|
||||
<div className='mx-auto max-w-4xl space-y-5 p-6'>
|
||||
@@ -336,30 +371,47 @@ export function TaskCardView() {
|
||||
{t('Needs Manager assistance')}
|
||||
</p>
|
||||
<div className='mt-3 flex flex-wrap gap-2'>
|
||||
<Button asChild size='sm' variant='outline' className='rounded-xl'>
|
||||
<Link to='/sk-sources'>
|
||||
<GitBranch className='mr-1 h-3.5 w-3.5' />
|
||||
{t('Open preparation checklist')}
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild size='sm' variant='outline' className='rounded-xl'>
|
||||
<Link to='/audit'>
|
||||
<ShieldCheck className='mr-1 h-3.5 w-3.5' />
|
||||
{t('Audit & approvals')}
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild size='sm' variant='outline' className='rounded-xl'>
|
||||
<Link to='/wallet'>
|
||||
<Wallet className='mr-1 h-3.5 w-3.5' />
|
||||
{t('Budget & usage')}
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild size='sm' variant='outline' className='rounded-xl'>
|
||||
<Link to='/events'>
|
||||
<ScrollText className='mr-1 h-3.5 w-3.5' />
|
||||
{t('View activity')}
|
||||
</Link>
|
||||
</Button>
|
||||
{managerActions.length > 0 ? (
|
||||
managerActions.map((act) => {
|
||||
const Icon = iconForAction(act.deeplink)
|
||||
const to = normalizeDeeplink(act.deeplink).split('?')[0]
|
||||
return (
|
||||
<Button
|
||||
key={`${act.label}-${act.deeplink}`}
|
||||
asChild
|
||||
size='sm'
|
||||
variant='outline'
|
||||
className='rounded-xl'
|
||||
>
|
||||
<a href={to}>
|
||||
<Icon className='mr-1 h-3.5 w-3.5' />
|
||||
{act.label}
|
||||
</a>
|
||||
</Button>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<>
|
||||
<Button asChild size='sm' variant='outline' className='rounded-xl'>
|
||||
<Link to='/sk-sources'>
|
||||
<GitBranch className='mr-1 h-3.5 w-3.5' />
|
||||
{t('Open preparation checklist')}
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild size='sm' variant='outline' className='rounded-xl'>
|
||||
<Link to='/audit'>
|
||||
<ShieldCheck className='mr-1 h-3.5 w-3.5' />
|
||||
{t('Audit & approvals')}
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild size='sm' variant='outline' className='rounded-xl'>
|
||||
<Link to='/wallet'>
|
||||
<Wallet className='mr-1 h-3.5 w-3.5' />
|
||||
{t('Budget & usage')}
|
||||
</Link>
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -396,14 +448,7 @@ export function TaskCardView() {
|
||||
<p className='text-[11px] font-semibold tracking-[0.16em] text-muted-foreground uppercase'>
|
||||
{t('Pending context')}
|
||||
</p>
|
||||
<ul className='mt-2 space-y-1.5 text-sm text-muted-foreground'>
|
||||
{pendingContext.map((line, i) => (
|
||||
<li key={i} className='flex items-start gap-2'>
|
||||
<span className='mt-1.5 inline-block h-1 w-1 shrink-0 rounded-full bg-primary' />
|
||||
<span>{line}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<p className='mt-2 text-sm text-muted-foreground'>{pendingContextLine}</p>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
|
||||
+11
-6
@@ -106,13 +106,18 @@ export type HeicodeThreadEntry = {
|
||||
followups?: HeicodeFollowup[]
|
||||
}
|
||||
|
||||
/** Task card — populated when all followups answered (§6.4). */
|
||||
/** Task card — populated when all followups answered (§6.4).
|
||||
* Shape verified via live smoke test against mcp-server stub. */
|
||||
export type HeicodeManagerAction = {
|
||||
label: string
|
||||
deeplink: string
|
||||
}
|
||||
export type HeicodeTaskCard = {
|
||||
objective?: string
|
||||
first_version_scope?: string[]
|
||||
auto_generated?: string[]
|
||||
pending_context?: string[]
|
||||
// additional fields per HeicodeTask card contract
|
||||
goal?: string // 任务目标(docs §10 "目标")
|
||||
scope?: string[] // 第一版范围
|
||||
generated_artifacts?: string[] // Heicode 自动生成
|
||||
manager_actions?: HeicodeManagerAction[] // Manager 辅助按钮
|
||||
// forward-compat passthrough
|
||||
[key: string]: unknown
|
||||
} | null
|
||||
|
||||
|
||||
Reference in New Issue
Block a user