feat(coding): add model tools and device packages

This commit is contained in:
2026-09-02 18:35:26 +08:00
parent 841273b433
commit 664b8823a0
60 changed files with 3847 additions and 2068 deletions

View File

@@ -629,6 +629,37 @@ const ToolDetails = memo(function ToolDetails({ details }: { details: KnownToolD
</div>
);
}
if (details.schema === 'makelore-model-tool.v1') {
return (
<div className="rounded-xl bg-foreground/[0.035] p-3 text-xs">
<p className="font-semibold">模型联网搜索 · {details.modelId}</p>
{details.status === 'succeeded' ? (
<p className="mt-1 text-muted-foreground">
{details.sources.length > 0
? `已返回 ${details.sources.length} 个来源`
: '提供方未返回结构化来源'}
</p>
) : (
<p className="mt-1 text-destructive">{details.error.code} · {details.error.message}</p>
)}
</div>
);
}
if (details.schema === 'makelore-device-package.v1') {
const subject = details.preview?.displayName
?? details.index?.packages.at(-1)?.displayName
?? '本机包';
return (
<div className="rounded-xl bg-foreground/[0.035] p-3 text-xs">
<p className="font-semibold">本机插件 · {subject}</p>
<p className="mt-1 text-muted-foreground">
{details.operation === 'prepare'
? `等待确认${details.preview?.includesExecutableCode ? ' · 包含可执行扩展' : ''}`
: `操作完成 · 共 ${details.index?.packages.length ?? 0} 个本机包`}
</p>
</div>
);
}
return <SubagentGraph details={details} />;
});
@@ -832,6 +863,16 @@ function toolDetailsProgress(details: KnownToolDetails | undefined): string | nu
? `${details.operation} · 成功`
: `${details.operation} · ${details.error ?? '请求失败'}`;
}
if (details.schema === 'makelore-model-tool.v1') {
return details.status === 'succeeded'
? `联网搜索 · ${details.sources.length} 个来源`
: `联网搜索 · ${details.error.code}`;
}
if (details.schema === 'makelore-device-package.v1') {
return details.operation === 'prepare'
? `本机插件 · 等待确认安装 ${details.preview?.displayName ?? ''}`.trim()
: `本机插件 · ${details.operation}`;
}
const task = details.tasks.find((candidate) => (
candidate.status === 'running' || candidate.status === 'queued'
)) ?? details.tasks.at(-1);

View File

