All docs

Plugin API contract

The API contract between a plugin app and Cadau, for the integrating team.

Source docs/en/site/sdk-platform-api.md

Platform plugin REST API contract.

Base URL: {Cadau site}/api/v1 Version: 0.1 (aligned with Cadau backend platform_plugin_*.go)

Scope: the REST below and platform_plugins[] registration are only for apps-desktop iframe plugins. To let a work assistant call a business API from conversation (face recognition, drawing, and similar), use skills + internet capability pack. See [../../agent-capability/README.md](/docs/sdk-agent-capability) and [../README.md](/docs/sdk-platform-plugin).


0. Registration and plugin gateway

Register in Cadau mindlink.jsonplatform_plugins[] (example registration.example.json):

{
  "module_id": "my-plugin",
  "display_name": "我的插件 (My plugin)",
  "entry_base_url": "/p/my-plugin/",
  "upstream_url": "http://127.0.0.1:3010",
  "enabled": true
}
FieldMeaning
module_idUnique id; gateway path is /p/{module_id}/
entry_base_urlLaunch appends launch_token then gives it to the iframe; prefer same-origin /p/{module_id}/
upstream_urlOptional; Cadau reverse-proxies /p/{module_id}/* to this internal URL
enabledWhen false, the desktop does not show it

Gateway request path (browser → Cadau Web → backend → upstream):

GET /p/my-plugin/api/health

→ reverse-proxied to {upstream_url}/api/health (prefix /p/my-plugin is stripped).

Without upstream: only entry_base_url points at your public/standalone URL; Cadau does not proxy static pages or plugin APIs.

Implementation: backend/internal/platformplugin/proxy.go; Web Nginx location /p/ → backend.


1. Cadau side (user signed in)

1.1 List registered plugins

GET /platform-plugins
Authorization: Bearer {user access_token}

Response items[]: module_id, display_name, suite, icon, entry_base_url, enabled (internal upstream_url is not returned to the client)

1.2 Launch a plugin

POST /workspaces/{workspace_id}/platform-plugins/{module_id}/launch
Authorization: Bearer {user access_token}

Response:

FieldMeaning
module_idPlugin id
display_nameHuman-readable app name. Chinese then English in parentheses, for example “邮件 (Email)”, so people who cannot read Chinese can still recognize it
launch_tokenJWT for the plugin, about 15 minutes; the Cadau shell silently renews before expiry and pushes to the iframe
entry_urliframe entry, already with launch_token and workspace_id query params
expires_atISO 8601
api_base_urlAPI prefix relative to the site, usually /api/v1
workspace_idCurrent workspace
member_roleowner / admin / member
data_namespaceWorkspace plugin data isolation (same shape as session)

1.3 Skills shipped with the plugin (install into the workspace skill center)

An app-style plugin may ship skills/{slug}/ in the repo (see platform-plugin §7.5). Installing into the workspace skill center uses user sign-in auth, not launch_token.

Product voice: write into the skill center idempotently by slug; agents in the workspace can recall by default; do not rewrite each agent’s available-skills narrow list at seed time.

GET /workspaces/{workspace_id}/platform-plugins/{module_id}/skills
Authorization: Bearer {user access_token}

Response:

FieldMeaning
module_idPlugin id
skills[]slug, installed, up_to_date, needs_update, skill_id, name, bundle_fingerprint, installed_fingerprint
any_missingWhether any item is not installed
any_needs_updateWhether any installed item’s fingerprint is behind
can_manage_membersWhether the current user can seed

With no registered shipped skills, returns skills: [] (still 200).

POST /workspaces/{workspace_id}/platform-plugins/{module_id}/skills/seed
Authorization: Bearer {user access_token}
Content-Type: application/json

{ "slug": "practice-checklist-to-pack" }
  • Requires a member who can manage.
  • slug is optional; omit to install/upgrade all registered shipped skills for that module.
  • Response: { "ok": true, "module_id", "results": [ { slug, created, upgraded, skipped, skill_id, name, fingerprint } ] }.
  • Unknown module_id/slug (no registered pack) → 404.

Implementation: backend/internal/skillfromchat/plugin_skill_seed.go (RegisterPluginSkillBundle); compliance registration in the same package compliance_practice_seed.go; author mirror: plugins/compliance/skills/.


2. Plugin side (Bearer launch_token)

The following APIs do not need a Cadau user cookie, only:

Authorization: Bearer {launch_token}

2.1 Verify session

GET /platform-plugins/session

Response:

{
  "module_id": "my-plugin",
  "module_display_name": "我的插件 (My plugin)",
  "user_id": "uuid",
  "user_display_name": "Zhang San",
  "workspace_id": "uuid",
  "workspace_name": "R&D team",
  "member_role": "admin",
  "expires_at": "2026-07-02T10:00:00Z",
  "data_namespace": {
    "kind": "postgres_schema",
    "location": "ws_…",
    "schema": "ws_…"
  }
}

data_namespace: workspace plugin data isolation (Postgres schema or SQLite file path/DSN). Launch response includes the same field.

Agents calling the plugin: tool plugin_invoke → Cadau issues a short-lived JWT (knd=plugin_invoke) → plugin POST /api/agent/invoke. Operation list: plugin operations.json (or configure operations_url).

Error codes: plugin_launch_invalid (expired or invalid), plugin_disabled, forbidden

2.1.1 Workspace members (for binding accounts)

GET /platform-plugins/workspace-members

Bearer launch_token. Returns a short list of current workspace members (user_id, display_name, email, phone, member_role) so the plugin can bind employee files and appoint in-plugin roles. Any workspace member can read.

2.1.2 Send SMS / email on behalf

GET /platform-plugins/notify
POST /platform-plugins/notify

Bearer is launch_token, or a short-lived JWT the plugin issues with the same jwt_secret as Cadau (knd=plugin_notify, wid / mod). Used for camera callbacks and similar alerts when there is no user session.

Query channels GET: same checks as send-on-behalf (local channel or router already enabled). Do not guess in the plugin.

{ "sms": { "configured": true }, "email": { "configured": false }, "wecom": { "configured": false, "note": "WeCom messages are not enabled yet" } }

Send POST:

{ "channel": "sms", "to": "13800138000", "text": "201811" }
{ "channel": "email", "to": "ops@example.com", "subject": "Security alert", "text": "body", "html": "<p>body</p><p><img src=\"cid:snap1\" alt=\"capture\"></p>", "images": [{ "cid": "snap1", "content_type": "image/jpeg", "filename": "capture.jpg", "data_base64": "…" }] }
  • channel: sms or email (WeCom messages not enabled yet)
  • SMS uses the site SMS channel (first template item is the alert summary; prefer ≤ 60 characters)
  • Email uses the site mail channel; custom subject and body; security alerts may include html and up to two images (JPEG, cid-embedded and as attachments, so webmail can show the capture)
  • At most 40 messages per minute per workspace

Error codes: sms_not_configured, smtp_not_configured, invalid_phone, invalid_email, rate_limited

2.2 Knowledge documents

Paths are relative to the plugin theme directory plugin-{module_id}/ (Cadau maintains the index).

List

GET /platform-plugins/knowledge/items

Read

GET /platform-plugins/knowledge/content?path=guide.md

Write (member_role owner/admin)

PUT /platform-plugins/knowledge/content
Content-Type: application/json

{
  "path": "guide.md",
  "content": "# Title\n\nBody…",
  "title": "Getting started",
  "summary": "One-line summary for retrieval matching",
  "tags": ["tag1", "tag2"]
}

Delete (owner/admin)

DELETE /platform-plugins/knowledge/content?path=guide.md

Batch sync (owner/admin)

POST /platform-plugins/knowledge/sync
Content-Type: application/json

{
  "documents": [
    { "path": "a.md", "content": "…", "title": "A" },
    { "path": "b.md", "content": "…", "title": "B" }
  ]
}

Response: { "ok": true, "synced": ["a.md","b.md"], "count": 2, "theme_dir": "plugin-my-plugin" }

Scope: plugin knowledge sync is Markdown body (path + content). Images/short video and other rich media in workspace knowledge are managed on the Cadau product side and are not uploaded via this sync API.

2.2.1 Standards library read-only (workspace regulations and management standards)

Source of truth and publish flow: product §3.2.2 / docs/core-mechanisms/规范库.md. The plugin may read only published and in-effect entries.

GET /platform-plugins/standards
GET /platform-plugins/standards?tag=hr
GET /platform-plugins/standards/{standardId}
GET /platform-plugins/standards/{standardId}/content

Auth: Authorization: Bearer <launch_token> (same as knowledge APIs).

2.2.2 Asset library read-only (workspace templates and tables)

Source of truth and publish flow: product §3.2.3 / docs/core-mechanisms/资料库.md. The plugin may read only published entries; /file returns a binary file stream.

GET /platform-plugins/assets
GET /platform-plugins/assets?tag=hr
GET /platform-plugins/assets/{assetId}
GET /platform-plugins/assets/{assetId}/file

Auth: Authorization: Bearer <launch_token> (same as knowledge APIs).

2.2.3 Workspace model (structured JSON)

Plugins must not bring their own model API key. For short-lived structured results such as “generate a survey from this body / recommend staffing”, use this API with the large model already configured for this workspace.

Unlike “in-plugin embed assistant conversation” (§2.3 embed): this API is a one-shot server system+user → JSON text, with no conversation state.

POST /platform-plugins/ai/json
Authorization: Bearer <launch_token>
Content-Type: application/json

{
  "system": "You are……output one JSON object only: {...}",
  "user": "{ \"context\": \"…\" }"
}

Response:

{ "text": "{ \"…\": \"JSON string the model returned (may still include prose; the caller should parse/clean)\" }" }
ConstraintMeaning
Authlaunch_token (same as knowledge / standards library)
system / userBoth required; length counted in Unicode characters: system ≤ 32000, user ≤ 400000 (fits large-context models; business should still split by task)
Model not configuredHTTP 503, codellm_disabled, copy says the workspace has no large model yet
Call failedHTTP 502, codellm_error
SDKGo Client.AIJSON · JS client.aiJSON(system, user)
Optionalattachment_ids attach images/PDF; pdf_pages=true attaches page screenshots even when the PDF has a text layer (used to extract ID photos)

Official usage: plugins/hr staffing recommendations, resume photo and certificate extract, plugins/compliance generate surveys from standards.

2.2.4 Plugin upload extract / crop

Resume analysis and similar extract embedded images from an already uploaded PDF / Word (.docx) / web resume / image, or crop ID photos and certificate scans by a normalized box. Results are written to the current user’s upload library and return a new file_id.

POST /platform-plugins/uploads/{id}/extract-images
Authorization: Bearer <launch_token>
POST /platform-plugins/uploads/{id}/crop
Authorization: Bearer <launch_token>
Content-Type: application/json

{ "items": [{ "page": 1, "x": 0.7, "y": 0.05, "width": 0.25, "height": 0.28, "label": "Applicant photo" }] }

x/y/width/height are 0–1 relative to the page (or the whole image). Both responses are { "images": [{ "file_id", "filename", "mime_type", "width", "height", "page" }] }.

2.3 In-plugin assistant token

POST /platform-plugins/embed-token
Content-Type: application/json

{
  "user_agent_id": "uuid",
  "app_id": "my-plugin",
  "ttl_seconds": 3600
}

Response is similar to POST /user-agents/{id}/embed-token: access_token, token_type, expires_in, expires_at, workspace_id, user_agent_id, app_id, permanent, record_id

Then the frontend loads {Cadau}/embed/mindlink-widget.min.js and init (see the embed SDK contract).


3. Plugin-owned HTTP conventions (recommended)

PathMeaning
POST /api/bootstrapbody { "launch_token" } → verify session, optionally sync knowledge, return user context (including data_namespace)
GET /api/sessionRefresh session with the launch token in Bearer or cookie
GET /api/healthHealth check
GET /operations.jsonOperations an agent can call; if there is a fill dialog, must include fill_open_form (see §3.2); may declare workflow_steps in the same file; or set operations_url at registration
POST /api/agent/invoke(Optional) agent plugin_invoke and workflow auto-nodes; verify JWT (knd=plugin_invoke), not launch_token

iframe session: frontend stores launch_token in sessionStorage; later requests send Authorization: Bearer …. Plugin-owned API paths should use a relative path or resolvePluginAPI(), so they work with the Cadau same-origin gateway /p/{module_id}/.

3.1 Host ↔ plugin postMessage

DirectiontypeMeaning
Host → pluginmindlink:launch_tokenSilent renew; must write sessionStorage (JS: listenHostLaunchToken)
Host → pluginmindlink:themetheme: light \dark (JS: listenHostTheme)
Host → pluginmindlink:localelocale: zh \en (JS: listenHostLocale)
Host → pluginmindlink:plugin_tabHelp deep-links and similar switch in-plugin tab (JS: listenHostPluginTab)
Host → pluginmindlink:plugin_data_changedBusiness data changed; suggest refresh (JS: listenHostPluginDataChanged)
Host → pluginmindlink:plugin_form_fillWrite fields the assistant parsed back into the current draft (§3.2; JS: listenHostPluginFormFill)
Plugin → hostmindlink:request_launch_tokensource must be mindlink-plugin; ask for a token on hot reload/401 (JS: requestHostLaunchToken)
Plugin → hostmindlink:request_localesource must be mindlink-plugin; ask for current language on first screen or hot reload (JS: requestHostLocale)
Plugin → hostmindlink:plugin_ui_changedReport the currently open fill dialog (§3.2; JS: postPluginUiChanged)

Host messages have source mindlink; plugin messages have source mindlink-plugin.

3.2 Recognize the open form (host protocol)

Plugins with a fillable dialog must implement this. Product voice: [../README.md §6.1](/docs/sdk-platform-plugin); spec §4.1.5.

When the user opens a fill dialog, the app assistant must recognize this form, write natural language back into the draft, and show it immediately; until the user says “save / create / submit / complete”, do not write the DB.

#### 3.2.1 Plugin → host: mindlink:plugin_ui_changed

window.parent.postMessage, source must be mindlink-plugin. JS: postPluginUiChanged.

{
  "source": "mindlink-plugin",
  "type": "mindlink:plugin_ui_changed",
  "module_id": "my-plugin",
  "tab": "home",
  "dialog": "draft",
  "dialog_title": "Example draft",
  "form": { "name": "Zhang San" },
  "field_hints": "Name,notes",
  "readonly": false
}
FieldMeaning
module_idSame as registration
tabCurrent tab (optional)
dialogCurrent dialog id; on close pass an empty string, and the host clears “the form being filled”
dialog_titleUser-visible title (used when asked “what is on screen”)
formAlready-filled non-empty fields (strings); an empty draft may report only dialog + field_hints
field_hintsUser-language names of writable fields, comma-separated, for example Name,notes
readonlytrue: view dialog; the assistant only names the title and does not call fill_open_form

Cadau puts dialog / dialog_title / form / field_hints / readonly into conversation client_context.

#### 3.2.2 Operation fill_open_form

In operations.json, name must be fill_open_form, mutates: false. Cadau POST /api/agent/invoke body:

{
  "operation": "fill_open_form",
  "args": {
    "form": "draft",
    "fields": { "name": "Zhang San", "备注": "urgent" }
  }
}
argsMeaning
formSame as current dialog; may also accept dialog / case_type
fieldsKey-values to write; Chinese or English field names; may nest; host/SDK flatten

Plugin success response (may be wrapped in { "ok": true, "result": { … } }; the host looks down for ui_action):

{
  "ui_action": "fill_form",
  "form": "draft",
  "fields": { "name": "Zhang San", "note": "urgent" },
  "unresolved": [],
  "note": "Written into the currently open form; not saved to the system yet"
}
FieldMeaning
ui_actionMust be fill_form, or the host will not write back to the UI
formTarget draft id
fieldsNormalized strings that can go straight into inputs (names already resolved to ids belong here)
unresolvedOptional; { "field", "value", "reason" }[], tell the assistant when it does not match an existing record
noteOptional; one sentence the assistant can read

Do not INSERT/UPDATE business tables in this operation. When you need to resolve “R&D” → department id, only change fields; still do not write the DB.

Go: FillOpenFormPassthrough(body.Args); TS: fillOpenFormPassthrough(args). Skeleton: templates/starter/cmd/plugin/main.go.

#### 3.2.3 Host → plugin: mindlink:plugin_form_fill

After the Cadau shell parses ui_action=fill_form, it postMessages the iframe (source=mindlink). JS: listenHostPluginFormFill.

{
  "source": "mindlink",
  "type": "mindlink:plugin_form_fill",
  "module_id": "my-plugin",
  "form": "draft",
  "fields": { "name": "Zhang San", "note": "urgent" }
}

The plugin merges fields into the currently open draft whose dialog matches, and refreshes inputs immediately. mutates: false; the host does not refresh lists because of this.

3.3 Extra workflow steps and list contract (workflow_steps / rubric)

A plugin can declare workflow_steps in the same operations.json, hanging domain auto-nodes on the process designer (they appear in the catalog only when that plugin is enabled). When the engine reaches that step it calls POST /api/agent/invoke (same entry as plugin_invoke).

{
  "module_id": "my-plugin",
  "operations": [
    { "name": "workflow_load_checklist", "mutates": false, "min_role": "member" }
  ],
  "workflow_steps": [
    {
      "type": "my-plugin.load_checklist",
      "label": "Load material list",
      "description": "Write instance rubric for upload evidence / auto score",
      "color": "#0891b2",
      "kind": "auto",
      "operation": "workflow_load_checklist",
      "config_hint": "Optional phases"
    }
  ]
}
workflow_steps[] fieldMeaning
typeStep type (prefer {module_id}.{action})
kindCurrently only auto: on arrival Invoke, then advance on always after success
operationMatching operation name in operations
label / description / colorDesigner display

Success response (load-style nodes return rubric; write-back nodes may return only variables):

{ "ok": true, "rubric": [ /* RubricItem[] */ ], "approval_doc": { }, "variables": { } }
  • rubric: written onto the instance for core nodes upload evidence (human.upload) and auto score (llm.score); must match RubricItem below. Required for load-style nodes.
  • approval_doc: optional. Business-document snapshot shown on the approval step (same idea as check items). Can also be sent in variables.approval_doc at start.
  • variables: merged into instance variables (flat keys). Write-back nodes (confirm staffing request, and similar) use this for staffing_req_status etc.
  • Failure: { "ok": false, "error": "user-visible explanation", "rubric": [] }.
  • Put ok at the HTTP body top level (do not nest it only under result). See plugins/compliance load standards and plugins/hr confirm/return staffing request.

