Phát triển Plugin

Viết plugin

Plugin thêm công cụ mới vào AI WorkDeck để AI có thể gọi mã của bạn từ cuộc trò chuyện. Hướng dẫn này bắt đầu với một ví dụ tối thiểu hoạt động được và kết thúc bằng việc nộp để xem xét.

Ai có thể xuất bản plugin

Plugin mã nguồn mở miễn phí có thể được gửi từ bất kỳ tài khoản đã đăng nhập nào — không cần xác minh.

Định giá plugin hoặc Pack, hoặc xuất bản mã nguồn đóng, yêu cầu tài khoản nhà phát triển đã xác minh (danh tính + phương thức thanh toán, được xem xét thủ công). Bạn có thể nộp đơn tại

Trung tâm nhà phát triển

SDK 1.6 views, connections, MCP, scoped settings, $schema/default compatibility fixes and host services require the new host. This implementation has not been released; installed clients do not gain support automatically. The simulator uses synthetic data and is not host compatibility evidence.

Trước tiên, hãy quyết định: bạn cần plugin hay Skill

Hai thứ này dễ nhầm lẫn, và chọn sai sẽ lãng phí rất nhiều công sức. Sự khác biệt gói gọn trong một câu: nếu cần viết code, hãy viết plugin; nếu chỉ cần giải thích rõ ràng là đủ, hãy viết Skill.

Kịch bảnTrường hợp sử dụng
Gọi API của hệ thống bên ngoài để lấy dữ liệuPlugin
Phân tích định dạng tệp đặc biệtPlugin
Yêu cầu AI viết nhận xét xem xét theo định dạng của hãng luật của bạnSkill
Định nghĩa các bước và cấu hình sản phẩm bàn giao cho một loại vụ việcSkill

Skill là văn bản thuần (một prompt cộng các từ kích hoạt): được công bố ngay khi gửi mà không cần duyệt, và viết nhanh hơn nhiều. Nếu một Skill đáp ứng được nhu cầu của bạn, hãy đi viết một Skill và bỏ qua phần còn lại của trang này.

Chạy plugin đầu tiên của bạn trong năm phút

Plugin là một dự án Java được biên dịch thành JAR, cộng thêm manifest.json, được nén thành zip và gửi lên. Bạn cần JDK 21 và Maven.

Tải xuống dự án mẫu ở trên — bản thân nó là một ví dụ hoàn chỉnh có thể chạy được. Chỉ có hai tệp là quan trọng. Tệp đầu tiên là lớp công cụ:

package com.example.myplugin;

import dev.langchain4j.agent.tool.Tool;

/**
 * Plugin tool class.
 *
 * Three hard rules — break any one of them and the tool will not be visible to the AI:
 * 1. Must have a no-arg constructor (instantiated via reflection by the host);
 * 2. Annotate tool methods with @Tool; the method name is the tool name and must match manifest.json tools[].name;
 * 3. Use String parameters and return values for simplicity (serialize complex structures as JSON strings).
 *
 * The @Tool description is read by the AI and determines when it invokes this tool.
 * State clearly when to call it rather than only how it is implemented.
 */
public class MyTools {

    @Tool("Count characters and CJK characters in a text passage and return a readable summary. Call when the user asks for character or word counts.")
    public String countChinese(String text) {
        if (text == null || text.isBlank()) {
            return "Empty input: 0 characters";
        }
        long cjk = text.codePoints()
                .filter(cp -> Character.UnicodeScript.of(cp) == Character.UnicodeScript.HAN)
                .count();
        return String.format("Total %d chars, including %d CJK chars", text.length(), cjk);
    }
}

Phần mô tả trong @Tool được viết cho AI — nó trực tiếp quyết định việc AI có gọi công cụ của bạn đúng lúc hay không. Viết "khi nào sử dụng" hữu ích hơn nhiều so với "phương thức này làm gì": vì cái trước là điều AI phải tự đánh giá.

Tệp thứ hai là manifest.json, mô tả plugin là gì:

{
  "id": "my-plugin",
  "name": "My Plugin",
  "version": "1.0.0",
  "description": "One sentence explaining what this plugin does for users, shown on the marketplace card.",
  "author": "Your Name or Team",
  "homepage": "https://example.com",
  "permissions": [],
  "tools": [
    {
      "name": "countChinese",
      "description": "Count characters in text",
      "permissions": []
    }
  ],
  "backendJars": ["my-plugin-1.0.0.jar"]
}

Tên công cụ phải khớp ở cả hai phía: tools[].name phải trùng với tên phương thức Java. Nếu khác nhau, công cụ sẽ không đăng ký được và AI không thể thấy nó.

