import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { ArrowLeft, Blocks, Check, Cpu, FileText, Files, FolderOpen, Plus, Puzzle, Trash2, Upload, X } from 'lucide-react'; import { toast } from 'sonner'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { ConfirmDialog } from '@/components/ui/confirm-dialog'; import { Sheet, SheetClose, SheetContent, SheetDescription, SheetTitle } from '@/components/ui/sheet'; import { AgentCreationDialog, type AgentCreationInput } from '@/components/coding/AgentCreationDialog'; import { PluginServices } from '@/components/plugins/PluginServices'; import { ProjectPublishAction } from '@/components/works/ProjectPublishAction'; import { buildCodingModelOptions, parseCodingModelKey, type CodingModelOption } from '@/lib/coding-model-options'; import { getCodingSkills } from '@/lib/coding-product-tools'; import { abortCodingConversation } from '@/lib/coding-conversations'; import { isCanonicalCodingProjectId } from '@/lib/coding-projects'; import { getSkillDisplayInfo } from '@/lib/skill-display'; import { getPluginServiceTab, type PluginServiceTab } from '@/lib/plugin-services'; import { getAgentAvatarSrc } from '@/lib/agent-avatars'; import { cn } from '@/lib/utils'; import { codingConversationStore } from '@/stores/coding-conversations'; import { codingPluginsStore, useCodingPluginsStore } from '@/stores/coding-plugins'; import { useCodingWorkspaceStore } from '@/stores/coding-workspace'; import { useProjectConfigStore } from '@/stores/project-config'; import { useProviderStore } from '@/stores/providers'; import type { CodingProjectAgent, CodingProjectConfig } from '@/types/coding-project'; import type { ProjectIdentityChoice } from '../../../shared/coding-project-contracts'; const EMPTY_FILES: string[] = []; type DrawerMode = 'models' | 'knowledge' | 'skills' | 'plugins' | null; type ProjectIdentityKind = ProjectIdentityChoice['kind']; type SkillStructureEntry = { path: string; type: 'file' | 'directory' }; type SkillInfo = { id: string; name: string; description: string; available: boolean; effective: boolean; location: string; content: string; entries: SkillStructureEntry[]; }; function createCustomAgent(index: number, modelKey: string): CodingProjectAgent { const now = new Date().toISOString(); const model = parseCodingModelKey(modelKey); return { id: `custom-agent-${Date.now()}-${index}`, avatarId: 'avatar-01', roleName: '项目智能体', name: '', builtIn: false, enabled: true, model: model ? { ...model, thinkingLevel: 'off' } : null, modelResolution: model ? 'resolved' : 'required', skillIds: [], responsibility: { mission: '', owns: [], boundaries: [], collaborators: [], principles: [] }, prompt: '', archivedAt: null, pinned: false, createdAt: now, updatedAt: now, }; } function ResourceCard({ id, icon, title, subtitle, onClick }: { id: string; icon: React.ReactNode; title: string; subtitle: string; onClick: () => void }) { return ; } function DrawerFrame({ title, desc, onClose, children, closeLabel = '关闭', closeIcon = 'close' }: { title: string; desc: string; onClose: () => void; children: React.ReactNode; closeLabel?: string; closeIcon?: 'close' | 'back' }) { const closeButton = ; return
{title}{desc}
{closeIcon === 'back' ? closeButton : {closeButton}}
{children}
; } function SkillDetail({ skill }: { skill: SkillInfo }) { return

技能目录结构

{skill.location}

{skill.entries.length > 0 ? skill.entries.map((entry) => { const depth = entry.path.split('/').length - 1; return
{entry.type === 'directory' ? : } {entry.type === 'directory' ? `${entry.path}/` : entry.path}
; }) :

暂无可展示的文件结构。

}

SKILL.md

{skill.content || 'SKILL.md 暂无内容。'}
; } function AgentCard({ agent, onOpen }: { agent: CodingProjectAgent; onOpen: () => void }) { return ; } function ModelCard({ model }: { model: CodingModelOption }) { return

{model.label}

已配置

{model.accountId}

; } function ModelList({ models }: { models: CodingModelOption[] }) { if (models.length === 0) { return
当前没有可用模型,请先在模型设置中完成配置。智能体的默认模型在智能体配置中设置。
; } return

此处仅展示已经配置的模型。每个智能体可单独设置新对话使用的默认模型。

{models.map((model) => )}
; } export function ProjectConfiguration() { const activeProject = useCodingWorkspaceStore((state) => state.activeProject); const conversations = useCodingWorkspaceStore((state) => state.conversations); const removeProject = useCodingWorkspaceStore((state) => state.removeProject); const reloadWorkspace = useCodingWorkspaceStore((state) => state.load); const stored = useProjectConfigStore((state) => activeProject ? state.configsByProjectId[activeProject.id] : undefined); const knowledge = useProjectConfigStore((state) => activeProject ? state.knowledgeByProjectId[activeProject.id] ?? EMPTY_FILES : EMPTY_FILES); const load = useProjectConfigStore((state) => state.load); const save = useProjectConfigStore((state) => state.save); const resolveIdentity = useProjectConfigStore((state) => state.resolveIdentity); const makeIndependentCopy = useProjectConfigStore((state) => state.makeIndependentCopy); const uploadKnowledge = useProjectConfigStore((state) => state.uploadKnowledge); const removeProjectConfig = useProjectConfigStore((state) => state.remove); const providerAccounts = useProviderStore((state) => state.accounts); const providerVendors = useProviderStore((state) => state.vendors); const refreshProviderSnapshot = useProviderStore((state) => state.refreshProviderSnapshot); const pluginProjection = useCodingPluginsStore((state) => state.projection); const pluginLoadState = useCodingPluginsStore((state) => state.loadState); const [draft, setDraft] = useState(null); const [drawerMode, setDrawerMode] = useState(null); const [pluginServiceTab, setPluginServiceTab] = useState('project'); const [activeSkillId, setActiveSkillId] = useState(null); const [activeAgentId, setActiveAgentId] = useState(null); const [skills, setSkills] = useState([]); const [saving, setSaving] = useState(false); const [identityKind, setIdentityKind] = useState('create'); const [identityProjectId, setIdentityProjectId] = useState(''); const [identitySaving, setIdentitySaving] = useState(false); const [identityError, setIdentityError] = useState(null); const [independentCopyDialogOpen, setIndependentCopyDialogOpen] = useState(false); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [partnerDialogOpen, setPartnerDialogOpen] = useState(false); const [archiveAgentId, setArchiveAgentId] = useState(null); const knowledgeInputRef = useRef(null); const navigate = useNavigate(); const location = useLocation(); const loadSkills = useCallback(() => { void getCodingSkills() .then((result) => setSkills(result.skills.map((skill) => ({ id: skill.id, ...getSkillDisplayInfo(skill.id), available: skill.available, effective: skill.effective, location: skill.location ?? '', content: skill.content ?? '', entries: skill.entries ?? [{ path: 'SKILL.md', type: 'file' }], })))) .catch(() => setSkills([])); }, []); useEffect(() => { if (activeProject) void load(activeProject.id); }, [activeProject, load]); useEffect(() => { if (stored) setDraft(structuredClone(stored)); }, [stored]); useEffect(() => { setIdentityKind('create'); setIdentityProjectId(''); setIdentityError(null); }, [activeProject?.id, draft?.projectId]); useEffect(() => { const requestedAgentId = new URLSearchParams(location.search).get('agent'); if (!requestedAgentId || !draft?.agents.some((agent) => agent.id === requestedAgentId)) return; setActiveAgentId(requestedAgentId); setPartnerDialogOpen(true); }, [draft, location.search]); useEffect(() => { const params = new URLSearchParams(location.search); if (params.get('resource') !== 'plugins') return; setPluginServiceTab(getPluginServiceTab(params.get('pluginTab'))); setDrawerMode('plugins'); }, [location.search]); useEffect(() => { loadSkills(); }, [loadSkills]); useEffect(() => { if (drawerMode === 'skills') loadSkills(); }, [drawerMode, loadSkills]); useEffect(() => { void refreshProviderSnapshot(); }, [refreshProviderSnapshot]); useEffect(() => { if (activeProject) void codingPluginsStore.getState().load(activeProject.id).catch(() => undefined); }, [activeProject]); const modelOptions = useMemo( () => buildCodingModelOptions(providerAccounts, providerVendors), [providerAccounts, providerVendors], ); const activeAgent = draft?.agents.find((agent) => agent.id === activeAgentId) ?? null; const activeSkill = skills.find((skill) => skill.id === activeSkillId) ?? null; const currentPluginProjection = pluginProjection?.project.localProjectId === activeProject?.id ? pluginProjection : null; const availablePluginCount = currentPluginProjection?.items.length ?? 0; const enabledPluginCount = currentPluginProjection?.items.filter(({ enabled }) => enabled).length ?? 0; const pluginSummary = !currentPluginProjection ? pluginLoadState === 'error' ? '项目插件暂不可用' : '正在读取项目插件' : `${enabledPluginCount} 个已启用 · ${availablePluginCount} 个可用`; const playSaveConfirmation = () => undefined; const handleBack = () => navigate(-1); const handlePluginServiceTabChange = (tab: PluginServiceTab) => { setPluginServiceTab(tab); const params = new URLSearchParams(location.search); params.set('resource', 'plugins'); params.set('pluginTab', tab); navigate({ pathname: '/project-config', search: `?${params.toString()}` }, { replace: true }); }; const openPluginServices = () => { setPluginServiceTab('project'); setDrawerMode('plugins'); const params = new URLSearchParams(location.search); params.set('resource', 'plugins'); params.set('pluginTab', 'project'); navigate({ pathname: '/project-config', search: `?${params.toString()}` }, { replace: true }); }; const closePluginServices = () => { setDrawerMode(null); const params = new URLSearchParams(location.search); params.delete('resource'); params.delete('pluginTab'); const search = params.toString(); navigate({ pathname: '/project-config', search: search ? `?${search}` : '' }, { replace: true }); }; if (!activeProject) return

一念成光,万物可创。

; if (!draft) return
正在读取项目配置…
; const updateAgent = (next: CodingProjectAgent) => setDraft((current) => current ? { ...current, agents: current.agents.map((item) => item.id === next.id ? { ...next, updatedAt: new Date().toISOString() } : item) } : current); const handlePartnerDialogOpenChange = (open: boolean) => { setPartnerDialogOpen(open); if (!open) setActiveAgentId(null); }; const handleCreatePartner = async (input: AgentCreationInput) => { const selectedModel = parseCodingModelKey(input.model); if (!selectedModel) throw new Error('默认模型无效'); const agent = { ...createCustomAgent(draft.agents.length + 1, input.model), name: input.name, avatarId: input.avatarId, avatarDataUrl: input.avatarDataUrl, model: { ...selectedModel, thinkingLevel: 'off' as const }, modelResolution: 'resolved' as const, prompt: input.prompt, skillIds: input.skillIds, responsibility: { mission: input.responsibility, owns: [], boundaries: [], collaborators: [], principles: [] }, }; setDraft((current) => current ? { ...current, agents: [...current.agents, agent] } : current); }; const handleUpdatePartner = async (input: AgentCreationInput) => { if (!activeAgentId) return; const selectedModel = parseCodingModelKey(input.model); if (!selectedModel) throw new Error('默认模型无效'); setDraft((current) => current ? { ...current, agents: current.agents.map((agent) => agent.id === activeAgentId ? { ...agent, name: input.name, avatarId: input.avatarId, avatarDataUrl: input.avatarDataUrl, model: { ...selectedModel, thinkingLevel: agent.model?.thinkingLevel ?? 'off', }, modelResolution: 'resolved', prompt: input.prompt, skillIds: input.skillIds, responsibility: { ...agent.responsibility, mission: input.responsibility }, updatedAt: new Date().toISOString(), } : agent), } : current); }; const submit = async () => { const names = draft.agents.map((agent) => agent.name.trim()).filter(Boolean); const invalid = draft.agents.some((agent) => ( !agent.name.trim() || !agent.avatarId.trim() || !agent.model || !agent.responsibility.mission.trim() )) || new Set(names).size !== draft.agents.length; if (invalid) { toast.error('请补全智能体名称、头像、默认模型和职责说明;名称需在当前项目内唯一。'); return; } setSaving(true); try { await save(activeProject.id, { ...draft, initialized: true }); await reloadWorkspace(); playSaveConfirmation(); toast.success(draft.initialized ? '项目配置已更新' : '项目初始化完成'); } catch (error) { toast.error(error instanceof Error ? error.message : String(error)); } finally { setSaving(false); } }; const handleResolveIdentity = async () => { const boundProjectId = identityProjectId.trim(); if (identityKind === 'bind' && !isCanonicalCodingProjectId(boundProjectId)) { setIdentityError('请输入有效的项目 ID(小写 UUID)。'); return; } setIdentitySaving(true); setIdentityError(null); try { await resolveIdentity(activeProject.id, identityKind === 'create' ? { kind: 'create' } : { kind: 'bind', projectId: boundProjectId }); await reloadWorkspace(); toast.success('项目身份已设置,之后不可通过普通保存修改。'); } catch (error) { const message = error instanceof Error ? error.message : String(error); setIdentityError(message); toast.error(message); } finally { setIdentitySaving(false); } }; const handleIndependentCopy = async () => { setIdentitySaving(true); try { await makeIndependentCopy(activeProject.id); await reloadWorkspace(); setIndependentCopyDialogOpen(false); toast.success('已设为独立副本,当前文件夹已获得新的项目 ID。'); } finally { setIdentitySaving(false); } }; const handleKnowledge = async (file?: File) => { if (!file) return; try { await uploadKnowledge(activeProject.id, file); toast.success(`已上传到 knowledge/${file.name}`); } catch (error) { toast.error(error instanceof Error ? error.message : String(error)); } }; const handleDeleteProject = async () => { await removeProject(activeProject.id); removeProjectConfig(activeProject.id); setDeleteDialogOpen(false); toast.success(`已移除项目 ${activeProject.name}`); navigate('/project-config'); }; const handleArchiveAgent = async () => { if (!archiveAgentId) return; const summaries = codingConversationStore.getState().summariesByConversationId; const runningConversationIds = conversations .filter((conversation) => ( conversation.agentId === archiveAgentId && summaries[conversation.id]?.runStatus !== 'idle' )) .map((conversation) => conversation.id); await Promise.allSettled(runningConversationIds.map(abortCodingConversation)); const archivedAt = new Date().toISOString(); setDraft((current) => current ? { ...current, agents: current.agents.map((agent) => agent.id === archiveAgentId ? { ...agent, archivedAt, updatedAt: archivedAt } : agent), } : current); setArchiveAgentId(null); setPartnerDialogOpen(false); setActiveAgentId(null); }; return

项目配置

{draft.projectId ? (

项目身份

当前项目 ID

{draft.projectId}

同一账号绑定相同项目 ID 会共享开发数据;项目 ID 不是凭据。

) : (
{ event.preventDefault(); void handleResolveIdentity(); }} >

为旧项目设置身份

这是一个没有项目 ID 的旧项目。此操作只会为当前文件夹设置一次身份;项目文件和云端数据都不会被复制。

选择项目 ID
{identityKind === 'bind' ? (
{ setIdentityProjectId(event.target.value); setIdentityError(null); }} placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" autoComplete="off" spellCheck={false} className="w-full rounded-md border border-foreground/15 bg-white px-3 py-2 font-mono text-sm font-medium outline-none focus:ring-2 focus:ring-brand/20" disabled={identitySaving} />

同一账号绑定相同项目 ID 会共享开发数据;项目 ID 不是凭据。

) : null} {identityError ?

{identityError}

: null}
)}
} title="可用模型" subtitle={`${modelOptions.length} 个可用模型`} onClick={() => setDrawerMode('models')} />} title="可用技能" subtitle={`${skills.length} 项可用技能`} onClick={() => { setActiveSkillId(null); setDrawerMode('skills'); }} />} title="知识文件" subtitle={`${knowledge.length} 个知识文件`} onClick={() => setDrawerMode('knowledge')} />} title="插件服务" subtitle={pluginSummary} onClick={openPluginServices} />

