feat(plugins): analyze plugin data in the built-in AI panel
This commit is contained in:
33 files changed
+716
-216
No files matched your search
@@ -73,6 +73,50 @@ describe("PluginHostBridge", () => {
|
||||
bridge.handleWindowMessage({ source: target, data: { source: "dbx-plugin", version: 1, type: "request", id: "open", method: "host.downloadFile", params: {} } } as MessageEvent);
|
||||
await vi.waitFor(() => expect(messages[1].error).toContain("desktop host"));
|
||||
});
|
||||
it("opens the built-in AI conversation only with the declared permission and host-owned identity", async () => {
|
||||
const messages: unknown[] = [];
|
||||
const target = { postMessage: (message: unknown) => messages.push(message) } as unknown as Window;
|
||||
const openAiConversation = vi.fn().mockResolvedValue(undefined);
|
||||
const api = { invoke: vi.fn(), notify: vi.fn(), sendBinary: vi.fn(), readAsset: vi.fn(), openAiConversation };
|
||||
const request = { source: "dbx-plugin", version: 1, type: "request", id: "ai", method: "host.ai.openConversation", params: { title: "自选分析", prompt: "分析", context: { snapshotId: "s1" }, send: true, pluginId: "forged" } };
|
||||
const denied = new PluginHostBridge(plugin(), workbench, {}, () => target, api);
|
||||
denied.handleWindowMessage({ source: target, data: request } as MessageEvent);
|
||||
await vi.waitFor(() => expect(messages).toHaveLength(1));
|
||||
expect(messages[0]).toMatchObject({ error: "Plugin has not declared permission 'host.ai'" });
|
||||
expect(openAiConversation).not.toHaveBeenCalled();
|
||||
|
||||
const allowed = new PluginHostBridge(plugin(["host.ai"]), workbench, {}, () => target, api);
|
||||
allowed.handleWindowMessage({ source: target, data: request } as MessageEvent);
|
||||
await vi.waitFor(() => expect(messages).toHaveLength(2));
|
||||
expect(openAiConversation).toHaveBeenCalledWith({ context: { pluginId: "sample", pluginName: "Sample", title: "自选分析", capturedAt: expect.any(String), data: { snapshotId: "s1" } }, prompt: "分析", send: true });
|
||||
expect(messages[1]).toMatchObject({ id: "ai", result: null });
|
||||
expect(api.invoke).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("advertises the ai capability group in the init message", () => {
|
||||
const messages: any[] = [];
|
||||
const target = { postMessage: (message: unknown) => messages.push(message) } as unknown as Window;
|
||||
const withAi = new PluginHostBridge(plugin(["host.ai"]), workbench, {}, () => target, {
|
||||
invoke: vi.fn(),
|
||||
notify: vi.fn(),
|
||||
sendBinary: vi.fn(),
|
||||
readAsset: vi.fn(),
|
||||
openAiConversation: vi.fn().mockResolvedValue(undefined),
|
||||
});
|
||||
withAi.sendInit();
|
||||
expect(messages[0].capabilities.ai).toBe(true);
|
||||
|
||||
messages.length = 0;
|
||||
const withoutAdapter = new PluginHostBridge(plugin(["host.ai"]), workbench, {}, () => target, {
|
||||
invoke: vi.fn(),
|
||||
notify: vi.fn(),
|
||||
sendBinary: vi.fn(),
|
||||
readAsset: vi.fn(),
|
||||
});
|
||||
withoutAdapter.sendInit();
|
||||
expect(messages[0].capabilities.ai).toBe(false);
|
||||
});
|
||||
|
||||
it("binds backend calls to the owning plugin identity", async () => {
|
||||
const messages: unknown[] = [];
|
||||
const target = { postMessage: (message: unknown) => messages.push(message) } as unknown as Window;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { InstalledPlugin, PluginBinaryEvent, PluginEvent, PluginUiAssetPayload, PluginUiContribution } from "@/types/database";
|
||||
import { clonePluginData, snapshotPluginWorkbenchContext } from "./pluginData";
|
||||
import { MAX_PLUGIN_PLAN_SQL_CHARS, MAX_PLUGIN_PLAN_TIMEOUT_MS, PLUGIN_PLAN_PERMISSION, type PluginPlanCapabilities, type PluginPlanRequest, type PluginPlanResult } from "@/types/pluginPlan";
|
||||
import { createPluginAiConversation, type AiPluginConversationRequest } from "@/lib/ai/aiPluginConversation";
|
||||
|
||||
const PLUGIN_MESSAGE_SOURCE = "dbx-plugin";
|
||||
const HOST_MESSAGE_SOURCE = "dbx-host";
|
||||
@@ -92,6 +93,7 @@ export interface PluginHostBridgeApi {
|
||||
notify(pluginId: string, method: string, params?: unknown): Promise<void>;
|
||||
sendBinary(pluginId: string, channel: string, dataBase64: string): Promise<void>;
|
||||
readAsset(pluginId: string, path: string): Promise<PluginUiAssetPayload>;
|
||||
openAiConversation?(request: AiPluginConversationRequest): Promise<void>;
|
||||
openWorkbench?(pluginId: string, contributionId: string, context?: PluginWorkbenchContext, options?: { forceNew?: boolean }): Promise<void> | void;
|
||||
openFilesystem?(pluginId: string, providerId: string, context?: PluginWorkbenchContext): Promise<void> | void;
|
||||
/** Explicit user-triggered reconnect of an owned plugin connection (full flow, interactive password prompt allowed). */
|
||||
@@ -248,6 +250,7 @@ export class PluginHostBridge {
|
||||
downloadFile: !!this.api.downloadFile,
|
||||
planApi: !!this.api.getPlanCapabilities && !!this.api.explainPlan,
|
||||
storage: !!this.api.storageGet && !!this.api.storageSet && !!this.api.storageDelete,
|
||||
ai: !!this.api.openAiConversation,
|
||||
},
|
||||
context: snapshotPluginWorkbenchContext(this.context),
|
||||
});
|
||||
@@ -331,6 +334,13 @@ export class PluginHostBridge {
|
||||
return null;
|
||||
}
|
||||
if (method === "host.getContext") return snapshotPluginWorkbenchContext(this.context);
|
||||
if (method === "host.ai.openConversation") {
|
||||
this.requirePermission("host.ai");
|
||||
if (!this.api.openAiConversation) throw new Error("DBX AI conversation panel is unavailable");
|
||||
const request = createPluginAiConversation(this.plugin.manifest, params);
|
||||
await this.api.openAiConversation(request);
|
||||
return null;
|
||||
}
|
||||
if (method === "backend.invoke") {
|
||||
const input = requireRecord(params, "backend.invoke params");
|
||||
const backendMethod = requireProtocolName(input.method, "backend method");
|
||||
@@ -786,6 +796,7 @@ export function pluginSdkSource(initialTheme?: PluginBridgeTheme): string {
|
||||
downloadFile: (options) => request('host.downloadFile', options),
|
||||
cancelDownload: (downloadId) => request('host.cancelDownload', { downloadId }),
|
||||
request,
|
||||
ai: Object.freeze({ openConversation: (options) => request('host.ai.openConversation', options) }),
|
||||
invoke: (method, params, options = {}) => request('backend.invoke', { method, params, timeoutMs: options.timeoutMs }),
|
||||
stream,
|
||||
notify: (method, params) => request('backend.notify', { method, params }),
|
||||
|
||||
Reference in new issue
Block a user