@@ -5,7 +5,9 @@ import { toast } from 'sonner';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import type { MarketplaceInstallation, MarketplaceLibrarySnapshot } from '@/lib/plugin-marketplace';
import type { DevicePackageIndexV1 } from '@/lib/device-packages';
import { pluginMarketplaceStore, usePluginMarketplaceStore } from '@/stores/plugin-marketplace';
import { devicePackageStore, useDevicePackageStore } from '@/stores/device-packages';
import { useAuthStore } from '@/stores/auth';
import { isCodeOwnedOptionalBundledPluginId } from '../../../shared/coding-plugins';
@@ -50,19 +52,29 @@ export type MyPluginsViewProps = {
onUninstall(pluginId: string): void | Promise<void>;
onRemove(pluginId: string): void | Promise<void>;
onReacquire(pluginId: string): void | Promise<void>;
deviceIndex?: DevicePackageIndexV1 | null;
deviceState?: 'idle' | 'loading' | 'ready' | 'error';
deviceError?: string | null;
devicePending?: Record<string, true>;
onDeviceRefresh?(): void | Promise<void>;
onDeviceSetEnabled?(packageId: string, enabled: boolean): void | Promise<void>;
onDeviceUninstall?(packageId: string): void | Promise<void>;
};
export function MyPluginsView(props: MyPluginsViewProps) {
const visibleError = actionErrorText(props.error);
const devicePackages = props.deviceIndex?.packages ?? [];
const devicePending = props.devicePending ?? {};
return (
<main data-testid="my-plugins-page" className="mx-auto w-full max-w-7xl text-foreground">
<header className="flex flex-wrap items-start justify-between gap-4"><div><h1 className="text-balance text-3xl font-semibold tracking-[-0.03em]">我的插件</h1><p className="mt-2 max-w-2xl text-pretty text-sm text-muted-foreground">官方插件随 MakeLore 提供;其他插件的账号获取与本机下载分开管理。任何插件都不会自动启用到项目或分配给伙伴。</p></div><Button type="button" variant="outline" className="min-h-10" onClick={() => void props.onRefresh()}><RefreshCw className="mr-2 h-4 w-4" />刷新</Button></header>
<header className="flex flex-wrap items-start justify-between gap-4"><div><h1 className="text-balance text-3xl font-semibold tracking-[-0.03em]">我的插件</h1><p className="mt-2 max-w-2xl text-pretty text-sm text-muted-foreground">官方插件与这台设备上通过对话安装的 Skill、Pi extension 分开管理。本机包启用后会自动载入新建的主伙伴会话。</p></div><Button type="button" variant="outline" className="min-h-10" onClick={() => { void props.onRefresh(); void props.onDeviceRefresh?.(); }}><RefreshCw className="mr-2 h-4 w-4" />刷新</Button></header>
<h2 className="mt-8 text-xl font-semibold">官方插件</h2>
{props.library?.stale ? <p role="status" className="mt-4 rounded-xl bg-warning/10 p-3 text-pretty text-sm">正在显示上次可信的插件库;服务恢复前,在线计量或托管能力保持不可用。</p> : null}
{props.state === 'loading' && !props.library ? <p role="status" className="mt-8 text-sm">正在读取我的插件…</p> : null}
{props.state === 'error' && !props.library ? <div role="alert" className="mt-8 rounded-2xl bg-warning/10 p-5"><h2 className="font-semibold">无法读取我的插件</h2><p className="mt-2 text-pretty text-sm">{props.error ?? '请稍后重试。'}</p></div> : null}
{visibleError && props.library ? <p role="alert" className="mt-4 rounded-xl bg-warning/10 p-3 text-pretty text-sm">{visibleError}</p> : null}
{props.library && props.library.items.length === 0 ? <div className="mt-8 rounded-2xl bg-surface-subtle p-6 text-center"><PackageCheck className="mx-auto h-8 w-8 text-muted-foreground" /><h2 className="mt-3 text-balance font-semibold">账号插件库还是空的</h2><p className="mt-1 text-pretty text-sm text-muted-foreground">前往插件中心免费获取需要的插件。</p><Button asChild className="mt-4 min-h-10"><Link to="/plugin-marketplace">浏览插件中心</Link></Button></div> : null}
<section aria-label="账号插件库" className="mt-6 space-y-4">
<section aria-label="官方插件" className="mt-4 space-y-4">
{props.library?.items.map((plugin) => {
const installed = props.installations[plugin.pluginId];
const systemIncluded = plugin.acquisition === 'system_included';
@@ -96,6 +108,57 @@ export function MyPluginsView(props: MyPluginsViewProps) {
</article>;
})}
</section>
<section aria-labelledby="device-packages-heading" className="mt-10">
<div>
<h2 id="device-packages-heading" className="text-xl font-semibold">本机安装</h2>
<p className="mt-1 text-pretty text-sm text-muted-foreground">
在对话中告诉 MakeLore 要安装的 npm 包、Git 仓库 Skill 或本机绝对路径;确认预览后才会写入这台设备。
</p>
</div>
{props.deviceState === 'loading' && !props.deviceIndex
? <p role="status" className="mt-4 text-sm">正在读取本机插件…</p>
: null}
{props.deviceState === 'error'
? <p role="alert" className="mt-4 rounded-xl bg-warning/10 p-3 text-sm">{props.deviceError ?? '无法读取本机插件。'}</p>
: null}
{devicePackages.length === 0 && props.deviceState !== 'loading'
? <div className="mt-4 rounded-2xl bg-surface-subtle p-5 text-sm text-muted-foreground">这台设备还没有通过对话安装的包。</div>
: null}
<div className="mt-4 space-y-4">
{devicePackages.map((item) => {
const busy = Object.keys(devicePending).some((key) => key.endsWith(`:${item.packageId}`));
const kind = item.kind === 'skill-only' ? 'Skill'
: item.kind === 'pi-extension' ? 'Pi extension' : 'Skill + Pi extension';
return <article key={item.packageId} className="rounded-2xl bg-background p-5 shadow-soft ring-1 ring-border/70">
<div className="flex flex-wrap items-start justify-between gap-4">
<div>
<div className="flex flex-wrap items-center gap-2">
<h3 className="text-lg font-semibold">{item.displayName}</h3>
<Badge variant={item.enabled ? 'secondary' : 'outline'}>{item.enabled ? '已启用' : '已停用'}</Badge>
</div>
<p className="mt-2 text-sm text-muted-foreground">{kind} · {item.source.kind.toUpperCase()} · {item.source.requested}</p>
</div>
<p className="text-sm tabular-nums">版本 {item.resolvedVersion}</p>
</div>
{item.confirmedExecutableCode
? <p className="mt-4 text-pretty text-sm text-warning"><AlertCircle className="mr-1 inline h-4 w-4" />包含可执行 Pi extension,运行时拥有完整桌面权限。</p>
: null}
<div className="mt-5 flex flex-wrap gap-2">
<Button type="button" variant="outline" className="min-h-10" disabled={busy}
aria-label={`${item.enabled ? '停用' : '启用'} ${item.displayName}`}
onClick={() => void props.onDeviceSetEnabled?.(item.packageId, !item.enabled)}>
{item.enabled ? '停用' : '启用'}
</Button>
<Button type="button" variant="ghost" className="min-h-10" disabled={busy}
aria-label={`移除本机包 ${item.displayName}`}
onClick={() => void props.onDeviceUninstall?.(item.packageId)}>
<Trash2 className="mr-2 h-4 w-4" />移除本机包
</Button>
</div>
</article>;
})}
</div>
</section>
</main>
);
}
@@ -108,13 +171,21 @@ export function MyPlugins() {
const state = usePluginMarketplaceStore((value) => value.libraryState);
const error = usePluginMarketplaceStore((value) => value.libraryError);
const pending = usePluginMarketplaceStore((value) => value.pending);
const deviceIndex = useDevicePackageStore((value) => value.index);
const deviceState = useDevicePackageStore((value) => value.state);
const deviceError = useDevicePackageStore((value) => value.error);
const devicePending = useDevicePackageStore((value) => value.pending);
useEffect(() => {
pluginMarketplaceStore.getState().activateAccount(accountKey);
if (accountKey) void pluginMarketplaceStore.getState().loadLibrary().catch(() => undefined);
}, [accountKey]);
useEffect(() => {
void devicePackageStore.getState().load().catch(() => undefined);
}, []);
const safe = (operation: Promise<void>) => operation.catch((reason) => {
toast.error(reason instanceof Error ? reason.message : String(reason));
});
const store = pluginMarketplaceStore.getState;
return <MyPluginsView library={library} installations={installations} state={state} error={error} pending={pending} onRefresh={() => safe(store().loadLibrary())} onInstall={(id) => safe(store().install(id))} onInstallBeta={(id) => safe(store().installBeta(id))} onUpdate={(id) => safe(store().update(id))} onUninstall={(id) => safe(store().uninstall(id))} onRemove={(id) => safe(store().remove(id))} onReacquire={(id) => safe(store().acquire(id))} />;
const deviceStore = devicePackageStore.getState;
return <MyPluginsView library={library} installations={installations} state={state} error={error} pending={pending} onRefresh={() => safe(store().loadLibrary())} onInstall={(id) => safe(store().install(id))} onInstallBeta={(id) => safe(store().installBeta(id))} onUpdate={(id) => safe(store().update(id))} onUninstall={(id) => safe(store().uninstall(id))} onRemove={(id) => safe(store().remove(id))} onReacquire={(id) => safe(store().acquire(id))} deviceIndex={deviceIndex} deviceState={deviceState} deviceError={deviceError} devicePending={devicePending} onDeviceRefresh={() => safe(deviceStore().load())} onDeviceSetEnabled={(id, enabled) => safe(deviceStore().setEnabled(id, enabled))} onDeviceUninstall={(id) => safe(deviceStore().uninstall(id))} />;
}