Sau đó đóng gói nó:

mvn package
mkdir -p dist && cp target/my-plugin-1.0.0.jar manifest.json dist/
cd dist && zip -r ../my-plugin-1.0.0.zip . && cd ..

Lưu ý rằng zip phải chứa chính các tệp — đừng lồng thêm một thư mục. Khi giải nén phải thấy manifest.json ngay, chứ không phải một thư mục.

Hãy thử trên máy của bạn trước khi gửi

Không cần chờ phê duyệt. Sao chép toàn bộ thư mục dist/ vào thư mục plugin cục bộ của bạn và khởi động lại AI WorkDeck:

# macOS / Linux
~/.aiworkdeck/plugins/my-plugin/

Plugin của bạn sẽ xuất hiện trong Chợ Plugin → Đã cài đặt. Bật nó lên, rồi hỏi điều gì đó trong cuộc trò chuyện mà lẽ ra phải dùng công cụ của bạn và xem AI có gọi nó không. Nếu không, phần mô tả @Tool nhiều khả năng không giải thích được khi nào sử dụng nó.

Ý nghĩa của từng trường trong manifest.json

TrườngMô tả
idPhải là duy nhất toàn cầu; chỉ dùng chữ thường, chữ số và gạch nối. Không thể thay đổi sau khi xuất bản — đây là cách nâng cấp nhận diện "cùng một plugin".
versionPhiên bản ngữ nghĩa. Mỗi lần gửi phải cao hơn lần trước, nếu không sẽ bị từ chối.
name / descriptionHiển thị trên thẻ marketplace. Hãy mô tả nó mang lại gì cho người dùng, chứ không phải cách triển khai.
author / homepageTác giả và trang chủ của dự án. Không bắt buộc nhưng nên có — người dùng đánh giá độ tin cậy dựa vào những thông tin này.
permissionsCác quyền năng mà plugin này sử dụng; xem phần tiếp theo.
toolsDanh sách công cụ. name phải trùng với tên phương thức Java; hãy viết description sao cho mục đích rõ ràng.
backendJarsTên tệp JAR tính theo đường dẫn tương đối so với gốc gói. Không cho phép đường dẫn ../ ra ngoài gói.

permissions: khai báo trung thực — quá trình duyệt sẽ đối chiếu kiểm tra

Bốn giá trị tùy chọn; hãy khai báo những gì bạn sử dụng:

  • file_readĐọc tệp dự án
  • file_writeTạo, sửa đổi hoặc xóa tệp
  • networkTruy cập mạng bên ngoài
  • editorVận hành trình soạn thảo tài liệu

Đây không phải là sandbox

Plugin chạy trong cùng tiến trình với ứng dụng chủ, nên hành vi chưa được khai báo không thể bị chặn về mặt kỹ thuật — công cụ không khai báo quyền nào vẫn có thể đọc tệp. Việc khai báo không phải là giới hạn khi chạy mà là bằng chứng rà soát: chúng tôi đối chiếu với bản quét tĩnh của JAR. Khai báo không dùng mạng nhưng lại tham chiếu API mạng — hoặc ngược lại — sẽ khiến bài nộp bị từ chối.

Nội dung rà soát

Mỗi phiên bản đều được con người rà soát, thường trong vòng một đến hai ngày làm việc. Quét tự động chạy trước, và báo cáo của nó được đặt cạnh bản khai báo quyền của bạn trên bàn của người rà soát.

Những điều sau đây bị từ chối ngay lập tức:

  • Quyền khai báo không khớp với API thực sự được gọi
  • TrustManager tùy chỉnh hoặc bất kỳ cách nào khác để bypass việc xác thực chứng chỉ
  • Địa chỉ IP được hardcode, hoặc dữ liệu gửi đi qua HTTP dạng văn bản thuần
  • Reflection vào các đối tượng nội bộ của host (kết nối cơ sở dữ liệu, dịch vụ cấu hình, v.v.)
  • Mã bị làm rối hoặc đóng gói, hoặc bất cứ thứ gì che giấu chức năng của mã
  • Số phiên bản không cao hơn phiên bản trước

Sau khi được phê duyệt, nền tảng ký toàn bộ gói bằng khóa riêng của mình và các client xác minh chữ ký khi cài đặt. Sau đó, không ai — kể cả chúng tôi — có thể thay đổi nội dung gói mà không ký lại.

Nếu phát hiện sự cố sau khi phát hành, chúng tôi thu hồi phiên bản đó. Các client nhận danh sách thu hồi, tự động vô hiệu hóa và thông báo cho người dùng.

Lời hứa với người dùng — và ràng buộc với bạn

