feat(coding): add model tools and device packages
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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))} />;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user