项目智能体

智能体配置仅属于当前项目;每个智能体可以拥有多条独立对话。

{draft.agents.filter((agent) => !agent.archivedAt).map((agent) => { setActiveAgentId(agent.id); setPartnerDialogOpen(true); }} />)}{draft.agents.every((agent) => agent.archivedAt) ?
当前项目还没有智能体,点击右上角的 + 创建。
: null}
{draft.agents.some((agent) => agent.archivedAt) ?

已归档

{draft.agents.filter((agent) => agent.archivedAt).map((agent) => )}
: null}
{draft.projectType === 'mini_game' || draft.projectType === 'mini_program' ? ( ) : null}
agent.id !== activeAgentId).map((agent) => agent.name)} onCreate={handleCreatePartner} onUpdate={handleUpdatePartner} onArchive={activeAgent && !activeAgent.builtIn ? () => setArchiveAgentId(activeAgent.id) : undefined} title={activeAgent ? `${activeAgent.name || '未命名智能体'} · 编辑智能体` : '创建项目智能体'} submitLabel={activeAgent ? '保存智能体' : '创建智能体'} /> { if (!open) { if (drawerMode === 'plugins') closePluginServices(); else setDrawerMode(null); setActiveSkillId(null); } }}> {drawerMode === 'models' ? setDrawerMode(null)}> : null} {drawerMode === 'knowledge' ? setDrawerMode(null)}> { void handleKnowledge(event.target.files?.[0]); event.currentTarget.value = ''; }} />
{knowledge.length ? knowledge.map((file) =>
{file}
) :
当前还没有知识文件。
}
: null} {drawerMode === 'skills' && activeSkill ? setActiveSkillId(null)} closeLabel="返回技能列表" closeIcon="back"> : null} {drawerMode === 'skills' && !activeSkill ? setDrawerMode(null)}>
{skills.length > 0 ? skills.map((skill) => ) :
当前没有可查看的技能。
}
: null} {drawerMode === 'plugins' ? : null}
agent.id === archiveAgentId)?.name || '未命名智能体'}”?`} message="归档后将停止提供新的使用入口,但会保留智能体配置和全部对话记录;你可以之后在智能体列表底部恢复。" confirmLabel="确认归档" cancelLabel="取消" onCancel={() => setArchiveAgentId(null)} onConfirm={handleArchiveAgent} onError={(error) => toast.error(error instanceof Error ? error.message : String(error))} /> setDeleteDialogOpen(false)} onConfirm={handleDeleteProject} onError={(error) => toast.error(error instanceof Error ? error.message : String(error))} /> setIndependentCopyDialogOpen(false)} onConfirm={handleIndependentCopy} onError={(error) => toast.error(error instanceof Error ? error.message : String(error))} />
; } export default ProjectConfiguration;