refactor(client): retire client-side resources UI per new product spec

The 2026-05-08 product package (docs/product-package/) redraws the
client / Manager boundary. Per 08-client-guide.md §1-7, the client
explicitly does NOT carry resource binding, permission grants, or
any account / security surface — those move entirely to Manager.

This commit removes the client-side resources surface that landed in
slices 2-4 (commits d1db2c1, c0363be, a28c900):

Deleted:
  - cc-haha/desktop/src/api/heicodeResources.ts          (API client)
  - cc-haha/desktop/src/stores/resourceStore.ts          (zustand)
  - cc-haha/desktop/src/pages/ResourceBindings.tsx       (page)
  - cc-haha/desktop/src/components/resources/Modals.tsx  (3 modals)
  - cc-haha/src/server/api/heicode-resources.ts          (proxy)

Reverted:
  - Sidebar.tsx: drop the Resources nav item + RESOURCES_TAB_ID import
  - ContentRouter.tsx: drop the 'resources' branch + import
  - tabStore.ts: drop RESOURCES_TAB_ID + 'resources' from TabType
  - router.ts: drop 'heicode-resources' case + handler import
  - i18n zh.ts + en.ts: strip ~63 keys (sidebar.resources +
    resources.* + grants.*)

Kept (still useful for the new spec's Manager-side data needs):
  - mcpAuth schema in types/provider.ts
  - mcpAuth wired through CreateProviderInput / UpdateProviderInput
  - providerService persistence of mcpAuth on add/update
  - Path A login flow that decodes JWT exp claims and stores the
    pair on the saved provider

Why keep token persistence even though the client doesn't expose
binding/grant UI any more? Per product spec the Manager will surface
余额 / 模型 / 用量 / 调用日志 (§2.3.1 in mcp-server's 待办 doc), and
the client will surface high-risk approvals (08-client-guide.md
§5). Both flows need a JWT pair we can refresh without re-prompting
for password — that machinery is already in place.

Next slice candidates per product spec (08 + 10 + 11):
  - High-risk approval dialog (新增 Tier 1, mock-wired UI first)
  - Task card + intent input as main client surface
  - Execution feedback panel (Agnet sub-stage status)
  - Delivery result panel
None of those are in this commit; this commit is purely cleanup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-08 11:19:21 +08:00
co-authored by Claude Opus 4.7
parent a28c90040f
commit 4827682de5
25 changed files with 2933 additions and 1778 deletions
-180
View File
@@ -1,180 +0,0 @@
// desktop/src/api/heicodeResources.ts
//
// API client for the local server's /api/heicode-resources/* proxy.
// The local server forwards each call to mcp-server (apimtaiji.azure-api.net)
// using the JWT stored in the active provider's mcpAuth, with transparent
// refresh on the 24h boundary.
//
// Server contract: cc-haha/src/server/api/heicode-resources.ts
// mcp-server contract: Heicode-接口契约文档.md §2 + §3
import { api } from './client'
export type ResourceType =
| 'git'
| 'sk'
| 'project_doc'
| 'cloud_account'
| 'cloud_resource'
export type ResourceStatus = 'pending' | 'active' | 'disabled' | 'revoked'
export type ResourceBinding = {
id: string
user_id: string
type: ResourceType
name: string
external_ref?: string
metadata: Record<string, unknown>
permission_scope: string[]
constraints: Record<string, unknown>
secret_ref?: string
status: ResourceStatus
created_by: string
updated_by: string
created_at: string
updated_at: string
}
export type ResourceBindingsList = {
items: ResourceBinding[]
total: number
offset: number
limit: number
}
export type GrantStatus = 'active' | 'suspended' | 'revoked' | 'expired'
export type ResourceGrant = {
id: string
user_id: string
resource_id: string
binding_scope: string
role?: string
agent_id?: string | null
allowed_actions: string[]
constraints: Record<string, unknown>
status: GrantStatus
expires_at?: string | null
created_by: string
revoked_by?: string | null
created_at: string
revoked_at?: string | null
}
export type ResourceGrantsList = {
items: ResourceGrant[]
total: number
offset: number
limit: number
}
export type ApiEnvelope<T> = {
success: boolean
data?: T
message?: string | null
}
// ─── Create / Update payloads (mcp-server contract §2.1 / §2.4 / §3.1) ───
export type CreateBindingInput = {
type: ResourceType
name: string
external_ref?: string
metadata?: Record<string, unknown>
permission_scope?: string[]
constraints?: Record<string, unknown>
secret_ref?: string
status?: ResourceStatus
}
export type UpdateBindingInput = Partial<{
name: string
external_ref: string
metadata: Record<string, unknown>
permission_scope: string[]
constraints: Record<string, unknown>
secret_ref: string
status: ResourceStatus
}>
export type CreateGrantInput = {
resource_id: string
binding_scope: string
role?: string
agent_id?: string | null
allowed_actions?: string[]
constraints?: Record<string, unknown>
expires_at?: string | null
status?: GrantStatus
}
const BASE = '/api/heicode-resources'
export const heicodeResourcesApi = {
// ── ResourceBindings ──────────────────────────────────────────
listBindings(opts?: { type?: ResourceType; status?: ResourceStatus }) {
const params = new URLSearchParams()
if (opts?.type) params.set('type', opts.type)
if (opts?.status) params.set('status', opts.status)
const qs = params.toString()
return api.get<ApiEnvelope<ResourceBindingsList>>(
`${BASE}${qs ? `?${qs}` : ''}`,
)
},
getBinding(id: string) {
return api.get<ApiEnvelope<ResourceBinding>>(`${BASE}/${encodeURIComponent(id)}`)
},
createBinding(input: CreateBindingInput) {
return api.post<ApiEnvelope<ResourceBinding>>(BASE, input)
},
updateBinding(id: string, input: UpdateBindingInput) {
return api.put<ApiEnvelope<ResourceBinding>>(
`${BASE}/${encodeURIComponent(id)}`,
input,
)
},
deleteBinding(id: string) {
return api.delete<ApiEnvelope<{ id: string; status: ResourceStatus }>>(
`${BASE}/${encodeURIComponent(id)}`,
)
},
// ── ResourceGrants ────────────────────────────────────────────
listGrants(opts?: {
resource_id?: string
role?: string
binding_scope?: string
status?: GrantStatus
}) {
const params = new URLSearchParams()
if (opts?.resource_id) params.set('resource_id', opts.resource_id)
if (opts?.role) params.set('role', opts.role)
if (opts?.binding_scope) params.set('binding_scope', opts.binding_scope)
if (opts?.status) params.set('status', opts.status)
const qs = params.toString()
return api.get<ApiEnvelope<ResourceGrantsList>>(
`${BASE}/grants${qs ? `?${qs}` : ''}`,
)
},
getGrant(id: string) {
return api.get<ApiEnvelope<ResourceGrant>>(
`${BASE}/grants/${encodeURIComponent(id)}`,
)
},
createGrant(input: CreateGrantInput) {
return api.post<ApiEnvelope<ResourceGrant>>(`${BASE}/grants`, input)
},
revokeGrant(id: string) {
return api.delete<ApiEnvelope<ResourceGrant>>(
`${BASE}/grants/${encodeURIComponent(id)}`,
)
},
}
@@ -4,7 +4,6 @@ import { EmptySession } from '../../pages/EmptySession'
import { ActiveSession } from '../../pages/ActiveSession'
import { ScheduledTasks } from '../../pages/ScheduledTasks'
import { Settings } from '../../pages/Settings'
import { ResourceBindings } from '../../pages/ResourceBindings'
import { TerminalSettings } from '../../pages/TerminalSettings'
export function ContentRouter() {
@@ -20,8 +19,6 @@ export function ContentRouter() {
page = <Settings />
} else if (activeTabType === 'scheduled') {
page = <ScheduledTasks />
} else if (activeTabType === 'resources') {
page = <ResourceBindings />
} else if (activeTabType !== 'terminal') {
page = <ActiveSession />
}
@@ -5,7 +5,7 @@ import { useTranslation } from '../../i18n'
import { ProjectFilter } from './ProjectFilter'
import { ConfirmDialog } from '../shared/ConfirmDialog'
import type { SessionListItem } from '../../types/session'
import { useTabStore, SETTINGS_TAB_ID, SCHEDULED_TAB_ID, RESOURCES_TAB_ID } from '../../stores/tabStore'
import { useTabStore, SETTINGS_TAB_ID, SCHEDULED_TAB_ID } from '../../stores/tabStore'
import { useChatStore } from '../../stores/chatStore'
import { useHeicodeAuthStore } from '../../stores/heicodeAuthStore'
@@ -195,15 +195,6 @@ export function Sidebar() {
>
{t('sidebar.scheduled')}
</NavItem>
<NavItem
active={activeTabId === RESOURCES_TAB_ID}
collapsed={!sidebarOpen}
label={t('sidebar.resources')}
onClick={() => useTabStore.getState().openTab(RESOURCES_TAB_ID, t('sidebar.resources'), 'resources')}
icon={<span className="material-symbols-outlined text-[18px]">link</span>}
>
{t('sidebar.resources')}
</NavItem>
<NavItem
active={activeTabType === 'terminal'}
collapsed={!sidebarOpen}
@@ -1,538 +0,0 @@
// desktop/src/components/resources/Modals.tsx
//
// Modal dialogs for the Resources page (Slice 4):
// - ConfirmDialog — generic yes/no confirmation
// - BindingFormModal — create a new ResourceBinding
// - GrantFormModal — create a new ResourceGrant (allowed_actions
// constrained to picked binding's permission_scope)
import { useEffect, useMemo, useState } from 'react'
import { useTranslation } from '../../i18n'
import {
type CreateBindingInput,
type CreateGrantInput,
type ResourceBinding,
type ResourceStatus,
type ResourceType,
} from '../../api/heicodeResources'
// ─── ConfirmDialog ──────────────────────────────────────────────
type ConfirmDialogProps = {
open: boolean
title: string
body: string
confirmLabel: string
danger?: boolean
busy?: boolean
errorMessage?: string | null
onConfirm: () => void
onCancel: () => void
}
export function ConfirmDialog({
open,
title,
body,
confirmLabel,
danger,
busy,
errorMessage,
onConfirm,
onCancel,
}: ConfirmDialogProps) {
const t = useTranslation()
if (!open) return null
return (
<ModalScrim onCancel={onCancel}>
<div className="w-full max-w-md rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-surface-container)] shadow-[var(--shadow-dropdown)]">
<div className="border-b border-[var(--color-border-separator)] px-6 py-4">
<h2 className="text-base font-semibold text-[var(--color-text-primary)]">
{title}
</h2>
</div>
<div className="px-6 py-5 text-sm leading-relaxed text-[var(--color-text-secondary)]">
{body}
</div>
{errorMessage ? (
<div className="mx-6 mb-4 rounded-[var(--radius-md)] border border-[var(--color-error)]/30 bg-[var(--color-error-container)] px-3 py-2 text-xs text-[var(--color-error)]">
{errorMessage}
</div>
) : null}
<div className="flex items-center justify-end gap-2 border-t border-[var(--color-border-separator)] px-6 py-4">
<button
type="button"
onClick={onCancel}
disabled={busy}
className="rounded-[var(--radius-md)] border border-[var(--color-border)] px-3 py-1.5 text-sm text-[var(--color-text-secondary)] transition-colors hover:border-[var(--color-border-hi,rgba(255,255,255,0.14))] hover:text-[var(--color-text-primary)] disabled:opacity-50"
>
{t('common.cancel')}
</button>
<button
type="button"
onClick={onConfirm}
disabled={busy}
className={
danger
? 'rounded-[var(--radius-md)] border border-[var(--color-error)]/40 bg-[var(--color-error)] px-3 py-1.5 text-sm font-medium text-white transition-opacity hover:opacity-90 disabled:opacity-50'
: 'rounded-[var(--radius-md)] border border-[var(--color-primary)]/40 bg-[var(--color-primary)] px-3 py-1.5 text-sm font-medium text-[var(--color-on-primary)] transition-colors hover:bg-[var(--color-primary-fixed-dim)] disabled:opacity-50'
}
>
{busy ? t('common.processing') : confirmLabel}
</button>
</div>
</div>
</ModalScrim>
)
}
// ─── BindingFormModal ──────────────────────────────────────────
type BindingFormProps = {
open: boolean
busy?: boolean
errorMessage?: string | null
onSubmit: (input: CreateBindingInput) => void
onCancel: () => void
}
export function BindingFormModal({
open,
busy,
errorMessage,
onSubmit,
onCancel,
}: BindingFormProps) {
const t = useTranslation()
const [type, setType] = useState<ResourceType>('git')
const [name, setName] = useState('')
const [externalRef, setExternalRef] = useState('')
const [permissionScope, setPermissionScope] = useState('')
const [secretRef, setSecretRef] = useState('')
const [status, setStatus] = useState<ResourceStatus>('pending')
// reset when reopened
useEffect(() => {
if (open) {
setType('git')
setName('')
setExternalRef('')
setPermissionScope('')
setSecretRef('')
setStatus('pending')
}
}, [open])
if (!open) return null
const canSubmit = name.trim().length > 0 && !busy
const handleSubmit = () => {
const scope = permissionScope
.split(/[\n,]/)
.map((s) => s.trim())
.filter(Boolean)
const input: CreateBindingInput = {
type,
name: name.trim(),
...(externalRef.trim() && { external_ref: externalRef.trim() }),
...(scope.length > 0 && { permission_scope: scope }),
...(secretRef.trim() && { secret_ref: secretRef.trim() }),
status,
}
onSubmit(input)
}
return (
<ModalScrim onCancel={onCancel}>
<div className="w-full max-w-xl rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-surface-container)] shadow-[var(--shadow-dropdown)]">
<div className="border-b border-[var(--color-border-separator)] px-6 py-4">
<h2 className="text-base font-semibold text-[var(--color-text-primary)]">
{t('resources.modal.create.title')}
</h2>
<p className="mt-1 text-xs text-[var(--color-text-tertiary)]">
{t('resources.modal.create.subtitle')}
</p>
</div>
<div className="grid gap-4 px-6 py-5">
<Field label={t('resources.field.type')}>
<select
value={type}
onChange={(e) => setType(e.target.value as ResourceType)}
disabled={busy}
className={inputClass}
>
<option value="git">{t('resources.type.git')}</option>
<option value="sk">{t('resources.type.sk')}</option>
<option value="project_doc">{t('resources.type.project_doc')}</option>
<option value="cloud_account">{t('resources.type.cloud_account')}</option>
<option value="cloud_resource">{t('resources.type.cloud_resource')}</option>
</select>
</Field>
<Field label={t('resources.field.name')} required>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
disabled={busy}
placeholder="my-repo"
maxLength={255}
className={inputClass}
/>
</Field>
<Field label={t('resources.field.externalRef')}>
<input
type="text"
value={externalRef}
onChange={(e) => setExternalRef(e.target.value)}
disabled={busy}
placeholder="https://example.com/org/repo.git"
className={inputClass}
/>
</Field>
<Field
label={t('resources.field.permissionScope')}
hint={t('resources.field.permissionScope.hint')}
>
<textarea
value={permissionScope}
onChange={(e) => setPermissionScope(e.target.value)}
disabled={busy}
rows={3}
placeholder="repo:read&#10;repo:write:current-branch"
className={`${inputClass} resize-none font-mono text-xs`}
/>
</Field>
<Field
label={t('resources.field.secretRef')}
hint={t('resources.field.secretRef.hint')}
>
<input
type="text"
value={secretRef}
onChange={(e) => setSecretRef(e.target.value)}
disabled={busy}
placeholder="vault://secret/users/{user_id}/bindings/my-repo"
maxLength={500}
className={inputClass}
/>
</Field>
<Field label={t('resources.field.status')}>
<select
value={status}
onChange={(e) => setStatus(e.target.value as ResourceStatus)}
disabled={busy}
className={inputClass}
>
<option value="pending">pending</option>
<option value="active">active</option>
<option value="disabled">disabled</option>
</select>
</Field>
</div>
{errorMessage ? (
<div className="mx-6 mb-4 rounded-[var(--radius-md)] border border-[var(--color-error)]/30 bg-[var(--color-error-container)] px-3 py-2 text-xs text-[var(--color-error)]">
{errorMessage}
</div>
) : null}
<div className="flex items-center justify-end gap-2 border-t border-[var(--color-border-separator)] px-6 py-4">
<button
type="button"
onClick={onCancel}
disabled={busy}
className="rounded-[var(--radius-md)] border border-[var(--color-border)] px-3 py-1.5 text-sm text-[var(--color-text-secondary)] hover:border-[var(--color-border-hi,rgba(255,255,255,0.14))] hover:text-[var(--color-text-primary)] disabled:opacity-50"
>
{t('common.cancel')}
</button>
<button
type="button"
onClick={handleSubmit}
disabled={!canSubmit}
className="rounded-[var(--radius-md)] border border-[var(--color-primary)]/40 bg-[var(--color-primary)] px-3 py-1.5 text-sm font-medium text-[var(--color-on-primary)] hover:bg-[var(--color-primary-fixed-dim)] disabled:cursor-not-allowed disabled:bg-[var(--color-surface-container-high)] disabled:text-[var(--color-text-tertiary)]"
>
{busy ? t('common.processing') : t('resources.modal.create.submit')}
</button>
</div>
</div>
</ModalScrim>
)
}
// ─── GrantFormModal ────────────────────────────────────────────
type GrantFormProps = {
open: boolean
bindings: ResourceBinding[]
busy?: boolean
errorMessage?: string | null
onSubmit: (input: CreateGrantInput) => void
onCancel: () => void
}
export function GrantFormModal({
open,
bindings,
busy,
errorMessage,
onSubmit,
onCancel,
}: GrantFormProps) {
const t = useTranslation()
const activeBindings = useMemo(
() => bindings.filter((b) => b.status !== 'revoked'),
[bindings],
)
const [resourceId, setResourceId] = useState('')
const [bindingScope, setBindingScope] = useState('main')
const [role, setRole] = useState('backend')
const [allowedActions, setAllowedActions] = useState<Set<string>>(new Set())
const [expiresAt, setExpiresAt] = useState('')
const selectedBinding = bindings.find((b) => b.id === resourceId)
const availableActions = selectedBinding?.permission_scope ?? []
useEffect(() => {
if (open) {
const first = activeBindings[0]
setResourceId(first?.id ?? '')
setBindingScope('main')
setRole('backend')
setAllowedActions(new Set())
setExpiresAt('')
}
}, [open, activeBindings])
// Auto-clean allowedActions when the selected binding changes — anything
// that is no longer in availableActions gets dropped.
useEffect(() => {
setAllowedActions((prev) => {
let changed = false
const next = new Set<string>()
for (const a of prev) {
if (availableActions.includes(a)) next.add(a)
else changed = true
}
return changed ? next : prev
})
}, [resourceId, availableActions])
if (!open) return null
const canSubmit =
!!resourceId && bindingScope.trim().length > 0 && !busy
const toggleAction = (action: string) => {
setAllowedActions((prev) => {
const next = new Set(prev)
if (next.has(action)) next.delete(action)
else next.add(action)
return next
})
}
const handleSubmit = () => {
const input: CreateGrantInput = {
resource_id: resourceId,
binding_scope: bindingScope.trim(),
role: role.trim() || undefined,
allowed_actions: [...allowedActions],
...(expiresAt && { expires_at: new Date(expiresAt).toISOString() }),
status: 'active',
}
onSubmit(input)
}
return (
<ModalScrim onCancel={onCancel}>
<div className="w-full max-w-xl rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-surface-container)] shadow-[var(--shadow-dropdown)]">
<div className="border-b border-[var(--color-border-separator)] px-6 py-4">
<h2 className="text-base font-semibold text-[var(--color-text-primary)]">
{t('resources.modal.grant.title')}
</h2>
<p className="mt-1 text-xs text-[var(--color-text-tertiary)]">
{t('resources.modal.grant.subtitle')}
</p>
</div>
<div className="grid gap-4 px-6 py-5">
<Field label={t('resources.field.binding')} required>
<select
value={resourceId}
onChange={(e) => setResourceId(e.target.value)}
disabled={busy || activeBindings.length === 0}
className={inputClass}
>
{activeBindings.length === 0 ? (
<option value="">{t('resources.modal.grant.noBindings')}</option>
) : (
activeBindings.map((b) => (
<option key={b.id} value={b.id}>
{b.name} · {b.type}
</option>
))
)}
</select>
</Field>
<Field
label={t('resources.field.allowedActions')}
hint={t('resources.field.allowedActions.hint')}
>
{availableActions.length === 0 ? (
<div className="rounded-[var(--radius-md)] border border-dashed border-[var(--color-border)] bg-[var(--color-surface-container-low)] px-3 py-2 text-xs text-[var(--color-text-tertiary)]">
{t('resources.field.allowedActions.empty')}
</div>
) : (
<div className="flex flex-wrap gap-2">
{availableActions.map((a) => {
const checked = allowedActions.has(a)
return (
<label
key={a}
className={`flex cursor-pointer items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs transition-colors ${
checked
? 'border-[var(--color-primary)]/40 bg-[var(--color-primary)]/10 text-[var(--color-primary)]'
: 'border-[var(--color-border)] bg-[var(--color-surface-container-low)] text-[var(--color-text-secondary)] hover:border-[var(--color-border-hi,rgba(255,255,255,0.14))]'
}`}
>
<input
type="checkbox"
checked={checked}
onChange={() => toggleAction(a)}
disabled={busy}
className="hidden"
/>
<code className="font-mono">{a}</code>
</label>
)
})}
</div>
)}
</Field>
<Field label={t('resources.field.bindingScope')} required>
<input
type="text"
value={bindingScope}
onChange={(e) => setBindingScope(e.target.value)}
disabled={busy}
placeholder="main"
maxLength={255}
className={inputClass}
/>
</Field>
<Field label={t('resources.field.role')}>
<select
value={role}
onChange={(e) => setRole(e.target.value)}
disabled={busy}
className={inputClass}
>
<option value="product">product</option>
<option value="frontend">frontend</option>
<option value="backend">backend</option>
<option value="reviewer">reviewer</option>
<option value="ops">ops</option>
</select>
</Field>
<Field
label={t('resources.field.expiresAt')}
hint={t('resources.field.expiresAt.hint')}
>
<input
type="datetime-local"
value={expiresAt}
onChange={(e) => setExpiresAt(e.target.value)}
disabled={busy}
className={inputClass}
/>
</Field>
</div>
{errorMessage ? (
<div className="mx-6 mb-4 rounded-[var(--radius-md)] border border-[var(--color-error)]/30 bg-[var(--color-error-container)] px-3 py-2 text-xs text-[var(--color-error)]">
{errorMessage}
</div>
) : null}
<div className="flex items-center justify-end gap-2 border-t border-[var(--color-border-separator)] px-6 py-4">
<button
type="button"
onClick={onCancel}
disabled={busy}
className="rounded-[var(--radius-md)] border border-[var(--color-border)] px-3 py-1.5 text-sm text-[var(--color-text-secondary)] hover:border-[var(--color-border-hi,rgba(255,255,255,0.14))] hover:text-[var(--color-text-primary)] disabled:opacity-50"
>
{t('common.cancel')}
</button>
<button
type="button"
onClick={handleSubmit}
disabled={!canSubmit}
className="rounded-[var(--radius-md)] border border-[var(--color-primary)]/40 bg-[var(--color-primary)] px-3 py-1.5 text-sm font-medium text-[var(--color-on-primary)] hover:bg-[var(--color-primary-fixed-dim)] disabled:cursor-not-allowed disabled:bg-[var(--color-surface-container-high)] disabled:text-[var(--color-text-tertiary)]"
>
{busy ? t('common.processing') : t('resources.modal.grant.submit')}
</button>
</div>
</div>
</ModalScrim>
)
}
// ─── Shared bits ──────────────────────────────────────────────
const inputClass =
'w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-surface-container-low)] px-3 py-2 text-sm text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)] outline-none transition-colors focus:border-[var(--color-primary)] focus:shadow-[var(--shadow-focus-ring)] disabled:cursor-not-allowed disabled:opacity-60'
function Field({
label,
hint,
required,
children,
}: {
label: string
hint?: string
required?: boolean
children: React.ReactNode
}) {
return (
<label className="flex flex-col gap-1.5">
<span className="text-xs font-medium uppercase tracking-wider text-[var(--color-text-tertiary)]">
{label}
{required ? <span className="ml-1 text-[var(--color-primary)]">*</span> : null}
</span>
{children}
{hint ? (
<span className="text-[10px] text-[var(--color-text-tertiary)]">{hint}</span>
) : null}
</label>
)
}
function ModalScrim({
onCancel,
children,
}: {
onCancel: () => void
children: React.ReactNode
}) {
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-[var(--color-overlay-scrim)] px-4 py-8"
onClick={onCancel}
>
<div onClick={(e) => e.stopPropagation()} className="max-h-full overflow-auto">
{children}
</div>
</div>
)
}
-70
View File
@@ -20,7 +20,6 @@ export const en = {
// ─── Sidebar ──────────────────────────────────────
'sidebar.newSession': 'New session',
'sidebar.scheduled': 'Scheduled',
'sidebar.resources': 'Resources',
'sidebar.terminal': 'Terminal',
'sidebar.settings': 'Settings',
'sidebar.searchPlaceholder': 'Search sessions...',
@@ -959,81 +958,12 @@ export const en = {
'serverVerb.Task in progress': 'Task in progress',
// ─── Resource Bindings (mcp-server P1 nine endpoints) ────
'resources.title': 'Resources',
'resources.subtitle': 'Bind Git repos / SK / project docs / cloud accounts / cloud resources, then grant them to sub-agents.',
'resources.refresh': 'Refresh',
'resources.refreshing': 'Refreshing…',
'resources.empty.title': 'No resource bindings yet',
'resources.empty.body': 'Create Git repo / SK / project doc / cloud account / cloud resource bindings under your Heicode Manager account to manage and grant them here.',
'resources.empty.comingSoon': 'Create / delete UI ships in the next slice; until then create them via Heicode Web console.',
'resources.error.title': 'Failed to load',
'resources.col.name': 'Name',
'resources.col.type': 'Type',
'resources.col.externalRef': 'External ref',
'resources.col.status': 'Status',
'resources.col.scope': 'Permission scope',
'resources.col.actions': 'Actions',
// tabs / actions
'resources.tab.bindings': 'Bindings',
'resources.tab.grants': 'Grants',
'resources.action.createBinding': 'New binding',
'resources.action.createGrant': 'New grant',
'resources.action.delete': 'Delete',
'resources.action.revoke': 'Revoke',
'resources.action.deleteBinding': 'Delete binding',
'resources.action.revokeGrant': 'Revoke grant',
// create binding modal
'resources.modal.create.title': 'New resource binding',
'resources.modal.create.subtitle': 'Once created, you can grant it to a role / sub-agent from the Grants tab.',
'resources.modal.create.submit': 'Create',
'resources.field.type': 'Type',
'resources.field.name': 'Name',
'resources.field.externalRef': 'External ref',
'resources.field.permissionScope': 'Permission scope',
'resources.field.permissionScope.hint': 'One per line or comma-separated, e.g. repo:read, repo:write:current-branch, cloud:list, vm:ssh:approved-window.',
'resources.field.secretRef': 'Secret ref',
'resources.field.secretRef.hint': 'Reference only (e.g. vault://...), never the actual secret. The server rejects plaintext credentials.',
'resources.field.status': 'Status',
// resource type labels
'resources.type.git': 'Git repo',
'resources.type.sk': 'SK',
'resources.type.project_doc': 'Project doc',
'resources.type.cloud_account': 'Cloud account',
'resources.type.cloud_resource': 'Cloud resource',
// create grant modal
'resources.modal.grant.title': 'New grant',
'resources.modal.grant.subtitle': 'Grant an existing binding to a role or sub-agent. Allowed actions must be a subset of the binding\'s permission scope.',
'resources.modal.grant.submit': 'Grant',
'resources.modal.grant.noBindings': 'No grantable bindings yet',
'resources.field.binding': 'Binding',
'resources.field.allowedActions': 'Allowed actions',
'resources.field.allowedActions.hint': 'Select a subset of the binding\'s permission scope; leaving empty grants metadata-read only.',
'resources.field.allowedActions.empty': 'This binding has no permission scope defined, so no action can be granted. Edit the binding to add scopes first.',
'resources.field.bindingScope': 'Binding scope',
'resources.field.role': 'Role',
'resources.field.expiresAt': 'Expires at',
'resources.field.expiresAt.hint': 'Leave empty for long-lived; recommended for high-risk resources.',
// grants table
'resources.grants.col.binding': 'Binding',
'resources.grants.col.role': 'Role',
'resources.grants.col.actions': 'Allowed actions',
'resources.grants.col.scope': 'Scope',
'resources.grants.col.status': 'Status',
'resources.grants.col.expires': 'Expires',
'resources.grants.empty.title': 'No grants yet',
'resources.grants.empty.body': 'Grant a binding from the Bindings tab to a role and sub-agents will be able to use it once deployed.',
// confirm dialogs
'resources.confirm.delete.title': 'Delete resource binding?',
'resources.confirm.delete.body': 'Binding "{name}" will be marked as revoked (soft-delete). Existing grants on this binding are NOT auto-revoked — handle them manually.',
'resources.confirm.revoke.title': 'Revoke grant?',
'resources.confirm.revoke.body': 'After revoke, the role / sub-agent can no longer use the resource. This action cannot be undone.',
// ─── HeiCode Login ──────────────────────────────────────
'login.subtitle': 'Sign in to start using Heicode',
'login.footnote': 'Heicode talks directly to TaijiAICloud; your API key never leaves this machine.',
-70
View File
@@ -22,7 +22,6 @@ export const zh: Record<TranslationKey, string> = {
// ─── Sidebar ──────────────────────────────────────
'sidebar.newSession': '新建会话',
'sidebar.scheduled': '定时任务',
'sidebar.resources': '资源绑定',
'sidebar.terminal': '终端',
'sidebar.settings': '设置',
'sidebar.searchPlaceholder': '搜索会话...',
@@ -961,81 +960,12 @@ export const zh: Record<TranslationKey, string> = {
'serverVerb.Task in progress': '任务进行中',
// ─── 资源绑定(接 mcp-server P1 9 接口)──────────────
'resources.title': '资源绑定',
'resources.subtitle': '绑定 Git 仓库 / SK / 项目文档 / 云账号 / 云资源,并把它们授给子 Agent 使用。',
'resources.refresh': '刷新',
'resources.refreshing': '刷新中…',
'resources.empty.title': '还没有任何资源绑定',
'resources.empty.body': '在 Heicode Manager 平台账号下创建 Git 仓库、SK、项目文档、云账号或云资源的绑定,便可在此处统一管理与授权。',
'resources.empty.comingSoon': '客户端创建/删除入口在下一版本上线;当前可在 Heicode Web 控制台创建。',
'resources.error.title': '加载失败',
'resources.col.name': '名称',
'resources.col.type': '类型',
'resources.col.externalRef': '外部引用',
'resources.col.status': '状态',
'resources.col.scope': '权限范围',
'resources.col.actions': '操作',
// tabs / actions
'resources.tab.bindings': '资源绑定',
'resources.tab.grants': '授权',
'resources.action.createBinding': '新建绑定',
'resources.action.createGrant': '新建授权',
'resources.action.delete': '删除',
'resources.action.revoke': '撤销',
'resources.action.deleteBinding': '删除绑定',
'resources.action.revokeGrant': '撤销授权',
// create binding modal
'resources.modal.create.title': '新建资源绑定',
'resources.modal.create.subtitle': '绑定后可在"授权"标签页把它授给某个角色 / 子 Agent 使用。',
'resources.modal.create.submit': '创建',
'resources.field.type': '类型',
'resources.field.name': '名称',
'resources.field.externalRef': '外部引用',
'resources.field.permissionScope': '权限范围',
'resources.field.permissionScope.hint': '每行或逗号分隔一项,例如:repo:read、repo:write:current-branch、cloud:list、vm:ssh:approved-window 等',
'resources.field.secretRef': '凭据引用 (secret_ref)',
'resources.field.secretRef.hint': '只填引用(如 vault://...),不要填密钥本身。后端会拒绝任何明文凭据。',
'resources.field.status': '状态',
// resource type labels
'resources.type.git': 'Git 仓库',
'resources.type.sk': 'SK',
'resources.type.project_doc': '项目文档',
'resources.type.cloud_account': '云账号',
'resources.type.cloud_resource': '云资源',
// create grant modal
'resources.modal.grant.title': '新建授权',
'resources.modal.grant.subtitle': '把已有绑定授权给一个角色或子 Agent 使用。允许动作必须是绑定权限范围的子集。',
'resources.modal.grant.submit': '授权',
'resources.modal.grant.noBindings': '暂无可授权的绑定',
'resources.field.binding': '绑定',
'resources.field.allowedActions': '允许动作',
'resources.field.allowedActions.hint': '从绑定的权限范围中勾选;不勾代表不授任何动作(仅允许读取元数据)。',
'resources.field.allowedActions.empty': '该绑定没有定义权限范围,无法授任何动作。建议先编辑该绑定补充权限。',
'resources.field.bindingScope': '作用域 (binding_scope)',
'resources.field.role': '角色',
'resources.field.expiresAt': '过期时间',
'resources.field.expiresAt.hint': '留空表示长期有效;高危资源建议设置。',
// grants table
'resources.grants.col.binding': '绑定',
'resources.grants.col.role': '角色',
'resources.grants.col.actions': '允许动作',
'resources.grants.col.scope': '作用域',
'resources.grants.col.status': '状态',
'resources.grants.col.expires': '过期',
'resources.grants.empty.title': '还没有任何授权',
'resources.grants.empty.body': '把"资源绑定"标签页里的某个绑定授给一个角色,子 Agent 部署时就能使用它。',
// confirm dialogs
'resources.confirm.delete.title': '删除资源绑定?',
'resources.confirm.delete.body': '将把绑定 "{name}" 标记为已撤销(软删除)。基于该绑定的授权不会自动撤销,需要手动处理。',
'resources.confirm.revoke.title': '撤销授权?',
'resources.confirm.revoke.body': '撤销后该角色 / 子 Agent 不再能使用对应的资源。撤销不可逆。',
// ─── HeiCode 登录 ──────────────────────────────────────
'login.subtitle': '登录以开始使用 Heicode',
'login.footnote': 'Heicode 直接连 TaijiAICloud,API Key 仅保存在你这台机器上。',
@@ -1,528 +0,0 @@
// desktop/src/pages/ResourceBindings.tsx
//
// Resources page (slice 4):
// - Tab switcher: Bindings / Grants
// - Bindings tab: list + Create + Delete (soft-revoke) + Refresh
// - Grants tab: list + Create + Revoke
// - All operations go through local server proxy → mcp-server.
import { useEffect, useMemo, useState } from 'react'
import { useTranslation } from '../i18n'
import { useResourceStore } from '../stores/resourceStore'
import type {
GrantStatus,
ResourceBinding,
ResourceGrant,
ResourceStatus,
} from '../api/heicodeResources'
import {
BindingFormModal,
ConfirmDialog,
GrantFormModal,
} from '../components/resources/Modals'
type TabKey = 'bindings' | 'grants'
const BINDING_STATUS_TONE: Record<ResourceStatus, string> = {
active: 'text-[var(--color-success)] border-[var(--color-success)]/30 bg-[var(--color-success)]/10',
pending: 'text-[var(--color-warning)] border-[var(--color-warning)]/30 bg-[var(--color-warning)]/10',
disabled: 'text-[var(--color-text-tertiary)] border-[var(--color-border)] bg-[var(--color-surface-container-low)]',
revoked: 'text-[var(--color-error)] border-[var(--color-error)]/30 bg-[var(--color-error)]/10',
}
const GRANT_STATUS_TONE: Record<GrantStatus, string> = {
active: 'text-[var(--color-success)] border-[var(--color-success)]/30 bg-[var(--color-success)]/10',
suspended: 'text-[var(--color-warning)] border-[var(--color-warning)]/30 bg-[var(--color-warning)]/10',
revoked: 'text-[var(--color-error)] border-[var(--color-error)]/30 bg-[var(--color-error)]/10',
expired: 'text-[var(--color-text-tertiary)] border-[var(--color-border)] bg-[var(--color-surface-container-low)]',
}
export function ResourceBindings() {
const t = useTranslation()
const [tab, setTab] = useState<TabKey>('bindings')
const [showCreateBinding, setShowCreateBinding] = useState(false)
const [bindingToDelete, setBindingToDelete] = useState<ResourceBinding | null>(null)
const [showCreateGrant, setShowCreateGrant] = useState(false)
const [grantToRevoke, setGrantToRevoke] = useState<ResourceGrant | null>(null)
const {
bindings, bindingsLoading, bindingsFetched, bindingsError,
grants, grantsLoading, grantsFetched, grantsError,
isMutating, mutationError,
fetchBindings, fetchGrants,
createBinding, deleteBinding,
createGrant, revokeGrant,
clearBindingsError, clearGrantsError, clearMutationError,
} = useResourceStore()
useEffect(() => {
if (tab === 'bindings' && !bindingsFetched) void fetchBindings()
if (tab === 'grants' && !grantsFetched) {
void fetchGrants()
// Always need bindings list available for the Grant create modal,
// even when the user opens the Grants tab first.
if (!bindingsFetched) void fetchBindings()
}
}, [tab, bindingsFetched, grantsFetched, fetchBindings, fetchGrants])
const bindingMap = useMemo(() => {
const m = new Map<string, ResourceBinding>()
for (const b of bindings) m.set(b.id, b)
return m
}, [bindings])
return (
<div className="relative flex flex-1 flex-col overflow-auto bg-[var(--color-surface)]">
<div className="mx-auto w-full max-w-5xl px-8 py-10">
<header className="mb-6">
<h1
className="text-2xl font-bold tracking-tight text-[var(--color-text-primary)]"
style={{ fontFamily: 'var(--font-headline)' }}
>
{t('resources.title')}
</h1>
<p className="mt-2 max-w-2xl text-sm text-[var(--color-text-secondary)]">
{t('resources.subtitle')}
</p>
</header>
{/* Tabs */}
<div className="mb-6 flex items-center justify-between border-b border-[var(--color-border)]">
<div className="flex gap-1">
<TabButton active={tab === 'bindings'} onClick={() => setTab('bindings')}>
{t('resources.tab.bindings')}
<span className="ml-1.5 text-[10px] text-[var(--color-text-tertiary)]">
{bindings.length}
</span>
</TabButton>
<TabButton active={tab === 'grants'} onClick={() => setTab('grants')}>
{t('resources.tab.grants')}
<span className="ml-1.5 text-[10px] text-[var(--color-text-tertiary)]">
{grants.length}
</span>
</TabButton>
</div>
<div className="flex items-center gap-2 pb-2">
<button
type="button"
onClick={() => (tab === 'bindings' ? fetchBindings() : fetchGrants())}
disabled={tab === 'bindings' ? bindingsLoading : grantsLoading}
className="rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-surface-container)] px-3 py-1 text-xs text-[var(--color-text-secondary)] transition-colors hover:border-[var(--color-border-hi,rgba(255,255,255,0.14))] hover:text-[var(--color-text-primary)] disabled:opacity-50"
>
{(tab === 'bindings' ? bindingsLoading : grantsLoading)
? t('resources.refreshing')
: t('resources.refresh')}
</button>
<button
type="button"
onClick={() => {
clearMutationError()
if (tab === 'bindings') setShowCreateBinding(true)
else setShowCreateGrant(true)
}}
className="rounded-[var(--radius-md)] border border-[var(--color-primary)]/40 bg-[var(--color-primary)] px-3 py-1 text-xs font-medium text-[var(--color-on-primary)] hover:bg-[var(--color-primary-fixed-dim)]"
>
{tab === 'bindings'
? t('resources.action.createBinding')
: t('resources.action.createGrant')}
</button>
</div>
</div>
{/* Error banners */}
{tab === 'bindings' && bindingsError ? (
<ErrorBanner
title={t('resources.error.title')}
message={bindingsError}
onDismiss={clearBindingsError}
dismissLabel={t('common.dismiss')}
/>
) : null}
{tab === 'grants' && grantsError ? (
<ErrorBanner
title={t('resources.error.title')}
message={grantsError}
onDismiss={clearGrantsError}
dismissLabel={t('common.dismiss')}
/>
) : null}
{/* Initial loading */}
{tab === 'bindings' && !bindingsFetched && bindingsLoading ? (
<LoadingCard label={t('common.loading')} />
) : null}
{tab === 'grants' && !grantsFetched && grantsLoading ? (
<LoadingCard label={t('common.loading')} />
) : null}
{/* Bindings tab */}
{tab === 'bindings' && bindingsFetched ? (
bindings.length === 0 ? (
<EmptyCard
icon="link_off"
title={t('resources.empty.title')}
body={t('resources.empty.body')}
ctaLabel={t('resources.action.createBinding')}
onCta={() => setShowCreateBinding(true)}
/>
) : (
<BindingsTable
bindings={bindings}
onDelete={(b) => setBindingToDelete(b)}
/>
)
) : null}
{/* Grants tab */}
{tab === 'grants' && grantsFetched ? (
grants.length === 0 ? (
<EmptyCard
icon="key_off"
title={t('resources.grants.empty.title')}
body={t('resources.grants.empty.body')}
ctaLabel={t('resources.action.createGrant')}
onCta={() => setShowCreateGrant(true)}
/>
) : (
<GrantsTable
grants={grants}
bindingMap={bindingMap}
onRevoke={(g) => setGrantToRevoke(g)}
/>
)
) : null}
</div>
{/* Modals */}
<BindingFormModal
open={showCreateBinding}
busy={isMutating}
errorMessage={mutationError}
onCancel={() => {
setShowCreateBinding(false)
clearMutationError()
}}
onSubmit={async (input) => {
const created = await createBinding(input)
if (created) {
setShowCreateBinding(false)
}
}}
/>
<GrantFormModal
open={showCreateGrant}
bindings={bindings}
busy={isMutating}
errorMessage={mutationError}
onCancel={() => {
setShowCreateGrant(false)
clearMutationError()
}}
onSubmit={async (input) => {
const created = await createGrant(input)
if (created) setShowCreateGrant(false)
}}
/>
<ConfirmDialog
open={!!bindingToDelete}
title={t('resources.confirm.delete.title')}
body={t('resources.confirm.delete.body', { name: bindingToDelete?.name ?? '' })}
confirmLabel={t('resources.action.deleteBinding')}
danger
busy={isMutating}
errorMessage={mutationError}
onCancel={() => {
setBindingToDelete(null)
clearMutationError()
}}
onConfirm={async () => {
if (!bindingToDelete) return
const ok = await deleteBinding(bindingToDelete.id)
if (ok) setBindingToDelete(null)
}}
/>
<ConfirmDialog
open={!!grantToRevoke}
title={t('resources.confirm.revoke.title')}
body={t('resources.confirm.revoke.body')}
confirmLabel={t('resources.action.revokeGrant')}
danger
busy={isMutating}
errorMessage={mutationError}
onCancel={() => {
setGrantToRevoke(null)
clearMutationError()
}}
onConfirm={async () => {
if (!grantToRevoke) return
const ok = await revokeGrant(grantToRevoke.id)
if (ok) setGrantToRevoke(null)
}}
/>
</div>
)
}
// ─── Sub-components ────────────────────────────────────────────
function TabButton({
active,
onClick,
children,
}: {
active: boolean
onClick: () => void
children: React.ReactNode
}) {
return (
<button
type="button"
onClick={onClick}
className={`relative px-4 py-2.5 text-sm transition-colors ${
active
? 'font-medium text-[var(--color-text-primary)]'
: 'text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]'
}`}
>
{children}
{active ? (
<span className="absolute -bottom-px left-0 right-0 h-px bg-[var(--color-primary)]" />
) : null}
</button>
)
}
function ErrorBanner({
title,
message,
onDismiss,
dismissLabel,
}: {
title: string
message: string
onDismiss: () => void
dismissLabel: string
}) {
return (
<div className="mb-6 rounded-[var(--radius-md)] border border-[var(--color-error)]/30 bg-[var(--color-error-container)] px-4 py-3 text-sm text-[var(--color-error)]">
<div className="flex items-start justify-between gap-3">
<div className="flex-1">
<div className="font-medium">{title}</div>
<div className="mt-1 text-xs opacity-80 break-words">{message}</div>
</div>
<button type="button" onClick={onDismiss} className="text-xs underline-offset-2 hover:underline">
{dismissLabel}
</button>
</div>
</div>
)
}
function LoadingCard({ label }: { label: string }) {
return (
<div className="rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-surface-container)] p-8 text-center text-sm text-[var(--color-text-tertiary)]">
{label}
</div>
)
}
function EmptyCard({
icon,
title,
body,
ctaLabel,
onCta,
}: {
icon: string
title: string
body: string
ctaLabel: string
onCta: () => void
}) {
return (
<section className="rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-surface-container)] p-8 text-center shadow-[var(--shadow-dropdown)]">
<div className="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-full border border-[var(--color-border)] bg-[var(--color-surface-container-low)] text-[var(--color-text-tertiary)]">
<span className="material-symbols-outlined text-[22px]">{icon}</span>
</div>
<h2 className="text-base font-semibold text-[var(--color-text-primary)]">{title}</h2>
<p className="mx-auto mt-2 max-w-md text-sm leading-relaxed text-[var(--color-text-secondary)]">{body}</p>
<button
type="button"
onClick={onCta}
className="mt-5 rounded-[var(--radius-md)] border border-[var(--color-primary)]/40 bg-[var(--color-primary)] px-4 py-2 text-sm font-medium text-[var(--color-on-primary)] hover:bg-[var(--color-primary-fixed-dim)]"
>
{ctaLabel}
</button>
</section>
)
}
const TYPE_LABELS: Record<ResourceBinding['type'], string> = {
git: 'Git',
sk: 'SK',
project_doc: 'Doc',
cloud_account: 'Cloud',
cloud_resource: 'Resource',
}
function BindingsTable({
bindings,
onDelete,
}: {
bindings: ResourceBinding[]
onDelete: (b: ResourceBinding) => void
}) {
const t = useTranslation()
return (
<div className="overflow-hidden rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-surface-container)]">
<table className="w-full text-sm">
<thead className="border-b border-[var(--color-border)] bg-[var(--color-surface-container-low)] text-[11px] uppercase tracking-wider text-[var(--color-text-tertiary)]">
<tr>
<th className="px-4 py-3 text-left font-medium">{t('resources.col.name')}</th>
<th className="px-4 py-3 text-left font-medium">{t('resources.col.type')}</th>
<th className="px-4 py-3 text-left font-medium">{t('resources.col.externalRef')}</th>
<th className="px-4 py-3 text-left font-medium">{t('resources.col.status')}</th>
<th className="px-4 py-3 text-left font-medium">{t('resources.col.scope')}</th>
<th className="px-4 py-3 text-right font-medium">{t('resources.col.actions')}</th>
</tr>
</thead>
<tbody>
{bindings.map((b) => (
<tr key={b.id} className="border-b border-[var(--color-border-separator)] last:border-0 transition-colors hover:bg-[var(--color-surface-hover)]">
<td className="px-4 py-3 align-top">
<div className="font-medium text-[var(--color-text-primary)]">{b.name}</div>
<div className="mt-0.5 font-mono text-[10px] text-[var(--color-text-tertiary)]">{b.id.slice(0, 8)}…</div>
</td>
<td className="px-4 py-3 align-top">
<span className="rounded-[var(--radius-sm)] border border-[var(--color-border)] bg-[var(--color-surface-container-low)] px-2 py-0.5 text-xs text-[var(--color-text-secondary)]">
{TYPE_LABELS[b.type] ?? b.type}
</span>
</td>
<td className="px-4 py-3 align-top text-xs text-[var(--color-text-secondary)] break-all max-w-[220px]">
{b.external_ref ?? '—'}
</td>
<td className="px-4 py-3 align-top">
<span className={`rounded-full border px-2 py-0.5 text-[10px] font-medium uppercase tracking-wider ${BINDING_STATUS_TONE[b.status]}`}>
{b.status}
</span>
</td>
<td className="px-4 py-3 align-top text-xs text-[var(--color-text-secondary)]">
{b.permission_scope.length > 0 ? (
<div className="flex flex-wrap gap-1">
{b.permission_scope.slice(0, 2).map((s) => (
<code key={s} className="rounded-[var(--radius-sm)] border border-[var(--color-border)] bg-[var(--color-surface-container-low)] px-1.5 py-0.5 font-mono text-[10px]">
{s}
</code>
))}
{b.permission_scope.length > 2 ? (
<span className="text-[10px] text-[var(--color-text-tertiary)]">+{b.permission_scope.length - 2}</span>
) : null}
</div>
) : '—'}
</td>
<td className="px-4 py-3 align-top text-right">
{b.status !== 'revoked' ? (
<button
type="button"
onClick={() => onDelete(b)}
className="text-xs text-[var(--color-error)] hover:underline"
>
{t('resources.action.delete')}
</button>
) : (
<span className="text-xs text-[var(--color-text-tertiary)]">—</span>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
)
}
function GrantsTable({
grants,
bindingMap,
onRevoke,
}: {
grants: ResourceGrant[]
bindingMap: Map<string, ResourceBinding>
onRevoke: (g: ResourceGrant) => void
}) {
const t = useTranslation()
return (
<div className="overflow-hidden rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-surface-container)]">
<table className="w-full text-sm">
<thead className="border-b border-[var(--color-border)] bg-[var(--color-surface-container-low)] text-[11px] uppercase tracking-wider text-[var(--color-text-tertiary)]">
<tr>
<th className="px-4 py-3 text-left font-medium">{t('resources.grants.col.binding')}</th>
<th className="px-4 py-3 text-left font-medium">{t('resources.grants.col.role')}</th>
<th className="px-4 py-3 text-left font-medium">{t('resources.grants.col.actions')}</th>
<th className="px-4 py-3 text-left font-medium">{t('resources.grants.col.scope')}</th>
<th className="px-4 py-3 text-left font-medium">{t('resources.grants.col.status')}</th>
<th className="px-4 py-3 text-left font-medium">{t('resources.grants.col.expires')}</th>
<th className="px-4 py-3 text-right font-medium">{t('resources.col.actions')}</th>
</tr>
</thead>
<tbody>
{grants.map((g) => {
const b = bindingMap.get(g.resource_id)
return (
<tr key={g.id} className="border-b border-[var(--color-border-separator)] last:border-0 transition-colors hover:bg-[var(--color-surface-hover)]">
<td className="px-4 py-3 align-top">
<div className="font-medium text-[var(--color-text-primary)]">{b?.name ?? g.resource_id.slice(0, 8) + '…'}</div>
{b ? (
<div className="mt-0.5 text-[10px] text-[var(--color-text-tertiary)]">{TYPE_LABELS[b.type]}</div>
) : null}
</td>
<td className="px-4 py-3 align-top text-xs text-[var(--color-text-secondary)]">
{g.role || '—'}
</td>
<td className="px-4 py-3 align-top text-xs text-[var(--color-text-secondary)]">
{g.allowed_actions.length > 0 ? (
<div className="flex flex-wrap gap-1">
{g.allowed_actions.slice(0, 2).map((a) => (
<code key={a} className="rounded-[var(--radius-sm)] border border-[var(--color-border)] bg-[var(--color-surface-container-low)] px-1.5 py-0.5 font-mono text-[10px]">
{a}
</code>
))}
{g.allowed_actions.length > 2 ? (
<span className="text-[10px] text-[var(--color-text-tertiary)]">+{g.allowed_actions.length - 2}</span>
) : null}
</div>
) : '—'}
</td>
<td className="px-4 py-3 align-top text-xs text-[var(--color-text-secondary)]">{g.binding_scope}</td>
<td className="px-4 py-3 align-top">
<span className={`rounded-full border px-2 py-0.5 text-[10px] font-medium uppercase tracking-wider ${GRANT_STATUS_TONE[g.status]}`}>
{g.status}
</span>
</td>
<td className="px-4 py-3 align-top text-xs text-[var(--color-text-tertiary)]">
{g.expires_at ? new Date(g.expires_at).toLocaleDateString() : '—'}
</td>
<td className="px-4 py-3 align-top text-right">
{g.status === 'active' ? (
<button
type="button"
onClick={() => onRevoke(g)}
className="text-xs text-[var(--color-error)] hover:underline"
>
{t('resources.action.revoke')}
</button>
) : (
<span className="text-xs text-[var(--color-text-tertiary)]">—</span>
)}
</td>
</tr>
)
})}
</tbody>
</table>
</div>
)
}
-178
View File
@@ -1,178 +0,0 @@
// desktop/src/stores/resourceStore.ts
//
// Zustand store for ResourceBinding / ResourceGrant. Talks to the local
// /api/heicode-resources/* proxy.
import { create } from 'zustand'
import {
heicodeResourcesApi,
type CreateBindingInput,
type CreateGrantInput,
type ResourceBinding,
type ResourceGrant,
} from '../api/heicodeResources'
type ResourceState = {
// Bindings
bindings: ResourceBinding[]
bindingsLoading: boolean
bindingsFetched: boolean
bindingsError: string | null
// Grants
grants: ResourceGrant[]
grantsLoading: boolean
grantsFetched: boolean
grantsError: string | null
// Mutation state (shared — only one create/delete in flight at a time)
isMutating: boolean
mutationError: string | null
// Actions
fetchBindings: () => Promise<void>
fetchGrants: () => Promise<void>
createBinding: (input: CreateBindingInput) => Promise<ResourceBinding | null>
deleteBinding: (id: string) => Promise<boolean>
createGrant: (input: CreateGrantInput) => Promise<ResourceGrant | null>
revokeGrant: (id: string) => Promise<boolean>
clearBindingsError: () => void
clearGrantsError: () => void
clearMutationError: () => void
}
function extractErrorMessage(err: unknown, fallback: string): string {
if (err instanceof Error) return err.message
return typeof err === 'string' ? err : fallback
}
export const useResourceStore = create<ResourceState>((set, get) => ({
bindings: [],
bindingsLoading: false,
bindingsFetched: false,
bindingsError: null,
grants: [],
grantsLoading: false,
grantsFetched: false,
grantsError: null,
isMutating: false,
mutationError: null,
fetchBindings: async () => {
set({ bindingsLoading: true, bindingsError: null })
try {
const res = await heicodeResourcesApi.listBindings()
set({
bindings: res.success ? (res.data?.items ?? []) : [],
bindingsLoading: false,
bindingsFetched: true,
bindingsError: res.success ? null : (res.message || '加载资源绑定失败'),
})
} catch (err) {
set({
bindingsLoading: false,
bindingsFetched: true,
bindingsError: extractErrorMessage(err, '加载资源绑定失败'),
})
}
},
fetchGrants: async () => {
set({ grantsLoading: true, grantsError: null })
try {
const res = await heicodeResourcesApi.listGrants()
set({
grants: res.success ? (res.data?.items ?? []) : [],
grantsLoading: false,
grantsFetched: true,
grantsError: res.success ? null : (res.message || '加载资源授权失败'),
})
} catch (err) {
set({
grantsLoading: false,
grantsFetched: true,
grantsError: extractErrorMessage(err, '加载资源授权失败'),
})
}
},
createBinding: async (input) => {
set({ isMutating: true, mutationError: null })
try {
const res = await heicodeResourcesApi.createBinding(input)
if (!res.success || !res.data) {
set({ isMutating: false, mutationError: res.message || '创建资源绑定失败' })
return null
}
// Optimistic add (server is source of truth, but UX wins).
set((s) => ({
isMutating: false,
bindings: [res.data!, ...s.bindings],
}))
return res.data
} catch (err) {
set({ isMutating: false, mutationError: extractErrorMessage(err, '创建资源绑定失败') })
return null
}
},
deleteBinding: async (id) => {
set({ isMutating: true, mutationError: null })
try {
const res = await heicodeResourcesApi.deleteBinding(id)
if (!res.success) {
set({ isMutating: false, mutationError: res.message || '删除失败' })
return false
}
// Server soft-deletes (status=revoked). Refetch is safest.
await get().fetchBindings()
set({ isMutating: false })
return true
} catch (err) {
set({ isMutating: false, mutationError: extractErrorMessage(err, '删除失败') })
return false
}
},
createGrant: async (input) => {
set({ isMutating: true, mutationError: null })
try {
const res = await heicodeResourcesApi.createGrant(input)
if (!res.success || !res.data) {
set({ isMutating: false, mutationError: res.message || '创建授权失败' })
return null
}
set((s) => ({
isMutating: false,
grants: [res.data!, ...s.grants],
}))
return res.data
} catch (err) {
set({ isMutating: false, mutationError: extractErrorMessage(err, '创建授权失败') })
return null
}
},
revokeGrant: async (id) => {
set({ isMutating: true, mutationError: null })
try {
const res = await heicodeResourcesApi.revokeGrant(id)
if (!res.success) {
set({ isMutating: false, mutationError: res.message || '撤销失败' })
return false
}
await get().fetchGrants()
set({ isMutating: false })
return true
} catch (err) {
set({ isMutating: false, mutationError: extractErrorMessage(err, '撤销失败') })
return false
}
},
clearBindingsError: () => set({ bindingsError: null }),
clearGrantsError: () => set({ grantsError: null }),
clearMutationError: () => set({ mutationError: null }),
}))
+3 -4
View File
@@ -5,10 +5,9 @@ const TAB_STORAGE_KEY = 'cc-haha-open-tabs'
export const SETTINGS_TAB_ID = '__settings__'
export const SCHEDULED_TAB_ID = '__scheduled__'
export const RESOURCES_TAB_ID = '__resources__'
export const TERMINAL_TAB_PREFIX = '__terminal__'
export type TabType = 'session' | 'settings' | 'scheduled' | 'resources' | 'terminal'
export type TabType = 'session' | 'settings' | 'scheduled' | 'terminal'
export type Tab = {
sessionId: string
@@ -163,13 +162,13 @@ export const useTabStore = create<TabStore>((set, get) => ({
const validTabs: Tab[] = data.openTabs
.filter((t) => {
// Special tabs are always valid
if (t.type === 'settings' || t.type === 'scheduled' || t.type === 'resources') return true
if (t.type === 'settings' || t.type === 'scheduled') return true
if (t.type === 'terminal') return false
// Session tabs must exist on server
return existingIds.has(t.sessionId)
})
.map((t) => {
if (t.type === 'settings' || t.type === 'scheduled' || t.type === 'resources') {
if (t.type === 'settings' || t.type === 'scheduled') {
return { sessionId: t.sessionId, title: t.title, type: t.type, status: 'idle' as const }
}
return {
-193
View File
@@ -1,193 +0,0 @@
/**
* Heicode Resources Proxy — exposes mcp-server's P1 ResourceBinding /
* ResourceGrant endpoints to the desktop client through the local server.
*
* Why proxy?
* - Desktop UI shouldn't deal with cross-origin / token refresh / Manager
* base URL switching. The local Bun server already holds the tokens
* (in providers.json under `mcpAuth`) so it's the natural place to
* attach the Authorization header and refresh transparently.
*
* Routes (mirror mcp-server contract `Heicode-接口契约文档.md` §2 + §3):
*
* GET /api/heicode-resources → /api/resources
* POST /api/heicode-resources → /api/resources
* GET /api/heicode-resources/:id → /api/resources/:id
* PUT /api/heicode-resources/:id → /api/resources/:id
* DELETE /api/heicode-resources/:id → /api/resources/:id
*
* GET /api/heicode-resources/grants → /api/resource-grants
* POST /api/heicode-resources/grants → /api/resource-grants
* GET /api/heicode-resources/grants/:id → /api/resource-grants/:id
* DELETE /api/heicode-resources/grants/:id → /api/resource-grants/:id
*
* Auth: pulls JWT pair from active provider's `mcpAuth`. If the access
* token is within 60s of expiry (or already expired) but refresh token is
* still valid, transparently calls mcp-server's /api/auth/refresh and
* persists the new token pair before forwarding the request.
*/
import { ProviderService } from '../services/providerService.js'
import { ApiError, errorResponse } from '../middleware/errorHandler.js'
import type { McpAuth, SavedProvider } from '../types/provider.js'
const providerService = new ProviderService()
const ACCESS_REFRESH_BUFFER_MS = 60_000 // refresh if < 60s left on access token
export async function handleHeicodeResourcesApi(
req: Request,
url: URL,
segments: string[],
): Promise<Response> {
try {
// segments: ['api', 'heicode-resources', ...rest]
const rest = segments.slice(2)
const isGrants = rest[0] === 'grants'
const upstreamBase = isGrants ? '/api/resource-grants' : '/api/resources'
const tail = isGrants ? rest.slice(1) : rest
const upstreamPath = tail.length > 0 ? `${upstreamBase}/${tail.join('/')}` : upstreamBase
const { provider, mcpAuth } = await getActiveMcpAuth()
const freshAuth = await ensureFreshAccessToken(provider, mcpAuth)
const upstreamUrl =
`${freshAuth.managerLoginUrl.replace(/\/+$/, '')}${upstreamPath}` +
(url.search || '')
const headers: Record<string, string> = {
Authorization: `Bearer ${freshAuth.accessToken}`,
'X-Request-Id': crypto.randomUUID(),
}
if (req.method === 'POST' || req.method === 'PUT') {
headers['Content-Type'] = 'application/json'
}
headers['Accept'] = 'application/json'
const init: RequestInit = {
method: req.method,
headers,
signal: AbortSignal.timeout(20_000),
}
if (req.method === 'POST' || req.method === 'PUT') {
init.body = await req.text()
}
const upstream = await fetch(upstreamUrl, init).catch((err) => {
throw ApiError.badGateway(
`调用 Heicode Manager 失败: ${err instanceof Error ? err.message : String(err)}`,
)
})
// Pass through status + body. We don't stream — payloads are small.
const body = await upstream.text()
return new Response(body, {
status: upstream.status,
headers: {
'Content-Type': upstream.headers.get('content-type') || 'application/json',
},
})
} catch (err) {
return errorResponse(err)
}
}
// ─── Helpers ──────────────────────────────────────────────────────
async function getActiveMcpAuth(): Promise<{ provider: SavedProvider; mcpAuth: McpAuth }> {
const { providers, activeId } = await providerService.listProviders()
if (!activeId) {
throw ApiError.unauthorized('未登录 Heicode Manager。请先登录。')
}
const provider = providers.find(p => p.id === activeId)
if (!provider) {
throw ApiError.unauthorized('未找到当前活跃 provider。请重新登录。')
}
if (!provider.mcpAuth) {
throw ApiError.unauthorized(
'当前登录方式不支持资源绑定接口。请退出后用邮箱+密码登录(mcp-server 平台账号)。',
)
}
return { provider, mcpAuth: provider.mcpAuth }
}
async function ensureFreshAccessToken(
provider: SavedProvider,
mcpAuth: McpAuth,
): Promise<McpAuth> {
const now = Date.now()
// Access token still has plenty of life → use as-is.
if (mcpAuth.accessExpiresAt - now > ACCESS_REFRESH_BUFFER_MS) {
return mcpAuth
}
// Refresh token also expired → user must re-login.
if (mcpAuth.refreshExpiresAt - now <= 0) {
throw ApiError.unauthorized(
'Heicode Manager 登录会话已过期(refresh token 失效)。请退出重新登录。',
)
}
// Refresh.
const refreshUrl = `${mcpAuth.managerLoginUrl.replace(/\/+$/, '')}/api/auth/refresh`
const res = await fetch(refreshUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
Authorization: `Bearer ${mcpAuth.refreshToken}`,
},
redirect: 'manual',
signal: AbortSignal.timeout(15_000),
}).catch((err) => {
throw ApiError.badGateway(
`刷新 Manager token 失败: ${err instanceof Error ? err.message : String(err)}`,
)
})
let body: {
success?: boolean
message?: string | null
data?: { token?: string; refreshToken?: string }
} = {}
try { body = (await res.json()) as typeof body } catch { /* */ }
if (!body.success || !body.data?.token || !body.data.refreshToken) {
throw ApiError.unauthorized(
`刷新 Manager token 失败: ${body.message || `HTTP ${res.status}`}。请退出重新登录。`,
)
}
// Re-decode exp claims.
const newAccess = body.data.token
const newRefresh = body.data.refreshToken
const accessExp = decodeJwtExpMs(newAccess)
const refreshExp = decodeJwtExpMs(newRefresh)
const refreshed: McpAuth = {
...mcpAuth,
accessToken: newAccess,
refreshToken: newRefresh,
accessExpiresAt: accessExp ?? Date.now() + 24 * 3600 * 1000,
refreshExpiresAt: refreshExp ?? Date.now() + 7 * 24 * 3600 * 1000,
}
// Persist back to providers.json so the next request reuses the new token.
await providerService.updateProvider(provider.id, { mcpAuth: refreshed })
return refreshed
}
function decodeJwtExpMs(jwt: string): number | null {
try {
const parts = jwt.split('.')
if (parts.length !== 3) return null
const payloadB64 = parts[1]!
const base64 = payloadB64.replace(/-/g, '+').replace(/_/g, '/')
const padded = base64 + '='.repeat((4 - (base64.length % 4)) % 4)
const json = Buffer.from(padded, 'base64').toString('utf-8')
const claims = JSON.parse(json) as { exp?: number }
if (typeof claims.exp !== 'number') return null
return claims.exp * 1000
} catch {
return null
}
}
-4
View File
@@ -19,7 +19,6 @@ import { handleSkillsApi } from './api/skills.js'
import { handleComputerUseApi } from './api/computer-use.js'
import { handleHahaOAuthApi } from './api/haha-oauth.js'
import { handleHeicodeAuthApi } from './api/heicode-auth.js'
import { handleHeicodeResourcesApi } from './api/heicode-resources.js'
import { handleMcpApi } from './api/mcp.js'
export async function handleApiRequest(req: Request, url: URL): Promise<Response> {
@@ -77,9 +76,6 @@ export async function handleApiRequest(req: Request, url: URL): Promise<Response
case 'heicode-auth':
return handleHeicodeAuthApi(req, url, segments)
case 'heicode-resources':
return handleHeicodeResourcesApi(req, url, segments)
case 'adapters':
return handleAdaptersApi(req, url, segments)
@@ -0,0 +1,82 @@
# 01. 产品说明文档
## 产品名称
Heicode
## 产品定位
Heicode 是一款覆盖软件生命周期的智能开发 Code 工具。它面向的是“从想法到上线”的完整开发过程,而不是单个模型聊天窗口、CodeGW 管理后台或简单的 Agent 控制台。
用户只需要注册登录,输入想法和约束,Heicode 就帮助用户组织子 Agnet 团队,接入用户授权的 Git、SK、项目文档和云资源,完成产品设计、代码开发、代码检查、部署、观测和后续维护。
## 目标用户
| 用户 | 典型问题 | Heicode 价值 |
|------|----------|--------------|
| 独立开发者 | 有想法但缺工程团队 | 用 AI 团队完成从需求到上线 |
| 创业团队 | 人少、迭代快、交付压力大 | 用 Heicode 编排产品、开发、测试、部署流程 |
| 企业创新团队 | 有资源和代码,但协作成本高 | 在既有 Git 和云资源边界内受控开发 |
| 技术负责人 | 担心权限、成本、质量和审计 | 统一资源授权、模型用量、日志和审计 |
## 核心价值
1. 把自然语言想法变成可执行的软件开发任务。
2. 把 Git、文档、SK、云资源变成可授权、可撤销、可审计的资源。
3. 把子 Agnet 的角色、权限、模型和上下文组织成可运行的开发团队。
4. 把持续开发拆成需求、设计、开发、测试、修复、部署等子环节,由 Agnet 持续推进。
5. 让 Agnet 在执行过程中可以调用项目 SK 工具和外部能力,而不是只做一次性任务分发。
6. 把模型调用、余额、额度和日志统一展示给用户。
7. 把长期密钥放入密钥保管器,只给子 Agnet 短期、最小权限凭证。
8. 把开发、检查、部署、维护纳入同一个生命周期闭环。
## 产品形态
| 产品面 | 说明 |
|--------|------|
| Heicode 客户端 | 用户主体验,负责本地对话、输入想法、继续开发、查看执行反馈、接收交付结果和高危审批,只登录 Heicode,只使用 Heicode 提供的模型 |
| Heicode Manager / 浏览器控制台 | 辅助控制台,负责账号与安全、客户端下载、Git/云资源绑定、Agnet 部署、任务状态总览、模型余额与用量、审计与日志查看 |
| Agnet 平台 | 执行层,负责部署和运行子 Agnet,在任务推进过程中完成需求、开发、测试、修复、交付与部署,并回传日志、状态、事件和指标 |
| CodeGW | 内部模型网关与计费服务,普通用户不直接进入后台 |
| 密钥保管器 | OpenBao 实现,保存长期凭证,按审批和权限提供短期凭证租约 |
说明:Heicode 客户端是用户主体验,用户不在网页上编码。Heicode Manager 是浏览器里的辅助控制台,承担资源、部署、状态、余额、审计和下载等辅助操作。真正持续推进任务的是 Heicode 调度下的 Agnet 执行闭环,Agnet 在过程中还可以调用已授权的 SK 工具和外部能力,最终完成交付与部署并把结果回传到客户端。
## 核心流程
```text
注册登录
-> 输入产品想法
-> 生成产品文档和任务计划
-> 在 Manager 绑定 Git 和云资源
-> 在 Manager 部署 Agnet
-> 客户端继续追加需求和修正方向
-> Agnet 按子环节执行需求、开发、测试、修复
-> Agnet 按需要调用已授权 SK 工具
-> 客户端确认高危操作
-> Agnet 完成交付物整理和部署
-> Manager 查看状态、余额、审计和下载
-> 客户端展示执行反馈和交付结果
-> 持续维护和升级
```
## 差异化
| 对比对象 | 差异 |
|----------|------|
| 普通 AI 聊天工具 | Heicode 不只回答问题,而是组织资源、权限和执行团队 |
| IDE 插件 | Heicode 不局限于本地代码编辑,而覆盖部署、审计和生命周期 |
| CodeGW 后台 | Heicode 不管理渠道后台,面向用户完成开发任务 |
| 传统 DevOps 平台 | Heicode 以自然语言和 AI 团队为核心,自动生成任务和执行上下文 |
## 产品承诺
1. 用户不需要理解底层模型供应商。
2. 用户不需要把密钥交给子 Agnet 长期保存。
3. 用户可以清楚知道哪些资源被哪个角色使用。
4. 高危操作必须经过客户端审批。
5. 所有关键动作都能审计。
## 当前一句话卖点
> 从一个想法开始,让 Heicode 组织 AI 开发团队,持续调用 Agnet 完成开发、测试、交付与部署,安全接入你的代码和云资源,把产品推进到生产环境。
@@ -0,0 +1,173 @@
# 02. 产品首页设计文档
## 首页目标
首页要让用户在第一屏理解三件事:
1. Heicode 是全流程智能开发工具。
2. 用户可以从想法开始,到代码、部署和维护。
3. Heicode 能安全接入 Git、云资源、模型和密钥保管器。
首页不是 CodeGW 后台入口,也不是单纯 Agent 平台介绍页。
## 首页受众
| 受众 | 第一眼要看到 |
|------|--------------|
| 创业者 | 想法可以快速变成产品 |
| 开发者 | 能接入真实代码仓库和云资源 |
| 技术负责人 | 权限、密钥、审计和成本可控 |
| 投资人/客户 | 这是完整的软件生命周期平台 |
## 首屏文案
标题建议:
> 从一个想法,到可上线的软件产品
副标题建议:
> Heicode 组织 AI 开发团队,接入你的代码、文档、SK 和云资源,完成需求、开发、检查、部署和后续维护。
主按钮:
- 开始使用 Heicode
次按钮:
- 查看产品演示
首屏辅助信息:
- 绑定 Git 与云资源
- 生成子 Agnet 团队
- 密钥保管器托管凭证
- CodeGW 统一模型与用量
## 首页结构
### 1. Hero 区
重点表达:
```text
想法输入
-> AI 开发团队
-> 代码与云资源
-> 生产部署
```
视觉建议:
- 使用真实产品界面截图或产品工作流图。
- 不做抽象渐变英雄图。
- 第一屏必须看到 Heicode 名称和产品主张。
- CTA 明确指向登录或演示。
### 2. 问题区
标题:
> 现在的软件开发,不缺模型,缺完整流程
要点:
- 想法和代码之间缺少可执行路径。
- AI 工具无法安全使用用户的云资源和密钥。
- 多个 Agent 缺少角色、权限和审计。
- 模型用量、余额、日志分散在后台。
- 部署和后续维护没有闭环。
### 3. 解决方案区
标题:
> Heicode 把开发流程变成可编排的 AI 团队任务
四个能力:
| 能力 | 说明 |
|------|------|
| 想法转任务 | 把自然语言目标转成产品文档、任务和角色 |
| 资源绑定 | 接入 Git、SK、项目文档、云账号和云资源 |
| 权限控制 | 按角色分配 Resource Grant 和审批规则 |
| 执行闭环 | 子 Agnet 持续推进开发、测试、修复和部署,可调用 SK 工具,Heicode 展示日志和审计 |
### 4. 工作流区
标题:
> 从登录到上线,一条主流程
流程:
1. 登录 Heicode。
2. 输入想法。
3. 绑定资源。
4. 生成 AI 开发团队。
5. 确认权限和审批。
6. 执行开发与部署。
7. 查看日志、用量和审计。
8. 持续维护。
### 5. 安全区
标题:
> 资源可以使用,密钥不能外泄
说明:
- 长期密钥进入密钥保管器。
- Heicode 服务端数据库只保存 `secret_ref`。
- 子 Agnet 只拿短期凭证。
- 高危操作在客户端审批。
- 日志、Markdown、前端响应不显示明文密钥。
### 6. 系统边界区
标题:
> 每个系统各司其职
| 系统 | 职责 |
|------|------|
| Heicode Manager | 资源、部署、状态、用量和审计辅助控制台 |
| 客户端 | 主对话体验、任务推进和高危审批 |
| Agnet 平台 | AKS 执行层 |
| CodeGW | 模型网关与用量 |
| OpenBao | 密钥保管器 |
补充说明:
- 用户主要在客户端对话和推进任务。
- Manager 主要承担资源准备、Agnet 部署、用量和审计辅助。
### 7. 适用场景区
场景:
- 从 0 到 1 创建 SaaS 产品。
- 为已有代码库增加功能。
- 让 AI 团队维护和升级系统。
- 在云资源边界内自动部署。
- 对模型用量和高危操作做审计。
### 8. CTA 区
标题:
> 让你的下一个想法进入开发流程
按钮:
- 登录 Heicode
- 观看 5 分钟演示
## 风格建议
- 产品感应偏专业、清晰、可信,不要娱乐化。
- 信息密度适中,避免空泛大词。
- 主色与 Heicode 和客户端保持一致。
- 图片优先使用真实界面和流程,不使用无意义装饰图。
- 首页文案避免出现 CodeGW 后台、OpenBao 控制台、模型提供方配置等内部概念。
@@ -0,0 +1,184 @@
# 03. 用户旅程与核心流程
## 旅程目标
用户从“我有一个想法”开始,不需要先学习 Agent、CodeGW、密钥保管器或部署系统。Heicode 应该用一个连续流程带用户完成:
```text
想法
-> 资源
-> 团队
-> 权限
-> 执行
-> 上线
-> 维护
```
## 用户旅程总览
| 阶段 | 用户动作 | 平台动作 | 输出 |
|------|----------|----------|------|
| 1. 登录 | 注册或登录 Heicode | 获取用户身份和 channelId | 用户会话 |
| 2. 输入想法 | 描述要做的产品或任务 | 生成需求摘要和任务草案 | 需求草案 |
| 3. 绑定资源 | 授权 Git、文档、SK、云资源 | 保存资源元数据和 `secret_ref` | Resource Binding |
| 4. 生成团队 | 确认开发方法和角色 | 推荐子 Agnet 角色 | 角色方案 |
| 5. 分配权限 | 确认每个角色能用什么 | 生成 Resource Grant | 权限清单 |
| 6. 审批高危操作 | 在客户端确认高危动作 | 记录 approval | 审批记录 |
| 7. 执行任务 | 持续追加需求、查看进度 | Agnet 平台运行子 Agnet,按子环节推进开发 | 状态、日志、事件 |
| 8. SK 工具调用 | 允许平台使用技能能力 | Agnet 按权限调用 SK 工具和外部能力 | 中间产物、检查结果 |
| 9. 交付上线 | 确认部署结果 | Agnet 完成交付整理、部署并回写审计 | 生产服务 |
| 10. 维护升级 | 提出迭代或修复 | 复用上下文和权限再次进入 Agnet 闭环 | 新版本计划 |
## 关键任务状态流程
### 1. 初始入口
用户看到:
- 当前任务。
- 任务上下文状态。
- 模型余额和用量。
- 最近失败或待审批项。
- 客户端下载入口。
页面目标:
- 让用户知道下一步该做什么。
- 避免用户先进入复杂配置页。
### 2. 想法输入与追问
用户输入:
- 产品想法。
- 目标用户。
- 功能范围。
- 已有代码或是否从零开始。
- 部署目标。
- 风险和约束。
平台输出:
- 需求摘要。
- 产品文档草案。
- 原型描述。
- 推荐资源需求。
- 推荐子 Agnet 角色。
### 3. 任务上下文准备
用户应该看到简化路径:
```text
Heicode 判断本任务缺少哪些上下文
-> 用户授权代码、文档、SK 或云账号
-> 平台自动发现资源
-> 用户确认本任务允许使用的资源范围
-> Heicode 保存元数据和 secret_ref
```
任务上下文不要设计成大量裸字段表单。云账号授权后,应尽量自动展示已发现的 VM、数据库、对象存储、Kubernetes 或资源组。
### 4. 风险和权限确认卡
默认展示角色视角:
```text
Backend Agnet
- 可读写:后端代码路径
- 可读:项目文档
- 可部署:测试环境
- 生产部署:需要审批
```
高级用户可以打开 manifest 预览,但普通流程不应要求用户手写 manifest。
### 5. 开始执行前确认
展示:
- 本次目标。
- 子 Agnet 数量和角色。
- 每个角色使用的资源。
- 是否会访问密钥。
- 是否会部署云资源。
- 预计模型预算。
- 审批项。
用户确认后,Heicode 生成 Agnet 平台 payload。
### 6. 执行中的任务空间
展示:
- 子 Agnet 活跃状态。
- 当前步骤。
- 最近日志。
- 失败原因。
- 模型用量。
- 资源访问记录。
- 审批记录。
用户可以:
- 暂停任务。
- 停止任务。
- 查看日志。
- 撤销资源授权。
- 发起修复或继续迭代。
### 7. Agnet 执行闭环
Heicode 不是只把任务丢给 Agnet 一次就结束,而是会在开发过程中持续调用 Agnet 完成子环节。
闭环应表达为:
```text
客户端输入目标或追加需求
-> Heicode 生成下一步任务
-> Agnet 执行需求/设计/开发/测试/修复中的当前子环节
-> Agnet 按需要调用已授权的 SK 工具
-> Heicode 回传中间结果给客户端
-> 用户继续追问、修正或审批
-> Agnet 继续下一子环节
-> 最终由 Agnet 完成交付整理与部署
```
这意味着用户看到的不是一次性“已部署 Agent”,而是一个可连续推进的开发循环。
## 简化原则
1. 用户先表达目标,再补资源。
2. 用户选择资源,不手写底层配置。
3. 用户选择角色权限,不手写策略。
4. 高危操作单独审批,不埋在复杂表单里。
5. 技术细节可展开,但默认隐藏。
6. 所有资源、权限、模型、日志都回到同一个任务视角。
## 复杂度拆解
资源绑定复杂度来自三类问题:
| 复杂点 | 简化方式 |
|--------|----------|
| 云资源字段多 | 绑定云账号后自动发现资源 |
| 权限动作多 | 使用角色模板和推荐权限 |
| 密钥安全难理解 | 用户只看“密钥保管器已托管”,不看明文 |
## MVP 用户路径
MVP 最小路径:
1. 用户登录 Heicode。
2. 下载并登录客户端。
3. 输入一个产品想法。
4. 绑定一个 Git 仓库。
5. 绑定一个云账号或导入云资源。
6. 选择推荐角色。
7. 确认资源权限。
8. 预览 manifest。
9. 创建 Agnet 部署占位任务。
10. 在客户端持续推进子环节开发与测试。
11. 查看任务状态、日志、审计和交付结果。
当前唯一可延期项:Heicode 到 Agnet 平台真实部署 API 的完整联调。
@@ -0,0 +1,185 @@
# 04. 登录平台和使用平台指南
## 面向用户
本文面向第一次使用 Heicode 的普通用户,说明如何登录、绑定资源、下载客户端、创建任务和查看结果。
## 访问入口
平台入口:
```text
https://code.xinghanlab.com
```
用户只需要登录 Heicode。普通用户不需要登录 CodeGW 后台,也不需要登录 OpenBao。
## 第一步:登录 Heicode
1. 打开 `https://code.xinghanlab.com`。
2. 输入账号和密码,或使用平台支持的登录方式。
3. 登录成功后进入 Heicode 控制台。
4. 进入首页,查看资源、模型、任务和客户端下载状态。
登录后平台会读取:
- 用户 ID。
- 用户邮箱。
- 用户角色。
- `channelId`。
这些信息用于用户身份、CodeGW 额度和用量映射。
## 第二步:下载客户端
1. 在 Heicode 控制台进入 `客户端下载`。
2. 选择对应系统版本,例如 macOS 或 Windows。
3. 下载并安装 Heicode 客户端。
4. 打开客户端后,只需要登录 `https://code.xinghanlab.com`。
客户端不会要求选择模型提供方。客户端可用模型由 Heicode 提供。
macOS 提示说明:
- 如果安装时出现系统安全提示,说明当前安装包可能还没有完成 Apple Developer ID 签名和 notarization。
- 用户可以按平台提供的安装说明继续打开。
- 长期方案应完成签名和 notarization。
## 第三步:在客户端输入想法
主要在 Heicode 客户端输入要做的事情。Manager 负责下载、资源绑定、Agnet 部署、状态和审计等辅助能力,不承担主开发对话。
例如:
```text
我想做一个面向小团队的任务管理 SaaS,
需要登录、项目、任务、评论、通知和后台管理,
希望部署到 Azure。
```
平台会生成:
- 需求摘要。
- 产品文档草案。
- 推荐开发角色。
- 需要绑定的资源。
- 高危操作提示。
## 第四步:绑定资源
进入 `资源绑定`。
可绑定资源:
- 代码仓库。
- SK 仓库或技能包。
- 项目文档。
- 云账号。
- 云资源。
推荐流程:
```text
先绑定 Git
-> 再绑定项目文档或 SK
-> 再绑定云账号
-> 从云账号自动发现资源
-> 选择允许使用的云资源
```
用户不需要把密钥贴到文档里。真实凭证由 Heicode 写入密钥保管器,Heicode 只保存 `secret_ref`。
## 第五步:分配权限
用户可以使用平台推荐角色:
- Product Agnet。
- Architect Agnet。
- Frontend Agnet。
- Backend Agnet。
- Reviewer Agnet。
- Ops Agnet。
每个角色确认:
- 能访问哪些仓库。
- 能读写哪些路径。
- 能使用哪些云资源。
- 哪些操作需要审批。
- 授权是否有有效期。
## 第六步:确认高危操作
高危操作会在客户端审批。
常见高危操作:
- 生产部署。
- 修改数据库。
- 创建或删除云资源。
- 访问生产密钥。
- 大额模型预算消耗。
用户确认后,平台才会为子 Agnet 派生短期凭证。
## 第七步:启动任务
用户确认任务后,Heicode 会生成:
- 子 Agnet 角色。
- AGENT.md。
- resource context。
- permission manifest。
- 模型和预算配置。
- 审批策略。
Heicode 将这些内容提交给 Agnet 平台执行。
## 第八步:在客户端持续推进
任务开始后,主流程回到客户端。
闭环应理解为:
```text
客户端继续补充要求
-> Heicode 生成下一步子任务
-> Agnet 执行需求/开发/测试/修复/部署中的当前环节
-> Agnet 按权限调用需要的 SK 工具
-> 中间结果和失败原因回到客户端
-> 用户继续修正或审批
-> 最终返回交付物和部署结果
```
## 第九步:查看执行结果
用户可以在 Heicode 查看:
- 当前任务状态。
- 子 Agnet 活动状态。
- 运行日志。
- 失败原因。
- 模型用量。
- 资源访问记录。
- 高危审批记录。
- 部署结果。
## 第十步:维护和升级
上线后,用户可以继续输入:
- 修复 bug。
- 增加功能。
- 优化性能。
- 升级依赖。
- 调整部署资源。
Heicode 会复用已有项目上下文、资源绑定和审计信息。
## 用户注意事项
1. 不要把密钥写入需求、Markdown、聊天内容或 Git。
2. 授权云资源时先从测试环境开始。
3. 生产部署必须仔细确认审批内容。
4. 如果不确定某个资源是否应该授权,选择只读或暂不授权。
5. CodeGW 后台不是普通用户入口,模型和余额在 Heicode 中查看。
+191
View File
@@ -0,0 +1,191 @@
# 05. 产品 PPT 文案
## Slide 1:标题
标题:
> Heicode
副标题:
> 从一个想法,到可上线的软件产品
讲述:
Heicode 是一款全流程智能开发 Code 工具。它让用户输入想法,接入自己的代码和云资源,由 AI 开发团队完成需求、开发、检查、部署和维护。
## Slide 2:问题
标题:
> AI 已经会写代码,但软件交付仍然很难
要点:
- 想法到需求、原型、代码之间仍然断裂。
- AI 工具通常不能安全使用真实 Git、云资源和密钥。
- 多 Agent 协作缺少角色、权限、状态和审计。
- 模型用量、余额和日志分散在后台系统。
- 部署和后续维护没有统一生命周期。
## Slide 3:解决方案
标题:
> Heicode 把开发过程变成可编排的 AI 团队任务
要点:
- 用户输入自然语言目标。
- Heicode 生成任务、角色、权限和执行上下文。
- 客户端是主体验,Manager 负责资源、部署和审计辅助。
- 子 Agnet 按角色推进开发、测试、修复和部署,并可调用 SK 工具。
- 密钥进入密钥保管器,子 Agnet 只拿短期凭证。
- Heicode 展示模型用量、日志、事件和审计。
## Slide 4:产品工作流
标题:
> 一条从想法到上线的主流程
流程:
```text
登录
-> 客户端输入想法
-> Manager 绑定资源
-> Manager 部署 Agnet
-> 客户端继续推进任务
-> Agnet 执行开发/测试/修复并调用 SK
-> 审批高危操作
-> Agnet 交付与部署
-> 观测与维护
```
讲述:
用户不需要直接操作 CodeGW 后台,也不需要直接管理 OpenBao。所有操作都在 Heicode 和客户端内完成。
## Slide 5:核心能力
标题:
> Heicode 的四个核心能力
1. 想法转开发任务。
2. 资源绑定和权限分配。
3. 子 Agnet 编排和执行。
4. 用量、日志、安全和审计闭环。
## Slide 6:系统架构
标题:
> Heicode 产品面连接执行、模型和密钥底座
架构:
```text
用户 / 客户端
-> Heicode Manager
-> Agnet 平台
-> CodeGW
-> OpenBao
-> Git / SK / 云资源
```
讲述:
客户端是主体验,Manager 是辅助控制台。Agnet 平台是执行层,执行过程中可调用 SK。CodeGW 是模型网关和计费服务。OpenBao 是密钥保管器。
## Slide 7:安全模型
标题:
> 资源可用,密钥不外泄
要点:
- Heicode 服务端数据库只保存 `secret_ref`。
- 长期密钥进入 OpenBao。
- 子 Agnet 只获得短期、最小权限凭证。
- 高危操作必须在客户端审批。
- 日志、Markdown、前端响应不包含明文密钥。
## Slide 8:用户价值
标题:
> 让小团队拥有完整软件交付能力
对独立开发者:
- 更快从想法到 MVP。
对创业团队:
- 用更少人力完成产品迭代。
对企业团队:
- 在权限和审计边界内使用 AI 开发。
对技术负责人:
- 控制资源、成本、质量和风险。
## Slide 9:当前 MVP
标题:
> 当前 MVP 先闭环三件事
1. Heicode、CodeGW、OpenBao 解耦部署。
2. 资源绑定、Resource Grant、manifest 和密钥保管。
3. 客户端只登录 Heicode,只使用 Heicode 模型。
延期项:
- Heicode 到 Agnet 平台真实部署 API 的完整联调。
## Slide 10:路线图
标题:
> 从可用工具到生命周期平台
阶段:
1. 资源绑定和密钥保管。
2. CodeGW 用户侧模型和用量展示。
3. 客户端登录、审批和下载闭环。
4. Agnet 平台真实部署联调。
5. 自动维护、升级、成本和质量观测。
## Slide 11:演示故事
标题:
> 5 分钟看懂 Heicode
演示:
1. 登录 Heicode。
2. 客户端输入产品想法。
3. Manager 绑定 Git 和云资源。
4. Manager 部署 Agnet 并展示权限。
5. 客户端继续推进任务。
6. 展示 Agnet 调用 SK 和当前子环节。
7. 客户端审批高危操作。
8. 查看交付、部署结果、日志和模型用量。
## Slide 12:结束页
标题:
> Heicode,让想法进入真实开发流程
收尾语:
> 不是只生成代码,而是把产品、代码、权限、模型、部署和维护组织成一个可审计的软件生命周期。
@@ -0,0 +1,108 @@
# 06. 安全与凭证管理说明
## 核心原则
Heicode 是 SaaS 产品,不能把凭证管理转嫁给用户,也不能让密钥散落在 Git、Markdown、日志或子 Agnet 长期状态中。
安全目标:
1. 用户授权资源,平台托管凭证。
2. Heicode 服务端数据库只保存元数据和 `secret_ref`。
3. 长期密钥进入密钥保管器。
4. 子 Agnet 只拿短期、最小权限、可审计凭证。
5. 高危操作必须由客户端审批。
## 凭证分类
| 凭证 | 示例 | 存放位置 |
|------|------|----------|
| Git 凭证 | GitHub token、SSH key | 密钥保管器 |
| 云凭证 | Azure、AWS、GCP access key | 密钥保管器 |
| 数据库凭证 | DB password、connection secret | 密钥保管器 |
| CodeGW 服务凭据 | 内部服务 token | 服务环境或密钥保管器 |
| 短期凭证 | 临时云 token、临时 Git token | 运行时注入,TTL 到期失效 |
## 密钥保管器
用户可见名称:密钥保管器。
技术实现:OpenBao,兼容 Vault API。
用户不需要直接登录 OpenBao,也不需要看到 OpenBao 控制台。
## Secret Broker 流程
```text
用户绑定资源
-> Heicode 接收授权结果
-> Heicode Secret Broker 写入 OpenBao
-> OpenBao 返回或形成 secret_ref
-> Heicode DB 保存 secret_ref
-> 前端只展示脱敏引用和状态
```
Heicode 接口不得返回明文密钥。日志、错误信息、审计摘要也不得包含明文密钥。
## 子 Agnet 访问凭证
子 Agnet 不保存长期密钥。
推荐流程:
```text
Heicode 生成 Resource Grant
-> Agnet 平台部署子 Agnet
-> Agnet 平台绑定运行时身份
-> 子 Agnet 请求使用资源
-> 高危操作先走客户端审批
-> 审批通过后派生短期凭证
-> 短期凭证注入运行时
-> TTL 到期或任务结束后失效
```
## 高危操作
高危操作包括:
- 生产环境部署。
- 云资源创建、删除、扩缩容。
- 数据库迁移或写入。
- 访问生产密钥。
- 大额模型预算消耗。
审批必须记录:
- 审批人。
- 审批时间。
- 操作类型。
- 资源范围。
- 风险等级。
- TTL。
- 对应 deployment 或任务。
## 禁止事项
1. 禁止把 token 写入 Git。
2. 禁止把密钥写入 Markdown。
3. 禁止前端返回明文密钥。
4. 禁止在日志中打印密钥。
5. 禁止把长期云密钥注入子 Agnet。
6. 禁止把 OpenBao 暴露为普通公网入口。
7. 禁止把 CodeGW key 原文交给客户端长期保存。
## 用户侧解释
推荐文案:
> Heicode 会把你的授权凭证存入密钥保管器。平台只在你授权的范围内,为对应任务和角色生成短期访问能力。密钥不会写入 Git、文档、日志或子 Agnet 长期状态。
## 验收标准
| 验收项 | 标准 |
|--------|------|
| 数据库 | 只保存 `secret_ref`,不保存明文密钥 |
| 前端 | 不显示明文密钥 |
| 日志 | 不包含 token、password、private key、access key |
| 子 Agnet | 不保存长期凭证 |
| 高危操作 | 有客户端审批记录 |
| OpenBao | 不对普通公网暴露 |
@@ -0,0 +1,148 @@
# 07. CodeGW、OpenBao、Agnet 平台边界说明
## 目标
本文用于讲清楚 Heicode 与 CodeGW、OpenBao、Agnet 平台之间的产品和技术边界,避免再次变成缝合式系统。
## 总体边界
| 系统 | 能力归属 | 普通用户是否直接进入 |
|------|----------|----------------------|
| Heicode | 产品总入口,包含客户端主体验与 Manager 辅助控制台 | 是 |
| CodeGW | 模型网关、模型调用、余额、额度、用量日志 | 否 |
| OpenBao | 长期凭证保管、短期凭证租约 | 否 |
| Agnet 平台 | AKS 执行层、状态、日志、事件、指标 | 否 |
## Heicode 与 CodeGW
Heicode 使用 CodeGW 的方式:
- 查询普通用户可见模型。
- 查询余额和额度。
- 查询用量和调用日志。
- 建立 `channelId`、CodeGW user、token 或 group 的映射。
Heicode 不做:
- CodeGW 渠道管理。
- CodeGW 模型供应商配置。
- CodeGW 价格配置。
- CodeGW 管理员用户管理。
- 把 CodeGW 后台开放给普通用户。
产品口径:
> CodeGW 是 Heicode 的模型网关和计费底座,用户在 Heicode 中查看模型和用量,不直接使用 CodeGW 后台。
## Heicode 与 OpenBao
Heicode 使用 OpenBao 的方式:
- 写入用户授权后的长期凭证。
- 保存和管理 `secret_ref`。
- 执行密钥 health、metadata、轮换、禁用、撤销。
- 为高危操作申请短期凭证。
Heicode 不做:
- 在数据库保存明文密钥。
- 把 OpenBao token 交给前端。
- 把 OpenBao 作为普通公网服务暴露。
产品口径:
> 密钥保管器负责保存凭证,Heicode 只保存引用。子 Agnet 使用资源时,平台按权限和审批结果获取短期访问能力。
## Heicode 与 Agnet 平台
Heicode 传给 Agnet 平台:
- 用户上下文。
- 任务目标。
- 子 Agnet 角色。
- AGENT.md。
- resource context。
- permission manifest。
- `secret_ref`。
- 审批策略。
- 模型和预算配置。
- `sk_context` / `tool_permissions`。
- 回调地址。
Agnet 平台回传 Heicode:
- deployment 状态。
- 子 Agnet 状态。
- 事件。
- 日志。
- 指标。
- 审计事件。
- 使用量。
- 当前子环节状态。
- 中间交付物。
- 部署结果。
Heicode 不传:
- 长期明文密钥。
- CodeGW key 原文。
- OpenBao root token。
- 数据库密码。
产品口径:
> Heicode 决定用户想做什么、能用什么资源和谁来做;Agnet 平台负责把这些任务安全运行起来,在过程中按权限调用需要的 SK 工具,并把子环节状态、交付物和部署结果回传给 Heicode。
## 客户端与 Manager 的边界
客户端主做:
- 登录 Heicode。
- 对话和任务输入。
- 继续补充要求和修正方向。
- 展示 Heicode 提供的模型。
- 高危操作审批。
- 查看执行反馈和交付结果。
Manager 主做:
- 账号与安全。
- 客户端下载。
- Git、文档、SK、云资源绑定。
- Agnet 部署。
- 任务状态、用量和审计查看。
- 风险与授权范围确认。
客户端不做:
- 选择模型提供方。
- 配置 CodeGW 渠道。
- 直接访问 OpenBao。
- 直接保存长期云密钥。
Manager 不做:
- 主开发对话。
- 网页编码。
- 替代客户端完成高危审批。
## 当前延期项
Heicode 到 Agnet 平台真实部署 API 的完整联调仍可延期。
但 Heicode 侧必须准备好:
- payload。
- manifest。
- 审计字段。
- callback URL。
- 本地占位控制面。
- 日志、事件、指标的数据展示位置。
## 验收口径
1. 普通用户只进入 Heicode。
2. 客户端只登录 Heicode。
3. CodeGW 独立服务运行。
4. OpenBao 只通过受控内网或后端访问。
5. Agnet 平台接口只收结构化上下文和 `secret_ref`,不收长期明文密钥。
+118
View File
@@ -0,0 +1,118 @@
# 08. Heicode 客户端说明
## 客户端定位
Heicode 客户端是用户主体验,承担本地对话、想法输入、任务推进、执行反馈、交付接收和高危审批。它不是独立模型配置工具,也不是 CodeGW 后台客户端。
Heicode Manager 不是主体验,但它仍然负责账号与安全、客户端下载、Git/云资源绑定、Agnet 部署、任务状态、余额与审计等辅助操作。用户不在网页上编码,主要在客户端里推进开发任务。
客户端必须保持:
- 只登录 Heicode。
- 只使用 Heicode 提供的模型。
- 不出现模型提供方选择。
- 不出现旧服务入口。
- 视觉风格与 Heicode 保持一致。
## 登录
客户端登录地址:
```text
https://code.xinghanlab.com
```
用户打开客户端后,点击登录,跳转或打开 Heicode 登录流程。登录完成后,客户端读取 Heicode 会话状态。
## 模型
客户端模型来源:
```text
Heicode
-> CodeGW 用户侧模型能力
-> 客户端展示可用模型
```
客户端不内置默认 Claude 模型,也不让用户选择模型提供方。用户看到什么模型,由 Heicode 和 CodeGW 映射决定。
## 主要功能
| 功能 | 说明 |
|------|------|
| 对话输入 | 用户描述产品想法、开发任务或维护需求 |
| 任务推进 | 继续要求、查看 Agnet 执行反馈、接收交付结果 |
| 任务状态 | 查看 Heicode 返回的任务和子 Agnet 进度 |
| 模型选择 | 从 Heicode 提供的模型中选择 |
| 高危审批 | 对生产部署、密钥访问、云操作做确认 |
| 日志反馈 | 展示关键执行反馈和失败原因 |
| 安装更新 | 从 Heicode 下载页获取安装包 |
## 客户端里的 Agnet 闭环
客户端里发生的不是单次对话,而是持续推进的任务闭环:
```text
用户输入想法或补充要求
-> Heicode 判断当前子任务
-> 调用 Agnet 执行需求/开发/测试/修复/部署中的某一环
-> Agnet 按权限调用需要的 SK 工具
-> 中间结果、失败原因和下一步建议回到客户端
-> 用户继续补充、修正或审批
-> Agnet 继续推进
-> 最终返回交付物和部署结果
```
所以客户端应该更像“任务驾驶舱”,而不是一次性聊天窗口。
## 高危审批体验
审批弹窗应清楚展示:
- 操作类型。
- 目标资源。
- 请求角色。
- 风险等级。
- 有效期。
- 预计影响。
- 是否会派生短期凭证。
用户操作:
- 批准。
- 拒绝。
- 查看详情。
## 安装包分发
安装包由 Heicode 下载页提供。
平台:
- macOS arm64。
- Windows。
- 后续可扩展 Linux。
macOS 注意:
- 如果未完成 Developer ID 签名和 notarization,系统可能弹出安全提示。
- 产品文案应说明当前安装方式和后续签名计划。
## 客户端不应该出现的内容
1. 模型提供方配置。
2. 旧登录入口。
3. 第三方路由登录入口。
4. CodeGW 后台入口。
5. OpenBao 地址或 token。
6. 长期云密钥输入框。
7. 无法解释的个人 API key 自动复制。
## 验收标准
1. 打开客户端只看到 Heicode 登录入口。
2. 登录地址是 `https://code.xinghanlab.com`。
3. 模型列表来自 Heicode。
4. 没有模型提供方菜单。
5. 没有旧品牌和旧模型默认文案。
6. 高危审批能明确展示风险和资源范围。
+177
View File
@@ -0,0 +1,177 @@
# 09. 产品演示脚本
## 演示目标
用 5 分钟讲清楚 Heicode 的核心价值:
> 用户从一个想法开始,绑定自己的代码和云资源,由 Heicode 组织 AI 开发团队完成开发、部署和审计。
## 演示准备
演示账号:
- 使用演示专用账号。
- 不展示真实密码。
- 不使用真实生产密钥。
演示资源:
- 一个演示 Git 仓库。
- 一个演示 SK 仓库或技能包。
- 一个演示云资源列表。
- 一个可展示的模型余额或用量页面。
- 一个客户端安装包下载入口。
## 5 分钟脚本
### 0:00 到 0:30 开场
话术:
> 今天演示 Heicode。它不是一个普通聊天工具,也不是 CodeGW 后台,而是一个从想法到生产的软件生命周期工具。用户输入想法,平台组织 AI 开发团队,并在权限和审计边界内使用代码、文档和云资源。
展示:
- 首页或 Heicode 登录页。
- 一句话定位。
### 0:30 到 1:00 登录和首页
操作:
1. 登录 `https://code.xinghanlab.com`。
2. 进入 Heicode 工作区。
话术:
> 用户只登录 Heicode。模型、资源、任务、日志和客户端下载都从这里进入,普通用户不需要进入 CodeGW 后台,也不需要直接接触密钥保管器。
展示:
- 首页。
- 模型或余额摘要。
- 客户端下载入口。
### 1:00 到 1:45 输入想法
操作:
输入演示想法:
```text
我想做一个小团队任务管理 SaaS,
需要登录、项目、任务、评论、通知和后台管理,
希望部署到 Azure。
```
话术:
> Heicode 先把自然语言想法整理成产品目标、功能范围、开发任务和需要的资源。
展示:
- 需求摘要。
- 推荐角色。
- 资源需求提示。
### 1:45 到 2:30 资源绑定
操作:
1. 打开资源绑定。
2. 选择演示 Git 仓库。
3. 选择演示云资源。
4. 展示密钥保管状态。
话术:
> 资源绑定不是把密钥贴到页面里。用户授权后,真实凭证进入密钥保管器,Heicode 只保存 secret_ref。子 Agnet 能看到的是资源元数据和权限,不是长期密钥。
展示:
- Git 资源。
- 云资源。
- `secret_ref` 或密钥保管状态。
### 2:30 到 3:15 角色和权限
操作:
1. 展示推荐子 Agnet 角色。
2. 打开某个角色权限。
3. 展示 manifest 预览。
话术:
> Heicode 会把资源按角色授权。例如 Backend Agnet 可以读写后端路径,Ops Agnet 可以部署测试环境,生产部署需要客户端审批。
展示:
- Product、Backend、Frontend、Reviewer、Ops 角色。
- Resource Grant。
- manifest 预览。
### 3:15 到 4:00 客户端和审批
操作:
1. 展示客户端下载入口。
2. 展示客户端登录只指向 Heicode。
3. 展示高危审批弹窗或审批设计稿。
话术:
> 客户端不选择模型提供方,也不配置 CodeGW。它只登录 Heicode,使用 Heicode 提供的模型。高危操作,例如生产部署或访问生产密钥,必须在客户端确认。
展示:
- 客户端登录页。
- 模型选择。
- 审批弹窗。
### 4:00 到 4:40 执行和观测
操作:
1. 切回客户端,追加一句新要求。
2. 展示当前 Agnet 子环节和 SK 工具调用。
3. 展示任务状态、日志、模型用量和审计。
话术:
> 主体验在客户端。用户继续补充要求后,Heicode 会调用 Agnet 推进下一子环节,例如开发、测试或修复。Agnet 在过程中还可以调用已授权的 SK 工具,最后把中间结果、交付物和部署结果回传回来。Manager 负责辅助查看状态、日志、用量和审计。
展示:
- 客户端追加需求。
- 当前 Agnet 子环节和 SK 工具调用。
- 子 Agnet 状态。
- 日志。
- 用量。
- 审计记录。
### 4:40 到 5:00 总结
话术:
> Heicode 的核心不是只生成代码,而是把想法、代码、云资源、模型、密钥、权限、部署和维护组织成一个可审计的软件生命周期。
结束 CTA:
- 开始使用 Heicode。
- 查看产品文档。
- 下载客户端。
## 演示注意事项
1. 不展示真实密钥。
2. 不进入 CodeGW 管理后台。
3. 不展示 OpenBao token。
4. 不强调底层配置复杂度,强调用户流程。
5. 如果 Agnet 真实部署尚未联调,明确说当前展示的是 Heicode 侧 payload、manifest 和占位控制面。
## 备用 1 分钟版本
话术:
> Heicode 是从想法到上线的智能开发工具。用户主要在客户端输入想法和继续推进任务,在 Manager 绑定 Git、文档、SK 和云资源并部署 Agnet。Heicode 生成子 Agnet 团队、角色和权限,密钥进入密钥保管器,子 Agnet 只拿短期凭证。Agnet 在执行中可以调用 SK 工具,最后完成交付与部署;CodeGW 提供模型、余额和用量,Heicode 展示日志、状态和审计。用户不需要进入 CodeGW 后台,也不需要管理 OpenBao,只在 Heicode 和客户端完成整个开发流程。
@@ -0,0 +1,328 @@
# 10. 前端细节说明
## 目标
本文用于指导 Heicode 和 Heicode 客户端的前端产品设计。重点不是罗列后台页面,而是明确用户如何在一个任务空间里完成“想法 -> 追问 -> 上下文授权 -> 执行 -> 审批 -> 交付”。
## 设计原则
1. 第一屏只强调“你想做什么”。
2. 主流程围绕当前任务展开,不围绕后台模块展开。
3. 资源、权限、模型、日志和审计都作为任务上下文出现。
4. 复杂配置默认由 Heicode 解释成确认卡片,高级细节可以展开。
5. 密钥永不明文展示,只展示密钥保管状态和 `secret_ref`。
6. 客户端只登录 Heicode,只展示 Heicode 提供的模型。
7. 页面状态必须覆盖空态、加载、错误、成功、撤销和过期。
## 全局体验结构
Heicode 不以后台菜单作为主体验。建议使用顶部轻量入口和主任务空间:
| 入口 | 作用 | 出现方式 |
|------|------|----------|
| 输入想法 | 创建新产品、新功能或维护任务 | 首屏主输入框 |
| 当前任务 | 查看草案、上下文、执行状态和下一步 | 首页主区域和任务详情 |
| 资源上下文 | 选择本任务可用的代码、文档、SK、云资源 | 任务准备卡片 |
| 风险确认 | 确认可用资源、禁止动作和审批规则 | 开始执行前确认卡 |
| 执行记录 | 查看日志、用量、资源访问和审计 | 任务详情抽屉 |
| 客户端下载 | 下载本地客户端并完成高危审批 | 顶部辅助入口或审批提示 |
产品面边界需要在信息架构中明确:
- Heicode 客户端是主任务驾驶舱。
- Heicode Manager 是资源、部署、状态、余额和审计的辅助控制台。
- 用户不在网页上编码,但会在网页完成辅助管理动作。
禁止普通用户体验出现:
- CodeGW 后台。
- 渠道管理。
- 模型供应商配置。
- OpenBao 控制台。
- 模型提供方选择。
- 旧服务入口。
## 视觉风格
| 元素 | 要求 |
|------|------|
| 色系 | Heicode 与客户端保持统一,专业、可信、低噪音 |
| 布局 | 任务空间优先,辅助信息以抽屉、浮层或卡片呈现 |
| 卡片 | 只用于任务、上下文、审批、交付物等明确对象 |
| 按钮 | 主按钮用于继续任务,次按钮用于查看详情或取消 |
| 状态 | 使用清晰标签:待确认、运行中、待审批、失败、已完成、已撤销 |
| 图标 | 资源、审批、日志、模型、客户端下载使用稳定图标语义 |
## 初始首页
### 目标
让用户登录后马上知道可以输入想法或继续任务。
### 内容
| 区块 | 内容 |
|------|------|
| 主输入 | “你想把什么想法变成可以上线的软件?” |
| 继续任务 | 最近任务、运行状态、待审批提示 |
| 今日焦点 | 当前最重要任务、失败任务、待确认事项 |
| 辅助入口 | 客户端下载、账户安全、最近审计 |
### 空态文案
> 输入一个产品想法,Heicode 会帮你生成任务、需要的上下文和执行计划。
## 想法输入与追问
### 目标
把用户自然语言输入转成可执行任务,而不是让用户先填完整表单。
### 初始输入
只展示一个多行输入框。用户可以说产品想法、开发任务、修复目标或维护诉求。
### Heicode 追问
Heicode 只追问影响执行路径的少量问题:
| 问题 | 选项示例 |
|------|----------|
| 是否基于已有代码 | 从零开始 / 选择已有仓库 |
| 是否需要部署 | 暂不部署 / 测试环境 / 生产环境需审批 |
| 执行风格 | 保守 / 平衡 / 主动 |
| 预算限制 | 使用默认 / 自定义预算 |
更多约束放在“继续补充要求”中,不作为第一屏表单。
## 任务卡
### 目标
让用户确认“我要做什么”和“第一版做到哪里”。
### 内容
| 区块 | 内容 |
|------|------|
| 目标 | Heicode 总结的产品或任务目标 |
| 第一版范围 | 功能清单、交付边界、暂不做事项 |
| 自动生成 | 产品说明、原型描述、开发任务、检查清单、部署步骤 |
| 待确认上下文 | 代码和文档、云资源、风险审批 |
### 交互
- 用户可以直接修改目标。
- 用户可以继续用自然语言补充要求。
- 下一步不是“去配置资源”,而是“确认任务,准备上下文”。
## 任务上下文卡
### 目标
让用户理解这个任务需要哪些上下文,并用最少操作授权。
### 卡片类型
| 卡片 | 用户看到的内容 | 系统处理 |
|------|----------------|----------|
| 代码上下文 | 选择仓库或从零开始 | 保存仓库元数据和授权引用 |
| 文档和 SK | 选择已有产品文档或 SK 仓库 | 作为任务上下文提供给执行层 |
| 云资源 | 连接云账号并自动发现资源 | 展示可用资源和风险级别 |
| 密钥保管 | 凭证将保存到密钥保管器 | 前端不展示明文密钥 |
### 简化要求
- 云账号绑定后优先自动发现 VM、数据库、对象存储、Kubernetes 或资源组。
- 不要求用户手写 VM ID、数据库 ID、AKS ID 等复杂字段。
- 对不能自动发现的资源提供“手动补充”兜底,但放在次级入口。
- 默认推荐低风险资源,高风险资源默认不选。
## 执行前确认卡
### 目标
让用户在开始前确认结果、边界和风险。
### 内容
| 区块 | 内容 |
|------|------|
| 本次会做 | 需求、代码、检查、测试部署等 |
| 本次允许使用 | 仓库、文档、SK、测试云资源 |
| 本次不会做 | 生产部署、生产数据库访问、导出长期密钥 |
| 高危规则 | 生产部署、生产密钥访问、破坏性操作必须客户端审批 |
| 预计消耗 | 模型预算或任务预算 |
### 高级展开
高级用户可以展开 manifest 预览,但默认折叠。展开后只展示:
- `resource_id`
- `resource_type`
- `allowed_actions`
- `constraints`
- `secret_ref`
不得展示明文密钥。
## 执行中的任务空间
### 目标
让用户看到任务进展、产物和下一步。
### 内容
| 区块 | 内容 |
|------|------|
| 当前子环节 | 需求、设计、后端、前端、检查、测试、部署的阶段状态 |
| Agnet 调用 | 当前角色、正在调用的 SK 工具、工具输出摘要 |
| 最新动态 | Heicode 翻译后的关键执行事件 |
| 当前产物 | 产品文档、接口草案、代码变更、检查报告 |
| 继续输入 | 用户继续追加需求或修正方向 |
| 交付回流 | 交付物、测试结果、部署结果、待审批动作 |
| 辅助查看 | 资源使用、模型消耗、审计记录、停止任务 |
### 交互要求
- 日志默认摘要化,用户需要时再展开原始日志。
- 失败必须展示可理解原因和建议动作。
- 停止任务要说明影响范围。
- 当前子环节必须可见,不能只显示“运行中”。
- 如果 Agnet 调用了 SK 工具,前端要展示“调用了什么、产出了什么、是否失败”。
- 交付和部署结果必须回到客户端任务空间,而不是只留在后台日志里。
## 任务用量与审计
### 目标
回答“本任务用了什么模型、访问了什么资源、谁批准了什么”。
### 呈现方式
默认作为任务详情里的抽屉或浮层,不作为主体验。
### 内容
| 类型 | 示例 |
|------|------|
| 模型用量 | 任务、模型、消耗、状态、失败原因 |
| 资源访问 | 仓库、文档、云资源、动作、角色 |
| 审批记录 | 批准、拒绝、过期、有效期 |
| 安全记录 | 密钥轮换、短期凭证租约、撤销 |
## 交付结果
### 目标
让用户看到完成结果、质量证明和下一步。
### 内容
| 区块 | 内容 |
|------|------|
| 交付物 | 产品说明、代码变更、测试环境、部署链接 |
| 质量结果 | 代码检查、安全检查、测试结果 |
| 可信记录 | 使用资源、模型消耗、高危审批 |
| 继续动作 | 继续迭代、发起维护任务、查看完整记录 |
## 客户端下载
### 目标
让用户清楚下载哪个客户端,以及安装后只登录 Heicode。
### 内容
| 区块 | 内容 |
|------|------|
| 推荐版本 | 根据系统提示 macOS 或 Windows |
| 下载列表 | macOS arm64、Windows |
| 安装说明 | macOS 安全提示、Windows 安装说明 |
| 登录说明 | 只登录 `https://code.xinghanlab.com` |
| 版本信息 | 版本号、构建时间、校验值 |
## 账户与安全
### 目标
管理账号基础信息、安全设置和通知。
### 禁止
- 不自动生成无法解释的普通密钥。
- 若未来需要 API 令牌,命名为“Heicode API 访问令牌”,并说明用途、权限、过期时间和撤销方式。
- 无用途的 Token 入口应删除。
## 客户端前端细节
### 登录
只保留:
- Heicode 品牌。
- Heicode 登录按钮。
- 登录地址说明:`https://code.xinghanlab.com`。
- 登录失败提示。
不出现:
- 模型提供方卡片。
- 旧服务入口。
- 第三方路由入口。
- Claude 默认模型说明。
### 主对话
显示:
- 当前任务。
- 输入框。
- Heicode 提供的模型列表。
- 文件和资源上下文。
- 执行状态。
### 高危审批
字段:
- 操作名称。
- 风险等级。
- 目标资源。
- 请求角色。
- 有效期。
- 是否需要短期凭证。
- 批准/拒绝按钮。
文案示例:
> Heicode 请求在生产环境执行数据库迁移。该操作将使用密钥保管器派生的 15 分钟短期凭证。请确认资源、范围和影响后再批准。
## 响应式要求
| 视口 | 要求 |
|------|------|
| 桌面 | 顶部入口 + 主任务空间 + 可选右侧上下文 |
| 平板 | 顶部入口收进更多操作,主流程保持单列 |
| 手机 | 只保留继续输入、查看状态和审批能力 |
## 状态清单
每个关键状态必须设计:
- 加载态。
- 空态。
- 成功态。
- 错误态。
- 权限不足态。
- 网络异常态。
- 撤销/禁用后的只读态。
## 文案规则
- 用“任务上下文”或“资源上下文”,不要把主流程叫成资源管理。
- 用“密钥保管器”,不用“OpenBao”作为普通用户标题。
- 用“客户端”,不用“本地模型提供方”。
- 用“模型与余额”,不用“CodeGW 后台”。
- 用“执行计划”,不用“控制面草案”。
@@ -0,0 +1,454 @@
# 11. Heicode 客户端主体验与 Manager 辅助控制台原型图
## 说明
本文是 Heicode 产品的低保真原型图,用来说明 Heicode 客户端和 Heicode Manager 分别承担什么用户体验。
核心边界:
- Heicode 客户端是主体验。用户主要在客户端里对话、描述想法、推进开发任务、查看执行反馈和审批高危操作。
- Heicode Manager / 浏览器控制台是辅助控制台。它负责账号与安全、客户端下载、Git 绑定、云资源绑定、Agnet 部署、任务状态总览、模型余额与用量、审计与日志查看。
- 用户不在网页上编码,也不在网页上完成主要开发对话。
- 用户会在 Manager 里做辅助操作和观测,但主开发体验仍然在客户端。
- CodeGW、密钥保管器、Agnet 平台后台都不是普通用户入口,只作为 Heicode 的底层能力。
## 原型范围
| 范围 | 是否属于本文原型 | 说明 |
|------|------------------|------|
| Heicode 客户端 | 是,主体验 | 本地对话、想法输入、任务推进、执行反馈、高危审批 |
| Heicode Manager / 浏览器控制台 | 是,辅助控制台 | 账号与安全、客户端下载、Git/云资源绑定、Agnet 部署、任务总览、余额与审计 |
| 网页编码界面 | 否 | 用户不在网页上编码 |
| 底层服务后台 | 否 | CodeGW、密钥保管器、Agnet 平台后台不直接暴露给普通用户 |
## 原型原则
1. 客户端承载“我要做什么”和“继续开发”的主流程。
2. Manager 承载资源准备、部署准备、任务总览、用量和审计等辅助能力。
3. 主开发对话、编码推进和高危审批仍然以客户端为主。
4. 高危动作必须在客户端清晰审批。
5. 继续开发时,Heicode 会持续调用 Agnet 推进需求、开发、测试、修复和部署等子环节。
6. Agnet 执行过程中可以按权限调用 SK 工具。
7. Manager 可以查看任务状态、用量和审计,但不把网页设计成 IDE 或网页编码工作台。
8. 客户端只登录 Heicode,只使用 Heicode 提供的模型。
## 总体关系
```mermaid
flowchart LR
Client["Heicode 客户端主体验"] --> Idea["输入想法 / 继续开发"]
Idea --> Task["生成任务卡"]
Task --> Manager["Manager 准备资源 / 查看辅助信息"]
Manager --> Deploy["Manager 部署 Agnet"]
Deploy --> Loop["Agnet 子环节循环"]
Loop --> Dev["需求 / 开发 / 测试 / 修复"]
Dev --> SK["按权限调用 SK 工具"]
SK --> Run["执行反馈和对话"]
Run --> Approval["高危审批"]
Approval --> Loop
Run --> Result["交付结果 / 部署结果"]
Manager --> Git["绑定 Git"]
Manager --> Cloud["绑定云资源"]
Manager --> Audit["任务总览 / 用量 / 审计"]
Manager --> Download["客户端下载 / 账号安全"]
Git --> Deploy
Cloud --> Deploy
Result --> Client
```
## 1. 客户端登录
客户端是用户进入 Heicode 主体验的入口。
```text
┌──────────────────────────────────────────────┐
│ │
│ Heicode │
│ 从一个想法,到可上线的软件产品 │
│ │
│ 登录地址 │
│ https://code.xinghanlab.com │
│ │
│ [登录 Heicode] │
│ │
│ 登录后,客户端会使用 Heicode 提供的模型。 │
│ │
└──────────────────────────────────────────────┘
```
### 设计意图
- 不出现模型提供方选择。
- 不出现旧服务入口或第三方路由入口。
- 登录目标只有 Heicode。
## 2. 客户端主界面
用户在客户端里输入想法、继续开发、查看反馈。
```text
┌──────────────────────────────────────────────────────────────────────────────┐
│ Heicode Client 模型:heicode-code 用户头像 │
├──────────────────────────────────────────────────────────────────────────────┤
│ 当前任务:未选择 │
│ │
│ 你想把什么想法变成可以上线的软件? │
│ ┌──────────────────────────────────────────────────────────────────────────┐ │
│ │ 做一个小团队任务管理 SaaS,需要登录、任务、评论、通知,部署到 Azure。 │ │
│ └──────────────────────────────────────────────────────────────────────────┘ │
│ [发送] │
│ │
│ 最近任务 │
│ ┌────────────────────────────┐ ┌────────────────────────────┐ │
│ │ 小团队任务管理 SaaS │ │ 企业微信通知集成 │ │
│ │ 后端实现中 │ │ 等待生产部署审批 │ │
│ └────────────────────────────┘ └────────────────────────────┘ │
│ │
│ 辅助:打开 Manager 绑定资源、部署 Agnet、查看状态 │
└──────────────────────────────────────────────────────────────────────────────┘
```
### 设计意图
- 主界面像本地开发对话空间,不像后台控制台。
- 用户不在网页上编码,主要通过客户端持续表达需求和接收反馈。
- Manager 承担辅助配置和观测,但不是主开发对话空间。
## 3. 客户端追问和任务卡
Heicode 在客户端里追问关键缺口,并生成任务卡。
```text
┌──────────────────────────────────────────────────────────────────────────────┐
│ Heicode Client / 小团队任务管理 SaaS │
├──────────────────────────────────────────────────────────────────────────────┤
│ │
│ 你:做一个小团队任务管理 SaaS,需要登录、任务、评论、通知,部署到 Azure。 │
│ │
│ Heicode:我可以开始整理任务。还需要确认 3 件事: │
│ │
│ 1. 基于已有仓库还是从零开始? │
│ [从零开始] [选择已有仓库] │
│ │
│ 2. 第一版是否需要部署? │
│ [暂不部署] [Azure 测试环境] [生产环境,需审批] │
│ │
│ 3. 执行风格? │
│ [保守] [平衡] [主动] │
│ │
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ 继续补充你的要求... │ │
│ └────────────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────────┘
```
任务卡:
```text
┌──────────────────────────────────────────────────────────────────────────────┐
│ 小团队任务管理 SaaS / 任务卡 │
├──────────────────────────────────────────────────────────────────────────────┤
│ 目标:做一个面向小团队的任务协作 SaaS。 │
│ │
│ 第一版范围 │
│ - 登录和团队空间 │
│ - 项目、任务、评论 │
│ - 通知能力 │
│ - Azure 测试环境部署 │
│ │
│ Heicode 将自动生成 │
│ 产品说明 / 原型描述 / 开发任务 / 检查清单 / 部署步骤 │
│ │
│ 需要 Manager 辅助确认 │
│ [绑定 Git] [绑定云资源] [部署 Agnet] [查看预算与审计] │
│ │
│ [修改目标] [去 Manager 准备] │
└──────────────────────────────────────────────────────────────────────────────┘
```
## 4. Manager 辅助控制台
Manager 是网页端辅助控制台,不是编码主体验,但依然承载资源、部署、账号、下载、任务状态、余额和审计等辅助操作。
```text
┌──────────────────────────────────────────────────────────────────────────────┐
│ Heicode Manager 模型余额 ¥128.40 用户头像 │
├──────────────────────────────────────────────────────────────────────────────┤
│ │
│ 当前任务辅助状态:小团队任务管理 SaaS │
│ │
│ ┌────────────────────────────┐ ┌────────────────────────────┐ │
│ │ Git 绑定 │ │ 云资源绑定 │ │
│ │ repo-main / main │ │ Azure / aks-test / db-dev │ │
│ └────────────────────────────┘ └────────────────────────────┘ │
│ ┌────────────────────────────┐ ┌────────────────────────────┐ │
│ │ 模型与余额 │ │ 审计与风险 │ │
│ │ ¥128.40 / 今日 ¥12.30 │ │ 1 个高危审批待处理 │ │
│ └────────────────────────────┘ └────────────────────────────┘ │
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ Agnet 部署 │ │
│ │ 角色:Product / Backend / Frontend / Reviewer / Ops │ │
│ │ 状态:待部署 │ │
│ │ [部署 Agnet] │ │
│ └────────────────────────────────────────────────────────────────────────┘ │
│ │
│ 你可以在这里做辅助操作: │
│ [下载客户端] [查看任务状态] [确认资源范围] [查看审计记录] [管理账户安全] │
│ │
│ 提示:想法输入、开发对话、编码推进和审批请回到 Heicode 客户端。 │
└──────────────────────────────────────────────────────────────────────────────┘
```
### 设计意图
- Manager 展示资源、部署、任务状态、余额和审计等辅助信息。
- Manager 不承载网页编码。
- Manager 不替代客户端对话主流程。
## 5. Manager 绑定 Git 和云资源
Git 和云资源在 Manager 中绑定,作为部署 Agnet 的前置条件。
```text
┌──────────────────────────────────────────────────────────────────────────────┐
│ Heicode Manager / 绑定 Git 和云资源 │
├──────────────────────────────────────────────────────────────────────────────┤
│ 客户端任务:小团队任务管理 SaaS │
│ │
│ ┌──────────────────────────────────────────────────────────────────────────┐ │
│ │ Git 仓库 │ │
│ │ 状态:还没有可用仓库 │ │
│ │ [授权 GitHub/GitLab/Gitee/自建 Git] │ │
│ └──────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────────────────────┐ │
│ │ 云资源 │ │
│ │ 已发现:aks-test、db-dev、aks-prod、db-prod │ │
│ │ [选择本任务允许使用的资源] │ │
│ └──────────────────────────────────────────────────────────────────────────┘ │
│ │
│ [保存资源绑定] [继续部署 Agnet] │
└──────────────────────────────────────────────────────────────────────────────┘
```
### 设计意图
- Git 和云资源绑定属于 Manager。
- 用户不在网页上写代码。
- 绑定完成后可以继续部署 Agnet,也可以返回查看任务状态与辅助信息。
## 6. Manager 云资源自动发现
```text
┌────────────────────── Heicode 发现了可用 Azure 资源 ────────────────────────┐
│ │
│ 你不需要手动填写资源 ID。请选择客户端任务允许使用的范围: │
│ │
│ 建议用于本任务 │
│ [x] aks-test Kubernetes 测试部署 │
│ [x] db-dev PostgreSQL 开发读写 │
│ │
│ 高风险资源 │
│ [ ] aks-prod Kubernetes 生产部署,需要客户端审批 │
│ [ ] db-prod PostgreSQL 生产数据,默认禁止 │
│ │
│ 凭证处理 │
│ 长期凭证保存到密钥保管器。执行时只申请短期凭证。 │
│ │
│ [取消] [允许本任务使用] │
└──────────────────────────────────────────────────────────────────────────────┘
```
## 7. Manager 部署 Agnet
```text
┌──────────────────────────────────────────────────────────────────────────────┐
│ Heicode Manager / 部署 Agnet │
├──────────────────────────────────────────────────────────────────────────────┤
│ 任务:小团队任务管理 SaaS │
│ │
│ 已绑定资源 │
│ - Git:repo-main / main │
│ - 云资源:aks-test、db-dev │
│ │
│ Agnet 角色 │
│ [x] Product 需求和验收 │
│ [x] Backend API / 数据模型 │
│ [x] Frontend 页面和交互 │
│ [x] Reviewer 质量和安全检查 │
│ [x] Ops 测试环境部署 │
│ │
│ 安全边界 │
│ - 生产部署需要客户端审批 │
│ - 生产数据默认禁止 │
│ - 长期密钥不下发给 Agnet │
│ │
│ [返回修改资源] [部署 Agnet] │
└──────────────────────────────────────────────────────────────────────────────┘
```
### 设计意图
- Manager 的最终动作是部署 Agnet。
- Manager 不继续承担开发任务执行。
- 部署完成后主流程回到客户端,但状态和审计仍可在 Manager 查看。
## 8. 客户端执行空间
Agnet 部署完成后,主流程回到客户端。
```text
┌──────────────────────────────────────────────────────────────────────────────┐
│ Heicode Client / 小团队任务管理 SaaS 运行中 ¥12.30 │
├──────────────────────────────────────────────────────────────────────────────┤
│ │
│ Heicode:Agnet 已部署,我开始推进当前开发任务。 │
│ │
│ 当前 Agnet 子环节 │
│ - 需求和原型:已完成 │
│ - 后端开发:进行中 │
│ - 前端开发:等待接口确认 │
│ - 代码检查:等待代码变更 │
│ - 测试部署:等待构建 │
│ │
│ 正在调用的能力 │
│ - Git 代码仓库:已授权 │
│ - SK 工具:e2e-test / deploy-check / api-review │
│ │
│ 当前产物 │
│ [产品文档] [接口草案] [代码变更] [检查报告] │
│ │
│ ┌──────────────────────────────────────────────────────────────────────────┐ │
│ │ 继续实现通知功能,并在完成后给我一份测试结果。 │ │
│ └──────────────────────────────────────────────────────────────────────────┘ │
│ [发送] │
│ │
│ 辅助查看:打开 Manager 查看 Git/云资源绑定、Agnet 部署、用量和审计 │
└──────────────────────────────────────────────────────────────────────────────┘
```
### 设计意图
- 主执行反馈在客户端。
- 用户看到的是 Agnet 持续推进的子环节,而不是一次性执行结果。
- Agnet 可以在执行过程中调用已授权的 SK 工具。
- Manager 仍然是资源、部署、状态、余额和审计的辅助入口。
- 用户持续通过自然语言推进任务。
## 9. 客户端高危审批
高危动作必须在客户端确认。
```text
┌──────────────────────── Heicode 高危操作审批 ───────────────────────────────┐
│ │
│ 任务:小团队任务管理 SaaS │
│ 请求:部署到生产环境 │
│ 资源:aks-prod │
│ 风险:会更新对外服务 │
│ 凭证:将申请 15 分钟短期凭证 │
│ │
│ Heicode 建议:先完成测试环境验证,再批准生产部署。 │
│ │
│ [拒绝] [稍后提醒] [批准 15 分钟] │
└──────────────────────────────────────────────────────────────────────────────┘
```
## 10. 客户端交付结果
```text
┌──────────────────────────────────────────────────────────────────────────────┐
│ Heicode Client / 小团队任务管理 SaaS 已完成 │
├──────────────────────────────────────────────────────────────────────────────┤
│ │
│ Heicode:第一版已完成,Agnet 已整理交付物并完成目标环境部署。 │
│ │
│ 交付物 │
│ ┌────────────────────┐ ┌────────────────────┐ ┌────────────────────┐ │
│ │ 产品说明 │ │ 代码变更 │ │ 测试环境 / 生产环境 │ │
│ │ 查看 / 导出 │ │ 查看 diff │ │ 打开服务 / 看部署记录 │ │
│ └────────────────────┘ └────────────────────┘ └────────────────────┘ │
│ │
│ 质量结果 │
│ - 代码检查:通过 │
│ - 安全检查:1 个问题已修复 │
│ - 部署:测试环境成功,生产环境待你确认后可继续 │
│ │
│ [继续迭代] [发起维护任务] [查看 Agnet 部署信息] │
└──────────────────────────────────────────────────────────────────────────────┘
```
## 11. 移动端
移动端只做三件事:继续输入、查看状态、审批风险。
```text
┌────────────────────────┐
│ Heicode │
├────────────────────────┤
│ 你想继续做什么? │
│ ┌────────────────────┐ │
│ │ 输入想法或追加需求 │ │
│ └────────────────────┘ │
├────────────────────────┤
│ 当前任务 │
│ 小团队任务管理 SaaS │
│ 后端开发中 │
├────────────────────────┤
│ 待审批 │
│ 生产部署请求 │
│ [查看] │
├────────────────────────┤
│ 本次消耗 ¥12.30 │
└────────────────────────┘
```
## 不应该画成什么
这些不是 Heicode Manager 的主体验:
```text
┌────────────────────┐
│ 网页代码编辑器 │
│ 网页主开发对话 │
│ 网页编码推进 │
│ 任务推进 │
└────────────────────┘
```
正确表达应该是:
```text
客户端说目标
-> Heicode 生成任务
-> Manager 绑定 Git 和云资源
-> Manager 部署 Agnet
-> 客户端持续追加需求和修正方向
-> Agnet 执行需求、开发、测试、修复等子环节
-> Agnet 按权限调用 SK 工具
-> 客户端接收中间结果并审批高危动作
-> Agnet 完成交付整理和部署
-> Manager 提供状态、余额、下载和审计辅助
-> 客户端接收交付结果
```
## 页面关系图
```mermaid
flowchart TD
ClientHome["客户端输入想法"] --> ClientAsk["客户端追问缺口"]
ClientAsk --> Task["客户端任务卡"]
Task --> Manager["Manager 绑定资源 / 查看状态"]
Manager --> Deploy["Manager 部署 Agnet"]
Deploy --> Loop["Agnet 子环节循环"]
Loop --> ClientRun["客户端执行空间"]
Loop --> SK["SK 工具调用"]
SK --> ClientRun
Manager --> Audit["Manager 审计 / 余额 / 下载"]
ClientRun --> Approval["客户端高危审批"]
Approval --> Loop
ClientRun --> Result["客户端交付结果"]
Result --> ClientHome
```
@@ -0,0 +1,341 @@
# 12. 口述用户操作流程
## 使用场景
本文用于产品演示、培训、录屏旁白和销售讲解。语气以真实用户操作为主,不讲复杂技术实现;当需要解释安全、模型、密钥和 Agnet 平台时,只讲用户需要理解的边界。
## 一句话开场
> 我现在打开 Heicode,从一个产品想法开始,绑定我的代码和云资源,让平台生成 AI 开发团队,并在我确认权限和高危操作后推进开发、部署和维护。
## 完整口述流程
### 1. 打开平台
我先打开 Heicode 的平台地址:
```text
https://code.xinghanlab.com
```
这里是 Heicode 的浏览器控制台,也就是 Heicode Manager。后续我不会直接进入 CodeGW 后台,也不会直接进入密钥保管器后台。Manager 负责资源、部署、状态和审计等辅助动作;真正持续推进开发任务和高危审批,主要会回到客户端完成。
### 2. 登录账号
我输入账号和密码登录。
登录成功后,平台会识别我的用户身份,包括用户 ID、邮箱、角色和 `channelId`。这些信息用于后续模型额度、用量和审计归属。
用户不需要理解 `channelId` 的技术细节,只需要知道:我的模型余额、调用记录和任务消耗都能在 Heicode 里看到。
### 3. 进入首页
登录后,我首先进入首页。
首页会告诉我:
- 当前有没有正在运行的任务。
- 有没有失败任务。
- 有没有待我审批的高危操作。
- 我绑定了哪些资源。
- 模型余额和今日用量。
- 客户端是否已经下载。
如果我是第一次使用,首页会提示我下一步:输入产品想法、绑定资源或下载客户端。
### 4. 下载客户端
如果我还没有安装客户端,我会先进入“客户端下载”。
这里可以看到适合我系统的安装包,比如 macOS 或 Windows。下载安装后,我打开客户端。
客户端只需要登录:
```text
https://code.xinghanlab.com
```
客户端不会让我选择模型提供方,也不会出现 旧服务入口 或其他旧入口。客户端里能用什么模型,由 Heicode 提供。
### 5. 输入产品想法
回到 Heicode 或客户端,我输入我的产品想法。
比如我输入:
```text
我想做一个小团队任务管理 SaaS,
需要登录、项目、任务、评论、通知和后台管理,
希望部署到 Azure。
```
我不需要一开始就写完整 PRD。Heicode 会先理解我的目标,再帮我生成需求摘要、功能清单、原型描述和需要的资源。
### 6. 查看平台生成的任务草案
平台会根据我的输入生成一个任务草案。
我可以看到:
- 这个产品要解决什么问题。
- 第一版包含哪些功能。
- 推荐哪些页面和接口。
- 需要哪些代码仓库、文档、SK 和云资源。
- 建议使用哪些子 Agnet 角色。
- 哪些操作可能属于高危操作。
如果草案不准确,我可以直接修改。确认后,Heicode 会提示我补充这个任务需要的上下文。
### 7. 绑定代码仓库
在任务上下文卡片里,我先授权代码仓库。
如果我已有项目仓库,我可以授权 GitHub、GitLab、Gitea、Gitee 或自建 Git。平台会让我选择仓库、分支和允许访问的路径。
例如:
- 仓库:`repo-main`
- 分支:`main`
- 后端路径:`services/api/**`
- 前端路径:`web/**`
这里的重点不是把 Git token 贴在页面里,而是完成授权。真实凭证会进入密钥保管器,Heicode 只保存密钥引用。
### 8. 绑定项目文档和 SK
如果我有产品文档、原型描述或技能仓库,我继续绑定项目文档和 SK。
项目文档可以作为 Product Agnet 和 Architect Agnet 的上下文。SK 可以作为子 Agnet 可调用的技能来源。
绑定后,平台知道:
- 哪些文档可以读。
- 哪些技能可以使用。
- 哪些路径和版本有效。
### 9. 绑定云账号
接下来我绑定云资源,比如 Azure、AWS 或 GCP。
以 Azure 为例,我授权云账号后,平台会尽量自动发现我账号下的资源,比如:
- 虚拟机。
- 数据库。
- 存储。
- Kubernetes 或 AKS。
- 资源组。
我不应该手动填写一堆复杂资源 ID。正常体验应该是:平台发现资源,我选择哪些资源允许 Heicode 使用。
### 10. 选择允许使用的云资源
云账号授权后,我从自动发现列表里选择资源。
例如:
- 允许使用测试环境 AKS。
- 允许读取开发数据库。
- 不允许直接操作生产数据库。
- 生产部署必须审批。
保存后,这些云资源会变成 Resource Binding。
### 11. 确认密钥保管状态
资源绑定完成后,我能看到密钥保管状态。
我看到的是:
- 已托管。
- 无需密钥。
- 待补充。
- 已撤销。
- 异常。
我不会看到明文密钥。平台最多展示 `secret_ref` 这种引用,让我知道凭证由密钥保管器管理。
### 12. 生成子 Agnet 团队
资源准备好后,Heicode 会推荐一组子 Agnet 角色。
例如:
- Product Agnet:负责需求和验收标准。
- Architect Agnet:负责架构和技术方案。
- Frontend Agnet:负责页面和交互。
- Backend Agnet:负责 API、数据模型和服务逻辑。
- Reviewer Agnet:负责代码检查和安全审查。
- Ops Agnet:负责构建、部署和运行观察。
我可以使用推荐团队,也可以增减角色。
### 13. 分配资源权限
接下来我确认每个角色可以使用哪些资源。
例如:
Backend Agnet 可以:
- 读项目文档。
- 读写后端代码路径。
- 使用开发数据库。
Frontend Agnet 可以:
- 读项目文档。
- 读写前端代码路径。
Ops Agnet 可以:
- 部署测试环境。
- 请求生产部署审批。
这一步不要求我手写复杂策略。页面应该用角色卡片告诉我:这个角色能用什么、能做什么、是否需要审批。
### 14. 预览权限清单
如果我想看更详细的权限,我可以打开 manifest 预览。
这里会展示:
- 资源 ID。
- 资源类型。
- 允许动作。
- 限制条件。
- `secret_ref`。
manifest 不会显示 Git token、云 access key、SSH 私钥、数据库密码或 CodeGW key 原文。
### 15. 确认开始执行
在开始执行前,Heicode 会用一张确认卡展示本次任务摘要:
- 本次要做什么。
- 会启动哪些子 Agnet。
- 每个子 Agnet 使用哪些资源。
- 是否会访问密钥。
- 是否会部署云资源。
- 预计模型预算。
- 哪些动作需要审批。
确认无误后,我点击开始执行。真实部署动作仍由任务过程触发,并在高危场景下要求客户端审批。
### 16. 客户端审批高危操作
如果任务过程中出现高危操作,客户端会弹出审批。
例如:
```text
Ops Agnet 请求部署到生产环境。
目标资源:aks-prod
操作:cloud:deploy
风险等级:高
凭证:密钥保管器将派生 15 分钟短期凭证
```
这时我可以批准或拒绝。
如果我批准,平台会使用密钥保管器派生短期、最小权限凭证。子 Agnet 不会拿到长期密钥。
### 17. 在客户端持续推进开发
任务启动后,我不会停留在网页上编码,而是回到客户端继续推进。
这里的真实闭环是:
```text
我在客户端补充要求
-> Heicode 判断下一步要推进哪个子环节
-> Agnet 执行需求、开发、测试、修复或部署中的当前任务
-> Agnet 按权限调用已绑定的 SK 工具
-> 中间结果回到客户端
-> 我继续修正方向或批准高危动作
-> Agnet 继续推进直到交付和部署完成
```
### 18. 查看执行状态
任务启动后,我在 Heicode 里查看执行状态。
我能看到:
- Product Agnet 是否完成需求。
- Backend Agnet 是否正在写代码。
- Frontend Agnet 是否等待后端接口。
- Reviewer Agnet 是否发现问题。
- Ops Agnet 是否等待审批或部署中。
如果失败,页面应该显示失败原因,而不是只显示一个错误码。
### 19. 查看执行记录和模型用量
我可以在当前任务里打开执行记录。
这里能看到:
- 子 Agnet 运行日志。
- 模型调用日志。
- 消耗了多少额度。
- 哪个任务使用了哪个模型。
- 哪些资源被访问。
- 哪些高危操作被批准或拒绝。
模型和余额来自 CodeGW,但我不需要进入 CodeGW 后台。Heicode 会展示用户需要知道的信息。
### 19. 查看交付结果
当任务完成后,我查看交付结果。
结果可能包括:
- 产品文档。
- 原型描述。
- 代码变更。
- 测试结果。
- 部署结果。
- 访问地址。
- 审计记录。
如果部署到生产,我还可以看到部署时间、目标资源和审批记录。
### 20. 后续维护和升级
上线后,如果我想继续迭代,我可以直接输入新的需求。
例如:
```text
给任务管理 SaaS 增加企业微信通知,
并检查现有权限模型是否支持团队成员邀请。
```
Heicode 会复用之前的项目上下文、资源绑定和 Agnet 角色,生成新的开发任务。
## 简短演示口述版
> 我先登录 Heicode 的浏览器控制台。这里不是 CodeGW 后台,而是资源、部署、状态和审计的辅助入口。登录后,我会在客户端输入一个产品想法,比如做一个小团队任务管理 SaaS。平台会生成需求草案、推荐资源和子 Agnet 团队。接下来我绑定 Git 仓库、项目文档、SK 和云资源。真实密钥会进入密钥保管器,Heicode 只保存 secret_ref。然后我确认每个子 Agnet 的角色和权限,比如 Backend 能写后端路径,Ops 可以部署测试环境,但生产部署需要客户端审批。任务运行后,Agnet 会持续推进开发、测试和部署,并按权限调用需要的 SK 工具;我可以看到每个子 Agnet 的状态、日志、模型用量和审计记录。客户端只登录 code.xinghanlab.com,不选择模型提供方,模型由 Heicode 提供。整个流程的重点是:从一个想法开始,把产品、代码、云资源、模型、密钥和审计组织成一个完整的软件生命周期。
## 极简销售口述版
> Heicode 让用户从一个想法开始,接入自己的代码和云资源,由 Heicode 生成 AI 开发团队。平台负责资源授权、密钥保管、模型用量和审计;子 Agnet 负责开发、检查和部署。用户只在 Heicode 和客户端中操作,不需要进入 CodeGW 后台,也不会把长期密钥交给子 Agnet。
## 口述时必须强调
1. Heicode 是用户主入口。
2. 客户端只登录 Heicode。
3. CodeGW 是内部模型网关,不是普通用户后台。
4. 密钥进入密钥保管器,不进入 Git、Markdown 或日志。
5. 高危操作在客户端审批。
6. 子 Agnet 拿短期凭证,不拿长期密钥。
7. 资源绑定和权限分配要尽量自动化和角色化。
## 口述时不要说
1. 让用户去 CodeGW 后台配置模型。
2. 让用户去 OpenBao 控制台管理密钥。
3. 让客户端选择模型提供方。
4. 让用户手写复杂 manifest。
5. 把 tenant/project 当作当前产品主轴。
6. 把 Agnet 平台说成用户控制台。
@@ -0,0 +1,392 @@
# 13. 平台说明文档
## 文档目的
本文用于向用户、团队、客户和合作方说明 Heicode 平台是什么、由哪些部分组成、用户如何使用、各系统如何协作,以及平台如何处理模型、权限、密钥和审计。
产品说明文档重点回答“为什么需要 Heicode”;平台说明文档重点回答“Heicode 如何被使用、如何运转”。
## 一、平台概述
Heicode 是一款面向全流程智能开发的 SaaS 平台。用户注册登录后,可以从一个产品想法开始,接入自己的代码仓库、项目文档、SK 技能和云资源,由平台生成 AI 开发团队,完成需求、开发、检查、部署、观测和后续维护。
Heicode 平台由五个主要部分组成:
| 部分 | 用户理解 | 平台职责 |
|------|----------|----------|
| Heicode Manager | 浏览器辅助控制台 | 账号与安全、客户端下载、绑定资源、部署 Agnet、查看状态、余额和审计 |
| Heicode 客户端 | 本地主体验和审批入口 | 登录 Heicode、输入任务、继续开发、查看反馈、接收交付结果、审批高危操作 |
| Agnet 平台 | AI 开发团队执行层 | 在 AKS 上运行子 Agnet,回传状态、日志、事件和指标 |
| CodeGW | 模型与用量底座 | 提供模型调用、余额、额度、用量和调用日志 |
| 密钥保管器 | 凭证托管服务 | 保存长期密钥,按审批和权限生成短期访问能力 |
用户主要使用客户端和 Manager。CodeGW、密钥保管器和 Agnet 平台是底层能力,不作为普通用户日常操作后台。
## 二、平台适合谁
| 用户 | 使用场景 |
|------|----------|
| 独立开发者 | 从一个想法开始生成 MVP,并部署上线 |
| 创业团队 | 用较少人力完成产品迭代、测试和部署 |
| 企业创新团队 | 在已有代码仓库和云资源边界内使用 AI 开发 |
| 技术负责人 | 控制资源权限、模型消耗、安全审批和审计 |
| 运维负责人 | 管理部署、运行日志、资源访问和生产操作审批 |
## 三、平台核心能力
### 1. 想法输入
用户可以用自然语言描述产品想法或开发任务。平台会生成需求摘要、功能清单、原型描述、任务计划和需要的资源建议。
示例:
```text
我想做一个小团队任务管理 SaaS,
需要登录、项目、任务、评论、通知和后台管理,
希望部署到 Azure。
```
### 2. 资源绑定
用户可以把外部资源授权给 Heicode 使用。
资源类型包括:
- Git 仓库。
- SK 仓库或技能包。
- 项目文档。
- 云账号。
- 云资源,例如 VM、数据库、对象存储、Kubernetes。
资源绑定不是简单保存密钥,而是生成可分配、可撤销、可审计的资源授权基础。
### 3. AI 开发团队生成
平台根据任务目标推荐子 Agnet 角色。
常见角色:
- Product Agnet。
- Architect Agnet。
- Frontend Agnet。
- Backend Agnet。
- Reviewer Agnet。
- Ops Agnet。
每个子 Agnet 都有明确职责、可用资源、权限范围、模型配置和审计记录。
### 3.1 Agnet 持续执行闭环
Heicode 不是把任务分发给 Agnet 一次就结束,而是在继续开发中持续调用 Agnet 完成子环节。
完整闭环应表达为:
```text
客户端输入想法或追加需求
-> Manager 准备资源和部署条件
-> Heicode 调度 Agnet 执行当前子环节
-> Agnet 完成需求/开发/测试/修复/部署中的一步
-> Agnet 按权限调用 SK 工具
-> 中间结果和状态回传给客户端与 Manager
-> 用户继续补充、修正或审批
-> 最终由 Agnet 完成交付整理和部署
```
### 4. 权限分配
Heicode 使用 Resource Grant 描述“哪个角色能用哪个资源、能做什么、是否需要审批”。
示例:
```text
Backend Agnet
-> 可读项目文档
-> 可读写后端代码路径
-> 可访问开发数据库
-> 生产数据库访问需要审批
```
普通用户看到的是角色和权限摘要,高级用户可以预览 permission manifest。
### 5. 高危操作审批
高危操作必须在客户端审批。
高危操作包括:
- 生产部署。
- 云资源创建、删除、扩缩容。
- 数据库迁移或写入。
- 访问生产密钥。
- 大额模型预算消耗。
审批通过后,平台可以从密钥保管器派生短期、最小权限凭证给子 Agnet 使用。
### 6. 模型与余额
Heicode 展示用户可见的模型、余额、额度、用量和调用日志。这些能力来自 CodeGW,但普通用户不需要进入 CodeGW 后台。
用户能看到:
- 可用模型。
- 当前余额。
- 当前额度。
- 今日消耗。
- 调用日志。
- 失败日志。
用户看不到:
- 渠道管理。
- 模型供应商后台配置。
- 价格配置。
- CodeGW 管理员设置。
### 7. 日志与审计
Heicode 统一展示任务执行、模型调用、资源访问、高危审批和部署记录。
审计要能回答:
```text
谁
在什么时候
为了哪个任务
让哪个子 Agnet
使用了哪个资源
执行了什么操作
是否经过审批
消耗了多少模型额度
```
### 8. 部署和维护
Agnet 平台负责执行开发、检查、测试、修复和部署任务。Heicode 展示部署状态、失败原因、日志、指标和审计。
上线后,用户可以继续输入维护和升级需求,平台复用已有项目上下文、资源绑定和权限规则。
## 四、用户使用流程
完整流程:
```text
登录 Heicode
-> 下载并登录客户端
-> 客户端输入产品想法
-> 查看任务草案
-> 绑定 Git / 文档 / SK / 云资源
-> 生成子 Agnet 团队
-> 分配角色权限
-> 预览 permission manifest
-> 确认部署计划
-> Manager 部署 Agnet
-> 客户端持续推进任务
-> 客户端审批高危操作
-> Agnet 平台执行任务并调用 SK
-> Heicode 查看日志、用量、审计和交付结果
-> 后续维护和升级
```
## 五、平台组成说明
### 5.1 Heicode Manager
Heicode Manager 是浏览器辅助控制台。
用户在 Manager 中完成:
- 登录和查看首页。
- 绑定资源。
- 分配角色和权限。
- 创建或部署 Agnet 任务。
- 查看模型和余额。
- 查看日志与审计。
- 下载客户端。
Manager 不应该变成 CodeGW 后台,也不应该要求用户直接管理底层密钥系统,更不应该承担网页编码主体验。
### 5.2 Heicode 客户端
客户端是用户本地交互入口。
客户端负责:
- 登录 Heicode。
- 输入对话和任务。
- 持续追加需求和修正方向。
- 展示 Heicode 提供的模型。
- 审批高危操作。
- 查看执行反馈和交付结果。
客户端不负责:
- 选择模型提供方。
- 配置 CodeGW 渠道。
- 直接访问密钥保管器。
- 保存长期云密钥。
### 5.3 Agnet 平台
Agnet 平台是运行时执行层。
它负责:
- 在 AKS 上部署子 Agnet。
- 维护子 Agnet 状态。
- 执行开发、检查、测试、修复、部署和维护任务。
- 在执行过程中按权限调用 SK 工具。
- 回传日志、事件、指标和审计数据。
- 按 Resource Grant 和运行时身份访问资源。
普通用户不直接操作 Agnet 平台后台,而是在 Heicode 中查看 Agnet 状态和结果。
### 5.4 CodeGW
CodeGW 是内部模型网关和计费服务。
它负责:
- 模型调用。
- 用户或 Token 维度的额度。
- 余额。
- 调用日志。
- 模型可用性。
Heicode 通过服务端调用 CodeGW,并把普通用户需要的信息展示出来。CodeGW 后台不对普通用户开放。
### 5.5 密钥保管器
密钥保管器用于保存长期凭证。当前技术实现是 OpenBao。
它负责:
- 保存 Git token、SSH key、云 access key、数据库密码等长期凭证。
- 为高危操作派生短期凭证。
- 支持撤销、轮换、禁用和审计。
用户不会直接看到明文密钥。Heicode 服务端数据库只保存 `secret_ref`。
## 六、任务资源上下文说明
任务资源上下文的目标是让 Heicode 安全、可审计地使用用户授权的外部资源,同时不把用户拉进资源管理后台。
任务上下文准备流程:
```text
Heicode 判断当前任务需要哪些上下文
-> 用户完成授权或连接配置
-> Heicode 保存资源元数据
-> Secret Broker 把凭证写入密钥保管器
-> Heicode 保存 secret_ref
-> 用户确认本任务允许使用的资源范围
```
云资源授权后应尽量自动发现资源。用户绑定 Azure、AWS 或 GCP 后,平台应展示可选择的 VM、数据库、对象存储、Kubernetes 或资源组,减少手工填写。
## 七、权限和审批说明
权限确认围绕当前任务和角色进行。
用户不需要手写复杂策略,只需要确认:
- 这个角色能看什么。
- 这个角色能改什么。
- 这个角色能部署到哪里。
- 哪些操作需要审批。
- 授权什么时候过期。
高危操作审批只在客户端完成。审批结果会进入任务上下文和审计记录。
## 八、模型和余额说明
用户在 Heicode 中查看模型与余额。
展示内容:
- 可用模型。
- 余额。
- 额度。
- 调用量。
- 调用日志。
- 失败日志。
子 Agnet 的运行模型属于 Agnet 平台部署配置,不等同于 CodeGW 后台的模型供应商配置。
## 九、密钥保管说明
长期密钥不会进入:
- Git。
- Markdown。
- 前端响应。
- 普通日志。
- 子 Agnet 长期状态。
长期密钥进入密钥保管器。Heicode 保存 `secret_ref`。子 Agnet 使用资源时,只获取短期、最小权限、可审计凭证。
## 十、平台边界
用户需要使用:
- Heicode。
- Heicode 客户端。
用户不需要直接使用:
- CodeGW 后台。
- OpenBao 控制台。
- Agnet 平台后台。
用户不应该看到:
- 模型提供方选择。
- 旧服务入口。
- 明文密钥。
- OpenBao token。
- CodeGW 管理员配置。
## 十一、常见问题
### 1. 我需要会写代码吗?
不一定。Heicode 的目标是让用户从自然语言想法开始。但如果用户已有代码仓库,平台可以更好地基于现有项目继续开发。
### 2. 我必须绑定 Git 吗?
如果要让子 Agnet 修改代码,建议绑定 Git 仓库。只做产品文档或原型描述时,可以先不绑定代码仓库。
### 3. 我必须绑定云资源吗?
如果需要部署、测试环境或生产环境操作,就需要绑定云资源。只做需求、代码或本地检查时,可以先不绑定云资源。
### 4. 密钥安全吗?
平台设计上不把明文密钥保存到 Heicode 服务端数据库、Git、Markdown、日志或前端响应中。真实凭证进入密钥保管器,子 Agnet 只使用短期、最小权限凭证。
### 5. 客户端必须安装吗?
客户端用于本地对话和高危操作审批。普通浏览器流程可以完成很多管理操作,但高危操作审批建议通过客户端完成。
### 6. CodeGW 是什么?
CodeGW 是 Heicode 的模型网关和计费服务。普通用户不需要进入 CodeGW 后台,只在 Heicode 中查看模型、余额、额度和调用日志。
### 7. Agnet 是什么?
Agnet 是平台生成的 AI 开发角色。不同 Agnet 负责不同任务,例如产品、架构、前端、后端、审查和部署。
### 8. 生产部署怎么保证安全?
生产部署属于高危操作,需要客户端审批。审批通过后,平台按权限和 TTL 派生短期凭证,并记录完整审计。
### 9. 如果授权错了资源怎么办?
用户可以在任务资源上下文或权限确认卡中禁用、撤销或调整授权。撤销后,新的 permission manifest 不应再包含对应授权。
### 10. 平台会不会自动删除我的云资源?
不会默认执行高危操作。删除、生产部署、数据库写入等高危动作必须经过客户端审批。
## 十二、平台说明标准话术
> Heicode 是一个从想法到上线的软件生命周期平台。用户在 Heicode 中输入产品想法,绑定代码、文档、SK 和云资源。Heicode 生成子 Agnet 团队和权限清单,客户端负责高危审批,Agnet 平台负责执行开发和部署,CodeGW 提供模型和用量,密钥保管器保存长期凭证。用户只需要使用 Heicode 和客户端,不需要直接进入 CodeGW 后台或密钥保管器后台。
+48
View File
@@ -0,0 +1,48 @@
# Heicode 产品资料包
版本:v0.1
日期:2026-05-07
用途:对外介绍、官网设计、销售沟通、内部产品对齐、演示和用户上手。
## 资料包目录
| 文档 | 用途 |
|------|------|
| [`01-product-overview.md`](./01-product-overview.md) | 产品说明文档,回答 Heicode 是什么、解决什么问题、系统边界是什么 |
| [`02-homepage-design-brief.md`](./02-homepage-design-brief.md) | 产品首页设计文档,指导官网首页结构、文案和视觉重点 |
| [`03-user-journey-and-core-flow.md`](./03-user-journey-and-core-flow.md) | 用户旅程与核心流程,描述从想法到上线的完整路径 |
| [`04-platform-usage-guide.md`](./04-platform-usage-guide.md) | 登录平台和使用平台指南,面向真实用户上手 |
| [`05-ppt-copy.md`](./05-ppt-copy.md) | 产品 PPT 文案,面向路演、客户介绍和团队同步 |
| [`06-security-and-credential-management.md`](./06-security-and-credential-management.md) | 安全与凭证管理说明,解释密钥保管器、`secret_ref`、高危审批 |
| [`07-integration-boundaries.md`](./07-integration-boundaries.md) | CodeGW、OpenBao、Agnet 平台边界说明 |
| [`08-client-guide.md`](./08-client-guide.md) | Heicode 客户端说明,强调只登录 Heicode、只使用 Heicode 模型 |
| [`09-demo-script.md`](./09-demo-script.md) | 5 分钟演示脚本,用于产品演示和录屏 |
| [`10-frontend-detail-spec.md`](./10-frontend-detail-spec.md) | 前端细节说明,覆盖 Heicode 和客户端页面、组件、状态、文案规则 |
| [`11-product-prototype-wireframes.md`](./11-product-prototype-wireframes.md) | 产品原型图,说明 Heicode 客户端主体验、Manager 辅助控制台与 Agnet 执行闭环的边界 |
| [`12-narrated-user-operation-flow.md`](./12-narrated-user-operation-flow.md) | 口述用户操作流程,用于演示、培训、录屏旁白和销售讲解 |
| [`13-platform-description.md`](./13-platform-description.md) | 平台说明文档,解释平台组成、使用方式、系统边界和常见问题 |
## 统一表述
Heicode 是一款面向全流程智能开发的 SaaS Code 工具。用户注册登录后,只需要输入想法,平台逐步完成需求澄清、产品文档、原型描述、代码开发、代码检查、生产部署、后续维护和升级。
对外一句话:
> Heicode 让用户从一个想法开始,组织 AI 开发团队,接入自己的代码和云资源,完成从产品设计到生产部署的软件生命周期。
## 命名规则
- 用户侧叫 `Heicode`,不要叫 CodeGW 后台。
- `Heicode 客户端` 是用户主体验,用于对话、输入想法、推进开发任务、查看执行反馈、高危审批和接收交付结果。
- `Heicode Manager` 是浏览器里的辅助控制台,负责账号与安全、客户端下载、Git/云资源绑定、Agnet 部署、任务状态、余额与审计。
- 继续开发时,Heicode 会持续调用 Agnet 推进需求、开发、测试、修复和部署等子环节。
- Agnet 在执行过程中可以调用已授权的 SK 工具,最终完成交付与部署并把结果回传给客户端和 Manager。
- 用户侧叫 `密钥保管器`,技术实现可以说明是 OpenBao。
- 用户侧叫 `资源绑定`,不要叫 Git 来源。
- 客户端不出现 `模型提供方`、`旧服务入口`、`第三方路由入口`。
- CodeGW 是内部模型网关和计费服务,不作为普通用户产品入口。
- Agnet 平台是执行层,不替代 Heicode 的用户控制台。
## 当前产品边界
当前资料包以 `docs/heicode.md`、`docs/plan.md`、`docs/product-requirements.md` 为依据。旧 Agnet 草案、旧 M1-M5 计划和旧 UI 命名不作为产品主线。