fix: identify bundled official plugins in workspace

This commit is contained in:
2026-09-05 10:41:05 +08:00
parent 9ed9bf4a71
commit 6d102b2c6d
5 changed files with 121 additions and 4 deletions

View File

@@ -0,0 +1,69 @@
# Task: Fix missing official plugin acquire and download action
## Identity
- Task ID: 20260905-plugin-download-action-6c8e4a21
- Mode: Feature
- Branch: codex/20260905-plugin-download-action-6c8e4a21-plugin-download-action
- Worktree: D:\Datas\OthersProjects\.codex-worktrees\makelore\20260905-plugin-download-action-6c8e4a21
- Base commit: 9ed9bf4a715e300420e7a764a4cf576a6dfc4938
- Owner: codex-root
- Status: Ready for Integration
## Scope
- Reproduce and fix the unified `/plugins` projection for the code-owned bundled
`makelore.project-scaffold` Plugin when the Account Library has not acquired it.
- Keep the existing acquisition, device-package, project-enablement, and Agent
assignment authorities separate; cover the public Renderer seam with a focused
regression test.
- Limit product changes to the unified Plugin workspace projection/presentation and
its focused tests unless the reproduction proves the fault is owned elsewhere.
## Intent And Constraints
- A bundled Project Scaffold Plugin never has a device download/update/uninstall
action. Before acquisition it must offer the existing free Account Library acquire
action; after acquisition it may offer project enablement and Agent assignment.
- Do not add a visible local package/Skill installation picker, change server/Main
contracts, or collapse Account Library acquisition into device installation.
- Preserve the occupied client root worktree and its three adopted untracked task
records exactly. Work only in this isolated feature worktree.
- Concurrent Task Gate: Passed. Planning Gate: Passed after loading the current
integrated state, ADR-008, Plugin business rules/module boundaries, and the completed
root Project Scaffold integration task. No active product writer overlaps this scope.
## Outcome
- Added one shared projection predicate for code-owned bundled official Plugins and
used it on both the unified Plugin card and detail dialog. An unacquired bundled
Project Scaffold now reports `随应用提供` rather than the false
`未下载官方包` state.
- Preserved the existing authority boundary: the client does not synthesize a
Marketplace catalog entry or acquisition action. The existing `免费获取` action
still appears only when the server catalog contains the Plugin and the authenticated
Account Library snapshot confirms it is not acquired.
## Verification
- TDD red: the new public Renderer regression failed because both card and detail
reported the bundled Plugin as not downloaded.
- Focused green: `plugins-page` plus `plugin-workspace-model` = 35 passed.
- `corepack pnpm run typecheck`: passed.
- Scoped ESLint on the three changed product files and focused test: passed.
- `corepack pnpm run build:vite`: passed for Renderer, Main, Preload, and utility
targets with existing size/import warnings.
- Full unit run: 225 files / 1887 passed / 2 skipped, with one unrelated real-process
timing threshold failure (`2657ms < 2000ms`). The exact failed file then passed 6/6
in isolation; the separately skipped pressure suite passed 1/1.
- `git diff --check`: passed.
## Follow-ups
- Deploy the current Works Square server revision and migrate production through
`20260904_project_scaffold_0085`; until then the live catalog has no authority to
offer `免费获取` for `makelore.project-scaffold`.
## Promotion Candidates
- None recorded.

View File

