feat(manager): wire UI to mcp-server contract instead of local controllers
After reading the Heicode-接口契约文档 v2.2 in
~/Desktop/taijigit/taiji-AI-PAD/Docs/ the right data sources are clear:
§1 /api/auth/* — already wired (features/auth/api.ts)
§4 /api/user/heicode/* — model balance + usage + logs
§5 /api/agnet/* — Agnet platform stub (deployments / audit / etc)
§6 /api/user/tasks/* — HeicodeTask intent → followups → card
The first wave UI work (commit 2df233b) used Heicode-local controllers
as the data source (AgnetDeployment.orchestration_plan as a stand-in
for the task object). That was wrong — the contract document is clear
that HeicodeTask (§6) is the canonical user-facing task object, and the
mcp-server stub at §5 is the canonical deployment source.
This commit redirects the data plumbing without touching the UI shells:
1. New lib/heicode-mcp.ts — typed client that calls mcp-server through
the existing same-origin /api/heicode-auth/* proxy. Implements the
subset of §4/§5/§6 the Manager UI needs:
createTaskFromIntent / listHeicodeTasks / getHeicodeTask / answer
getHeicodeBalance / getHeicodeModels / getHeicodeUsage / getHeicodeLogs
listMcpAgnetDeployments / listMcpAuditLogs
2. HomeHero (features/dashboard/components/home-hero.tsx):
- Idea input now POSTs /api/user/tasks/intent and routes the user
to /tasks/$id once the server returns the new task with its first
round of follow-ups. Previously it only stashed the idea in
localStorage which the docs §10 didn't actually require.
- ContinueTasks + TodayFocus now consume listHeicodeTasks output
(HeicodeTask.status / status_caption / updated_at:ms) instead of
AgnetDeployment shape.
3. TaskCardView (features/tasks/task-card-view.tsx):
- Reads getHeicodeTask(id) from mcp-server (refetch every 15s).
- When status=configuring renders the open follow-ups from the most
recent heicode thread entry as clickable option chips; clicking
POSTs answer to /api/user/tasks/$id/answer and the server-side
state machine advances. high-risk options get a red badge per §6.
- When status=running (followups answered, card materialised) the
four blocks docs §10 任务卡 mandates are rendered from task.card:
目标 / 第一版范围 / 自动生成 / 待确认上下文.
4. AgnetAuditPage (features/agnet-console/pages.tsx):
- Switched queryFn from local getAgnetAuditLogs to mcp-server
listMcpAuditLogs. The redacted-card renderer already accepts any
{resource_id, allowed_actions, constraints, secret_ref} shape so
no UI change needed; banner still announces no plaintext.
Notes:
- /wallet refactor to §4 deferred — it pulls multiple legacy series
from the local NewAPI controllers and the rewrite is a separate
pass. Manager users see local data for now; the call is identical
shape so swap is mechanical once we get there.
- Local TS check clean. Not deployed.
- Earlier 2df233b's UI structures (HomeHero shape, TaskCard layout,
recommendation dialog, audit redacted view) stay verbatim — only
the data fetching layer moved.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -39,7 +39,6 @@ import {
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import {
|
||||
getAgnetAuditLogs,
|
||||
getAgnetDeploymentEvents,
|
||||
getAgnetSnapshots,
|
||||
createGitSource,
|
||||
@@ -54,6 +53,11 @@ import {
|
||||
type GitSourcePayload,
|
||||
type GitSourceUsage,
|
||||
} from './api'
|
||||
// /audit pulls from mcp-server §5.10 stub now, not the Heicode-local
|
||||
// controller — the contract doc names that endpoint as the canonical
|
||||
// source. The shape of McpAuditEntry is wider than the legacy local one
|
||||
// so the redacted-card renderer keeps working.
|
||||
import { listMcpAuditLogs } from '@/lib/heicode-mcp'
|
||||
import { CreateAgnetDeploymentSheet } from './create-agnet-deployment-sheet'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
@@ -818,8 +822,8 @@ export function AgnetAuditPage() {
|
||||
const [actionFilter, setActionFilter] = useState('')
|
||||
|
||||
const { data = [], isLoading } = useQuery({
|
||||
queryKey: ['agnet', 'audit'],
|
||||
queryFn: getAgnetAuditLogs,
|
||||
queryKey: ['heicode', 'agnet', 'audit'],
|
||||
queryFn: () => listMcpAuditLogs({ limit: 200 }),
|
||||
refetchInterval: 60_000,
|
||||
})
|
||||
|
||||
|
||||
@@ -18,10 +18,9 @@
|
||||
* 还没接,等接通后这个 stash 流程换成 POST /api/task。
|
||||
*/
|
||||
import { useCallback, useMemo, useRef, useState } from 'react'
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import { Link, useNavigate } from '@tanstack/react-router'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
import {
|
||||
ArrowRight,
|
||||
ArrowUpRight,
|
||||
@@ -41,9 +40,11 @@ import { Button } from '@/components/ui/button'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { cn } from '@/lib/utils'
|
||||
import {
|
||||
listAgnetDeploymentsQuiet,
|
||||
type AgnetDeployment,
|
||||
} from '@/features/agnet-console/api'
|
||||
createTaskFromIntent,
|
||||
listHeicodeTasks,
|
||||
type HeicodeTask,
|
||||
} from '@/lib/heicode-mcp'
|
||||
import { toast as sonnerToast } from 'sonner'
|
||||
|
||||
const DRAFT_STORAGE_KEY = 'heicode_idea_draft'
|
||||
|
||||
@@ -102,11 +103,10 @@ function StatusBadge({ phase }: { phase: string }) {
|
||||
)
|
||||
}
|
||||
|
||||
function formatRelativeTime(value?: string): string {
|
||||
if (!value) return '—'
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) return value
|
||||
const diff = Date.now() - date.getTime()
|
||||
function formatRelativeTimeMs(ms?: number): string {
|
||||
if (!ms) return '—'
|
||||
const diff = Date.now() - ms
|
||||
if (diff < 0) return '—'
|
||||
const sec = Math.round(diff / 1000)
|
||||
if (sec < 60) return `${sec}s`
|
||||
const min = Math.round(sec / 60)
|
||||
@@ -121,9 +121,16 @@ function IdeaInput({ t }: { t: ReturnType<typeof useTranslation>['t'] }) {
|
||||
if (typeof window === 'undefined') return ''
|
||||
return window.localStorage.getItem(DRAFT_STORAGE_KEY) ?? ''
|
||||
})
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const taRef = useRef<HTMLTextAreaElement | null>(null)
|
||||
const navigate = useNavigate()
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
// POST /api/user/tasks/intent on the mcp-server. Backend returns a new
|
||||
// HeicodeTask with status=configuring and a thread containing the first
|
||||
// round of follow-up questions. We navigate to /tasks/$id so the user
|
||||
// can answer them — that's where the recommendation summary materialises
|
||||
// (status flips to running once all followups answered, see §6.4).
|
||||
const handleSubmit = useCallback(async () => {
|
||||
const trimmed = value.trim()
|
||||
if (!trimmed) {
|
||||
taRef.current?.focus()
|
||||
@@ -134,8 +141,17 @@ function IdeaInput({ t }: { t: ReturnType<typeof useTranslation>['t'] }) {
|
||||
} catch {
|
||||
/* localStorage unavailable — ignore */
|
||||
}
|
||||
toast.success(t('Idea captured. Open the desktop client to continue.'))
|
||||
}, [value, t])
|
||||
setSubmitting(true)
|
||||
try {
|
||||
const task = await createTaskFromIntent(trimmed)
|
||||
sonnerToast.success(t('Task drafted. Answer the follow-ups to build the recommendation.'))
|
||||
void navigate({ to: '/tasks/$id', params: { id: task.id } })
|
||||
} catch (err) {
|
||||
sonnerToast.error(err instanceof Error ? err.message : t('Failed to create task'))
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}, [value, t, navigate])
|
||||
|
||||
return (
|
||||
<section
|
||||
@@ -225,22 +241,18 @@ function IdeaInput({ t }: { t: ReturnType<typeof useTranslation>['t'] }) {
|
||||
}
|
||||
|
||||
function ContinueTasks({
|
||||
deployments,
|
||||
tasks,
|
||||
isLoading,
|
||||
t,
|
||||
}: {
|
||||
deployments: AgnetDeployment[]
|
||||
tasks: HeicodeTask[]
|
||||
isLoading: boolean
|
||||
t: ReturnType<typeof useTranslation>['t']
|
||||
}) {
|
||||
const recent = useMemo(() => {
|
||||
const sorted = [...deployments].sort((a, b) => {
|
||||
const at = new Date(a.updated_at || a.created_at || 0).getTime()
|
||||
const bt = new Date(b.updated_at || b.created_at || 0).getTime()
|
||||
return bt - at
|
||||
})
|
||||
const sorted = [...tasks].sort((a, b) => (b.updated_at || 0) - (a.updated_at || 0))
|
||||
return sorted.slice(0, 4)
|
||||
}, [deployments])
|
||||
}, [tasks])
|
||||
|
||||
return (
|
||||
<section className='rounded-2xl border border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] bg-[color-mix(in_oklch,var(--card)_60%,transparent)] p-5'>
|
||||
@@ -279,24 +291,25 @@ function ContinueTasks({
|
||||
</p>
|
||||
) : (
|
||||
<ul className='space-y-2'>
|
||||
{recent.map((dep) => (
|
||||
<li key={dep.deployment_id}>
|
||||
{recent.map((task) => (
|
||||
<li key={task.id}>
|
||||
<Link
|
||||
to='/tasks/$id'
|
||||
params={{ id: dep.deployment_id }}
|
||||
params={{ id: task.id }}
|
||||
className='flex items-start justify-between gap-3 rounded-xl border border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] bg-background/40 p-3 transition hover:-translate-y-px hover:border-primary/40 hover:shadow-[0_18px_48px_-32px_rgba(123,107,227,0.4)]'
|
||||
>
|
||||
<div className='min-w-0 flex-1'>
|
||||
<p className='line-clamp-1 text-sm font-medium'>
|
||||
{dep.orchestration_plan?.objective ||
|
||||
dep.orchestration_plan?.template_hint ||
|
||||
t('No objective')}
|
||||
{task.name || task.intent || t('Untitled task')}
|
||||
</p>
|
||||
<p className='mt-1 text-[11px] text-muted-foreground'>
|
||||
{t('Updated')} {formatRelativeTime(dep.updated_at || dep.created_at)} {t('ago')}
|
||||
{task.status_caption || task.status}
|
||||
{' · '}
|
||||
{t('Updated')}{' '}
|
||||
{formatRelativeTimeMs(task.updated_at)} {t('ago')}
|
||||
</p>
|
||||
</div>
|
||||
<StatusBadge phase={dep.phase || dep.status || 'pending'} />
|
||||
<StatusBadge phase={task.status} />
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
@@ -307,26 +320,25 @@ function ContinueTasks({
|
||||
}
|
||||
|
||||
function TodayFocus({
|
||||
deployments,
|
||||
tasks,
|
||||
isLoading,
|
||||
t,
|
||||
}: {
|
||||
deployments: AgnetDeployment[]
|
||||
tasks: HeicodeTask[]
|
||||
isLoading: boolean
|
||||
t: ReturnType<typeof useTranslation>['t']
|
||||
}) {
|
||||
const buckets = useMemo(() => {
|
||||
const failed = deployments.filter(
|
||||
(d) => classifyStatus(d.phase || d.status || '') === 'failed'
|
||||
)
|
||||
const pending = deployments.filter(
|
||||
(d) => classifyStatus(d.phase || d.status || '') === 'pending'
|
||||
)
|
||||
const running = deployments.filter(
|
||||
(d) => classifyStatus(d.phase || d.status || '') === 'running'
|
||||
const failed = tasks.filter((task) => classifyStatus(task.status) === 'failed')
|
||||
// "Pending confirmation" maps to HeicodeTask.status = 'configuring'
|
||||
// (followups not all answered yet) AND tasks awaiting_approval.
|
||||
const pending = tasks.filter(
|
||||
(task) =>
|
||||
task.status === 'configuring' || task.status === 'awaiting_approval'
|
||||
)
|
||||
const running = tasks.filter((task) => classifyStatus(task.status) === 'running')
|
||||
return { failed, pending, running }
|
||||
}, [deployments])
|
||||
}, [tasks])
|
||||
|
||||
const focusItems: Array<{
|
||||
Icon: typeof XCircle
|
||||
@@ -504,18 +516,22 @@ function HelperEntries({
|
||||
|
||||
export function HomeHeroView() {
|
||||
const { t } = useTranslation()
|
||||
const { data = [], isLoading } = useQuery({
|
||||
queryKey: ['agnet', 'deployments'],
|
||||
queryFn: listAgnetDeploymentsQuiet,
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['heicode', 'tasks', 'recent'],
|
||||
queryFn: () => listHeicodeTasks({ limit: 20 }),
|
||||
refetchInterval: 60_000,
|
||||
// mcp-server may be unreachable while the JWT bridge isn't established.
|
||||
// Treat that as empty list instead of a hard error in the hero.
|
||||
retry: false,
|
||||
})
|
||||
const tasks = data?.items ?? []
|
||||
|
||||
return (
|
||||
<div className='space-y-5'>
|
||||
<IdeaInput t={t} />
|
||||
<div className='grid gap-5 lg:grid-cols-[minmax(0,1.05fr)_minmax(0,1fr)]'>
|
||||
<ContinueTasks deployments={data} isLoading={isLoading} t={t} />
|
||||
<TodayFocus deployments={data} isLoading={isLoading} t={t} />
|
||||
<ContinueTasks tasks={tasks} isLoading={isLoading} t={t} />
|
||||
<TodayFocus tasks={tasks} isLoading={isLoading} t={t} />
|
||||
</div>
|
||||
<HelperEntries t={t} />
|
||||
</div>
|
||||
|
||||
+162
-72
@@ -1,19 +1,20 @@
|
||||
/**
|
||||
* Task Card — implements docs/product-package/10 §"任务卡" + 11 §3
|
||||
* 任务卡 wireframe. Renders one AgnetDeployment as the user-facing
|
||||
* task object: 目标 / 第一版范围 / 自动生成 / 待确认上下文 + Manager 辅助按钮.
|
||||
* Task Card route — implements docs/product-package/10 §"任务卡" + 11 §3
|
||||
* 任务卡 wireframe, backed by mcp-server §6 HeicodeTask object:
|
||||
*
|
||||
* Source of data: existing /api/agnet/deployments list (filtered by id).
|
||||
* When backend gains a /api/task/$id idea-task endpoint we'll switch the
|
||||
* data source over; the card shape stays the same.
|
||||
* - status = "configuring" → render the open follow-up questions
|
||||
* (each option becomes a clickable chip; clicking POSTs answer to
|
||||
* /api/user/tasks/$id/answer and the backend state machine advances).
|
||||
* - status = "running" / etc → render task.card with the four blocks
|
||||
* the spec mandates: 目标 / 第一版范围 / 自动生成 / 待确认上下文.
|
||||
*
|
||||
* Forbidden per §10 "高级展开": no JSON editor, no payload table, no
|
||||
* permission manifest, no resource_grant editor. Only show脱敏 summary.
|
||||
* Forbidden per §10 高级展开: no JSON editor, no permission manifest, no
|
||||
* resource_grant editor. Only the user-facing summary fields.
|
||||
*/
|
||||
import { useMemo } from 'react'
|
||||
import { Link, getRouteApi } from '@tanstack/react-router'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
import {
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
@@ -21,6 +22,7 @@ import {
|
||||
CircleDashed,
|
||||
GitBranch,
|
||||
ListChecks,
|
||||
MessageSquare,
|
||||
PencilLine,
|
||||
PlayCircle,
|
||||
Rocket,
|
||||
@@ -34,35 +36,33 @@ import { Button } from '@/components/ui/button'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { cn } from '@/lib/utils'
|
||||
import {
|
||||
listAgnetDeploymentsQuiet,
|
||||
type AgnetDeployment,
|
||||
} from '@/features/agnet-console/api'
|
||||
answerHeicodeTask,
|
||||
getHeicodeTask,
|
||||
type HeicodeFollowup,
|
||||
type HeicodeTask,
|
||||
type HeicodeTaskStatus,
|
||||
} from '@/lib/heicode-mcp'
|
||||
|
||||
const route = getRouteApi('/_authenticated/tasks/$id')
|
||||
|
||||
type StatusKey = 'running' | 'success' | 'failed' | 'pending'
|
||||
|
||||
const STATUS_MAP: Record<string, StatusKey> = {
|
||||
const STATUS_MAP: Record<HeicodeTaskStatus | string, StatusKey> = {
|
||||
draft: 'pending',
|
||||
configuring: 'pending',
|
||||
awaiting_approval: 'pending',
|
||||
running: 'running',
|
||||
active: 'running',
|
||||
in_progress: 'running',
|
||||
succeeded: 'success',
|
||||
success: 'success',
|
||||
paused: 'pending',
|
||||
completed: 'success',
|
||||
failed: 'failed',
|
||||
error: 'failed',
|
||||
rejected: 'failed',
|
||||
pending: 'pending',
|
||||
queued: 'pending',
|
||||
awaiting: 'pending',
|
||||
}
|
||||
|
||||
function classifyStatus(s: string): StatusKey {
|
||||
return STATUS_MAP[(s || '').toLowerCase()] ?? 'pending'
|
||||
return STATUS_MAP[s] ?? 'pending'
|
||||
}
|
||||
|
||||
function StatusBadge({ phase }: { phase: string }) {
|
||||
const k = classifyStatus(phase)
|
||||
function StatusBadge({ status }: { status: HeicodeTaskStatus | string }) {
|
||||
const k = classifyStatus(status)
|
||||
const map: Record<StatusKey, { cls: string; Icon: typeof PlayCircle }> = {
|
||||
running: {
|
||||
cls: 'bg-[color-mix(in_oklch,var(--primary)_22%,transparent)] text-primary ring-primary/40',
|
||||
@@ -90,42 +90,55 @@ function StatusBadge({ phase }: { phase: string }) {
|
||||
)}
|
||||
>
|
||||
<p.Icon className='h-3 w-3' />
|
||||
{phase || 'pending'}
|
||||
{status}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive the "第一版范围" (first-version scope) bullet list from the
|
||||
* orchestration_plan.agents goals. If the plan didn't capture explicit
|
||||
* scope items we fall back to a single "see objective" line so the card
|
||||
* never renders a blank section.
|
||||
*/
|
||||
function deriveFirstVersionScope(dep: AgnetDeployment, fallback: string): string[] {
|
||||
const agents = dep.orchestration_plan?.agents ?? []
|
||||
const goals = agents
|
||||
.map((a) => (a.goal || '').trim())
|
||||
.filter((g) => g.length > 0)
|
||||
if (goals.length > 0) return goals.slice(0, 6)
|
||||
return [fallback]
|
||||
function readScopeArray(card: HeicodeTask['card'], key: string): string[] {
|
||||
if (!card) return []
|
||||
const v = (card as Record<string, unknown>)[key]
|
||||
if (Array.isArray(v)) return v.filter((x): x is string => typeof x === 'string')
|
||||
if (typeof v === 'string' && v.trim()) return [v]
|
||||
return []
|
||||
}
|
||||
|
||||
/** 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--) {
|
||||
const entry = task.thread[i]!
|
||||
if (entry.kind === 'heicode' && entry.followups && entry.followups.length > 0) {
|
||||
return entry.followups
|
||||
}
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
export function TaskCardView() {
|
||||
const { t } = useTranslation()
|
||||
const { id } = route.useParams()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
// Source: the existing deployments list (filter by id). When the backend
|
||||
// gains a /api/task/$id idea-task endpoint we swap this for a single
|
||||
// useQuery against that endpoint.
|
||||
const { data: deployments = [], isLoading } = useQuery({
|
||||
queryKey: ['agnet', 'deployments'],
|
||||
queryFn: listAgnetDeploymentsQuiet,
|
||||
const { data: task, isLoading } = useQuery({
|
||||
queryKey: ['heicode', 'task', id],
|
||||
queryFn: () => getHeicodeTask(id),
|
||||
refetchInterval: 15_000,
|
||||
})
|
||||
|
||||
const dep = useMemo(
|
||||
() => deployments.find((d) => d.deployment_id === id),
|
||||
[deployments, id]
|
||||
)
|
||||
const answerMutation = useMutation({
|
||||
mutationFn: ({ qid, oid }: { qid: string; oid: string }) =>
|
||||
answerHeicodeTask(id, qid, oid),
|
||||
onSuccess: (updated) => {
|
||||
queryClient.setQueryData(['heicode', 'task', id], updated)
|
||||
void queryClient.invalidateQueries({ queryKey: ['heicode', 'tasks', 'recent'] })
|
||||
if (updated.status === 'running') {
|
||||
toast.success(t('All follow-ups answered. Heicode generated the recommendation summary.'))
|
||||
}
|
||||
},
|
||||
onError: (err) => {
|
||||
toast.error(err instanceof Error ? err.message : t('Failed to answer follow-up'))
|
||||
},
|
||||
})
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
@@ -137,7 +150,7 @@ export function TaskCardView() {
|
||||
)
|
||||
}
|
||||
|
||||
if (!dep) {
|
||||
if (!task) {
|
||||
return (
|
||||
<div className='mx-auto max-w-3xl space-y-4 p-6'>
|
||||
<Button asChild variant='ghost' size='sm' className='gap-1'>
|
||||
@@ -155,23 +168,34 @@ export function TaskCardView() {
|
||||
)
|
||||
}
|
||||
|
||||
const phase = dep.phase || dep.status
|
||||
const openFollowups = collectOpenFollowups(task)
|
||||
const objective =
|
||||
dep.orchestration_plan?.objective ||
|
||||
dep.orchestration_plan?.template_hint ||
|
||||
(task.card && (task.card as Record<string, unknown>).objective as string | undefined) ||
|
||||
task.intent ||
|
||||
task.name ||
|
||||
t('No objective')
|
||||
const firstVersionScope = deriveFirstVersionScope(dep, objective)
|
||||
|
||||
// Static text — Heicode platform always produces these artifacts. Per
|
||||
// §11 §3 wireframe the card literally lists them so the user knows the
|
||||
// platform is doing the heavy lifting.
|
||||
const autoGenerated = [
|
||||
const firstVersionScope =
|
||||
readScopeArray(task.card, 'first_version_scope').length > 0
|
||||
? readScopeArray(task.card, 'first_version_scope')
|
||||
: [task.intent || objective]
|
||||
const autoGenerated =
|
||||
readScopeArray(task.card, 'auto_generated').length > 0
|
||||
? readScopeArray(task.card, 'auto_generated')
|
||||
: [
|
||||
t('Product brief'),
|
||||
t('Prototype description'),
|
||||
t('Development tasks'),
|
||||
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.'
|
||||
),
|
||||
]
|
||||
|
||||
return (
|
||||
<div className='mx-auto max-w-4xl space-y-5 p-6'>
|
||||
@@ -183,10 +207,75 @@ export function TaskCardView() {
|
||||
</Link>
|
||||
</Button>
|
||||
<span className='font-mono text-[11px] text-muted-foreground'>
|
||||
{dep.deployment_id}
|
||||
{task.id}
|
||||
</span>
|
||||
</header>
|
||||
|
||||
{/* ── Follow-up questions (only while status=configuring) ─────────── */}
|
||||
{task.status === 'configuring' && openFollowups.length > 0 && (
|
||||
<section
|
||||
className='overflow-hidden rounded-3xl border p-5 sm:p-6'
|
||||
style={{
|
||||
borderColor: 'rgba(123,107,227,0.28)',
|
||||
backgroundImage:
|
||||
'linear-gradient(180deg, rgba(123,107,227,0.07) 0%, rgba(123,107,227,0.00) 60%)',
|
||||
}}
|
||||
>
|
||||
<div className='flex items-start gap-3'>
|
||||
<span
|
||||
className='inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-2xl text-white'
|
||||
style={{ backgroundImage: 'var(--gradient-brand)' }}
|
||||
>
|
||||
<MessageSquare className='h-4 w-4' />
|
||||
</span>
|
||||
<div className='min-w-0 flex-1'>
|
||||
<p className='text-[11px] font-semibold tracking-[0.16em] text-muted-foreground uppercase'>
|
||||
{t('Heicode is asking')}
|
||||
</p>
|
||||
<p className='mt-1 text-sm text-foreground'>
|
||||
{task.status_caption || t('Answer a few questions so Heicode can draft the right plan.')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ol className='mt-5 space-y-4'>
|
||||
{openFollowups.map((q) => (
|
||||
<li
|
||||
key={q.id}
|
||||
className='rounded-2xl border border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] bg-background/40 p-4'
|
||||
>
|
||||
<p className='text-sm font-medium'>{q.question}</p>
|
||||
<div className='mt-3 flex flex-wrap gap-2'>
|
||||
{q.options.map((opt) => (
|
||||
<Button
|
||||
key={opt.id}
|
||||
type='button'
|
||||
size='sm'
|
||||
variant='outline'
|
||||
disabled={answerMutation.isPending}
|
||||
onClick={() => answerMutation.mutate({ qid: q.id, oid: opt.id })}
|
||||
className={cn(
|
||||
'rounded-xl text-xs',
|
||||
opt.risk === 'high-risk' &&
|
||||
'border-rose-500/40 text-rose-300 hover:border-rose-500/70'
|
||||
)}
|
||||
>
|
||||
{opt.label}
|
||||
{opt.risk === 'high-risk' && (
|
||||
<span className='ms-1 rounded-full bg-rose-500/15 px-1.5 text-[9px] uppercase tracking-wider text-rose-300'>
|
||||
{t('high-risk')}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* ── Task card (objective / scope / auto / pending) ───────────── */}
|
||||
<section
|
||||
className='overflow-hidden rounded-3xl border p-6 sm:p-8'
|
||||
style={{
|
||||
@@ -203,12 +292,14 @@ export function TaskCardView() {
|
||||
<h1 className='mt-2 text-2xl font-semibold tracking-tight sm:text-3xl'>
|
||||
{objective}
|
||||
</h1>
|
||||
{task.intent && task.intent !== objective && (
|
||||
<p className='mt-2 text-sm text-muted-foreground'>{task.intent}</p>
|
||||
)}
|
||||
</div>
|
||||
<StatusBadge phase={phase} />
|
||||
<StatusBadge status={task.status} />
|
||||
</div>
|
||||
|
||||
<div className='mt-6 grid gap-5 md:grid-cols-2'>
|
||||
{/* 第一版范围 */}
|
||||
<div className='rounded-2xl border border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] bg-background/40 p-4'>
|
||||
<p className='flex items-center gap-1.5 text-sm font-semibold'>
|
||||
<ListChecks className='h-4 w-4 text-primary' />
|
||||
@@ -224,7 +315,6 @@ export function TaskCardView() {
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* 自动生成 */}
|
||||
<div className='rounded-2xl border border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] bg-background/40 p-4'>
|
||||
<p className='flex items-center gap-1.5 text-sm font-semibold'>
|
||||
<Sparkles className='h-4 w-4 text-primary' />
|
||||
@@ -241,7 +331,6 @@ export function TaskCardView() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 需要 Manager 辅助完成 */}
|
||||
<div className='mt-5 rounded-2xl border border-dashed border-[color-mix(in_oklch,var(--primary)_22%,var(--border))] bg-background/30 p-4'>
|
||||
<p className='text-[11px] font-semibold tracking-[0.14em] text-muted-foreground uppercase'>
|
||||
{t('Needs Manager assistance')}
|
||||
@@ -274,7 +363,6 @@ export function TaskCardView() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CTA footer */}
|
||||
<footer className='mt-6 flex flex-wrap items-center justify-between gap-3 border-t border-dashed border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] pt-4'>
|
||||
<Button
|
||||
variant='ghost'
|
||||
@@ -304,16 +392,18 @@ export function TaskCardView() {
|
||||
</footer>
|
||||
</section>
|
||||
|
||||
{/* 待确认上下文 — surfaced as a small follow-up card under the main one */}
|
||||
<section className='rounded-2xl border border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] bg-[color-mix(in_oklch,var(--card)_55%,transparent)] p-4'>
|
||||
<p className='text-[11px] font-semibold tracking-[0.16em] text-muted-foreground uppercase'>
|
||||
{t('Pending context')}
|
||||
</p>
|
||||
<p className='mt-2 text-sm text-muted-foreground'>
|
||||
{t(
|
||||
'Code, docs, cloud resources and high-risk approval rules. Connect them in the preparation checklist; Heicode will auto-discover what it can and only ask for the rest.'
|
||||
)}
|
||||
</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>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -204,6 +204,7 @@
|
||||
"All": "All",
|
||||
"All edits are overwrite operations. Leave fields empty to keep current values unchanged.": "All edits are overwrite operations. Leave fields empty to keep current values unchanged.",
|
||||
"All files exceed the maximum size.": "All files exceed the maximum size.",
|
||||
"All follow-ups answered. Heicode generated the recommendation summary.": "All follow-ups answered. Heicode generated the recommendation summary.",
|
||||
"All Groups": "All Groups",
|
||||
"All Models": "All Models",
|
||||
"All models in use are properly configured.": "All models in use are properly configured.",
|
||||
@@ -274,6 +275,7 @@
|
||||
"Announcements": "Announcements",
|
||||
"Announcements saved successfully": "Announcements saved successfully",
|
||||
"Answer": "Answer",
|
||||
"Answer a few questions so Heicode can draft the right plan.": "Answer a few questions so Heicode can draft the right plan.",
|
||||
"Anthropic": "Anthropic",
|
||||
"API Addresses": "API Addresses",
|
||||
"API Base URL (Important: Not Chat API) *": "API Base URL (Important: Not Chat API) *",
|
||||
@@ -1413,6 +1415,7 @@
|
||||
"Failed (visible window)": "Failed (visible window)",
|
||||
"Failed tasks": "Failed tasks",
|
||||
"Failed to adjust quota": "Failed to adjust quota",
|
||||
"Failed to answer follow-up": "Failed to answer follow-up",
|
||||
"Failed to apply overwrite.": "Failed to apply overwrite.",
|
||||
"Failed to bind email": "Failed to bind email",
|
||||
"Failed to change password": "Failed to change password",
|
||||
@@ -1431,6 +1434,7 @@
|
||||
"Failed to create deployment": "Failed to create deployment",
|
||||
"Failed to create provider": "Failed to create provider",
|
||||
"Failed to create redemption code": "Failed to create redemption code",
|
||||
"Failed to create task": "Failed to create task",
|
||||
"Failed to create user": "Failed to create user",
|
||||
"Failed to delete account": "Failed to delete account",
|
||||
"Failed to delete API key": "Failed to delete API key",
|
||||
@@ -1754,12 +1758,14 @@
|
||||
"Heicode auto-generates": "Heicode auto-generates",
|
||||
"Heicode desktop client": "Heicode desktop client",
|
||||
"Heicode generates the parameters automatically. You only confirm allowed scope and risk.": "Heicode generates the parameters automatically. You only confirm allowed scope and risk.",
|
||||
"Heicode is asking": "Heicode is asking",
|
||||
"Heicode Manager only captures the idea. The main task conversation happens in the desktop client.": "Heicode Manager only captures the idea. The main task conversation happens in the desktop client.",
|
||||
"Helpers": "Helpers",
|
||||
"Hidden — verify to reveal": "Hidden — verify to reveal",
|
||||
"Hide": "Hide",
|
||||
"Hide API key": "Hide API key",
|
||||
"High Performance": "High Performance",
|
||||
"high-risk": "high-risk",
|
||||
"High-risk action": "High-risk action",
|
||||
"High-risk operation confirmation": "High-risk Operation Confirmation",
|
||||
"High-risk rules": "High-risk rules",
|
||||
@@ -3453,6 +3459,7 @@
|
||||
"Target group": "Target group",
|
||||
"Task": "Task",
|
||||
"Task card": "Task card",
|
||||
"Task drafted. Answer the follow-ups to build the recommendation.": "Task drafted. Answer the follow-ups to build the recommendation.",
|
||||
"Task ID": "Task ID",
|
||||
"Task ID:": "Task ID:",
|
||||
"Task logs": "Task logs",
|
||||
@@ -3704,6 +3711,7 @@
|
||||
"Unsaved changes": "Unsaved changes",
|
||||
"Until": "Until",
|
||||
"Untitled": "Untitled",
|
||||
"Untitled task": "Untitled task",
|
||||
"Untrusted upstream data:": "Untrusted upstream data:",
|
||||
"Unused": "Unused",
|
||||
"Update": "Update",
|
||||
|
||||
@@ -204,6 +204,7 @@
|
||||
"All": "全部",
|
||||
"All edits are overwrite operations. Leave fields empty to keep current values unchanged.": "所有编辑都是覆盖操作。留空字段将保持当前值不变。",
|
||||
"All files exceed the maximum size.": "所有文件都超过最大尺寸。",
|
||||
"All follow-ups answered. Heicode generated the recommendation summary.": "所有追问已回答,Heicode 已生成推荐摘要。",
|
||||
"All Groups": "所有分组",
|
||||
"All Models": "所有模型",
|
||||
"All models in use are properly configured.": "所有正在使用的模型都已正确配置。",
|
||||
@@ -274,6 +275,7 @@
|
||||
"Announcements": "公告",
|
||||
"Announcements saved successfully": "公告保存成功",
|
||||
"Answer": "答案",
|
||||
"Answer a few questions so Heicode can draft the right plan.": "回答几个问题,让 Heicode 起草合适的执行计划。",
|
||||
"Anthropic": "Anthropic",
|
||||
"API Addresses": "API 地址",
|
||||
"API Base URL (Important: Not Chat API) *": "API 基础 URL (重要:非聊天 API) *",
|
||||
@@ -1413,6 +1415,7 @@
|
||||
"Failed (visible window)": "失败(当前窗口)",
|
||||
"Failed tasks": "失败任务",
|
||||
"Failed to adjust quota": "调整额度失败",
|
||||
"Failed to answer follow-up": "回答追问失败",
|
||||
"Failed to apply overwrite.": "应用覆盖失败。",
|
||||
"Failed to bind email": "绑定邮箱失败",
|
||||
"Failed to change password": "修改密码失败",
|
||||
@@ -1431,6 +1434,7 @@
|
||||
"Failed to create deployment": "创建部署失败",
|
||||
"Failed to create provider": "创建提供商失败",
|
||||
"Failed to create redemption code": "创建兑换码失败",
|
||||
"Failed to create task": "创建任务失败",
|
||||
"Failed to create user": "创建用户失败",
|
||||
"Failed to delete account": "删除账号失败",
|
||||
"Failed to delete API key": "删除API密钥失败",
|
||||
@@ -1754,12 +1758,14 @@
|
||||
"Heicode auto-generates": "Heicode 将自动生成",
|
||||
"Heicode desktop client": "Heicode 桌面客户端",
|
||||
"Heicode generates the parameters automatically. You only confirm allowed scope and risk.": "Heicode 自动生成参数,你只需要确认允许范围和风险",
|
||||
"Heicode is asking": "Heicode 在追问",
|
||||
"Heicode Manager only captures the idea. The main task conversation happens in the desktop client.": "Manager 只暂存想法。主要的任务对话和推进发生在客户端。",
|
||||
"Helpers": "辅助入口",
|
||||
"Hidden — verify to reveal": "隐藏 — 验证以显示",
|
||||
"Hide": "隐藏",
|
||||
"Hide API key": "隐藏 API 密钥",
|
||||
"High Performance": "高性能",
|
||||
"high-risk": "高风险",
|
||||
"High-risk action": "高危操作",
|
||||
"High-risk operation confirmation": "高危操作确认",
|
||||
"High-risk rules": "高危规则",
|
||||
@@ -3453,6 +3459,7 @@
|
||||
"Target group": "目标分组",
|
||||
"Task": "任务",
|
||||
"Task card": "任务卡",
|
||||
"Task drafted. Answer the follow-ups to build the recommendation.": "已生成任务草案,回答几个追问就能拿到推荐摘要。",
|
||||
"Task ID": "任务 ID",
|
||||
"Task ID:": "任务 ID:",
|
||||
"Task logs": "任务日志",
|
||||
@@ -3704,6 +3711,7 @@
|
||||
"Unsaved changes": "未保存的更改",
|
||||
"Until": "至",
|
||||
"Untitled": "未命名",
|
||||
"Untitled task": "未命名任务",
|
||||
"Untrusted upstream data:": "不受信任的上游数据:",
|
||||
"Unused": "未使用",
|
||||
"Update": "更新",
|
||||
|
||||
+334
@@ -0,0 +1,334 @@
|
||||
/**
|
||||
* mcp-server (Heicode Manager backend at apimtaiji.azure-api.net/api/mcp)
|
||||
* typed client.
|
||||
*
|
||||
* All calls go through the same-origin proxy at /api/heicode-auth/*
|
||||
* (Heicode Go backend → mcp-server) so the browser never has to deal with
|
||||
* APIM CORS. The proxy is implemented in heicode/controller/heicode_auth_proxy.go
|
||||
* and forwards the Authorization header verbatim.
|
||||
*
|
||||
* Endpoint surface follows
|
||||
* docs/product-package/../taijigit/Heicode-接口契约文档.md v2.2:
|
||||
* §1 /api/auth/* authentication (handled by features/auth/api.ts)
|
||||
* §2 /api/resources/* ResourceBinding (5)
|
||||
* §3 /api/resource-grants/* ResourceGrant (4)
|
||||
* §4 /api/user/heicode/* NewAPI metadata passthrough (4)
|
||||
* §5 /api/agnet/* Agnet platform stub (12 mock endpoints)
|
||||
* §6 /api/user/tasks/* HeicodeTask orchestration (5)
|
||||
*
|
||||
* The shapes below are what mcp-server actually returns — they intentionally
|
||||
* differ from the Heicode-local /api/agnet/* shapes used in earlier UI work.
|
||||
* That earlier work treated AgnetDeployment as the user-facing task object;
|
||||
* the contract document is clear that HeicodeTask (§6) is the right object.
|
||||
*/
|
||||
|
||||
const MCP_BASE = (
|
||||
(import.meta.env.VITE_HEICODE_AUTH_BASE_URL as string | undefined) ||
|
||||
'/api/heicode-auth'
|
||||
).replace(/\/+$/, '')
|
||||
|
||||
const ACCESS_TOKEN_KEY = 'heicode_access_token'
|
||||
|
||||
function readToken(): string {
|
||||
if (typeof window === 'undefined') return ''
|
||||
return window.localStorage.getItem(ACCESS_TOKEN_KEY) || ''
|
||||
}
|
||||
|
||||
async function mcpFetch<T>(
|
||||
path: string,
|
||||
init: RequestInit = {}
|
||||
): Promise<T> {
|
||||
const token = readToken()
|
||||
const requestId = `heicode-mcp-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`
|
||||
const res = await fetch(`${MCP_BASE}${path}`, {
|
||||
...init,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'X-Request-Id': requestId,
|
||||
...(init.body && !init.headers
|
||||
? { 'Content-Type': 'application/json' }
|
||||
: {}),
|
||||
...(init.headers || {}),
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
},
|
||||
})
|
||||
let data: unknown = null
|
||||
try {
|
||||
data = await res.json()
|
||||
} catch {
|
||||
data = null
|
||||
}
|
||||
if (!res.ok) {
|
||||
const detail =
|
||||
(data as { detail?: string | { message?: string; code?: string } } | null)
|
||||
?.detail
|
||||
const message =
|
||||
typeof detail === 'string'
|
||||
? detail
|
||||
: detail?.message ||
|
||||
(data as { message?: string } | null)?.message ||
|
||||
`mcp-server request failed (${res.status})`
|
||||
throw new Error(message)
|
||||
}
|
||||
return data as T
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// §6 HeicodeTask (任务编排) — 5 endpoints
|
||||
// Shape per contract doc §6.1-6.5.
|
||||
// =============================================================================
|
||||
|
||||
export type HeicodeTaskStatus =
|
||||
| 'draft'
|
||||
| 'configuring'
|
||||
| 'running'
|
||||
| 'awaiting_approval'
|
||||
| 'completed'
|
||||
| 'failed'
|
||||
| 'paused'
|
||||
|
||||
export type HeicodeFollowupOption = {
|
||||
id: string
|
||||
label: string
|
||||
risk?: 'high-risk' | string
|
||||
}
|
||||
|
||||
export type HeicodeFollowup = {
|
||||
id: string
|
||||
question: string
|
||||
options: HeicodeFollowupOption[]
|
||||
}
|
||||
|
||||
export type HeicodeThreadEntry = {
|
||||
kind: 'user' | 'heicode'
|
||||
text: string
|
||||
at: number
|
||||
followups?: HeicodeFollowup[]
|
||||
}
|
||||
|
||||
/** Task card — populated when all followups answered (§6.4). */
|
||||
export type HeicodeTaskCard = {
|
||||
objective?: string
|
||||
first_version_scope?: string[]
|
||||
auto_generated?: string[]
|
||||
pending_context?: string[]
|
||||
// additional fields per HeicodeTask card contract
|
||||
[key: string]: unknown
|
||||
} | null
|
||||
|
||||
export type HeicodeTask = {
|
||||
id: string
|
||||
user_id: string
|
||||
name: string
|
||||
status: HeicodeTaskStatus
|
||||
status_caption?: string
|
||||
intent: string
|
||||
thread: HeicodeThreadEntry[]
|
||||
card: HeicodeTaskCard
|
||||
created_at: number
|
||||
updated_at: number
|
||||
}
|
||||
|
||||
type Envelope<T> = { success: boolean; data?: T; message?: string }
|
||||
|
||||
export async function createTaskFromIntent(intent: string, name?: string): Promise<HeicodeTask> {
|
||||
const env = await mcpFetch<Envelope<HeicodeTask>>('/api/user/tasks/intent', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ intent, ...(name ? { name } : {}) }),
|
||||
})
|
||||
if (!env.success || !env.data) {
|
||||
throw new Error(env.message || 'createTaskFromIntent failed')
|
||||
}
|
||||
return env.data
|
||||
}
|
||||
|
||||
export async function listHeicodeTasks(params?: {
|
||||
status?: HeicodeTaskStatus
|
||||
limit?: number
|
||||
offset?: number
|
||||
}): Promise<{ items: HeicodeTask[]; total: number; offset: number; limit: number }> {
|
||||
const qs = new URLSearchParams()
|
||||
if (params?.status) qs.set('status', params.status)
|
||||
if (params?.limit != null) qs.set('limit', String(params.limit))
|
||||
if (params?.offset != null) qs.set('offset', String(params.offset))
|
||||
const suffix = qs.toString() ? `?${qs.toString()}` : ''
|
||||
const env = await mcpFetch<Envelope<{ items: HeicodeTask[]; total: number; offset: number; limit: number }>>(
|
||||
`/api/user/tasks${suffix}`
|
||||
)
|
||||
return env.data ?? { items: [], total: 0, offset: 0, limit: 0 }
|
||||
}
|
||||
|
||||
export async function getHeicodeTask(id: string): Promise<HeicodeTask | null> {
|
||||
try {
|
||||
const env = await mcpFetch<Envelope<HeicodeTask>>(`/api/user/tasks/${encodeURIComponent(id)}`)
|
||||
return env.data ?? null
|
||||
} catch (e) {
|
||||
if (e instanceof Error && /NOT_FOUND|404/i.test(e.message)) return null
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
export async function answerHeicodeTask(
|
||||
id: string,
|
||||
questionId: string,
|
||||
optionId: string
|
||||
): Promise<HeicodeTask> {
|
||||
const env = await mcpFetch<Envelope<HeicodeTask>>(
|
||||
`/api/user/tasks/${encodeURIComponent(id)}/answer`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ question_id: questionId, option_id: optionId }),
|
||||
}
|
||||
)
|
||||
if (!env.success || !env.data) {
|
||||
throw new Error(env.message || 'answerHeicodeTask failed')
|
||||
}
|
||||
return env.data
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// §4 NewAPI metadata passthrough — 4 endpoints
|
||||
// All return Envelope<…>.
|
||||
// =============================================================================
|
||||
|
||||
export type HeicodeBalance = {
|
||||
heicodeUserId: number
|
||||
email: string
|
||||
username: string
|
||||
displayName: string
|
||||
group: string
|
||||
status: number
|
||||
quota: number
|
||||
usedQuota: number
|
||||
requestCount: number
|
||||
}
|
||||
|
||||
export async function getHeicodeBalance(): Promise<HeicodeBalance | null> {
|
||||
try {
|
||||
const env = await mcpFetch<Envelope<HeicodeBalance>>('/api/user/heicode/balance')
|
||||
return env.data ?? null
|
||||
} catch (e) {
|
||||
if (e instanceof Error && /NOT_CONFIGURED|HEICODE_USER_NOT_FOUND/i.test(e.message)) {
|
||||
return null
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
export async function getHeicodeModels(): Promise<{
|
||||
heicodeUserId: number
|
||||
email: string
|
||||
items: Array<Record<string, unknown>>
|
||||
count: number
|
||||
} | null> {
|
||||
try {
|
||||
const env = await mcpFetch<Envelope<{
|
||||
heicodeUserId: number
|
||||
email: string
|
||||
items: Array<Record<string, unknown>>
|
||||
count: number
|
||||
}>>('/api/user/heicode/models')
|
||||
return env.data ?? null
|
||||
} catch (e) {
|
||||
if (e instanceof Error && /NOT_CONFIGURED|HEICODE_USER_NOT_FOUND/i.test(e.message)) {
|
||||
return null
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
export async function getHeicodeUsage(days = 30): Promise<Array<Record<string, unknown>>> {
|
||||
const env = await mcpFetch<Envelope<{
|
||||
items: Array<Record<string, unknown>>
|
||||
count: number
|
||||
}>>(`/api/user/heicode/usage?days=${Math.max(1, Math.min(90, days))}`)
|
||||
return env.data?.items ?? []
|
||||
}
|
||||
|
||||
export async function getHeicodeLogs(limit = 50, page = 1): Promise<Array<Record<string, unknown>>> {
|
||||
const env = await mcpFetch<Envelope<{
|
||||
items: Array<Record<string, unknown>>
|
||||
count: number
|
||||
}>>(`/api/user/heicode/logs?limit=${Math.max(1, Math.min(200, limit))}&page=${Math.max(1, page)}`)
|
||||
return env.data?.items ?? []
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// §5 Agnet stub — only the endpoints UI needs.
|
||||
// =============================================================================
|
||||
|
||||
export type McpAgnetDeployment = {
|
||||
deployment_id: string
|
||||
status: string
|
||||
phase?: string
|
||||
risk_level?: string
|
||||
user_id?: string
|
||||
binding_scope?: string
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
agent_instances_count?: number
|
||||
orchestration_plan?: Record<string, unknown>
|
||||
budget?: {
|
||||
max_usd?: number
|
||||
consumed_usd?: number
|
||||
remaining_usd?: number
|
||||
}
|
||||
}
|
||||
|
||||
export async function listMcpAgnetDeployments(params?: {
|
||||
status?: string
|
||||
limit?: number
|
||||
binding_scope?: string
|
||||
}): Promise<McpAgnetDeployment[]> {
|
||||
const qs = new URLSearchParams()
|
||||
if (params?.status) qs.set('status', params.status)
|
||||
if (params?.limit != null) qs.set('limit', String(params.limit))
|
||||
if (params?.binding_scope) qs.set('binding_scope', params.binding_scope)
|
||||
const suffix = qs.toString() ? `?${qs.toString()}` : ''
|
||||
try {
|
||||
const env = await mcpFetch<Envelope<{ items?: McpAgnetDeployment[]; total?: number }>>(
|
||||
`/api/agnet/deployments${suffix}`
|
||||
)
|
||||
return env.data?.items ?? []
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export type McpAuditEntry = {
|
||||
audit_id?: string
|
||||
user_id?: string
|
||||
binding_scope?: string
|
||||
actor?: string
|
||||
action?: string
|
||||
resource_id?: string
|
||||
resource_type?: string
|
||||
allowed_actions?: string[]
|
||||
constraints?: Record<string, unknown>
|
||||
secret_ref?: string
|
||||
occurred_at?: string
|
||||
// any extra fields are passed through but rendered redacted by UI
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export async function listMcpAuditLogs(params?: {
|
||||
binding_scope?: string
|
||||
actor?: string
|
||||
action?: string
|
||||
limit?: number
|
||||
}): Promise<McpAuditEntry[]> {
|
||||
const qs = new URLSearchParams()
|
||||
if (params?.binding_scope) qs.set('binding_scope', params.binding_scope)
|
||||
if (params?.actor) qs.set('actor', params.actor)
|
||||
if (params?.action) qs.set('action', params.action)
|
||||
if (params?.limit != null) qs.set('limit', String(params.limit))
|
||||
const suffix = qs.toString() ? `?${qs.toString()}` : ''
|
||||
try {
|
||||
const env = await mcpFetch<Envelope<{ items?: McpAuditEntry[]; total?: number }>>(
|
||||
`/api/agnet/audit-logs${suffix}`
|
||||
)
|
||||
return env.data?.items ?? []
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user