feat(extension): 实现插件命令与设置贡献
This commit is contained in:
@@ -271,6 +271,66 @@ export interface PluginHostStatus {
|
||||
error?: string | null
|
||||
}
|
||||
|
||||
export type PluginCommandLocation = 'command_palette' | 'context_menu' | 'toolbar'
|
||||
|
||||
export interface PluginCommand {
|
||||
command_id: string
|
||||
plugin_id: string
|
||||
title: string
|
||||
description: string
|
||||
icon?: string | null
|
||||
locations: PluginCommandLocation[]
|
||||
when: string[]
|
||||
parameters: Record<string, unknown>
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export interface PluginCommandContext {
|
||||
vault_id?: string | null
|
||||
note_id?: string | null
|
||||
file_path?: string | null
|
||||
selection?: string | null
|
||||
}
|
||||
|
||||
export interface PluginCommandEffect {
|
||||
type: 'none' | 'notification' | 'navigate' | 'refresh' | 'job'
|
||||
payload: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface PluginCommandResult {
|
||||
command_id: string
|
||||
status: 'completed'
|
||||
effect: PluginCommandEffect
|
||||
}
|
||||
|
||||
export type PluginSettingType = 'string' | 'number' | 'boolean' | 'select' | 'secret'
|
||||
|
||||
export interface PluginSettingField {
|
||||
key: string
|
||||
label: string
|
||||
description: string
|
||||
type: PluginSettingType
|
||||
required: boolean
|
||||
default?: unknown
|
||||
minimum?: number | null
|
||||
maximum?: number | null
|
||||
options: string[]
|
||||
}
|
||||
|
||||
export interface PluginSettingsSchema {
|
||||
plugin_id: string
|
||||
schema_version: number
|
||||
fields: PluginSettingField[]
|
||||
values: Record<string, unknown>
|
||||
secrets: Record<string, { configured: boolean }>
|
||||
}
|
||||
|
||||
export interface PluginSecretStatus {
|
||||
plugin_id: string
|
||||
key: string
|
||||
configured: boolean
|
||||
}
|
||||
|
||||
export interface PluginContribution {
|
||||
type: 'tool' | 'command' | 'importer' | 'exporter' | 'sidebar_panel' | 'settings_section'
|
||||
id: string
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import * as pluginService from './pluginService'
|
||||
|
||||
function jsonResponse(body: unknown) {
|
||||
return new Response(JSON.stringify(body), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal('fetch', vi.fn())
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('pluginService contribution adapter', () => {
|
||||
it('lists and executes Plugin Commands with scoped wire fields', async () => {
|
||||
const fetchMock = vi.mocked(fetch)
|
||||
fetchMock
|
||||
.mockResolvedValueOnce(jsonResponse({ items: [{ command_id: 'text-tools.uppercase-selection' }] }))
|
||||
.mockResolvedValueOnce(jsonResponse({
|
||||
command_id: 'text-tools.uppercase-selection',
|
||||
status: 'completed',
|
||||
effect: { type: 'notification', payload: { message: 'HELLO' } },
|
||||
}))
|
||||
|
||||
const commands = await pluginService.listPluginCommands('command_palette')
|
||||
const result = await pluginService.executePluginCommand(
|
||||
'text-tools.uppercase-selection',
|
||||
{},
|
||||
{ note_id: 'note-1', selection: 'hello' },
|
||||
)
|
||||
|
||||
expect(commands[0].command_id).toBe('text-tools.uppercase-selection')
|
||||
expect(result.effect.payload.message).toBe('HELLO')
|
||||
expect(fetchMock.mock.calls[0][0]).toBe(
|
||||
'/api/plugin-contributions/commands?location=command_palette',
|
||||
)
|
||||
expect(JSON.parse(String(fetchMock.mock.calls[1][1]?.body))).toEqual({
|
||||
arguments: {},
|
||||
context: { note_id: 'note-1', selection: 'hello' },
|
||||
})
|
||||
})
|
||||
|
||||
it('uses separate Settings and Secret endpoints', async () => {
|
||||
const fetchMock = vi.mocked(fetch)
|
||||
fetchMock
|
||||
.mockResolvedValueOnce(jsonResponse({
|
||||
plugin_id: 'text-tools', schema_version: 1, fields: [],
|
||||
values: { result_limit: 10 }, secrets: { api_key: { configured: false } },
|
||||
}))
|
||||
.mockResolvedValueOnce(jsonResponse({
|
||||
plugin_id: 'text-tools', schema_version: 1, fields: [],
|
||||
values: { result_limit: 20 }, secrets: { api_key: { configured: false } },
|
||||
}))
|
||||
.mockResolvedValueOnce(jsonResponse({ plugin_id: 'text-tools', key: 'api_key', configured: true }))
|
||||
.mockResolvedValueOnce(jsonResponse({ plugin_id: 'text-tools', key: 'api_key', configured: false }))
|
||||
|
||||
await pluginService.getPluginSettings('text-tools')
|
||||
await pluginService.updatePluginSettings('text-tools', 1, { result_limit: 20 })
|
||||
await pluginService.putPluginSecret('text-tools', 'api_key', 'request-only-secret')
|
||||
await pluginService.deletePluginSecret('text-tools', 'api_key')
|
||||
|
||||
expect(fetchMock.mock.calls.map(([url]) => url)).toEqual([
|
||||
'/api/plugins/text-tools/settings',
|
||||
'/api/plugins/text-tools/settings',
|
||||
'/api/plugins/text-tools/settings/api_key/secret',
|
||||
'/api/plugins/text-tools/settings/api_key/secret',
|
||||
])
|
||||
expect(JSON.parse(String(fetchMock.mock.calls[1][1]?.body))).toEqual({
|
||||
schema_version: 1,
|
||||
values: { result_limit: 20 },
|
||||
})
|
||||
expect(JSON.parse(String(fetchMock.mock.calls[2][1]?.body))).toEqual({
|
||||
secret: 'request-only-secret',
|
||||
})
|
||||
expect(fetchMock.mock.calls[3][1]?.method).toBe('DELETE')
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,17 @@
|
||||
import apiClient from './apiClient'
|
||||
import type { ApiPlugin, OperationResponse, Plugin, PluginContribution, PluginHostStatus } from '@/contracts'
|
||||
import type {
|
||||
ApiPlugin,
|
||||
OperationResponse,
|
||||
Plugin,
|
||||
PluginCommand,
|
||||
PluginCommandContext,
|
||||
PluginCommandLocation,
|
||||
PluginCommandResult,
|
||||
PluginContribution,
|
||||
PluginHostStatus,
|
||||
PluginSecretStatus,
|
||||
PluginSettingsSchema,
|
||||
} from '@/contracts'
|
||||
|
||||
function toPlugin(plugin: ApiPlugin): Plugin {
|
||||
const { manifest } = plugin
|
||||
@@ -62,6 +74,55 @@ export async function restartPluginHost(pluginId: string): Promise<OperationResp
|
||||
return apiClient.post(`/api/plugins/${pluginId}/host/restart`)
|
||||
}
|
||||
|
||||
export async function listPluginCommands(location?: PluginCommandLocation): Promise<PluginCommand[]> {
|
||||
const query = location ? `?location=${encodeURIComponent(location)}` : ''
|
||||
const response = await apiClient.get<{ items: PluginCommand[] }>(`/api/plugin-contributions/commands${query}`)
|
||||
return response.items
|
||||
}
|
||||
|
||||
export async function executePluginCommand(
|
||||
commandId: string,
|
||||
argumentsValue: Record<string, unknown> = {},
|
||||
context: PluginCommandContext = {},
|
||||
): Promise<PluginCommandResult> {
|
||||
return apiClient.post(`/api/plugin-contributions/commands/${encodeURIComponent(commandId)}/execute`, {
|
||||
arguments: argumentsValue,
|
||||
context,
|
||||
})
|
||||
}
|
||||
|
||||
export async function getPluginSettings(pluginId: string): Promise<PluginSettingsSchema> {
|
||||
return apiClient.get(`/api/plugins/${encodeURIComponent(pluginId)}/settings`)
|
||||
}
|
||||
|
||||
export async function updatePluginSettings(
|
||||
pluginId: string,
|
||||
schemaVersion: number,
|
||||
values: Record<string, unknown>,
|
||||
): Promise<PluginSettingsSchema> {
|
||||
return apiClient.put(`/api/plugins/${encodeURIComponent(pluginId)}/settings`, {
|
||||
schema_version: schemaVersion,
|
||||
values,
|
||||
})
|
||||
}
|
||||
|
||||
export async function putPluginSecret(
|
||||
pluginId: string,
|
||||
key: string,
|
||||
secret: string,
|
||||
): Promise<PluginSecretStatus> {
|
||||
return apiClient.put(
|
||||
`/api/plugins/${encodeURIComponent(pluginId)}/settings/${encodeURIComponent(key)}/secret`,
|
||||
{ secret },
|
||||
)
|
||||
}
|
||||
|
||||
export async function deletePluginSecret(pluginId: string, key: string): Promise<PluginSecretStatus> {
|
||||
return apiClient.delete(
|
||||
`/api/plugins/${encodeURIComponent(pluginId)}/settings/${encodeURIComponent(key)}/secret`,
|
||||
)
|
||||
}
|
||||
|
||||
export async function uninstallPlugin(pluginId: string): Promise<OperationResponse> {
|
||||
return apiClient.delete(`/api/plugins/${pluginId}`)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user