@@ -12,7 +12,11 @@ import {
DialogTitle,
} from '@/components/ui/dialog';
import type { DataServiceInstanceState } from '../../../shared/data-service';
import type { PluginWorkspaceCommand, PluginWorkspaceItem } from './plugin-workspace-model';
import {
isBundledOfficialPlugin,
type PluginWorkspaceCommand,
type PluginWorkspaceItem,
} from './plugin-workspace-model';
interface DetailOperation {
capabilityId: string;
@@ -286,7 +290,7 @@ export function PluginDetails(props: PluginDetailsProps) {
<dd className="mt-1 text-sm font-semibold">
{props.item.source === 'local'
? (props.item.localEnabled ? '本机全局已启用' : '本机全局已停用')
: installation?.version ? `官方包 ${installation.version}` : props.item.delivery === 'system_included' ? '随应用提供' : '未下载官方包'}
: installation?.version ? `官方包 ${installation.version}` : isBundledOfficialPlugin(props.item) ? '随应用提供' : '未下载官方包'}
</dd>
</div>
<div className="rounded-xl bg-surface-subtle p-3">

View File

@@ -21,7 +21,7 @@ import type {
PluginWorkspaceProjection,
PluginWorkspaceState,
} from './plugin-workspace-model';
import { buildPluginWorkspaceProjection } from './plugin-workspace-model';
import { buildPluginWorkspaceProjection, isBundledOfficialPlugin } from './plugin-workspace-model';
import { resolvePluginWorkspaceSearch, serializePluginWorkspaceSearch } from './plugin-workspace-query';
const DELIVERY_TEXT = {
@@ -111,7 +111,7 @@ function PluginCard({
<p className="mt-2 line-clamp-3 text-pretty text-sm leading-6 text-muted-foreground">{item.summary}</p>
<dl className="mt-4 space-y-2 text-sm">
<div className="flex justify-between gap-4"><dt className="text-muted-foreground"></dt><dd className="text-right font-medium">{PROJECT_TEXT[item.projectState]}</dd></div>
<div className="flex justify-between gap-4"><dt className="text-muted-foreground"></dt><dd className="text-right font-medium">{item.source === 'local' ? (item.localEnabled ? '本机全局已启用' : '本机全局已停用') : item.official?.installation?.version ? `官方包 ${item.official.installation.version}` : item.delivery === 'system_included' ? '随应用提供' : '未下载官方包'}</dd></div>
<div className="flex justify-between gap-4"><dt className="text-muted-foreground"></dt><dd className="text-right font-medium">{item.source === 'local' ? (item.localEnabled ? '本机全局已启用' : '本机全局已停用') : item.official?.installation?.version ? `官方包 ${item.official.installation.version}` : isBundledOfficialPlugin(item) ? '随应用提供' : '未下载官方包'}</dd></div>
<div className="flex justify-between gap-4"><dt className="text-muted-foreground"></dt><dd className="text-right font-medium">{agentText(item)}</dd></div>
<div className="flex justify-between gap-4"><dt className="text-muted-foreground"></dt><dd className="text-right font-medium">{BILLING_TEXT[item.billing]}</dd></div>
</dl>

View File

@@ -104,6 +104,11 @@ export interface PluginWorkspaceItem {
local: DevicePackageRecordV1 | null;
}
export function isBundledOfficialPlugin(item: PluginWorkspaceItem): boolean {
return item.source === 'official'
&& (item.delivery === 'system_included' || isCodeOwnedOptionalBundledPluginId(item.pluginId));
}
export type PluginWorkspaceNotice =
| { kind: 'scope_fallback'; message: string }
| { kind: 'source_unavailable'; source: 'catalog' | 'library' | 'device' | 'project'; message: string }

View File

@@ -449,6 +449,45 @@ describe('PluginsView', () => {
expect(view.onFiltersChange).toHaveBeenCalledWith(expect.objectContaining({ search: 'web' }));
});
it('labels an unacquired code-owned bundled plugin as supplied by MakeLore instead of not downloaded', () => {
const scaffoldItem: PluginWorkspaceItem = {
...officialItem,
key: 'official:makelore.project-scaffold',
pluginId: 'makelore.project-scaffold',
title: 'MakeLore 项目脚手架',
delivery: 'not_acquired',
projectState: 'disabled',
assignedAgentIds: [],
assignedAgentNames: [],
commands: [],
official: {
catalog: null,
detail: null,
library: null,
installation: null,
project: {
...projectPlugin,
id: 'makelore.project-scaffold',
displayName: 'MakeLore 项目脚手架',
enabled: false,
state: 'disabled',
},
},
};
const view = props(scaffoldItem);
view.projection = {
items: [scaffoldItem],
selected: scaffoldItem,
counts: { all: 1, available: 0, mine: 0, enabled: 0, update: 0, unavailable: 0 },
notices: [],
};
render(<MemoryRouter><PluginsView {...view} /></MemoryRouter>);
expect(screen.getAllByText('随应用提供')).toHaveLength(2);
expect(screen.queryByText('未下载官方包')).not.toBeInTheDocument();
});
it('shows one dialog detail surface, confirms destructive commands, and embeds Data Service settings', () => {
const view = props(officialItem);
render(<MemoryRouter><PluginsView {...view} /></MemoryRouter>);