mirror of
https://github.com/Tencent/WeKnora.git
synced 2026-10-24 23:12:53 +08:00
feat(plugin): event delivery and webhooks for plugins
Events:
- A plugin lists what it wants in permissions.events: knowledge.ingested,
knowledge.failed, knowledge.deleted or chat.answered. The admin reviews
the list at install; a plugin that only listens needs no other
contribution.
- A publish enqueues one plugin:event task per subscribed plugin the
workspace has on (maintenance queue, Redis or Lite).
- The task calls the plugin's new /v1/events at least once. Retryable
failures retry up to 10 times, keeping the event ID; other failures drop
the event.
- The plugin host refuses a process that subscribes without serving
events.
Where events come from:
- A document reaches completed or failed through a dozen paths but a few
repository writes, so a knowledge repository wrapper raises
knowledge.ingested and knowledge.failed on the transition only:
UpdateKnowledge, UpdateKnowledgeForTransfer, UpdateKnowledgeColumns,
FinalizeSubtask, CompleteProcessingWithoutSubtasks.
- knowledge.deleted comes from the shared delete path.
- chat.answered comes from where RAG and agent answers are persisted. The
stop button's early completion, which has no question, is skipped.
Webhooks:
- contributes.webhooks gives each workspace a secret URL per webhook:
/api/v1/plugin-callbacks/{plugin}/{webhook}/{tenant}.{mac}. The MAC is
keyed from SYSTEM_AES_KEY/JWT_SECRET.
- WeKnora answers 404 unless the URL is genuine, the webhook exists and the
workspace has the plugin on. Calls are rate-limited per URL, bodies are
capped at 1 MB and cookies are stripped.
- The call is relayed to the plugin's /v1/webhooks/{id} with that
workspace's envelope; the plugin verifies the sender.
- Admins get the URLs from GET /plugins/:id/webhooks, shown with copy
buttons in the plugin center's configure drawer. Plugins get them in
context.webhooks when APP_EXTERNAL_URL is set.
SDKs:
- Go: OnEvent and Webhook.
- Python: @plugin.on_event and @plugin.webhook.
- Conformance, OpenAPI and READMEs are updated.
Also: examples/plugins/activity (Python) keeps a workspace activity feed
from events and a secret-checked note webhook, and shows it on a toolbox
page.
This commit is contained in:
@@ -218,6 +218,56 @@ paths:
|
||||
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 }
|
||||
@@ -269,6 +319,10 @@ components:
|
||||
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
|
||||
@@ -375,6 +429,54 @@ components:
|
||||
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]
|
||||
|
||||
Reference in New Issue
Block a user