Files
Artemis-Eapil/apps/showcase_ui/src/app/pages/plugins/plugins.component.html
T

58 lines
8.5 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<main class="plugins-page">
<header><div><span class="eyebrow">ARTEMIS · 扩展能力</span><h1>插件</h1><p>为测试接入电脑和其他工具,统一执行并保留证据。</p></div>
<button (click)="refresh(); refreshCatalog()" [disabled]="busy()">刷新</button></header>
@if (error()) { <div class="message error" role="alert">{{error()}}</div> }
@if (notice()) { <div class="message success" role="status">{{notice()}}</div> }
@for (resource of blocked(); track resource[0]) {
<section class="message error" role="alert"><strong>资源保护 · {{resource[0]}}</strong><p>{{resource[1]}}</p><button (click)="confirmingResource.set(resource[0])" [disabled]="busy()">核对并解除保护</button></section>
}
@if (progress(); as job) {
<section class="install-progress" aria-live="polite"><div><span>{{job.stage}}</span><strong>{{job.progress}}%</strong></div>
<progress max="100" [value]="job.progress" [attr.aria-label]="job.stage"></progress></section>
}
<div class="tabs" role="tablist" aria-label="插件来源">
<button role="tab" [attr.aria-selected]="tab() === 'installed'" [class.active]="tab() === 'installed'" (click)="tab.set('installed')">已安装 <span>{{installed().length}}</span></button>
<button role="tab" [attr.aria-selected]="tab() === 'market'" [class.active]="tab() === 'market'" (click)="tab.set('market')">插件市场</button>
</div>
@if (tab() === 'installed') {
@if (loading()) { <p role="status" class="empty">正在读取插件…</p> }
@else if (!installed().length) { <section class="empty"><span class="material-symbols-outlined">extension</span><h2>还没有安装插件</h2><p>从市场安装经过验证的扩展,或在下方导入自己的插件。</p><button class="primary" (click)="tab.set('market')">浏览插件市场</button></section> }
<div class="cards">@for (plugin of installed(); track plugin.manifest.id) {
<article class="card"><div class="card-top"><div class="plugin-icon"><span class="material-symbols-outlined">extension</span></div><div><h2>{{plugin.manifest.name}}</h2><span class="meta">v{{plugin.manifest.version}} · {{plugin.source === 'marketplace' ? '已验证市场插件' : '本地 / 自定义'}}</span></div><span class="badge" [class.enabled]="plugin.enabled">{{plugin.enabled ? '已启用' : '未启用'}}</span></div>
<p>{{plugin.manifest.description || '暂无描述'}}</p><div class="meta">{{plugin.profiles.length}} 套连接配置</div>
<footer><button (click)="edit(plugin)" [disabled]="busy()">配置</button><button (click)="diagnose(plugin)" [disabled]="busy() || !plugin.enabled">检查连接</button><button [class.primary]="!plugin.enabled" (click)="toggle(plugin)" [disabled]="busy()">{{plugin.enabled ? '停用' : '启用'}}</button><button class="text-button" (click)="removing.set(plugin)" [disabled]="busy()">卸载</button></footer>
</article>
}</div>
<details class="advanced"><summary>高级接入 · 本地插件与自定义 MCP</summary><p>本地插件需要包含 plugin.json;导入后配置授权范围,再手动启用。</p>
<div class="inline"><input aria-label="本地插件路径" [(ngModel)]="localPath" placeholder="插件 ZIP 文件或目录的完整路径"><button (click)="install({local_path: localPath})" [disabled]="busy() || !localPath.trim()">导入</button></div>
<details><summary>添加自定义 MCP / Skill</summary><label>插件清单 JSON<textarea rows="12" [(ngModel)]="customManifest" spellcheck="false"></textarea></label><label>Skill 内容(可选)<textarea rows="4" [(ngModel)]="customSkill"></textarea></label><button (click)="addCustom()" [disabled]="busy()">添加插件</button></details>
</details>
} @else {
@if (catalogError()) { <div class="message error" role="alert">{{catalogError()}} <button (click)="refreshCatalog()">重试</button></div> }
@else if (!market().length) { <section class="empty"><h2>首批插件验证中</h2><p>验证通过的插件会出现在这里;不会自动安装未经验证的版本。</p></section> }
<div class="cards">@for (entry of market(); track entry.manifest.id + entry.manifest.version) {
<article class="card"><div class="card-top"><div class="plugin-icon"><span class="material-symbols-outlined">verified</span></div><div><h2>{{entry.manifest.name}}</h2><span class="meta">v{{entry.manifest.version}} · {{entry.manifest.platforms.join(' / ')}}</span></div></div><p>{{entry.manifest.description}}</p><details><summary>验证记录</summary><p>{{entry.verification}}</p></details><footer><button class="primary" (click)="install({plugin_id: entry.manifest.id, version: entry.manifest.version})" [disabled]="busy() || installedVersion(entry.manifest.id) === entry.manifest.version">{{installedVersion(entry.manifest.id) === entry.manifest.version ? '已安装' : installedVersion(entry.manifest.id) ? '安装此版本' : '安装插件'}}</button></footer></article>
}</div>
}
</main>
@if (confirmingResource(); as resource) {
<div class="modal-backdrop"><section class="dialog small" role="dialog" aria-modal="true" aria-labelledby="resource-confirm-title"><h2 id="resource-confirm-title">确认 {{resource}} 已停止操作</h2><p>请先核对目标设备或远程服务,确认之前的插件动作已经停止,并检查实际状态。解除后任务才可再次操作此资源。</p><footer><button (click)="confirmingResource.set(null)" [disabled]="busy()">取消</button><button class="primary" (click)="confirmStopped()" [disabled]="busy()">已核对,解除保护</button></footer></section></div>
}
@if (editing(); as plugin) {
<div class="modal-backdrop"><section class="dialog" role="dialog" aria-modal="true" aria-labelledby="plugin-config-title"><header><div><h2 id="plugin-config-title">{{plugin.manifest.name}}</h2><p>配置连接与允许控制的范围</p></div><button aria-label="关闭配置" (click)="editing.set(null)" [disabled]="busy()">×</button></header>
<div class="inline"><select aria-label="选择连接配置" [ngModel]="profile.name" (ngModelChange)="selectProfile($event)">@for (p of plugin.profiles; track p.name) { <option [value]="p.name">{{p.label}}</option> }</select><button (click)="newProfile()">新增配置</button></div>
<div class="two-columns"><label>配置标识<input [(ngModel)]="profile.name" placeholder="如 lab-router"></label><label>显示名称<input [(ngModel)]="profile.label" placeholder="如实验室路由器"></label></div>
@for (entry of fields(); track entry[0]) { <label>{{entry[1].label}}{{entry[1].required ? ' *' : ''}}
@if (entry[1].secret) { <input type="password" autocomplete="new-password" [(ngModel)]="profile.secrets![entry[0]]" [placeholder]="profile.configured_secrets?.includes(entry[0]) ? '已保存,留空保留' : '仅保存在本机'"> }
@else { <input [(ngModel)]="profile.values[entry[0]]" [placeholder]="entry[1].default"> }
@if (entry[1].description) { <small>{{entry[1].description}}</small> }
</label> }
<details><summary>控制范围与共享资源</summary><label>允许工具(每行一个,留空使用全部工具)<textarea rows="3" [(ngModel)]="allowedTools"></textarea></label><label>参数约束 JSON<textarea rows="4" [(ngModel)]="constraints" placeholder="按工具名称配置允许的参数值"></textarea></label><label>共享资源(同一目标使用相同标识,每行一个)<textarea rows="2" [(ngModel)]="resources" placeholder="router:lab"></textarea></label></details>
<label class="checkbox"><input type="checkbox" [(ngModel)]="profile.is_default">作为自动选择插件时的默认连接配置</label>
<label class="checkbox"><input type="checkbox" [(ngModel)]="profile.authorized">允许 ARTEMIS 使用此配置执行插件工具</label><p class="meta">插件进程使用当前用户的系统权限。工具与参数范围由 ARTEMIS 检查。</p>
@if (error()) { <p class="message error" role="alert">{{error()}}</p> }
<footer><button (click)="editing.set(null)" [disabled]="busy()">取消</button><button class="primary" (click)="save()" [disabled]="busy() || !profile.name.trim()">{{busy() ? '保存中…' : '保存配置'}}</button></footer>
</section></div>
}
@if (removing(); as plugin) { <div class="modal-backdrop"><section class="dialog small" role="dialog" aria-modal="true" aria-labelledby="remove-title"><h2 id="remove-title">卸载 {{plugin.manifest.name}}?</h2><p>此插件及其连接配置将不再供新任务使用。</p><footer><button (click)="removing.set(null)" [disabled]="busy()">取消</button><button class="danger" (click)="remove()" [disabled]="busy()">确认卸载</button></footer></section></div> }