Người dùng của chúng tôi là luật sư, và máy của họ lưu trữ tài liệu mật của khách hàng. Plugin có quyền truy cập giống như ứng dụng host — vượt xa những gì nó thực sự cần.

Vì vậy, việc xét duyệt nghiêm về phía chặt chẽ, và việc từ chối luôn kèm lý do. Nếu plugin của bạn thực sự cần một khả năng trông nhạy cảm, hãy giải thích lý do trong ghi chú gửi — điều đó giúp quá trình xét duyệt nhanh hơn nhiều.

Plugin web: xây dựng UI bằng HTML/JS

Nếu thứ bạn muốn là một giao diện — biểu mẫu, công cụ tra cứu, bảng điều khiển — bạn không cần Java. Đặt một web/ thư mục trong gói (HTML/JS/CSS tĩnh thuần, điểm vào web/index.html) và trỏ manifest frontendEntry at it. It may ship no JAR at all.

Here the permissions are real. The desktop app loads a Web plugin into a sandboxed iframe, deliberately not same-origin: plugin scripts never see the host session, every capability travels over the postMessage bridge, and the host checks each call against the manifest permissions. Unlike a JAR plugin, this declaration is an enforcement boundary rather than a self-description.

web/awd-plugin-sdk.js in the template wraps that bridge. Nothing works before the handshake:

MethodParamsResultPermission
settings.open / settings.update{} / {values}{opened} / {}user / project settings; installation settings stay in trusted host UI
connections.list / connect / disconnect / request{id, operation?, params?}connection state / service JSONnetwork; host-held credentials and explicit developer trust
mcp.servers / mcp.call{} / {serverId, name, arguments}{servers} / MCP CallToolResultnetwork + project access; public HTTPS Streamable HTTP
billing.status / billing.checkout{connectionId?}{provider, active, plan?} / {opened}host purchase entitlement or developer subscription; no automatic charge
services.request / services.cancel{service, operation, params, requestId} / {requestId}operation resultexplicit service catalog; per-operation permission checks
context.get{}{ pluginId, projectId, language, theme, themeTokens }-
files.list{}{ files: [{ path, name, size }] }file_read
im.downloadAttachment{ conversationId, attachmentId }{ started, name }im (server-side attachment ACL check; no project write permission required)
notifications.publish{ eventKey, title, body?, actionState?, target? }notificationnotifications (current account inbox; target opens only this plugin)
notifications.resolve{ eventKey, state }notificationnotifications (updates only this plugin’s notifications; marking read never completes business actions)
im.request{ action, params }objectim (host 0.61.0+; fixed messaging actions, project archiving also requires file_write)
chat.addFile{ path }{}file_read (host 0.61.0+; attaches material without auto-sending)
files.prepareDrag{ path }{ fileId, dragToken, dragType, expiresAt, ... }file_read (host 0.61.0+; short-lived single-use drag ticket)
files.endDrag{}{}file_read (host 0.61.0+)
files.read{ path }{ path, content, truncated }file_read
ui.toast{ message }{}-
storage.get{ key }{ key, value }-
storage.set{ key, value }{}-
evidence.link{ anchor: { selection: true } | { quote }, docPath?, targets: [{ path, locator?, relation?, method?, note? }] }{ linkKey, targetIds }editor
evidence.list{ docPath?, path?, sectionPath?, status? }{ links: [{ linkKey, docPath, anchorText, sectionPath, status, targets }] }file_read
evidence.locate{ linkKey, targetId? }{}editor
tools.invoke{ name, args? }{ output }- (tool must be declared in this plugin’s manifest)
chat.send{ prompt }{}- (up to 4,000 characters)
ui.openFile{ path }{}file_read
doc.exec{ action, params? }{ result }editor (host 0.27.4+; action restricted to safe doc_/sheet_/slide_ subset)
doc.active{}{ fileId, kind }editor (host 0.27.4+)
events.subscribe{ events: ["files.changed" | "selection.changed" | "project.switched"] }{ subscribed }per event (host 0.27.4+)
events.unsubscribe{ events }{ subscribed }-
ai.request{ prompt, system?, purpose? }{ text, modelId }ai (host 0.27.4+; 16,000 chars, 10 calls/min, billed via user Credits)
settings.get{ key }{ key, value }- (host 0.28+; reads non-secret keys declared in top-level manifest settings)
<script src="awd-plugin-sdk.js"></script>
<script>
  const ctx = await awd.ready();                   // { pluginId, projectId, language, theme, themeTokens }
  const files = await awd.files.list();            // requires file_read
  const doc = await awd.files.read(files[0].path); // { path, content, truncated }
  await awd.ui.toast('Completed');
  await awd.storage.set('draft', { title: 'x' });  // plugin-scoped KV, capped at 64 KB