#### RubricItem (list item · implementation source of truth)

Aligned with backend backend/internal/standards.RubricItem and SDK type RubricItem (Go / TS). Other domain load nodes (onboarding materials, training confirmation, and similar) must output the same shape; do not invent field names.

FieldTypeRequiredMeaning
idstringYesStable primary key; upload item_photos, comments, and score results all attach by this id
requirement_textstringYesRequirement copy shown on the handling page
scoring_textstringUpload can be loose; recommended when connecting auto scorePass/score criteria
codestringNoDisplay number
phasestringNoPhase key (compared lowercase); for upload-node config.phases filter
tierstringNored \baseline \excellence
photo_standard_textstringNoPhoto/evidence hint
reference_photo_upload_idsstring[]NoPassing sample image upload_ids (at most 3); handling page shows them for comparison
must_passboolNoRed-line item; default false
iway_refstringNoDomain reference; omit if not generally needed

Reference: plugins/compliance workflow_load_check_itemstoRubric. Product notes: docs/core-mechanisms/workflow.json-v1.md §check-item list contract; mechanism guide: [../README.md](/docs/sdk-platform-plugin) §6.2.

3.4 Cookie / storage names (do not mix)

NameWho writesUse
ml_plugin_gwCadau gatewayPath=/p/{module_id}/; gateway auth when the browser hits the reverse-proxy path
ml_plugin_launchPlugin process (Go bootstrap)Helper cookie on the plugin origin (optional)
ml_plugin_launch_tokenPlugin frontend sessionStorageBearer source of truth; must update after renew

4. Security response headers (plugin service)

Content-Security-Policy: frame-ancestors 'self' http://localhost:8080 http://127.0.0.1:8080

In production, replace with the customer Cadau Web origin. Go SDK: mindlinkplugin.FrameAncestorsCSP(...).


5. Error body

{
  "error": "user-visible explanation",
  "code": "machine_code",
  "request_id": "…"
}

Common code: unauthorized, plugin_launch_invalid, forbidden, validation_error, not_found