feat(plugins): merge Skill management into plugins

This commit is contained in:
inman committed 2026-09-07 18:36:33 +08:00
1 parent bb8a19eecf
commit ab9f889233
49 files changed
+461 -1218

No files matched your search

+87 -12
View File
@@ -182,6 +182,8 @@ function props(selected: PluginWorkspaceItem | null = null) {
sourceLoading: [],
dataService: null,
dataServicePending: {},
activeTab: 'mine' as const,
onTabChange: vi.fn(),
onSelect: vi.fn(),
onCommand: vi.fn(),
isCommandPending: vi.fn().mockReturnValue(false),
@@ -241,10 +243,15 @@ function preparePluginsRouteState() {
describe('PluginsView', () => {
it('renders a direct Skill-like list in the project drawer without filters or refresh', () => {
render(<MemoryRouter><PluginsView {...props()} embedded /></MemoryRouter>);
const view = props();
render(<MemoryRouter><PluginsView {...view} embedded /></MemoryRouter>);
expect(screen.getByTestId('plugins-page')).toBeVisible();
expect(screen.queryByRole('heading', { name: '插件', level: 1 })).not.toBeInTheDocument();
expect(screen.getByRole('tab', { name: /可选/ })).toHaveAttribute('aria-selected', 'false');
expect(screen.getByRole('tab', { name: /已安装/ })).toHaveAttribute('aria-selected', 'true');
fireEvent.mouseDown(screen.getByRole('tab', { name: /可选/ }), { button: 0, ctrlKey: false });
expect(view.onTabChange).toHaveBeenCalledWith('available');
expect(screen.getByTestId('plugin-list')).toHaveClass('space-y-3');
expect(screen.queryByRole('button', { name: /刷新/ })).not.toBeInTheDocument();
expect(screen.queryByRole('searchbox')).not.toBeInTheDocument();
@@ -252,6 +259,7 @@ describe('PluginsView', () => {
expect(screen.getByRole('button', { name: '查看开发数据服务详情' })).toBeVisible();
expect(screen.getByRole('button', { name: '开发数据服务:已添加到项目' })).toBeDisabled();
expect(screen.getByRole('button', { name: 'Pi Web Search:已安装' })).toBeDisabled();
expect(screen.getByText('Skill + 插件')).toBeVisible();
});
it('keeps the production URL and fallback notice aligned without exposing query controls', async () => {
@@ -263,7 +271,7 @@ describe('PluginsView', () => {
);
await waitFor(() => expect(screen.getByTestId('plugins-location')).toHaveTextContent(
'/project-config/plugins?scope=all&source=all&state=all',
'/project-config/plugins?scope=all&source=all&state=available',
));
expect(screen.queryByLabelText('范围')).not.toBeInTheDocument();
expect(screen.getByText('当前没有打开的项目,已显示全部插件。')).toBeVisible();
@@ -278,25 +286,25 @@ describe('PluginsView', () => {
act(() => codingWorkspaceStore.setState({ activeProjectId: activeProject.id, activeProject }));
fireEvent.click(screen.getByRole('button', { name: '打开裸插件地址' }));
await waitFor(() => expect(screen.getByTestId('plugins-location')).toHaveTextContent(
'/project-config/plugins?scope=project&source=all&state=all',
'/project-config/plugins?scope=all&source=all&state=available',
));
expect(screen.queryByLabelText('范围')).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '打开非法插件地址' }));
await waitFor(() => expect(screen.getByTestId('plugins-location')).toHaveTextContent(
'/project-config/plugins?scope=project&source=all&state=all',
'/project-config/plugins?scope=all&source=all&state=available',
));
expect(screen.queryByLabelText('范围')).not.toBeInTheDocument();
act(() => codingWorkspaceStore.setState({ activeProjectId: null, activeProject: null }));
await waitFor(() => expect(screen.getByTestId('plugins-location')).toHaveTextContent(
'/project-config/plugins?scope=all&source=all&state=all',
'/project-config/plugins?scope=all&source=all&state=available',
));
expect(screen.queryByLabelText('范围')).not.toBeInTheDocument();
expect(screen.getByText('当前没有打开的项目,已显示全部插件。')).toBeVisible();
expect(screen.queryByText('当前没有打开的项目,已显示全部插件。')).not.toBeInTheDocument();
});
it('keeps available rows visible when individual sources fail to load', async () => {
it('keeps installed rows visible when individual sources fail to load', async () => {
const activeProject = {
id: 'project-a',
name: 'Project A',
@@ -344,7 +352,7 @@ describe('PluginsView', () => {
vi.spyOn(devicePackageStore.getState(), 'load').mockResolvedValue(undefined);
vi.spyOn(codingPluginsStore.getState(), 'load').mockResolvedValue(undefined);
render(<MemoryRouter initialEntries={['/project-config/plugins?scope=project&source=all&state=all']}><Plugins /></MemoryRouter>);
render(<MemoryRouter initialEntries={['/project-config/plugins?scope=project&source=all&state=mine']}><Plugins /></MemoryRouter>);
for (const name of ['开发数据服务', 'Pi Web Search', 'makelore.retained']) {
expect(await screen.findByRole('heading', { name })).toBeVisible();
@@ -353,6 +361,73 @@ describe('PluginsView', () => {
expect(screen.queryByText('缓存')).not.toBeInTheDocument();
});
it('clears legacy source filters when switching to installed so local Skills remain visible', async () => {
preparePluginsRouteState();
pluginMarketplaceStore.setState({
catalog: {
items: [{
pluginId: 'makelore.notes',
title: 'Notes',
summary: 'Write project notes',
category: 'productivity',
tags: ['notes'],
providerDisplayName: 'MakeLore',
runtimeKind: 'skill_only',
runtimeStatus: 'enabled',
acquisition: 'free',
usageBilling: 'included',
includedOperationCount: 1,
meteredOperationCount: 0,
stableVersion: '1.0.0',
betaVersion: null,
}],
nextCursor: null,
total: 1,
catalogGeneration: 1,
etag: 'catalog-1',
pricingVersionId: null,
stale: false,
fetchedAt: 1,
},
});
devicePackageStore.setState({
index: {
schemaVersion: 1,
generation: 2,
packages: [{
...localItem.local!,
packageId: 'local-writing',
displayName: 'Local Writing',
source: {
kind: 'file',
requested: '/Users/example/.codex/skills/local-writing/SKILL.md',
resolved: '/Users/example/.codex/skills/local-writing/SKILL.md',
},
kind: 'skill-only',
skillEntries: [{ id: 'local-writing', entryPath: 'SKILL.md' }],
extensionEntries: [],
confirmedExecutableCode: false,
}],
},
});
render(
<MemoryRouter initialEntries={[
'/project-config/plugins?scope=all&source=official&state=available',
]}>
<PluginsRouteHarness />
</MemoryRouter>,
);
expect(await screen.findByRole('heading', { name: 'Notes' })).toBeVisible();
fireEvent.mouseDown(screen.getByRole('tab', { name: /已安装/ }), { button: 0, ctrlKey: false });
await waitFor(() => expect(screen.getByTestId('plugins-location')).toHaveTextContent(
'/project-config/plugins?scope=all&source=all&state=mine',
));
expect(await screen.findByRole('heading', { name: 'Local Writing' })).toBeVisible();
expect(screen.getByText('Skill')).toBeVisible();
});
it('attempts a rejected official detail load once per open and retries only after close and reopen', async () => {
preparePluginsRouteState();
pluginMarketplaceStore.setState({
@@ -393,7 +468,7 @@ describe('PluginsView', () => {
render(
<MemoryRouter initialEntries={[
'/project-config/plugins?scope=all&source=all&state=all&plugin=official%3Amakelore.notes',
'/project-config/plugins?scope=all&source=all&state=available&plugin=official%3Amakelore.notes',
]}>
<Plugins />
</MemoryRouter>,
@@ -431,7 +506,7 @@ describe('PluginsView', () => {
render(<MemoryRouter><PluginsView {...view} /></MemoryRouter>);
expect(screen.getByRole('heading', { name: '插件' })).toBeVisible();
expect(screen.getByText('为你的智能体添加插件,拓展更多能力。')).toBeVisible();
expect(screen.getByText('插件与 Skill 统一管理;云端能力在这里下发,本机安装的 Skill 也会集中显示。')).toBeVisible();
expect(screen.getByText('开发数据服务')).toBeVisible();
expect(screen.getByText('Pi Web Search')).toBeVisible();
expect(screen.getByRole('button', { name: '查看开发数据服务详情' })).toBeVisible();
@@ -471,7 +546,7 @@ describe('PluginsView', () => {
view.projection = {
items: [scaffoldItem],
selected: scaffoldItem,
counts: { all: 1, available: 0, mine: 0, enabled: 0, update: 0, unavailable: 0 },
counts: { all: 1, available: 1, mine: 0, enabled: 0, update: 0, unavailable: 0 },
notices: [],
};
@@ -531,7 +606,7 @@ describe('PluginsView', () => {
render(<MemoryRouter><PluginsView {...props(scriptSkill)} /></MemoryRouter>);
const dialog = screen.getByRole('dialog', { name: 'MakeLore 项目初始化' });
expect(dialog).toHaveTextContent('为智能体提供可在本机使用的扩展能力。');
expect(dialog).toHaveTextContent('这是用户自行安装在本机的 Skill,已统一收纳在插件模块中。');
expect(dialog).not.toHaveTextContent('Skill 脚本');
expect(dialog).not.toHaveTextContent('Pi extension');
expect(dialog).not.toHaveTextContent('完整桌面权限');