diff --git a/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.html b/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.html index c36c4ba..b8204e9 100644 --- a/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.html +++ b/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.html @@ -81,6 +81,7 @@ limitations under the License. (click)="selectTask(session.session_id, $event)" >
+
@if (getTaskStatus(session) === 'running') { @@ -88,6 +89,7 @@ limitations under the License. {{ getTaskStatus(session) | uiLabel }} +
@if (getDeviceSerial(session)) { diff --git a/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.scss b/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.scss index e29f215..57795d9 100644 --- a/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.scss +++ b/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.scss @@ -3694,10 +3694,13 @@ $stream-row-height: 25px; .item-header { display: flex; align-items: center; - justify-content: space-between; + justify-content: flex-start; + flex-wrap: wrap; gap: 8px; min-width: 0; + .task-status-group { display: inline-flex; align-items: center; gap: 6px; flex-shrink: 0; } + .task-badge { flex-shrink: 0; display: inline-flex; @@ -3790,6 +3793,7 @@ $stream-row-height: 25px; } .item-header-actions { + margin-left: auto; display: flex; align-items: center; gap: 4px; diff --git a/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.html b/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.html index 8e689b1..e618fa7 100644 --- a/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.html +++ b/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.html @@ -77,6 +77,7 @@ limitations under the License. (click)="selectTask(session.session_id)" >
+
@if (getTaskStatus(session) === 'running') { @@ -84,6 +85,7 @@ limitations under the License. {{ getTaskStatus(session) | uiLabel }} +
@if (getDeviceSerial(session)) { diff --git a/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.scss b/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.scss index 96246ec..b8128cc 100644 --- a/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.scss +++ b/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.scss @@ -329,11 +329,14 @@ $text-muted: #a1a1aa; .task-card-header { display: flex; - justify-content: space-between; + justify-content: flex-start; + flex-wrap: wrap; align-items: center; gap: 8px; min-width: 0; + .task-status-group { display: inline-flex; align-items: center; gap: 6px; flex-shrink: 0; } + .task-badge { flex-shrink: 0; display: inline-flex; @@ -413,6 +416,7 @@ $text-muted: #a1a1aa; } .task-card-header-actions { + margin-left: auto; display: flex; align-items: center; gap: 4px; diff --git a/apps/showcase_ui/src/app/components/task-plugins/task-plugins.component.html b/apps/showcase_ui/src/app/components/task-plugins/task-plugins.component.html index f1e8176..d861327 100644 --- a/apps/showcase_ui/src/app/components/task-plugins/task-plugins.component.html +++ b/apps/showcase_ui/src/app/components/task-plugins/task-plugins.component.html @@ -13,15 +13,17 @@ @for (plugin of available(); track plugin.manifest.id) {
- + @if (authorizedProfiles(plugin).length > 1) { + + }
} @empty {

还没有可用插件,请先安装、授权并启用。

}

不勾选任何插件时,本次任务只使用手机能力。

} @else { -

{{ available().length ? '默认连接用于自动调用,也可以关闭自动选择,指定本次可用的插件。' : '还没有可用插件,手机测试不受影响。' }}

+

{{ available().length ? 'AI 会按需使用可用插件,无需逐次选择。关闭自动使用后,可指定本次允许使用的插件。' : '还没有可用插件,手机测试不受影响。' }}

} 管理插件 diff --git a/apps/showcase_ui/src/app/components/task-plugins/task-plugins.component.spec.ts b/apps/showcase_ui/src/app/components/task-plugins/task-plugins.component.spec.ts index 6acb54e..f4bc2c4 100644 --- a/apps/showcase_ui/src/app/components/task-plugins/task-plugins.component.spec.ts +++ b/apps/showcase_ui/src/app/components/task-plugins/task-plugins.component.spec.ts @@ -30,7 +30,7 @@ describe('TaskPluginsComponent', () => { expect(emitted).toHaveBeenCalledWith({}); }); - it('offers authorized cross-device connections and uses the default profile', async () => { + it('uses the single authorized setting without exposing a connection selector', async () => { const fixture = TestBed.createComponent(TaskPluginsComponent); const component = fixture.componentInstance; component.toggle(); @@ -45,7 +45,26 @@ describe('TaskPluginsComponent', () => { checkbox.click(); fixture.detectChanges(); await fixture.whenStable(); expect(emitted).toHaveBeenCalledWith({ plugins: [{ id: 'desktop', profile: 'lab' }] }); expect(fixture.nativeElement.querySelectorAll('.choice').length).toBe(1); - expect(fixture.nativeElement.querySelector('select').textContent).not.toContain('旧配置'); + expect(fixture.nativeElement.querySelector('select')).toBeNull(); + expect(fixture.nativeElement.textContent).not.toContain('连接配置'); + }); + + it('allows choosing among multiple authorized settings and excludes unauthorized ones', async () => { + const fixture = TestBed.createComponent(TaskPluginsComponent); const component = fixture.componentInstance; + component.toggle(); + http.expectOne('/api/plugins').flush({ plugins: [{ enabled: true, + manifest: { id: 'desktop', name: '电脑控制', targets: ['mixed'] }, profiles: [ + { name: 'old', label: '旧配置', authorized: false }, + { name: 'lab', label: '实验室', authorized: true, is_default: true }, + { name: 'office', label: '办公室', authorized: true } + ] }] }); + component.automatic = false; fixture.detectChanges(); await fixture.whenStable(); + const emitted = jasmine.createSpy('changed'); component.changed.subscribe(emitted); + fixture.nativeElement.querySelector('.choice input').click(); fixture.detectChanges(); await fixture.whenStable(); + const select: HTMLSelectElement = fixture.nativeElement.querySelector('select'); + expect(select.value).toBe('lab'); expect(select.textContent).not.toContain('旧配置'); + select.value = 'office'; select.dispatchEvent(new Event('change')); fixture.detectChanges(); await fixture.whenStable(); + expect(emitted).toHaveBeenCalledWith({ plugins: [{ id: 'desktop', profile: 'office' }] }); }); it('shows a retryable error and keeps automatic mode on lookup failure', () => { diff --git a/apps/showcase_ui/src/app/components/task-plugins/task-plugins.component.ts b/apps/showcase_ui/src/app/components/task-plugins/task-plugins.component.ts index 7840a1d..1655e85 100644 --- a/apps/showcase_ui/src/app/components/task-plugins/task-plugins.component.ts +++ b/apps/showcase_ui/src/app/components/task-plugins/task-plugins.component.ts @@ -28,6 +28,7 @@ export class TaskPluginsComponent { return this.plugins().filter(p => p.enabled && p.profiles.some(profile => profile.authorized) && p.manifest.targets.some(target => target === 'android' || target === 'mixed')); } + authorizedProfiles(plugin: InstalledPlugin) { return plugin.profiles.filter(p => p.authorized); } toggle() { this.open.update(v => !v); if (this.open()) this.load(); @@ -41,7 +42,7 @@ export class TaskPluginsComponent { } choose(plugin: InstalledPlugin, event: Event) { if ((event.target as HTMLInputElement).checked) { - const profiles = plugin.profiles.filter(p => p.authorized); + const profiles = this.authorizedProfiles(plugin); this.selected[plugin.manifest.id] = (profiles.find(p => p.is_default) || profiles[0]).name; } else delete this.selected[plugin.manifest.id]; this.emit(); diff --git a/apps/showcase_ui/src/app/pages/plugins/plugins.component.html b/apps/showcase_ui/src/app/pages/plugins/plugins.component.html index af354aa..3e53f68 100644 --- a/apps/showcase_ui/src/app/pages/plugins/plugins.component.html +++ b/apps/showcase_ui/src/app/pages/plugins/plugins.component.html @@ -7,7 +7,7 @@ } @if (progress(); as job) { -
{{job.stage}}{{job.progress}}%
+
{{job.status === 'completed' ? '安装完成,请点击启用。' : job.stage}}{{job.progress}}%
}
@@ -19,11 +19,11 @@ @else if (!installed().length) {
extension

还没有安装插件

从市场安装经过验证的扩展,或在下方导入自己的插件。

}
@for (plugin of installed(); track plugin.manifest.id) {
extension

{{plugin.manifest.name}}

v{{plugin.manifest.version}} · {{plugin.source === 'marketplace' ? '插件市场' : '本地 / 自定义'}}
{{plugin.enabled ? '已启用' : '未启用'}}
-

{{plugin.manifest.description || '暂无描述'}}

{{plugin.profiles.length}} 套连接配置
-
+

{{plugin.manifest.description || '暂无描述'}}

{{hasSettings(plugin) ? '可在设置中调整插件参数。' : '无需填写参数,启用后即可使用。'}}
+
}
-
高级接入 · 本地插件与自定义 MCP

本地插件需要包含 plugin.json;导入后配置授权范围,再手动启用。

+
高级接入 · 本地插件与自定义 MCP

本地插件需要包含 plugin.json;导入后点击启用,按提示完成设置。

添加自定义 MCP / Skill
@@ -36,23 +36,38 @@ }
} +@if (confirmingEnable(); as plugin) { + +} @if (confirmingResource(); as resource) { } @if (editing(); as plugin) { -