</script>

Errors arrive as rejected promises with err.code: permission_denied when the manifest does not declare what the call needs, unknown_method when the host does not recognize it. files.read caps text at 5 MB (truncated becomes true), and storage caps each plugin at 64 KB total.

Theme channel (v2.6)

The handshake context now carries theme ('light'|'dark') and themeTokens (a table of --awd-* CSS variables); the host also pushes { type: 'theme', theme, tokens } on every later switch. The SDK applies it automatically — setting data-theme, toggling the awd-theme-light/awd-theme-dark class, and writing each token as a CSS custom property — so plugin CSS can just use var(--awd-surface) with zero JS. For script-driven behavior, use awd.theme.get() to read the current value and awd.theme.onChange(cb) to subscribe. The host simulator has a light/dark toggle button in its header, so you can test both themes without installing the desktop app.

The template also ships a host simulator, dev/host-simulator.html: it impersonates the desktop host, performs the handshake, serves every method with fake data, and logs each bridge message. Run a local static server and you can develop in a browser without installing AI WorkDeck.

python3 -m http.server 8000
# -> http://localhost:8000/dev/host-simulator.html

Opening it directly over file:// usually shows a blank frame — browsers refuse to load sandboxed child documents from file://. The server must not rewrite URLs either: npx serve enables clean URLs by default, redirecting /web/index.html to /web so the relative SDK reference 404s.

SDK 1.6 platform APIs (new host preview)

The Web download includes a standalone Node.js 20+ CLI. Keep scripts/ and sdk/plugin-sdk/ in their relative locations. Test UI in the simulator, then permissions, project switching, disabling, cancellation and reconnection in a compatible isolated host. Example domains are placeholders, not working services.

Declare sidebar/workspace views with project/global scope; one view per placement and an existing web/ entry. Sidebar docks are left/right/bottom. Settings default to installation scope; user/project settings may be changed by the plugin. Installation settings remain in trusted host UI. Secrets never return to the iframe; use connections for per-user credentials.

Connections declare a fixed public HTTPS origin and fixed GET/POST operations. Authentication is none, Bearer or public-client OAuth2 PKCE. connect opens trusted host UI; tokens stay in the host. Never pass arbitrary URLs, authorization headers or host setting keys. BYO services and shared host AI/speech channels are separate choices with their own permissions and usage constraints.

OAuth uses Authorization Code with S256 PKCE. A local host uses its actual port at http://127.0.0.1:<port>/api/plugins/oauth/callback; server deployments must set ai.plugins.oauth-redirect-uri to a trusted HTTPS callback and register it with the provider. Expired credentials without refresh or credentials rejected with 401/403 return needsReconnect and require host-side login again.

MCP currently supports public HTTPS Streamable HTTP only: no stdio, local processes, private addresses or redirects. Declare network, every server and tool; remote names and schemas must match. Input roots are objects. Supported schema keywords are type/title/description, object properties/required/boolean additionalProperties, array items and string enum. Types include boolean/integer/number. No $ref, oneOf, nullable or other keywords. Limits: depth 8, 64 properties per object and 64 tools per server. connectionId references a same-origin connection in this plugin. Legacy credentialKey only references a declared installation secret.

Development installs require explicit administrator service trust in the host settings page. Trust is bound to the full manifest SHA256 and expires when it changes. Packages must not contain .awd-dev, .awd-service-trust or .awd-bundled markers. Trust does not unlock development JARs or local processes.

speech, tasks and clipboard permissions grant only catalogued services.request operations; this is not an arbitrary backend proxy. services.cancel cancels this plugin’s requests. Handle unsupported hosts, missing permissions and failures explicitly; do not silently switch to external or paid services.

Platform billing currently covers plugin purchase entitlement only; submitted prices are one-time purchases. There is no platform per-call collection or revenue-sharing API. Developer-owned subscriptions may declare connections.billing status and checkout. An opened checkout is not a successful payment.

Host billing.status refreshes the current account entitlement and returns refreshed; false means the refresh did not succeed and active may come from a still-valid cache, not a fresh payment confirmation. Checkout waits for the browser-opening result and rejects failures. Test actual host login, token expiry, subscription status, MCP errors, account switching and shared/BYOK routing; a synthetic provider verifies host integration, not live OAuth or payment certification.

Complete the free developer eligibility review and generate a submission token in the developer center. Supply it through the local AWD_PLUGIN_TOKEN environment variable. Never put it in source, Agent prompts or packages. Valid submissions remain pending human review and signing; passing local validation is not marketplace approval.

