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:
2026-05-12 13:46:27 +08:00
co-authored by Claude Opus 4.7
parent a1529f18c1
commit ab53f34334
2 changed files with 102 additions and 52 deletions
+91 -46
View File
@@ -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
View File
@@ -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