Files
WeKnora/pluginsdk/pluginapi/openapi.yaml
T
lyingbug cf502ba46a feat(plugin): event delivery and webhooks for plugins (P2 batch 6) (#3728)
* feat(plugin): event delivery and webhooks for plugins
2026-09-26 14:47:13 +08:00

520 lines
18 KiB
YAML

openapi: 3.1.0
info:
title: WeKnora extension protocol
version: "1"
description: |
What WeKnora calls on a code plugin. Every POST takes an Envelope and
answers {"output": ...}, or streams NDJSON Events. Failures answer a
non-2xx status with an ErrorBody. Within a major version the protocol
only grows; both sides ignore fields they do not know.
Host plugins (started by the WeKnora plugin host) are called over the
socket from their handshake with `Authorization: Bearer <token>`. Remote
plugins are called over HTTPS and verify `X-WeKnora-Signature`:
hex HMAC-SHA256 of "<X-WeKnora-Timestamp>.<body>" with the shared secret,
rejecting timestamps more than 5 minutes off.
servers:
- url: http://plugin
security:
- hostToken: []
- signature: []
paths:
/v1/manifest:
get:
summary: What this process is and serves
responses:
"200":
description: The manifest
content:
application/json:
schema: { $ref: "#/components/schemas/Manifest" }
default: { $ref: "#/components/responses/Error" }
/v1/health:
get:
summary: Liveness
responses:
"200":
description: Healthy
content:
application/json:
schema:
type: object
required: [status]
properties: { status: { const: ok } }
default: { $ref: "#/components/responses/Error" }
/v1/config/validate:
post:
summary: Check the plugin's system and tenant configuration
requestBody: { $ref: "#/components/requestBodies/Envelope" }
responses:
"200": { $ref: "#/components/responses/EmptyOutput" }
default: { $ref: "#/components/responses/Error" }
/v1/websearch/{id}/search:
post:
summary: Web search (WebSearchProvider.Search)
parameters: [{ $ref: "#/components/parameters/ContributionID" }]
requestBody:
required: true
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/Envelope"
- properties: { input: { $ref: "#/components/schemas/SearchInput" } }
responses:
"200":
description: Results
content:
application/json:
schema:
type: object
required: [output]
properties: { output: { $ref: "#/components/schemas/SearchOutput" } }
default: { $ref: "#/components/responses/Error" }
/v1/connectors/{id}/validate:
post:
summary: Check a data source configuration (Connector.Validate)
parameters: [{ $ref: "#/components/parameters/ContributionID" }]
requestBody: { $ref: "#/components/requestBodies/Envelope" }
responses:
"200": { $ref: "#/components/responses/EmptyOutput" }
default: { $ref: "#/components/responses/Error" }
/v1/connectors/{id}/list-resources:
post:
summary: List syncable resources under a parent (Connector.ListResources)
parameters: [{ $ref: "#/components/parameters/ContributionID" }]
requestBody:
required: true
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/Envelope"
- properties:
input:
type: object
properties: { parentId: { type: string } }
responses:
"200":
description: Resources
content:
application/json:
schema:
type: object
required: [output]
properties:
output:
type: object
required: [resources]
properties:
resources: { type: array, items: { $ref: "#/components/schemas/Resource" } }
default: { $ref: "#/components/responses/Error" }
/v1/connectors/{id}/resolve-ancestors:
post:
summary: Ancestors a lazy picker must expand to show a selection
parameters: [{ $ref: "#/components/parameters/ContributionID" }]
requestBody:
required: true
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/Envelope"
- properties:
input:
type: object
properties: { resourceIds: { type: array, items: { type: string } } }
responses:
"200":
description: Ancestor IDs
content:
application/json:
schema:
type: object
required: [output]
properties:
output:
type: object
required: [ancestors]
properties: { ancestors: { type: array, items: { type: string } } }
default: { $ref: "#/components/responses/Error" }
/v1/connectors/{id}/fetch:
post:
summary: Stream a full or incremental sync
description: |
Answers NDJSON: "item" events carry a FetchedItem, "checkpoint"
events a Cursor (a complete, resumable snapshot), and the stream
ends with "end" (data = final Cursor) or "error". A stream that
stops without either is treated as interrupted.
parameters: [{ $ref: "#/components/parameters/ContributionID" }]
requestBody:
required: true
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/Envelope"
- properties: { input: { $ref: "#/components/schemas/FetchInput" } }
responses:
"200":
description: Event stream
content:
application/x-ndjson:
schema: { $ref: "#/components/schemas/Event" }
default: { $ref: "#/components/responses/Error" }
/v1/parsers/{id}/parse:
post:
summary: Parse a document into Markdown (DocReader.Read)
description: |
The file arrives as base64 content (or a URL). Answer Markdown and
the images it references; WeKnora stores the images and chunks the
text. A non-retryable error fails the document for good; a
retryable one (unavailable, rate_limited) is retried later.
parameters: [{ $ref: "#/components/parameters/ContributionID" }]
requestBody:
required: true
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/Envelope"
- properties: { input: { $ref: "#/components/schemas/ParseInput" } }
responses:
"200":
description: The parsed document
content:
application/json:
schema:
type: object
required: [output]
properties: { output: { $ref: "#/components/schemas/ParseOutput" } }
default: { $ref: "#/components/responses/Error" }
/v1/ui/request:
post:
summary: Answer a request from one of the plugin's pages
description: |
Plugin pages (pages, settingsSections, kbTabs) run in a sandboxed
iframe and reach the plugin only through the WeKnora bridge.
WeKnora checks the caller may open the page (its minRole) and
forwards the request with the usual envelope. Method and path are
the page's own routing. Plugins with pages that make no requests
answer not_found. The manifest lists "ui": ["request"] under
contributes when the plugin answers.
requestBody:
required: true
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/Envelope"
- properties: { input: { $ref: "#/components/schemas/UIRequest" } }
responses:
"200":
description: What the page receives
content:
application/json:
schema:
type: object
required: [output]
properties: { output: { $ref: "#/components/schemas/UIResponse" } }
default: { $ref: "#/components/responses/Error" }
/v1/events:
post:
summary: Receive an event the plugin subscribed to (permissions.events)
description: |
At least once: a retryable error or a timeout brings the same event
back later with the same id and a higher attempt, so handle it
idempotently. A non-retryable error drops it. Ignore unknown types.
The manifest lists "events": ["handler"] under contributes when the
plugin handles events.
requestBody:
required: true
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/Envelope"
- properties: { input: { $ref: "#/components/schemas/EventDelivery" } }
responses:
"200":
description: Handled
content:
application/json:
schema: { type: object, required: [output], properties: { output: { type: object } } }
default: { $ref: "#/components/responses/Error" }
/v1/webhooks/{id}:
post:
summary: Answer an inbound call to webhook id (contributes.webhooks)
description: |
Third parties call a secret per-workspace URL of WeKnora; WeKnora
relays the call here with that workspace's envelope. Verify the
sender (e.g. a signature header) with the workspace configuration.
parameters: [{ $ref: "#/components/parameters/ContributionID" }]
requestBody:
required: true
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/Envelope"
- properties: { input: { $ref: "#/components/schemas/WebhookRequest" } }
responses:
"200":
description: The answer to the caller
content:
application/json:
schema:
type: object
required: [output]
properties: { output: { $ref: "#/components/schemas/WebhookResponse" } }
default: { $ref: "#/components/responses/Error" }
components:
securitySchemes:
hostToken: { type: http, scheme: bearer }
signature: { type: apiKey, in: header, name: X-WeKnora-Signature }
parameters:
ContributionID:
name: id
in: path
required: true
description: The contribution's local ID from plugin.yaml
schema: { type: string }
requestBodies:
Envelope:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/Envelope" }
responses:
EmptyOutput:
description: Accepted
content:
application/json:
schema:
type: object
required: [output]
properties: { output: { type: object } }
Error:
description: A failed call
content:
application/json:
schema: { $ref: "#/components/schemas/ErrorBody" }
schemas:
Envelope:
type: object
required: [context, config]
properties:
context:
type: object
required: [tenantId]
properties:
tenantId: { type: integer }
userId: { type: string }
locale: { type: string }
requestId: { type: string }
deadline: { type: string, format: date-time }
host:
type: object
description: Host API access; absent without granted scopes
properties:
url: { type: string }
token: { type: string }
webhooks:
type: object
description: This workspace's webhook URLs by webhook ID, when WeKnora knows its public address
additionalProperties: { type: string }
config:
type: object
description: Configuration scopes, secrets decrypted, never merged
properties:
system: { type: object }
tenant: { type: object }
instance: { type: object }
input: {}
Manifest:
type: object
required: [id, version, apiVersion, contributes]
properties:
id: { type: string }
version: { type: string }
apiVersion: { const: weknora.plugin/v1 }
contributes:
type: object
additionalProperties: { type: array, items: { type: string } }
SearchInput:
type: object
required: [query]
properties:
query: { type: string }
maxResults: { type: integer }
includeDate: { type: boolean }
region: { type: string }
freshness: { type: string }
SearchOutput:
type: object
required: [results]
properties:
results:
type: array
items:
type: object
required: [title, url]
properties:
title: { type: string }
url: { type: string }
snippet: { type: string }
content: { type: string }
age: { type: string }
publishedAt: { type: string, format: date-time }
Resource:
type: object
required: [externalId, name]
properties:
externalId: { type: string }
name: { type: string }
type: { type: string }
description: { type: string }
url: { type: string }
modifiedAt: { type: string, format: date-time }
parentId: { type: string }
hasChildren: { type: boolean }
metadata: { type: object }
FetchInput:
type: object
required: [mode]
properties:
mode: { enum: [full, incremental] }
cursor: { $ref: "#/components/schemas/Cursor" }
resourceIds: { type: array, items: { type: string } }
Cursor:
type: object
properties:
lastSyncTime: { type: string, format: date-time }
state: { type: object }
FetchedItem:
type: object
required: [externalId, title]
properties:
externalId: { type: string }
title: { type: string }
content: { type: string, contentEncoding: base64 }
contentType: { type: string }
fileName: { type: string }
url: { type: string }
updatedAt: { type: string, format: date-time }
createdAt: { type: string, format: date-time }
metadata: { type: object, additionalProperties: { type: string } }
isDeleted: { type: boolean }
sourceResourceId: { type: string }
ParseInput:
type: object
properties:
fileName: { type: string }
fileType: { type: string, description: "Lower-case extension without the dot" }
content: { type: string, contentEncoding: base64 }
url: { type: string }
title: { type: string }
ParseOutput:
type: object
required: [markdown]
properties:
markdown: { type: string }
images:
type: array
items:
type: object
required: [originalRef, data]
properties:
originalRef: { type: string }
mimeType: { type: string }
data: { type: string, contentEncoding: base64 }
metadata: { type: object, additionalProperties: { type: string } }
EventDelivery:
type: object
required: [id, type, occurredAt, attempt]
properties:
id: { type: string, description: Idempotency key; kept across retries }
type: { enum: [knowledge.ingested, knowledge.failed, knowledge.deleted, chat.answered] }
occurredAt: { type: string, format: date-time }
attempt: { type: integer, minimum: 1 }
data:
oneOf:
- $ref: "#/components/schemas/KnowledgeEventData"
- $ref: "#/components/schemas/ChatEventData"
KnowledgeEventData:
type: object
required: [knowledgeBaseId, knowledgeId]
properties:
knowledgeBaseId: { type: string }
knowledgeId: { type: string }
title: { type: string }
fileName: { type: string }
fileType: { type: string }
source: { type: string }
error: { type: string }
ChatEventData:
type: object
required: [sessionId, messageId, answer]
properties:
sessionId: { type: string }
messageId: { type: string }
agentId: { type: string }
userId: { type: string }
question: { type: string }
answer: { type: string }
WebhookRequest:
type: object
required: [method, path]
properties:
method: { type: string }
path: { type: string }
query: { type: string }
headers: { type: object, additionalProperties: { type: string } }
body: { type: string, contentEncoding: base64 }
WebhookResponse:
type: object
properties:
status: { type: integer }
contentType: { type: string }
body: { type: string, contentEncoding: base64 }
UIRequest:
type: object
required: [mount, method, path]
properties:
mount: { type: string, description: '"<point>/<id>" of the page, e.g. pages/links' }
method: { type: string }
path: { type: string }
body: { description: JSON the page sent }
role: { enum: [viewer, contributor, admin, owner] }
UIResponse:
type: object
properties:
status: { type: integer, description: HTTP-style status for the page; 200 when absent }
body: { description: JSON returned to the page }
Event:
type: object
required: [type]
properties:
type: { enum: [item, checkpoint, progress, log, error, end] }
data: {}
error: { $ref: "#/components/schemas/Error" }
message: { type: string }
level: { enum: [debug, info, warn, error] }
Error:
type: object
required: [code, message]
properties:
code: { enum: [invalid_config, unauthorized, not_found, rate_limited, unavailable, bad_request, internal] }
message: { type: string }
retryable: { type: boolean }
details:
type: object
properties:
fields: { type: object, additionalProperties: { type: string } }
retryAfter: { type: integer }
ErrorBody:
type: object
required: [error]
properties:
error: { $ref: "#/components/schemas/Error" }