node scripts/plugin-dev.mjs validate .
node scripts/plugin-dev.mjs package . --out ../my-plugin.zip
# Start the synthetic UI simulator, then open /dev/host-simulator.html
python3 -m http.server 8000
# Explicit development installation: select an isolated imported project/folder first.
# Replace PORT with the isolated host port. AWD_SESSION_ID is supplied locally if required.
# Do not paste credentials into source or an Agent prompt.
node scripts/plugin-dev.mjs install --endpoint http://127.0.0.1:PORT --project-id PROJECT_ID --folder-id FOLDER_ID
# Explicit marketplace submission only; provide AWD_PLUGIN_TOKEN locally.
node scripts/plugin-dev.mjs submit . --endpoint https://www.aiworkdeck.com/api/plugins --price-cents 0
node scripts/plugin-dev.mjs submissions --endpoint https://www.aiworkdeck.com/api/plugins
{
  "id": "my-service-plugin",
  "name": "Service panel",
  "version": "1.0.0",
  "description": "SDK 1.6 host preview example",
  "author": "Example developer",
  "frontendEntry": "web/index.html",
  "permissions": [
    "network",
    "ai",
    "speech",
    "tasks",
    "clipboard"
  ],
  "settings": [
    {
      "key": "language",
      "label": "Language",
      "type": "string",
      "scope": "user",
      "default": "zh-CN"
    },
    {
      "key": "summaryStyle",
      "label": "Summary style",
      "type": "string",
      "scope": "project",
      "default": "brief"
    }
  ],
  "contributes": {
    "views": [
      {
        "id": "service-panel",
        "name": "Service panel",
        "entry": "web/index.html",
        "placement": "sidebar",
        "scope": "project",
        "defaultDock": "left",
        "allowedDocks": [
          "left",
          "right",
          "bottom"
        ]
      }
    ],
    "connections": [
      {
        "id": "service",
        "label": "My service",
        "baseUrl": "https://api.example.com",
        "auth": {
          "type": "bearer"
        },
        "operations": {
          "summarize": {
            "method": "POST",
            "path": "/v1/summarize"
          },
          "status": {
            "method": "GET",
            "path": "/v1/subscription"
          }
        },
        "billing": {
          "statusOperation": "status",
          "checkoutUrl": "https://example.com/subscribe"
        }
      }
    ],
    "mcpServers": [
      {
        "id": "remote",
        "transport": "streamable-http",
        "url": "https://api.example.com/mcp",
        "connectionId": "service",
        "tools": [
          {
            "name": "summarize",
            "description": "Summarize supplied text",
            "inputSchema": {
              "type": "object",
              "properties": {
                "text": {
                  "type": "string"
                }
              },
              "required": [
                "text"
              ],
              "additionalProperties": false
            }
          }
        ]
      }
    ]
  }
}

Complete task to give your Agent

Implement this requirement using the downloaded AI WorkDeck Web plugin template: [feature, inputs, outputs, acceptance criteria].
Read README.md, sdk/plugin-sdk/awd-plugin-sdk.d.ts and manifest.schema.json first. Use minimum permissions. SDK 1.6 platform features require the unreleased new host; simulator success does not prove installed-client support.
Edit only this plugin. Preserve scripts/ and sdk/plugin-sdk/ layout. Do not modify the SDK to bypass checks. Support --awd-* theme tokens, keyboard interaction, narrow panels and light/dark themes.
Declare views and installation/user/project settings as needed. Credentials must use trusted host connections; never put tokens in the iframe, logs, repository or zip.
Choose shared host AI/speech/tasks/clipboard with the required permissions, or fixed HTTPS BYO operations with network permission. MCP must use public HTTPS Streamable HTTP and explicitly declared tools with the supported schema subset. No stdio, private hosts, arbitrary URLs or system settings.
Use synthetic simulator data to verify success, permission denial, disconnect, retry and cancel. Do not access customer data, real login or paid services. Use only my designated isolated project for host installation; ask the host administrator to grant service trust in settings after manifest changes.
Run node scripts/plugin-dev.mjs validate . and node scripts/plugin-dev.mjs package . --out ../my-plugin.zip. Verify no .env, credentials, private keys or host trust markers. Report changes, checks, unverified boundaries and zip location.
Only submit when I explicitly request it, using a locally provided AWD_PLUGIN_TOKEN without printing it. Report pending review accurately; never claim approval, payment or release.

MCP Streamable HTTP specification

Ready to go

Nén thành file zip và gửi — chúng tôi sẽ xem xét trong thời gian sớm nhất.