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 ;
}
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 不是凭据。
) : (
)}
} 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;